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

597 lines
20 KiB
JavaScript
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.

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;