feat(产品): 重构产品属性管理功能
- 新增产品属性类型定义和常量配置 - 将产品属性相关配置提取到单独文件 - 重构产品列表页属性展示方式 - 调整路由配置,合并属性相关页面 - 优化字典管理页面样式和交互
This commit is contained in:
parent
ef838123f3
commit
6244438f26
16
.umirc.ts
16
.umirc.ts
|
|
@ -93,19 +93,9 @@ 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',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// 中文注释:限定允许管理的字典名称集合
|
||||||
|
export const allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']);
|
||||||
|
|
@ -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 = () => {
|
||||||
// 中文注释:左侧字典列表状态
|
// 中文注释:左侧字典列表状态
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -688,6 +688,7 @@ declare namespace API {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Product = {
|
type Product = {
|
||||||
|
attributes: never[];
|
||||||
/** ID */
|
/** ID */
|
||||||
id: number;
|
id: number;
|
||||||
/** 产品名称 */
|
/** 产品名称 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue