feat: 添加产品工具, 重构产品 #31

Closed
zksu wants to merge 37 commits from (deleted):main into main
16 changed files with 725 additions and 286 deletions
Showing only changes of commit 0f264c15a5 - Show all commits

View File

@ -4,10 +4,10 @@
"scripts": {
"build": "max build",
"dev": "max dev",
"fix:openapi2ts": "sed -i '' 's/\r$//' /Users/zksu/Developer/work/workcode/web/node_modules/@umijs/openapi/dist/cli.js",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"openapi2ts": "openapi2ts",
"fix:openapi2ts": "sed -i '' 's/\r$//' /Users/zksu/Developer/work/workcode/web/node_modules/@umijs/openapi/dist/cli.js",
"prepare": "husky",
"setup": "max setup",
"start": "npm run dev"

View File

@ -1,17 +1,47 @@
export default (initialState: any) => {
const isSuper = initialState?.user?.isSuper ?? false;
const isAdmin = initialState?.user?.Admin ?? false;
const canSeeOrganiza = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('organiza') ?? false);
const canSeeProduct = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('product') ?? false);
const canSeeStock = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('stock') ?? false);
const canSeeOrder = (isSuper || isAdmin) ||
((initialState?.user?.permissions?.includes('order') ?? false) || (initialState?.user?.permissions?.includes('order-10-days') ?? false));
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
const canSeeDict = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('dict') ?? false);
const canSeeTemplate = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('template') ?? false);
const canSeeOrganiza =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('organiza') ?? false);
const canSeeProduct =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('product') ?? false);
const canSeeStock =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('stock') ?? false);
const canSeeOrder =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('order') ?? false) ||
(initialState?.user?.permissions?.includes('order-10-days') ?? false);
const canSeeCustomer =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('customer') ?? false);
const canSeeLogistics =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('logistics') ?? false);
const canSeeStatistics =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('site') ?? false);
const canSeeDict =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('dict') ?? false);
const canSeeTemplate =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('template') ?? false);
return {
canSeeOrganiza,
canSeeProduct,

View File

@ -116,5 +116,5 @@ export const ORDER_STATUS_ENUM: ProSchemaValueEnumObj = {
refund_cancelled: {
text: '已取消退款',
status: 'refund_cancelled',
}
},
};

View File

