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>
|
<template>
|
||||||
<el-card shadow="never" class="category-tree">
|
<div class="page-container">
|
||||||
<div class="tree-header">
|
<div class="category-tree">
|
||||||
<span class="stat-label">语料库目录</span>
|
<!-- 头部 -->
|
||||||
<el-button size="small" type="primary" plain>+新建分类</el-button>
|
<div class="tree-header">
|
||||||
|
<span class="stat-label">语料库目录</span>
|
||||||
|
<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 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) => {
|
const handleNodeClick = (data) => {
|
||||||
emit('select-category', 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>
|
||||||
|
.page-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
.category-tree {
|
.category-tree {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
padding: 20px;
|
||||||
.tree-header {
|
border-radius: 12px;
|
||||||
display: flex;
|
background: #fff;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
.tree-header {
|
||||||
margin-bottom: 12px;
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
.stat-label {
|
align-items: center;
|
||||||
font-size: 13px;
|
margin-bottom: 12px;
|
||||||
color: #64748b;
|
|
||||||
font-weight: 500;
|
.stat-label {
|
||||||
}
|
font-size: 16px;
|
||||||
:deep(.el-tree-node__content) {
|
color: #000;
|
||||||
height: 32px;
|
font-weight: bold;
|
||||||
.el-tree-node__label {
|
}
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
&::after {
|
|
||||||
content: attr(data-count);
|
|
||||||
margin-left: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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>
|
</style>
|
||||||
@ -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
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<StatCards />
|
<StatCards />
|
||||||
|
|
||||||
<!-- 主体:目录树 + 列表 + 详情 -->
|
<!-- 主体:目录树 + 列表 + 详情 -->
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<CategoryTree @select-category="onSelectCategory" />
|
<CategoryTree @select-category="onSelectCategory" />
|
||||||
</div>
|
</div>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<CorpusList @view-detail="onViewDetail" />
|
<CorpusList @view-detail="onViewDetail" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -22,38 +22,45 @@ import CorpusList from './components/CorpusList.vue'
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-container {
|
.page-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-layout {
|
.main-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
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 {
|
|
||||||
position: static;
|
.left-panel {
|
||||||
}
|
position: static;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user