Files
hms/apps/web/src/pages/health/ArticleCategoryManage.tsx
iven ba93e6585c fix(web): 文章编辑修复 + ESLint 合规
- ArticleEditor: tag_ids 过滤 null/undefined 值,修复 422 错误
- ArticleCategoryManage: 删除分类传递 version 字段,修复 enforce_version 校验
- articles API: ArticleCategory 接口增加 version 字段
- usePaginatedData: ref 赋值移入 useEffect,修复 react-hooks/refs 规则
- ArticleCategoryManage/ArticleManageList: 函数用 useCallback 包裹,修复 exhaustive-deps
2026-05-26 01:13:59 +08:00

272 lines
7.7 KiB
TypeScript
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 { useEffect, useState, useCallback, useMemo } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Select,
Popconfirm,
message,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import {
articleCategoryApi,
type ArticleCategory,
type CreateCategoryReq,
type UpdateCategoryReq,
} from '../../api/health/articles';
import { handleApiError } from '../../api/client';
import { useThemeMode } from '../../hooks/useThemeMode';
import { AuthButton } from '../../components/AuthButton';
export default function ArticleCategoryManage() {
const [categories, setCategories] = useState<ArticleCategory[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editingCategory, setEditingCategory] = useState<ArticleCategory | null>(null);
const [form] = Form.useForm();
const isDark = useThemeMode();
const fetchCategories = useCallback(async () => {
setLoading(true);
try {
const result = await articleCategoryApi.list();
setCategories(result);
} catch {
message.error('加载分类列表失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchCategories();
}, [fetchCategories]);
const openCreateModal = () => {
setEditingCategory(null);
form.resetFields();
form.setFieldsValue({ sort_order: 0 });
setModalOpen(true);
};
const openEditModal = useCallback((record: ArticleCategory) => {
setEditingCategory(record);
form.setFieldsValue({
name: record.name,
slug: record.slug,
parent_id: record.parent_id,
sort_order: record.sort_order,
description: record.description,
});
setModalOpen(true);
}, [form]);
const closeModal = () => {
setModalOpen(false);
setEditingCategory(null);
form.resetFields();
};
const handleSubmit = async (values: {
name: string;
slug?: string;
parent_id?: string;
sort_order?: number;
description?: string;
}) => {
try {
if (editingCategory) {
const req: UpdateCategoryReq = {
name: values.name,
slug: values.slug,
parent_id: values.parent_id,
sort_order: values.sort_order,
description: values.description,
};
await articleCategoryApi.update(editingCategory.id, req);
message.success('分类更新成功');
} else {
const req: CreateCategoryReq = {
name: values.name,
slug: values.slug,
parent_id: values.parent_id,
sort_order: values.sort_order,
description: values.description,
};
await articleCategoryApi.create(req);
message.success('分类创建成功');
}
closeModal();
fetchCategories();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
};
const handleDelete = useCallback(async (record: ArticleCategory) => {
try {
await articleCategoryApi.delete(record.id, record.version ?? 0);
message.success('分类已删除');
fetchCategories();
} catch {
message.error('删除失败,可能该分类下还有文章');
}
}, [fetchCategories]);
// 构建父分类选项(排除自身)
const parentOptions = categories
.filter((c) => !editingCategory || c.id !== editingCategory.id)
.map((c) => ({ label: c.name, value: c.id }));
const columns = useMemo(() => [
{
title: '分类名称',
dataIndex: 'name',
key: 'name',
render: (name: string) => (
<span style={{ fontWeight: 500 }}>{name}</span>
),
},
{
title: '别名 (Slug)',
dataIndex: 'slug',
key: 'slug',
width: 180,
render: (v?: string) => v || <span style={{ color: isDark ? '#475569' : '#cbd5e1' }}>-</span>,
},
{
title: '父分类',
dataIndex: 'parent_name',
key: 'parent_name',
width: 140,
render: (_v: string | undefined, record: ArticleCategory) => {
if (!record.parent_id) return <span style={{ color: isDark ? '#475569' : '#cbd5e1' }}>-</span>;
const parent = categories.find((c) => c.id === record.parent_id);
return parent?.name || record.parent_id;
},
},
{
title: '排序',
dataIndex: 'sort_order',
key: 'sort_order',
width: 80,
render: (v: number) => v ?? 0,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
render: (v?: string) => v || <span style={{ color: isDark ? '#475569' : '#cbd5e1' }}>-</span>,
},
{
title: '操作',
key: 'actions',
width: 120,
render: (_: unknown, record: ArticleCategory) => (
<AuthButton code="health.articles.manage">
<Space size={4}>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Popconfirm
title="确定删除此分类?"
description="删除后不可恢复,关联文章将变为未分类"
onConfirm={() => handleDelete(record)}
>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</Space>
</AuthButton>
),
},
], [isDark, categories, openEditModal, handleDelete]);
return (
<div>
{/* 页面标题和工具栏 */}
<div className="erp-page-header">
<div>
<h4></h4>
<div className="erp-page-subtitle"></div>
</div>
<AuthButton code="health.articles.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
</AuthButton>
</div>
{/* 表格容器 */}
<div
style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
}}
>
<Table
columns={columns}
dataSource={categories}
rowKey="id"
loading={loading}
pagination={false}
/>
</div>
{/* 新建/编辑分类弹窗 */}
<Modal
title={editingCategory ? '编辑分类' : '新建分类'}
open={modalOpen}
onCancel={closeModal}
onOk={() => form.submit()}
width={520}
>
<Form
form={form}
onFinish={handleSubmit}
layout="vertical"
style={{ marginTop: 16 }}
>
<Form.Item
name="name"
label="分类名称"
rules={[{ required: true, message: '请输入分类名称' }]}
>
<Input placeholder="请输入分类名称" maxLength={50} />
</Form.Item>
<Form.Item name="slug" label="别名 (Slug)">
<Input placeholder="例如: health-tips留空则自动生成" />
</Form.Item>
<Form.Item name="parent_id" label="父分类">
<Select
placeholder="选择父分类(可选)"
allowClear
options={parentOptions}
/>
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} placeholder="0" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={2} placeholder="请输入分类描述" />
</Form.Item>
</Form>
</Modal>
</div>
);
}