本文章比较长,全程代码,前方高能哈哈。
有对oss上传,或者文件管理的实现感兴趣的耐心看完,大概花费个20分钟。
效果图如下:
首先左侧菜单实现:这边会用到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
代码比较长但比较详细,不懂的可以评论区交流,耐心看完你会有所收获的。
如果觉得该文章对你有帮助,帮忙关注点个赞呗~~