Files
xin-procurement/web/pages/product/category.tsx
T

181 lines
4.8 KiB
TypeScript

import React, { useState } from 'react';
import {Button, Image, Tag, Typography} 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 { CATEGORY_STATUS_MAP } from '@/domain/iProductCategory.ts';
import { getCategoryTable } from '@/api/product/category.ts';
const { Title, Text } = Typography;
/**
* 递归收集全部节点 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<IProductCategory[]>([]);
const columns: XinTableColumn<IProductCategory>[] = [
{
title: '分类名称',
dataIndex: 'name',
valueType: 'text',
required: true,
rules: [{ required: true, message: '请输入分类名称' }],
},
{
title: '上级分类',
dataIndex: 'parent_id',
valueType: 'treeSelect',
hideInTable: true,
hideInSearch: true,
initialValue: 0,
fieldProps: {
treeData: [{ id: 0, name: '顶级分类', children: categoryTree }],
fieldNames: { label: 'name', value: 'id', children: 'children' },
placeholder: '默认顶级分类',
treeDefaultExpandAll: true,
},
},
{
title: '排序',
dataIndex: 'sort',
valueType: 'digit',
hideInSearch: true,
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: 'icon',
valueType: 'image',
fieldProps: {
action: '/product/category/upload',
mode: 'single',
maxCount: 1,
changeType: "id"
},
render: (_, record: IProductCategory) => {
const url = record.icon_url
if (!url) return '-';
return (
<Image
src={url}
width={60}
height={60}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
);
},
align: 'center',
hideInSearch: true,
},
{
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 ?? [];
setCategoryTree(tree);
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;