469 lines
10 KiB
Vue
469 lines
10 KiB
Vue
<template>
|
|
<div class="category-manager">
|
|
<div class="category-list" v-loading="loading">
|
|
<div v-if="filteredCategories.length === 0" class="empty-state"></div>
|
|
|
|
<div v-else class="category-tree">
|
|
<category-node
|
|
v-for="category in filteredCategories"
|
|
:key="category.id"
|
|
:item="category"
|
|
:level="0"
|
|
@edit="handleEdit"
|
|
@add-child="handleAddChild"
|
|
@delete="handleDelete"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<edit-cate
|
|
v-model="dialogVisible"
|
|
:category="currentEdit"
|
|
@saved="fetchCategories"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted, nextTick } from "vue";
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
import {
|
|
Plus,
|
|
Edit,
|
|
Delete,
|
|
Check,
|
|
Close,
|
|
Search,
|
|
Refresh,
|
|
Document,
|
|
Picture,
|
|
ArrowDown,
|
|
ArrowRight,
|
|
} from "@element-plus/icons-vue";
|
|
import { allCategories, deleteCategory } from "@/api/article";
|
|
import EditCate from "@/views/apps/cms/articles/components/edit-cate.vue";
|
|
import CategoryNode from "./components/CategoryNode.vue";
|
|
|
|
// 颜色预设
|
|
|
|
// 响应式数据
|
|
const loading = ref(false);
|
|
const dialogVisible = ref(false);
|
|
const currentEdit = ref(null);
|
|
const searchText = ref("");
|
|
const categories = ref([]);
|
|
|
|
// 计算属性
|
|
const totalCount = computed(() => categories.value.length);
|
|
|
|
const filteredCategories = computed(() => {
|
|
if (!searchText.value) {
|
|
return categories.value;
|
|
}
|
|
|
|
const search = searchText.value.toLowerCase();
|
|
return categories.value.filter(
|
|
(category) =>
|
|
category.label.toLowerCase().includes(search) ||
|
|
(category.remark && category.remark.toLowerCase().includes(search)),
|
|
);
|
|
});
|
|
|
|
// 工具函数
|
|
function buildTree(data) {
|
|
// 1. 标准化数据字段
|
|
const transformed = data.map((item) => ({
|
|
...item,
|
|
label: item.name ?? item.label ?? "",
|
|
remark: item.desc ?? item.remark ?? "",
|
|
parentId: item.parentId ?? item.cid ?? 0,
|
|
children: [],
|
|
expanded: true, // 默认展开所有级
|
|
}));
|
|
|
|
const map = new Map();
|
|
transformed.forEach((item) => map.set(item.id, item));
|
|
|
|
const roots = [];
|
|
transformed.forEach((item) => {
|
|
const parent = map.get(item.parentId);
|
|
if (parent) {
|
|
parent.children.push(item);
|
|
} else {
|
|
// 如果没有父节点,或者是顶级节点 (parentId 为 0)
|
|
roots.push(item);
|
|
}
|
|
});
|
|
|
|
return roots;
|
|
}
|
|
|
|
// 事件处理
|
|
function handleCreate() {
|
|
currentEdit.value = null;
|
|
dialogVisible.value = true;
|
|
}
|
|
|
|
function handleEdit(category) {
|
|
currentEdit.value = category;
|
|
dialogVisible.value = true;
|
|
}
|
|
|
|
function handleAddChild(parent) {
|
|
currentEdit.value = { parentId: parent.id };
|
|
dialogVisible.value = true;
|
|
}
|
|
|
|
function handleRefresh() {
|
|
searchText.value = "";
|
|
fetchCategories();
|
|
}
|
|
|
|
async function handleSearch() {
|
|
await fetchCategories();
|
|
}
|
|
|
|
function clearSearch() {
|
|
searchText.value = "";
|
|
fetchCategories();
|
|
}
|
|
|
|
function toggleExpand(category) {
|
|
category.expanded = !category.expanded;
|
|
}
|
|
|
|
function handleDelete(category) {
|
|
ElMessageBox.confirm(`确定要删除分类\"${category.label}\"吗?`, "提示", {
|
|
confirmButtonText: "确定",
|
|
cancelButtonText: "取消",
|
|
type: "warning",
|
|
}).then(() => {
|
|
deleteCategory(category.id)
|
|
.then(() => {
|
|
ElMessage.success("删除成功");
|
|
fetchCategories();
|
|
})
|
|
.catch((error) => {
|
|
console.error("删除分类失败:", error);
|
|
ElMessage.error("删除失败");
|
|
});
|
|
});
|
|
}
|
|
|
|
// API调用
|
|
async function fetchCategories() {
|
|
try {
|
|
loading.value = true;
|
|
const response = await allCategories({ keyword: searchText.value });
|
|
if (response.code === 200) {
|
|
const categoryList = response.data || [];
|
|
// 这里会递归生成三级、四级等
|
|
categories.value = buildTree(categoryList);
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error("获取失败");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
fetchCategories();
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.category-manager {
|
|
min-height: 100vh;
|
|
|
|
// 页面头部样式
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 24px;
|
|
background: var(--el-bg-color);
|
|
padding: 24px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
.page-title {
|
|
h3 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
p {
|
|
margin: 0;
|
|
color: #86909c;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
.page-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
.el-button {
|
|
border-radius: 6px;
|
|
font-weight: 500;
|
|
.el-icon {
|
|
margin-right: 6px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 搜索栏样式
|
|
.search-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
background: var(--el-bg-color);
|
|
padding: 16px 24px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
.search {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
.el-input {
|
|
border-radius: 6px;
|
|
}
|
|
.search-stats {
|
|
color: #86909c;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
// 分类列表容器
|
|
.category-list {
|
|
background: var(--el-bg-color);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
overflow: hidden;
|
|
|
|
.empty-state {
|
|
padding: 80px 40px;
|
|
.empty-icon {
|
|
color: #c9cdd4;
|
|
}
|
|
.el-button {
|
|
border-radius: 6px;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
// 递归树结构核心样式
|
|
.category-tree {
|
|
.category-item {
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
transition: all 0.2s;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
// 禁用状态
|
|
&.disabled {
|
|
.category-main {
|
|
opacity: 0.6;
|
|
background-color: var(--el-fill-color-lightest);
|
|
}
|
|
}
|
|
|
|
// 层级背景区分 (可选:让子级背景稍微深一点点)
|
|
&.child-item {
|
|
.category-main {
|
|
background-color: rgba(245, 247, 250, 0.2);
|
|
}
|
|
}
|
|
|
|
.category-main {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 14px 24px; // 左右 padding 保持,左侧缩进通过内联 style 控制
|
|
transition: background-color 0.2s;
|
|
position: relative;
|
|
|
|
&:hover {
|
|
background-color: var(--el-fill-color-light) !important;
|
|
}
|
|
|
|
// 展开收起按钮占位
|
|
.expand-btn,
|
|
.expand-spacer {
|
|
width: 28px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.expand-btn {
|
|
.expand-button {
|
|
padding: 4px;
|
|
color: var(--el-text-color-placeholder);
|
|
&:hover {
|
|
color: var(--el-color-primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 分类具体内容
|
|
.category-info {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
min-width: 0;
|
|
|
|
.color-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.category-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
background-color: var(--el-fill-color-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 12px;
|
|
flex-shrink: 0;
|
|
.el-icon {
|
|
font-size: 16px;
|
|
color: var(--el-text-color-regular);
|
|
}
|
|
}
|
|
|
|
.category-text {
|
|
flex: 1;
|
|
min-width: 0;
|
|
.category-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 2px;
|
|
.name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
.status-tag {
|
|
font-size: 10px;
|
|
height: 20px;
|
|
padding: 0 6px;
|
|
}
|
|
}
|
|
.category-desc {
|
|
color: var(--el-text-color-placeholder);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 操作按钮组
|
|
.category-actions {
|
|
flex-shrink: 0;
|
|
margin-left: 16px;
|
|
opacity: 0.4; // 默认低透明度,鼠标悬浮时高亮
|
|
transition: opacity 0.2s;
|
|
|
|
.el-button-group .el-button {
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
margin-left: 4px;
|
|
&.danger:hover {
|
|
color: var(--el-color-danger);
|
|
}
|
|
&:hover {
|
|
color: var(--el-color-primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 鼠标悬停时显示按钮
|
|
&:hover .category-actions {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 子分类容器样式
|
|
.category-children {
|
|
margin-left: 0;
|
|
|
|
.category-node-wrapper {
|
|
margin-top: 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Element Plus 对话框深度样式美化
|
|
:deep(.el-dialog) {
|
|
border-radius: 12px;
|
|
.el-dialog__header {
|
|
padding: 20px 24px;
|
|
margin: 0;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
.el-dialog__title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
.el-dialog__body {
|
|
padding: 24px;
|
|
}
|
|
.el-dialog__footer {
|
|
padding: 16px 24px 24px;
|
|
}
|
|
|
|
.el-form-item__label {
|
|
font-weight: 500;
|
|
}
|
|
.el-input__inner,
|
|
.el-textarea__inner {
|
|
border-radius: 6px;
|
|
}
|
|
}
|
|
|
|
// 移动端适配
|
|
@media (max-width: 768px) {
|
|
.category-manager {
|
|
padding: 12px;
|
|
.page-header {
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
.page-actions {
|
|
width: 100%;
|
|
}
|
|
}
|
|
.search-bar {
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.category-list .category-tree .category-item .category-main {
|
|
padding: 12px !important; // 移动端取消大缩进,改用其他视觉暗示
|
|
.category-actions {
|
|
opacity: 1;
|
|
}
|
|
.category-desc {
|
|
display: none;
|
|
} // 隐藏描述节省空间
|
|
}
|
|
}
|
|
}
|
|
</style>
|