vue 简单实现图片管理,上传到oss指定目录,查看与删除图片

84 阅读1分钟

本文章比较长,全程代码,前方高能哈哈。

有对oss上传,或者文件管理的实现感兴趣的耐心看完,大概花费个20分钟。

效果图如下:

image.png

首先左侧菜单实现:这边会用到vue的一个递归调用

menuTree.vue文件:

<template>
  <div class="tree">
    <MenuTreeNode
      v-for="(sub, index) in menuData"
      :key="index"
      :node="sub"
      :activeKey="activeKey || defaultActive"
      @getCurNode="getCurNode"
      @extendNode="extendNode"
      :label="config.label"
      :value="config.value"
      :children="config.children"
    ></MenuTreeNode>
  </div>
</template>

<script>
import MenuTreeNode from './treeNode.vue'
export default {
  components: {
    MenuTreeNode,
  },
  name: 'MenuTree',
  props: {
    // 菜单数据
    menuList: {
      type: Array,
      default: () => {
        return []
      },
    },
    config: {
      type: Object,
      default: () => {},
    },
    // 默认选中的文件夹key
    defaultActive: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      menuData: [],
      activeKey: '', //当前查看的文件夹
    }
  },
  mounted() {
    if (this.menuList[0]) {
      this.activeKey = this.menuList[0].key
    }
  },
  watch: {
    menuList: {
      handler(val) {
        if (val) {
          let arr = [...val]
          this.getIndex(arr, 0)
          this.menuData = arr
        }
      },
      immediate: true,
      deep: true,
    },
  },
  methods: {
    getCurNode(item) {
      this.activeKey = item.key
      this.$emit('getCurNode', item)
    },
    extendNode(item) {
      this.$set(item, 'open', !item.open)
      this.$emit('extendNode', item)
    },
    /** 增加默认索引值 */
    getIndex(arr, a) {
      arr.forEach((v, i) => {
        v.defaultIndex = a
        if (v.children && v.children.length) {
          this.getIndex(v.children, a + 1)
        }
      })

      // return arr
    },
  },
}
</script>

<style lang="less" scoped>
.tree {
  .item {
    height: 48px;
    padding: 0px 10px;
    border-bottom: 1px solid #ccc;
    display: flex;
    justify-content: space-between;
    align-items: center;
    cursor: pointer;
    box-sizing: border-box;
    .item-left {
      height: 100%;
      flex: 1;
      display: flex;
      align-items: center;
      .folder-icon {
        font-size: 20px;
        margin-right: 10px;
        color: #30a3f1;
      }
    }

    .open-icon {
      cursor: pointer;
      height: 100%;
      display: flex;
      align-items: center;
      padding-left: 10px;
    }
  }
}
</style>

treeNode.vue文件(菜单展开与点击逻辑):

<template>
  <div>
    <div
      class="item"
      :style="{
        'padding-left': 10 * (node.defaultIndex + 1) + 'px',
        border: activeKey === node.key ? '2px solid #38c' : '1px solid #ccc',
      }"
    >
      <div class="item-left" @click.stop="getCurNode(node)">
        <i
          class="folder-icon"
          :class="node.open ? 'el-icon-folder-opened' : 'el-icon-folder'"
          size="defalt"
        ></i>
        <span :style="{ width: 180 - 10 * (node.defaultIndex + 1) + 'px' }">{{
          node.defaultIndex === 0
            ? node[label]
            : node[label].replace(node.parentKey, '')
        }}</span>
      </div>
      <div class="open-icon" @click.stop="extendNode(node)">
        <i
          :class="node.open ? 'el-icon-caret-bottom' : 'el-icon-caret-right'"
        ></i>
      </div>
    </div>
    <!-- 递归调用组件本身 -->
    <div v-if="node[children] && node[children].length && node.open">
      <MenuTreeNode
        v-for="(sub, index) in node[children]"
        :key="index"
        :node="sub"
        :activeKey="activeKey"
        @getCurNode="getCurNode"
        @extendNode="extendNode"
        :label="label"
        :value="value"
        :children="children"
      ></MenuTreeNode>
    </div>
  </div>
