1. 程式人生 > 其它 >SpringBoot2+vue 實現頭像上傳 + 支援jar包部署

SpringBoot2+vue 實現頭像上傳 + 支援jar包部署

技術標籤:後端技術前端技術vue.jsspring bootupload

最近在做springboot +vue 前後端分離專案,之前一直是打war包部署到tomcat ,最近想打jar包部署,畢竟springboot 的優勢是可以打jar包部署(因為jar包裡內建了tomcat ,為以後的微服務及分散式部署打下基礎),但是打jar 包部署會涉及到讀取檔案,之前用專案路徑的方式就不起作用了,都要改為流的方式讀取,之前我有一篇文章專門介紹了在jar裡讀取流的三種方式(classLoader、Thread、classpathResource)。今天我主要介紹頭像上傳,廢話不多說,直接上程式碼:

1.專案路徑
在這裡插入圖片描述
application-dev.yml裡圖片配置路徑

prop:
  upload_folder_headImg: D:/upload_files/headImage/

2.配置一下通過前端vue訪問圖片路徑許可權:




@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  @Value("${prop.upload_folder_headImg}")
  private String UPLOAD_FOLDER_IMG_HEAD;
  @Autowired
  private
RefererInterceptor refererInterceptor; @Bean public RefererInterceptor getRefererInterceptor(){ return new RefererInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(refererInterceptor).addPathPatterns("/**") .
excludePathPatterns("/sys/user/login","/sys/user/logout","/sys/user/tologin","/sys/user/unauth"); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedMethods("*")// 3允許任何方法(post、get等) .allowedOrigins("*")// 1允許任何域名使用 .allowedHeaders("*")// 2允許任何頭 .allowCredentials(true) .maxAge(3600L);// 4.解決跨域請求兩次,預檢請求的有效期,單位為秒 } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**"); // 對映圖片訪問路徑 registry.addResourceHandler("/img/**").addResourceLocations("file:" + UPLOAD_FOLDER_IMG_HEAD); } }

3.圖片上傳FileUploadController

package com.dechnic.omsdc.server.common.controller;

import com.dechnic.omsdc.server.admin.controller.BaseController;
import com.dechnic.omsdc.server.admin.dto.JsonResult;
import com.dechnic.omsdc.server.common.utils.SpringUtil;
import com.dechnic.omsdc.server.common.utils.UUIDUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;


@RestController
@RequestMapping("/upload")
@Slf4j
public class FileUploadController extends BaseController {
  @Value("${prop.upload_folder_headImg}")
  private String uploadFolderHeadImg;
  @Value("${spring.servlet.multipart.max-file-size}")
  private String maxFileSize;

  SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");


  @PostMapping("/headImg")
  public JsonResult uploadHeadImg(
          @RequestParam(name = "file", required = false) MultipartFile file,
          HttpServletRequest request) throws IOException {
    JsonResult ret = null;
    // 對file 進行清潔處理
    if (Objects.isNull(file) || file.isEmpty()) {
      log.info("上傳頭像為空,請重新上傳");
      return JsonResult.error("上傳頭像為空,請重新上傳");
    }
    String oldName = file.getOriginalFilename();
    String suffix = oldName.substring(oldName.lastIndexOf(".")+1);
    if (!"jpg,jpeg,gif,png".toUpperCase().contains(suffix.toUpperCase())){
      log.info("請選擇jpg,jpeg,gif,png格式的圖片");
      return JsonResult.error("請選擇jpg,jpeg,gif,png格式的圖片");
    }
    long fileSize = file.getSize();
    DecimalFormat df = new DecimalFormat("#.00");
    double mVal = (double) fileSize / (1024 * 1024);
    if (mVal>Double.parseDouble(maxFileSize.substring(0,maxFileSize.trim().length()-2))){
      log.info("上傳頭像超出大小限制:"+maxFileSize);
      return JsonResult.error("上傳頭像超出大小限制:"+maxFileSize);
    }
    String format = sdf.format(new Date());
    Path directory = Paths.get(uploadFolderHeadImg + format);
    log.info("directory=========:"+directory);

    InputStream inputStream = file.getInputStream();
    if (!Files.exists(directory)){
      Files.createDirectories(directory);
    }
    String newName = UUIDUtils.getUUID() +"."+suffix;
    log.info("newFileName:======"+newName);

    Files.copy(inputStream,directory.resolve(newName));
    ret = JsonResult.success("頭像上傳成功");
    String saveName = format+"/"+newName;
    ret.put("avatar",saveName);
    return ret;
  }

4.前端Vue程式碼:
BaseSetting.vue

<template>
  <div class="account-settings-info-view">
    <a-row :gutter="16">
      <a-col :md="24" :lg="16">

        <a-form layout="vertical">
          <a-form-item
            label="暱稱"
          >
            <a-input placeholder="給自己起個名字" maxLength="20" v-model="params.nick" />
            <a-input type="hidden" v-model="params.avatar"/>
          </a-form-item>
          <!-- <a-form-item
            label="Bio"
          >
            <a-textarea rows="4" placeholder="You are not alone."/>
          </a-form-item>

          <a-form-item
            label="電子郵件"
            :required="false"
          >
            <a-input placeholder="[email protected]"/>
          </a-form-item>
          <a-form-item
            label="加密方式"
            :required="false"
          >
            <a-select defaultValue="aes-256-cfb">
              <a-select-option value="aes-256-cfb">aes-256-cfb</a-select-option>
              <a-select-option value="aes-128-cfb">aes-128-cfb</a-select-option>
              <a-select-option value="chacha20">chacha20</a-select-option>
            </a-select>
          </a-form-item>
          <a-form-item
            label="連線密碼"
            :required="false"
          >
            <a-input placeholder="h3gSbecd"/>
          </a-form-item>
          <a-form-item
            label="登陸密碼"
            :required="false"
          >
            <a-input placeholder="密碼"/>
          </a-form-item>-->

          <a-form-item>
            <!--<a-button type="primary">提交</a-button>-->
            <a-button style="margin-left: 8px" @click="saveForm">儲存</a-button>
          </a-form-item>
        </a-form>

      </a-col>
      <a-col :md="24" :lg="8" :style="{ minHeight: '180px' }">
        <a-upload
          name="avatar"
          listType="picture-card"
          class="avatar-uploader"
          :showUploadList="false"
          :beforeUpload="beforeUpload"
          :customRequest="function(){}"
          @change="handleChange"
        >
          <div class="ant-upload-preview" >
            <img class="default-img" v-if="imageUrl" :src="getAvatar()" alt="avatar" />
            <div v-else class="mask">
              <a-icon type="plus" />
            </div>
            <!--<div v-else>
              <a-icon :type="loading ? 'loading' : 'plus'" />
              <div class="ant-upload-text">點選上傳</div>
            </div>-->
          </div>
        </a-upload>

        <!--<div class="ant-upload-preview" @click="$refs.modal.edit(1)" >
          <a-icon type="cloud-upload-o" class="upload-icon"/>
          <div class="mask">
            <a-icon type="plus" />
          </div>
          <img :src="option.img"/>
        </div>-->
      </a-col>

    </a-row>
    <!--modal-->
    <avatar-modal ref="AvatarModal" @ok="handleCropperSuccess">
    </avatar-modal>
  </div>
</template>

<script>
import AvatarModal from './AvatarModal'
import { mapActions, mapGetters } from 'vuex'
import { updateNick } from '@/api/user'
import { file2Base64 } from '@/utils/util'
import { uploadHeadImg } from '@api/upload'
import { baseUrl } from '@/config/env'

export default {
  components: {
    AvatarModal
  },
  props: {
    // 圖片格式
    imgFormat: {
      type: Array,
      default: function () {
        return ['image/jpeg', 'image/png', 'image/jpg']
      }
    },
    // 圖片大小
    imgSize: {
      type: Number,
      default: 2
    },
    // 圖片裁切配置
    options: {
      type: Object,
      default: function () {
        return {
          autoCropWidth: 180,
          autoCropHeight: 180
        }
      }
    },
    // 回顯圖片路徑
    value: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      loading: false,
      imageUrl: '',
      params: {
        nick: '',
        avatar: ''
      }
    }
  },
  watch: {
    value: {
      handler (val) {
        this.imageUrl = val || ''
      },
      immediate: true
    }
  },

  methods: {
    ...mapGetters(['nickname', 'avatar']),
    ...mapActions(['refreshUserNickNameAndAvatar']),
    // 從本地選擇檔案
    handleChange (info) {
      const { options } = this
      file2Base64(info.file.originFileObj, (imageUrl) => {
        const target = Object.assign({}, options, {
          img: imageUrl
        })
        this.$refs['AvatarModal'].edit(target)
      })
    },
    // 上傳之前 格式與大小校驗
    beforeUpload (file) {
      const { imgFormat, imgSize } = this
      const isFormat = imgFormat.includes(file.type)
      if (!isFormat) {
        this.$message.error('圖片格式不支援!')
      }
      const isLt2M = file.size / 1024 / 1024 <= imgSize
      if (!isLt2M) {
        this.$message.error('圖片大小限制在' + imgSize + 'MB內!')
      }
      return isFormat && isLt2M
    },
    // 裁剪成功後的File物件
    handleCropperSuccess (data) {
      console.log('File:', data)
      // 進行圖片上傳動作
      // 模擬後端請求 2000 毫秒延遲
      const that = this
      that.loading = true
      const formData = new FormData()
      formData.set('file', data)
      uploadHeadImg(formData).then(res => {
        if (!res.code) {
          // that.$message.success('上傳成功')
          // 將返回的資料回顯
          // that.imageUrl = res.imgUrl
          this.params.avatar = res.avatar
          this.imageUrl = (that.avatar == null || that.avatar === '') ? require('@/assets/user.png') : baseUrl + '/img/' + that.avatar
          this.saveForm()
          // that.$emit('ok')
        } else {
          that.$message.error('上傳失敗:' + res.msg)
        }
      })

      /* new Promise((resolve) => {
        setTimeout(() => resolve(), 2000)
      }).then((res) => {
        that.$message.success('上傳成功')
        // 將返回的資料回顯
        that.imageUrl = res.img
        that.$emit('ok')
      }).catch(() => {
        // Do something
      }).finally(() => {
        that.loading = false
      }) */
    },
    getAvatar () {
      return (this.avatar() == null || this.avatar() === '') ? require('@/assets/user.png') : baseUrl + '/img/' + this.avatar()
    },
    saveForm () {
      const { refreshUserNickNameAndAvatar } = this
      updateNick(this.params).then(res => {
        if (!res.code) {
          refreshUserNickNameAndAvatar(this.params)
          this.$message.success('修改成功!')
        }
      })
    }

  },
  computed: {

  },
  created () {
    this.params.nick = this.nickname()
    this.imageUrl = (this.avatar() == null || this.avatar() === '') ? require('@/assets/user.png') : baseUrl + '/img/' + this.avatar()
    // this.imageUrl = 'http://localhost:8080/omsdc/img/2020/03/03/b3b87ff51dc8460cb9c47d8f3b14edca.jpg'
  }

}
</script>

<style lang="less" scoped>

  .avatar-upload-wrapper {
    height: 200px;
    width: 100%;
  }

  .ant-upload-preview {
    position: relative;
    margin: 0 auto;
    width: 100%;
    max-width: 180px;
    border-radius: 50%;
    box-shadow: 0 0 4px #ccc;

    .upload-icon {
      position: absolute;
      top: 0;
      right: 10px;
      font-size: 1.4rem;
      padding: 0.5rem;
      background: rgba(222, 221, 221, 0.7);
      border-radius: 50%;
      border: 1px solid rgba(0, 0, 0, 0.2);
    }
    .mask {
      opacity: 0;
      position: absolute;
      background: rgba(0,0,0,0.4);
      cursor: pointer;
      transition: opacity 0.4s;

      &:hover {
        opacity: 1;
      }

      i {
        font-size: 2rem;
        position: absolute;
        top: 50%;
        left: 50%;
        margin-left: -1rem;
        margin-top: -1rem;
        color: #d6d6d6;
      }
    }

    img, .mask {
      width: 100%;
      max-width: 180px;
      height: 100%;
      border-radius: 50%;
      overflow: hidden;
    }
  }
</style>

AvatarModal.vue

<template>
  <a-modal
    title="修改頭像"
    :visible="visible"
    :maskClosable="false"
    :confirmLoading="confirmLoading"
    :width="800"
    @cancel="cancelHandel">
    <a-row>
      <a-col :xs="24" :md="12" :style="{height: '350px'}">
        <vue-cropper
          ref="cropper"
          :img="options.img"
          :info="true"
          :autoCrop="options.autoCrop"
          :autoCropWidth="options.autoCropWidth"
          :autoCropHeight="options.autoCropHeight"
          :fixedBox="options.fixedBox"
          @realTime="realTime"
        >
        </vue-cropper>
      </a-col>
      <a-col :xs="24" :md="12" :style="{height: '350px'}">
        <div class="avatar-upload-preview">
          <img :src="previews.url" :style="previews.img"/>
        </div>
      </a-col>
    </a-row>

    <template slot="footer">
      <a-button key="back" @click="cancelHandel">取消</a-button>
      <a-button key="submit" type="primary" :loading="confirmLoading" @click="okHandel">儲存</a-button>
    </template>
  </a-modal>
</template>
<script>
import { VueCropper } from 'vue-cropper'
import { dataURLtoFile, blobToBase64 } from '@/utils/util'

export default {
  components: {
    VueCropper
  },
  data () {
    return {
      visible: false,
      id: null,
      confirmLoading: false,

      options: {
        img: '/avatar2.jpg',
        autoCrop: true,
        autoCropWidth: 200,
        autoCropHeight: 200,
        fixedBox: true
      },
      previews: {}
    }
  },
  methods: {
    edit (record) {
      const { options } = this
      this.visible = true
      this.options = Object.assign({}, options, record)
    },
    close () {
      this.options = {
        img: '/avatar2.jpg',
        autoCrop: true,
        autoCropWidth: 200,
        autoCropHeight: 200,
        fixedBox: true
      }
      this.visible = false
    },
    cancelHandel () {
      this.close()
    },
    okHandel () {
      const that = this
      that.confirmLoading = true
      // 獲取截圖的base64 資料
      this.$refs.cropper.getCropData((data) => {
        // 轉換為File物件
        const file = dataURLtoFile(data, 'cropperHeader.jpg')
        // 將裁剪侯的圖片物件返回給父元件
        that.$emit('ok', file)
        that.cancelHandel()
      })
      /* const vm = this

      vm.confirmLoading = true
      setTimeout(() => {
        vm.confirmLoading = false
        vm.close()
        vm.$message.success('上傳頭像成功')
      }, 2000) */
    },
    // 下載輸入框裡的圖片
    downloadNewImg () {
      // 獲取截圖的blob資料
      this.$refs.cropper.getCropBlob((data) => {
        blobToBase64(data).then(res => {
          //  Utils.downLoadImage(res, '測試的喲')
        })
      })
    },
    // 移動框的事件
    realTime (data) {
      this.previews = data
    }
  }
}
</script>

<style lang="less" scoped>

  .avatar-upload-preview {
    position: absolute;
    top: 50%;
    transform: translate(50%, -50%);
    width: 180px;
    height: 180px;
    border-radius: 50%;
    box-shadow: 0 0 4px #ccc;
    overflow: hidden;

    img {
      width: 100%;
      height: 100%;
    }
  }
</style>

5.補充後臺方法讀取圖片的實現方式:

 /**
     * 讀取圖片
     *
     * @param response
     * @param name
     * @throws Exception
     */
    @GetMapping("/getImg/{name}")
    public void getImage(HttpServletResponse response, String name) throws Exception {
        response.setContentType("image/jpeg;charset=utf-8");
        response.setHeader("Content-Disposition", "inline; filename=girls.png");
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(Files.readAllBytes(Paths.get(UPLOAD_PATH).resolve(name)));
        outputStream.flush();
        outputStream.close();
    }


至此,jar方式的圖片上傳功能完成,喜歡我的朋友請給我一下關注,我會努力給大家奉獻好的作品