From db0bea991c50d46d69b5d38534f143dcf38149fe Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 10 Dec 2025 15:32:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=AB=99=E7=82=B9=E7=AE=A1=E7=90=86=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=BA=97=E9=93=BA=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(产品管理): 优化产品属性排列组件逻辑 feat(物流管理): 添加批量操作功能并优化状态显示 style(订单管理): 统一站点名称字段为name并修复分页问题 feat(字典管理): 新增简称和图片字段支持 perf(用户管理): 添加排序和筛选功能提升用户体验 chore(依赖): 添加monaco-editor和tinymce-react等新依赖 feat(媒体库): 实现多站点媒体文件管理功能 fix(订阅管理): 修正订阅状态显示和操作逻辑 build(配置): 更新路由配置和API类型定义 --- .umirc.ts | 101 +- package.json | 3 + src/access.ts | 2 + src/components/SyncForm.tsx | 29 +- src/pages/Customer/List/index.tsx | 2 + src/pages/Dict/List/index.tsx | 19 + src/pages/Logistics/List/index.tsx | 2 +- src/pages/Order/Items/index.tsx | 2 +- src/pages/Order/List/index.tsx | 15 +- src/pages/Organiza/User/index.tsx | 27 +- src/pages/Product/Attribute/index.tsx | 18 +- src/pages/Product/List/CreateForm.tsx | 390 ++++++ src/pages/Product/List/EditForm.tsx | 356 ++++++ src/pages/Product/List/index.tsx | 1105 ++++++----------- .../Permutation/components/CreateModal.tsx | 157 +++ src/pages/Product/Permutation/index.tsx | 321 +++++ src/pages/Product/Sync/index.tsx | 380 ++++-- src/pages/Site/List/index.tsx | 86 +- src/pages/Site/Shop/Customers/index.tsx | 221 ++++ src/pages/Site/Shop/Layout.tsx | 130 ++ src/pages/Site/Shop/Logistics/index.tsx | 217 ++++ src/pages/Site/Shop/Media/index.tsx | 250 ++++ src/pages/Site/Shop/Orders/index.tsx | 305 +++++ src/pages/Site/Shop/Products/index.tsx | 298 +++++ src/pages/Site/Shop/Subscriptions/index.tsx | 210 ++++ .../Site/Shop/components/Order/Forms.tsx | 711 +++++++++++ .../Site/Shop/components/Product/Forms.tsx | 653 ++++++++++ .../Site/Shop/components/Product/utils.ts | 172 +++ src/pages/Statistics/Order/index.tsx | 6 +- src/pages/Statistics/Sales/index.tsx | 2 +- src/pages/Stock/PurchaseOrder/index.tsx | 110 +- src/pages/Subscription/List/index.tsx | 10 +- .../Subscription/Orders/OrderDetailDrawer.tsx | 2 +- src/pages/Subscription/Orders/index.tsx | 2 +- src/pages/Template/index.tsx | 191 ++- src/pages/Track/index.tsx | 2 +- src/pages/Woo/Media/index.tsx | 167 +++ src/pages/Woo/Product/List/index.tsx | 552 +++++++- src/servers/api/product.ts | 30 + src/servers/api/typings.d.ts | 33 + src/servers/api/wpProduct.ts | 91 +- 41 files changed, 6381 insertions(+), 999 deletions(-) create mode 100644 src/pages/Product/List/CreateForm.tsx create mode 100644 src/pages/Product/List/EditForm.tsx create mode 100644 src/pages/Product/Permutation/components/CreateModal.tsx create mode 100644 src/pages/Product/Permutation/index.tsx create mode 100644 src/pages/Site/Shop/Customers/index.tsx create mode 100644 src/pages/Site/Shop/Layout.tsx create mode 100644 src/pages/Site/Shop/Logistics/index.tsx create mode 100644 src/pages/Site/Shop/Media/index.tsx create mode 100644 src/pages/Site/Shop/Orders/index.tsx create mode 100644 src/pages/Site/Shop/Products/index.tsx create mode 100644 src/pages/Site/Shop/Subscriptions/index.tsx create mode 100644 src/pages/Site/Shop/components/Order/Forms.tsx create mode 100644 src/pages/Site/Shop/components/Product/Forms.tsx create mode 100644 src/pages/Site/Shop/components/Product/utils.ts create mode 100644 src/pages/Woo/Media/index.tsx diff --git a/.umirc.ts b/.umirc.ts index 4d9e0ad..5610920 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -44,32 +44,7 @@ export default defineConfig({ ], }, - { - name: '字典管理', - path: '/dict', - access: 'canSeeDict', - routes: [ - { - name: '字典列表', - path: '/dict/list', - component: './Dict/List', - }, - ], - }, - { - // 模板管理 - name: '模板管理', - path: '/template', - access: 'canSeeDict', // 权限暂用 canSeeDict - routes: [ - { - // 模板列表 - name: '模板列表', - path: '/template/list', - component: './Template', // 对应 src/pages/Template/index.tsx - }, - ], - }, + { name: '地区管理', path: '/area', @@ -96,6 +71,19 @@ export default defineConfig({ name: '站点列表', path: '/site/list', component: './Site/List', + }, + { + name: '店铺页面', + path: '/site/shop', + component: './Site/Shop/Layout', + routes: [ + { path: '/site/shop/:siteId/products', component: './Site/Shop/Products' }, + { path: '/site/shop/:siteId/orders', component: './Site/Shop/Orders' }, + { path: '/site/shop/:siteId/subscriptions', component: './Site/Shop/Subscriptions' }, + { path: '/site/shop/:siteId/logistics', component: './Site/Shop/Logistics' }, + { path: '/site/shop/:siteId/media', component: './Site/Shop/Media' }, + { path: '/site/shop/:siteId/customers', component: './Site/Shop/Customers' }, + ], }, { name: 'Woo产品列表', @@ -109,7 +97,18 @@ export default defineConfig({ }, ], }, - + { + name: '客户管理', + path: '/customer', + access: 'canSeeCustomer', + routes: [ + { + name: '客户列表', + path: '/customer/list', + component: './Customer/List', + }, + ], + }, { name: '产品管理', path: '/product', @@ -120,6 +119,11 @@ export default defineConfig({ path: '/product/list', component: './Product/List', }, + { + name: '产品属性排列', + path: '/product/permutation', + component: './Product/Permutation', + }, { name: "产品分类", path: '/product/category', @@ -205,18 +209,6 @@ export default defineConfig({ }, ], }, - { - name: '客户管理', - path: '/customer', - access: 'canSeeCustomer', - routes: [ - { - name: '客户列表', - path: '/customer/list', - component: './Customer/List', - }, - ], - }, { name: '物流管理', path: '/logistics', @@ -276,6 +268,37 @@ export default defineConfig({ }, ], }, + { + name: '系统管理', + path: '/system', + access: 'canSeeSystem', + routes: [ + { + name: '字典管理', + path: '/system/dict', + access: 'canSeeDict', + routes: [ + { + name: '字典列表', + path: '/system/dict/list', + component: './Dict/List', + }, + ], + }, + { + name: '模板管理', + path: '/system/template', + access: 'canSeeTemplate', + routes: [ + { + name: '模板列表', + path: '/system/template/list', + component: './Template', + }, + ], + }, + ], + }, // { // path: '*', // component: './404', diff --git a/package.json b/package.json index 11b5a35..1d9e98a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@ant-design/icons": "^5.0.1", "@ant-design/pro-components": "^2.4.4", "@fingerprintjs/fingerprintjs": "^4.6.2", + "@monaco-editor/react": "^4.7.0", + "@tinymce/tinymce-react": "^6.3.0", "@umijs/max": "^4.4.4", "@umijs/max-plugin-openapi": "^2.0.3", "@umijs/plugin-openapi": "^1.3.3", @@ -27,6 +29,7 @@ "file-saver": "^2.0.5", "i18n-iso-countries": "^7.14.0", "print-js": "^1.6.0", + "react-json-view": "^1.21.3", "react-phone-input-2": "^2.15.1", "react-toastify": "^11.0.5", "xlsx": "^0.18.5" diff --git a/src/access.ts b/src/access.ts index a4c8180..574ee12 100644 --- a/src/access.ts +++ b/src/access.ts @@ -46,6 +46,7 @@ export default (initialState: any) => { isSuper || isAdmin || (initialState?.user?.permissions?.includes('area') ?? false); + const canSeeSystem = canSeeDict || canSeeTemplate; return { canSeeOrganiza, canSeeProduct, @@ -58,5 +59,6 @@ export default (initialState: any) => { canSeeDict, canSeeTemplate, canSeeArea, + canSeeSystem, }; }; diff --git a/src/components/SyncForm.tsx b/src/components/SyncForm.tsx index c4b8ea3..dadb23a 100644 --- a/src/components/SyncForm.tsx +++ b/src/components/SyncForm.tsx @@ -13,6 +13,7 @@ import React from 'react'; interface SyncFormProps { tableRef: React.MutableRefObject; onFinish: (values: any) => Promise; + siteId?: string; } /** @@ -20,8 +21,30 @@ interface SyncFormProps { * @param {SyncFormProps} props 组件属性 * @returns {React.ReactElement} 抽屉表单 */ -const SyncForm: React.FC = ({ tableRef, onFinish }) => { +const SyncForm: React.FC = ({ tableRef, onFinish, siteId }) => { // 使用 antd 的 App 组件提供的 message API + const [loading, setLoading] = React.useState(false); + + if (siteId) { + return ( + + ); + } // 返回一个抽屉表单 return ( @@ -54,8 +77,8 @@ const SyncForm: React.FC = ({ tableRef, onFinish }) => { request={async () => { const { data = [] } = await sitecontrollerAll(); // 将返回的数据格式化为 ProFormSelect 需要的格式 - return data.map((item) => ({ - label: item.siteName, + return data.map((item: any) => ({ + label: item.name || String(item.id), value: item.id, })); }} diff --git a/src/pages/Customer/List/index.tsx b/src/pages/Customer/List/index.tsx index d0ca5e0..8cfd687 100644 --- a/src/pages/Customer/List/index.tsx +++ b/src/pages/Customer/List/index.tsx @@ -177,6 +177,7 @@ const ListPage: React.FC = () => { title: '操作', dataIndex: 'option', valueType: 'option', + fixed: 'right', render: (_, record) => { return ( @@ -198,6 +199,7 @@ const ListPage: React.FC = () => { return ( { key: 'name', copyable: true, }, + { + title: '简称', + dataIndex: 'shortName', + key: 'shortName', + copyable: true, + }, + { + title: '图片', + dataIndex: 'image', + key: 'image', + valueType: 'image', + width: 80, + }, { title: '标题', dataIndex: 'title', @@ -418,6 +431,12 @@ const DictPage: React.FC = () => { + + + + + + diff --git a/src/pages/Logistics/List/index.tsx b/src/pages/Logistics/List/index.tsx index 9f48d4e..0fcd7db 100644 --- a/src/pages/Logistics/List/index.tsx +++ b/src/pages/Logistics/List/index.tsx @@ -52,7 +52,7 @@ const ListPage: React.FC = () => { request: async () => { const { data = [] } = await sitecontrollerAll(); return data.map((item) => ({ - label: item.siteName, + label: item.name, value: item.id, })); }, diff --git a/src/pages/Order/Items/index.tsx b/src/pages/Order/Items/index.tsx index adaa383..de1e310 100644 --- a/src/pages/Order/Items/index.tsx +++ b/src/pages/Order/Items/index.tsx @@ -92,7 +92,7 @@ const OrderItemsPage: React.FC = () => { // 拉取站点列表(后台 /site/all) const { data = [] } = await sitecontrollerAll(); return (data || []).map((item: any) => ({ - label: item.siteName, + label: item.name, value: item.id, })); }, diff --git a/src/pages/Order/List/index.tsx b/src/pages/Order/List/index.tsx index ed5b688..0614757 100644 --- a/src/pages/Order/List/index.tsx +++ b/src/pages/Order/List/index.tsx @@ -189,7 +189,7 @@ const ListPage: React.FC = () => { request: async () => { const { data = [] } = await sitecontrollerAll(); return data.map((item) => ({ - label: item.siteName, + label: item.name, value: item.id, })); }, @@ -451,6 +451,11 @@ const ListPage: React.FC = () => { ? styles['selected-line-order-protable'] : ''; }} + pagination={{ + pageSizeOptions: ['10', '20', '50', '100', '1000'], + showSizeChanger: true, + defaultPageSize: 10, + }} toolBarRender={() => [ , { const { data = [] } = await sitecontrollerAll(); return data.map((item) => ({ - label: item.siteName, + label: item.name, value: item.id, })); }} @@ -1255,9 +1260,9 @@ const Shipping: React.FC<{ signature_requirement: 'not-required', }, origin: { - name: data?.siteName, + name: data?.name, email_addresses: data?.email, - contact_name: data?.siteName, + contact_name: data?.name, phone_number: shipmentInfo?.phone_number, address: { region: shipmentInfo?.region, @@ -1376,7 +1381,7 @@ const Shipping: React.FC<{ }); if (success) { return data.map((v) => ({ - label: `${v.siteName} ${v.externalOrderId}`, + label: `${v.name} ${v.externalOrderId}`, value: v.id, })); } diff --git a/src/pages/Organiza/User/index.tsx b/src/pages/Organiza/User/index.tsx index 939be46..d128618 100644 --- a/src/pages/Organiza/User/index.tsx +++ b/src/pages/Organiza/User/index.tsx @@ -26,6 +26,7 @@ const ListPage: React.FC = () => { { title: '用户名', dataIndex: 'username', + sorter: true, }, { @@ -36,6 +37,9 @@ const ListPage: React.FC = () => { true: { text: '是' }, false: { text: '否' }, }, + sorter: true, + filters: true, + filterMultiple: false, }, { title: '激活', @@ -48,6 +52,9 @@ const ListPage: React.FC = () => { text: '否', }, }, + sorter: true, + filters: true, + filterMultiple: false, render: (_, record: any) => ( {record?.isActive ? '启用中' : '已禁用'} @@ -93,7 +100,7 @@ const ListPage: React.FC = () => { headerTitle="查询表格" actionRef={actionRef} rowKey="id" - request={async (params) => { + request={async (params, sort, filter) => { const { current = 1, pageSize = 10, @@ -102,14 +109,30 @@ const ListPage: React.FC = () => { isSuper, remark, } = params as any; - console.log(`params`, params); + console.log(`params`, params, sort); 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 (filter.isActive && filter.isActive.length > 0) { + qp.isActive = filter.isActive[0]; + } + if (filter.isSuper && filter.isSuper.length > 0) { + qp.isSuper = filter.isSuper[0]; + } + if (remark) qp.remark = remark; + + const sortField = Object.keys(sort)[0]; + if (sortField) { + qp.sortField = sortField; + qp.sortOrder = sort[sortField]; + } + const { data, success } = await usercontrollerListusers({ params: qp, }); diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx index a013076..351ccb3 100644 --- a/src/pages/Product/Attribute/index.tsx +++ b/src/pages/Product/Attribute/index.tsx @@ -102,9 +102,13 @@ const AttributePage: React.FC = () => { // 删除字典项 const handleDeleteDictItem = async (itemId: number) => { try { - await request(`/dict/item/${itemId}`, { method: 'DELETE' }); - message.success('删除成功'); - actionRef.current?.reload(); // 刷新 ProTable + const success = await request(`/dict/item/${itemId}`, { method: 'DELETE' }); + if (success) { + message.success('删除成功'); + actionRef.current?.reload(); // 刷新 ProTable + } else { + message.error('删除失败'); + } } catch (error) { message.error('删除失败'); } @@ -121,6 +125,8 @@ const AttributePage: React.FC = () => { { title: '名称', dataIndex: 'name', key: 'name', copyable: true }, { title: '标题', dataIndex: 'title', key: 'title', copyable: true }, { title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true }, + { title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true }, + { title: '图片', dataIndex: 'image', key: 'image', valueType: 'image', width: 80 }, { title: '操作', key: 'action', @@ -294,6 +300,12 @@ const AttributePage: React.FC = () => { + + + + + + diff --git a/src/pages/Product/List/CreateForm.tsx b/src/pages/Product/List/CreateForm.tsx new file mode 100644 index 0000000..a0f5e68 --- /dev/null +++ b/src/pages/Product/List/CreateForm.tsx @@ -0,0 +1,390 @@ +import { + productcontrollerCreateproduct, + productcontrollerGetcategoriesall, + productcontrollerGetcategoryattributes, +} from '@/servers/api/product'; +import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; +import { templatecontrollerRendertemplate } from '@/servers/api/template'; +import { PlusOutlined } from '@ant-design/icons'; +import { + ActionType, + DrawerForm, + ProForm, + ProFormDigit, + ProFormInstance, + ProFormList, + ProFormSelect, + ProFormText, + ProFormTextArea, +} from '@ant-design/pro-components'; +import { App, Button, Tag } from 'antd'; +import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem'; +import React, { useEffect, useRef, useState } from 'react'; + +const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1); + +const CreateForm: React.FC<{ + tableRef: React.MutableRefObject; +}> = ({ tableRef }) => { + // antd 的消息提醒 + const { message } = App.useApp(); + // 表单引用 + const formRef = useRef(); + const [stockStatus, setStockStatus] = useState< + 'in-stock' | 'out-of-stock' | null + >(null); + + const [categories, setCategories] = useState([]); + const [activeAttributes, setActiveAttributes] = useState([]); + + useEffect(() => { + productcontrollerGetcategoriesall().then((res: any) => { + setCategories(res?.data || []); + }); + }, []); + + const handleCategoryChange = async (categoryId: number) => { + if (!categoryId) { + setActiveAttributes([]); + return; + } + try { + const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); + setActiveAttributes(res?.data || []); + } catch (error) { + message.error('获取分类属性失败'); + } + }; + + /** + * @description 生成 SKU + */ + const handleGenerateSku = async () => { + try { + // 从表单引用中获取当前表单的值 + const formValues = formRef.current?.getFieldsValue(); + const { humidityValues, brandValues, strengthValues, flavorValues } = + formValues; + // 检查是否所有必需的字段都已选择 + // 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑 + // 暂时保持原样,假设常用属性会被配置 + + // 所选值(用于 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; + + 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 ( + + title="新建" + formRef={formRef} // Pass formRef + trigger={ + + } + autoFocusFirstInput + drawerProps={{ + destroyOnHidden: true, + }} + onValuesChange={async (changedValues) => { + // 当 Category 发生变化时 + if ('categoryId' in changedValues) { + handleCategoryChange(changedValues.categoryId); + } + + // 当 SKU 发生变化时 + if ('sku' in changedValues) { + const sku = changedValues.sku; + // 如果 sku 存在 + if (sku) { + // 获取库存信息 + const { data } = await getStocks({ + sku: sku, + } as any); + // 如果库存信息存在且不为空 + if (data && data.items && data.items.length > 0) { + // 设置在库状态 + setStockStatus('in-stock'); + // 设置产品类型为单品 + formRef.current?.setFieldsValue({ type: 'single' }); + } else { + // 设置未在库状态 + setStockStatus('out-of-stock'); + // 设置产品类型为套装 + formRef.current?.setFieldsValue({ type: 'bundle' }); + } + } else { + // 如果 sku 不存在,则重置状态 + setStockStatus(null); + formRef.current?.setFieldsValue({ type: null }); + } + } + }} + onFinish={async (values: any) => { + // 组装 attributes(根据 activeAttributes 动态组装) + const attributes = activeAttributes.flatMap((attr: any) => { + const dictName = attr.name; + const key = `${dictName}Values`; + const vals = values[key]; + if (vals && Array.isArray(vals)) { + return vals.map((v: string) => ({ + dictName: dictName, + name: v, + })); + } + return []; + }); + + const payload: any = { + name: (values as any).name, + description: (values as any).description, + shortDescription: (values as any).shortDescription, + sku: (values as any).sku, + price: (values as any).price, + promotionPrice: (values as any).promotionPrice, + attributes, + type: values.type, // 直接使用 type + components: values.components, + categoryId: values.categoryId, + siteSkus: values.siteSkus, + }; + const { success, message: errMsg } = + await productcontrollerCreateproduct(payload); + if (success) { + message.success('提交成功'); + tableRef.current?.reloadAndRest?.(); + return true; + } + message.error(errMsg); + return false; + }} + > + + + + + {stockStatus && ( + + {stockStatus === 'in-stock' ? '在库' : '未在库'} + + )} + + + + + + + + prevValues.type !== curValues.type + } + noStyle + > + {({ getFieldValue }: { getFieldValue: (name: string) => any }) => + getFieldValue('type') === 'bundle' ? ( + + + { + const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; + const { data } = await getStocks(params as any); + if (!data || !data.items) { + return []; + } + return data.items + .filter(item => item.sku) + .map(item => ({ + label: `${item.sku} - ${item.name}`, + value: item.sku, + })); + }} + /> + + + + ) : null + } + + + ({ label: c.title, value: c.id }))} + placeholder="请选择分类" + rules={[{ required: true, message: '请选择分类' }]} + /> + + {activeAttributes.map((attr: any) => ( + + ))} + + + + + + + + ); +}; + +export default CreateForm; diff --git a/src/pages/Product/List/EditForm.tsx b/src/pages/Product/List/EditForm.tsx new file mode 100644 index 0000000..04abe1b --- /dev/null +++ b/src/pages/Product/List/EditForm.tsx @@ -0,0 +1,356 @@ +import { + productcontrollerGetcategoriesall, + productcontrollerGetcategoryattributes, + productcontrollerGetproductcomponents, + productcontrollerUpdateproduct, +} from '@/servers/api/product'; +import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; +import { + ActionType, + DrawerForm, + ProForm, + ProFormInstance, + ProFormList, + ProFormSelect, + ProFormText, + ProFormTextArea, +} from '@ant-design/pro-components'; +import { App, Button, Tag } from 'antd'; +import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +const EditForm: React.FC<{ + record: API.Product; + tableRef: React.MutableRefObject; + trigger?: JSX.Element; +}> = ({ record, tableRef, trigger }) => { + const { message } = App.useApp(); + const formRef = useRef(); + const [components, setComponents] = useState< + { sku: string; quantity: number }[] + >([]); + const [type, setType] = useState<'single' | 'bundle' | null>(null); + const [stockStatus, setStockStatus] = useState< + 'in-stock' | 'out-of-stock' | null + >(null); + + const [categories, setCategories] = useState([]); + const [activeAttributes, setActiveAttributes] = useState([]); + + useEffect(() => { + productcontrollerGetcategoriesall().then((res: any) => { + setCategories(res?.data || []); + }); + }, []); + + useEffect(() => { + const categoryId = (record as any).categoryId || (record as any).category?.id; + if (categoryId) { + productcontrollerGetcategoryattributes({ id: categoryId }).then((res: any) => { + setActiveAttributes(res?.data || []); + }); + } else { + setActiveAttributes([]); + } + }, [record]); + + const handleCategoryChange = async (categoryId: number) => { + if (!categoryId) { + setActiveAttributes([]); + return; + } + try { + const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); + setActiveAttributes(res?.data || []); + } catch (error) { + message.error('获取分类属性失败'); + } + }; + + React.useEffect(() => { + (async () => { + const { data: stockData } = await getStocks({ + sku: record.sku, + } as any); + if (stockData && stockData.items && stockData.items.length > 0) { + // 如果有库存,则为单品 + setType('single'); + setStockStatus('in-stock'); + formRef.current?.setFieldsValue({ type: 'single' }); + } else { + // 如果没有库存,则为套装 + setType('bundle'); + setStockStatus('out-of-stock'); + formRef.current?.setFieldsValue({ type: 'bundle' }); + } + const { data: componentsData } = + await productcontrollerGetproductcomponents({ id: record.id }); + setComponents(componentsData || []); + })(); + }, [record]); + + 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] = []; + } + acc[key].push(cur.name); + } + return acc; + }, {} as any), + components: components, + type: type, + categoryId: (record as any).categoryId || (record as any).category?.id, + siteSkus: (record as any).siteSkus?.map((s: any) => s.code) || [], + }; + }, [record, components, type]); + + return ( + + title="编辑" + formRef={formRef} + trigger={trigger || } + autoFocusFirstInput + drawerProps={{ + destroyOnHidden: true, + }} + initialValues={initialValues} + onValuesChange={async (changedValues) => { + // 当 Category 发生变化时 + if ('categoryId' in changedValues) { + handleCategoryChange(changedValues.categoryId); + } + + // 当 SKU 发生变化时 + if ('sku' in changedValues) { + const sku = changedValues.sku; + // 如果 sku 存在 + if (sku) { + // 获取库存信息 + const { data } = await getStocks({ + sku: sku, + } as any); + // 如果库存信息存在且不为空 + if (data && data.items && data.items.length > 0) { + // 设置产品类型为单品 + formRef.current?.setFieldsValue({ type: 'single' }); + } else { + // 设置产品类型为套装 + formRef.current?.setFieldsValue({ type: 'bundle' }); + } + } else { + // 如果 sku 不存在,则重置状态 + formRef.current?.setFieldsValue({ type: null }); + } + } + }} + onFinish={async (values) => { + // 组装 attributes + const attributes = activeAttributes.flatMap((attr: any) => { + const dictName = attr.name; + const key = `${dictName}Values`; + const vals = values[key]; + if (vals && Array.isArray(vals)) { + return vals.map((v: string) => ({ + dictName: dictName, + name: v, + })); + } + return []; + }); + + const payload: any = { + name: (values as any).name, + description: (values as any).description, + shortDescription: (values as any).shortDescription, + sku: (values as any).sku, + price: (values as any).price, + promotionPrice: (values as any).promotionPrice, + attributes, + type: values.type, // 直接使用 type + categoryId: values.categoryId, + siteSkus: values.siteSkus, + // 连带更新 components + components: values.type === 'bundle' ? (values.components || []).map((c: any) => ({ + sku: c.sku, + quantity: Number(c.quantity), + })) : [], + }; + + const { success, message: errMsg } = + await productcontrollerUpdateproduct({ id: record.id }, payload); + + if (success) { + message.success('提交成功'); + tableRef.current?.reloadAndRest?.(); + return true; + } + message.error(errMsg); + return false; + }} + > + + + {stockStatus && ( + + {stockStatus === 'in-stock' ? '在库' : '未在库'} + + )} + + + + + + + + + prevValues.type !== curValues.type + } + noStyle + > + {({ getFieldValue }: { getFieldValue: (name: string) => any }) => + getFieldValue('type') === 'bundle' ? ( + ( +
+ {listDom} + {action} +
+ )} + > + + { + const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; + const { data } = await getStocks(params as any); + if (!data || !data.items) { + return []; + } + return data.items + .filter(item => item.sku) + .map(item => ({ + label: `${item.sku} - ${item.name}`, + value: item.sku, + })); + }} + /> + + +
+ ) : null + } +
+ + + + + + + ({ label: c.title, value: c.id }))} + placeholder="请选择分类" + rules={[{ required: true, message: '请选择分类' }]} + /> + + {activeAttributes.map((attr: any) => ( + + ))} + + + + + ); +}; + +export default EditForm; diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx index edef1e0..510afae 100644 --- a/src/pages/Product/List/index.tsx +++ b/src/pages/Product/List/index.tsx @@ -1,38 +1,24 @@ import { - productcontrollerCreateproduct, + productcontrollerBatchupdateproduct, productcontrollerDeleteproduct, + productcontrollerBatchdeleteproduct, productcontrollerGetcategoriesall, - productcontrollerGetcategoryattributes, productcontrollerGetproductcomponents, productcontrollerGetproductlist, - productcontrollerSetproductcomponents, productcontrollerUpdatenamecn, - productcontrollerUpdateproduct, } from '@/servers/api/product'; -import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock'; -import { templatecontrollerRendertemplate } from '@/servers/api/template'; -import { PlusOutlined } from '@ant-design/icons'; +import { sitecontrollerAll } from '@/servers/api/site'; import { - ActionType, - DrawerForm, - PageContainer, - ProColumns, - ProForm, - ProFormDigit, - ProFormInstance, - ProFormList, - ProFormSelect, - ProFormText, - ProFormTextArea, - ProTable, -} from '@ant-design/pro-components'; + wpproductcontrollerBatchSyncToSite, + wpproductcontrollerGetwpproducts, + wpproductcontrollerSyncToProduct, +} from '@/servers/api/wpProduct'; +import { ActionType, ModalForm, PageContainer, ProColumns, ProFormSelect, ProFormText, 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'; - -import React, { useEffect, useMemo, useRef, useState } from 'react'; -const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1); +import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; +import CreateForm from './CreateForm'; +import EditForm from './EditForm'; const NameCn: React.FC<{ id: number; @@ -108,10 +94,264 @@ const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => { ); }; +const BatchEditModal: React.FC<{ + visible: boolean; + onClose: () => void; + selectedRows: API.Product[]; + tableRef: React.MutableRefObject; + onSuccess: () => void; +}> = ({ visible, onClose, selectedRows, tableRef, onSuccess }) => { + const { message } = App.useApp(); + const [categories, setCategories] = useState([]); + + useEffect(() => { + if (visible) { + productcontrollerGetcategoriesall().then((res: any) => { + setCategories(res?.data || []); + }); + } + }, [visible]); + + return ( + !open && onClose()} + modalProps={{ destroyOnClose: true }} + onFinish={async (values) => { + const ids = selectedRows.map((row) => row.id); + const updateData: any = { ids }; + // 只有当用户输入了值才进行更新 + if (values.price) updateData.price = Number(values.price); + if (values.promotionPrice) updateData.promotionPrice = Number(values.promotionPrice); + if (values.categoryId) updateData.categoryId = values.categoryId; + + if (Object.keys(updateData).length <= 1) { + message.warning('未修改任何属性'); + return false; + } + + const { success, message: errMsg } = await productcontrollerBatchupdateproduct(updateData); + if (success) { + message.success('批量修改成功'); + onSuccess(); + tableRef.current?.reload(); + return true; + } else { + message.error(errMsg); + return false; + } + }} + > + + + ({ label: c.title, value: c.id }))} + placeholder="不修改请留空" + /> + + ); +}; + +const SyncToSiteModal: React.FC<{ + visible: boolean; + onClose: () => void; + productIds: number[]; + onSuccess: () => void; +}> = ({ visible, onClose, productIds, onSuccess }) => { + const { message } = App.useApp(); + const [sites, setSites] = useState([]); + + useEffect(() => { + if (visible) { + sitecontrollerAll().then((res: any) => { + setSites(res?.data || []); + }); + } + }, [visible]); + + return ( + !open && onClose()} + modalProps={{ destroyOnClose: true }} + onFinish={async (values) => { + if (!values.siteId) return false; + try { + await wpproductcontrollerBatchSyncToSite( + { siteId: values.siteId }, + { productIds } + ); + message.success('同步任务已提交'); + onSuccess(); + return true; + } catch (error: any) { + message.error(error.message || '同步失败'); + return false; + } + }} + > + ({ label: site.name, value: site.id }))} + rules={[{ required: true, message: '请选择站点' }]} + /> + + ); +}; + +const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTableRef: React.MutableRefObject }> = ({ + skus, + record, + parentTableRef, +}) => { + const actionRef = useRef(); + const { message } = App.useApp(); + + return ( + [ + , + ]} + request={async () => { + if (!skus || skus.length === 0) return { data: [] }; + const { data } = await wpproductcontrollerGetwpproducts( + { + skus, + pageSize: 100, + current: 1, + }, + { + paramsSerializer: (params: any) => { + const searchParams = new URLSearchParams(); + Object.keys(params).forEach((key) => { + const value = params[key]; + if (Array.isArray(value)) { + value.forEach((v) => searchParams.append(key, v)); + } else if (value !== undefined && value !== null) { + searchParams.append(key, value); + } + }); + return searchParams.toString(); + }, + }, + ); + return { + data: data?.items || [], + success: true, + }; + }} + columns={[ + { + title: '站点', + dataIndex: ['site', 'name'], + }, + { + title: 'SKU', + dataIndex: 'sku', + }, + { + title: '价格', + dataIndex: 'regular_price', + render: (_, row) => ( +
+
常规: {row.regular_price}
+
促销: {row.sale_price}
+
+ ), + }, + { + title: '状态', + dataIndex: 'status', + }, + { + title: '操作', + valueType: 'option', + render: (_, wpRow) => [ + { + try { + await wpproductcontrollerBatchSyncToSite( + { siteId: wpRow.siteId }, + { productIds: [record.id] }, + ); + message.success('同步到站点成功'); + actionRef.current?.reload(); + } catch (e: any) { + message.error(e.message || '同步失败'); + } + }} + > + 同步到站点 + , + { + try { + await wpproductcontrollerSyncToProduct({ id: wpRow.id }); + message.success('同步进商品成功'); + parentTableRef.current?.reload(); + } catch (e: any) { + message.error(e.message || '同步失败'); + } + }} + > + 同步进商品 + , + { + try { + await request(`/wp_product/${wpRow.id}`, { method: 'DELETE' }); + message.success('删除成功'); + actionRef.current?.reload(); + } catch (e: any) { + message.error(e.message || '删除失败'); + } + }} + > + 删除 + , + ], + }, + ]} + /> + ); +}; + const List: React.FC = () => { const actionRef = useRef(); // 状态:存储当前选中的行 const [selectedRows, setSelectedRows] = React.useState([]); + const [batchEditModalVisible, setBatchEditModalVisible] = useState(false); + const [syncModalVisible, setSyncModalVisible] = useState(false); + const [syncProductIds, setSyncProductIds] = useState([]); const { message } = App.useApp(); // 导出产品 CSV(带认证请求) @@ -144,6 +384,19 @@ const List: React.FC = () => { dataIndex: 'sku', sorter: true, }, + { + title: '商品SKU', + dataIndex: 'siteSkus', + render: (_, record) => ( + <> + {record.siteSkus?.map((item, index) => ( + + {item.code} + + ))} + + ), + }, { title: '名称', dataIndex: 'name', @@ -184,7 +437,7 @@ const List: React.FC = () => { hideInSearch: true, render: (_, record) => , }, - { + { title: '产品类型', dataIndex: 'type', valueType: 'select', @@ -235,10 +488,19 @@ const List: React.FC = () => { title: '操作', dataIndex: 'option', valueType: 'option', - fixed: true, + fixed: 'right', render: (_, record) => ( <> + { return ( + scroll={{ x: 'max-content' }} + headerTitle="查询表格" actionRef={actionRef} rowKey="id" toolBarRender={() => [ // 新建按钮 , + // 批量编辑按钮 + + , + // 批量同步按钮 + , + // 批量删除按钮 + , // 导出 CSV(后端返回 text/csv,直接新窗口下载) , // 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权) @@ -286,16 +592,49 @@ const List: React.FC = () => { const formData = new FormData(); formData.append('file', file); try { - await request('/product/import', { + const res = await request('/product/import', { method: 'POST', data: formData, requestType: 'form', }); - message.success('导入完成'); + + const { created = 0, updated = 0, errors = [] } = res.data || {}; + + if (errors && errors.length > 0) { + Modal.warning({ + title: '导入结果 (存在错误)', + width: 600, + content: ( +
+

创建成功: {created}

+

更新成功: {updated}

+

失败数量: {errors.length}

+
+ {errors.map((err: string, idx: number) => ( +
+ {idx + 1}. {err} +
+ ))} +
+
+ ), + }); + } else { + message.success(`导入成功: 创建 ${created}, 更新 ${updated}`); + } + onSuccess?.('ok'); actionRef.current?.reload(); } catch (error: any) { - message.error('导入失败'); + message.error('导入失败: ' + (error.message || '未知错误')); onError?.(error); } }} @@ -325,6 +664,17 @@ const List: React.FC = () => { }; }} columns={columns} + expandable={{ + expandedRowRender: (record) => ( + s.code) || []} + record={record} + parentTableRef={actionRef} + /> + ), + rowExpandable: (record) => + !!(record.siteSkus && record.siteSkus.length > 0), + }} editable={{ type: 'single', onSave: async (key, record, originRow) => { @@ -335,681 +685,28 @@ const List: React.FC = () => { onChange: (_, selectedRows) => setSelectedRows(selectedRows), }} /> + setBatchEditModalVisible(false)} + selectedRows={selectedRows} + tableRef={actionRef} + onSuccess={() => { + setBatchEditModalVisible(false); + setSelectedRows([]); + }} + /> + setSyncModalVisible(false)} + productIds={syncProductIds} + onSuccess={() => { + setSyncModalVisible(false); + setSelectedRows([]); + actionRef.current?.reload(); + }} + />
); }; -const CreateForm: React.FC<{ - tableRef: React.MutableRefObject; -}> = ({ tableRef }) => { - // antd 的消息提醒 - const { message } = App.useApp(); - // 表单引用 - const formRef = useRef(); - const [stockStatus, setStockStatus] = useState< - 'in-stock' | 'out-of-stock' | null - >(null); - - const [categories, setCategories] = useState([]); - const [activeAttributes, setActiveAttributes] = useState([]); - - useEffect(() => { - productcontrollerGetcategoriesall().then((res: any) => { - setCategories(res?.data || []); - }); - }, []); - - const handleCategoryChange = async (categoryId: number) => { - if (!categoryId) { - setActiveAttributes([]); - return; - } - try { - const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); - setActiveAttributes(res?.data || []); - } catch (error) { - message.error('获取分类属性失败'); - } - }; - - /** - * @description 生成 SKU - */ - const handleGenerateSku = async () => { - try { - // 从表单引用中获取当前表单的值 - const formValues = formRef.current?.getFieldsValue(); - const { humidityValues, brandValues, strengthValues, flavorValues } = - formValues; - // 检查是否所有必需的字段都已选择 - // 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑 - // 暂时保持原样,假设常用属性会被配置 - - // 所选值(用于 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; - - 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 ( - - title="新建" - formRef={formRef} // Pass formRef - trigger={ - - } - autoFocusFirstInput - drawerProps={{ - destroyOnHidden: true, - }} - onValuesChange={async (changedValues) => { - // 当 Category 发生变化时 - if ('categoryId' in changedValues) { - handleCategoryChange(changedValues.categoryId); - } - - // 当 SKU 发生变化时 - if ('sku' in changedValues) { - const sku = changedValues.sku; - // 如果 sku 存在 - if (sku) { - // 获取库存信息 - const { data } = await getStocks({ - sku: sku, - } as any); - // 如果库存信息存在且不为空 - if (data && data.items && data.items.length > 0) { - // 设置在库状态 - setStockStatus('in-stock'); - // 设置产品类型为单品 - formRef.current?.setFieldsValue({ type: 'single' }); - } else { - // 设置未在库状态 - setStockStatus('out-of-stock'); - // 设置产品类型为套装 - formRef.current?.setFieldsValue({ type: 'bundle' }); - } - } else { - // 如果 sku 不存在,则重置状态 - setStockStatus(null); - formRef.current?.setFieldsValue({ type: null }); - } - } - }} - onFinish={async (values: any) => { - // 组装 attributes(根据 activeAttributes 动态组装) - const attributes = activeAttributes.flatMap((attr: any) => { - const dictName = attr.name; - const key = `${dictName}Values`; - const vals = values[key]; - if (vals && Array.isArray(vals)) { - return vals.map((v: string) => ({ - dictName: dictName, - name: v, - })); - } - return []; - }); - - 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, - type: values.type, // 直接使用 type - components: values.components, - categoryId: values.categoryId, - }; - const { success, message: errMsg } = - await productcontrollerCreateproduct(payload); - if (success) { - message.success('提交成功'); - tableRef.current?.reloadAndRest?.(); - return true; - } - message.error(errMsg); - return false; - }} - > - - - - {stockStatus && ( - - {stockStatus === 'in-stock' ? '在库' : '未在库'} - - )} - - - - - - - - prevValues.type !== curValues.type - } - noStyle - > - {({ getFieldValue }: { getFieldValue: (name: string) => any }) => - getFieldValue('type') === 'bundle' ? ( - - - { - const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; - const { data } = await getStocks(params as any); - if (!data || !data.items) { - return []; - } - return data.items - .filter(item => item.sku) - .map(item => ({ - label: `${item.sku} - ${item.name}`, - value: item.sku, - })); - }} - /> - - - - ) : null - } - - - ({ label: c.title, value: c.id }))} - placeholder="请选择分类" - rules={[{ required: true, message: '请选择分类' }]} - /> - - {activeAttributes.map((attr: any) => ( - - ))} - - - - - - ); -}; - export default List; - -const EditForm: React.FC<{ - record: API.Product; - tableRef: React.MutableRefObject; -}> = ({ record, tableRef }) => { - const { message } = App.useApp(); - const formRef = useRef(); - const [components, setComponents] = useState< - { sku: string; quantity: number }[] - >([]); - const [type, setType] = useState<'single' | 'bundle' | null>(null); - const [stockStatus, setStockStatus] = useState< - 'in-stock' | 'out-of-stock' | null - >(null); - - const [categories, setCategories] = useState([]); - const [activeAttributes, setActiveAttributes] = useState([]); - - useEffect(() => { - productcontrollerGetcategoriesall().then((res: any) => { - setCategories(res?.data || []); - }); - }, []); - - useEffect(() => { - const categoryId = (record as any).categoryId || (record as any).category?.id; - if (categoryId) { - productcontrollerGetcategoryattributes({ id: categoryId }).then((res: any) => { - setActiveAttributes(res?.data || []); - }); - } else { - setActiveAttributes([]); - } - }, [record]); - - const handleCategoryChange = async (categoryId: number) => { - if (!categoryId) { - setActiveAttributes([]); - return; - } - try { - const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); - setActiveAttributes(res?.data || []); - } catch (error) { - message.error('获取分类属性失败'); - } - }; - - React.useEffect(() => { - (async () => { - const { data: stockData } = await getStocks({ - sku: record.sku, - } as any); - if (stockData && stockData.items && stockData.items.length > 0) { - // 如果有库存,则为单品 - setType('single'); - setStockStatus('in-stock'); - formRef.current?.setFieldsValue({ type: 'single' }); - } else { - // 如果没有库存,则为套装 - setType('bundle'); - setStockStatus('out-of-stock'); - formRef.current?.setFieldsValue({ type: 'bundle' }); - } - const { data: componentsData } = - await productcontrollerGetproductcomponents({ id: record.id }); - setComponents(componentsData || []); - })(); - }, [record]); - - 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] = []; - } - acc[key].push(cur.name); - } - return acc; - }, {} as any), - components: components, - type: type, - categoryId: (record as any).categoryId || (record as any).category?.id, - }; - }, [record, components, type]); - - return ( - - title="编辑" - formRef={formRef} - trigger={} - autoFocusFirstInput - drawerProps={{ - destroyOnHidden: true, - }} - initialValues={initialValues} - onValuesChange={async (changedValues) => { - // 当 Category 发生变化时 - if ('categoryId' in changedValues) { - handleCategoryChange(changedValues.categoryId); - } - - // 当 SKU 发生变化时 - if ('sku' in changedValues) { - const sku = changedValues.sku; - // 如果 sku 存在 - if (sku) { - // 获取库存信息 - const { data } = await getStocks({ - sku: sku, - } as any); - // 如果库存信息存在且不为空 - if (data && data.items && data.items.length > 0) { - // 设置产品类型为单品 - formRef.current?.setFieldsValue({ type: 'single' }); - } else { - // 设置产品类型为套装 - formRef.current?.setFieldsValue({ type: 'bundle' }); - } - } else { - // 如果 sku 不存在,则重置状态 - formRef.current?.setFieldsValue({ type: null }); - } - } - }} - onFinish={async (values) => { - // 组装 attributes - const attributes = activeAttributes.flatMap((attr: any) => { - const dictName = attr.name; - const key = `${dictName}Values`; - const vals = values[key]; - if (vals && Array.isArray(vals)) { - return vals.map((v: string) => ({ - dictName: dictName, - name: v, - })); - } - return []; - }); - - 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, - type: values.type, // 直接使用 type - categoryId: values.categoryId, - }; - - const { success, message: errMsg } = - await productcontrollerUpdateproduct({ id: record.id }, payload); - - if (values.type === 'bundle') { - const { success: success2, message: errMsg2 } = - await productcontrollerSetproductcomponents( - { id: record.id }, - { - components: (values.components || []).map((c: any) => ({ - sku: c.sku, - quantity: Number(c.quantity), - })), - } as any, - ); - if (!success2) { - message.error(errMsg2); - return false; - } - } - - if (success) { - message.success('提交成功'); - tableRef.current?.reloadAndRest?.(); - return true; - } - message.error(errMsg); - return false; - }} - > - - - {stockStatus && ( - - {stockStatus === 'in-stock' ? '在库' : '未在库'} - - )} - - - - - - - prevValues.type !== curValues.type - } - noStyle - > - {({ getFieldValue }: { getFieldValue: (name: string) => any }) => - getFieldValue('type') === 'bundle' ? ( - ( -
- {listDom} - {action} -
- )} - > - - { - const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; - const { data } = await getStocks(params as any); - if (!data || !data.items) { - return []; - } - return data.items - .filter(item => item.sku) - .map(item => ({ - label: `${item.sku} - ${item.name}`, - value: item.sku, - })); - }} - /> - - -
- ) : null - } -
- - - - - - - ({ label: c.title, value: c.id }))} - placeholder="请选择分类" - rules={[{ required: true, message: '请选择分类' }]} - /> - - {activeAttributes.map((attr: any) => ( - - ))} - - ); -}; diff --git a/src/pages/Product/Permutation/components/CreateModal.tsx b/src/pages/Product/Permutation/components/CreateModal.tsx new file mode 100644 index 0000000..126981d --- /dev/null +++ b/src/pages/Product/Permutation/components/CreateModal.tsx @@ -0,0 +1,157 @@ +import { productcontrollerCreateproduct } from '@/servers/api/product'; +import { templatecontrollerRendertemplate } from '@/servers/api/template'; +import { ModalForm, ProFormText } from '@ant-design/pro-components'; +import { App, Descriptions, Form, Tag } from 'antd'; +import React, { useEffect } from 'react'; + +interface CreateModalProps { + visible: boolean; + onClose: () => void; + onSuccess: () => void; + category: { id: number; name: string } | null; + permutation: Record; + attributes: any[]; // The attribute definitions +} + +const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1); + +const CreateModal: React.FC = ({ + visible, + onClose, + onSuccess, + category, + permutation, + attributes, +}) => { + const { message } = App.useApp(); + const [form] = Form.useForm(); + + // Helper to generate default name based on attributes + const generateDefaultName = () => { + if (!category) return ''; + const parts = [category.name]; + attributes.forEach(attr => { + const val = permutation[attr.name]; + if (val) parts.push(val.name); + }); + return parts.join(' - '); + }; + + useEffect(() => { + if (visible && permutation) { + const generateSku = async () => { + try { + // Extract values from permutation based on known keys + // Keys in permutation are dict names (e.g. 'brand', 'strength') + const brand = permutation['brand']?.name || ''; + const strength = permutation['strength']?.name || ''; + const flavor = permutation['flavor']?.name || ''; + const humidity = permutation['humidity']?.name || ''; + const model = permutation['model']?.name || ''; + + const variables = { + brand, + strength, + flavor, + model, + humidity: humidity ? capitalize(humidity) : '', + }; + + const { success, data: rendered } = await templatecontrollerRendertemplate( + { name: 'product.sku' }, + variables + ); + + if (success && rendered) { + form.setFieldValue('sku', rendered); + } + } catch (error) { + console.error('Failed to generate SKU', error); + } + }; + + generateSku(); + form.setFieldValue('name', generateDefaultName()); + } + }, [visible, permutation, category]); + + return ( + { + if (!category) return false; + + // Construct attributes payload + // Expected format: [{ dictName: 'Size', name: 'S' }, ...] + const payloadAttributes = attributes + .filter(attr => permutation[attr.name]) + .map(attr => ({ + dictName: attr.name, + name: permutation[attr.name].name, + })); + + const payload = { + name: values.name, + sku: values.sku, + categoryId: category.id, + attributes: payloadAttributes, + type: 'single', // Default to single + }; + + try { + const { success, message: errMsg } = await productcontrollerCreateproduct(payload as any); + if (success) { + message.success('产品创建成功'); + onSuccess(); + return true; + } else { + message.error(errMsg || '创建产品失败'); + return false; + } + } catch (error) { + message.error('发生错误'); + return false; + } + }} + > + + + {category?.name} + + + {attributes.map(attr => { + const val = permutation[attr.name]; + if (!val) return null; + return ( + + {attr.title || attr.name}: {val.name} + + ); + })} + + + + + + + + ); +}; + +export default CreateModal; diff --git a/src/pages/Product/Permutation/index.tsx b/src/pages/Product/Permutation/index.tsx new file mode 100644 index 0000000..033ccc9 --- /dev/null +++ b/src/pages/Product/Permutation/index.tsx @@ -0,0 +1,321 @@ +import { + productcontrollerCreateproduct, + productcontrollerGetcategoriesall, + productcontrollerGetcategoryattributes, + productcontrollerGetproductlist, +} from '@/servers/api/product'; +import { request } from '@umijs/max'; +import { + ActionType, + PageContainer, + ProCard, + ProColumns, + ProForm, + ProFormSelect, + ProTable, +} from '@ant-design/pro-components'; +import { Button, Table, Tag, message } from 'antd'; +import React, { useEffect, useState, useRef } from 'react'; +import CreateModal from './components/CreateModal'; +import EditForm from '../List/EditForm'; + +const PermutationPage: React.FC = () => { + const [categoryId, setCategoryId] = useState(); + const [attributes, setAttributes] = useState([]); + const [loading, setLoading] = useState(false); + const [attributeValues, setAttributeValues] = useState>({}); + const [permutations, setPermutations] = useState([]); + const [existingProducts, setExistingProducts] = useState>(new Map()); + const [productsLoading, setProductsLoading] = useState(false); + + const [createModalVisible, setCreateModalVisible] = useState(false); + const [selectedPermutation, setSelectedPermutation] = useState(null); + const [categories, setCategories] = useState([]); + const [form] = ProForm.useForm(); + + // Create a ref to mock ActionType for EditForm + const actionRef = useRef(); + + useEffect(() => { + productcontrollerGetcategoriesall().then((res) => { + const list = Array.isArray(res) ? res : (res?.data || []); + setCategories(list); + if (list.length > 0) { + setCategoryId(list[0].id); + form.setFieldValue('categoryId', list[0].id); + } + }); + }, []); + + const fetchProducts = async (catId: number) => { + setProductsLoading(true); + try { + const productRes = await productcontrollerGetproductlist({ + categoryId: catId, + pageSize: 2000, + current: 1 + }); + const products = productRes.data?.items || []; + + const productMap = new Map(); + products.forEach((p: any) => { + if (p.attributes && Array.isArray(p.attributes)) { + const key = generateAttributeKey(p.attributes); + if (key) productMap.set(key, p); + } + }); + setExistingProducts(productMap); + } catch (error) { + console.error(error); + message.error('获取现有产品失败'); + } finally { + setProductsLoading(false); + } + }; + + // Assign reload method to actionRef + useEffect(() => { + actionRef.current = { + reload: async () => { + if (categoryId) await fetchProducts(categoryId); + }, + reloadAndRest: async () => { + if (categoryId) await fetchProducts(categoryId); + }, + reset: () => {}, + clearSelected: () => {}, + } as any; + }, [categoryId]); + + // Fetch attributes and products when category changes + useEffect(() => { + if (!categoryId) { + setAttributes([]); + setAttributeValues({}); + setPermutations([]); + setExistingProducts(new Map()); + return; + } + + const fetchData = async () => { + setLoading(true); + try { + // 1. Fetch Attributes + const attrRes = await productcontrollerGetcategoryattributes({ id: categoryId }); + const attrs = Array.isArray(attrRes) ? attrRes : (attrRes?.data || []); + setAttributes(attrs); + + // 2. Fetch Attribute Values (Dict Items) + const valuesMap: Record = {}; + for (const attr of attrs) { + const dictId = attr.dict?.id || attr.dictId; + if (dictId) { + const itemsRes = await request('/dict/items', { params: { dictId } }); + valuesMap[attr.name] = itemsRes || []; + } + } + setAttributeValues(valuesMap); + + // 3. Fetch Existing Products + await fetchProducts(categoryId); + + } catch (error) { + console.error(error); + message.error('获取数据失败'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [categoryId]); + + // Generate Permutations when attributes or values change + useEffect(() => { + if (attributes.length === 0 || Object.keys(attributeValues).length === 0) { + setPermutations([]); + return; + } + + const validAttributes = attributes.filter(attr => attributeValues[attr.name]?.length > 0); + + if (validAttributes.length === 0) { + setPermutations([]); + return; + } + + const generateCombinations = (index: number, current: any): any[] => { + if (index === validAttributes.length) { + return [current]; + } + + const attr = validAttributes[index]; + const values = attributeValues[attr.name]; + let res: any[] = []; + + for (const val of values) { + res = res.concat(generateCombinations(index + 1, { ...current, [attr.name]: val })); + } + return res; + }; + + const combos = generateCombinations(0, {}); + setPermutations(combos); + + }, [attributes, attributeValues]); + + const generateAttributeKey = (attrs: any[]) => { + const parts = attrs.map(a => { + const key = a.dict?.name || a.dictName; + const val = a.name || a.value; + return `${key}:${val}`; + }); + return parts.sort().join('|'); + }; + + const generateKeyFromPermutation = (perm: any) => { + const parts = Object.keys(perm).map(attrName => { + const valItem = perm[attrName]; + const val = valItem.name; + return `${attrName}:${val}`; + }); + return parts.sort().join('|'); + }; + + const handleAdd = (record: any) => { + setSelectedPermutation(record); + setCreateModalVisible(true); + }; + + const columns: any[] = [ + ...attributes.map(attr => ({ + title: attr.title || attr.name, + dataIndex: attr.name, + width: 100, // Make columns narrower + render: (item: any) => item?.name || '-', + sorter: (a: any, b: any) => { + const valA = a[attr.name]?.name || ''; + const valB = b[attr.name]?.name || ''; + return valA.localeCompare(valB); + }, + filters: attributeValues[attr.name]?.map((v: any) => ({ text: v.name, value: v.name })), + onFilter: (value: any, record: any) => record[attr.name]?.name === value, + })), + { + title: '现有 SKU', + key: 'sku', + width: 150, + sorter: (a: any, b: any) => { + const keyA = generateKeyFromPermutation(a); + const productA = existingProducts.get(keyA); + const skuA = productA?.sku || ''; + + const keyB = generateKeyFromPermutation(b); + const productB = existingProducts.get(keyB); + const skuB = productB?.sku || ''; + + return skuA.localeCompare(skuB); + }, + filters: [ + { text: '已存在', value: 'exists' }, + { text: '未创建', value: 'missing' }, + ], + onFilter: (value: any, record: any) => { + const key = generateKeyFromPermutation(record); + const exists = existingProducts.has(key); + if (value === 'exists') return exists; + if (value === 'missing') return !exists; + return true; + }, + render: (_: any, record: any) => { + const key = generateKeyFromPermutation(record); + const product = existingProducts.get(key); + return product ? {product.sku} : '-'; + }, + }, + { + title: '操作', + key: 'action', + width: 100, + render: (_: any, record: any) => { + const key = generateKeyFromPermutation(record); + const product = existingProducts.get(key); + if (product) { + return ( + 编辑} + /> + ); + } + + return ( + + ); + }, + }, + ]; + + return ( + + + + ({ + label: item.name, + value: item.id, + }))} + fieldProps={{ + onChange: (val) => setCategoryId(val as number), + }} + /> + + + {categoryId && ( + generateKeyFromPermutation(record)} + pagination={{ + defaultPageSize: 50, + showSizeChanger: true, + pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'] + }} + scroll={{ x: 'max-content' }} + search={false} + toolBarRender={false} + /> + )} + + + {selectedPermutation && ( + setCreateModalVisible(false)} + onSuccess={() => { + setCreateModalVisible(false); + if (categoryId) fetchProducts(categoryId); + }} + category={categories.find(c => c.id === categoryId) || null} + permutation={selectedPermutation} + attributes={attributes} + /> + )} + + ); +}; + +export default PermutationPage; diff --git a/src/pages/Product/Sync/index.tsx b/src/pages/Product/Sync/index.tsx index 7a0fa5f..291516f 100644 --- a/src/pages/Product/Sync/index.tsx +++ b/src/pages/Product/Sync/index.tsx @@ -1,32 +1,41 @@ -import React, { useEffect, useState } from 'react'; -import { ProTable, ProColumns } from '@ant-design/pro-components'; -import { Card, Spin, Empty, message } from 'antd'; +import { ModalForm, ProFormText } from '@ant-design/pro-components'; +import { productcontrollerGetproductlist } from '@/servers/api/product'; +import { templatecontrollerGettemplatebyname } from '@/servers/api/template'; +import { EditOutlined, SyncOutlined } from '@ant-design/icons'; +import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; import { request } from '@umijs/max'; +import { Card, Spin, Tag, message, Button } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; +import EditForm from '../List/EditForm'; // 定义站点接口 interface Site { id: string; name: string; - prefix?: string; + skuPrefix?: string; isDisabled?: boolean; } // 定义WordPress商品接口 interface WpProduct { + id?: number; + externalProductId?: string; sku: string; name: string; price: string; - stockQuantity: number; + regular_price?: string; + sale_price?: string; + stock_quantity: number; + stockQuantity?: number; status: string; - attributes?: Record; + attributes?: any[]; + constitution?: { sku: string; quantity: number }[]; } -// 定义基础商品信息接口 -interface ProductBase { - sku: string; - name: string; - attributes: Record; +// 扩展本地产品接口,包含对应的 WP 产品信息 +interface ProductWithWP extends API.Product { wpProducts: Record; + attributes?: any[]; } // 定义API响应接口 @@ -41,14 +50,14 @@ const getSites = async (): Promise> => { const res = await request('/site/list', { method: 'GET', params: { - current: 1, - pageSize: 1000 - } + current: 1, + pageSize: 1000, + }, }); return { data: res.data?.items || [], success: res.success, - message: res.message + message: res.message, }; }; @@ -59,104 +68,190 @@ const getWPProducts = async (): Promise> => { }; const ProductSyncPage: React.FC = () => { - const [loading, setLoading] = useState(true); const [sites, setSites] = useState([]); - const [products, setProducts] = useState([]); - const [wpProducts, setWpProducts] = useState([]); + // 存储所有 WP 产品,用于查找匹配。 Key: SKU (包含前缀) + const [wpProductMap, setWpProductMap] = useState>( + new Map(), + ); + const [skuTemplate, setSkuTemplate] = useState(''); + const [initialLoading, setInitialLoading] = useState(true); + const actionRef = useRef(); - // 从 SKU 中去除站点前缀 - const removeSitePrefix = (sku: string, sitePrefixes: string[]): string => { - for (const prefix of sitePrefixes) { - if (prefix && sku.startsWith(prefix)) { - return sku.substring(prefix.length); - } - } - return sku; - }; - - // 初始化数据 + // 初始化数据:获取站点和所有 WP 产品 useEffect(() => { const fetchData = async () => { try { - setLoading(true); + setInitialLoading(true); // 获取所有站点 const sitesResponse = await getSites(); const rawSiteList = sitesResponse.data || []; // 过滤掉已禁用的站点 - const siteList: Site[] = rawSiteList.filter(site => !site.isDisabled); + const siteList: Site[] = rawSiteList.filter((site) => !site.isDisabled); setSites(siteList); // 获取所有 WordPress 商品 const wpProductsResponse = await getWPProducts(); const wpProductList: WpProduct[] = wpProductsResponse.data || []; - setWpProducts(wpProductList); - - // 提取所有站点前缀 - const sitePrefixes = siteList.map(site => site.prefix || ''); - - // 按基础 SKU 分组商品 - const productMap = new Map(); - - wpProductList.forEach((wpProduct: WpProduct) => { - // 去除前缀获取基础 SKU - const baseSku = removeSitePrefix(wpProduct.sku, sitePrefixes); - - if (!productMap.has(baseSku)) { - productMap.set(baseSku, { - sku: baseSku, - name: wpProduct.name, - attributes: wpProduct.attributes || {}, - wpProducts: {}, - }); - } - - // 查找对应的站点 - const site = siteList.find((s: Site) => s.prefix && wpProduct.sku.startsWith(s.prefix)); - if (site) { - const product = productMap.get(baseSku); - if (product) { - product.wpProducts[site.id] = wpProduct; + + // 构建 WP 产品 Map,Key 为 SKU + const map = new Map(); + wpProductList.forEach((p) => { + if (p.sku) { + map.set(p.sku, p); } - } }); + setWpProductMap(map); + + // 获取 SKU 模板 + try { + const templateRes = await templatecontrollerGettemplatebyname({ name: 'site.product.sku' }); + if (templateRes && templateRes.value) { + setSkuTemplate(templateRes.value); + } + } catch (e) { + console.log('Template site.product.sku not found, using default.'); + } - // 转换为数组 - setProducts(Array.from(productMap.values())); } catch (error) { - message.error('获取数据失败,请重试'); + message.error('获取基础数据失败,请重试'); console.error('Error fetching data:', error); } finally { - setLoading(false); + setInitialLoading(false); } }; fetchData(); }, []); + // 同步产品到站点 + const syncProductToSite = async (values: any, record: ProductWithWP, site: Site, wpProductId?: string) => { + try { + const hide = message.loading('正在同步...', 0); + const data = { + name: record.name, + sku: values.sku, + regular_price: record.price?.toString(), + sale_price: record.promotionPrice?.toString(), + type: record.type === 'bundle' ? 'simple' : record.type, + description: record.description, + status: 'publish', + stock_status: 'instock', + manage_stock: false, + }; + + let res; + if (wpProductId) { + res = await request(`/wp_product/siteId/${site.id}/products/${wpProductId}`, { + method: 'PUT', + data, + }); + } else { + res = await request(`/wp_product/siteId/${site.id}/products`, { + method: 'POST', + data, + }); + } + + console.log('res', res); + if (!res.success) { + hide(); + throw new Error(res.message || '同步失败'); + + } + // 更新本地缓存 Map,避免刷新 + setWpProductMap((prev) => { + const newMap = new Map(prev); + if (res.data && typeof res.data === 'object') { + newMap.set(values.sku, res.data as WpProduct); + } + return newMap; + }); + + hide(); + message.success('同步成功'); + return true; + } catch (error: any) { + message.error('同步失败: ' + (error.message || error.toString())); + return false; + }finally { + + } + }; + + // 简单的模板渲染函数 + const renderSku = (template: string, data: any) => { + if (!template) return ''; + // 支持 <%= it.path %> (Eta) 和 {{ path }} (Mustache/Handlebars) + return template.replace(/<%=\s*it\.([\w.]+)\s*%>|\{\{\s*([\w.]+)\s*\}\}/g, (_, p1, p2) => { + const path = p1 || p2; + const keys = path.split('.'); + let value = data; + for (const key of keys) { + value = value?.[key]; + } + return value === undefined || value === null ? '' : String(value); + }); + }; + // 生成表格列配置 - const generateColumns = (): ProColumns[] => { - const columns: ProColumns[] = [ + const generateColumns = (): ProColumns[] => { + const columns: ProColumns[] = [ { - title: '商品 SKU', + title: 'SKU', dataIndex: 'sku', key: 'sku', width: 150, fixed: 'left', + copyable: true, }, { title: '商品信息', - dataIndex: 'name', - key: 'name', - width: 200, + key: 'profile', + width: 300, fixed: 'left', - render: (dom: React.ReactNode, record: ProductBase) => ( + render: (_, record) => (
-
{dom}
-
- {Object.entries(record.attributes || {}) - .map(([key, value]) => `${key}: ${value}`) - .join(', ')} +
+
+ {record.name} +
+ } + />
+
+ + 价格: {record.price} + + {record.promotionPrice && ( + + 促销价: {record.promotionPrice} + + )} +
+ + {/* 属性 */} +
+ {record.attributes?.map((attr: any, idx: number) => ( + + {attr.dict?.name || attr.name}: {attr.name} + + ))} +
+ + {/* 组成 (如果是 Bundle) */} + {record.type === 'bundle' && record.components && record.components.length > 0 && ( +
+
Components:
+ {record.components.map((comp: any, idx: number) => ( +
+ {comp.sku} × {comp.quantity} +
+ ))} +
+ )}
), }, @@ -164,22 +259,92 @@ const ProductSyncPage: React.FC = () => { // 为每个站点生成列 sites.forEach((site: Site) => { - const siteColumn: ProColumns = { + const siteColumn: ProColumns = { title: site.name, key: `site_${site.id}`, hideInSearch: true, - width: 250, - render: (_, record: ProductBase) => { - const wpProduct = record.wpProducts[site.id]; + width: 220, + render: (_, record) => { + // 根据模板或默认规则生成期望的 SKU + const expectedSku = skuTemplate + ? renderSku(skuTemplate, { site, product: record }) + : `${site.skuPrefix || ''}-${record.sku}`; + + // 尝试用期望的 SKU 获取 WP 产品 + let wpProduct = wpProductMap.get(expectedSku); + + // 如果没找到,且没有模板(或者即使有模板),尝试回退到默认规则查找(以防万一) + if (!wpProduct && skuTemplate) { + const fallbackSku = `${site.skuPrefix || ''}-${record.sku}`; + wpProduct = wpProductMap.get(fallbackSku); + } + if (!wpProduct) { - return ; + return ( + }> + 同步到站点 + + } + width={400} + onFinish={async (values) => { + return await syncProductToSite(values, record, site); + }} + initialValues={{ + sku: skuTemplate + ? renderSku(skuTemplate, { site, product: record }) + : `${site.skuPrefix || ''}-${record.sku}` + }} + > + + + ); } return ( -
-
SKU: {wpProduct.sku}
-
价格: {wpProduct.price}
-
库存: {wpProduct.stockQuantity}
-
状态: {wpProduct.status === 'publish' ? '已发布' : '草稿'}
+
+
+
{wpProduct.sku}
+ }> + + } + width={400} + onFinish={async (values) => { + return await syncProductToSite(values, record, site, wpProduct.externalProductId); + }} + initialValues={{ + sku: wpProduct.sku + }} + > + +
+ 确定要将本地产品数据更新到站点吗? +
+
+
+
Price: {wpProduct.regular_price ?? wpProduct.price}
+ {wpProduct.sale_price && ( +
Sale: {wpProduct.sale_price}
+ )} +
Stock: {wpProduct.stock_quantity ?? wpProduct.stockQuantity}
+
+ Status: {wpProduct.status === 'publish' ? Published : {wpProduct.status}} +
); }, @@ -190,19 +355,42 @@ const ProductSyncPage: React.FC = () => { return columns; }; + if (initialLoading) { + return ( + + + + ) + } + return ( - {loading ? ( - - ) : ( - + columns={generateColumns()} - dataSource={products} - rowKey="sku" + actionRef={actionRef} + rowKey="id" + request={async (params, sort, filter) => { + // 调用本地获取产品列表 API + const { data, success } = await productcontrollerGetproductlist({ + ...params, + current: params.current, + pageSize: params.pageSize, + // 传递搜索参数 + keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段 + sku: (params as any).sku, + name: (params as any).name, + } as any); + + // 返回给 ProTable + return { + data: (data?.items || []) as ProductWithWP[], + success, + total: data?.total || 0, + }; + }} pagination={{ pageSize: 10, showSizeChanger: true, - showTotal: (total) => `共 ${total} 个商品`, }} scroll={{ x: 'max-content' }} search={{ @@ -210,14 +398,12 @@ const ProductSyncPage: React.FC = () => { }} options={{ density: true, + fullScreen: true, }} - locale={{ - emptyText: '暂无商品数据', - }} + dateFormatter="string" /> - )} ); }; -export default ProductSyncPage; \ No newline at end of file +export default ProductSyncPage; diff --git a/src/pages/Site/List/index.tsx b/src/pages/Site/List/index.tsx index 55dd006..b365ce0 100644 --- a/src/pages/Site/List/index.tsx +++ b/src/pages/Site/List/index.tsx @@ -19,20 +19,29 @@ interface AreaItem { name: string; } +// 仓库数据项类型 +interface StockPointItem { + id: number; + name: string; +} + // 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥) interface SiteItem { id: number; name: string; + description?: string; apiUrl?: string; type?: 'woocommerce' | 'shopyy'; skuPrefix?: string; isDisabled: number; areas?: AreaItem[]; + stockPoints?: StockPointItem[]; } // 创建/更新表单的值类型,包含可选的密钥字段 interface SiteFormValues { name: string; + description?: string; apiUrl?: string; type?: 'woocommerce' | 'shopyy'; isDisabled?: boolean; @@ -41,6 +50,7 @@ interface SiteFormValues { token?: string; // Shopyy token skuPrefix?: string; areas?: string[]; + stockPointIds?: number[]; } const SiteList: React.FC = () => { @@ -54,6 +64,7 @@ const SiteList: React.FC = () => { if (editing) { formRef.current?.setFieldsValue({ name: editing.name, + description: editing.description, apiUrl: editing.apiUrl, type: editing.type, skuPrefix: editing.skuPrefix, @@ -62,10 +73,12 @@ const SiteList: React.FC = () => { consumerSecret: undefined, token: undefined, areas: editing.areas?.map((area) => area.code) ?? [], + stockPointIds: editing.stockPoints?.map((sp) => sp.id) ?? [], }); } else { formRef.current?.setFieldsValue({ name: undefined, + description: undefined, apiUrl: undefined, type: 'woocommerce', skuPrefix: undefined, @@ -73,6 +86,8 @@ const SiteList: React.FC = () => { consumerKey: undefined, consumerSecret: undefined, token: undefined, + areas: [], + stockPointIds: [], }); } }, [open, editing]); @@ -87,6 +102,7 @@ const SiteList: React.FC = () => { hideInSearch: true, }, { title: '名称', dataIndex: 'name', width: 220 }, + { title: '描述', dataIndex: 'description', width: 220, hideInSearch: true }, { title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true }, { title: 'SKU 前缀', @@ -104,19 +120,37 @@ const SiteList: React.FC = () => { { label: 'Shopyy', value: 'shopyy' }, ], }, + // { + // title: '区域', + // dataIndex: 'areas', + // width: 200, + // hideInSearch: true, + // render: (_, row) => { + // if (!row.areas || row.areas.length === 0) { + // return 全球; + // } + // return ( + // + // {row.areas.map((area) => ( + // {area.name} + // ))} + // + // ); + // }, + // }, { - title: '区域', - dataIndex: 'areas', + title: '关联仓库', + dataIndex: 'stockPoints', width: 200, hideInSearch: true, render: (_, row) => { - if (!row.areas || row.areas.length === 0) { - return 全球; + if (!row.stockPoints || row.stockPoints.length === 0) { + return ; } return ( - {row.areas.map((area) => ( - {area.name} + {row.stockPoints.map((sp) => ( + {sp.name} ))} ); @@ -212,6 +246,7 @@ const SiteList: React.FC = () => { const payload: Record = { // 仅提交存在的字段,避免覆盖为 null/空 ...(values.name ? { name: values.name } : {}), + ...(values.description ? { description: values.description } : {}), ...(apiUrl ? { apiUrl: apiUrl } : {}), ...(values.type ? { type: values.type } : {}), ...(typeof values.isDisabled === 'boolean' @@ -219,6 +254,7 @@ const SiteList: React.FC = () => { : {}), ...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}), areas: values.areas ?? [], + stockPointIds: values.stockPointIds ?? [], }; if (isShopyy) { @@ -255,6 +291,7 @@ const SiteList: React.FC = () => { method: 'POST', data: { name: values.name, + description: values.description, apiUrl: apiUrl, type: values.type || 'woocommerce', consumerKey: isShopyy ? undefined : values.consumerKey, @@ -262,6 +299,7 @@ const SiteList: React.FC = () => { token: isShopyy ? values.token : undefined, skuPrefix: values.skuPrefix, areas: values.areas ?? [], + stockPointIds: values.stockPointIds ?? [], }, }); } @@ -316,8 +354,39 @@ const SiteList: React.FC = () => { placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} /> - {/* 区域选择 */} + + + + {/* 仓库选择 */} { + try { + const resp = await request('/stock/stock-point/all', { + method: 'GET', + }); + if (resp.success) { + return resp.data.map((item: any) => ({ + label: item.name, + value: item.id, + })); + } + return []; + } catch (e) { + return []; + } + }} + /> + + {/* 区域选择 - 暂时隐藏 */} + {/* { return []; } }} - /> + /> */} + {/* 平台类型选择 */} { + const { message } = App.useApp(); + const { siteId } = useParams<{ siteId: string }>(); + const [editing, setEditing] = useState(null); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const actionRef = useRef(); + + const handleDelete = async (id: number) => { + if (!siteId) return; + try { + const res = await request(`/site-api/${siteId}/customers/${id}`, { method: 'DELETE' }); + if (res.success) { + message.success('删除成功'); + actionRef.current?.reload(); + } else { + message.error(res.message || '删除失败'); + } + } catch (e) { + message.error('删除失败'); + } + }; + + const columns: ProColumns[] = [ + { + title: '头像', + dataIndex: 'avatar_url', + hideInSearch: true, + width: 80, + render: (_, record) => { + // 从raw数据中获取头像URL,因为DTO中没有这个字段 + const avatarUrl = record.raw?.avatar_url || record.avatar_url; + return } size="large" />; + }, + }, + { + title: '姓名', + dataIndex: 'username', + render: (_, record) => { + // DTO中有first_name和last_name字段,username可能从raw数据中获取 + const username = record.username || record.raw?.username || 'N/A'; + return ( +
+
{username}
+
+ {record.first_name} {record.last_name} +
+
+ ); + }, + }, + { + title: '邮箱', + dataIndex: 'email', + copyable: true, + }, + { + title: '角色', + dataIndex: 'role', + render: (_, record) => { + // 角色信息可能从raw数据中获取,因为DTO中没有这个字段 + const role = record.role || record.raw?.role || 'N/A'; + return {role}; + }, + }, + { + title: '账单地址', + dataIndex: 'billing', + hideInSearch: true, + render: (_, record) => { + const { billing } = record; + if (!billing) return '-'; + return ( +
+
{billing.address_1} {billing.address_2}
+
{billing.city}, {billing.state}, {billing.postcode}
+
{billing.country}
+
{billing.phone}
+
+ ); + }, + }, + { + title: '注册时间', + dataIndex: 'date_created', + valueType: 'dateTime', + hideInSearch: true, + }, + { + title: '操作', + valueType: 'option', + width: 120, + render: (_, record) => ( + +