</template>

<script>
export default {
  name: 'MenuTreeNode',
  props: {
    node: {
      type: Object,
      default: () => {},
    },
    activeKey: {
      type: String,
    },
    value: {
      type: String,
      default: 'value',
    },
    label: {
      type: String,
      default: 'label',
    },
    children: {
      type: String,
      default: 'children',
    },
  },
  data() {
    return {}
  },
  mounted() {},
  methods: {
    getCurNode(item) {
      console.log('31312311111111')
      this.$emit('getCurNode', item)
    },
    extendNode(item) {
      console.log('12312312')
      this.$emit('extendNode', item)
    },
  },
}
</script>

<style lang="less" scoped>
.item {
  background-color: white;
  height: 48px;
  padding: 0px 10px;
  border-bottom: 1px solid #ccc;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  box-sizing: border-box;
  .item-left {
    height: 100%;
    flex: 1;

    display: flex;
    align-items: center;
    span {
      // width: 140px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .folder-icon {
      font-size: 20px;
      margin-right: 10px;
      color: #30a3f1;
    }
  }

  .open-icon {
    cursor: pointer;
    height: 100%;
    display: flex;
    align-items: center;
    padding-left: 10px;
  }
}
</style>

finder.vue文件(主页面代码):

<template>
  <div class="finder-box">
    <div class="header">
      <div class="left-action">
        <me-upload
          @success="uploadSuccess"
          class="file-upload"
          osstype="kysys"
          :show-file-list="false"
          width="100%"
          height="100%"
          :cusSavePath="curNodeData.key"
        >
          <el-button size="small" icon="el-icon-upload">上传</el-button>
        </me-upload>

        <el-button size="small" icon="el-icon-folder-add"
          >创建子文件夹
        </el-button>
        <el-button size="small" icon="el-icon-full-screen">全屏</el-button>
      </div>
      <div class="right-action">
        <el-input placeholder="过滤" />
        <i class="el-icon-s-tools"></i>
      </div>
    </div>
    <div class="container">
      <div class="menu">
        <MenuTree
          :config="{ label: 'name' }"
          :menuList="menuList"
          :defaultActive="defaultActive"
          @getCurNode="getCurNode"
          @extendNode="extendNode"
        ></MenuTree>
      </div>
      <div class="file-content" v-loading="contentLoading">
        <div class="file-box" v-if="picList && picList.length">
          <div
            class="file-item"
            :class="{ 'is-active': curPic.name == item.name }"
            v-for="(item, index) in picList"
            :key="item.name"
            @click="fileClick(item)"
            @contextmenu.prevent="(e) => rightClick(e, item, index)"
          >
            <img :src="item.url" alt="" />
            <div class="file-desc">
              <p class="name" v-if="curNodeData && curNodeData.key">
                {{ item.name.replace(curNodeData.key, '') }}
              </p>
              <p class="detail">
                <span>{{ item.lastModified }}</span>
                <br />
                <span>{{ $utils.getNumfixed(item.size / 1000, 1) }} KB</span>
              </p>
            </div>
            <div class="check-icon">
              <i class="el-icon-check"></i>
            </div>
          </div>
        </div>
        <div v-else class="file-null">
          <div>该文件夹是空的</div>
        </div>
      </div>
    </div>
    // 鼠标右击弹窗内容
    <div v-show="menuVisible">
      <ul id="picAction" class="pic-action">
        <a>
          <li
            class="pic-action-item"
            style="margin-top: 3px"
            @click="preViewPic"
          >
            查看
          </li>
        </a>
        <a>
          <li class="pic-action-item" style="margin-top: 3px" @click="delFile">
            删除
          </li>
        </a>
      </ul>
    </div>
    <me-viewer
      :visible.sync="showViewer"
      :list="imgList"
      :currentIndex="currentIndex"
    />
  </div>
