feat: 左侧菜单栏添加标签管理功能,包括修改、删除和排序

This commit is contained in:
jmal 2025-02-19 16:42:02 +08:00
parent 68357040fb
commit 442bbf922e
2 changed files with 284 additions and 15 deletions

View File

@ -39,6 +39,28 @@ export default {
return qs.stringify(params, { arrayFormat: 'repeat' })
}
})
},
// 修改标签排序
setTagSort: function(data) {
return request({
url: '/tag/sort',
method: 'post',
data
})
},
deleteFileTag: function(params) {
return request({
url: '/deleteTag',
method: 'delete',
params
})
},
updateFileTag: function(params) {
return request({
url: '/updateTag',
method: 'put',
params
})
}
}

View File

@ -1,5 +1,26 @@
<template>
<div :class="{ 'has-logo': showLogo }">
<el-dialog class="dialog-cm" width="500px" title="修改标签" :close-on-click-modal="false" :visible.sync="dialogTagVisible" append-to-body>
<el-form ref="managerForm" size="medium" :model="form" label-position="left" :rules="rules" label-width="95px">
<el-form-item label="标签名称:" prop="name">
<el-input v-model="form.name" placeholder="请输入标签名称"/>
</el-form-item>
<el-form-item label="标签颜色:" prop="color">
<div class="form-tag-color">
<svg-icon class="tag-color-icon" :style="{ color: tagColors.hex }" icon-class="tag2"></svg-icon>
<sketch-picker v-model="tagColors"/>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogTagVisible = false">取 消</el-button>
<el-button size="small" native-type="submit" type="primary" :loading="tagUpdateLoading"
@click.native.prevent="updateTag">保 存
</el-button>
</div>
</el-dialog>
<div class="scrollbar-head">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
@ -22,6 +43,21 @@
</el-menu>
</el-scrollbar>
</div>
<!--标签菜单-->
<e-vue-contextmenu ref="contextShow" @ctx-show="show" @ctx-hide="hide">
<div ref="contextShow" class="tag-menu">
<ul>
<li v-for="(item, index) in tagMenus" :key="item.label" @click="tagMenusOperation(item.operation)">
<label class="menuitem">
<svg-icon :icon-class="item.iconClass" />
<span class="menuitem text">{{ item.label }}</span>
</label>
</li>
</ul>
</div>
</e-vue-contextmenu>
<div v-if="showTag && tagList.length > 0" class="scrollbar-tag" :class="{ collapse: isCollapse }">标签</div>
<div class="scroll-decoration-top"></div>
<el-scrollbar class="tag-list" wrap-class="scrollbar-wrapper">
@ -33,22 +69,25 @@
:content="tag.name"
placement="right"
v-for="tag in tagList"
:key="tag.tagId">
<li class="infinite-list-item collapse">
:key="tag.tagId"
>
<li class="infinite-list-item collapse" @click="tagClick(tag)">
<svg-icon :style="{ color: tag.color, fontSize: '14px' }" icon-class="tag2"></svg-icon>
</li>
</el-tooltip>
</ul>
</div>
<div v-else>
<div v-else class="tag-drop-container">
<ul class="infinite-list">
<li v-for="tag in tagList"
v-if="showTag"
:key="tag.id"
<li v-for="tag in tagList" v-if="showTag" :key="tag.id"
class="infinite-list-item"
@click="tagClick(tag)">
<svg-icon :style="{ color: tag.color, fontSize: '14px' }" icon-class="tag2"></svg-icon>
<span>{{ tag.name }}</span>
@click="tagClick(tag)"
>
<div class="tag-name-icon">
<svg-icon :style="{ color: tag.color, fontSize: '14px' }" icon-class="tag2"></svg-icon>
<div class="tag-name">{{ tag.name }}</div>
</div>
<el-button slot="reference" class="btn-more" size="mini" icon="el-icon-more" @click.stop.prevent="clickTagMore(tag, $event)"></el-button>
</li>
</ul>
</div>
@ -88,6 +127,8 @@
</template>
<script>
import UploadImageInput from '@/components/input/UploadImageInput.vue'
import Sortable from 'sortablejs'
import { mapGetters, mapState } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
@ -97,10 +138,22 @@ import fileConfig from "@/utils/file-config";
import Clipboard from "clipboard";
import tagApi from '@/api/tag'
import AppLink from './Link'
import { Sketch } from 'vue-color'
export default {
components: { SidebarItem, Logo, AppLink },
components: { UploadImageInput, SidebarItem, Logo, AppLink , 'sketch-picker': Sketch },
data() {
const checkTagName = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入标签名称'));
} else if (/[\[\]\/\\"<>?*]/gi.test(value)) {
callback(new Error('标签名称不能包含这些字符:<,>,|,*,?,,/,[,]'));
} else {
callback();
}
};
return {
percentage: 0,
customColors: [
@ -114,7 +167,27 @@ export default {
showCopyBtn: false,
takeUpSpace: 0,
tagList: [],
showTag: false
showTag: false,
tagMenus: [
{ iconClass: 'menu-rename', label: '修改', operation: 'update' },
{ iconClass: 'menu-remove', label: '删除', operation: 'delete' }
],
contextTag: {},
tagMenusHideTime: 0,
dialogTagVisible: false,
form: {
tagId: '',
name: '',
},
tagColors: {
hex: '#f56c6c',
},
rules: {
name: [
{ validator: checkTagName, trigger: 'blur' },
]
},
tagUpdateLoading: false
}
},
computed: {
@ -179,6 +252,14 @@ export default {
},
mounted() {
this.getTagList()
// 阻止默认行为
document.body.ondrop = function (event) {
event.preventDefault();
event.stopPropagation();
};
this.tagDrop()
},
methods: {
getTagList() {
@ -192,6 +273,30 @@ export default {
this.setTagListData(res.data)
})
},
// tag拖拽
tagDrop() {
const tagList = document.querySelector('.tag-drop-container > .infinite-list')
if (!tagList) return
const _this = this
Sortable.create(tagList, {
animation: 150,
ghostClass: 'blue-background-class',
onEnd({ newIndex, oldIndex }) {
const currRow = _this.tagList.splice(oldIndex, 1)[0]
_this.tagList.splice(newIndex, 0, currRow)
let tagIdList = []
_this.tagList.forEach(tag => {
tagIdList.push(tag.id)
})
tagApi.setTagSort(tagIdList).then(()=>{
_this.$message.success('标签顺序已更新');
}).catch(() => {
})
}
})
},
setTagListData(tagList) {
this.tagList = tagList
this.showTag = this.tagList.length > 0
@ -221,6 +326,67 @@ export default {
tagClick(tag) {
this.$router.push(`/tag?tagId=${tag.id}`);
},
clickTagMore(tag, event) {
this.contextTag = tag
event.stopPropagation()
if (new Date().getTime() - this.tagMenusHideTime < 10) {
return
}
const e = {}
e.pageX = event.pageX + 5
e.pageY = event.pageY + 2
e.clientX = event.clientX + 5
e.clientY = event.clientY + 2
this.$refs.contextShow.showMenu(event)
},
tagMenusOperation(operation) {
if (operation === 'update') {
this.$refs.contextShow.hideMenu()
this.form.name = this.contextTag.name
this.form.tagId = this.contextTag.id
this.tagColors.hex = this.contextTag.color
this.dialogTagVisible = true
} else if (operation === 'delete') {
this.$refs.contextShow.hideMenu()
this.$confirm('此操作将永久删除该标签, 是否继续?', `删除标签: ${this.contextTag.name}`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
tagApi.deleteFileTag({ tagId: this.contextTag.id }).then(() => {
this.getTagList()
this.$message({
type: 'success',
message: '删除成功!'
})
})
}).catch(() => {
})
}
},
updateTag() {
this.$refs.managerForm.validate((valid) => {
if (valid) {
this.tagUpdateLoading = true
this.form.color = this.tagColors.hex
tagApi.updateFileTag(this.form).then(() => {
this.dialogTagVisible = false
this.tagUpdateLoading = false
this.$message({
type: 'success',
message: '修改成功'
})
}).catch(() => {
this.tagUpdateLoading = false
})
}
})
},
show() {
},
hide() {
this.tagMenusHideTime = new Date().getTime()
},
progressFormat() {
if (this.takeUpSpace === 0) {
this.takeUpSpace = this.userInfo.takeUpSpace
@ -241,6 +407,44 @@ export default {
}
</script>
<style lang="scss" scoped>
>>> .ctx-menu-container {
border: 0 solid rgba(0, 0, 0, 0);
min-width: unset;
}
.tag-menu ul {
list-style: none;
padding-inline-start: 0;
margin-top: 0;
margin-bottom: 0;
padding: 0 5px;
}
.tag-menu li {
cursor: pointer;
margin: 0;
padding: 0;
min-width: 110px;
}
.tag-menu li:hover {
cursor: pointer;
border-radius: 5px;
background-color: #409eff30;
}
.tag-menu li > .menuitem {
cursor: pointer;
line-height: 28px;
margin-left: 10px;
}
.tag-menu li > .menuitem > .text {
cursor: pointer;
font-weight: normal;
}
.tag-menu li > .menuitem > .svg-icon {
cursor: pointer;
margin-right: 8px !important;
}
.sidebar-container.has-logo {
display: flex;
flex-direction: column;
@ -268,20 +472,52 @@ export default {
.infinite-list-item {
height: 30px;
line-height: 30px;
padding-left: 10px;
padding: 0 5px 0 5px;
font-size: 14px;
color: #606266;
cursor: pointer;
margin: 0 8px 0 8px;
margin: 0 5px 0 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
justify-content: space-between;
align-items: center;
.tag-name-icon {
display: flex;
justify-content: flex-start;
align-items: center;
.tag-name {
width: 90px;
white-space: nowrap; /* 强制单行显示 */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 添加省略号 */
}
}
.btn-more {
display: none;
padding: 2px 5px;
background: #eaeaea;
border: 0;
}
.svg-icon {
margin-right: 5px !important;
margin-right: 12px !important;
}
.el-button:hover {
color: #409EFF;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
&:hover {
background-color: #d9d9d9 !important;
background-color: #eaeaea !important;
border-radius: 4px;
.btn-more {
display: block;
}
}
}
.infinite-list-item.collapse {
@ -377,4 +613,15 @@ export default {
}
}
}
.blue-background-class {
background-color: rgba(64, 158, 255, 0.82);
border-radius: 4px;
}
.form-tag-color {
display: flex;
.svg-icon {
font-size: 36px;
margin-right: 20px;
}
}
</style>