filesend/backend/app/uploads/09c5d2e2190d412a9c5405cf9dfc7ba0_CategoryManagement.js

597 lines
20 KiB
JavaScript
Raw Normal View History

2025-10-10 17:25:29 +08:00
import React, { useState, useEffect } from 'react';
import axios from '../../services/axios';
import { adminAPI } from '../../services/api';
import { toast } from 'react-toastify';
import '../../styles/admin/CategoryManagement.css';
const CategoryManagement = () => {
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showTemplateModal, setShowTemplateModal] = useState(false);
const [currentCategory, setCurrentCategory] = useState(null);
const [categoryName, setCategoryName] = useState('');
const [categoryDescription, setCategoryDescription] = useState('');
const [parentCategory, setParentCategory] = useState(''); // 用于创建和编辑时的父分类选择
const [templates, setTemplates] = useState([]);
const [selectedTemplate, setSelectedTemplate] = useState('');
const [expandedCategories, setExpandedCategories] = useState({}); // 用于跟踪哪些分类被展开
// 切换分类展开/折叠状态
const toggleCategory = (categoryId) => {
setExpandedCategories(prev => {
const newState = { ...prev };
// 如果当前是折叠状态,则展开;如果是展开状态,则折叠
newState[categoryId] = !prev[categoryId];
return newState;
});
};
// 辅助函数:渲染分类列表(递归)
const renderCategoryList = (categoryList, isRoot = true) => {
if (!categoryList || !Array.isArray(categoryList)) {
console.error('renderCategoryList: categoryList is not an array', categoryList);
return null;
}
// 如果不是根级列表,并且父分类没有被展开,则不渲染
if (!isRoot && categoryList[0]?.parent_id && !expandedCategories[categoryList[0]?.parent_id]) {
return null;
}
return categoryList.map(category => {
const hasChildren = category.children && Array.isArray(category.children) && category.children.length > 0;
const isExpanded = expandedCategories[category.id];
return (
<React.Fragment key={category.id}>
<tr className={`category-row ${category.level > 1 ? 'child-category' : 'parent-category'}`}>
<td>{category.id}</td>
<td>
<div className="d-flex align-items-center">
{/* 缩进显示 */}
<div style={{ width: `${(category.level - 1) * 20}px` }}></div>
{/* 展开/折叠图标 */}
<span
className="category-toggle me-2"
onClick={() => toggleCategory(category.id)}
style={{ cursor: 'pointer' }}
>
{hasChildren ? (
isExpanded ? (
<span className="chevron-down">&#9660;</span>
) : (
<span className="chevron-right">&#9654;</span>
)
) : (
<span className="category-no-children">&#9679;</span>
)}
</span>
{/* 分类名称 */}
<span className={hasChildren ? 'fw-bold' : ''}>
{category.name}
</span>
{/* 父级标识 - 移除显示父级ID通过缩进和层次结构已经清晰表达 */}
</div>
</td>
<td>{category.description}</td>
<td>
<button
className="btn btn-sm btn-primary me-2"
onClick={() => openEditModal(category)}
>
编辑
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => deleteCategory(category.id)}
>
删除
</button>
</td>
</tr>
{/* 子分类,只有当父分类被展开时才渲染 */}
{hasChildren && isExpanded && renderCategoryList(category.children, false)}
</React.Fragment>
);
});
};
// 获取分类列表
const fetchCategories = async () => {
try {
setLoading(true);
// 添加hierarchical=true参数获取层次化的分类数据
const res = await adminAPI.getCategories({hierarchical: true});
// 检查返回的数据结构
console.log('API返回的分类数据:', res.data);
// 确保categories是一个数组
let categoriesData = [];
if (res.data && res.data.categories) {
// 如果API返回的是 {categories: [...]} 格式
categoriesData = res.data.categories || [];
} else if (Array.isArray(res.data)) {
// 如果API直接返回数组
categoriesData = res.data;
} else {
console.error('API返回的分类数据格式不正确:', res.data);
categoriesData = [];
}
// 递归处理分类数据,确保每个分类都有必要的字段
const processCategories = (categories) => {
return categories.map(cat => ({
...cat,
id: cat.id || cat._id || cat.category_id || '',
name: cat.name || cat.category_name || '未命名分类',
level: cat.level || 1,
parent_id: cat.parent_id || null,
children: cat.children ? processCategories(cat.children) : []
}));
};
const formattedCategories = processCategories(categoriesData);
// 初始化展开状态 - 默认只展开顶级分类
const initialExpandedState = {};
formattedCategories.forEach(cat => {
// 只有顶级分类parent_id为null或undefined的分类默认展开
if (!cat.parent_id) {
initialExpandedState[cat.id] = true;
}
});
setExpandedCategories(initialExpandedState);
setCategories(formattedCategories);
} catch (err) {
toast.error('获取分类列表失败');
console.error(err);
setCategories([]);
} finally {
setLoading(false);
}
};
// 获取分类模板列表
const fetchTemplates = async () => {
try {
const res = await axios.get('/api/admin/category-templates');
// 确保templates是一个数组
let templatesData = [];
if (res.data && res.data.templates) {
// 如果API返回的是 {templates: [...]} 格式
templatesData = res.data.templates || [];
} else if (Array.isArray(res.data)) {
// 如果API直接返回数组
templatesData = res.data;
} else {
console.error('API返回的模板数据格式不正确:', res.data);
templatesData = [];
}
// 确保每个模板对象都有id和name属性
const formattedTemplates = templatesData.map(template => {
return {
...template,
id: template.id || template._id || template.template_id || '',
name: template.name || template.template_name || '未命名模板'
};
});
setTemplates(formattedTemplates);
} catch (err) {
toast.error('获取分类模板列表失败');
console.error(err);
setTemplates([]);
}
};
// 应用分类模板
const applyTemplate = async () => {
if (!selectedTemplate) {
toast.error('请选择一个模板');
return;
}
try {
await axios.post(`/api/admin/category-templates/${selectedTemplate}/apply`);
toast.success('分类模板应用成功');
setShowTemplateModal(false);
setSelectedTemplate('');
fetchCategories();
} catch (err) {
toast.error('应用分类模板失败');
console.error(err);
}
};
useEffect(() => {
fetchCategories();
fetchTemplates();
}, []);
// 检查是否存在重复的分类名称(在同一父级下)
const checkDuplicateCategoryName = (name, parentId, excludeId = null) => {
// 扁平化分类列表,便于检查
const flattenCategories = (cats) => {
let result = [];
cats.forEach(cat => {
result.push(cat);
if (cat.children && cat.children.length > 0) {
result = [...result, ...flattenCategories(cat.children)];
}
});
return result;
};
const allCategories = flattenCategories(categories);
// 检查是否存在同名分类(在相同父级下,排除自身)
return allCategories.some(cat =>
cat.name.toLowerCase() === name.toLowerCase() &&
cat.parent_id === parentId &&
cat.id !== excludeId
);
};
// 创建分类
const createCategory = async () => {
try {
// 检查分类名称是否为空
if (!categoryName.trim()) {
toast.error('分类名称不能为空');
return;
}
// 检查是否存在同名分类(在相同父级下)
const isCategoryNameExists = checkDuplicateCategoryName(categoryName.trim(), parentCategory);
if (isCategoryNameExists) {
toast.error('该父级下已存在同名分类,请使用其他名称');
return;
}
await adminAPI.createCategory({
name: categoryName,
description: categoryDescription,
parent_id: parentCategory || null // 如果没有选择父分类则为null
});
toast.success('分类创建成功');
setShowCreateModal(false);
setCategoryName('');
setCategoryDescription('');
setParentCategory('');
fetchCategories();
} catch (err) {
toast.error('创建分类失败');
console.error(err);
}
};
// 更新分类
const updateCategory = async () => {
if (!currentCategory) return;
try {
// 检查分类名称是否为空
if (!categoryName.trim()) {
toast.error('分类名称不能为空');
return;
}
// 检查是否存在同名分类(在相同父级下,排除自身)
const isCategoryNameExists = checkDuplicateCategoryName(
categoryName.trim(),
currentCategory.parent_id,
currentCategory.id
);
if (isCategoryNameExists) {
toast.error('该父级下已存在同名分类,请使用其他名称');
return;
}
await adminAPI.updateCategory(currentCategory.id, {
name: categoryName,
description: categoryDescription
// parent_id 不允许直接修改,因为会影响路径,由后端处理
});
toast.success('分类更新成功');
setShowEditModal(false);
fetchCategories();
} catch (err) {
toast.error('更新分类失败');
console.error(err);
}
};
// 删除分类
const deleteCategory = async (categoryId) => {
if (window.confirm('确定要删除这个分类吗?')) {
try {
await adminAPI.deleteCategory(categoryId);
toast.success('分类已删除');
fetchCategories();
} catch (err) {
toast.error('删除分类失败');
console.error(err);
}
}
};
// 打开创建分类模态框
const openCreateModal = () => {
setCategoryName('');
setCategoryDescription('');
setParentCategory('');
setShowCreateModal(true);
};
// 打开模板模态框
const openTemplateModal = () => {
setSelectedTemplate('');
setShowTemplateModal(true);
};
// 打开编辑分类模态框
const openEditModal = (category) => {
setCurrentCategory(category);
setCategoryName(category.name);
setCategoryDescription(category.description);
setParentCategory(category.parent_id || ''); // 设置当前父分类
setShowEditModal(true);
};
// 辅助函数:生成父分类选项(扁平化列表)
const getParentOptions = (cats, level = 0) => {
let options = [];
cats.forEach(cat => {
options.push({
id: cat.id,
name: '— '.repeat(level) + cat.name
});
if (cat.children && cat.children.length > 0) {
options = options.concat(getParentOptions(cat.children, level + 1));
}
});
return options;
};
if (loading) {
return (
<div className="d-flex justify-content-center align-items-center vh-100">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
return (
<div className="category-management-container">
<div className="d-flex justify-content-between align-items-center mb-4">
<h1 className="category-title">分类管理</h1>
<div className="category-actions">
{/*<button */}
{/* className="btn btn-success me-2"*/}
{/* onClick={openTemplateModal}*/}
{/*>*/}
{/* 应用模板*/}
{/*</button>*/}
<button className="btn-admin-primary" onClick={openCreateModal}>
<i className="bi bi-plus-circle me-2"></i>
</button>
</div>
</div>
<div className="card">
<div className="card-body">
<div className="table-responsive">
<table className="table table-hover">
<thead className="category-table-header">
<tr>
<th>ID</th>
<th>名称</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody className="category-table-body">
{categories && categories.length > 0 ? (
renderCategoryList(categories)
) : (
<tr>
<td colSpan="4" className="text-center">暂无分类数据</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
{/* 创建分类模态框 */}
{showCreateModal && (
<div className="modal-overlay" onClick={() => setShowCreateModal(false)}>
<div className="admin-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h5 className="modal-title">创建分类</h5>
<button className="modal-close" onClick={() => setShowCreateModal(false)}>×</button>
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label">分类名称</label>
<input
type="text"
className="form-control"
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label">描述</label>
<textarea
className="form-control"
rows="3"
value={categoryDescription}
onChange={(e) => setCategoryDescription(e.target.value)}
></textarea>
</div>
<div className="mb-3">
<label className="form-label">父分类 (可选)</label>
<select
className="form-select"
value={parentCategory}
onChange={(e) => setParentCategory(e.target.value)}
>
<option value=""></option>
{getParentOptions(categories).map(cat => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
</div>
</div>
<div className="modal-footer">
<button className="btn-admin-outline-secondary" onClick={() => setShowCreateModal(false)}>
取消
</button>
<button className="btn-admin-outline-primary" onClick={createCategory}>
创建
</button>
</div>
</div>
</div>
)}
{/* 编辑分类模态框 */}
{showEditModal && (
<div className="modal show" style={{ display: 'block' }}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">编辑分类</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowEditModal(false)}
></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label">分类名称</label>
<input
type="text"
className="form-control"
value={categoryName}
onChange={(e) => setCategoryName(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label">描述</label>
<textarea
className="form-control"
rows="3"
value={categoryDescription}
onChange={(e) => setCategoryDescription(e.target.value)}
></textarea>
</div>
{/* 父分类不允许直接修改,因为会影响路径,由后端处理 */}
{currentCategory && currentCategory.parent_id && (
<div className="mb-3">
<label className="form-label">父分类</label>
<input
type="text"
className="form-control"
value={getParentOptions(categories).find(cat => cat.id === currentCategory.parent_id)?.name || '无'}
disabled
/>
</div>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowEditModal(false)}
>
取消
</button>
<button
type="button"
className="btn btn-primary"
onClick={updateCategory}
>
更新
</button>
</div>
</div>
</div>
</div>
)}
{/* 分类模板模态框 */}
{showTemplateModal && (
<div className="modal show" style={{ display: 'block' }}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">应用分类模板</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowTemplateModal(false)}
></button>
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label">选择模板</label>
<select
className="form-select"
value={selectedTemplate}
onChange={(e) => setSelectedTemplate(e.target.value)}
>
<option value="">请选择模板</option>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.name}
</option>
))}
</select>
</div>
<div className="alert alert-info">
<small>应用模板将创建预设的分类结构不会影响现有分类</small>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowTemplateModal(false)}
>
取消
</button>
<button
type="button"
className="btn btn-success"
onClick={applyTemplate}
>
应用模板
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default CategoryManagement;