</template>

<script>
import MenuTree from './components/menuTree.vue'
import InitOSS from '@/utils/oss.js'
export default {
  components: {
    MenuTree,
  },
  data() {
    return {
      menuList: [],
      osstype: 'kysys',
      picList: [], // 图片资源
      curNodeData: {},
      curPic: '', //当前选中的图片
      menuVisible: false,
      contentLoading: false,
      fileUrl: '',
      currentIndex: 0,
      imgList: [],
      showViewer: false,
      defaultActive: '',
    }
  },
  async mounted() {
    const res = await this.getFileList('kysys/')
    // const res = await this.getFileList('health_care/app')
    console.log(res, 'sakdbajhsbdjhbjh')
    this.menuList = res.formatArr
    if (this.menuList && this.menuList.length) {
      this.curNodeData = this.menuList[0]
      this.defaultActive = this.curNodeData.key
      this.getCurNode(this.curNodeData)
    }
  },

  methods: {
    /**
     * 获取上传权限
     */
    getOssToken(type) {
      let params = {
        fileTypeName: type,
      }
      return new Promise((reslove, reject) => {
        this.$api.getOSSToken(params).then((res) => {
          let oc = InitOSS(res)
          reslove(oc)
          return oc
        })
      })
    },
    getFileList(dir) {
      return new Promise(async (reslove, reject) => {
        const client = await this.getOssToken(this.osstype)
        try {
          let result = await client.list({
            prefix: dir,
            delimiter: '/',
          })

          const formatArr = result.prefixes
            ? result.prefixes.map((item) => {
                return {
                  name: item,
                  key: item,
                  parentKey: dir,
                }
              })
            : []
          reslove({ ...result, formatArr })
        } catch (e) {
          console.log(e)
        }
      })
    },
    /**获取当前节点信息 */
    async getCurNode(item) {
      this.contentLoading = true
      this.curNodeData = item
      const res = await this.getFileList(item.key)
      this.picList = res.objects || []
      this.contentLoading = false
      // if (res.formatArr && res.formatArr.length) {
      //   this.$set(item, 'children', res.formatArr)
      //   // item.children = res.formatArr
      // }
    },
    /**获取当前节点下文件信息 */
    async extendNode(item) {
      console.log('test:', item)

      // console.log(result, 'resultppppppppppp')
      const res = await this.getFileList(item.key)
      if (res.formatArr && res.formatArr.length) {
        this.$set(item, 'children', res.formatArr)
      }
    },
    fileClick(item) {
      this.curPic = item
    },
    /** 图片右击事件 */
    rightClick(MouseEvent, item, index) {
      this.curPic = { ...item, picIndex: index }
      this.menuVisible = false // 先把模态框关死,目的是 第二次或者第n次右键鼠标的时候 它默认的是true
      this.menuVisible = true // 显示模态窗口,跳出自定义菜单栏
      var menu = document.querySelector('#picAction')
      menu.style.left = MouseEvent.clientX + 10 + 'px'

      document.addEventListener('click', this.removeEvent) // 给整个document添加监听鼠标事件,点击任何位置执行foo方法
      menu.style.top = MouseEvent.clientY + 10 + 'px'
    },

    removeEvent() {
      // 取消鼠标监听事件 菜单栏
      this.menuVisible = false
      document.removeEventListener('click', this.removeEvent) // 要及时关掉监听,不关掉的是一个坑,不信你试试,虽然前台显示的时候没有啥毛病,加一个alert你就知道了
    },

    /**
     * 上传成功
     */
    uploadSuccess(val) {
      this.fileUrl = val[0]
      this.getCurNode(this.curNodeData)
    },
    /**
     * 预览图片
     */
    preViewPic() {
      const arr = []
      this.picList.forEach((item) => {
        arr.push(item.url)
      })
      this.viewImg(arr, this.curNodeData.picIndex)
    },
    /**
     * 删除图片
     */
    async delFile() {
      const client = await this.getOssToken(this.osstype)
      try {
        let result = await client.delete(this.curPic.name)

        this.getCurNode(this.curNodeData)
      } catch (e) {
        console.log(e)
      }
    },
    /* 图片预览 */
    viewImg(url, index) {
      this.currentIndex = index || 0
      this.imgList = url
      this.showViewer = true
    },
  },
}
</script>

