feat: 分类树

This commit is contained in:
zhanghao 2026-07-06 20:57:20 +08:00
parent ecd13e2494
commit 36fad9c1ca
4 changed files with 333 additions and 105 deletions

View File

@ -0,0 +1,31 @@
import request from '@/utils/request'
export function addCategory(data) {
return request({
url: '/tts/corpus/category',
method: 'post',
data: data
})
}
export function getCategoryTreeApi() {
return request({
url: '/tts/corpus/category/tree',
method: 'get'
})
}
export function updateCategoryTreeApi(data) {
return request({
url: '/tts/corpus/category',
method: 'put',
data: data
})
}
export function deleteCategory(id) {
return request({
url: `/tts/corpus/category/${id}`,
method: 'delete',
})
}

View File

@ -1,83 +1,283 @@
<template> <template>
<el-card shadow="never" class="category-tree"> <div class="page-container">
<div class="category-tree">
<!-- 头部 -->
<div class="tree-header"> <div class="tree-header">
<span class="stat-label">语料库目录</span> <span class="stat-label">语料库目录</span>
<el-button size="small" type="primary" plain>新建分类</el-button> <el-button size="small" type="primary" plain @click="handleAddRoot">
新建分类
</el-button>
</div>
<!-- -->
<el-tree ref="treeRef" :data="treeData" :props="defaultProps" default-expand-all highlight-current
node-key="id" @node-click="handleNodeClick">
<template #default="{ data }">
<div class="tree-node">
<span class="node-label">{{ data.categoryName }}</span>
<!-- v-if="selectedNode && selectedNode.id === data.id" -->
<span class="node-actions">
<!-- 新增子分类所有节点均可 -->
<el-button size="small" link circle type="primary" @click.stop="handleAddChild(data)"
title="新增子分类">
<el-icon>
<Plus />
</el-icon>
</el-button>
<!-- 修改根节点不可修改 -->
<el-button v-if="data.id !== 'root'" size="small" link circle type="warning"
@click.stop="handleEdit(data)" title="修改分类">
<el-icon>
<Edit />
</el-icon>
</el-button>
<!-- 删除根节点不可删除 -->
<el-button v-if="data.id !== 'root'" size="small" link circle type="danger"
@click.stop="handleDelete(data)" title="删除分类">
<el-icon>
<Delete />
</el-icon>
</el-button>
</span>
</div>
</template>
</el-tree>
</div>
<!-- 弹窗新增 / 修改分类 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" :append-to-body="true" width="400px"
@close="resetDialog">
<el-form ref="ruleFormRef" :model="ruleForm" label-width="auto">
<el-form-item label="分类名称" prop="categoryName"
:rules="[{ required: true, message: '请输入分类名称', trigger: 'blur' }]">
<el-input v-model="ruleForm.categoryName" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmDialog">确定</el-button>
</template>
</el-dialog>
</div> </div>
<el-tree
:data="treeData"
:props="defaultProps"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</el-card>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, reactive, nextTick, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const emit = defineEmits(['select-category']) import { Plus, Edit, Delete } from '@element-plus/icons-vue'
import { addCategory, getCategoryTreeApi, updateCategoryTreeApi, deleteCategory } from '@/api/corpusManger/corpus.js'
// ---------- ----------
const defaultProps = { const defaultProps = {
children: 'children', children: 'children',
label: 'label' label: 'label'
} }
const treeData = ref([ // count
{ const treeData = ref([])
label: '全部语料',
count: '1,245,678',
children: [
{ label: '控制指令', count: '245,678', children: [
{ label: '车窗控制', count: '82,345' },
{ label: '空调控制', count: '51,234' },
{ label: '座椅调节', count: '32,145' }
]},
{ label: '导航交互', count: '183,456' },
{ label: '媒体娱乐', count: '98,765' },
{ label: '电话消息', count: '124,567' },
{ label: '场景对话', count: '156,789', children: [
{ label: '日常对话', count: '78,654' },
{ label: '出行场景', count: '78,135' }
]},
{ label: '系统设置', count: '21,345' }
]
}
])
const handleNodeClick = (data) => { const getCategoryTree = async () => {
emit('select-category', data) const res = await getCategoryTreeApi()
if (res.code === 200) {
treeData.value = res.data
} }
}
//
const selectedNode = ref(null)
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogMode = ref('add') // 'add' | 'edit'
const dialogParent = ref(null) //
const dialogTarget = ref(null) //
const ruleFormRef = ref(null)
const ruleForm = ref({
categoryName: ''
})
// ---------- ----------
//
const handleNodeClick = (data) => {
selectedNode.value = data
}
//
const handleAddRoot = () => {
openDialog('add', null, null)
}
//
const handleAddChild = (parent) => {
openDialog('add', parent, null)
}
//
const handleEdit = (node) => {
openDialog('edit', null, node)
}
//
const handleDelete = (node) => {
ElMessageBox.confirm(`确定要删除分类“${node.categoryName}”及其所有子分类吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteCategory(node.id)
if (res.code === 200) {
selectedNode.value = null
getCategoryTree()
ElMessage.success(res.msg)
}
}).catch(() => { })
}
//
const openDialog = (mode, parent, target) => {
dialogMode.value = mode
dialogParent.value = parent
dialogTarget.value = target
if (mode === 'add') {
dialogTitle.value = parent ? `在“${parent.categoryName}”下新增子分类` : '新增一级分类';
ruleForm.value.categoryName = ''
} else {
dialogTitle.value = `修改分类“${target.categoryName}`
ruleForm.value.categoryName = target.categoryName
}
dialogVisible.value = true
}
//
const confirmDialog = async () => {
const result = await ruleFormRef.value.validate()
if (result) {
if (dialogMode.value === 'add') {
const parent = dialogParent.value
if (parent) {
if (!parent.children) {
parent.children = []
}
const exists = parent.children.some(child => child.categoryName === name)
if (exists) {
ElMessage.warning('该分类已存在,请勿重复添加')
return
}
}
//
const res = await addCategory({
parentId: parent?.id || null,
categoryName: ruleForm.value.categoryName
})
if (res.code === 200) {
ElMessage.success(res.msg)
getCategoryTree()
}
} else {
//
const res = await updateCategoryTreeApi({
id: dialogTarget.value.id,
categoryName: ruleForm.value.categoryName
})
if (res.code === 200) {
ElMessage.success(res.msg)
getCategoryTree()
}
}
dialogVisible.value = false
}
}
//
const resetDialog = () => {
ruleForm.value.categoryName = ''
}
//
const findParent = (root, childId) => {
if (!root.children) return null
for (const child of root.children) {
if (child.id === childId) {
return root
}
if (child.children) {
const found = findParent(child, childId)
if (found) return found
}
}
return null
}
//
const treeRef = ref(null)
onMounted(() => {
getCategoryTree()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.category-tree { .page-container {
height: 100%; height: 100%;
} }
.category-tree {
height: 100%;
padding: 20px;
border-radius: 12px;
background: #fff;
.tree-header { .tree-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
}
.stat-label { .stat-label {
font-size: 13px; font-size: 16px;
color: #64748b; color: #000;
font-weight: 500; font-weight: bold;
} }
:deep(.el-tree-node__content) { }
height: 32px;
.el-tree-node__label { .tree-node {
font-size: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
&::after { width: 100%;
content: attr(data-count);
.node-label {
font-size: 14px;
flex: 0 0 auto;
}
.node-count {
margin-left: 8px; margin-left: 8px;
font-size: 12px; font-size: 12px;
color: #94a3b8; color: #94a3b8;
flex: 0 0 auto;
}
.node-actions {
margin-left: auto;
display: none;
align-items: center;
// gap: 2px;
.el-button {
margin-left: 0;
} }
} }
&:hover {
.node-actions {
display: flex;
}
}
}
} }
</style> </style>

View File

@ -3,17 +3,7 @@
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="list-toolbar"> <div class="list-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<el-input
v-model="searchText"
placeholder="搜索文本内容、语料编号或说话人"
size="default"
style="width: 300px;"
clearable
prefix-icon="Search"
/>
<el-button type="primary" size="default">新建语料</el-button> <el-button type="primary" size="default">新建语料</el-button>
<el-button size="default" plain>批量导入</el-button>
<el-button size="default" plain>导出数据</el-button>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<el-tag <el-tag

View File

@ -35,23 +35,30 @@ import CorpusList from './components/CorpusList.vue'
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
gap: 20px; gap: 20px;
align-items: start; align-items: start;
flex: 1;
} }
.left-panel { .left-panel {
position: sticky; position: sticky;
top: 20px; top: 20px;
height: 100%;
} }
.right-panel { .right-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.detail-wrapper { .detail-wrapper {
min-height: 200px; min-height: 200px;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.main-layout { .main-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.left-panel { .left-panel {
position: static; position: static;
} }