WEB/src/pages/Product/List/index.tsx

710 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
productcontrollerCreateproduct,
productcontrollerDeleteproduct,
productcontrollerGetproductlist,
productcontrollerUpdateproductnamecn,
productcontrollerUpdateproduct,
productcontrollerGetproductcomponents,
productcontrollerSetproductcomponents,
productcontrollerAutobindcomponents,
productcontrollerGetattributelist,
} 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 {
ActionType,
DrawerForm,
PageContainer,
ProColumns,
ProForm,
ProFormInstance,
ProFormList,
ProFormSelect,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Popconfirm, Tag, Upload } from 'antd';
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
import React, { useMemo, useRef, useState } from 'react';
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
// TODO
interface DictItem {
id: number;
name: string;
title: string;
}
const NameCn: React.FC<{
id: number;
value: string | undefined;
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ value, tableRef, id }) => {
const { message } = App.useApp();
const [editable, setEditable] = React.useState<boolean>(false);
if (!editable)
return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
return (
<ProFormText
initialValue={value}
fieldProps={{
autoFocus: true,
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) return setEditable(false);
const { success, message: errMsg } =
await productcontrollerUpdateproductnamecn({
id,
nameCn: e.target.value,
});
setEditable(false);
if (!success) {
return message.error(errMsg);
}
tableRef?.current?.reloadAndRest?.();
},
}}
/>
);
};
const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
return (
<div>
{(record.attributes || []).map((data: any, idx: number) => (
<Tag key={idx} color="purple" style={{ marginBottom: 4 }}>
{data?.dict?.name}: {data.name}
</Tag>
))}
</div>
);
};
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
const [components, setComponents] = React.useState<any[]>([]);
React.useEffect(() => {
(async () => {
const { data = [] } = await productcontrollerGetproductcomponents({ id: productId });
setComponents(data || []);
})();
}, [productId]);
return (
<div>
{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(', ') || '-'}
</Tag>
))
) : (
<span>-</span>
)}
</div>
);
};
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
// 状态:存储当前选中的行
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
const { message } = App.useApp();
// 中文注释:导出产品 CSV带认证请求
const handleDownloadProductsCSV = async () => {
try {
// 中文注释:发起认证请求获取 CSV Blob
const blob = await request('/product/export', { responseType: 'blob' });
// 中文注释:构建下载文件名
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 url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
message.error('导出失败');
}
};
const columns: ProColumns<API.Product>[] = [
{
title: 'sku',
dataIndex: 'sku',
},
{
title: '名称',
dataIndex: 'name',
},
{
title: '中文名',
dataIndex: 'nameCn',
render: (_, record) => {
return (
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
);
},
},
{
title: '价格',
dataIndex: 'price',
hideInSearch: true,
},
{
title: '促销价',
dataIndex: 'promotionPrice',
hideInSearch: true,
},
{
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: 'updatedAt',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '创建时间',
dataIndex: 'createdAt',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => (
<>
<EditForm record={record} tableRef={actionRef} />
<Popconfirm
title="删除"
description="确认删除?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await productcontrollerDeleteproduct({ id: record.id });
if (!success) {
throw new Error(errMsg);
}
actionRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</>
),
},
];
return (
<PageContainer header={{ title: '产品列表' }}>
<ProTable<API.Product>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
toolBarRender={() => [
// 中文注释:新建按钮
<CreateForm tableRef={actionRef} />,
// 中文注释:导出 CSV后端返回 text/csv直接新窗口下载
<Button onClick={handleDownloadProductsCSV}>CSV</Button>,
// 中文注释:导入 CSV上传文件成功后刷新表格
<Upload
name="file"
action="/product/import"
accept=".csv"
showUploadList={false}
maxCount={1}
onChange={({ file }) => {
if (file.status === 'done') {
message.success('导入完成');
actionRef.current?.reload();
} else if (file.status === 'error') {
message.error('导入失败');
}
}}
>
<Button>CSV</Button>
</Upload>,
]}
request={async (params) => {
const { data, success } = await productcontrollerGetproductlist(
params,
);
return {
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
columns={columns}
editable={{
type: 'single',
onSave: async (key, record, originRow) => {
console.log('保存数据:', record);
},
}}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
/>
</PageContainer>
);
};
const CreateForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ tableRef }) => {
// antd 的消息提醒
const { message } = App.useApp();
// 表单引用
const formRef = useRef<ProFormInstance>();
const [productType, setProductType] = useState<'single' | 'bundle' | null>(null);
/**
* @description 生成 SKU
*/
const handleGenerateSku = async () => {
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
// 检查是否所有必需的字段都已选择
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
// 所选值(用于 SKU 模板传入 name
const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[0]);
const flavorName: string = String(flavorValues[0]);
const humidityName: string = String(humidityValues[0]);
// 调用模板渲染API来生成SKU
const { data: rendered, message: msg, success } = await templatecontrollerRendertemplate(
{ name: 'product.sku' },
{
brand: brandName || "",
strength: strengthName || '',
flavor: flavorName || '',
humidity: humidityName ? capitalize(humidityName) : '',
},
);
if (!success) {
throw new Error(msg);
}
// 将生成的SKU设置到表单字段中
formRef.current?.setFieldsValue({ sku: rendered });
} catch (error: any) {
message.error(`生成失败: ${error.message}`);
}
};
/**
* @description 生成产品名称
*/
const handleGenerateName = async () => {
try {
// 从表单引用中获取当前表单的值
const formValues = formRef.current?.getFieldsValue();
const { humidityValues, brandValues, strengthValues, flavorValues } = formValues;
// 检查是否所有必需的字段都已选择
if (!brandValues?.length || !strengthValues?.length || !flavorValues?.length || !humidityValues?.length) {
message.warning('请先选择品牌、强度、口味和干湿');
return;
}
const brandName: string = String(brandValues[0]);
const strengthName: string = String(strengthValues[0]);
const flavorName: string = String(flavorValues[0]);
const humidityName: string = String(humidityValues[0]);
const brandTitle = brandName;
const strengthTitle = strengthName;
const flavorTitle = flavorName;
// 调用模板渲染API来生成产品名称
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),
},
);
if (!success) {
throw new Error(msg);
}
// 将生成的名称设置到表单字段中
formRef.current?.setFieldsValue({ name: rendered });
} catch (error: any) {
message.error(`生成失败: ${error.message}`);
}
};
// TODO 可以输入brand等
return (
<DrawerForm<any>
title="新建"
formRef={formRef} // Pass formRef
trigger={
<Button type="primary">
<PlusOutlined />
</Button>
}
autoFocusFirstInput
drawerProps={{
destroyOnHidden: true,
}}
onValuesChange={async (changedValues) => {
if ('sku' in changedValues) {
const sku = changedValues.sku;
if (sku) {
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
if (data && data.items && data.items.length > 0) {
setProductType('single');
} else {
setProductType('bundle');
}
} else {
setProductType(null);
}
}
}}
onFinish={async (values) => {
// 中文注释:组装 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 }] : []),
];
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 productcontrollerCreateproduct(payload);
if (success) {
message.success('提交成功');
tableRef.current?.reloadAndRest?.();
return true;
}
message.error(errMsg);
return false;
}}
>
<ProForm.Group>
<ProFormText
name="sku"
label="SKU"
width="md"
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button>
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)}
</ProForm.Group>
<ProForm.Group>
<ProFormText
name="name"
label="名称"
width="md"
placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]}
/>
<Button style={{ marginTop: '32px' }} onClick={handleGenerateName}>
</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 />
<AttributeFormItem dictName="size" name="sizeValues" label="大小" isTag />
<AttributeFormItem dictName="category" name="category" label="分类" isTag />
<ProFormText
name="price"
label="价格"
width="md"
placeholder="请输入价格"
rules={[{ required: false }]}
/>
<ProFormText
name="promotionPrice"
label="促销价"
width="md"
placeholder="请输入促销价"
rules={[{ required: false }]}
/>
<ProFormTextArea
name="description"
width="lg"
label="产品描述"
placeholder="请输入产品描述"
/>
</DrawerForm>
);
};
export default List;
const EditForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
record: API.Product;
}> = ({ tableRef, record }) => {
const { message } = App.useApp();
const formRef = useRef<ProFormInstance>();
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');
}
}
})();
}, [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] = [];
}
group[dictName].push(attr.name);
return group;
}, {} as Record<string, string[]>);
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="编辑"
trigger={<Button type="link"></Button>}
initialValues={initialValues}
onValuesChange={async (changedValues) => {
if ('sku' in changedValues) {
const sku = changedValues.sku;
if (sku) {
const { data } = await stockcontrollerGetstocks({ productSku: sku } as any);
if (data && data.items && data.items.length > 0) {
setProductType('single');
} else {
setProductType('bundle');
}
} else {
setProductType(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 }] : []),
];
const updatePayload: any = {
name: values.name,
sku: values.sku,
description: values.description,
price: values.price,
promotionPrice: values.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 });
}
message.success('更新成功');
tableRef.current?.reloadAndRest?.();
return true;
}
message.error(errMsg);
return false;
}}
>
{/* 在这里列举attribute字段 */}
<ProForm.Group>
<ProFormText
name="sku"
label="SKU"
width="md"
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
{productType && (
<Tag style={{ marginTop: '32px' }} color={productType === 'single' ? 'green' : 'orange'}>
{productType === 'single' ? '单品' : '套装'}
</Tag>
)}
<ProFormText
name="name"
label="名称"
width="md"
placeholder="请输入名称"
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 }]}
/>
<ProFormTextArea
name="description"
width="lg"
label="产品描述"
placeholder="请输入产品描述"
/>
{/* 中文注释编辑产品组成库存ID + 数量) */}
<ProForm.Group>
<ProFormText
name={['__helper']}
hidden
/>
<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 });
}}
>
SKU
</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>
</DrawerForm >
);
};