feat: 分类树
This commit is contained in:
parent
ecd13e2494
commit
36fad9c1ca
31
src/api/corpusManger/corpus.js
Normal file
31
src/api/corpusManger/corpus.js
Normal 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',
|
||||
})
|
||||
}
|
||||
@ -1,83 +1,283 @@
|
||||
<template>
|
||||
<el-card shadow="never" class="category-tree">
|
||||
<div class="page-container">
|
||||
<div class="category-tree">
|
||||
<!-- 头部 -->
|
||||
<div class="tree-header">
|
||||
<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>
|
||||
<el-tree
|
||||
:data="treeData"
|
||||
:props="defaultProps"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['select-category'])
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { addCategory, getCategoryTreeApi, updateCategoryTreeApi, deleteCategory } from '@/api/corpusManger/corpus.js'
|
||||
|
||||
// ---------- 数据 ----------
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
}
|
||||
|
||||
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' }
|
||||
]
|
||||
}
|
||||
])
|
||||
// 初始树数据(含count字段用于显示,可选)
|
||||
const treeData = ref([])
|
||||
|
||||
const handleNodeClick = (data) => {
|
||||
emit('select-category', data)
|
||||
const getCategoryTree = async () => {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.category-tree {
|
||||
.page-container {
|
||||
height: 100%;
|
||||
}
|
||||
.tree-header {
|
||||
.category-tree {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
|
||||
.tree-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 32px;
|
||||
.el-tree-node__label {
|
||||
font-size: 14px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&::after {
|
||||
content: attr(data-count);
|
||||
width: 100%;
|
||||
|
||||
.node-label {
|
||||
font-size: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.node-count {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
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>
|
||||
@ -3,17 +3,7 @@
|
||||
<!-- 工具栏 -->
|
||||
<div class="list-toolbar">
|
||||
<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 size="default" plain>批量导入</el-button>
|
||||
<el-button size="default" plain>导出数据</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-tag
|
||||
|
||||
@ -35,23 +35,30 @@ import CorpusList from './components/CorpusList.vue'
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.detail-wrapper {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user