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 = [ ...@@ -150,6 +150,12 @@ export const constantRouterMap = [
component: () => import('@/views/descriptionGroup/DescriptionGroupManage'), component: () => import('@/views/descriptionGroup/DescriptionGroupManage'),
meta: { title: '描述组管理' } meta: { title: '描述组管理' }
}, },
{
path: '/assetManagement/material-tag',
name: 'assetManagement.material-tag',
component: () => import('@/views/materialTag'),
meta: { title: '标签管理' }
},
] ]
}, },
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
<el-menu-item index="/assetManagement/createDelivery">素材组</el-menu-item> <el-menu-item index="/assetManagement/createDelivery">素材组</el-menu-item>
<el-menu-item index="/assetManagement/title-group">标题组管理</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/description-group">描述组管理</el-menu-item>
<el-menu-item index="/assetManagement/material-tag">标签管理</el-menu-item>
</el-submenu> </el-submenu>
<el-submenu index="3"> <el-submenu index="3">
......
...@@ -2,26 +2,74 @@ ...@@ -2,26 +2,74 @@
<div class="tag-management"> <div class="tag-management">
<el-row :gutter="20"> <el-row :gutter="20">
<!-- 左侧树形结构 --> <!-- 左侧树形结构 -->
<el-col :span="6"> <el-col :span="4">
<el-card class="tree-card"> <el-card class="tree-card">
<div slot="header" class="card-header"> <!-- 搜索框和添加按钮 -->
<span>标签树形结构</span> <div class="search-container">
<el-button type="text" @click="handleAddRoot">添加根标签</el-button> <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>
<div v-loading="treeLoading" class="tree-container">
<el-tree <el-tree
:data="treeData" ref="tagTree"
:data="filteredTreeData"
:props="defaultProps" :props="defaultProps"
@node-click="handleNodeClick" @node-click="handleNodeClick"
default-expand-all 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-card>
</el-col> </el-col>
<!-- 右侧表单和列表 --> <!-- 右侧表单和列表 -->
<el-col :span="18"> <el-col :span="20">
<el-card> <el-card>
<div slot="header" class="card-header"> <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> <el-button type="primary" size="small" @click="handleAdd">新增标签</el-button>
</div> </div>
...@@ -37,12 +85,16 @@ ...@@ -37,12 +85,16 @@
</el-form> </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="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="标签名称"></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="remark" label="标签备注1"></el-table-column>
<el-table-column prop="remark2" label="标签备注2"></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"> <el-table-column label="操作" width="200">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button> <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
...@@ -90,12 +142,17 @@ ...@@ -90,12 +142,17 @@
</template> </template>
<script> <script>
import { getAllMaterialTags, getMaterialTagById, createMaterialTag, updateMaterialTag, deleteMaterialTag } from '@/api/materialTag'
export default { export default {
name: 'TagManagement', name: 'TagManagement',
data() { data() {
return { return {
// 树形数据 // 树形数据
treeData: [], treeData: [],
filteredTreeData: [],
searchTreeKeyword: '',
treeLoading: false,
defaultProps: { defaultProps: {
children: 'children', children: 'children',
label: 'name' label: 'name'
...@@ -108,6 +165,7 @@ export default { ...@@ -108,6 +165,7 @@ export default {
// 表格数据 // 表格数据
tableData: [], tableData: [],
tableLoading: false,
// 分页 // 分页
pagination: { pagination: {
...@@ -124,7 +182,7 @@ export default { ...@@ -124,7 +182,7 @@ export default {
name: '', name: '',
remark: '', remark: '',
remark2: '', remark2: '',
parent_id: null parentId: null
}, },
// 表单验证规则 // 表单验证规则
...@@ -136,50 +194,148 @@ export default { ...@@ -136,50 +194,148 @@ export default {
}, },
// 当前选中的节点 // 当前选中的节点
currentNode: null currentNode: null,
allTags: [],
breadcrumbItems: [],
rootTagName: '素材标签',
isInitialLoad: true,
activeNodeId: null
} }
}, },
created() { created() {
this.fetchData() this.fetchAllTags(false)
this.fetchTreeData() },
watch: {
treeData: {
handler(val) {
this.filteredTreeData = [...val]
},
immediate: true
}
}, },
methods: { 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 { try {
// 这里替换为实际的API调用 const response = await getAllMaterialTags()
const response = await this.axios.get('/api/material-tags', { if (response.status === 200) {
params: { this.allTags = response.result.data || []
page: this.pagination.currentPage, this.buildTreeData()
pageSize: this.pagination.pageSize,
name: this.searchForm.name // 只有在需要更新表格时才执行查询
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 updateTableData() {
} catch (error) { // 如果是初始加载且没有选中节点,则不显示数据
this.$message.error('获取数据失败') 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() { getFilteredTags() {
try { // 如果是初始加载且没有选中节点,则返回空数组
// 这里替换为实际的API调用 if (this.isInitialLoad && !this.currentNode) {
const response = await this.axios.get('/api/material-tags/tree') return []
this.treeData = response.data
} catch (error) {
this.$message.error('获取树形数据失败')
} }
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() { handleSearch() {
this.pagination.currentPage = 1 this.pagination.currentPage = 1
this.fetchData() this.isInitialLoad = false // 搜索时强制显示结果
this.updateTableData()
this.pagination.total = this.getFilteredTags().length
}, },
// 重置搜索 // 重置搜索
...@@ -193,20 +349,101 @@ export default { ...@@ -193,20 +349,101 @@ export default {
// 处理分页大小变化 // 处理分页大小变化
handleSizeChange(val) { handleSizeChange(val) {
this.pagination.pageSize = val this.pagination.pageSize = val
this.fetchData() this.updateTableData()
}, },
// 处理页码变化 // 处理页码变化
handleCurrentChange(val) { handleCurrentChange(val) {
this.pagination.currentPage = val this.pagination.currentPage = val
this.fetchData() this.updateTableData()
}, },
// 处理节点点击 // 处理节点点击
handleNodeClick(data) { handleNodeClick(data) {
// 如果是虚拟根节点,则不设置当前节点
if (data.id === 'root') {
this.currentNode = null
} else {
this.currentNode = data 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 { ...@@ -217,9 +454,12 @@ export default {
name: '', name: '',
remark: '', remark: '',
remark2: '', remark2: '',
parent_id: this.currentNode ? this.currentNode.id : null parentId: this.currentNode ? this.currentNode.id : null
} }
this.dialogVisible = true this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
}, },
// 添加根标签 // 添加根标签
...@@ -230,68 +470,132 @@ export default { ...@@ -230,68 +470,132 @@ export default {
name: '', name: '',
remark: '', remark: '',
remark2: '', remark2: '',
parent_id: null parentId: null
} }
this.dialogVisible = true this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
}, },
// 添加子标签 // 添加子标签
handleAddChild(row) { handleAddChild(row) {
this.dialogTitle = '添加子标签' this.dialogTitle = `添加 ${row.name} 的子标签`
this.form = { this.form = {
id: null, id: null,
name: '', name: '',
remark: '', remark: '',
remark2: '', remark2: '',
parent_id: row.id parentId: row.id
} }
this.dialogVisible = true this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.resetFields()
})
}, },
// 编辑标签 // 编辑标签
handleEdit(row) { handleEdit(row) {
this.dialogTitle = '编辑标签' this.dialogTitle = '编辑标签'
this.form = { ...row } this.form = JSON.parse(JSON.stringify(row))
this.dialogVisible = true this.dialogVisible = true
this.$nextTick(() => {
this.$refs.form && this.$refs.form.clearValidate()
})
}, },
// 删除标签 // 删除标签
handleDelete(row) { 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' type: 'warning'
}).then(async () => { }).then(() => {
try { deleteMaterialTag(row.id).then(response => {
// 这里替换为实际的API调用 if (response.status === 200) {
await this.axios.delete(`/api/material-tags/${row.id}`)
this.$message.success('删除成功') this.$message.success('删除成功')
this.fetchData() this.fetchAllTags()
this.fetchTreeData() } else {
} catch (error) {
this.$message.error('删除失败') 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(() => {}) }).catch(() => {})
}, },
// 提交表单 // 提交表单
submitForm() { submitForm() {
this.$refs.form.validate(async (valid) => { this.$refs.form.validate(valid => {
if (valid) { if (valid) {
try { const isEdit = !!this.form.id
if (this.form.id) { const formData = { ...this.form }
// 编辑
await this.axios.put(`/api/material-tags/${this.form.id}`, this.form) const apiCall = isEdit
} else { ? updateMaterialTag(formData)
// 新增 : createMaterialTag(formData)
await this.axios.post('/api/material-tags', this.form)
}
this.$message.success('保存成功') apiCall.then(response => {
if (response.status === 200) {
this.$message.success(isEdit ? '更新成功' : '创建成功')
this.dialogVisible = false this.dialogVisible = false
this.fetchData() this.fetchAllTags()
this.fetchTreeData() } else {
} catch (error) { this.$message.error(isEdit ? '更新失败' : '创建失败')
this.$message.error('保存失败')
} }
}).catch(error => {
console.error(isEdit ? '更新标签失败:' : '创建标签失败:', error)
this.$message.error((isEdit ? '更新' : '创建') + '标签失败: ' + error.message)
})
} }
}) })
} }
...@@ -304,16 +608,78 @@ export default { ...@@ -304,16 +608,78 @@ export default {
padding: 20px; 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 { .tree-card {
min-height: 400px; min-height: 500px;
} }
.card-header { .search-container {
display: flex; 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; justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.node-actions {
display: flex;
align-items: center; align-items: center;
} }
.delete-icon {
color: #F56C6C;
margin-left: 4px;
}
.delete-icon:hover {
color: #F78989;
}
.search-form { .search-form {
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -326,4 +692,8 @@ export default { ...@@ -326,4 +692,8 @@ export default {
.delete-btn { .delete-btn {
color: #F56C6C; color: #F56C6C;
} }
.delete-btn:hover {
color: #F78989;
}
</style> </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