vue+el+oss上传/预览/下载/删除/存数据库(前端操作oss的详细实现)

注:
1. 一般情况不会让前端直接操作oss(增删查),可是我遇到了这样的需求
2. 默认已经开通oss,在oss官网完成了配置
3. 需要使用到element-ui的upload组件

点击跳转oss的官方API
点击跳转element的官方API

需求描述

  1. 前端直接操作oss,类型可以是任何类型。本demo以图片和压缩包为例,图片可以预览、删除;压缩包可以下载、删除。
  2. 在表单填写时,一个表单可能有多个不同类型文件的需要上传;表单涉及新增/查看/编辑功能,复用性要强。
  3. 新增表单的具体需求:表单新增之后oss即可编辑,一旦新增过文件后要么删除已上传文件,要么提交表单,提交不成功根据后端提示重新修改表单的错误重新提交,或者删除放弃提交。
  4. 查看表单的具体需求:查看表单不可操作oss,但是对于图片可以进行预览,压缩包可以进行下载。
  5. 修改表单的具体需求:一旦修改过文件(删除,新增,修改)必须提交表单,提交不成功根据后端提示重新修改表单的错误重新提交,直到成功为止。
  6. F5强刷也要保证上述5条,私密麻生,我太难了,我没做

效果展示

新增

新增

查看

查看

编辑

编辑

图片预览(放大)

图片预览(放大)

新增但不提交的提示

新增提示

删除但不提交的提示

删除提示

修改但不提交的提示

修改提示

前期准备

  1. npm install ali-oss
  2. 需要运维提供accessKeyIdaccessKeySecretbucket
  3. 其中bucket最好区分开发环境和生产环境
  4. oss官方推荐使用STS临时授权访问,我这里没有用,有兴趣的话可以移步OSS临时授权

utils.js(工具类)

import axios from 'axios'
import OSS from 'ali-oss'
import UUID4 from 'uuid/v4'

// 下载oss指定地址
export function downloadURL(url) {
  let link = document.createElement('a');
  link.style.display = 'none';
  link.href = url;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

// 上传oss
export function ossUpload(picForProject, fileBuffer) {
  let bucketName = '';
  // 判断环境 生产环境并且不为测试状态
  process.env.NODE_ENV === 'production' ? bucketName = '填你自己申请的' : bucketName = '填你自己申请的';

  let client = new OSS({
    region: 'oss-cn-beijing',
    secure: true, // https
    accessKeyId: '填你自己申请的',
    accessKeySecret: '填你自己申请的',
    bucket: bucketName
  });

  const fileName = UUID4().toUpperCase().replace(/-/g, '')
  const suffix = fileBuffer.name.substr(fileBuffer.name.indexOf('.'))
  const url = `${picForProject}${fileName}${suffix}`

  async function put() {
    try {
      await client.put(url, fileBuffer)
      // await xxx(url) 在这里也可以直接把地址传给后端
      return { 'code': 200, 'url': url }
    } catch (e) {
      return e.message
    }
  }

  return put()
};

// 获取oss地址
export function showPrivateOss(picName) {
  // 这里和上传oss的client是一样的,应该放在store里面同一管理的,demo这里写在这里了
  let bucketName = '';
  process.env.NODE_ENV === 'production' ? bucketName = '填你自己申请的' : bucketName = '填你自己申请的';

  let client = new OSS({
    secure: true, // https
    accessKeyId: '填你自己申请的',
    accessKeySecret: '填你自己申请的',
    bucket: bucketName,
    endpoint: 'oss-cn-beijing.aliyuncs.com'
  });

  async function getOssAddr() {
    try {
      let signUrl = client.signatureUrl(picName, { expires: 1800 }); // expires单位为秒
      return { 'code': 200, 'url': signUrl }
    } catch (e) {
      return e.message
    }
  }

  return getOssAddr()
};

// 删除oss
export function deletePrivateOss(picName) {
  let bucketName = '';
  process.env.NODE_ENV === 'production' ? bucketName = '填你自己申请的' : bucketName = '填你自己申请的';

  let client = new OSS({
    secure: true, // https
    accessKeyId: '填你自己申请的',
    accessKeySecret: '填你自己申请的',
    bucket: bucketName,
    endpoint: 'oss-cn-beijing.aliyuncs.com'
  });

  async function deleteOssAddr() {
    try {
      return client.delete(picName);
    } catch (e) {
      return e.message
    }
  }

  return deleteOssAddr()
};

oss.vue(oss组件)

<template>
  <div>
    <el-upload
      class="avatar-uploader"
      action="oss地址"
      :show-file-list="false"
      :http-request="uploadToOss"
      :before-upload="beforeAvatarUpload"
      :disabled="disabled"
      :accept="accept"
      :typeName="typeName"
    >
      <div v-loading="loading">
        <template v-if="haveUrl">
          <img :src="imgUrl" class="avatar" />
          <span class="show_icon" v-if="!disabled">
            <i class="el-icon-view" @click.stop="handleView()" v-if="typeName==='图片'"></i>
            <i class="el-icon-download" @click.stop="handleDownload()" v-else></i>
            <i class="el-icon-delete" @click.stop="handleRemove()"></i>
          </span>
          <span class="show_icon" v-else>
            <i class="el-icon-view" @click.stop="handleView()" v-if="typeName==='图片'"></i>
            <i class="el-icon-download" @click.stop="handleDownload()" v-else></i>
          </span>
        </template>
        <i v-else class="el-icon-plus avatar-uploader-icon"></i>
      </div>
    </el-upload>
    <!--查看图片-->
    <el-dialog title="照片详情" :visible.sync="showBigPic" width="720px" append-to-body>
      <img :src="imgUrl" width="100%" />
    </el-dialog>
  </div>
</template>

<script>
// 引用工具类(下载指定地址的oss文件、上传oss、展示oss图片、删除指定地址的oss文件)
import {
  downloadURL,
  ossUpload,
  showPrivateOss,
  deletePrivateOss,
} from "@/assets/scripts/utils";
// 二次封装的element的message组件,代码略
import { errorMsg, successMsg } from "@/components/message";

export default {
  props: {
    //对文件的限制
    limit: {
      type: Object,
      default: function () {
        return {
          size: 5,
          types: ["image/jpeg", "image/png"],
        };
      },
    },
    // 是否有url
    haveUrl: {
      type: Boolean,
      default: false,
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // url的名称
    urlName: {
      type: String,
      default: "",
    },
    // 对应的后端字段名,同一页面可能有多个需要上传oss的
    name: {
      type: String,
      default: "",
    },
    // 上传弹窗,过滤文件类型
    accept: {
      type: String,
      default: "image/*",
    },
    // 上传文件类型(文件大小过大提示语也会用到)
    typeName: {
      type: String,
      default: "图片",
    },
  },
  data() {
    return {
      showBigPic: false, // 预览图片
      imgUrl: null,     // 图片地址
      ossFileName: "", // oss文件名
      loading: false, // 是否上传中
    };
  },
  watch: {
    urlName: {
      handler(newValue, oldValue) {
        if (newValue) {
          if (this.typeName !== "图片" && this.haveUrl) {
            this.imgUrl = require("@/assets/images/uploaded.jpg");
          } else {
            showPrivateOss(newValue).then((res) => {
              if (res && res.code) {
                this.imgUrl = res.url;
              } else {
                errorMsg(res);
              }
            });
          }
        }
      },
      immediate: true,
    },
  },
  methods: {
    handleView() {
      this.showBigPic = true;
    },
    handleDownload() {
      showPrivateOss(this.urlName).then((res) => {
        if (res && res.code) {
          downloadURL(res.url);
        } else {
          errorMsg(res);
        }
      });
    },
    handleRemove() {
      let _this = this;
      deletePrivateOss(this.urlName).then((res) => {
        if (res.res.status === 204) {
          successMsg();
          // ossDone参数:url文件地址, haveUrl是否有地址, name字段名
          this.$emit("callback", null, false, _this.name);
        } else {
          errorMsg(res);
        }
      });
    },
    // 上传oss
    uploadToOss(elUpload) {
      this.loading = true;
      let _this = this;
      ossUpload("back-image/" + this.ossFileName, elUpload.file).then((res) => {
        if (res && res.code) {
          // ossDone参数:url文件地址, haveUrl是否有地址, name字段名
          this.$emit("callback", res.url, true, _this.name);
        } else {
          errorMsg(res);
        }
        this.loading = false;
      });
    },
    // 图片上传前 验证图片大小
    beforeAvatarUpload(file) {
      let isSize = false;
      if (this.limit.sizeType === "kb") {
        isSize = file.size / 1024 < this.limit.size;
      } else {
        isSize = file.size / 1024 / 1024 < this.limit.size;
      }
      if (!isSize) {
        errorMsg(
          `${this.typeName}大小不能超过${this.limit.size}${
            this.limit.sizeType ? this.limit.sizeType : "MB"
          }!`
        );
      }
      return isSize;
    },
  },
};
</script>

<style lang="less" scoped>
.show_icon {
  display: inline-block;
  position: absolute;
  top: 0;
  right: 0;
  width: 108px;
  height: 72px;
  i {
    display: none;
  }
  &:hover {
    background-color: rgba(0, 0, 0, 0.5);
    i {
      display: inline;
      line-height: 72px;
      color: #fff;
      font-size: 20px;
      &:not(:last-child) {
        margin-right: 10px;
      }
    }
  }
}
</style>

addOrEdit.vue(表单组件;新增/查看/编辑为同一个组件)

/*
**这是一个弹窗组件,为了代码简洁,删除了其他表单内容
**父组件传来id,即判定是查看/编辑,需要请求后端详情
**:before-close需要带参数,重要!!
*/
<template>
  <el-dialog
    @open="setData"
    :visible.sync="params.show"
    :before-close="() => cancel(false)"
    :close-on-click-modal="false"
    width="1100px"
    append-to-body
  >
    <div slot="title">
      {{params.title}}
      <span class="title_required">
        <span>*</span>为必填项
      </span>
    </div>
    <el-form
      :model="addForm"
      :inline="true"
      ref="addForm"
      label-position="left"
      label-width="125px"
    >
      // 其他item略
      <el-form-item
        label="营业执照照片"
        prop="businessLicenseUrl"
        :rules="[{required: true,message: '不能为空'}]"
      >
        <oss
          @callback="ossDone"
          :haveUrl="haveUrl_businessLicenseUrl"
          :disabled="isDisabled"
          :urlName="addForm.businessLicenseUrl"
          name="businessLicenseUrl"
        />
      </el-form-item>
      <el-form-item label="附件">
        <oss
          @callback="ossDone"
          :limit="limit"
          :haveUrl="haveUrl_attachment"
          :urlName="addForm.attachment"
          :disabled="isDisabled"
          accept=".zip, .rar"
          name="attachment"
          typeName="压缩包"
        />
      </el-form-item>
    </el-form>
    <span slot="footer">
      <el-button @click="cancel(false)" class="cancelBtn">取 消</el-button>
      <el-button
        @click="submit"
        type="primary"
        class="confirmBtn"
        v-if="this.params.title !=='查看'"
      >确 定</el-button>
    </span>
  </el-dialog>
</template>

<script>
import oss from "@/components/oss";
import { methodPost } from "@/api";  // 新增/修改用【代码略】
import { GET_DETAILS } from "@/api/company"; //详情【代码略】
import { errorMsg, successMsg } from "@/components/message"; // 二次封装的message【代码略】

export default {
  components: { oss },
  data() {
    return {
      // 是否有执照
      haveUrl_businessLicenseUrl: false,
      // 是否有附件
      haveUrl_attachment: false,
      // 编辑时,附件的返回值(作为flag)
      flag_attachment: null,
      // 编辑时,执照的返回值(作为flag)
      flag_businessLicenseUrl: null,
      // 附件的大小
      limit: {
        size: 10
      }
    };
  },
  methods: {
    // 操作oss后的$emit
    ossDone(url, haveUrl, name) {
      this.$set(this.addForm, `${name}`, url);
      this.$set(this, `haveUrl_${name}`, haveUrl);
    },
    // 弹窗初始化
    setData() {
      this.addForm = {};
      this.isDisabled = false;
      if (this.params.title !== "新增") {
        this.haveUrl_businessLicenseUrl = true;
        this.getDetails({ id: this.params.id });
      } else {
        this.haveUrl_attachment = false;
        this.haveUrl_businessLicenseUrl = false;
        this.flag_attachment = null;
        this.flag_businessLicenseUrl = null;
      }
    },
    getDetails(id) {
      GET_DETAILS(id).then((res) => {
        if (res.code === 10000) {
          this.addForm = res.result;
          this.flag_attachment = res.result.attachment;
          this.flag_businessLicenseUrl = res.result.businessLicenseUrl;
          if (res.result.attachment) {
            this.haveUrl_attachment = true;
          } else {
            this.haveUrl_attachment = false;
          }
        }
        if (this.params.title === "查看") {
          this.isDisabled = true;
        }
      });
    },
    submit() {
      // 新增/编辑
      this.$refs.addForm.validate((valid) => {
        if (valid) {
          let url = "/company/";
          if (this.params.data) {
            url += "update";
          } else {
            url += "insert";
          }
          methodPost(url, this.addForm, true).then((res) => {
            if (res.code === 10000) {
              successMsg();
              this.cancel(true);
            }
          });
        } else {
          errorMsg();
        }
      });
    },
    cancel(refresh) {
      if (this.params.title !== "新增") {
        if (!refresh) {
          if (this.flag_attachment !== this.addForm.attachment) {
            errorMsg("修改附件后,请点击‘确定’按钮");
            return;
          }
          if (
            this.flag_businessLicenseUrl !== this.addForm.businessLicenseUrl
          ) {
            errorMsg("修改营业执照后,请点击‘确定’按钮");
            return;
          }
        }
      } else {
        if (this.addForm.businessLicenseUrl || this.addForm.attachment) {
          errorMsg("点击‘确定’提交新增内容,或删除已上传文件");
          return;
        }
      }
      this.$refs.addForm.resetFields();
      this.$emit("callback", refresh); // 通知父组件刷新list页,按实际取舍,父组件代码略
    },
  },
};
</script>
阅读 208

推荐阅读