Commit 2b9434c0 authored by lijin's avatar lijin

增加标签管理功能

parent 9bafed4e
import axios from 'axios'
// 获取所有标签
export function getAllMaterialTags() {
return axios.get(process.env.PUTIN_API + '/material_tags')
.then(response => {
return response.data
})
}
// 获取单个标签
export function getMaterialTagById(id) {
return axios.get(`${process.env.PUTIN_API}/material_tags/${id}`)
.then(response => {
return response.data
})
}
// 创建标签
export function createMaterialTag(data) {
return axios.post(`${process.env.PUTIN_API}/material_tags`, data)
.then(response => {
return response.data
})
}
// 更新标签
export function updateMaterialTag(data) {
return axios.put(`${process.env.PUTIN_API}/material_tags`, data)
.then(response => {
return response.data
})
}
// 删除标签
export function deleteMaterialTag(id) {
return axios.delete(`${process.env.PUTIN_API}/material_tags/${id}`)
.then(response => {
return response.data
})
}
......@@ -150,6 +150,12 @@ export const constantRouterMap = [
component: () => import('@/views/descriptionGroup/DescriptionGroupManage'),
meta: { title: '描述组管理' }
},
{
path: '/assetManagement/material-tag',
name: 'assetManagement.material-tag',
component: () => import('@/views/materialTag'),
meta: { title: '标签管理' }
},
]
},
......
......@@ -32,6 +32,7 @@
<el-menu-item index="/assetManagement/createDelivery">素材组</el-menu-item>
<el-menu-item index="/assetManagement/title-group">标题组管理</el-menu-item>
<el-menu-item index="/assetManagement/description-group">描述组管理</el-menu-item>
<el-menu-item index="/assetManagement/material-tag">标签管理</el-menu-item>
</el-submenu>
<el-submenu index="3">
......
......@@ -2,26 +2,74 @@
<div class="tag-management">
<el-row :gutter="20">
<!-- 左侧树形结构 -->
<el-col :span="6">
<el-col :span="4">
<el-card class="tree-card">
<div slot="header" class="card-header">
<span>标签树形结构</span>
<el-button type="text" @click="handleAddRoot">添加根标签</el-button>
<!-- 搜索框和添加按钮 -->
<div class="search-container">
<el-input
placeholder="搜索标签"
v-model="searchTreeKeyword"
prefix-icon="el-icon-search"
clearable
@input="filterTree"
class="search-input"
></el-input>
<el-button
type="primary"
icon="el-icon-plus"
circle
size="mini"
@click="handleAddRoot"
class="add-button"
></el-button>
</div>
<div v-loading="treeLoading" class="tree-container">
<el-tree
:data="treeData"
ref="tagTree"
:data="filteredTreeData"
:props="defaultProps"
@node-click="handleNodeClick"
default-expand-all
></el-tree>
node-key="id"
:highlight-current="true"
:expand-on-click-node="false"
>
<span class="custom-tree-node" slot-scope="{ node, data }" @mouseenter="handleMouseEnter(data)" @mouseleave="handleMouseLeave">
<span>{{ node.label }}</span>
<span v-show="activeNodeId === data.id" class="node-actions">
<el-button
type="text"
size="mini"
icon="el-icon-plus"
@click.stop="handleAddChild(data)"
></el-button>
<el-button
type="text"
size="mini"
icon="el-icon-delete"
class="delete-icon"
@click.stop="handleDeleteWithChildren(data)"
></el-button>
</span>
</span>
</el-tree>
</div>
</el-card>
</el-col>
<!-- 右侧表单和列表 -->
<el-col :span="18">
<el-col :span="20">
<el-card>
<div slot="header" class="card-header">
<span>标签列表</span>
<div class="breadcrumb-container">
<el-breadcrumb separator="/">
<el-breadcrumb-item>素材标签</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="index">
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-button type="primary" size="small" @click="handleAdd">新增标签</el-button>
</div>
......@@ -37,12 +85,16 @@
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" border style="width: 100%">
<el-table :data="tableData" border style="width: 100%" v-loading="tableLoading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="标签名称"></el-table-column>
<el-table-column prop="remark" label="标签备注1"></el-table-column>
<el-table-column prop="remark2" label="标签备注2"></el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180"></el-table-column>
<el-table-column label="创建时间" width="180">
<template slot-scope="scope">
{{ formatDate(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
......@@ -90,12 +142,17 @@
</template>
<script>
import { getAllMaterialTags, getMaterialTagById, createMaterialTag, updateMaterialTag, deleteMaterialTag } from '@/api/materialTag'
export default {
name: 'TagManagement',
data() {
return {
// 树形数据
treeData: [],
filteredTreeData: [],
searchTreeKeyword: '',
treeLoading: false,
defaultProps: {
children: 'children',
label: 'name'
......@@ -108,6 +165,7 @@ export default {
// 表格数据
tableData: [],
tableLoading: false,
// 分页
pagination: {
......@@ -124,7 +182,7 @@ export default {
name: '',
remark: '',
remark2: '',
parent_id: null
parentId: null
},
// 表单验证规则
......@@ -136,50 +194,148 @@ export default {
},
// 当前选中的节点
currentNode: null
currentNode: null,
allTags: [],
breadcrumbItems: [],
rootTagName: '素材标签',
isInitialLoad: true,
activeNodeId: null
}
},
created() {
this.fetchData()
this.fetchTreeData()
this.fetchAllTags(false)
},
watch: {
treeData: {
handler(val) {
this.filteredTreeData = [...val]
},
immediate: true
}
},
methods: {
// 获取表格数据
async fetchData() {
// 格式化日期
formatDate(date) {
if (!date) return ''
const d = new Date(date)
return d.toLocaleString()
},
// 获取所有标签数据
async fetchAllTags(updateTable = true) {
this.treeLoading = true
this.tableLoading = true
try {
// 这里替换为实际的API调用
const response = await this.axios.get('/api/material-tags', {
params: {
page: this.pagination.currentPage,
pageSize: this.pagination.pageSize,
name: this.searchForm.name
const response = await getAllMaterialTags()
if (response.status === 200) {
this.allTags = response.result.data || []
this.buildTreeData()
// 只有在需要更新表格时才执行查询
if (updateTable) {
this.updateTableData()
this.pagination.total = this.getFilteredTags().length
} else {
// 初始加载时不显示数据
this.tableData = []
this.pagination.total = 0
}
} else {
this.$message.error('获取标签数据失败')
}
} catch (error) {
console.error('获取标签数据失败:', error)
this.$message.error('获取标签数据失败: ' + error.message)
} finally {
this.treeLoading = false
this.tableLoading = false
this.isInitialLoad = false
}
},
// 构建树形数据
buildTreeData() {
// 创建一个虚拟根节点,所有标签都作为它的子节点
const rootTag = {
id: 'root',
name: this.rootTagName,
children: []
}
// 找出所有根节点(parentId为null或0的节点)
const rootNodes = this.allTags.filter(tag => !tag.parentId)
rootTag.children = this.buildTree(rootNodes)
this.treeData = [rootTag]
this.filteredTreeData = [...this.treeData]
},
// 递归构建树
buildTree(nodes) {
return nodes.map(node => {
// 找出当前节点的所有子节点
const children = this.allTags.filter(tag => tag.parentId === node.id)
const newNode = { ...node }
if (children.length > 0) {
newNode.children = this.buildTree(children)
}
return newNode
})
},
this.tableData = response.data.list
this.pagination.total = response.data.total
} catch (error) {
this.$message.error('获取数据失败')
// 更新表格数据
updateTableData() {
// 如果是初始加载且没有选中节点,则不显示数据
if (this.isInitialLoad && !this.currentNode) {
this.tableData = []
return
}
const filteredTags = this.getFilteredTags()
const start = (this.pagination.currentPage - 1) * this.pagination.pageSize
const end = start + this.pagination.pageSize
this.tableData = filteredTags.slice(start, end)
},
// 获取树形数据
async fetchTreeData() {
try {
// 这里替换为实际的API调用
const response = await this.axios.get('/api/material-tags/tree')
this.treeData = response.data
} catch (error) {
this.$message.error('获取树形数据失败')
// 获取筛选后的标签
getFilteredTags() {
// 如果是初始加载且没有选中节点,则返回空数组
if (this.isInitialLoad && !this.currentNode) {
return []
}
let filteredTags = this.allTags
// 如果选中了节点,只显示其子标签
if (this.currentNode) {
filteredTags = this.allTags.filter(tag => tag.parentId === this.currentNode.id)
} else {
// 如果没有选中节点,显示所有一级标签
filteredTags = this.allTags.filter(tag => !tag.parentId)
}
// 如果有搜索条件,进行名称筛选
if (this.searchForm.name) {
filteredTags = filteredTags.filter(tag =>
tag.name.toLowerCase().includes(this.searchForm.name.toLowerCase())
)
}
return filteredTags
},
// 处理搜索
handleSearch() {
this.pagination.currentPage = 1
this.fetchData()
this.isInitialLoad = false // 搜索时强制显示结果
this.updateTableData()
this.pagination.total = this.getFilteredTags().length
},
// 重置搜索
......@@ -193,20 +349,101 @@ export default {
// 处理分页大小变化
handleSizeChange(val) {
this.pagination.pageSize = val
this.fetchData()
this.updateTableData()
},
// 处理页码变化
handleCurrentChange(val) {
this.pagination.currentPage = val
this.fetchData()
this.updateTableData()
},
// 处理节点点击
handleNodeClick(data) {
// 如果是虚拟根节点,则不设置当前节点
if (data.id === 'root') {
this.currentNode = null
} else {
this.currentNode = data
this.searchForm.name = ''
this.fetchData()
}
this.pagination.currentPage = 1
this.updateTableData()
this.pagination.total = this.getFilteredTags().length
this.isInitialLoad = false
// 更新面包屑
this.updateBreadcrumb(data)
},
// 鼠标移入节点
handleMouseEnter(data) {
this.activeNodeId = data.id
},
// 鼠标移出节点
handleMouseLeave() {
this.activeNodeId = null
},
// 更新面包屑
updateBreadcrumb(node) {
if (!node) {
this.breadcrumbItems = []
return
}
// 构建面包屑路径
const breadcrumbPath = []
let currentNode = node
// 递归查找父节点直到根节点
while (currentNode) {
breadcrumbPath.unshift({ id: currentNode.id, name: currentNode.name })
// 如果没有父ID,则结束
if (!currentNode.parentId) {
break
}
// 查找父节点
currentNode = this.allTags.find(tag => tag.id === currentNode.parentId)
}
this.breadcrumbItems = breadcrumbPath
},
// 过滤树
filterTree() {
if (!this.searchTreeKeyword) {
this.filteredTreeData = [...this.treeData]
return
}
const keyword = this.searchTreeKeyword.toLowerCase()
// 递归搜索树节点
const filterNode = (nodes, keyword) => {
return nodes.filter(node => {
// 当前节点是否匹配
const isMatch = node.name.toLowerCase().includes(keyword)
// 如果有子节点,递归过滤子节点
if (node.children && node.children.length) {
const filteredChildren = filterNode(node.children, keyword)
node.children = filteredChildren
// 如果子节点有匹配的,当前节点也应该保留
return isMatch || filteredChildren.length > 0
}
return isMatch
})
}
// 深拷贝原始树数据,避免修改原始数据
const clonedData = JSON.parse(JSON.stringify(this.treeData))
this.filteredTreeData = filterNode(clonedData, keyword)
},
// 新增标签
......@@ -217,9 +454,12 @@ export default {
name: '',
remark: '',
remark2: '',
parent_id: this.currentNode ? this.currentNode.id : null
parentId: this.currentNode ? this.currentNode.id : null
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
},
// 添加根标签
......@@ -230,68 +470,132 @@ export default {
name: '',
remark: '',
remark2: '',
parent_id: null
parentId: null
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
},
// 添加子标签
handleAddChild(row) {
this.dialogTitle = '添加子标签'
this.dialogTitle = `添加 ${row.name} 的子标签`
this.form = {
id: null,
name: '',
remark: '',
remark2: '',
parent_id: row.id
parentId: row.id
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
},
// 编辑标签
handleEdit(row) {
this.dialogTitle = '编辑标签'
this.form = { ...row }
this.form = JSON.parse(JSON.stringify(row))
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
})
},
// 删除标签
handleDelete(row) {
this.$confirm('确认删除该标签吗?', '提示', {
// 检查是否有子标签
const hasChildren = this.allTags.some(tag => tag.parentId === row.id)
if (hasChildren) {
this.$message.warning('该标签下有子标签,请先删除子标签')
return
}
this.$confirm('此操作将永久删除该标签, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
// 这里替换为实际的API调用
await this.axios.delete(`/api/material-tags/${row.id}`)
}).then(() => {
deleteMaterialTag(row.id).then(response => {
if (response.status === 200) {
this.$message.success('删除成功')
this.fetchData()
this.fetchTreeData()
} catch (error) {
this.fetchAllTags()
} else {
this.$message.error('删除失败')
}
}).catch(error => {
console.error('删除标签失败:', error)
this.$message.error('删除标签失败: ' + error.message)
})
}).catch(() => {})
},
// 删除标签及其所有子标签
handleDeleteWithChildren(data) {
// 如果是虚拟根节点,不允许删除
if (data.id === 'root') {
this.$message.warning('不能删除根标签')
return
}
this.$confirm(`此操作将永久删除 ${data.name} 及其所有子标签, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 递归获取所有子标签 ID
const getChildrenIds = (parentId) => {
const children = this.allTags.filter(tag => tag.parentId === parentId)
let ids = []
children.forEach(child => {
ids.push(child.id)
ids = ids.concat(getChildrenIds(child.id))
})
return ids
}
// 获取所有需要删除的标签 ID
const idsToDelete = [data.id, ...getChildrenIds(data.id)]
// 使用 Promise.all 并行删除所有标签
Promise.all(idsToDelete.map(id => deleteMaterialTag(id)))
.then(() => {
this.$message.success(`删除 ${idsToDelete.length} 个标签成功`)
this.fetchAllTags()
})
.catch(error => {
console.error('删除标签失败:', error)
this.$message.error('删除标签失败: ' + error.message)
})
}).catch(() => {})
},
// 提交表单
submitForm() {
this.$refs.form.validate(async (valid) => {
this.$refs.form.validate(valid => {
if (valid) {
try {
if (this.form.id) {
// 编辑
await this.axios.put(`/api/material-tags/${this.form.id}`, this.form)
} else {
// 新增
await this.axios.post('/api/material-tags', this.form)
}
const isEdit = !!this.form.id
const formData = { ...this.form }
const apiCall = isEdit
? updateMaterialTag(formData)
: createMaterialTag(formData)
this.$message.success('保存成功')
apiCall.then(response => {
if (response.status === 200) {
this.$message.success(isEdit ? '更新成功' : '创建成功')
this.dialogVisible = false
this.fetchData()
this.fetchTreeData()
} catch (error) {
this.$message.error('保存失败')
this.fetchAllTags()
} else {
this.$message.error(isEdit ? '更新失败' : '创建失败')
}
}).catch(error => {
console.error(isEdit ? '更新标签失败:' : '创建标签失败:', error)
this.$message.error((isEdit ? '更新' : '创建') + '标签失败: ' + error.message)
})
}
})
}
......@@ -304,16 +608,78 @@ export default {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.breadcrumb-container {
display: flex;
align-items: center;
}
.el-breadcrumb {
font-size: 16px;
font-weight: bold;
}
.el-breadcrumb__item {
color: #409EFF;
}
.el-breadcrumb__item:last-child {
color: #303133;
font-weight: bold;
}
.tree-card {
min-height: 400px;
min-height: 500px;
}
.card-header {
.search-container {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 0 5px;
}
.search-input {
flex: 1;
}
.add-button {
margin-left: 8px;
}
.tree-container {
max-height: 600px;
overflow-y: auto;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.node-actions {
display: flex;
align-items: center;
}
.delete-icon {
color: #F56C6C;
margin-left: 4px;
}
.delete-icon:hover {
color: #F78989;
}
.search-form {
margin-bottom: 20px;
}
......@@ -326,4 +692,8 @@ export default {
.delete-btn {
color: #F56C6C;
}
.delete-btn:hover {
color: #F78989;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment