195 lines
5.7 KiB
TypeScript
195 lines
5.7 KiB
TypeScript
import React, {useEffect, useMemo, useState} from 'react';
|
|
import {Button, Form, Tag, TreeSelect, Typography} from 'antd';
|
|
import type {FormInstance} from 'antd';
|
|
import {NodeExpandOutlined} from '@ant-design/icons';
|
|
import XinTable from '@/components/XinTable';
|
|
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
|
|
import type IProductCategory from '@/domain/iProductCategory.ts';
|
|
import type { IProductCategoryTree } from '@/domain/iProductCategory.ts';
|
|
import { CATEGORY_STATUS_MAP } from '@/domain/iProductCategory.ts';
|
|
import {getCategoryTable, getCategoryTree} from '@/api/product/category.ts';
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
/**
|
|
* 上级分类选择(分类最多二级):
|
|
* - 二级分类禁选(不能作为上级,否则出现三级)
|
|
* - 编辑时禁选自身
|
|
* - 编辑的分类含子分类时禁选所有一级分类(只能保持顶级,否则其子分类会变成三级)
|
|
*/
|
|
const ParentCategorySelect: React.FC<{
|
|
form: FormInstance;
|
|
tree: IProductCategoryTree[];
|
|
value?: number;
|
|
onChange?: (value: number) => void;
|
|
}> = ({ form, tree, value, onChange }) => {
|
|
// id/children 由 XinTable 编辑时 setFieldsValue(record) 写入(非表单项,需传 form 监听)
|
|
const editingId = Form.useWatch('id', form);
|
|
const children = Form.useWatch('children', form);
|
|
const hasChildren = Array.isArray(children) && children.length > 0;
|
|
|
|
const treeData = useMemo(() => {
|
|
const walk = (nodes: IProductCategoryTree[], depth: number): IProductCategoryTree[] =>
|
|
nodes.map((node) => ({
|
|
...node,
|
|
disabled: depth >= 2 || node.id === editingId || (hasChildren && depth >= 1),
|
|
children: node.children?.length ? walk(node.children, depth + 1) : node.children,
|
|
}));
|
|
return [{ id: 0, name: '顶级分类', children: walk(tree, 1) }];
|
|
}, [tree, editingId, hasChildren]);
|
|
|
|
return (
|
|
<TreeSelect
|
|
value={value}
|
|
onChange={onChange}
|
|
treeData={treeData}
|
|
fieldNames={{ label: 'name', value: 'id', children: 'children' }}
|
|
placeholder="默认顶级分类"
|
|
treeDefaultExpandAll
|
|
/>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 递归收集全部节点 id(用于展开整棵树)
|
|
*/
|
|
function collectAllIds(nodes: IProductCategory[]): number[] {
|
|
const ids: number[] = [];
|
|
const walk = (list: IProductCategory[]) => {
|
|
list.forEach((node) => {
|
|
if (node.id !== undefined) {
|
|
ids.push(node.id);
|
|
}
|
|
if (node.children?.length) {
|
|
walk(node.children);
|
|
}
|
|
});
|
|
};
|
|
walk(nodes);
|
|
return ids;
|
|
}
|
|
|
|
/**
|
|
* 商品分类管理(多级分类树表)
|
|
*/
|
|
const ProductCategoryPage: React.FC = () => {
|
|
const [expandedKeys, setExpandedKeys] = useState<number[]>([]);
|
|
const [allIds, setAllIds] = useState<number[]>([]);
|
|
const [categoryTree, setCategoryTree] = useState<IProductCategoryTree[]>([]);
|
|
|
|
useEffect(() => {
|
|
getCategoryTree().then((res) => setCategoryTree(res.data.data ?? []));
|
|
}, []);
|
|
|
|
const columns: XinTableColumn<IProductCategory>[] = [
|
|
{
|
|
title: '分类名称',
|
|
dataIndex: 'name',
|
|
valueType: 'text',
|
|
required: true,
|
|
rules: [{ required: true, message: '请输入分类名称' }],
|
|
},
|
|
{
|
|
title: '上级分类',
|
|
dataIndex: 'parent_id',
|
|
hideInTable: true,
|
|
hideInSearch: true,
|
|
initialValue: 0,
|
|
fieldRender: (form) => <ParentCategorySelect form={form} tree={categoryTree} />,
|
|
},
|
|
{
|
|
title: '排序',
|
|
dataIndex: 'sort',
|
|
valueType: 'digit',
|
|
hideInSearch: true,
|
|
initialValue: 0,
|
|
fieldProps: { min: 0 },
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
valueType: 'radioButton',
|
|
initialValue: 1,
|
|
hideInSearch: true,
|
|
fieldProps: {
|
|
options: [
|
|
{ value: 1, label: '正常' },
|
|
{ value: 0, label: '停用' },
|
|
],
|
|
},
|
|
render: (_, record) => {
|
|
const item = CATEGORY_STATUS_MAP[record.status ?? 1];
|
|
return <Tag color={item?.color}>{item?.text}</Tag>;
|
|
},
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'updated_at',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
},
|
|
];
|
|
|
|
const tableProps: XinTableProps<IProductCategory> = {
|
|
api: '/product/category',
|
|
columns,
|
|
rowKey: 'id',
|
|
accessName: 'product.category',
|
|
// 后端直接返回分类树,不走分页接口
|
|
handleRequest: async () => {
|
|
const res = await getCategoryTable();
|
|
const tree = res.data.data ?? [];
|
|
setAllIds(collectAllIds(tree));
|
|
return { data: tree, total: tree.length };
|
|
},
|
|
pagination: { pageSize: 200 },
|
|
expandable: {
|
|
expandedRowKeys: expandedKeys,
|
|
onExpandedRowsChange: (keys) => setExpandedKeys([...keys] as number[]),
|
|
},
|
|
actionBarRender: (dom) => [
|
|
dom.add,
|
|
<Button
|
|
icon={<NodeExpandOutlined />}
|
|
onClick={() =>
|
|
setExpandedKeys(expandedKeys.length ? [] : allIds)
|
|
}
|
|
>
|
|
{expandedKeys.length ? '全部收起' : '全部展开'}
|
|
</Button>,
|
|
dom.keywordSearch,
|
|
],
|
|
formProps: {
|
|
grid: true,
|
|
colProps: { span: 12 },
|
|
rowProps: { gutter: 20 },
|
|
layout: 'vertical',
|
|
},
|
|
modalProps: { width: 640 },
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className={"mb-5"}>
|
|
<Title level={3}>商品分类</Title>
|
|
<Text type="secondary">
|
|
多级分类(如蔬菜/水果/其他),采购单导出与对账筛选按分类归组;有子分类或挂载商品时不可删除。
|
|
</Text>
|
|
</div>
|
|
<XinTable<IProductCategory> {...tableProps} />
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ProductCategoryPage;
|