feat(产品): 重构产品属性管理功能

- 新增产品属性类型定义和常量配置
- 将产品属性相关配置提取到单独文件
- 重构产品列表页属性展示方式
- 调整路由配置,合并属性相关页面
- 优化字典管理页面样式和交互
This commit is contained in:
tikkhun 2025-11-29 11:02:45 +08:00
parent ef838123f3
commit 6244438f26
6 changed files with 102 additions and 56 deletions

View File

@ -93,21 +93,11 @@ export default defineConfig({
component: './Product/List', component: './Product/List',
}, },
{ {
name: '品牌', name: '产品属性',
path: '/product/brand', path: '/product/attribute',
component: './Product/Brand', component: './Product/Attribute',
}, },
{
name: '强度',
path: '/product/strength',
component: './Product/Strength',
},
{
name: '口味',
path: '/product/flavors',
component: './Product/Flavors',
},
{ {
name: 'WP商品列表', name: 'WP商品列表',
path: '/product/wp_list', path: '/product/wp_list',

View File

@ -245,7 +245,7 @@ const DictPage: React.FC = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space> <Space size="small">
<Button <Button
size='small' size='small'
type="link" type="link"
@ -313,15 +313,15 @@ const DictPage: React.FC = () => {
<PageContainer> <PageContainer>
<Layout style={{ background: '#fff' }}> <Layout style={{ background: '#fff' }}>
<Sider <Sider
width={300} width={240}
style={{ style={{
background: '#fff', background: '#fff',
padding: '16px', padding: '8px',
borderRight: '1px solid #f0f0f0', borderRight: '1px solid #f0f0f0',
}} }}
> >
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Input.Search <Input.Search size="small" size="small"
placeholder="搜索字典" placeholder="搜索字典"
onSearch={handleSearch} onSearch={handleSearch}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
@ -331,11 +331,11 @@ const DictPage: React.FC = () => {
<Button <Button
type="primary" type="primary"
onClick={() => setIsAddDictModalVisible(true)} onClick={() => setIsAddDictModalVisible(true)}
block size="small"
> >
</Button> </Button>
<Space> <Space size="small">
<Upload <Upload
name="file" name="file"
action="/dict/import" action="/dict/import"
@ -349,9 +349,9 @@ const DictPage: React.FC = () => {
} }
}} }}
> >
<Button icon={<UploadOutlined />}></Button> <Button size="small" icon={<UploadOutlined />}></Button>
</Upload> </Upload>
<Button onClick={handleDownloadDictTemplate}> <Button size="small" onClick={handleDownloadDictTemplate}>
</Button> </Button>
</Space> </Space>
@ -360,6 +360,7 @@ const DictPage: React.FC = () => {
columns={dictColumns} columns={dictColumns}
rowKey="id" rowKey="id"
loading={loadingDicts} loading={loadingDicts}
size="small"
onRow={(record) => ({ onRow={(record) => ({
onClick: () => { onClick: () => {
// 如果点击的是当前已选中的行,则取消选择 // 如果点击的是当前已选中的行,则取消选择
@ -377,13 +378,14 @@ const DictPage: React.FC = () => {
/> />
</Space> </Space>
</Sider> </Sider>
<Content style={{ padding: '16px' }}> <Content style={{ padding: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', gap: '2px' }}> <div style={{ width: '100%', display: 'flex', flexDirection: 'row', gap: '2px' }}>
<Button <Button
type="primary" type="primary"
onClick={handleAddDictItem} onClick={handleAddDictItem}
disabled={!selectedDict} disabled={!selectedDict}
size="small"
> >
</Button> </Button>
@ -402,13 +404,14 @@ const DictPage: React.FC = () => {
} }
}} }}
> >
<Button icon={<UploadOutlined />} disabled={!selectedDict}> <Button size="small" icon={<UploadOutlined />} disabled={!selectedDict}>
</Button> </Button>
</Upload> </Upload>
<Button <Button
onClick={handleDownloadDictItemTemplate} onClick={handleDownloadDictItemTemplate}
disabled={!selectedDict} disabled={!selectedDict}
size="small"
> >
</Button> </Button>
@ -418,6 +421,7 @@ const DictPage: React.FC = () => {
columns={dictItemColumns} columns={dictItemColumns}
rowKey="id" rowKey="id"
loading={loadingDictItems} loading={loadingDictItems}
size="small"
/> />
</Space> </Space>
</Content> </Content>

View File

@ -0,0 +1,2 @@
// 中文注释:限定允许管理的字典名称集合
export const allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']);

View File

@ -6,8 +6,7 @@ import React, { useEffect, useState } from 'react';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
// 中文注释:限定允许管理的字典名称集合 import { allowedDictNames } from './consts';
const allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']);
const AttributePage: React.FC = () => { const AttributePage: React.FC = () => {
// 中文注释:左侧字典列表状态 // 中文注释:左侧字典列表状态

View File

@ -1,5 +1,6 @@
import { import {
productcontrollerCreateproduct, productcontrollerCreateproduct,
productcontrollerCompatsizeall,
productcontrollerDeleteproduct, productcontrollerDeleteproduct,
productcontrollerCompatbrandall, productcontrollerCompatbrandall,
productcontrollerCompatflavorsall, productcontrollerCompatflavorsall,
@ -27,7 +28,8 @@ import {
ProFormTextArea, ProFormTextArea,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button, Popconfirm } from 'antd'; import { App, Button, Popconfirm, Tag } from 'antd';
import { allowedDictNames } from '@/pages/Product/Attribute/consts';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1); const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
// TODO // TODO
@ -67,6 +69,50 @@ const NameCn: React.FC<{
/> />
); );
}; };
const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
const items: { key: string; value: string }[] = [];
// 中文注释:按允许的属性集合收集展示值
if (allowedDictNames.has('brand') && record?.brand?.name) items.push({ key: '品牌', value: record.brand.name });
if (allowedDictNames.has('strength') && record?.strength?.name) items.push({ key: '强度', value: record.strength.name });
if (allowedDictNames.has('flavor') && record?.flavors?.name) items.push({ key: '口味', value: record.flavors.name });
if (allowedDictNames.has('size') && record?.size?.name) items.push({ key: '规格', value: record.size.name });
if (allowedDictNames.has('humidity') && record?.humidity) items.push({ key: '湿度', value: record.humidity });
return (
<div>
{items.length ? items.map((it, idx) => (
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
{it.key}: {it.value}
</Tag>
)) : <span>-</span>}
</div>
);
};
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
const [items, setItems] = React.useState<any[]>([]);
React.useEffect(() => {
(async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: productId });
setItems(data || []);
})();
}, [productId]);
return (
<div>
{items && items.length ? (
items.map((c: any) => (
<Tag key={c.id} color="blue" style={{ marginBottom: 4 }}>
{(c.stock && c.stock.productSku) || `#${c.stockId}`} × {c.quantity}{c.stock ? c.stock.quantity : '-'}
</Tag>
))
) : (
<span>-</span>
)}
</div>
);
};
const List: React.FC = () => { const List: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
// 状态:存储当前选中的行 // 状态:存储当前选中的行
@ -102,32 +148,23 @@ const List: React.FC = () => {
hideInSearch: true, hideInSearch: true,
}, },
{ {
title: '库存', title: '属性',
dataIndex: 'stock', dataIndex: 'attributes',
hideInSearch: true, hideInSearch: true,
render: (_, record) => <AttributesCell record={record} />,
}, },
{
title: '构成',
dataIndex: 'components',
hideInSearch: true,
render: (_, record) => <ComponentsCell productId={(record as any).id} />,
},
{ {
title: '描述', title: '描述',
dataIndex: 'description', dataIndex: 'description',
hideInSearch: true, hideInSearch: true,
}, },
{
title: '品牌',
dataIndex: 'brand.name',
},
{
title: '强度',
dataIndex: 'strength.name',
},
{
title: '口味',
dataIndex: 'flavors.name',
},
{
title: '湿度',
dataIndex: 'humidity',
},
{ {
title: '更新时间', title: '更新时间',
dataIndex: 'updatedAt', dataIndex: 'updatedAt',
@ -163,7 +200,7 @@ const List: React.FC = () => {
} }
}} }}
> >
<Button type="primary" danger> <Button type="link" danger>
</Button> </Button>
</Popconfirm> </Popconfirm>
@ -322,7 +359,8 @@ const CreateForm: React.FC<{
values.brandId ? { id: values.brandId } : null, values.brandId ? { id: values.brandId } : null,
values.strengthId ? { id: values.strengthId } : null, values.strengthId ? { id: values.strengthId } : null,
values.flavorsId ? { id: values.flavorsId } : null, values.flavorsId ? { id: values.flavorsId } : null,
values.humidity ? { dictName: 'humidity', name: values.humidity } : null, values.sizeId ? { id: values.sizeId } : null,
values.humidity ? { id: values.humidityId } : null,
].filter(Boolean), ].filter(Boolean),
}; };
const { success, message: errMsg } = const { success, message: errMsg } =
@ -381,6 +419,17 @@ const CreateForm: React.FC<{
}} }}
rules={[{ required: true, message: '请选择口味' }]} rules={[{ required: true, message: '请选择口味' }]}
/> />
<ProFormSelect
name="sizeId"
width="lg"
label="规格"
placeholder="请选择规格"
request={async () => {
const { data = [] } = await productcontrollerCompatsizeall();
return (data || []).map((item: any) => ({ label: item.name, value: item.id }));
}}
rules={[{ required: false }]}
/>
<ProFormSelect <ProFormSelect
name="humidity" name="humidity"
width="lg" width="lg"
@ -455,9 +504,9 @@ const EditForm: React.FC<{
const [components, setComponents] = useState<{ stockId: number; quantity: number }[]>([]); const [components, setComponents] = useState<{ stockId: number; quantity: number }[]>([]);
const setInitialIds = () => { const setInitialIds = () => {
const brand = brandOptions.find((item) => item.title === record.brand.name); const brand = brandOptions.find((item) => item.title === (record.brand?.name));
const strength = strengthOptions.find((item) => item.title === record.strength.name); const strength = strengthOptions.find((item) => item.title === (record.strength?.name));
const flavor = flavorOptions.find((item) => item.title === record.flavors.name); const flavor = flavorOptions.find((item) => item.title === (record.flavors?.name));
formRef.current?.setFieldsValue({ formRef.current?.setFieldsValue({
brandId: brand?.id, brandId: brand?.id,
strengthId: strength?.id, strengthId: strength?.id,
@ -495,10 +544,10 @@ const EditForm: React.FC<{
name: record.name, name: record.name,
sku: record.sku, sku: record.sku,
description: record.description, description: record.description,
humidity: record.humidity, price: record.price,
price: (record as any).price, promotionPrice: record.promotionPrice,
promotionPrice: (record as any).promotionPrice,
components, components,
attributes: record.attributes || [],
}} }}
onFinish={async (values) => { onFinish={async (values) => {
// 中文注释:组装 attributes若选择了则发送 // 中文注释:组装 attributes若选择了则发送
@ -506,6 +555,7 @@ const EditForm: React.FC<{
values.brandId ? { id: values.brandId } : null, values.brandId ? { id: values.brandId } : null,
values.strengthId ? { id: values.strengthId } : null, values.strengthId ? { id: values.strengthId } : null,
values.flavorsId ? { id: values.flavorsId } : null, values.flavorsId ? { id: values.flavorsId } : null,
values.sizeId ? { id: values.sizeId } : null,
values.humidity ? { dictName: 'humidity', name: values.humidity } : null, values.humidity ? { dictName: 'humidity', name: values.humidity } : null,
].filter(Boolean); ].filter(Boolean);
const updatePayload: any = { const updatePayload: any = {
@ -629,7 +679,7 @@ const EditForm: React.FC<{
formRef.current?.setFieldsValue({ components: items }); formRef.current?.setFieldsValue({ components: items });
}} }}
> >
SKU SKU
</Button> </Button>
</ProForm.Group> </ProForm.Group>
<ProFormList <ProFormList

View File

@ -688,6 +688,7 @@ declare namespace API {
}; };
type Product = { type Product = {
attributes: never[];
/** ID */ /** ID */
id: number; id: number;
/** 产品名称 */ /** 产品名称 */