@ -213,7 +213,9 @@ const DictPage: React.FC = () => {
const handleDownloadDictItemTemplate = async () => {
try {
// 使用带有认证拦截的 request 发起下载请求(后端鉴权通过)
const blob = await request('/dict/item/template', { responseType: 'blob' });
const blob = await request('/dict/item/template', {
responseType: 'blob',
});
// 创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
@ -247,7 +249,7 @@ const DictPage: React.FC = () => {
render: (_: any, record: any) => (
<Space size="small">
<Button
size='small'
size="small"
type="link"
onClick={(e) => {
e.stopPropagation();
@ -257,7 +259,7 @@ const DictPage: React.FC = () => {
</Button>
<Button
size='small'
size="small"
type="link"
danger
onClick={(e) => {
@ -321,7 +323,9 @@ const DictPage: React.FC = () => {
}}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Input.Search size="small" size="small"
<Input.Search
size="small"
size="small"
placeholder="搜索字典"
onSearch={handleSearch}
onChange={(e) => setSearchText(e.target.value)}
@ -332,7 +336,7 @@ const DictPage: React.FC = () => {
type="primary"
onClick={() => setIsAddDictModalVisible(true)}
size="small"
>
>
</Button>
<Space size="small">
@ -349,7 +353,9 @@ const DictPage: React.FC = () => {
}
}}
>
<Button size="small" icon={<UploadOutlined />}></Button>
<Button size="small" icon={<UploadOutlined />}>
</Button>
</Upload>
<Button size="small" onClick={handleDownloadDictTemplate}>
@ -380,7 +386,14 @@ const DictPage: React.FC = () => {
</Sider>
<Content style={{ padding: '8px' }}>
<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
type="primary"
onClick={handleAddDictItem}
@ -404,7 +417,11 @@ const DictPage: React.FC = () => {
}
}}
>
<Button size="small" icon={<UploadOutlined />} disabled={!selectedDict}>
<Button
size="small"
icon={<UploadOutlined />}
disabled={!selectedDict}
>
</Button>
</Upload>

View File

@ -37,7 +37,7 @@ const ListPage: React.FC = () => {
false: { text: '否' },
},
},
{
{
title: '激活',
dataIndex: 'isActive',
valueEnum: {
@ -72,7 +72,11 @@ const ListPage: React.FC = () => {
onClick={async () => {
// 中文注释软删除为禁用isActive=false再次点击可启用
const next = !record.isActive;
const { success, message: errMsg } = await usercontrollerToggleactive({ userId: record.id, isActive: next });
const { success, message: errMsg } =
await usercontrollerToggleactive({
userId: record.id,
isActive: next,
});
if (!success) return message.error(errMsg);
actionRef.current?.reload();
}}
@ -89,23 +93,32 @@ const ListPage: React.FC = () => {
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
request={async (params) => {
const { current = 1, pageSize = 10, username, isActive, isSuper, remark } = params as any;
console.log(`params`,params)
const {
current = 1,
pageSize = 10,
username,
isActive,
isSuper,
remark,
} = params as any;
console.log(`params`, params);
const qp: any = { current, pageSize };
if (username) qp.username = username;
if (typeof isActive !== 'undefined' && isActive !== '') qp.isActive = String(isActive);
if (typeof isSuper !== 'undefined' && isSuper !== '') qp.isSuper = String(isSuper);
if (typeof isActive !== 'undefined' && isActive !== '')
qp.isActive = String(isActive);
if (typeof isSuper !== 'undefined' && isSuper !== '')
qp.isSuper = String(isSuper);
if (remark) qp.remark = remark;
const { data, success } = await usercontrollerListusers({ params: qp });
const { data, success } = await usercontrollerListusers({
params: qp,
});
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
toolBarRender={() => [<CreateForm tableRef={actionRef} />]}
/>
@ -145,27 +158,32 @@ const CreateForm: React.FC<{
message.error(error.message);
}
}}
>
<ProForm.Group>
<ProFormText
name="username"
label="用户名"
width="lg"
placeholder="请输入用户名"
rules={[{ required: true, message: '请输入用户名' }]}
/>
<ProFormText
name="password"
label="密码"
width="lg"
placeholder="请输入密码"
rules={[{ required: true, message: '请输入密码' }]}
/>
<ProFormSwitch name="isSuper" label="超管" />
<ProFormSwitch name="isAdmin" label="管理员" />
<ProFormTextArea name="remark" label="备注" placeholder="请输入备注" fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }} />
</ProForm.Group>
</DrawerForm>
>
<ProForm.Group>
<ProFormText
name="username"
label="用户名"
width="lg"
placeholder="请输入用户名"
rules={[{ required: true, message: '请输入用户名' }]}
/>
<ProFormText
name="password"
label="密码"
width="lg"
placeholder="请输入密码"
rules={[{ required: true, message: '请输入密码' }]}
/>
<ProFormSwitch name="isSuper" label="超管" />
<ProFormSwitch name="isAdmin" label="管理员" />
<ProFormTextArea
name="remark"
label="备注"
placeholder="请输入备注"
fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }}
/>
</ProForm.Group>
</DrawerForm>
);
};
@ -187,7 +205,10 @@ const EditForm: React.FC<{
onFinish={async (values: any) => {
try {
// 中文注释:更新用户,密码可选填
const { success, message: err } = await usercontrollerUpdateuser({ id: record.id }, values);
const { success, message: err } = await usercontrollerUpdateuser(
{ id: record.id },
values,
);
if (!success) throw new Error(err);
tableRef.current?.reload();
message.success('更新成功');
@ -214,7 +235,12 @@ const EditForm: React.FC<{
/>
<ProFormSwitch name="isSuper" label="超管" />
<ProFormSwitch name="isAdmin" label="管理员" />
<ProFormTextArea name="remark" label="备注" placeholder="请输入备注" fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }} />
<ProFormTextArea
name="remark"
label="备注"
placeholder="请输入备注"
fieldProps={{ autoSize: { minRows: 2, maxRows: 4 } }}
/>
</ProForm.Group>
</DrawerForm>
);

View File

@ -1,7 +1,6 @@
import { productcontrollerGetattributelist } from '@/servers/api/product';
import { ProFormSelect } from '@ant-design/pro-components';
import { useState } from 'react';
import { productcontrollerGetattributeall, productcontrollerGetattributelist } from '@/servers/api/product';
interface AttributeFormItemProps {
dictName: string;
@ -11,12 +10,25 @@ interface AttributeFormItemProps {
}
const fetchDictOptions = async (dictName: string, keyword?: string) => {
const { data } = await productcontrollerGetattributelist({ dictName, name: keyword });
return (data?.items || []).map((item: any) => ({ label: item.name, value: item.name }));
const { data } = await productcontrollerGetattributelist({
dictName,
name: keyword,
});
return (data?.items || []).map((item: any) => ({
label: item.name,
value: item.name,
}));
};
const AttributeFormItem: React.FC<AttributeFormItemProps> = ({ dictName, name, label, isTag = false }) => {
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
const AttributeFormItem: React.FC<AttributeFormItemProps> = ({
dictName,
name,
label,
isTag = false,
}) => {
const [options, setOptions] = useState<{ label: string; value: string }[]>(
[],
);
if (isTag) {
return (

View File

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

View File

@ -1,7 +1,17 @@
import { UploadOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Form, Input, Layout, Modal, Space, Table, Upload, message } from 'antd';
import {
Button,
Form,
Input,
Layout,
Modal,
Space,
Table,
Upload,
message,
} from 'antd';
import React, { useEffect, useState } from 'react';
const { Sider, Content } = Layout;
@ -132,8 +142,21 @@ const AttributePage: React.FC = () => {
key: 'action',
render: (_: any, record: any) => (
<Space size="small">
<Button size="small" type="link" onClick={() => handleEditDictItem(record)}></Button>
<Button size="small" type="link" danger onClick={() => handleDeleteDictItem(record.id)}></Button>
<Button
size="small"
type="link"
onClick={() => handleEditDictItem(record)}
>
</Button>
<Button
size="small"
type="link"
danger
onClick={() => handleDeleteDictItem(record.id)}
>
</Button>
</Space>
),
},
@ -144,7 +167,11 @@ const AttributePage: React.FC = () => {
<Layout style={{ background: '#fff' }}>
<Sider
width={240}
style={{ background: '#fff', padding: '8px', borderRight: '1px solid #f0f0f0' }}
style={{
background: '#fff',
padding: '8px',
borderRight: '1px solid #f0f0f0',
}}
>
<Space direction="vertical" style={{ width: '100%' }} size="small">
<Input.Search
@ -171,15 +198,31 @@ const AttributePage: React.FC = () => {
}
},
})}
rowClassName={(record) => (selectedDict?.id === record.id ? 'ant-table-row-selected' : '')}
rowClassName={(record) =>
selectedDict?.id === record.id ? 'ant-table-row-selected' : ''
}
pagination={false}
/>
</Space>
</Sider>
<Content style={{ padding: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }} size="small">
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', gap: '4px' }}>
<Button type="primary" size="small" onClick={handleAddDictItem} disabled={!selectedDict}></Button>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
gap: '4px',
}}
>
<Button
type="primary"
size="small"
onClick={handleAddDictItem}
disabled={!selectedDict}
>
</Button>
<Upload
name="file"
action={`/dict/item/import`}
@ -196,7 +239,11 @@ const AttributePage: React.FC = () => {
}
}}
>
<Button size="small" icon={<UploadOutlined />} disabled={!selectedDict}>
<Button
size="small"
icon={<UploadOutlined />}
disabled={!selectedDict}
>
</Button>
</Upload>
@ -219,11 +266,23 @@ const AttributePage: React.FC = () => {
onCancel={() => setIsDictItemModalVisible(false)}
destroyOnClose
>
<Form form={dictItemForm} layout="vertical" onFinish={handleDictItemFormSubmit}>
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
<Form
form={dictItemForm}
layout="vertical"
onFinish={handleDictItemFormSubmit}
>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input size="small" placeholder="名称 (e.g., zyn)" />
</Form.Item>
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input size="small" placeholder="标题 (e.g., ZYN)" />
</Form.Item>
<Form.Item label="中文标题" name="titleCN">
@ -239,4 +298,3 @@ const AttributePage: React.FC = () => {
};
export default AttributePage;

View File

@ -1,16 +1,14 @@
import {
productcontrollerAutobindcomponents,
productcontrollerCreateproduct,
productcontrollerDeleteproduct,
productcontrollerGetproductlist,
productcontrollerUpdateproductnamecn,
productcontrollerUpdateproduct,
productcontrollerGetproductcomponents,
productcontrollerGetproductlist,
productcontrollerSetproductcomponents,
productcontrollerAutobindcomponents,
productcontrollerGetattributelist,
productcontrollerUpdateproduct,
productcontrollerUpdateproductnamecn,
} from '@/servers/api/product';
import { stockcontrollerGetstocks } from '@/servers/api/stock';
import { request } from '@umijs/max';
import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { PlusOutlined } from '@ant-design/icons';
import {
@ -19,6 +17,7 @@ import {
PageContainer,
ProColumns,
ProForm,
ProFormDigit,
ProFormInstance,
ProFormList,
ProFormSelect,
@ -26,6 +25,7 @@ import {
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { App, Button, Popconfirm, Tag, Upload } from 'antd';
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
@ -82,12 +82,13 @@ const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
);
};
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
const [components, setComponents] = React.useState<any[]>([]);
React.useEffect(() => {
(async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: productId });
const { data = [] } = await productcontrollerGetproductcomponents({
id: productId,
});
setComponents(data || []);
})();
}, [productId]);
@ -96,8 +97,11 @@ const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
{components && components.length ? (
components.map((component: any) => (
<Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
{(component.productSku) || `#${component.id}`} × {component.quantity}
{component.stock?.map((s: any) => `${s.name}:${s.quantity}`).join(', ') || '-'}
{component.productSku || `#${component.id}`} × {component.quantity}
{component.stock
?.map((s: any) => `${s.name}:${s.quantity}`)
.join(', ') || '-'}
</Tag>
))
@ -122,7 +126,9 @@ const List: React.FC = () => {
// 中文注释:构建下载文件名
const d = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const filename = `products-${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}.csv`;
const filename = `products-${d.getFullYear()}${pad(
d.getMonth() + 1,
)}${pad(d.getDate())}.csv`;
// 中文注释:创建临时链接并触发下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
@ -288,8 +294,9 @@ const CreateForm: React.FC<{
const { message } = App.useApp();
// 表单引用
const formRef = useRef<ProFormInstance>();
const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
const [stockStatus, setStockStatus] = useState<
'in-stock' | 'out-of-stock' | null
>(null);
/**
* @description SKU
@ -298,9 +305,15 @@ const CreateForm: React.FC<{
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
const { humidityValues, brandValues, strengthValues, flavorValues } =
formValues;
// 检查是否所有必需的字段都已选择
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
if (
!brandValues?.length ||
!strengthValues?.length ||
!flavorValues?.length ||
!humidityValues?.length
) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
@ -312,10 +325,14 @@ const CreateForm: React.FC<{
const humidityName: string = String(humidityValues[0]);
// 调用模板渲染API来生成SKU
const { data: rendered, message: msg, success } = await templatecontrollerRendertemplate(
const {
data: rendered,
message: msg,
success,
} = await templatecontrollerRendertemplate(
{ name: 'product.sku' },
{
brand: brandName || "",
brand: brandName || '',
strength: strengthName || '',
flavor: flavorName || '',
humidity: humidityName ? capitalize(humidityName) : '',
@ -339,9 +356,15 @@ const CreateForm: React.FC<{
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
const { humidityValues, brandValues, strengthValues, flavorValues } =
formValues;
// 检查是否所有必需的字段都已选择
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
if (
!brandValues?.length ||
!strengthValues?.length ||
!flavorValues?.length ||
!humidityValues?.length
) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
@ -355,14 +378,23 @@ const CreateForm: React.FC<{
const flavorTitle = flavorName;
// 调用模板渲染API来生成产品名称
const { message: msg, data: rendered, success } = await templatecontrollerRendertemplate(
const {
message: msg,
data: rendered,
success,
} = await templatecontrollerRendertemplate(
{ name: 'product.title' },
{
brand: brandTitle,
strength: strengthTitle,
flavor: flavorTitle,
model: '',
humidity: humidityName === 'dry' ? 'Dry' : humidityName === 'moisture' ? 'Moisture' : capitalize(humidityName),
humidity:
humidityName === 'dry'
? 'Dry'
: humidityName === 'moisture'
? 'Moisture'
: capitalize(humidityName),
},
);
if (!success) {
@ -390,31 +422,62 @@ const CreateForm: React.FC<{
destroyOnHidden: true,
}}
onValuesChange={async (changedValues) => {
// 当 SKU 发生变化时
if ('sku' in changedValues) {
const sku = changedValues.sku;
// 如果 sku 存在
if (sku) {
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
// 获取库存信息
const { data } = await stockcontrollerGetstocks({
productSku: sku,
} as any);
// 如果库存信息存在且不为空
if (data && data.items && data.items.length > 0) {
setProductType('single');
// 设置在库状态
setStockStatus('in-stock');
// 设置产品类型为单品
formRef.current?.setFieldsValue({ productType: 'single' });
} else {
setProductType('bundle');
// 设置未在库状态
setStockStatus('out-of-stock');
// 设置产品类型为套装
formRef.current?.setFieldsValue({ productType: 'bundle' });
}
} else {
setProductType(null);
// 如果 sku 不存在,则重置状态
setStockStatus(null);
formRef.current?.setFieldsValue({ productType: null });
}
}
}}
onFinish={async (values) => {
onFinish={async (values: any) => {
// 中文注释:组装 attributes支持输入新值按标题传入创建/绑定)
const attributes = [
...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
...(values.category ? [{ dictName: 'category', name: values.category }] : []),
];
...(values.brandValues || []).map((v: string) => ({
dictName: 'brand',
name: v,
})),
...(values.strengthValues || []).map((v: string) => ({
dictName: 'strength',
name: v,
})),
...(values.flavorValues || []).map((v: string) => ({
dictName: 'flavor',
name: v,
})),
...(values.humidityValues || []).map((v: string) => ({
dictName: 'humidity',
name: v,
})),
...(values.sizeValues || []).map((v: string) => ({
dictName: 'size',
name: v,
})),
...(values.category
? [{ dictName: 'category', name: values.category }]
: []),
];
const payload: any = {
name: (values as any).name,
description: (values as any).description,
@ -422,6 +485,8 @@ const CreateForm: React.FC<{
price: (values as any).price,
promotionPrice: (values as any).promotionPrice,
attributes,
components:
values.productType === 'bundle' ? values.components : undefined,
};
const { success, message: errMsg } =
await productcontrollerCreateproduct(payload);
@ -434,7 +499,6 @@ const CreateForm: React.FC<{
return false;
}}
>
<ProForm.Group>
<ProFormText
name="sku"
@ -446,9 +510,12 @@ const CreateForm: React.FC<{
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button>
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
{stockStatus && (
<Tag
style={{ marginTop: '32px' }}
color={stockStatus === 'in-stock' ? 'green' : 'orange'}
>
{stockStatus === 'in-stock' ? '在库' : '未在库'}
</Tag>
)}
</ProForm.Group>
@ -464,12 +531,83 @@ const CreateForm: React.FC<{
</Button>
</ProForm.Group>
<AttributeFormItem dictName="brand" name="brandValues" label="产品品牌" isTag />
<AttributeFormItem dictName="strength" name="strengthValues" label="强度" isTag />
<AttributeFormItem dictName="flavor" name="flavorValues" label="口味" isTag />
<AttributeFormItem dictName="humidity" name="humidityValues" label="干湿" isTag />
<ProFormSelect
name="productType"
label="产品类型"
options={[
{ value: 'single', label: '单品' },
{ value: 'bundle', label: '套装' },
]}
rules={[{ required: true, message: '请选择产品类型' }]}
/>
<ProForm.Item
shouldUpdate={(prevValues: any, curValues: any) =>
prevValues.productType !== curValues.productType
}
noStyle
>
{({ getFieldValue }: { getFieldValue: (name: string) => any }) =>
getFieldValue('productType') === 'bundle' ? (
<ProFormList
name="components"
label="产品组成"
initialValue={[{ sku: '', quantity: 1 }]}
creatorButtonProps={{
creatorButtonText: '添加子产品',
}}
>
<ProForm.Group>
<ProFormText
name="sku"
label="子产品SKU"
width="md"
placeholder="请输入子产品SKU"
rules={[{ required: true, message: '请输入子产品SKU' }]}
/>
<ProFormDigit
name="quantity"
label="数量"
width="xs"
min={1}
initialValue={1}
rules={[{ required: true, message: '请输入数量' }]}
/>
</ProForm.Group>
</ProFormList>
) : null
}
</ProForm.Item>
<AttributeFormItem
dictName="brand"
name="brandValues"
label="产品品牌"
isTag
/>
<AttributeFormItem
dictName="strength"
name="strengthValues"
label="强度"
isTag
/>
<AttributeFormItem
dictName="flavor"
name="flavorValues"
label="口味"
isTag
/>
<AttributeFormItem
dictName="humidity"
name="humidityValues"
label="干湿"
isTag
/>
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
<AttributeFormItem dictName="category" name="category" label="分类" isTag />
<AttributeFormItem
dictName="category"
name="category"
label="分类"
isTag
/>
<ProFormText
name="price"
label="价格"
@ -497,109 +635,147 @@ const CreateForm: React.FC<{
export default List;
const EditForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
record: API.Product;
}> = ({ tableRef, record }) => {
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ record, tableRef }) => {
const { message } = App.useApp();
const formRef = useRef<ProFormInstance>();
const [components, setComponents] = useState<{ productSku: string; quantity: number }[]>([]);
const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
const [components, setComponents] = useState<
{ productSku: string; quantity: number }[]
>([]);
const [productType, setProductType] = useState<'single' | 'bundle' | null>(
null,
);
React.useEffect(() => {
// 中文注释:加载当前产品的组成
(async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
const items = (data || []).map((c: any) => ({ productSku: c.productSku, quantity: c.quantity }));
setComponents(items as any);
formRef.current?.setFieldsValue({ components: items });
// 检查产品类型
if (record.sku) {
const { data: stockData } = await stockcontrollerGetstocks({ productSku: record.sku } as any);
if (stockData && stockData.items && stockData.items.length > 0) {
setProductType('single');
} else {
setProductType('bundle');
}
const { data: stockData } = await stockcontrollerGetstocks({
productSku: record.sku,
} as any);
if (stockData && stockData.items && stockData.items.length > 0) {
setProductType('single');
formRef.current?.setFieldsValue({ productType: 'single' });
} else {
setProductType('bundle');
formRef.current?.setFieldsValue({ productType: 'bundle' });
}
const { data: componentsData } =
await productcontrollerGetproductcomponents({ id: record.id });
setComponents(componentsData || []);
})();
}, [record]);
const initialValues = useMemo(() => {
if (!record) return {};
const attributes = record.attributes || [];
const attributesGroupedByName = (attributes as any[]).reduce((group, attr) => {
const dictName = attr.dict.name;
if (!group[dictName]) {
group[dictName] = [];
const initialValues = useMemo(() => {
return {
...record,
...((record as any).attributes || []).reduce((acc: any, cur: any) => {
const dictName = cur.dict?.name;
if (dictName) {
const key = `${dictName}Values`;
if (!acc[key]) {
acc[key] = [];
}
group[dictName].push(attr.name);
return group;
}, {} as Record<string, string[]>);
acc[key].push(cur.name);
}
return acc;
}, {} as any),
components: components,
productType: productType,
};
}, [record, components, productType]);
const values: any = {
...record,
brandValues: attributesGroupedByName.brand || [],
strengthValues: attributesGroupedByName.strength || [],
flavorValues: attributesGroupedByName.flavor || [],
humidityValues: attributesGroupedByName.humidity || [],
sizeValues: attributesGroupedByName.size || [],
category: attributesGroupedByName.category?.[0] || '',
};
return values;
}, [record]);
return (
<DrawerForm<any>
formRef={formRef}
title="编辑"
formRef={formRef}
trigger={<Button type="link"></Button>}
autoFocusFirstInput
drawerProps={{
destroyOnHidden: true,
}}
initialValues={initialValues}
onValuesChange={async (changedValues) => {
// 当 SKU 发生变化时
if ('sku' in changedValues) {
const sku = changedValues.sku;
// 如果 sku 存在
if (sku) {
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
// 获取库存信息
const { data } = await stockcontrollerGetstocks({
productSku: sku,
} as any);
// 如果库存信息存在且不为空
if (data && data.items && data.items.length > 0) {
setProductType('single');
// 设置产品类型为单品
formRef.current?.setFieldsValue({ productType: 'single' });
} else {
setProductType('bundle');
// 设置产品类型为套装
formRef.current?.setFieldsValue({ productType: 'bundle' });
}
} else {
setProductType(null);
// 如果 sku 不存在,则重置状态
formRef.current?.setFieldsValue({ productType: null });
}
}
}}
onFinish={async (values) => {
const attributes = [
...(values.brandValues || []).map((v: string) => ({ dictName: 'brand', name: v })),
...(values.strengthValues || []).map((v: string) => ({ dictName: 'strength', name: v })),
...(values.flavorValues || []).map((v: string) => ({ dictName: 'flavor', name: v })),
...(values.humidityValues || []).map((v: string) => ({ dictName: 'humidity', name: v })),
...(values.sizeValues || []).map((v: string) => ({ dictName: 'size', name: v })),
...(values.category ? [{ dictName: 'category', name: values.category }] : []),
...(values.brandValues || []).map((v: string) => ({
dictName: 'brand',
name: v,
})),
...(values.strengthValues || []).map((v: string) => ({
dictName: 'strength',
name: v,
})),
...(values.flavorValues || []).map((v: string) => ({
dictName: 'flavor',
name: v,
})),
...(values.humidityValues || []).map((v: string) => ({
dictName: 'humidity',
name: v,
})),
...(values.sizeValues || []).map((v: string) => ({
dictName: 'size',
name: v,
})),
...(values.category
? [{ dictName: 'category', name: values.category }]
: []),
];
const updatePayload: any = {
name: values.name,
sku: values.sku,
description: values.description,
price: values.price,
promotionPrice: values.promotionPrice,
const payload: any = {
name: (values as any).name,
description: (values as any).description,
sku: (values as any).sku,
price: (values as any).price,
promotionPrice: (values as any).promotionPrice,
attributes,
};
const { success, message: errMsg } = await productcontrollerUpdateproduct(
{ id: (record as any).id },
updatePayload,
);
if (success) {
// 中文注释:同步更新组成(覆盖式)
const items = (values as any)?.components || [];
if (Array.isArray(items)) {
const payloadItems = items
.filter((i: any) => i && i.productSku && i.quantity && i.quantity > 0)
.map((i: any) => ({ productSku: i.productSku, quantity: Number(i.quantity) }));
await productcontrollerSetproductcomponents({ id: (record as any).id }, { items: payloadItems as any });
const { success, message: errMsg } =
await productcontrollerUpdateproduct({ id: record.id }, payload);
if (values.productType === 'bundle') {
const { success: success2, message: errMsg2 } =
await productcontrollerSetproductcomponents(
{ id: record.id },
{
items: (values.components || []).map((c: any) => ({
sku: c.sku,
quantity: Number(c.quantity),
})),
},
);
if (!success2) {
message.error(errMsg2);
return false;
}
message.success('更新成功');
}
if (success) {
message.success('提交成功');
tableRef.current?.reloadAndRest?.();
return true;
}
@ -607,7 +783,6 @@ const EditForm: React.FC<{
return false;
}}
>
{/* 在这里列举attribute字段 */}
<ProForm.Group>
<ProFormText
name="sku"
@ -616,11 +791,23 @@ const EditForm: React.FC<{
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)}
<ProForm.Item noStyle shouldUpdate>
{() => {
const productType = formRef.current?.getFieldValue('productType');
return (
productType && (
<Tag
style={{ marginTop: '32px' }}
color={productType === 'single' ? 'green' : 'orange'}
>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)
);
}}
</ProForm.Item>
</ProForm.Group>
<ProForm.Group>
<ProFormText
name="name"
label="名称"
@ -629,81 +816,122 @@ const EditForm: React.FC<{
rules={[{ required: true, message: '请输入名称' }]}
/>
</ProForm.Group>
<AttributeFormItem dictName="brand" name="brandValues" label="产品品牌" isTag />
<AttributeFormItem dictName="strength" name="strengthValues" label="强度" isTag />
<AttributeFormItem dictName="flavor" name="flavorValues" label="口味" isTag />
<AttributeFormItem dictName="humidity" name="humidityValues" label="干湿" isTag />
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
<AttributeFormItem dictName="category" name="category" label="分类" />
<ProFormText
name="price"
label="价格"
width="md"
placeholder="请输入价格"
rules={[{ required: false }]}
/>
<ProFormText
name="promotionPrice"
label="促销价"
width="md"
placeholder="请输入促销价"
rules={[{ required: false }]}
<ProFormSelect
name="productType"
label="产品类型"
options={[
{ value: 'single', label: '单品' },
{ value: 'bundle', label: '套装' },
]}
rules={[{ required: true, message: '请选择产品类型' }]}
/>
<ProFormTextArea
name="description"
width="lg"
label="产品描述"
placeholder="请输入产品描述"
label="描述"
placeholder="请输入描述"
/>
{/* 中文注释编辑产品组成库存ID + 数量) */}
<ProForm.Group>
<ProFormText
name={['__helper']}
hidden
name="price"
label="价格"
width="md"
placeholder="请输入价格"
rules={[{ required: true, message: '请输入价格' }]}
/>
<ProFormText
name="promotionPrice"
label="促销价"
width="md"
placeholder="请输入促销价"
/>
</ProForm.Group>
<AttributeFormItem
dictName="brand"
name="brandValues"
label="产品品牌"
isTag
/>
<AttributeFormItem
dictName="strength"
name="strengthValues"
label="强度"
isTag
/>
<AttributeFormItem
dictName="flavor"
name="flavorValues"
label="口味"
isTag
/>
<AttributeFormItem
dictName="humidity"
name="humidityValues"
label="干湿度"
isTag
/>
<AttributeFormItem dictName="size" name="sizeValues" label="尺寸" isTag />
<AttributeFormItem dictName="category" name="category" label="分类" />
<ProForm.Group title="自动绑定">
<Button
disabled={productType === 'single'}
onClick={async () => {
await productcontrollerAutobindcomponents({ id: (record as any).id });
const { data = [] } = await productcontrollerGetproductcomponents({ id: (record as any).id });
const items = (data || []).map((c: any) => ({ stockId: c.stockId, quantity: c.quantity }));
formRef.current?.setFieldsValue({ components: items });
await productcontrollerAutobindcomponents({ id: record.id });
const { data: componentsData } =
await productcontrollerGetproductcomponents({ id: record.id });
formRef.current?.setFieldsValue({ components: componentsData });
}}
>
SKU
Run
</Button>
</ProForm.Group>
<ProForm.Item hidden={productType === 'single'}>
<ProFormList
name="components"
label="组成项"
creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增组成项' }}
itemRender={({ listDom, action }) => (
<div style={{ marginBottom: 8, display: 'flex', flexDirection: 'row', alignItems: 'end' }}>
{listDom}
{action}
</div>
)}
>
<ProForm.Group>
<ProFormText
name="productSku"
label="库存SKU"
width="md"
placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]}
/>
<ProFormText
name="quantity"
label="数量"
width="md"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
/>
</ProForm.Group>
</ProFormList>
<ProForm.Item
shouldUpdate={(prevValues: any, curValues: any) =>
prevValues.productType !== curValues.productType
}
noStyle
>
{({ getFieldValue }: { getFieldValue: (name: string) => any }) =>
getFieldValue('productType') === 'bundle' ? (
<ProFormList
name="components"
label="组成项"
creatorButtonProps={{
position: 'bottom',
creatorButtonText: '新增组成项',
}}
itemRender={({ listDom, action }) => (
<div
style={{
marginBottom: 8,
display: 'flex',
flexDirection: 'row',
alignItems: 'end',
}}
>
{listDom}
{action}
</div>
)}
>
<ProForm.Group>
<ProFormText
name="sku"
label="库存SKU"
width="md"
placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]}
/>
<ProFormText
name="quantity"
label="数量"
width="md"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
/>
</ProForm.Group>
</ProFormList>
) : null
}
</ProForm.Item>
</DrawerForm >
</DrawerForm>
);
};

View File

@ -31,6 +31,10 @@ import { useRef } from 'react';
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
const columns: ProColumns<API.WpProductDTO>[] = [
{
title: 'sku',
dataIndex: 'sku',
},
{
title: '名称',
dataIndex: 'name',
@ -42,22 +46,67 @@ const List: React.FC = () => {
request: async () => {
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.siteName,
label: item.name,
value: item.id,
}));
},
render: (_, record) => {
return record.site?.name;
},
},
{
title: 'sku',
dataIndex: 'sku',
hideInSearch: true,
},
{
title: '产品状态',
dataIndex: 'status',
valueType: 'select',
valueEnum: PRODUCT_STATUS_ENUM,
},
{
title: '产品类型',
dataIndex: 'type',
hideInSearch: true,
},
{
title: '总销量',
dataIndex: 'total_sales',
hideInSearch: true,
},
{
title: '库存',
dataIndex: 'stock_quantity',
hideInSearch: true,
},
{
title: '图片',
dataIndex: 'images',
hideInSearch: true,
render: (_, record) => {
if (record.images && record.images.length > 0) {
return <img src={record.images[0].src} width="50" />;
}
return null;
},
},
{
title: '分类',
dataIndex: 'categories',
hideInSearch: true,
render: (_, record) => {
return record.categories
?.map((item: { name: string }) => item.name)
.join(', ');
},
},
{
title: '标签',
dataIndex: 'tags',
hideInSearch: true,
render: (_, record) => {
return record.tags
?.map((item: { name: string }) => item.name)
.join(', ');
},
},
{
title: '常规价格',
dataIndex: 'regular_price',
@ -224,7 +273,7 @@ const SyncForm: React.FC<{
request={async () => {
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.siteName,
label: item.name,
value: item.id,
}));
}}
@ -240,7 +289,10 @@ const UpdateStatus: React.FC<{
}> = ({ tableRef, values: initialValues }) => {
const { message } = App.useApp();
return (
<DrawerForm<API.UpdateProductDTO>
<DrawerForm<{
status: API.WpProductDTO['status'];
stock_status: API.WpProductDTO['status'];
}>
title="修改产品上下架状态"
initialValues={initialValues}
trigger={
@ -316,15 +368,14 @@ const UpdateForm: React.FC<{
destroyOnHidden: true,
}}
onFinish={async (values) => {
const { siteId, ...params } = values;
try {
const { success, message: errMsg } =
await wpproductcontrollerUpdateproduct(
{
productId: initialValues.externalProductId,
siteId,
siteId: initialValues.siteId as number,
},
params,
values,
);
if (!success) {
throw new Error(errMsg);
@ -345,7 +396,7 @@ const UpdateForm: React.FC<{
request={async () => {
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.siteName,
label: item.name,
value: item.id,
}));
}}
@ -411,7 +462,7 @@ const UpdateVaritation: React.FC<{
const { success, message: errMsg } =
await wpproductcontrollerUpdatevariation(
{
siteId: initialValues.siteId,
siteId: initialValues.siteId as number,
productId: initialValues.externalProductId,
variationId: initialValues.externalVariationId,
},
@ -545,7 +596,7 @@ const SetComponent: React.FC<{
creatorButtonProps={{ children: '新增' }}
>
{(fields, idx, { remove }) => (
<div key={idx}>
<div key={fields.key}>
<ProFormSelect
request={async ({ keyWords }) => {
if (keyWords.length < 3) return [];
@ -566,7 +617,7 @@ const SetComponent: React.FC<{
return [];
}
}}
name="sku"
name={[fields.name, 'sku']}
label="产品"
width="lg"
placeholder="请选择产品"
@ -582,7 +633,7 @@ const SetComponent: React.FC<{
rules={[{ required: true, message: '请选择产品' }]}
/>
<ProFormDigit
name="quantity"
name={[fields.name, 'quantity']}
label="数量"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
@ -590,7 +641,7 @@ const SetComponent: React.FC<{
precision: 0,
}}
/>
<Button type="link" danger onClick={() => remove(fields.key)}>
<Button type="link" danger onClick={() => remove(idx)}>
</Button>
</div>

View File

@ -120,10 +120,19 @@ const ListPage: React.FC = () => {
const rows = (data?.items || []).map((item: API.StockDTO) => {
// 处理stockPoint可能为undefined的情况并正确定义类型
const stockMap = new Map<number, number>(
(item.stockPoint || []).map((sp: any) => [sp.id || 0, sp.quantity || 0]),
(item.stockPoint || []).map((sp: any) => [
sp.id || 0,
sp.quantity || 0,
]),
);
const stockRow = points.map((p) => stockMap.get(p.id || 0) || 0);
return [item.productName || '', item.productSku || '', ...stockRow];
const stockRow = points.map(
(p) => stockMap.get(p.id || 0) || 0,
);
return [
item.productName || '',
item.productSku || '',
...stockRow,
];
});
// 导出

View File

@ -15,7 +15,7 @@ import OrderDetailDrawer from './OrderDetailDrawer';
interface OrderItemRow {
id: number;
externalOrderId: string;
siteId: string;
siteId: number;
date_created: string;
customer_email: string;
payment_method: string;

View File

@ -19,6 +19,7 @@ declare namespace API {
createdAt: string;
/** 更新时间 */
updatedAt: string;
attributes?: any[];
};
type areacontrollerDeleteareaParams = {
@ -431,11 +432,11 @@ declare namespace API {
type ordercontrollerSyncorderbyidParams = {
orderId: string;
siteId: string;
siteId: number;
};
type ordercontrollerSyncorderParams = {
siteId: string;
siteId: number;
};
type ordercontrollerUpdateorderitemsParams = {
@ -1543,7 +1544,7 @@ declare namespace API {
};
type subscriptioncontrollerSyncParams = {
siteId: string;
siteId: number;
};
type SubscriptionListRes = {
@ -1704,7 +1705,7 @@ declare namespace API {
/** ID */
id: number;
/** wp网站ID */
siteId: string;
siteId: number;
/** wp产品ID */
externalProductId: string;
/** wp变体ID */
@ -1765,18 +1766,18 @@ declare namespace API {
};
type wpproductcontrollerSyncproductsParams = {
siteId: string;
siteId: number;
};
type wpproductcontrollerUpdateproductParams = {
productId: string;
siteId: string;
siteId: number;
};
type wpproductcontrollerUpdatevariationParams = {
variationId: string;
productId: string;
siteId: string;
siteId: number;
};
type wpproductcontrollerUpdatewpproductstateParams = {
@ -1787,7 +1788,7 @@ declare namespace API {
/** ID */
id: number;
/** wp网站ID */
siteId: string;
siteId: number;
/** wp产品ID */
externalProductId: string;
/** 商店sku */

View File

@ -95,8 +95,8 @@ export function formatUniuniShipmentState(state: string) {
'230': 'RETURN TO SENDER WAREHOUSE',
'231': 'FAILED_DELIVERY_RETRY1',
'232': 'FAILED_DELIVERY_RETRY2',
'255': 'Gateway_To_Gateway_Transit'
}
'255': 'Gateway_To_Gateway_Transit',
};
if (state in UNIUNI_STATUS_ENUM) {
return UNIUNI_STATUS_ENUM[state];
} else {