feat: 添加产品工具, 重构产品 #31
18
.umirc.ts
18
.umirc.ts
|
|
@ -93,21 +93,11 @@ export default defineConfig({
|
|||
component: './Product/List',
|
||||
},
|
||||
{
|
||||
name: '品牌',
|
||||
path: '/product/brand',
|
||||
component: './Product/Brand',
|
||||
name: '产品属性',
|
||||
path: '/product/attribute',
|
||||
component: './Product/Attribute',
|
||||
},
|
||||
{
|
||||
name: '强度',
|
||||
path: '/product/strength',
|
||||
component: './Product/Strength',
|
||||
},
|
||||
{
|
||||
name: '口味',
|
||||
path: '/product/flavors',
|
||||
component: './Product/Flavors',
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
name: 'WP商品列表',
|
||||
path: '/product/wp_list',
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ const DictPage: React.FC = () => {
|
|||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Space size="small">
|
||||
<Button
|
||||
size='small'
|
||||
type="link"
|
||||
|
|
@ -313,15 +313,15 @@ const DictPage: React.FC = () => {
|
|||
<PageContainer>
|
||||
<Layout style={{ background: '#fff' }}>
|
||||
<Sider
|
||||
width={300}
|
||||
width={240}
|
||||
style={{
|
||||
background: '#fff',
|
||||
padding: '16px',
|
||||
padding: '8px',
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Input.Search
|
||||
<Input.Search size="small" size="small"
|
||||
placeholder="搜索字典"
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
|
|
@ -331,11 +331,11 @@ const DictPage: React.FC = () => {
|
|||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsAddDictModalVisible(true)}
|
||||
block
|
||||
>
|
||||
size="small"
|
||||
>
|
||||
添加字典
|
||||
</Button>
|
||||
<Space>
|
||||
<Space size="small">
|
||||
<Upload
|
||||
name="file"
|
||||
action="/dict/import"
|
||||
|
|
@ -349,9 +349,9 @@ const DictPage: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>导入字典</Button>
|
||||
<Button size="small" icon={<UploadOutlined />}>导入字典</Button>
|
||||
</Upload>
|
||||
<Button onClick={handleDownloadDictTemplate}>
|
||||
<Button size="small" onClick={handleDownloadDictTemplate}>
|
||||
下载模板
|
||||
</Button>
|
||||
</Space>
|
||||
|
|
@ -360,6 +360,7 @@ const DictPage: React.FC = () => {
|
|||
columns={dictColumns}
|
||||
rowKey="id"
|
||||
loading={loadingDicts}
|
||||
size="small"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
// 如果点击的是当前已选中的行,则取消选择
|
||||
|
|
@ -377,13 +378,14 @@ const DictPage: React.FC = () => {
|
|||
/>
|
||||
</Space>
|
||||
</Sider>
|
||||
<Content style={{ padding: '16px' }}>
|
||||
<Content style={{ padding: '8px' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', gap: '2px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDictItem}
|
||||
disabled={!selectedDict}
|
||||
size="small"
|
||||
>
|
||||
添加字典项
|
||||
</Button>
|
||||
|
|
@ -402,13 +404,14 @@ const DictPage: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} disabled={!selectedDict}>
|
||||
<Button size="small" icon={<UploadOutlined />} disabled={!selectedDict}>
|
||||
导入字典项
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
onClick={handleDownloadDictItemTemplate}
|
||||
disabled={!selectedDict}
|
||||
size="small"
|
||||
>
|
||||
下载模板
|
||||
</Button>
|
||||
|
|
@ -418,6 +421,7 @@ const DictPage: React.FC = () => {
|
|||
columns={dictItemColumns}
|
||||
rowKey="id"
|
||||
loading={loadingDictItems}
|
||||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
</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 allowedDictNames = new Set(['brand', 'strength', 'flavor', 'size', 'humidity']);
|
||||
import { allowedDictNames } from './consts';
|
||||
|
||||
const AttributePage: React.FC = () => {
|
||||
// 中文注释:左侧字典列表状态
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
productcontrollerCreateproduct,
|
||||
productcontrollerCompatsizeall,
|
||||
productcontrollerDeleteproduct,
|
||||
productcontrollerCompatbrandall,
|
||||
productcontrollerCompatflavorsall,
|
||||
|
|
@ -27,7 +28,8 @@ import {
|
|||
ProFormTextArea,
|
||||
ProTable,
|
||||
} 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';
|
||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
||||
// 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 actionRef = useRef<ActionType>();
|
||||
// 状态:存储当前选中的行
|
||||
|
|
@ -102,32 +148,23 @@ const List: React.FC = () => {
|
|||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
title: '属性',
|
||||
dataIndex: 'attributes',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => <AttributesCell record={record} />,
|
||||
},
|
||||
{
|
||||
title: '构成',
|
||||
dataIndex: 'components',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => <ComponentsCell productId={(record as any).id} />,
|
||||
},
|
||||
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '品牌',
|
||||
dataIndex: 'brand.name',
|
||||
},
|
||||
{
|
||||
title: '强度',
|
||||
dataIndex: 'strength.name',
|
||||
},
|
||||
{
|
||||
title: '口味',
|
||||
dataIndex: 'flavors.name',
|
||||
},
|
||||
{
|
||||
title: '湿度',
|
||||
dataIndex: 'humidity',
|
||||
},
|
||||
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updatedAt',
|
||||
|
|
@ -163,7 +200,7 @@ const List: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" danger>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
|
@ -322,7 +359,8 @@ const CreateForm: React.FC<{
|
|||
values.brandId ? { id: values.brandId } : null,
|
||||
values.strengthId ? { id: values.strengthId } : 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),
|
||||
};
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -381,6 +419,17 @@ const CreateForm: React.FC<{
|
|||
}}
|
||||
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
|
||||
name="humidity"
|
||||
width="lg"
|
||||
|
|
@ -455,9 +504,9 @@ const EditForm: React.FC<{
|
|||
const [components, setComponents] = useState<{ stockId: number; quantity: number }[]>([]);
|
||||
|
||||
const setInitialIds = () => {
|
||||
const brand = brandOptions.find((item) => item.title === record.brand.name);
|
||||
const strength = strengthOptions.find((item) => item.title === record.strength.name);
|
||||
const flavor = flavorOptions.find((item) => item.title === record.flavors.name);
|
||||
const brand = brandOptions.find((item) => item.title === (record.brand?.name));
|
||||
const strength = strengthOptions.find((item) => item.title === (record.strength?.name));
|
||||
const flavor = flavorOptions.find((item) => item.title === (record.flavors?.name));
|
||||
formRef.current?.setFieldsValue({
|
||||
brandId: brand?.id,
|
||||
strengthId: strength?.id,
|
||||
|
|
@ -495,10 +544,10 @@ const EditForm: React.FC<{
|
|||
name: record.name,
|
||||
sku: record.sku,
|
||||
description: record.description,
|
||||
humidity: record.humidity,
|
||||
price: (record as any).price,
|
||||
promotionPrice: (record as any).promotionPrice,
|
||||
price: record.price,
|
||||
promotionPrice: record.promotionPrice,
|
||||
components,
|
||||
attributes: record.attributes || [],
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
// 中文注释:组装 attributes(若选择了则发送)
|
||||
|
|
@ -506,6 +555,7 @@ const EditForm: React.FC<{
|
|||
values.brandId ? { id: values.brandId } : null,
|
||||
values.strengthId ? { id: values.strengthId } : null,
|
||||
values.flavorsId ? { id: values.flavorsId } : null,
|
||||
values.sizeId ? { id: values.sizeId } : null,
|
||||
values.humidity ? { dictName: 'humidity', name: values.humidity } : null,
|
||||
].filter(Boolean);
|
||||
const updatePayload: any = {
|
||||
|
|
@ -629,7 +679,7 @@ const EditForm: React.FC<{
|
|||
formRef.current?.setFieldsValue({ components: items });
|
||||
}}
|
||||
>
|
||||
自动绑定(同 SKU)
|
||||
自动绑定组成(同 SKU)
|
||||
</Button>
|
||||
</ProForm.Group>
|
||||
<ProFormList
|
||||
|
|
|
|||
|
|
@ -688,6 +688,7 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type Product = {
|
||||
attributes: never[];
|
||||
/** ID */
|
||||
id: number;
|
||||
/** 产品名称 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue