当前位置:   article > 正文

SpringBoot2+vue 实现头像上传 + 支持jar包部署_vue2 头像上传

vue2 头像上传

最近在做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/
  • 1
  • 2

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);
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

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;
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

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="exp@admin.com"/>
          </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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297

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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134

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();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

至此,jar方式的图片上传功能完成,喜欢我的朋友请给我一下关注,我会努力给大家奉献好的作品

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/873406
推荐阅读
相关标签
  

闽ICP备14008679号