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

最近在做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="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>

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方式的图片上传功能完成,喜欢我的朋友请给我一下关注,我会努力给大家奉献好的作品

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
SpringBoot+SpringSecurity+Vue实现动态路由的过程如下: 1. 在后端(SpringBoot)中,首先需要定义一个权限表,用于存储所有的权限信息,包括权限名称、权限标识等。 2. 在前端(Vue)中,需要定义一个路由表,用于存储所有的路由信息,包括路由路径、组件名称等。 3. 后端需要提供一个接口,用于获取当前用户的权限列表。该接口会根据用户的角色查询对应的权限,并返回给前端。 4. 前端在登录成功后,会调用后端接口获取当前用户的权限列表,并将权限列表存储到本地(如localStorage或vuex)中。 5. 前端在路由跳转时,会根据当前用户的权限列表动态生成路由。可以通过遍历权限列表,根据权限标识匹配路由表中的路由信息,将匹配到的路由添加到路由表中。 6. 前端在生成路由后,需要使用Vue Router的addRoutes方法将动态生成的路由添加到路由表中。 7. 前端在路由跳转时,会根据用户的权限判断是否有权限访问该路由。可以通过导航守卫的beforeEach方法,在路由跳转前进行权限判断。 8. 后端可以使用Spring Security的注解对接口进行权限控制。可以通过在接口上添加注解,指定需要的权限才能访问该接口。 9. 后端在接口调用时,可以通过从redis中获取当前用户的权限列表,并进行权限判断。 10. 前端和后端通过接口交互,实现动态路由的权限控制。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值