backend/src/views/apps/cms/articles/category.vue
2026-01-28 23:13:58 +08:00

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>