Files
xj-admin/src/views/label/index.vue
T
wanghao 375011991e refactor(label): 清理标签分类页面未使用代码与废弃函数
删除弹窗状态变量、reactive 导入、废弃函数,提升可读性。
2026-06-26 14:43:54 +08:00

303 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="label-page">
<a-row :gutter="16">
<!-- 左侧分类树 -->
<a-col :span="7">
<a-card title="分类管理" :bordered="false" class="card-section">
<template #extra>
<a-button type="primary" size="small" @click="handleAddCategory()">
<template #icon><PlusOutlined /></template>
新增
</a-button>
</template>
<a-input-search v-model:value="categorySearch" placeholder="搜索分类" style="margin-bottom: 12px" allow-clear />
<a-spin :spinning="categoryLoading">
<div class="tree-wrap">
<a-tree
v-if="filteredTreeData.length > 0"
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:tree-data="filteredTreeData"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
block-node
@select="onSelectCategory"
>
<template #title="{ name, id, status, resourceCount }">
<div class="tree-node-content">
<span class="tree-name">{{ name }}</span>
<span v-if="resourceCount != null" class="tree-count">({{ resourceCount }})</span>
<a-tag v-if="status === 2" color="red" style="margin-left: 4px">冻结</a-tag>
<span class="tree-actions">
<a-button type="link" size="small" @click.stop="handleEditCategory({ id, name })"></a-button>
<a-button type="link" size="small" @click.stop="handleAddTagUnderCategory(id)">+标签</a-button>
<a-button type="link" size="small" danger @click.stop="handleDeleteCategory({ id, name })"></a-button>
</span>
</div>
</template>
</a-tree>
<a-empty v-else description="暂无分类" />
</div>
</a-spin>
</a-card>
</a-col>
<!-- 右侧标签云 + 权限矩阵 -->
<a-col :span="17">
<a-card title="标签管理" :bordered="false" class="card-section">
<template #extra>
<a-button type="primary" size="small" @click="handleAddTag">
<template #icon><PlusOutlined /></template>
新增标签
</a-button>
</template>
<a-input-search v-model:value="tagSearch" placeholder="搜索标签" style="width: 240px; margin-bottom: 12px" allow-clear />
<a-spin :spinning="tagLoading">
<div class="tag-cloud">
<a-tag
v-for="tag in filteredTags"
:key="tag.id"
:color="getTagColor(tag.id)"
class="tag-item-cloud"
closable
@close.prevent="handleDeleteTag(tag)"
>
{{ tag.name }}
<span v-if="tag.resourceCount != null" class="tag-count">{{ tag.resourceCount }}</span>
</a-tag>
<a-empty v-if="filteredTags.length === 0 && !tagLoading" description="暂无标签" />
</div>
</a-spin>
</a-card>
<a-card title="分类权限配置" :bordered="false" class="card-section">
<!-- <a-alert message="权限矩阵接口后端尚未提供,当前为占位展示" type="info" show-icon style="margin-bottom: 12px" />-->
<a-table :columns="permissionColumns" :data-source="permissionList" :pagination="false" size="middle" bordered>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex !== 'role'">
<a-tag :color="getPermissionColor(record.permissions[column.idx])">
{{ getPermissionText(record.permissions[column.idx]) }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<!-- 分类新增/编辑弹窗 -->
<CategoryFormModal @register="registerCategoryModal" @success="loadCategoryTree" />
<!-- 标签新增/编辑弹窗 -->
<TagFormModal @register="registerTagModal" @success="loadCategoryTree" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import { categoryListAllApi, categoryDeleteApi, tagDeleteApi, buildTree } from './label.api';
import CategoryFormModal from './components/CategoryFormModal.vue';
import TagFormModal from './components/TagFormModal.vue';
const { createMessage } = useMessage();
const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
const [registerTagModal, { openModal: openTagModal }] = useModal();
// ============ 分类树 ============
const categoryList = ref<any[]>([]);
const categoryLoading = ref(false);
const categorySearch = ref('');
const expandedKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]);
const categoryTree = computed(() => buildTree(categoryList.value));
const filteredTreeData = computed(() => {
if (!categorySearch.value) return categoryTree.value;
const kw = categorySearch.value.toLowerCase();
const filter = (list: any[]): any[] =>
list
.map((item) => {
const children = item.children ? filter(item.children) : undefined;
if (item.name.toLowerCase().includes(kw) || (children && children.length > 0)) {
return { ...item, children };
}
return null;
})
.filter(Boolean);
return filter(categoryTree.value);
});
function onSelectCategory(keys: string[]) {
// V3:标签数据从分类的 tags 字段取,不再单独调接口
if (keys.length > 0) {
const category = categoryList.value.find((c) => c.id === keys[0]);
tagList.value = category?.tags || [];
} else {
tagList.value = [];
}
}
function handleAddCategory(parentId?: string) {
openCategoryModal(true, { isUpdate: false, parentId });
}
// V3:分类项后面的「+标签」按钮,新增该分类下的标签
function handleAddTagUnderCategory(parentId: string) {
openTagModal(true, { isUpdate: false, parentId });
}
function handleEditCategory(record: any) {
openCategoryModal(true, { isUpdate: true, record });
}
function handleDeleteCategory(record: any) {
categoryDeleteApi({ id: record.id }, () => {
createMessage.success('已删除分类');
loadCategoryTree();
});
}
// ============ 标签 ============
const tagList = ref<any[]>([]);
const tagLoading = ref(false);
const tagSearch = ref('');
const filteredTags = computed(() => {
if (!tagSearch.value) return tagList.value;
return tagList.value.filter((t) => t.name.includes(tagSearch.value));
});
const tagColorPool = ['blue', 'green', 'orange', 'purple', 'red', 'default'];
function getTagColor(id: string) {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
return tagColorPool[Math.abs(hash) % tagColorPool.length];
}
function handleAddTag() {
openTagModal(true, { isUpdate: false });
}
function handleDeleteTag(tag: any) {
tagDeleteApi({ id: tag.id }, () => {
createMessage.success('已删除标签');
loadCategoryTree();
});
}
// ============ 权限矩阵(mock,后端未提供接口) ============
const permissionList = ref<any[]>([
{ role: '系统管理员', permissions: ['rw', 'rw', 'rw', 'rw', 'rw'] },
{ role: '内容编辑', permissions: ['r', 'rw', 'rw', 'r', 'rw'] },
{ role: '审核员', permissions: ['r', 'r', 'r', 'r', 'r'] },
{ role: '普通用户', permissions: ['n', 'n', 'n', 'n', 'n'] },
]);
const permissionColumns = [
{ title: '角色', dataIndex: 'role', align: 'center', width: 120 },
{ title: '心肺复苏', dataIndex: 'p0', align: 'center', idx: 0 },
{ title: '创伤处理', dataIndex: 'p1', align: 'center', idx: 1 },
{ title: '中暑急救', dataIndex: 'p2', align: 'center', idx: 2 },
{ title: '骨折固定', dataIndex: 'p3', align: 'center', idx: 3 },
{ title: '烧烫伤', dataIndex: 'p4', align: 'center', idx: 4 },
];
function getPermissionColor(code: string) {
if (code === 'rw') return 'blue';
if (code === 'r') return 'default';
if (code === 'n') return 'red';
return 'default';
}
function getPermissionText(code: string) {
if (code === 'rw') return '读写';
if (code === 'r') return '只读';
if (code === 'n') return '无权限';
return code;
}
// ============ 加载 ============
async function loadCategoryTree() {
categoryLoading.value = true;
try {
const res = await categoryListAllApi();
categoryList.value = res || [];
// 默认展开第一层
if (expandedKeys.value.length === 0 && categoryTree.value.length > 0) {
expandedKeys.value = [categoryTree.value[0].id];
}
// V3:默认选中第一个分类,标签从分类的 tags 字段取
if (selectedKeys.value.length === 0 && categoryList.value.length > 0) {
const first = categoryList.value[0];
selectedKeys.value = [first.id];
tagList.value = first?.tags || [];
} else if (selectedKeys.value.length > 0) {
// 已选中分类,重新拉数据后刷新该分类的标签(新增/编辑/删除标签后回显最新数据)
const selected = categoryList.value.find((c) => c.id === selectedKeys.value[0]);
tagList.value = selected?.tags || [];
}
} finally {
categoryLoading.value = false;
}
}
onMounted(() => {
loadCategoryTree();
});
</script>
<style lang="less" scoped>
.label-page {
padding: 0;
}
.card-section {
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.tree-wrap {
min-height: 200px;
}
.tree-node-content {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.tree-name {
flex: 1;
}
.tree-count {
font-size: 11px;
color: #999;
margin-left: 4px;
}
.tree-actions {
display: none;
margin-left: auto;
}
:deep(.ant-tree-node-content-wrapper:hover) .tree-actions,
:deep(.ant-tree-treenode-selected) .tree-actions {
display: inline-flex;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 60px;
}
.tag-item-cloud {
font-size: 13px;
padding: 4px 14px;
cursor: default;
}
.tag-count {
opacity: 0.5;
font-size: 11px;
margin-left: 4px;
}
</style>