<style lang="less" scoped>
.finder-box {
  border: 1px solid #ccc;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 10px;
    border-bottom: 1px solid #ccc;
    .left-action {
      display: flex;
      align-items: center;
      .file-upload {
        margin-right: 10px;
        /deep/.el-upload {
          border: 0;
        }
      }
    }

    .right-action {
      display: flex;
      align-items: center;
      .el-icon-s-tools {
        font-size: 20px;
        padding: 0px 10px;
      }
    }
  }
  .container {
    // flex: 1;
    height: calc(100% - 60px);
    display: flex;
    .menu {
      height: 100%;
      width: 360px;
      overflow-x: hidden;
      overflow-y: auto;
      border-right: 1px solid #ccc;
    }
    .file-content {
      width: 100%;
      height: 100%;
      overflow-y: auto;
      background-color: white;
      .file-box {
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        .file-item {
          margin: 20px 0px 0px 20px;
          width: 180px;
          height: 180px;
          background-color: #f7f8f9;
          position: relative;
          overflow: hidden;
          cursor: pointer;
          .check-icon {
            position: absolute;
            opacity: 0;
          }
          &.is-active {
            border: 2px solid #38c;
            position: relative;
            color: #373a3c;
            text-shadow: none;
            background-color: #fff;
            .check-icon {
              position: absolute;
              bottom: 0;
              right: 0;
              z-index: 10;
              font-size: 20px;
              color: white;
              opacity: 1;
            }

            &::after {
              content: '';
              position: absolute;
              right: 0;
              bottom: 0;
              width: 0;
              height: 4em;
              width: 3em;
              opacity: 1;
              background-color: #0a90eb;
              -webkit-transform: rotate(45deg) translate(33px, 5px);
              -ms-transform: rotate(45deg) translate(33px, 5px);
              transform: rotate(45deg) translate(33px, 5px);
              -webkit-transition: opacity 200ms ease-in-out;
              transition: opacity 200ms ease-in-out;
              z-index: 9;
            }
          }

          img {
            width: 100%;
            height: 80px;
            object-fit: cover;
          }
          .file-desc {
            padding: 20px 10px;

            .name {
              font-size: 13px;
              color: #333;
              overflow: hidden;
              text-overflow: ellipsis;
              white-space: nowrap;
            }
            .detail {
              margin-top: 8px;
              font-size: 13px;
              color: #666;
              overflow: hidden;
              text-overflow: ellipsis;
              white-space: nowrap;
            }
          }
        }
      }
      .file-null {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
      }
    }
  }

  .pic-action {
    // height: 85px;
    padding: 10px 0px;
    width: 90px;
    position: absolute;
    border-radius: 5px;
    border: 1px solid #999999;
    background-color: #f4f4f4;
    z-index: 99999;
    padding-inline-start: 0px;
    .pic-action-item {
      display: block;
      line-height: 20px;
      text-align: center;
      cursor: pointer;
      &:hover {
        background-color: #38c;
        color: white;
      }
    }
  }
}
</style>

oss.js文件:

import OSS from 'ali-oss'
const InitOSS = (res) =>
  new OSS({
    endpoint: res.endpoint,
    accessKeyId: res.accessKeyId,
    accessKeySecret: res.accessKeySecret,
    bucket: res.bucketName,
    stsToken: res.securityToken,
  })
export default InitOSS

代码比较长但比较详细,不懂的可以评论区交流,耐心看完你会有所收获的。

如果觉得该文章对你有帮助,帮忙关注点个赞呗~~