refactor: 优化代码格式和导入顺序,移除未使用的导入和代码

fix: 修复JSON文件格式错误

feat(api): 添加产品相关API接口和类型定义

feat(product): 新增ERP产品绑定功能组件

style: 统一代码缩进和格式化

chore: 更新依赖和配置文件

perf: 优化字典数据导出功能

docs: 更新类型定义文件

test: 移除临时测试代码

ci: 更新CI配置

build: 调整构建配置
This commit is contained in:
tikkhun 2025-12-19 16:59:32 +08:00
parent 8bb082187b
commit 54fa1b7ca2
45 changed files with 34826 additions and 2645 deletions

View File

@ -44,8 +44,7 @@ export default defineConfig({
], ],
}, },
{
{
name: '地区管理', name: '地区管理',
path: '/area', path: '/area',
access: 'canSeeArea', access: 'canSeeArea',
@ -55,7 +54,7 @@ export default defineConfig({
path: '/area/list', path: '/area/list',
component: './Area/List', component: './Area/List',
}, },
{ {
name: '地区地图', name: '地区地图',
path: '/area/map', path: '/area/map',
component: './Area/Map', component: './Area/Map',
@ -77,18 +76,31 @@ export default defineConfig({
path: '/site/shop', path: '/site/shop',
component: './Site/Shop/Layout', component: './Site/Shop/Layout',
routes: [ routes: [
{ path: '/site/shop/:siteId/products', component: './Site/Shop/Products' }, {
{ path: '/site/shop/:siteId/orders', component: './Site/Shop/Orders' }, path: '/site/shop/:siteId/products',
{ path: '/site/shop/:siteId/subscriptions', component: './Site/Shop/Subscriptions' }, component: './Site/Shop/Products',
{ 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' }, 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产品列表',
path: '/site/woocommerce/product/list',
component: './Woo/Product/List',
}, },
{ {
name: 'Woo标签工具', name: 'Woo标签工具',
@ -97,7 +109,7 @@ export default defineConfig({
}, },
], ],
}, },
{ {
name: '客户管理', name: '客户管理',
path: '/customer', path: '/customer',
access: 'canSeeCustomer', access: 'canSeeCustomer',
@ -125,7 +137,7 @@ export default defineConfig({
component: './Product/Permutation', component: './Product/Permutation',
}, },
{ {
name: "产品分类", name: '产品分类',
path: '/product/category', path: '/product/category',
component: './Product/Category', component: './Product/Category',
}, },

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
// 运行时配置 // 运行时配置
import { GlobalOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { import {
ProLayoutProps, ProLayoutProps,
ProSchemaValueEnumObj, ProSchemaValueEnumObj,

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import FingerprintJS from '@fingerprintjs/fingerprintjs'; import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { useEffect, useState } from 'react';
/** /**
* Hook: 获取设备指纹(visitorId) * Hook: 获取设备指纹(visitorId)

View File

@ -1,11 +1,10 @@
import { import {
ActionType, ActionType,
DrawerForm, DrawerForm,
ProColumns, ProColumns,
ProFormInstance, ProFormInstance,
ProFormSelect, ProFormSelect,
ProTable ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { Button, message, Popconfirm, Space } from 'antd'; import { Button, message, Popconfirm, Space } from 'antd';
@ -181,7 +180,10 @@ const AreaList: React.FC = () => {
<ProFormSelect <ProFormSelect
name="code" name="code"
label="国家/地区" label="国家/地区"
options={countries.map(c => ({ label: `${c.name}(${c.code})`, value: c.code }))} options={countries.map((c) => ({
label: `${c.name}(${c.code})`,
value: c.code,
}))}
placeholder="请选择国家/地区" placeholder="请选择国家/地区"
rules={[{ required: true, message: '国家/地区为必填项' }]} rules={[{ required: true, message: '国家/地区为必填项' }]}
showSearch showSearch

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { Spin, message } from 'antd'; import { Spin, message } from 'antd';
import * as echarts from 'echarts/core'; import ReactECharts from 'echarts-for-react';
import { MapChart } from 'echarts/charts'; import { MapChart } from 'echarts/charts';
import { TooltipComponent, VisualMapComponent } from 'echarts/components'; import { TooltipComponent, VisualMapComponent } from 'echarts/components';
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers'; import { CanvasRenderer } from 'echarts/renderers';
import * as countries from 'i18n-iso-countries'; import * as countries from 'i18n-iso-countries';
import React, { useEffect, useState } from 'react';
// 注册 ECharts 组件 // 注册 ECharts 组件
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]); echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
@ -46,7 +46,7 @@ const AreaMap: React.FC = () => {
const savedAreas: AreaItem[] = areaResponse.data?.list || []; const savedAreas: AreaItem[] = areaResponse.data?.list || [];
// 3. 将后端数据转换为 ECharts 需要的格式 // 3. 将后端数据转换为 ECharts 需要的格式
const mapData = savedAreas.map(area => { const mapData = savedAreas.map((area) => {
let nameEn = countries.getName(area.code, 'en'); let nameEn = countries.getName(area.code, 'en');
return { return {
name: nameEn || area.code, name: nameEn || area.code,
@ -55,7 +55,6 @@ const AreaMap: React.FC = () => {
}; };
}); });
// 4. 配置 ECharts 地图选项 // 4. 配置 ECharts 地图选项
const mapOption = { const mapOption = {
tooltip: { tooltip: {
@ -108,12 +107,20 @@ const AreaMap: React.FC = () => {
}, []); }, []);
if (loading) { if (loading) {
return <Spin size="large" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }} />; return (
<Spin
size="large"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
/>
);
} }
return ( return (
<ReactECharts <ReactECharts
echarts={echarts} echarts={echarts}
option={option} option={option}

View File

@ -1,7 +1,14 @@
import { PlusOutlined } from '@ant-design/icons';
import { import {
PageContainer, productcontrollerCreatecategory,
} from '@ant-design/pro-components'; productcontrollerCreatecategoryattribute,
productcontrollerDeletecategory,
productcontrollerDeletecategoryattribute,
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerUpdatecategory,
} from '@/servers/api/product';
import { PlusOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { import {
Button, Button,
@ -16,15 +23,6 @@ import {
message, message,
} from 'antd'; } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import {
productcontrollerCreatecategory,
productcontrollerCreatecategoryattribute,
productcontrollerDeletecategory,
productcontrollerDeletecategoryattribute,
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerUpdatecategory,
} from '@/servers/api/product';
import { attributes } from '../Attribute/consts'; import { attributes } from '../Attribute/consts';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
@ -62,7 +60,9 @@ const CategoryPage: React.FC = () => {
const fetchCategoryAttributes = async (categoryId: number) => { const fetchCategoryAttributes = async (categoryId: number) => {
setLoadingAttributes(true); setLoadingAttributes(true);
try { try {
const res = await productcontrollerGetcategoryattributes({ categoryItemId: categoryId }); const res = await productcontrollerGetcategoryattributes({
categoryItemId: categoryId,
});
setCategoryAttributes(res || []); setCategoryAttributes(res || []);
} catch (error) { } catch (error) {
message.error('获取分类属性失败'); message.error('获取分类属性失败');
@ -81,7 +81,10 @@ const CategoryPage: React.FC = () => {
const handleCategorySubmit = async (values: any) => { const handleCategorySubmit = async (values: any) => {
try { try {
if (editingCategory) { if (editingCategory) {
await productcontrollerUpdatecategory({ id: editingCategory.id }, values); await productcontrollerUpdatecategory(
{ id: editingCategory.id },
values,
);
message.success('更新成功'); message.success('更新成功');
} else { } else {
await productcontrollerCreatecategory(values); await productcontrollerCreatecategory(values);
@ -113,7 +116,9 @@ const CategoryPage: React.FC = () => {
const res = await request('/dict/list'); const res = await request('/dict/list');
const filtered = (res || []).filter((d: any) => attributes.has(d.name)); const filtered = (res || []).filter((d: any) => attributes.has(d.name));
// Filter out already added attributes // Filter out already added attributes
const existingDictIds = new Set(categoryAttributes.map((ca: any) => ca.dict.id)); const existingDictIds = new Set(
categoryAttributes.map((ca: any) => ca.dict.id),
);
const available = filtered.filter((d: any) => !existingDictIds.has(d.id)); const available = filtered.filter((d: any) => !existingDictIds.has(d.id));
setAvailableDicts(available); setAvailableDicts(available);
setSelectedDictIds([]); setSelectedDictIds([]);
@ -154,8 +159,22 @@ const CategoryPage: React.FC = () => {
return ( return (
<PageContainer> <PageContainer>
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}> <Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0', padding: '16px' }}> <Sider
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> width={300}
style={{
background: '#fff',
borderRight: '1px solid #f0f0f0',
padding: '16px',
}}
>
<div
style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span style={{ fontWeight: 'bold' }}></span> <span style={{ fontWeight: 'bold' }}></span>
<Button <Button
type="primary" type="primary"
@ -175,10 +194,15 @@ const CategoryPage: React.FC = () => {
dataSource={categories} dataSource={categories}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
className={selectedCategory?.id === item.id ? 'ant-list-item-active' : ''} className={
selectedCategory?.id === item.id ? 'ant-list-item-active' : ''
}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
background: selectedCategory?.id === item.id ? '#e6f7ff' : 'transparent', background:
selectedCategory?.id === item.id
? '#e6f7ff'
: 'transparent',
padding: '8px 12px', padding: '8px 12px',
borderRadius: '4px', borderRadius: '4px',
}} }}
@ -204,21 +228,30 @@ const CategoryPage: React.FC = () => {
}} }}
onCancel={(e) => e?.stopPropagation()} onCancel={(e) => e?.stopPropagation()}
> >
<a onClick={(e) => e.stopPropagation()} style={{ color: 'red' }}></a> <a
onClick={(e) => e.stopPropagation()}
style={{ color: 'red' }}
>
</a>
</Popconfirm>, </Popconfirm>,
]} ]}
> >
<List.Item.Meta <List.Item.Meta title={item.title} description={item.name} />
title={item.title}
description={item.name}
/>
</List.Item> </List.Item>
)} )}
/> />
</Sider> </Sider>
<Content style={{ padding: '24px' }}> <Content style={{ padding: '24px' }}>
{selectedCategory ? ( {selectedCategory ? (
<Card title={`分类:${selectedCategory.title} (${selectedCategory.name})`} extra={<Button type="primary" onClick={handleAddAttribute}></Button>}> <Card
title={`分类:${selectedCategory.title} (${selectedCategory.name})`}
extra={
<Button type="primary" onClick={handleAddAttribute}>
</Button>
}
>
<List <List
loading={loadingAttributes} loading={loadingAttributes}
dataSource={categoryAttributes} dataSource={categoryAttributes}
@ -229,8 +262,10 @@ const CategoryPage: React.FC = () => {
title="确定移除该属性吗?" title="确定移除该属性吗?"
onConfirm={() => handleDeleteAttribute(item.id)} onConfirm={() => handleDeleteAttribute(item.id)}
> >
<Button type="link" danger></Button> <Button type="link" danger>
</Popconfirm>
</Button>
</Popconfirm>,
]} ]}
> >
<List.Item.Meta <List.Item.Meta
@ -242,7 +277,15 @@ const CategoryPage: React.FC = () => {
/> />
</Card> </Card>
) : ( ) : (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#999' }}> <div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
color: '#999',
}}
>
</div> </div>
)} )}
@ -255,11 +298,19 @@ const CategoryPage: React.FC = () => {
onOk={() => categoryForm.submit()} onOk={() => categoryForm.submit()}
onCancel={() => setIsCategoryModalVisible(false)} onCancel={() => setIsCategoryModalVisible(false)}
> >
<Form form={categoryForm} onFinish={handleCategorySubmit} layout="vertical"> <Form
form={categoryForm}
onFinish={handleCategorySubmit}
layout="vertical"
>
<Form.Item name="title" label="标题" rules={[{ required: true }]}> <Form.Item name="title" label="标题" rules={[{ required: true }]}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name="name" label="标识 (Code)" rules={[{ required: true }]}> <Form.Item
name="name"
label="标识 (Code)"
rules={[{ required: true }]}
>
<Input /> <Input />
</Form.Item> </Form.Item>
</Form> </Form>
@ -279,7 +330,10 @@ const CategoryPage: React.FC = () => {
placeholder="请选择要关联的属性" placeholder="请选择要关联的属性"
value={selectedDictIds} value={selectedDictIds}
onChange={setSelectedDictIds} onChange={setSelectedDictIds}
options={availableDicts.map(d => ({ label: d.title, value: d.id }))} options={availableDicts.map((d) => ({
label: d.title,
value: d.id,
}))}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -199,7 +199,7 @@ const ListPage: React.FC = () => {
return ( return (
<PageContainer ghost> <PageContainer ghost>
<ProTable <ProTable
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
headerTitle="查询表格" headerTitle="查询表格"
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"

View File

@ -1,5 +1,9 @@
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { ActionType, PageContainer, ProTable } from '@ant-design/pro-components'; import {
ActionType,
PageContainer,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { import {
Button, Button,
@ -98,14 +102,30 @@ const DictPage: React.FC = () => {
}; };
// 下载字典导入模板 // 下载字典导入模板
const handleDownloadDictTemplate = () => { const handleDownloadDictTemplate = async () => {
// 创建一个空的 a 标签用于下载 try {
const link = document.createElement('a'); // 使用 request 函数获取带认证的文件数据
link.href = '/dict/template'; // 指向后端的模板下载接口 const response = await request('/dict/template', {
link.setAttribute('download', 'dict_template.xlsx'); method: 'GET',
document.body.appendChild(link); responseType: 'blob', // 指定响应类型为 blob
link.click(); skipErrorHandler: true, // 跳过默认错误处理,自己处理错误
document.body.removeChild(link); });
// 创建 blob 对象和下载链接
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'dict_template.xlsx');
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error: any) {
message.error('下载字典模板失败:' + (error.message || '未知错误'));
}
}; };
// 添加字典项 // 添加字典项
@ -153,14 +173,72 @@ const DictPage: React.FC = () => {
} }
}; };
// 下载字典项导入模板 // 导出字典项数据
const handleDownloadDictItemTemplate = () => { const handleExportDictItems = async () => {
const link = document.createElement('a'); if (!selectedDict) {
link.href = '/dict/item/template'; message.warning('请先选择字典');
link.setAttribute('download', 'dict_item_template.xlsx'); return;
document.body.appendChild(link); }
link.click();
document.body.removeChild(link); try {
// 获取当前字典的所有数据
const response = await request('/dict/items', {
method: 'GET',
params: { dictId: selectedDict.id },
});
if (!response || response.length === 0) {
message.warning('当前字典没有数据可导出');
return;
}
// 将数据转换为CSV格式
const headers = [
'name',
'title',
'titleCN',
'value',
'sort',
'image',
'shortName',
];
const csvContent = [
headers.join(','),
...response.map((item: any) =>
headers
.map((header) => {
const value = item[header] || '';
// 如果值包含逗号或引号,需要转义
if (
typeof value === 'string' &&
(value.includes(',') || value.includes('"'))
) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
})
.join(','),
),
].join('\n');
// 创建blob并下载
const blob = new Blob(['\ufeff' + csvContent], {
// 添加BOM以支持中文
type: 'text/csv;charset=utf-8',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
message.success(`成功导出 ${response.length} 条数据`);
} catch (error: any) {
message.error('导出字典项失败:' + (error.message || '未知错误'));
}
}; };
// Effects // Effects
@ -176,7 +254,11 @@ const DictPage: React.FC = () => {
key: 'action', key: 'action',
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<Space size="small"> <Space size="small">
<Button type="link" size="small" onClick={() => handleEditDict(record)}> <Button
type="link"
size="small"
onClick={() => handleEditDict(record)}
>
</Button> </Button>
<Button <Button
@ -291,7 +373,7 @@ const DictPage: React.FC = () => {
</Button> </Button>
</Upload> </Upload>
<Button size="small" onClick={handleDownloadDictTemplate}> <Button size="small" onClick={handleDownloadDictTemplate}>
</Button> </Button>
</Space> </Space>
<Table <Table
@ -359,11 +441,11 @@ const DictPage: React.FC = () => {
</Button> </Button>
</Upload> </Upload>
<Button <Button
onClick={handleDownloadDictItemTemplate} onClick={handleExportDictItems}
disabled={!selectedDict} disabled={!selectedDict}
size="small" size="small"
> >
</Button> </Button>
</div> </div>
<ProTable <ProTable

View File

@ -119,7 +119,8 @@ const AttributePage: React.FC = () => {
dictId: selectedDict.id, dictId: selectedDict.id,
}, },
}); });
const exists = Array.isArray(list) && list.some((it: any) => it.id === itemId); const exists =
Array.isArray(list) && list.some((it: any) => it.id === itemId);
if (exists) { if (exists) {
message.error('删除失败'); message.error('删除失败');
} else { } else {
@ -147,7 +148,13 @@ const AttributePage: React.FC = () => {
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true }, { title: '标题', dataIndex: 'title', key: 'title', copyable: true },
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true }, { title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
{ title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true }, { title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true },
{ title: '图片', dataIndex: 'image', key: 'image', valueType: 'image', width: 80 }, {
title: '图片',
dataIndex: 'image',
key: 'image',
valueType: 'image',
width: 80,
},
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
@ -195,7 +202,13 @@ const AttributePage: React.FC = () => {
size="small" size="small"
/> />
</Space> </Space>
<div style={{ marginTop: '8px', overflowY: 'auto', height: 'calc(100vh - 150px)' }}> <div
style={{
marginTop: '8px',
overflowY: 'auto',
height: 'calc(100vh - 150px)',
}}
>
<Table <Table
dataSource={dicts} dataSource={dicts}
columns={dictColumns} columns={dictColumns}

View File

@ -1,7 +1,14 @@
import { PlusOutlined } from '@ant-design/icons';
import { import {
PageContainer, productcontrollerCreatecategory,
} from '@ant-design/pro-components'; productcontrollerCreatecategoryattribute,
productcontrollerDeletecategory,
productcontrollerDeletecategoryattribute,
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerUpdatecategory,
} from '@/servers/api/product';
import { PlusOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { import {
Button, Button,
@ -16,15 +23,6 @@ import {
message, message,
} from 'antd'; } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import {
productcontrollerCreatecategory,
productcontrollerCreatecategoryattribute,
productcontrollerDeletecategory,
productcontrollerDeletecategoryattribute,
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerUpdatecategory,
} from '@/servers/api/product';
import { attributes } from '../Attribute/consts'; import { attributes } from '../Attribute/consts';
const { Sider, Content } = Layout; const { Sider, Content } = Layout;
@ -62,7 +60,9 @@ const CategoryPage: React.FC = () => {
const fetchCategoryAttributes = async (categoryId: number) => { const fetchCategoryAttributes = async (categoryId: number) => {
setLoadingAttributes(true); setLoadingAttributes(true);
try { try {
const res = await productcontrollerGetcategoryattributes({ id: categoryId }); const res = await productcontrollerGetcategoryattributes({
id: categoryId,
});
setCategoryAttributes(res?.data || []); setCategoryAttributes(res?.data || []);
} catch (error) { } catch (error) {
message.error('获取分类属性失败'); message.error('获取分类属性失败');
@ -81,7 +81,10 @@ const CategoryPage: React.FC = () => {
const handleCategorySubmit = async (values: any) => { const handleCategorySubmit = async (values: any) => {
try { try {
if (editingCategory) { if (editingCategory) {
await productcontrollerUpdatecategory({ id: editingCategory.id }, values); await productcontrollerUpdatecategory(
{ id: editingCategory.id },
values,
);
message.success('更新成功'); message.success('更新成功');
} else { } else {
await productcontrollerCreatecategory(values); await productcontrollerCreatecategory(values);
@ -112,10 +115,12 @@ const CategoryPage: React.FC = () => {
try { try {
const res = await request('/dict/list'); const res = await request('/dict/list');
// Defensive check for response structure: handle both raw array and wrapped response // Defensive check for response structure: handle both raw array and wrapped response
const list = Array.isArray(res) ? res : (res?.data || []); const list = Array.isArray(res) ? res : res?.data || [];
const filtered = list.filter((d: any) => attributes.has(d.name)); const filtered = list.filter((d: any) => attributes.has(d.name));
// Filter out already added attributes // Filter out already added attributes
const existingDictIds = new Set(categoryAttributes.map((ca: any) => ca.dictId)); const existingDictIds = new Set(
categoryAttributes.map((ca: any) => ca.dictId),
);
const available = filtered.filter((d: any) => !existingDictIds.has(d.id)); const available = filtered.filter((d: any) => !existingDictIds.has(d.id));
setAvailableDicts(available); setAvailableDicts(available);
setSelectedDictIds([]); setSelectedDictIds([]);
@ -132,12 +137,14 @@ const CategoryPage: React.FC = () => {
} }
try { try {
// Loop through selected IDs and create attribute for each // Loop through selected IDs and create attribute for each
await Promise.all(selectedDictIds.map(dictId => await Promise.all(
productcontrollerCreatecategoryattribute({ selectedDictIds.map((dictId) =>
categoryId: selectedCategory.id, productcontrollerCreatecategoryattribute({
dictId: dictId, categoryId: selectedCategory.id,
}) dictId: dictId,
)); }),
),
);
message.success('添加属性成功'); message.success('添加属性成功');
setIsAttributeModalVisible(false); setIsAttributeModalVisible(false);
fetchCategoryAttributes(selectedCategory.id); fetchCategoryAttributes(selectedCategory.id);
@ -159,8 +166,22 @@ const CategoryPage: React.FC = () => {
return ( return (
<PageContainer> <PageContainer>
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}> <Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0', padding: '16px' }}> <Sider
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> width={300}
style={{
background: '#fff',
borderRight: '1px solid #f0f0f0',
padding: '16px',
}}
>
<div
style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span style={{ fontWeight: 'bold' }}></span> <span style={{ fontWeight: 'bold' }}></span>
<Button <Button
type="primary" type="primary"
@ -180,10 +201,15 @@ const CategoryPage: React.FC = () => {
dataSource={categories} dataSource={categories}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
className={selectedCategory?.id === item.id ? 'ant-list-item-active' : ''} className={
selectedCategory?.id === item.id ? 'ant-list-item-active' : ''
}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
background: selectedCategory?.id === item.id ? '#e6f7ff' : 'transparent', background:
selectedCategory?.id === item.id
? '#e6f7ff'
: 'transparent',
padding: '8px 12px', padding: '8px 12px',
borderRadius: '4px', borderRadius: '4px',
}} }}
@ -209,21 +235,30 @@ const CategoryPage: React.FC = () => {
}} }}
onCancel={(e) => e?.stopPropagation()} onCancel={(e) => e?.stopPropagation()}
> >
<a onClick={(e) => e.stopPropagation()} style={{ color: 'red' }}></a> <a
onClick={(e) => e.stopPropagation()}
style={{ color: 'red' }}
>
</a>
</Popconfirm>, </Popconfirm>,
]} ]}
> >
<List.Item.Meta <List.Item.Meta title={item.title} description={item.name} />
title={item.title}
description={item.name}
/>
</List.Item> </List.Item>
)} )}
/> />
</Sider> </Sider>
<Content style={{ padding: '24px' }}> <Content style={{ padding: '24px' }}>
{selectedCategory ? ( {selectedCategory ? (
<Card title={`分类:${selectedCategory.title} (${selectedCategory.name})`} extra={<Button type="primary" onClick={handleAddAttribute}></Button>}> <Card
title={`分类:${selectedCategory.title} (${selectedCategory.name})`}
extra={
<Button type="primary" onClick={handleAddAttribute}>
</Button>
}
>
<List <List
loading={loadingAttributes} loading={loadingAttributes}
dataSource={categoryAttributes} dataSource={categoryAttributes}
@ -234,8 +269,10 @@ const CategoryPage: React.FC = () => {
title="确定移除该属性吗?" title="确定移除该属性吗?"
onConfirm={() => handleDeleteAttribute(item.id)} onConfirm={() => handleDeleteAttribute(item.id)}
> >
<Button type="link" danger></Button> <Button type="link" danger>
</Popconfirm>
</Button>
</Popconfirm>,
]} ]}
> >
<List.Item.Meta <List.Item.Meta
@ -247,7 +284,15 @@ const CategoryPage: React.FC = () => {
/> />
</Card> </Card>
) : ( ) : (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#999' }}> <div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
color: '#999',
}}
>
</div> </div>
)} )}
@ -260,11 +305,19 @@ const CategoryPage: React.FC = () => {
onOk={() => categoryForm.submit()} onOk={() => categoryForm.submit()}
onCancel={() => setIsCategoryModalVisible(false)} onCancel={() => setIsCategoryModalVisible(false)}
> >
<Form form={categoryForm} onFinish={handleCategorySubmit} layout="vertical"> <Form
form={categoryForm}
onFinish={handleCategorySubmit}
layout="vertical"
>
<Form.Item name="title" label="标题" rules={[{ required: true }]}> <Form.Item name="title" label="标题" rules={[{ required: true }]}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name="name" label="标识 (Code)" rules={[{ required: true }]}> <Form.Item
name="name"
label="标识 (Code)"
rules={[{ required: true }]}
>
<Input /> <Input />
</Form.Item> </Form.Item>
</Form> </Form>
@ -284,7 +337,10 @@ const CategoryPage: React.FC = () => {
placeholder="请选择要关联的属性" placeholder="请选择要关联的属性"
value={selectedDictIds} value={selectedDictIds}
onChange={setSelectedDictIds} onChange={setSelectedDictIds}
options={availableDicts.map(d => ({ label: d.title, value: d.id }))} options={availableDicts.map((d) => ({
label: d.title,
value: d.id,
}))}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -1,3 +1,4 @@
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
import { import {
productcontrollerCreateproduct, productcontrollerCreateproduct,
productcontrollerGetcategoriesall, productcontrollerGetcategoriesall,
@ -18,7 +19,6 @@ import {
ProFormTextArea, ProFormTextArea,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button, Tag } from 'antd'; import { App, Button, Tag } from 'antd';
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1); const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
@ -49,7 +49,9 @@ const CreateForm: React.FC<{
return; return;
} }
try { try {
const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); const res: any = await productcontrollerGetcategoryattributes({
id: categoryId,
});
setActiveAttributes(res?.data || []); setActiveAttributes(res?.data || []);
} catch (error) { } catch (error) {
message.error('获取分类属性失败'); message.error('获取分类属性失败');
@ -135,8 +137,8 @@ const CreateForm: React.FC<{
humidityName === 'dry' humidityName === 'dry'
? 'Dry' ? 'Dry'
: humidityName === 'moisture' : humidityName === 'moisture'
? 'Moisture' ? 'Moisture'
: capitalize(humidityName), : capitalize(humidityName),
}, },
); );
if (!success) { if (!success) {
@ -310,14 +312,16 @@ const CreateForm: React.FC<{
placeholder="请输入子产品SKU" placeholder="请输入子产品SKU"
rules={[{ required: true, message: '请输入子产品SKU' }]} rules={[{ required: true, message: '请输入子产品SKU' }]}
request={async ({ keyWords }) => { request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; const params = keyWords
? { sku: keyWords, name: keyWords }
: { pageSize: 9999 };
const { data } = await getStocks(params as any); const { data } = await getStocks(params as any);
if (!data || !data.items) { if (!data || !data.items) {
return []; return [];
} }
return data.items return data.items
.filter(item => item.sku) .filter((item) => item.sku)
.map(item => ({ .map((item) => ({
label: `${item.sku} - ${item.name}`, label: `${item.sku} - ${item.name}`,
value: item.sku, value: item.sku,
})); }));
@ -341,7 +345,7 @@ const CreateForm: React.FC<{
name="categoryId" name="categoryId"
label="分类" label="分类"
width="md" width="md"
options={categories.map(c => ({ label: c.title, value: c.id }))} options={categories.map((c) => ({ label: c.title, value: c.id }))}
placeholder="请选择分类" placeholder="请选择分类"
rules={[{ required: true, message: '请选择分类' }]} rules={[{ required: true, message: '请选择分类' }]}
/> />
@ -372,17 +376,16 @@ const CreateForm: React.FC<{
/> />
<ProFormTextArea <ProFormTextArea
name="shortDescription" name="shortDescription"
style={{width: '100%'}} style={{ width: '100%' }}
label="产品简短描述" label="产品简短描述"
placeholder="请输入产品简短描述" placeholder="请输入产品简短描述"
/> />
<ProFormTextArea <ProFormTextArea
name="description" name="description"
style={{width: '100%'}} style={{ width: '100%' }}
label="产品描述" label="产品描述"
placeholder="请输入产品描述" placeholder="请输入产品描述"
/> />
</DrawerForm> </DrawerForm>
); );
}; };

View File

@ -1,3 +1,4 @@
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
import { import {
productcontrollerGetcategoriesall, productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes, productcontrollerGetcategoryattributes,
@ -18,7 +19,6 @@ import {
ProFormTextArea, ProFormTextArea,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button, Tag } from 'antd'; import { App, Button, Tag } from 'antd';
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
const EditForm: React.FC<{ const EditForm: React.FC<{
@ -35,7 +35,7 @@ const EditForm: React.FC<{
const [stockStatus, setStockStatus] = useState< const [stockStatus, setStockStatus] = useState<
'in-stock' | 'out-of-stock' | null 'in-stock' | 'out-of-stock' | null
>(null); >(null);
const [siteSkuEntries, setSiteSkuEntries] = useState<any[]>([]); const [siteSkuCodes, setSiteSkuCodes] = useState<string[]>([]);
const [sites, setSites] = useState<any[]>([]); const [sites, setSites] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]); const [categories, setCategories] = useState<any[]>([]);
@ -52,11 +52,14 @@ const EditForm: React.FC<{
}, []); }, []);
useEffect(() => { useEffect(() => {
const categoryId = (record as any).categoryId || (record as any).category?.id; const categoryId =
(record as any).categoryId || (record as any).category?.id;
if (categoryId) { if (categoryId) {
productcontrollerGetcategoryattributes({ id: categoryId }).then((res: any) => { productcontrollerGetcategoryattributes({ id: categoryId }).then(
setActiveAttributes(res?.data || []); (res: any) => {
}); setActiveAttributes(res?.data || []);
},
);
} else { } else {
setActiveAttributes([]); setActiveAttributes([]);
} }
@ -68,7 +71,9 @@ const EditForm: React.FC<{
return; return;
} }
try { try {
const res: any = await productcontrollerGetcategoryattributes({ id: categoryId }); const res: any = await productcontrollerGetcategoryattributes({
id: categoryId,
});
setActiveAttributes(res?.data || []); setActiveAttributes(res?.data || []);
} catch (error) { } catch (error) {
message.error('获取分类属性失败'); message.error('获取分类属性失败');
@ -95,8 +100,14 @@ const EditForm: React.FC<{
await productcontrollerGetproductcomponents({ id: record.id }); await productcontrollerGetproductcomponents({ id: record.id });
setComponents(componentsData || []); setComponents(componentsData || []);
// 获取站点SKU详细信息 // 获取站点SKU详细信息
const { data: siteSkusData } = await productcontrollerGetproductsiteskus({ id: record.id }); const { data: siteSkusData } = await productcontrollerGetproductsiteskus({
setSiteSkuEntries(siteSkusData || []); id: record.id,
});
// 只提取code字段组成字符串数组
const codes = siteSkusData
? siteSkusData.map((item: any) => item.code)
: [];
setSiteSkuCodes(codes);
})(); })();
}, [record]); }, [record]);
@ -117,10 +128,10 @@ const EditForm: React.FC<{
components: components, components: components,
type: type, type: type,
categoryId: (record as any).categoryId || (record as any).category?.id, categoryId: (record as any).categoryId || (record as any).category?.id,
// 初始化站点SKU列表为对象形式 // 初始化站点SKU为字符串数组
siteSkus: siteSkuEntries && siteSkuEntries.length ? siteSkuEntries : [], siteSkus: siteSkuCodes,
}; };
}, [record, components, type, siteSkuEntries]); }, [record, components, type, siteSkuCodes]);
return ( return (
<DrawerForm<any> <DrawerForm<any>
@ -186,12 +197,15 @@ const EditForm: React.FC<{
attributes, attributes,
type: values.type, // 直接使用 type type: values.type, // 直接使用 type
categoryId: values.categoryId, categoryId: values.categoryId,
siteSkus: values.siteSkus, siteSkus: values.siteSkus || [], // 直接传递字符串数组
// 连带更新 components // 连带更新 components
components: values.type === 'bundle' ? (values.components || []).map((c: any) => ({ components:
sku: c.sku, values.type === 'bundle'
quantity: Number(c.quantity), ? (values.components || []).map((c: any) => ({
})) : [], sku: c.sku,
quantity: Number(c.quantity),
}))
: [],
}; };
const { success, message: errMsg } = const { success, message: errMsg } =
@ -223,43 +237,35 @@ const EditForm: React.FC<{
</Tag> </Tag>
)} )}
</ProForm.Group> </ProForm.Group>
<ProFormList <ProFormList
name="siteSkus" name="siteSkus"
label="站点SKU" label="站点SKU"
creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增站点SKU' }} creatorButtonProps={{
itemRender={({ listDom, action }) => ( position: 'bottom',
<div style={{ marginBottom: 8, display: 'flex', flexDirection: 'row', alignItems: 'end' }}> creatorButtonText: '新增站点SKU',
{listDom} }}
{action} itemRender={({ listDom, action }) => (
</div> <div
)} style={{
> marginBottom: 8,
<ProForm.Group> display: 'flex',
<ProFormSelect flexDirection: 'row',
name="siteId" alignItems: 'end',
label="站点" }}
width="md" >
options={sites.map((site) => ({ label: site.name, value: site.id }))} {listDom}
placeholder="请选择站点" {action}
rules={[{ required: true, message: '请选择站点' }]} </div>
/> )}
<ProFormText >
name="code" <ProFormText
label="站点SKU" name="code"
width="md" width="md"
placeholder="请输入站点SKU" placeholder="请输入站点SKU"
rules={[{ required: true, message: '请输入站点SKU' }]} rules={[{ required: true, message: '请输入站点SKU' }]}
/> />
<ProFormText </ProFormList>
name="quantity"
label="数量"
width="md"
placeholder="请输入数量"
/>
</ProForm.Group>
</ProFormList>
<ProForm.Group> <ProForm.Group>
<ProFormText <ProFormText
name="name" name="name"
label="名称" label="名称"
@ -316,14 +322,16 @@ const EditForm: React.FC<{
placeholder="请输入库存SKU" placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]} rules={[{ required: true, message: '请输入库存SKU' }]}
request={async ({ keyWords }) => { request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 }; const params = keyWords
? { sku: keyWords, name: keyWords }
: { pageSize: 9999 };
const { data } = await getStocks(params as any); const { data } = await getStocks(params as any);
if (!data || !data.items) { if (!data || !data.items) {
return []; return [];
} }
return data.items return data.items
.filter(item => item.sku) .filter((item) => item.sku)
.map(item => ({ .map((item) => ({
label: `${item.sku} - ${item.name}`, label: `${item.sku} - ${item.name}`,
value: item.sku, value: item.sku,
})); }));
@ -362,7 +370,7 @@ const EditForm: React.FC<{
name="categoryId" name="categoryId"
label="分类" label="分类"
width="md" width="md"
options={categories.map(c => ({ label: c.title, value: c.id }))} options={categories.map((c) => ({ label: c.title, value: c.id }))}
placeholder="请选择分类" placeholder="请选择分类"
rules={[{ required: true, message: '请选择分类' }]} rules={[{ required: true, message: '请选择分类' }]}
/> />
@ -388,7 +396,6 @@ const EditForm: React.FC<{
label="产品描述" label="产品描述"
placeholder="请输入产品描述" placeholder="请输入产品描述"
/> />
</DrawerForm> </DrawerForm>
); );
}; };

View File

@ -1,21 +1,28 @@
import { import {
productcontrollerBatchupdateproduct,
productcontrollerDeleteproduct,
productcontrollerBatchdeleteproduct, productcontrollerBatchdeleteproduct,
productcontrollerBatchupdateproduct,
productcontrollerBindproductsiteskus,
productcontrollerDeleteproduct,
productcontrollerGetcategoriesall, productcontrollerGetcategoriesall,
productcontrollerGetproductcomponents, productcontrollerGetproductcomponents,
productcontrollerGetproductlist, productcontrollerGetproductlist,
productcontrollerUpdatenamecn, productcontrollerUpdatenamecn,
productcontrollerBindproductsiteskus,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
import { import {
wpproductcontrollerBatchsynctosite, wpproductcontrollerBatchsynctosite,
wpproductcontrollerGetwpproducts,
wpproductcontrollerSynctoproduct, wpproductcontrollerSynctoproduct,
} from '@/servers/api/wpProduct'; } from '@/servers/api/wpProduct';
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi'; import {
import { ActionType, ModalForm, PageContainer, ProColumns, ProFormSelect, ProFormText, ProTable } from '@ant-design/pro-components'; ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormSelect,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd'; import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
@ -125,15 +132,17 @@ const BatchEditModal: React.FC<{
const updateData: any = { ids }; const updateData: any = { ids };
// 只有当用户输入了值才进行更新 // 只有当用户输入了值才进行更新
if (values.price) updateData.price = Number(values.price); if (values.price) updateData.price = Number(values.price);
if (values.promotionPrice) updateData.promotionPrice = Number(values.promotionPrice); if (values.promotionPrice)
updateData.promotionPrice = Number(values.promotionPrice);
if (values.categoryId) updateData.categoryId = values.categoryId; if (values.categoryId) updateData.categoryId = values.categoryId;
if (Object.keys(updateData).length <= 1) { if (Object.keys(updateData).length <= 1) {
message.warning('未修改任何属性'); message.warning('未修改任何属性');
return false; return false;
} }
const { success, message: errMsg } = await productcontrollerBatchupdateproduct(updateData); const { success, message: errMsg } =
await productcontrollerBatchupdateproduct(updateData);
if (success) { if (success) {
message.success('批量修改成功'); message.success('批量修改成功');
onSuccess(); onSuccess();
@ -145,11 +154,7 @@ const BatchEditModal: React.FC<{
} }
}} }}
> >
<ProFormText <ProFormText name="price" label="价格" placeholder="不修改请留空" />
name="price"
label="价格"
placeholder="不修改请留空"
/>
<ProFormText <ProFormText
name="promotionPrice" name="promotionPrice"
label="促销价格" label="促销价格"
@ -158,7 +163,7 @@ const BatchEditModal: React.FC<{
<ProFormSelect <ProFormSelect
name="categoryId" name="categoryId"
label="分类" label="分类"
options={categories.map(c => ({ label: c.title, value: c.id }))} options={categories.map((c) => ({ label: c.title, value: c.id }))}
placeholder="不修改请留空" placeholder="不修改请留空"
/> />
</ModalForm> </ModalForm>
@ -211,7 +216,7 @@ const SyncToSiteModal: React.FC<{
try { try {
await wpproductcontrollerBatchsynctosite( await wpproductcontrollerBatchsynctosite(
{ siteId: values.siteId }, { siteId: values.siteId },
{ productIds } { productIds },
); );
const map = values.productSiteSkus || {}; const map = values.productSiteSkus || {};
for (const currentProductId of productIds) { for (const currentProductId of productIds) {
@ -219,7 +224,15 @@ const SyncToSiteModal: React.FC<{
if (entry && entry.code) { if (entry && entry.code) {
await productcontrollerBindproductsiteskus( await productcontrollerBindproductsiteskus(
{ id: currentProductId }, { id: currentProductId },
{ siteSkus: [{ siteId: values.siteId, code: entry.code, quantity: entry.quantity }] } {
siteSkus: [
{
siteId: values.siteId,
code: entry.code,
quantity: entry.quantity,
},
],
},
); );
} }
} }
@ -239,7 +252,10 @@ const SyncToSiteModal: React.FC<{
rules={[{ required: true, message: '请选择站点' }]} rules={[{ required: true, message: '请选择站点' }]}
/> />
{productRows.map((row) => ( {productRows.map((row) => (
<div key={row.id} style={{ display: 'flex', gap: 12, alignItems: 'flex-end' }}> <div
key={row.id}
style={{ display: 'flex', gap: 12, alignItems: 'flex-end' }}
>
<div style={{ minWidth: 220 }}>SKU: {row.sku || '-'}</div> <div style={{ minWidth: 220 }}>SKU: {row.sku || '-'}</div>
<ProFormText <ProFormText
name={['productSiteSkus', row.id, 'code']} name={['productSiteSkus', row.id, 'code']}
@ -257,11 +273,11 @@ const SyncToSiteModal: React.FC<{
); );
}; };
const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTableRef: React.MutableRefObject<ActionType | undefined> }> = ({ const WpProductInfo: React.FC<{
skus, skus: string[];
record, record: API.Product;
parentTableRef, parentTableRef: React.MutableRefObject<ActionType | undefined>;
}) => { }> = ({ skus, record, parentTableRef }) => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const { message } = App.useApp(); const { message } = App.useApp();
@ -295,12 +311,13 @@ const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTable
// 遍历每一个SKU在当前站点进行搜索 // 遍历每一个SKU在当前站点进行搜索
for (const skuCode of skus) { for (const skuCode of skus) {
// 直接调用站点API根据搜索关键字获取产品列表 // 直接调用站点API根据搜索关键字获取产品列表
const { data: productPage } = await siteapicontrollerGetproducts({ const response = await siteapicontrollerGetproducts({
siteId: siteItem.id, siteId: Number(siteItem.id),
per_page: 100, per_page: 100,
search: skuCode, search: skuCode,
}); });
const siteProducts = productPage?.items || []; const productPage = response as any;
const siteProducts = productPage?.data?.items || [];
// 将站点信息附加到产品数据中便于展示 // 将站点信息附加到产品数据中便于展示
siteProducts.forEach((p: any) => { siteProducts.forEach((p: any) => {
aggregatedProducts.push({ aggregatedProducts.push({
@ -382,7 +399,9 @@ const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTable
description="确认删除?" description="确认删除?"
onConfirm={async () => { onConfirm={async () => {
try { try {
await request(`/wp_product/${wpRow.id}`, { method: 'DELETE' }); await request(`/wp_product/${wpRow.id}`, {
method: 'DELETE',
});
message.success('删除成功'); message.success('删除成功');
actionRef.current?.reload(); actionRef.current?.reload();
} catch (e: any) { } catch (e: any) {
@ -443,9 +462,9 @@ const List: React.FC = () => {
dataIndex: 'siteSkus', dataIndex: 'siteSkus',
render: (_, record) => ( render: (_, record) => (
<> <>
{record.siteSkus?.map((item, index) => ( {record.siteSkus?.map((code, index) => (
<Tag key={index} color="cyan"> <Tag key={index} color="cyan">
{item.code} {code}
</Tag> </Tag>
))} ))}
</> </>
@ -471,7 +490,7 @@ const List: React.FC = () => {
dataIndex: 'category', dataIndex: 'category',
render: (_, record: any) => { render: (_, record: any) => {
return record.category?.title || record.category?.name || '-'; return record.category?.title || record.category?.name || '-';
} },
}, },
{ {
title: '价格', title: '价格',
@ -584,7 +603,6 @@ const List: React.FC = () => {
<PageContainer header={{ title: '产品列表' }}> <PageContainer header={{ title: '产品列表' }}>
<ProTable<API.Product> <ProTable<API.Product>
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
headerTitle="查询表格" headerTitle="查询表格"
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"
@ -592,8 +610,12 @@ const List: React.FC = () => {
// 新建按钮 // 新建按钮
<CreateForm tableRef={actionRef} />, <CreateForm tableRef={actionRef} />,
// 批量编辑按钮 // 批量编辑按钮
<Button disabled={selectedRows.length <= 0} onClick={() => setBatchEditModalVisible(true)}></Button> <Button
, disabled={selectedRows.length <= 0}
onClick={() => setBatchEditModalVisible(true)}
>
</Button>,
// 批量同步按钮 // 批量同步按钮
<Button <Button
disabled={selectedRows.length <= 0} disabled={selectedRows.length <= 0}
@ -614,9 +636,10 @@ const List: React.FC = () => {
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`, content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
onOk: async () => { onOk: async () => {
try { try {
const { success, message: errMsg } = await productcontrollerBatchdeleteproduct({ const { success, message: errMsg } =
ids: selectedRows.map((row) => row.id), await productcontrollerBatchdeleteproduct({
}); ids: selectedRows.map((row) => row.id),
});
if (success) { if (success) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRows([]); setSelectedRows([]);
@ -652,7 +675,11 @@ const List: React.FC = () => {
requestType: 'form', requestType: 'form',
}); });
const { created = 0, updated = 0, errors = [] } = res.data || {}; const {
created = 0,
updated = 0,
errors = [],
} = res.data || {};
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
Modal.warning({ Modal.warning({
@ -662,18 +689,31 @@ const List: React.FC = () => {
<div> <div>
<p>: {created}</p> <p>: {created}</p>
<p>: {updated}</p> <p>: {updated}</p>
<p style={{ color: 'red', fontWeight: 'bold' }}>: {errors.length}</p> <p style={{ color: 'red', fontWeight: 'bold' }}>
<div style={{ : {errors.length}
maxHeight: '300px', </p>
overflowY: 'auto', <div
background: '#f5f5f5', style={{
padding: '8px', maxHeight: '300px',
marginTop: '8px', overflowY: 'auto',
borderRadius: '4px', background: '#f5f5f5',
border: '1px solid #d9d9d9' padding: '8px',
}}> marginTop: '8px',
borderRadius: '4px',
border: '1px solid #d9d9d9',
}}
>
{errors.map((err: string, idx: number) => ( {errors.map((err: string, idx: number) => (
<div key={idx} style={{ fontSize: '12px', marginBottom: '4px', borderBottom: '1px solid #e8e8e8', paddingBottom: '2px', color: '#ff4d4f' }}> <div
key={idx}
style={{
fontSize: '12px',
marginBottom: '4px',
borderBottom: '1px solid #e8e8e8',
paddingBottom: '2px',
color: '#ff4d4f',
}}
>
{idx + 1}. {err} {idx + 1}. {err}
</div> </div>
))} ))}
@ -721,7 +761,7 @@ const List: React.FC = () => {
expandable={{ expandable={{
expandedRowRender: (record) => ( expandedRowRender: (record) => (
<WpProductInfo <WpProductInfo
skus={record.siteSkus?.map((s) => s.code) || []} skus={(record.siteSkus as string[]) || []}
record={record} record={record}
parentTableRef={actionRef} parentTableRef={actionRef}
/> />

View File

@ -28,13 +28,13 @@ const CreateModal: React.FC<CreateModalProps> = ({
// Helper to generate default name based on attributes // Helper to generate default name based on attributes
const generateDefaultName = () => { const generateDefaultName = () => {
if (!category) return ''; if (!category) return '';
const parts = [category.name]; const parts = [category.name];
attributes.forEach(attr => { attributes.forEach((attr) => {
const val = permutation[attr.name]; const val = permutation[attr.name];
if (val) parts.push(val.name); if (val) parts.push(val.name);
}); });
return parts.join(' - '); return parts.join(' - ');
}; };
useEffect(() => { useEffect(() => {
@ -57,10 +57,11 @@ const CreateModal: React.FC<CreateModalProps> = ({
humidity: humidity ? capitalize(humidity) : '', humidity: humidity ? capitalize(humidity) : '',
}; };
const { success, data: rendered } = await templatecontrollerRendertemplate( const { success, data: rendered } =
{ name: 'product.sku' }, await templatecontrollerRendertemplate(
variables { name: 'product.sku' },
); variables,
);
if (success && rendered) { if (success && rendered) {
form.setFieldValue('sku', rendered); form.setFieldValue('sku', rendered);
@ -90,8 +91,8 @@ const CreateModal: React.FC<CreateModalProps> = ({
// Construct attributes payload // Construct attributes payload
// Expected format: [{ dictName: 'Size', name: 'S' }, ...] // Expected format: [{ dictName: 'Size', name: 'S' }, ...]
const payloadAttributes = attributes const payloadAttributes = attributes
.filter(attr => permutation[attr.name]) .filter((attr) => permutation[attr.name])
.map(attr => ({ .map((attr) => ({
dictName: attr.name, dictName: attr.name,
name: permutation[attr.name].name, name: permutation[attr.name].name,
})); }));
@ -105,7 +106,8 @@ const CreateModal: React.FC<CreateModalProps> = ({
}; };
try { try {
const { success, message: errMsg } = await productcontrollerCreateproduct(payload as any); const { success, message: errMsg } =
await productcontrollerCreateproduct(payload as any);
if (success) { if (success) {
message.success('产品创建成功'); message.success('产品创建成功');
onSuccess(); onSuccess();
@ -120,12 +122,15 @@ const CreateModal: React.FC<CreateModalProps> = ({
} }
}} }}
> >
<Descriptions column={1} bordered size="small" style={{ marginBottom: 24 }}> <Descriptions
<Descriptions.Item label="分类"> column={1}
{category?.name} bordered
</Descriptions.Item> size="small"
style={{ marginBottom: 24 }}
>
<Descriptions.Item label="分类">{category?.name}</Descriptions.Item>
<Descriptions.Item label="属性"> <Descriptions.Item label="属性">
{attributes.map(attr => { {attributes.map((attr) => {
const val = permutation[attr.name]; const val = permutation[attr.name];
if (!val) return null; if (!val) return null;
return ( return (

View File

@ -1,31 +1,33 @@
import { import {
productcontrollerCreateproduct,
productcontrollerGetcategoriesall, productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes, productcontrollerGetcategoryattributes,
productcontrollerGetproductlist, productcontrollerGetproductlist,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { request } from '@umijs/max';
import { import {
ActionType, ActionType,
PageContainer, PageContainer,
ProCard, ProCard,
ProColumns,
ProForm, ProForm,
ProFormSelect, ProFormSelect,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Table, Tag, message } from 'antd'; import { request } from '@umijs/max';
import React, { useEffect, useState, useRef } from 'react'; import { Button, Tag, message } from 'antd';
import CreateModal from './components/CreateModal'; import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm'; import EditForm from '../List/EditForm';
import CreateModal from './components/CreateModal';
const PermutationPage: React.FC = () => { const PermutationPage: React.FC = () => {
const [categoryId, setCategoryId] = useState<number>(); const [categoryId, setCategoryId] = useState<number>();
const [attributes, setAttributes] = useState<any[]>([]); const [attributes, setAttributes] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [attributeValues, setAttributeValues] = useState<Record<string, any[]>>({}); const [attributeValues, setAttributeValues] = useState<Record<string, any[]>>(
{},
);
const [permutations, setPermutations] = useState<any[]>([]); const [permutations, setPermutations] = useState<any[]>([]);
const [existingProducts, setExistingProducts] = useState<Map<string, API.Product>>(new Map()); const [existingProducts, setExistingProducts] = useState<
Map<string, API.Product>
>(new Map());
const [productsLoading, setProductsLoading] = useState(false); const [productsLoading, setProductsLoading] = useState(false);
const [createModalVisible, setCreateModalVisible] = useState(false); const [createModalVisible, setCreateModalVisible] = useState(false);
@ -38,7 +40,7 @@ const PermutationPage: React.FC = () => {
useEffect(() => { useEffect(() => {
productcontrollerGetcategoriesall().then((res) => { productcontrollerGetcategoriesall().then((res) => {
const list = Array.isArray(res) ? res : (res?.data || []); const list = Array.isArray(res) ? res : res?.data || [];
setCategories(list); setCategories(list);
if (list.length > 0) { if (list.length > 0) {
setCategoryId(list[0].id); setCategoryId(list[0].id);
@ -53,15 +55,15 @@ const PermutationPage: React.FC = () => {
const productRes = await productcontrollerGetproductlist({ const productRes = await productcontrollerGetproductlist({
categoryId: catId, categoryId: catId,
pageSize: 2000, pageSize: 2000,
current: 1 current: 1,
}); });
const products = productRes.data?.items || []; const products = productRes.data?.items || [];
const productMap = new Map<string, API.Product>(); const productMap = new Map<string, API.Product>();
products.forEach((p: any) => { products.forEach((p: any) => {
if (p.attributes && Array.isArray(p.attributes)) { if (p.attributes && Array.isArray(p.attributes)) {
const key = generateAttributeKey(p.attributes); const key = generateAttributeKey(p.attributes);
if (key) productMap.set(key, p); if (key) productMap.set(key, p);
} }
}); });
setExistingProducts(productMap); setExistingProducts(productMap);
@ -101,8 +103,10 @@ const PermutationPage: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
// 1. Fetch Attributes // 1. Fetch Attributes
const attrRes = await productcontrollerGetcategoryattributes({ id: categoryId }); const attrRes = await productcontrollerGetcategoryattributes({
const attrs = Array.isArray(attrRes) ? attrRes : (attrRes?.data || []); id: categoryId,
});
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
setAttributes(attrs); setAttributes(attrs);
// 2. Fetch Attribute Values (Dict Items) // 2. Fetch Attribute Values (Dict Items)
@ -110,15 +114,16 @@ const PermutationPage: React.FC = () => {
for (const attr of attrs) { for (const attr of attrs) {
const dictId = attr.dict?.id || attr.dictId; const dictId = attr.dict?.id || attr.dictId;
if (dictId) { if (dictId) {
const itemsRes = await request('/dict/items', { params: { dictId } }); const itemsRes = await request('/dict/items', {
valuesMap[attr.name] = itemsRes || []; params: { dictId },
});
valuesMap[attr.name] = itemsRes || [];
} }
} }
setAttributeValues(valuesMap); setAttributeValues(valuesMap);
// 3. Fetch Existing Products // 3. Fetch Existing Products
await fetchProducts(categoryId); await fetchProducts(categoryId);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
message.error('获取数据失败'); message.error('获取数据失败');
@ -137,11 +142,13 @@ const PermutationPage: React.FC = () => {
return; return;
} }
const validAttributes = attributes.filter(attr => attributeValues[attr.name]?.length > 0); const validAttributes = attributes.filter(
(attr) => attributeValues[attr.name]?.length > 0,
);
if (validAttributes.length === 0) { if (validAttributes.length === 0) {
setPermutations([]); setPermutations([]);
return; return;
} }
const generateCombinations = (index: number, current: any): any[] => { const generateCombinations = (index: number, current: any): any[] => {
@ -154,41 +161,42 @@ const PermutationPage: React.FC = () => {
let res: any[] = []; let res: any[] = [];
for (const val of values) { for (const val of values) {
res = res.concat(generateCombinations(index + 1, { ...current, [attr.name]: val })); res = res.concat(
generateCombinations(index + 1, { ...current, [attr.name]: val }),
);
} }
return res; return res;
}; };
const combos = generateCombinations(0, {}); const combos = generateCombinations(0, {});
setPermutations(combos); setPermutations(combos);
}, [attributes, attributeValues]); }, [attributes, attributeValues]);
const generateAttributeKey = (attrs: any[]) => { const generateAttributeKey = (attrs: any[]) => {
const parts = attrs.map(a => { const parts = attrs.map((a) => {
const key = a.dict?.name || a.dictName; const key = a.dict?.name || a.dictName;
const val = a.name || a.value; const val = a.name || a.value;
return `${key}:${val}`; return `${key}:${val}`;
}); });
return parts.sort().join('|'); return parts.sort().join('|');
}; };
const generateKeyFromPermutation = (perm: any) => { const generateKeyFromPermutation = (perm: any) => {
const parts = Object.keys(perm).map(attrName => { const parts = Object.keys(perm).map((attrName) => {
const valItem = perm[attrName]; const valItem = perm[attrName];
const val = valItem.name; const val = valItem.name;
return `${attrName}:${val}`; return `${attrName}:${val}`;
}); });
return parts.sort().join('|'); return parts.sort().join('|');
}; };
const handleAdd = (record: any) => { const handleAdd = (record: any) => {
setSelectedPermutation(record); setSelectedPermutation(record);
setCreateModalVisible(true); setCreateModalVisible(true);
}; };
const columns: any[] = [ const columns: any[] = [
...attributes.map(attr => ({ ...attributes.map((attr) => ({
title: attr.title || attr.name, title: attr.title || attr.name,
dataIndex: attr.name, dataIndex: attr.name,
width: 100, // Make columns narrower width: 100, // Make columns narrower
@ -198,7 +206,10 @@ const PermutationPage: React.FC = () => {
const valB = b[attr.name]?.name || ''; const valB = b[attr.name]?.name || '';
return valA.localeCompare(valB); return valA.localeCompare(valB);
}, },
filters: attributeValues[attr.name]?.map((v: any) => ({ text: v.name, value: v.name })), filters: attributeValues[attr.name]?.map((v: any) => ({
text: v.name,
value: v.name,
})),
onFilter: (value: any, record: any) => record[attr.name]?.name === value, onFilter: (value: any, record: any) => record[attr.name]?.name === value,
})), })),
{ {
@ -241,13 +252,17 @@ const PermutationPage: React.FC = () => {
const key = generateKeyFromPermutation(record); const key = generateKeyFromPermutation(record);
const product = existingProducts.get(key); const product = existingProducts.get(key);
if (product) { if (product) {
return ( return (
<EditForm <EditForm
record={product} record={product}
tableRef={actionRef} tableRef={actionRef}
trigger={<Button type="link" size="small"></Button>} trigger={
/> <Button type="link" size="small">
);
</Button>
}
/>
);
} }
return ( return (
@ -273,8 +288,8 @@ const PermutationPage: React.FC = () => {
label="选择分类" label="选择分类"
width="md" width="md"
options={categories.map((item: any) => ({ options={categories.map((item: any) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
fieldProps={{ fieldProps={{
onChange: (val) => setCategoryId(val as number), onChange: (val) => setCategoryId(val as number),
@ -283,35 +298,35 @@ const PermutationPage: React.FC = () => {
</ProForm> </ProForm>
{categoryId && ( {categoryId && (
<ProTable <ProTable
size="small" size="small"
dataSource={permutations} dataSource={permutations}
columns={columns} columns={columns}
loading={loading || productsLoading} loading={loading || productsLoading}
rowKey={(record) => generateKeyFromPermutation(record)} rowKey={(record) => generateKeyFromPermutation(record)}
pagination={{ pagination={{
defaultPageSize: 50, defaultPageSize: 50,
showSizeChanger: true, showSizeChanger: true,
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'] pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
}} }}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
search={false} search={false}
toolBarRender={false} toolBarRender={false}
/> />
)} )}
</ProCard> </ProCard>
{selectedPermutation && ( {selectedPermutation && (
<CreateModal <CreateModal
visible={createModalVisible} visible={createModalVisible}
onClose={() => setCreateModalVisible(false)} onClose={() => setCreateModalVisible(false)}
onSuccess={() => { onSuccess={() => {
setCreateModalVisible(false); setCreateModalVisible(false);
if (categoryId) fetchProducts(categoryId); if (categoryId) fetchProducts(categoryId);
}} }}
category={categories.find(c => c.id === categoryId) || null} category={categories.find((c) => c.id === categoryId) || null}
permutation={selectedPermutation} permutation={selectedPermutation}
attributes={attributes} attributes={attributes}
/> />
)} )}
</PageContainer> </PageContainer>

View File

@ -1,10 +1,15 @@
import { ModalForm, ProFormText } from '@ant-design/pro-components';
import { productcontrollerGetproductlist } from '@/servers/api/product'; import { productcontrollerGetproductlist } from '@/servers/api/product';
import { templatecontrollerGettemplatebyname } from '@/servers/api/template'; import { templatecontrollerGettemplatebyname } from '@/servers/api/template';
import { EditOutlined, SyncOutlined } from '@ant-design/icons'; import { EditOutlined, SyncOutlined } from '@ant-design/icons';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; import {
ActionType,
ModalForm,
ProColumns,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { Card, Spin, Tag, message, Button } from 'antd'; import { Button, Card, Spin, Tag, message, Select, Progress, Modal } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm'; import EditForm from '../List/EditForm';
@ -36,6 +41,10 @@ interface WpProduct {
interface ProductWithWP extends API.Product { interface ProductWithWP extends API.Product {
wpProducts: Record<string, WpProduct>; wpProducts: Record<string, WpProduct>;
attributes?: any[]; attributes?: any[];
siteSkus?: Array<{
siteSku: string;
[key: string]: any;
}>;
} }
// 定义API响应接口 // 定义API响应接口
@ -76,6 +85,11 @@ const ProductSyncPage: React.FC = () => {
const [skuTemplate, setSkuTemplate] = useState<string>(''); const [skuTemplate, setSkuTemplate] = useState<string>('');
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const [selectedSiteId, setSelectedSiteId] = useState<string>('');
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
const [syncProgress, setSyncProgress] = useState(0);
const [syncing, setSyncing] = useState(false);
const [syncResults, setSyncResults] = useState<{ success: number; failed: number; errors: string[] }>({ success: 0, failed: 0, errors: [] });
// 初始化数据:获取站点和所有 WP 产品 // 初始化数据:获取站点和所有 WP 产品
useEffect(() => { useEffect(() => {
@ -96,22 +110,23 @@ const ProductSyncPage: React.FC = () => {
// 构建 WP 产品 MapKey 为 SKU // 构建 WP 产品 MapKey 为 SKU
const map = new Map<string, WpProduct>(); const map = new Map<string, WpProduct>();
wpProductList.forEach((p) => { wpProductList.forEach((p) => {
if (p.sku) { if (p.sku) {
map.set(p.sku, p); map.set(p.sku, p);
} }
}); });
setWpProductMap(map); setWpProductMap(map);
// 获取 SKU 模板 // 获取 SKU 模板
try { try {
const templateRes = await templatecontrollerGettemplatebyname({ name: 'site.product.sku' }); const templateRes = await templatecontrollerGettemplatebyname({
name: 'site.product.sku',
});
if (templateRes && templateRes.value) { if (templateRes && templateRes.value) {
setSkuTemplate(templateRes.value); setSkuTemplate(templateRes.value);
} }
} catch (e) { } catch (e) {
console.log('Template site.product.sku not found, using default.'); console.log('Template site.product.sku not found, using default.');
} }
} catch (error) { } catch (error) {
message.error('获取基础数据失败,请重试'); message.error('获取基础数据失败,请重试');
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
@ -124,7 +139,12 @@ const ProductSyncPage: React.FC = () => {
}, []); }, []);
// 同步产品到站点 // 同步产品到站点
const syncProductToSite = async (values: any, record: ProductWithWP, site: Site, wpProductId?: string) => { const syncProductToSite = async (
values: any,
record: ProductWithWP,
site: Site,
wpProductId?: string,
) => {
try { try {
const hide = message.loading('正在同步...', 0); const hide = message.loading('正在同步...', 0);
const data = { const data = {
@ -141,14 +161,14 @@ const ProductSyncPage: React.FC = () => {
let res; let res;
if (wpProductId) { if (wpProductId) {
res = await request(`/wp_product/siteId/${site.id}/products/${wpProductId}`, { res = await request(`/site-api/${site.id}/products/${wpProductId}`, {
method: 'PUT', method: 'PUT',
data, data,
}); });
} else { } else {
res = await request(`/wp_product/siteId/${site.id}/products`, { res = await request(`/site-api/${site.id}/products`, {
method: 'POST', method: 'POST',
data, data,
}); });
} }
@ -156,16 +176,15 @@ const ProductSyncPage: React.FC = () => {
if (!res.success) { if (!res.success) {
hide(); hide();
throw new Error(res.message || '同步失败'); throw new Error(res.message || '同步失败');
} }
// 更新本地缓存 Map避免刷新 // 更新本地缓存 Map避免刷新
setWpProductMap((prev) => { setWpProductMap((prev) => {
const newMap = new Map(prev); const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') { if (res.data && typeof res.data === 'object') {
newMap.set(values.sku, res.data as WpProduct); newMap.set(values.sku, res.data as WpProduct);
} }
return newMap; return newMap;
}); });
hide(); hide();
message.success('同步成功'); message.success('同步成功');
@ -173,8 +192,139 @@ const ProductSyncPage: React.FC = () => {
} catch (error: any) { } catch (error: any) {
message.error('同步失败: ' + (error.message || error.toString())); message.error('同步失败: ' + (error.message || error.toString()));
return false; return false;
}finally { } finally {
}
};
// 批量同步产品到指定站点
const batchSyncProducts = async () => {
if (!selectedSiteId) {
message.error('请选择要同步到的站点');
return;
}
const targetSite = sites.find(site => site.id === selectedSiteId);
if (!targetSite) {
message.error('选择的站点不存在');
return;
}
setSyncing(true);
setSyncProgress(0);
setSyncResults({ success: 0, failed: 0, errors: [] });
try {
// 获取所有产品
const { data, success } = await productcontrollerGetproductlist({
current: 1,
pageSize: 10000, // 获取所有产品
} as any);
if (!success || !data?.items) {
message.error('获取产品列表失败');
return;
}
const products = data.items as ProductWithWP[];
const totalProducts = products.length;
let processed = 0;
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
// 逐个同步产品
for (const product of products) {
try {
// 获取该产品在目标站点的SKU
let siteProductSku = '';
if (product.siteSkus && product.siteSkus.length > 0) {
const siteSkuInfo = product.siteSkus.find((sku: any) => {
return sku.siteSku && sku.siteSku.includes(targetSite.skuPrefix || targetSite.name);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
}
}
// 如果没有找到实际的siteSku则根据模板生成
const expectedSku = siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site: targetSite, product })
: `${targetSite.skuPrefix || ''}-${product.sku}`
);
// 检查是否已存在
const existingProduct = wpProductMap.get(expectedSku);
// 准备同步数据
const syncData = {
name: product.name,
sku: expectedSku,
regular_price: product.price?.toString(),
sale_price: product.promotionPrice?.toString(),
type: product.type === 'bundle' ? 'simple' : product.type,
description: product.description,
status: 'publish',
stock_status: 'instock',
manage_stock: false,
};
let res;
if (existingProduct?.externalProductId) {
// 更新现有产品
res = await request(`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, {
method: 'PUT',
data: syncData,
});
} else {
// 创建新产品
res = await request(`/site-api/${targetSite.id}/products`, {
method: 'POST',
data: syncData,
});
}
console.log('res', res);
if (res.success) {
successCount++;
// 更新本地缓存
setWpProductMap((prev) => {
const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') {
newMap.set(expectedSku, res.data as WpProduct);
}
return newMap;
});
} else {
failedCount++;
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
}
} catch (error: any) {
failedCount++;
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
}
processed++;
setSyncProgress(Math.round((processed / totalProducts) * 100));
}
setSyncResults({ success: successCount, failed: failedCount, errors });
if (failedCount === 0) {
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
} else {
message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount}`);
}
// 刷新表格
actionRef.current?.reload();
} catch (error: any) {
message.error('批量同步失败: ' + (error.message || error.toString()));
} finally {
setSyncing(false);
} }
}; };
@ -182,15 +332,18 @@ const ProductSyncPage: React.FC = () => {
const renderSku = (template: string, data: any) => { const renderSku = (template: string, data: any) => {
if (!template) return ''; if (!template) return '';
// 支持 <%= it.path %> (Eta) 和 {{ path }} (Mustache/Handlebars) // 支持 <%= it.path %> (Eta) 和 {{ path }} (Mustache/Handlebars)
return template.replace(/<%=\s*it\.([\w.]+)\s*%>|\{\{\s*([\w.]+)\s*\}\}/g, (_, p1, p2) => { return template.replace(
/<%=\s*it\.([\w.]+)\s*%>|\{\{\s*([\w.]+)\s*\}\}/g,
(_, p1, p2) => {
const path = p1 || p2; const path = p1 || p2;
const keys = path.split('.'); const keys = path.split('.');
let value = data; let value = data;
for (const key of keys) { for (const key of keys) {
value = value?.[key]; value = value?.[key];
} }
return value === undefined || value === null ? '' : String(value); return value === undefined || value === null ? '' : String(value);
}); },
);
}; };
// 生成表格列配置 // 生成表格列配置
@ -211,20 +364,33 @@ const ProductSyncPage: React.FC = () => {
fixed: 'left', fixed: 'left',
render: (_, record) => ( render: (_, record) => (
<div> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}> <div
<div style={{ fontWeight: 'bold', fontSize: 14 }}> style={{
{record.name} display: 'flex',
</div> justifyContent: 'space-between',
<EditForm alignItems: 'center',
record={record} marginBottom: 4,
tableRef={actionRef} }}
trigger={<EditOutlined style={{ cursor: 'pointer', fontSize: 16, color: '#1890ff' }} />} >
/> <div style={{ fontWeight: 'bold', fontSize: 14 }}>
{record.name}
</div>
<EditForm
record={record}
tableRef={actionRef}
trigger={
<EditOutlined
style={{
cursor: 'pointer',
fontSize: 16,
color: '#1890ff',
}}
/>
}
/>
</div> </div>
<div style={{ fontSize: 12, color: '#666' }}> <div style={{ fontSize: 12, color: '#666' }}>
<span style={{ marginRight: 8 }}> <span style={{ marginRight: 8 }}>: {record.price}</span>
: {record.price}
</span>
{record.promotionPrice && ( {record.promotionPrice && (
<span style={{ color: 'red' }}> <span style={{ color: 'red' }}>
: {record.promotionPrice} : {record.promotionPrice}
@ -234,24 +400,39 @@ const ProductSyncPage: React.FC = () => {
{/* 属性 */} {/* 属性 */}
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
{record.attributes?.map((attr: any, idx: number) => ( {record.attributes?.map((attr: any, idx: number) => (
<Tag key={idx} style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}> <Tag
{attr.dict?.name || attr.name}: {attr.name} key={idx}
</Tag> style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}
))} >
{attr.dict?.name || attr.name}: {attr.name}
</Tag>
))}
</div> </div>
{/* 组成 (如果是 Bundle) */} {/* 组成 (如果是 Bundle) */}
{record.type === 'bundle' && record.components && record.components.length > 0 && ( {record.type === 'bundle' &&
<div style={{ marginTop: 8, fontSize: 12, background: '#f5f5f5', padding: 4, borderRadius: 4 }}> record.components &&
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>Components:</div> record.components.length > 0 && (
{record.components.map((comp: any, idx: number) => ( <div
<div key={idx}> style={{
{comp.sku} × {comp.quantity} marginTop: 8,
fontSize: 12,
background: '#f5f5f5',
padding: 4,
borderRadius: 4,
}}
>
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>
Components:
</div> </div>
))} {record.components.map((comp: any, idx: number) => (
</div> <div key={idx}>
)} {comp.sku} × {comp.quantity}
</div>
))}
</div>
)}
</div> </div>
), ),
}, },
@ -265,18 +446,34 @@ const ProductSyncPage: React.FC = () => {
hideInSearch: true, hideInSearch: true,
width: 220, width: 220,
render: (_, record) => { render: (_, record) => {
// 根据模板或默认规则生成期望的 SKU // 首先查找该产品在该站点的实际SKU
const expectedSku = skuTemplate let siteProductSku = '';
? renderSku(skuTemplate, { site, product: record }) if (record.siteSkus && record.siteSkus.length > 0) {
: `${site.skuPrefix || ''}-${record.sku}`; // 根据站点名称匹配对应的siteSku
const siteSkuInfo = record.siteSkus.find((sku: any) => {
// 这里假设可以根据站点名称或其他标识来匹配
// 如果需要更精确的匹配逻辑,可以根据实际需求调整
return sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
}
}
// 尝试用期望的 SKU 获取 WP 产品 // 如果没有找到实际的siteSku则根据模板或默认规则生成期望的SKU
const expectedSku = siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`
);
// 尝试用确定的SKU获取WP产品
let wpProduct = wpProductMap.get(expectedSku); let wpProduct = wpProductMap.get(expectedSku);
// 如果没找到,且没有模板(或者即使有模板),尝试回退到默认规则查找(以防万一) // 如果根据实际SKU没找到再尝试用模板生成的SKU查找
if (!wpProduct && skuTemplate) { if (!wpProduct && siteProductSku && skuTemplate) {
const fallbackSku = `${site.skuPrefix || ''}-${record.sku}`; const templateSku = renderSku(skuTemplate, { site, product: record });
wpProduct = wpProductMap.get(fallbackSku); wpProduct = wpProductMap.get(templateSku);
} }
if (!wpProduct) { if (!wpProduct) {
@ -284,66 +481,89 @@ const ProductSyncPage: React.FC = () => {
<ModalForm <ModalForm
title="同步产品" title="同步产品"
trigger={ trigger={
<Button type="link" icon={<SyncOutlined />}> <Button type="link" icon={<SyncOutlined />}>
</Button> </Button>
} }
width={400} width={400}
onFinish={async (values) => { onFinish={async (values) => {
return await syncProductToSite(values, record, site); return await syncProductToSite(values, record, site);
}} }}
initialValues={{ initialValues={{
sku: skuTemplate sku: siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site, product: record }) ? renderSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}` : `${site.skuPrefix || ''}-${record.sku}`
}} ),
}}
> >
<ProFormText <ProFormText
name="sku" name="sku"
label="商店 SKU" label="商店 SKU"
placeholder="请输入商店 SKU" placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]} rules={[{ required: true, message: '请输入 SKU' }]}
/> />
</ModalForm> </ModalForm>
); );
} }
return ( return (
<div style={{ fontSize: 12 }}> <div style={{ fontSize: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}> <div
<div style={{ fontWeight: 'bold' }}>{wpProduct.sku}</div> style={{
<ModalForm display: 'flex',
title="更新同步" justifyContent: 'space-between',
trigger={ alignItems: 'start',
<Button type="link" size="small" icon={<SyncOutlined spin={false} />}> }}
</Button> >
} <div style={{ fontWeight: 'bold' }}>{wpProduct.sku}</div>
width={400} <ModalForm
onFinish={async (values) => { title="更新同步"
return await syncProductToSite(values, record, site, wpProduct.externalProductId); trigger={
}} <Button
initialValues={{ type="link"
sku: wpProduct.sku size="small"
}} icon={<SyncOutlined spin={false} />}
> ></Button>
<ProFormText }
name="sku" width={400}
label="商店 SKU" onFinish={async (values) => {
placeholder="请输入商店 SKU" return await syncProductToSite(
rules={[{ required: true, message: '请输入 SKU' }]} values,
disabled record,
/> site,
<div style={{ marginBottom: 16, color: '#666' }}> wpProduct.externalProductId,
);
</div> }}
</ModalForm> initialValues={{
sku: wpProduct.sku,
}}
>
<ProFormText
name="sku"
label="商店 SKU"
placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]}
disabled
/>
<div style={{ marginBottom: 16, color: '#666' }}>
</div>
</ModalForm>
</div> </div>
<div>Price: {wpProduct.regular_price ?? wpProduct.price}</div> <div>Price: {wpProduct.regular_price ?? wpProduct.price}</div>
{wpProduct.sale_price && ( {wpProduct.sale_price && (
<div style={{ color: 'red' }}>Sale: {wpProduct.sale_price}</div> <div style={{ color: 'red' }}>Sale: {wpProduct.sale_price}</div>
)} )}
<div>Stock: {wpProduct.stock_quantity ?? wpProduct.stockQuantity}</div> <div>
Stock: {wpProduct.stock_quantity ?? wpProduct.stockQuantity}
</div>
<div style={{ marginTop: 2 }}> <div style={{ marginTop: 2 }}>
Status: {wpProduct.status === 'publish' ? <Tag color="green">Published</Tag> : <Tag>{wpProduct.status}</Tag>} Status:{' '}
{wpProduct.status === 'publish' ? (
<Tag color="green">Published</Tag>
) : (
<Tag>{wpProduct.status}</Tag>
)}
</div> </div>
</div> </div>
); );
@ -356,52 +576,137 @@ const ProductSyncPage: React.FC = () => {
}; };
if (initialLoading) { if (initialLoading) {
return ( return (
<Card title="商品同步状态" className="product-sync-card"> <Card title="商品同步状态" className="product-sync-card">
<Spin size="large" style={{ display: 'flex', justifyContent: 'center', padding: 40 }} /> <Spin
</Card> size="large"
) style={{ display: 'flex', justifyContent: 'center', padding: 40 }}
/>
</Card>
);
} }
return ( return (
<Card title="商品同步状态" className="product-sync-card"> <Card
<ProTable<ProductWithWP> title="商品同步状态"
columns={generateColumns()} className="product-sync-card"
actionRef={actionRef} extra={
rowKey="id" <div style={{ display: 'flex', gap: 8 }}>
request={async (params, sort, filter) => { <Select
// 调用本地获取产品列表 API style={{ width: 200 }}
const { data, success } = await productcontrollerGetproductlist({ placeholder="选择目标站点"
...params, value={selectedSiteId}
current: params.current, onChange={setSelectedSiteId}
pageSize: params.pageSize, options={sites.map(site => ({
// 传递搜索参数 label: site.name,
keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段 value: site.id,
sku: (params as any).sku, }))}
name: (params as any).name, />
} as any); <Button
type="primary"
icon={<SyncOutlined />}
onClick={() => setBatchSyncModalVisible(true)}
disabled={!selectedSiteId || sites.length === 0}
>
</Button>
</div>
}
>
<ProTable<ProductWithWP>
columns={generateColumns()}
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 // 返回给 ProTable
return { return {
data: (data?.items || []) as ProductWithWP[], data: (data?.items || []) as ProductWithWP[],
success, success,
total: data?.total || 0, total: data?.total || 0,
}; };
}} }}
pagination={{ pagination={{
pageSize: 10, pageSize: 10,
showSizeChanger: true, showSizeChanger: true,
}} }}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
search={{ search={{
labelWidth: 'auto', labelWidth: 'auto',
}} }}
options={{ options={{
density: true, density: true,
fullScreen: true, fullScreen: true,
}} }}
dateFormatter="string" dateFormatter="string"
/> />
{/* 批量同步模态框 */}
<Modal
title="批量同步产品"
open={batchSyncModalVisible}
onCancel={() => !syncing && setBatchSyncModalVisible(false)}
footer={null}
closable={!syncing}
maskClosable={!syncing}
>
<div style={{ marginBottom: 16 }}>
<p><strong>{sites.find(s => s.id === selectedSiteId)?.name}</strong></p>
<p></p>
</div>
{syncing && (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8 }}></div>
<Progress percent={syncProgress} status="active" />
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
{syncResults.success} | {syncResults.failed}
</div>
</div>
)}
{syncResults.errors.length > 0 && (
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
<div style={{ marginBottom: 8, color: '#ff4d4f' }}></div>
{syncResults.errors.slice(0, 10).map((error, index) => (
<div key={index} style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
{error}
</div>
))}
{syncResults.errors.length > 10 && (
<div style={{ fontSize: 12, color: '#999' }}>... {syncResults.errors.length - 10} </div>
)}
</div>
)}
<div style={{ textAlign: 'right' }}>
<Button
onClick={() => setBatchSyncModalVisible(false)}
disabled={syncing}
style={{ marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={batchSyncProducts}
loading={syncing}
disabled={syncing}
>
{syncing ? '同步中...' : '开始同步'}
</Button>
</div>
</Modal>
</Card> </Card>
); );
}; };

View File

@ -1,18 +1,13 @@
import { import { ordercontrollerSyncorder } from '@/servers/api/order';
ActionType,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { import {
sitecontrollerCreate, sitecontrollerCreate,
sitecontrollerDisable, sitecontrollerDisable,
sitecontrollerList, sitecontrollerList,
sitecontrollerUpdate, sitecontrollerUpdate,
} from '@/servers/api/site'; } from '@/servers/api/site';
import { wpproductcontrollerSyncproducts } from '@/servers/api/wpProduct';
import { ordercontrollerSyncorder } from '@/servers/api/order';
import { subscriptioncontrollerSync } from '@/servers/api/subscription'; import { subscriptioncontrollerSync } from '@/servers/api/subscription';
import { request } from '@umijs/max'; import { wpproductcontrollerSyncproducts } from '@/servers/api/wpProduct';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { Button, message, notification, Popconfirm, Space, Tag } from 'antd'; import { Button, message, notification, Popconfirm, Space, Tag } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件 import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
@ -91,9 +86,16 @@ const SiteList: React.FC = () => {
message: '同步完成', message: '同步完成',
description: ( description: (
<div> <div>
<p>产品: 成功 {stats.products.success}, {stats.products.fail}</p> <p>
<p>订单: 成功 {stats.orders.success}, {stats.orders.fail}</p> 产品: 成功 {stats.products.success}, {stats.products.fail}
<p>订阅: 成功 {stats.subscriptions.success}, {stats.subscriptions.fail}</p> </p>
<p>
订单: 成功 {stats.orders.success}, {stats.orders.fail}
</p>
<p>
订阅: 成功 {stats.subscriptions.success}, {' '}
{stats.subscriptions.fail}
</p>
</div> </div>
), ),
duration: null, // 不自动关闭 duration: null, // 不自动关闭
@ -124,7 +126,11 @@ const SiteList: React.FC = () => {
dataIndex: 'websiteUrl', dataIndex: 'websiteUrl',
width: 280, width: 280,
hideInSearch: true, hideInSearch: true,
render: (text) => <a href={text as string} target="_blank" rel="noopener noreferrer">{text}</a> render: (text) => (
<a href={text as string} target="_blank" rel="noopener noreferrer">
{text}
</a>
),
}, },
{ {
title: 'SKU 前缀', title: 'SKU 前缀',
@ -154,7 +160,9 @@ const SiteList: React.FC = () => {
return ( return (
<Space wrap> <Space wrap>
{row.stockPoints.map((sp) => ( {row.stockPoints.map((sp) => (
<Tag color="blue" key={sp.id}>{sp.name}</Tag> <Tag color="blue" key={sp.id}>
{sp.name}
</Tag>
))} ))}
</Space> </Space>
); );
@ -175,7 +183,7 @@ const SiteList: React.FC = () => {
title: '操作', title: '操作',
dataIndex: 'actions', dataIndex: 'actions',
width: 240, width: 240,
fixed:"right", fixed: 'right',
hideInSearch: true, hideInSearch: true,
render: (_, row) => ( render: (_, row) => (
<Space> <Space>
@ -197,12 +205,13 @@ const SiteList: React.FC = () => {
</Button> </Button>
<Popconfirm <Popconfirm
title={row.isDisabled ? '启用站点' : '禁用站点'} title={row.isDisabled ? '启用站点' : '禁用站点'}
description={ description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
}
onConfirm={async () => { onConfirm={async () => {
try { try {
await sitecontrollerDisable({ id: String(row.id) }, { disabled: !row.isDisabled }); await sitecontrollerDisable(
{ id: String(row.id) },
{ disabled: !row.isDisabled },
);
message.success('更新成功'); message.success('更新成功');
actionRef.current?.reload(); actionRef.current?.reload();
} catch (e: any) { } catch (e: any) {

View File

@ -1,8 +1,22 @@
import { ActionType, DrawerForm, ModalForm, PageContainer, ProColumns, ProFormText, ProFormTextArea, ProTable } from '@ant-design/pro-components'; import {
DeleteFilled,
EditOutlined,
PlusOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
ActionType,
DrawerForm,
ModalForm,
PageContainer,
ProColumns,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { request, useParams } from '@umijs/max'; import { request, useParams } from '@umijs/max';
import { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd'; import { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { DeleteFilled, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
const BatchEditCustomers: React.FC<{ const BatchEditCustomers: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>; tableRef: React.MutableRefObject<ActionType | undefined>;
@ -27,22 +41,27 @@ const BatchEditCustomers: React.FC<{
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
let ok = 0, fail = 0; let ok = 0,
fail = 0;
for (const id of selectedRowKeys) { for (const id of selectedRowKeys) {
try { try {
// Remove undefined values // Remove undefined values
const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== '')); const data = Object.fromEntries(
if (Object.keys(data).length === 0) continue; Object.entries(values).filter(
([_, v]) => v !== undefined && v !== '',
),
);
if (Object.keys(data).length === 0) continue;
const res = await request(`/site-api/${siteId}/customers/${id}`, { const res = await request(`/site-api/${siteId}/customers/${id}`, {
method: 'PUT', method: 'PUT',
data: data, data: data,
}); });
if (res.success) ok++; if (res.success) ok++;
else fail++; else fail++;
} catch (e) { } catch (e) {
fail++; fail++;
} }
} }
message.success(`成功 ${ok}, 失败 ${fail}`); message.success(`成功 ${ok}, 失败 ${fail}`);
tableRef.current?.reload(); tableRef.current?.reload();
@ -50,8 +69,16 @@ const BatchEditCustomers: React.FC<{
return true; return true;
}} }}
> >
<ProFormText name="role" label="角色" placeholder="请输入角色,不修改请留空" /> <ProFormText
<ProFormText name="phone" label="电话" placeholder="请输入电话,不修改请留空" /> name="role"
label="角色"
placeholder="请输入角色,不修改请留空"
/>
<ProFormText
name="phone"
label="电话"
placeholder="请输入电话,不修改请留空"
/>
</ModalForm> </ModalForm>
); );
}; };
@ -75,7 +102,9 @@ const CustomerPage: React.FC = () => {
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (!siteId) return; if (!siteId) return;
try { try {
const res = await request(`/site-api/${siteId}/customers/${id}`, { method: 'DELETE' }); const res = await request(`/site-api/${siteId}/customers/${id}`, {
method: 'DELETE',
});
if (res.success) { if (res.success) {
message.success('删除成功'); message.success('删除成功');
actionRef.current?.reload(); actionRef.current?.reload();
@ -112,7 +141,7 @@ const CustomerPage: React.FC = () => {
copyable: true, copyable: true,
render: (_, record) => { render: (_, record) => {
return record?.id ?? '-'; return record?.id ?? '-';
} },
}, },
{ {
title: '姓名', title: '姓名',
@ -139,7 +168,8 @@ const CustomerPage: React.FC = () => {
{ {
title: '电话', title: '电话',
dataIndex: 'phone', dataIndex: 'phone',
render: (_, record) => record.phone || record.billing?.phone || record.shipping?.phone || '-', render: (_, record) =>
record.phone || record.billing?.phone || record.shipping?.phone || '-',
copyable: true, copyable: true,
}, },
{ {
@ -159,12 +189,16 @@ const CustomerPage: React.FC = () => {
const { billing } = record; const { billing } = record;
if (!billing) return '-'; if (!billing) return '-';
return ( return (
<div style={{ fontSize: 12 }}> <div style={{ fontSize: 12 }}>
<div>{billing.address_1} {billing.address_2}</div> <div>
<div>{billing.city}, {billing.state}, {billing.postcode}</div> {billing.address_1} {billing.address_2}
<div>{billing.country}</div>
<div>{billing.phone}</div>
</div> </div>
<div>
{billing.city}, {billing.state}, {billing.postcode}
</div>
<div>{billing.country}</div>
<div>{billing.phone}</div>
</div>
); );
}, },
}, },
@ -178,14 +212,29 @@ const CustomerPage: React.FC = () => {
title: '操作', title: '操作',
valueType: 'option', valueType: 'option',
width: 120, width: 120,
fixed:"right", fixed: 'right',
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button type="link" title="编辑" icon={<EditOutlined />} onClick={() => setEditing(record)} /> <Button
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}> type="link"
title="编辑"
icon={<EditOutlined />}
onClick={() => setEditing(record)}
/>
<Popconfirm
title="确定删除?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" danger title="删除" icon={<DeleteFilled />} /> <Button type="link" danger title="删除" icon={<DeleteFilled />} />
</Popconfirm> </Popconfirm>
<Button type="link" title="查询订单" onClick={() => { setOrdersCustomer(record); setOrdersVisible(true); }}> <Button
type="link"
title="查询订单"
onClick={() => {
setOrdersCustomer(record);
setOrdersVisible(true);
}}
>
</Button> </Button>
</Space> </Space>
@ -195,11 +244,11 @@ const CustomerPage: React.FC = () => {
return ( return (
<PageContainer <PageContainer
ghost ghost
header={{ header={{
title: null, title: null,
breadcrumb: undefined breadcrumb: undefined,
}} }}
> >
<ProTable <ProTable
rowKey="id" rowKey="id"
@ -232,7 +281,7 @@ const CustomerPage: React.FC = () => {
page_size: pageSize, page_size: pageSize,
where, where,
...(orderObj ? { order: orderObj } : {}), ...(orderObj ? { order: orderObj } : {}),
...((name || email) ? { search: name || email } : {}), ...(name || email ? { search: name || email } : {}),
}, },
}); });
@ -255,10 +304,15 @@ const CustomerPage: React.FC = () => {
toolBarRender={() => [ toolBarRender={() => [
<DrawerForm <DrawerForm
title="新增客户" title="新增客户"
trigger={<Button type="primary" title="新增" icon={<PlusOutlined />} />} trigger={
<Button type="primary" title="新增" icon={<PlusOutlined />} />
}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
const res = await request(`/site-api/${siteId}/customers`, { method: 'POST', data: values }); const res = await request(`/site-api/${siteId}/customers`, {
method: 'POST',
data: values,
});
if (res.success) { if (res.success) {
message.success('新增成功'); message.success('新增成功');
actionRef.current?.reload(); actionRef.current?.reload();
@ -268,11 +322,15 @@ const CustomerPage: React.FC = () => {
return false; return false;
}} }}
> >
<ProFormText name="email" label="邮箱" rules={[{ required: true }]} /> <ProFormText
name="email"
label="邮箱"
rules={[{ required: true }]}
/>
<ProFormText name="first_name" label="名" /> <ProFormText name="first_name" label="名" />
<ProFormText name="last_name" label="姓" /> <ProFormText name="last_name" label="姓" />
<ProFormText name="username" label="用户名" /> <ProFormText name="username" label="用户名" />
<ProFormText name="phone" label="电话" /> <ProFormText name="phone" label="电话" />
</DrawerForm>, </DrawerForm>,
<BatchEditCustomers <BatchEditCustomers
tableRef={actionRef} tableRef={actionRef}
@ -284,10 +342,17 @@ const CustomerPage: React.FC = () => {
title="批量导出" title="批量导出"
onClick={async () => { onClick={async () => {
if (!siteId) return; if (!siteId) return;
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; const idsParam = selectedRowKeys.length
const res = await request(`/site-api/${siteId}/customers/export`, { params: { ids: idsParam } }); ? (selectedRowKeys as any[]).join(',')
: undefined;
const res = await request(
`/site-api/${siteId}/customers/export`,
{ params: { ids: idsParam } },
);
if (res?.success && res?.data?.csv) { if (res?.success && res?.data?.csv) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([res.data.csv], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@ -298,17 +363,26 @@ const CustomerPage: React.FC = () => {
message.error(res.message || '导出失败'); message.error(res.message || '导出失败');
} }
}} }}
></Button>, >
</Button>,
<ModalForm <ModalForm
title="批量导入客户" title="批量导入客户"
trigger={<Button type="primary" ghost></Button>} trigger={
<Button type="primary" ghost>
</Button>
}
width={600} width={600}
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
const csv = values.csv || ''; const csv = values.csv || '';
const items = values.items || []; const items = values.items || [];
const res = await request(`/site-api/${siteId}/customers/import`, { method: 'POST', data: { csv, items } }); const res = await request(
`/site-api/${siteId}/customers/import`,
{ method: 'POST', data: { csv, items } },
);
if (res.success) { if (res.success) {
message.success('导入完成'); message.success('导入完成');
actionRef.current?.reload(); actionRef.current?.reload();
@ -318,7 +392,11 @@ const CustomerPage: React.FC = () => {
return false; return false;
}} }}
> >
<ProFormTextArea name="csv" label="CSV文本" placeholder="粘贴CSV,首行为表头" /> <ProFormTextArea
name="csv"
label="CSV文本"
placeholder="粘贴CSV,首行为表头"
/>
</ModalForm>, </ModalForm>,
<Button <Button
@ -327,7 +405,10 @@ const CustomerPage: React.FC = () => {
icon={<DeleteFilled />} icon={<DeleteFilled />}
onClick={async () => { onClick={async () => {
if (!siteId) return; if (!siteId) return;
const res = await request(`/site-api/${siteId}/customers/batch`, { method: 'POST', data: { delete: selectedRowKeys } }); const res = await request(`/site-api/${siteId}/customers/batch`, {
method: 'POST',
data: { delete: selectedRowKeys },
});
actionRef.current?.reload(); actionRef.current?.reload();
setSelectedRowKeys([]); setSelectedRowKeys([]);
if (res.success) { if (res.success) {
@ -336,7 +417,7 @@ const CustomerPage: React.FC = () => {
message.warning(res.message || '部分删除失败'); message.warning(res.message || '部分删除失败');
} }
}} }}
/> />,
]} ]}
/> />
@ -347,7 +428,10 @@ const CustomerPage: React.FC = () => {
initialValues={editing || {}} initialValues={editing || {}}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId || !editing) return false; if (!siteId || !editing) return false;
const res = await request(`/site-api/${siteId}/customers/${editing.id}`, { method: 'PUT', data: values }); const res = await request(
`/site-api/${siteId}/customers/${editing.id}`,
{ method: 'PUT', data: values },
);
if (res.success) { if (res.success) {
message.success('更新成功'); message.success('更新成功');
actionRef.current?.reload(); actionRef.current?.reload();
@ -362,11 +446,14 @@ const CustomerPage: React.FC = () => {
<ProFormText name="first_name" label="名" /> <ProFormText name="first_name" label="名" />
<ProFormText name="last_name" label="姓" /> <ProFormText name="last_name" label="姓" />
<ProFormText name="username" label="用户名" /> <ProFormText name="username" label="用户名" />
<ProFormText name="phone" label="电话" /> <ProFormText name="phone" label="电话" />
</DrawerForm> </DrawerForm>
<Modal <Modal
open={ordersVisible} open={ordersVisible}
onCancel={() => { setOrdersVisible(false); setOrdersCustomer(null); }} onCancel={() => {
setOrdersVisible(false);
setOrdersCustomer(null);
}}
footer={null} footer={null}
width={1000} width={1000}
title="客户订单" title="客户订单"
@ -386,7 +473,12 @@ const CustomerPage: React.FC = () => {
return ordersCustomer?.email; return ordersCustomer?.email;
}, },
}, },
{ title: '支付时间', dataIndex: 'date_paid', valueType: 'dateTime', hideInSearch: true }, {
title: '支付时间',
dataIndex: 'date_paid',
valueType: 'dateTime',
hideInSearch: true,
},
{ title: '订单金额', dataIndex: 'total', hideInSearch: true }, { title: '订单金额', dataIndex: 'total', hideInSearch: true },
{ title: '状态', dataIndex: 'status', hideInSearch: true }, { title: '状态', dataIndex: 'status', hideInSearch: true },
{ title: '来源', dataIndex: 'created_via', hideInSearch: true }, { title: '来源', dataIndex: 'created_via', hideInSearch: true },
@ -408,13 +500,17 @@ const CustomerPage: React.FC = () => {
}, },
]} ]}
request={async (params) => { request={async (params) => {
if (!siteId || !ordersCustomer?.id) return { data: [], total: 0, success: true }; if (!siteId || !ordersCustomer?.id)
const res = await request(`/site-api/${siteId}/customers/${ordersCustomer.id}/orders`, { return { data: [], total: 0, success: true };
params: { const res = await request(
page: params.current, `/site-api/${siteId}/customers/${ordersCustomer.id}/orders`,
per_page: params.pageSize, {
params: {
page: params.current,
per_page: params.pageSize,
},
}, },
}); );
if (!res?.success) { if (!res?.success) {
message.error(res?.message || '获取订单失败'); message.error(res?.message || '获取订单失败');
return { data: [], total: 0, success: false }; return { data: [], total: 0, success: false };

View File

@ -1,4 +1,5 @@
import { areacontrollerGetarealist } from '@/servers/api/area';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { import {
DrawerForm, DrawerForm,
ProFormDependency, ProFormDependency,
@ -9,8 +10,6 @@ import {
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { areacontrollerGetarealist } from '@/servers/api/area';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
// 定义组件的 props 类型 // 定义组件的 props 类型
interface EditSiteFormProps { interface EditSiteFormProps {
@ -67,7 +66,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
rules={[{ required: true, message: '请输入名称' }]} rules={[{ required: true, message: '请输入名称' }]}
placeholder="请输入名称" placeholder="请输入名称"
/> />
<ProFormTextArea name="description" label="描述" placeholder="请输入描述" /> <ProFormTextArea
name="description"
label="描述"
placeholder="请输入描述"
/>
<ProFormText <ProFormText
name="apiUrl" name="apiUrl"
label="API 地址" label="API 地址"
@ -99,14 +102,22 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
<ProFormText <ProFormText
name="consumerKey" name="consumerKey"
label="Consumer Key" label="Consumer Key"
rules={[{ required: !isEdit, message: '请输入 Consumer Key' }]} rules={[
placeholder={isEdit ? '留空表示不修改' : '请输入 Consumer Key'} { required: !isEdit, message: '请输入 Consumer Key' },
]}
placeholder={
isEdit ? '留空表示不修改' : '请输入 Consumer Key'
}
/> />
<ProFormText <ProFormText
name="consumerSecret" name="consumerSecret"
label="Consumer Secret" label="Consumer Secret"
rules={[{ required: !isEdit, message: '请输入 Consumer Secret' }]} rules={[
placeholder={isEdit ? '留空表示不修改' : '请输入 Consumer Secret'} { required: !isEdit, message: '请输入 Consumer Secret' },
]}
placeholder={
isEdit ? '留空表示不修改' : '请输入 Consumer Secret'
}
/> />
</> </>
); );
@ -125,7 +136,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
return null; return null;
}} }}
</ProFormDependency> </ProFormDependency>
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder="请输入 SKU 前缀" /> <ProFormText
name="skuPrefix"
label="SKU 前缀"
placeholder="请输入 SKU 前缀"
/>
<ProFormSelect <ProFormSelect
name="areas" name="areas"
label="区域" label="区域"
@ -135,7 +150,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
// 从后端接口获取区域数据 // 从后端接口获取区域数据
const res = await areacontrollerGetarealist({ pageSize: 1000 }); const res = await areacontrollerGetarealist({ pageSize: 1000 });
// areacontrollerGetarealist 直接返回数组, 所以不需要 .data.list // areacontrollerGetarealist 直接返回数组, 所以不需要 .data.list
return res.map((area: any) => ({ label: area.name, value: area.code })); return res.map((area: any) => ({
label: area.name,
value: area.code,
}));
}} }}
/> />
<ProFormSelect <ProFormSelect
@ -147,7 +165,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
// 从后端接口获取仓库数据 // 从后端接口获取仓库数据
const res = await stockcontrollerGetallstockpoints(); const res = await stockcontrollerGetallstockpoints();
// 使用可选链和空值合并运算符来安全地处理可能未定义的数据 // 使用可选链和空值合并运算符来安全地处理可能未定义的数据
return res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ?? []; return (
res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ??
[]
);
}} }}
/> />
<ProFormSwitch name="isDisabled" label="是否禁用" /> <ProFormSwitch name="isDisabled" label="是否禁用" />

View File

@ -1,14 +1,11 @@
import { EditOutlined } from '@ant-design/icons';
import { request } from '@umijs/max';
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
import { import { EditOutlined } from '@ant-design/icons';
PageContainer, import { PageContainer } from '@ant-design/pro-components';
} from '@ant-design/pro-components'; import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
import { Outlet, history, useLocation, useParams } from '@umijs/max';
import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd'; import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import EditSiteForm from './EditSiteForm';
import type { SiteItem } from '../List/index'; import type { SiteItem } from '../List/index';
import EditSiteForm from './EditSiteForm';
const ShopLayout: React.FC = () => { const ShopLayout: React.FC = () => {
const [sites, setSites] = useState<any[]>([]); const [sites, setSites] = useState<any[]>([]);
@ -102,15 +99,29 @@ const ShopLayout: React.FC = () => {
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}> <Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
<Col span={4} style={{ height: '100%' }}> <Col span={4} style={{ height: '100%' }}>
<Card <Card
bodyStyle={{ padding: '10px 0', height: '100%', display: 'flex', flexDirection: 'column' }} bodyStyle={{
padding: '10px 0',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
style={{ height: '100%', overflow: 'hidden' }} style={{ height: '100%', overflow: 'hidden' }}
> >
<div style={{ padding: '0 10px 16px' }}> <div style={{ padding: '0 10px 16px' }}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}> <div
style={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
<Select <Select
style={{ flex: 1 }} style={{ flex: 1 }}
placeholder="请选择店铺" placeholder="请选择店铺"
options={sites.map(site => ({ label: site.name, value: site.id }))} options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
value={siteId ? Number(siteId) : undefined} value={siteId ? Number(siteId) : undefined}
onChange={handleSiteChange} onChange={handleSiteChange}
showSearch showSearch
@ -120,7 +131,9 @@ const ShopLayout: React.FC = () => {
icon={<EditOutlined />} icon={<EditOutlined />}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
onClick={() => { onClick={() => {
const currentSite = sites.find(site => site.id === Number(siteId)); const currentSite = sites.find(
(site) => site.id === Number(siteId),
);
if (currentSite) { if (currentSite) {
setEditingSite(currentSite); setEditingSite(currentSite);
setEditModalOpen(true); setEditModalOpen(true);

View File

@ -7,7 +7,12 @@ import {
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock'; import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { formatUniuniShipmentState } from '@/utils/format'; import { formatUniuniShipmentState } from '@/utils/format';
import { printPDF } from '@/utils/util'; import { printPDF } from '@/utils/util';
import { CopyOutlined, FilePdfOutlined, ReloadOutlined, DeleteFilled } from '@ant-design/icons'; import {
CopyOutlined,
DeleteFilled,
FilePdfOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { import {
ActionType, ActionType,
PageContainer, PageContainer,
@ -150,7 +155,12 @@ const LogisticsPage: React.FC = () => {
} }
}} }}
> >
<Button type="primary" danger title="删除" icon={<DeleteFilled />} /> <Button
type="primary"
danger
title="删除"
icon={<DeleteFilled />}
/>
</Popconfirm> </Popconfirm>
<ToastContainer /> <ToastContainer />
</> </>
@ -183,7 +193,11 @@ const LogisticsPage: React.FC = () => {
if (siteId) { if (siteId) {
params.siteId = Number(siteId); params.siteId = Number(siteId);
} }
const { data, success, message: errMsg } = await logisticscontrollerGetlist({ const {
data,
success,
message: errMsg,
} = await logisticscontrollerGetlist({
params, params,
}); });
if (success) { if (success) {
@ -217,8 +231,9 @@ const LogisticsPage: React.FC = () => {
setIsLoading(true); setIsLoading(true);
let ok = 0; let ok = 0;
for (const row of selectedRows) { for (const row of selectedRows) {
const { success } = await logisticscontrollerDeleteshipment({ id: row.id }); const { success } =
if (success) ok++; await logisticscontrollerDeleteshipment({ id: row.id });
if (success) ok++;
} }
message.success(`成功删除 ${ok}`); message.success(`成功删除 ${ok}`);
setIsLoading(false); setIsLoading(false);

View File

@ -1,8 +1,16 @@
import { ModalForm, PageContainer, ProColumns, ProFormText, ProFormTextArea, ProFormUploadButton, ProTable } from '@ant-design/pro-components'; import { siteapicontrollerCreatemedia } from '@/servers/api/siteApi';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
ModalForm,
PageContainer,
ProColumns,
ProFormText,
ProFormUploadButton,
ProTable,
} from '@ant-design/pro-components';
import { request, useParams } from '@umijs/max'; import { request, useParams } from '@umijs/max';
import { App, Button, Image, Popconfirm, Space } from 'antd'; import { App, Button, Image, Popconfirm, Space } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
const MediaPage: React.FC = () => { const MediaPage: React.FC = () => {
const { message } = App.useApp(); const { message } = App.useApp();
@ -62,7 +70,7 @@ const MediaPage: React.FC = () => {
copyable: true, copyable: true,
render: (_, record) => { render: (_, record) => {
return record?.id ?? '-'; return record?.id ?? '-';
} },
}, },
{ {
title: '展示', title: '展示',
@ -71,7 +79,12 @@ const MediaPage: React.FC = () => {
render: (_, record) => ( render: (_, record) => (
<Image <Image
src={record.source_url} src={record.source_url}
style={{ width: 60, height: 60, objectFit: 'contain', background: '#f0f0f0' }} style={{
width: 60,
height: 60,
objectFit: 'contain',
background: '#f0f0f0',
}}
fallback="https://via.placeholder.com/60?text=No+Img" fallback="https://via.placeholder.com/60?text=No+Img"
/> />
), ),
@ -100,6 +113,31 @@ const MediaPage: React.FC = () => {
dataIndex: 'mime_type', dataIndex: 'mime_type',
width: 120, width: 120,
}, },
{
// 文件大小列
title: '文件大小',
dataIndex: 'file_size',
hideInSearch: true,
width: 120,
render: (_: any, record: any) => {
// 获取文件大小
const fileSize = record.file_size;
// 如果文件大小不存在,则直接返回-
if (!fileSize) {
return '-';
}
// 如果文件大小小于1024,则单位为B
if (fileSize < 1024) {
return `${fileSize} B`;
// 如果文件大小小于1024*1024,则单位为KB
} else if (fileSize < 1024 * 1024) {
return `${(fileSize / 1024).toFixed(2)} KB`;
// 否则单位为MB
} else {
return `${(fileSize / (1024 * 1024)).toFixed(2)} MB`;
}
},
},
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'date_created', dataIndex: 'date_created',
@ -140,11 +178,11 @@ const MediaPage: React.FC = () => {
return ( return (
<PageContainer <PageContainer
ghost ghost
header={{ header={{
title: null, title: null,
breadcrumb: undefined breadcrumb: undefined,
}} }}
> >
<ProTable <ProTable
rowKey="id" rowKey="id"
@ -192,7 +230,11 @@ const MediaPage: React.FC = () => {
toolBarRender={() => [ toolBarRender={() => [
<ModalForm <ModalForm
title="上传媒体" title="上传媒体"
trigger={<Button type="primary" title="上传媒体" icon={<PlusOutlined />}></Button>} trigger={
<Button type="primary" title="上传媒体" icon={<PlusOutlined />}>
</Button>
}
width={500} width={500}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
@ -204,13 +246,12 @@ const MediaPage: React.FC = () => {
formData.append('file', f.originFileObj); formData.append('file', f.originFileObj);
}); });
} else { } else {
message.warning('请选择文件'); message.warning('请选择文件');
return false; return false;
} }
const res = await request('/media/upload', { const res = await siteapicontrollerCreatemedia({
method: 'POST', body: formData,
data: formData,
}); });
if (res.success) { if (res.success) {
@ -230,23 +271,27 @@ const MediaPage: React.FC = () => {
<ProFormUploadButton <ProFormUploadButton
name="file" name="file"
label="文件" label="文件"
max={1}
fieldProps={{ fieldProps={{
name: 'file', name: 'file',
listType: 'picture-card', listType: 'picture-card',
}} }}
rules={[{ required: true, message: '请选择文件' }]} rules={[{ required: true, message: '请选择文件' }]}
/> />
</ModalForm> </ModalForm>,
,
<Button <Button
title="批量导出" title="批量导出"
onClick={async () => { onClick={async () => {
if (!siteId) return; if (!siteId) return;
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; const idsParam = selectedRowKeys.length
const res = await request(`/site-api/${siteId}/media/export`, { params: { ids: idsParam } }); ? (selectedRowKeys as any[]).join(',')
: undefined;
const res = await request(`/site-api/${siteId}/media/export`, {
params: { ids: idsParam },
});
if (res?.success && res?.data?.csv) { if (res?.success && res?.data?.csv) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([res.data.csv], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@ -269,7 +314,10 @@ const MediaPage: React.FC = () => {
// 条件判断 如果站点編號不存在則直接返回 // 条件判断 如果站点編號不存在則直接返回
if (!siteId) return; if (!siteId) return;
// 发起批量删除请求 // 发起批量删除请求
const response = await request(`/site-api/${siteId}/media/batch`, { method: 'POST', data: { delete: selectedRowKeys } }); const response = await request(
`/site-api/${siteId}/media/batch`,
{ method: 'POST', data: { delete: selectedRowKeys } },
);
// 条件判断 根据接口返回结果进行提示 // 条件判断 根据接口返回结果进行提示
if (response.success) { if (response.success) {
message.success('批量删除成功'); message.success('批量删除成功');
@ -290,9 +338,8 @@ const MediaPage: React.FC = () => {
> >
</Button> </Button>
</Popconfirm> </Popconfirm>,
,
<Button <Button
title="批量转换为WebP" title="批量转换为WebP"
disabled={!selectedRowKeys.length} disabled={!selectedRowKeys.length}
@ -301,16 +348,21 @@ const MediaPage: React.FC = () => {
if (!siteId) return; if (!siteId) return;
try { try {
// 发起后端批量转换请求 // 发起后端批量转换请求
const response = await request(`/site-api/${siteId}/media/convert-webp`, { const response = await request(
method: 'POST', `/site-api/${siteId}/media/convert-webp`,
data: { ids: selectedRowKeys }, {
}); method: 'POST',
data: { ids: selectedRowKeys },
},
);
// 条件判断 根据接口返回结果进行提示 // 条件判断 根据接口返回结果进行提示
if (response.success) { if (response.success) {
const convertedCount = response?.data?.converted?.length || 0; const convertedCount = response?.data?.converted?.length || 0;
const failedCount = response?.data?.failed?.length || 0; const failedCount = response?.data?.failed?.length || 0;
if (failedCount > 0) { if (failedCount > 0) {
message.warning(`部分转换失败 已转换 ${convertedCount} 失败 ${failedCount}`); message.warning(
`部分转换失败 已转换 ${convertedCount} 失败 ${failedCount}`,
);
} else { } else {
message.success(`转换成功 已转换 ${convertedCount}`); message.success(`转换成功 已转换 ${convertedCount}`);
} }
@ -325,7 +377,7 @@ const MediaPage: React.FC = () => {
}} }}
> >
WebP WebP
</Button> </Button>,
]} ]}
/> />

View File

@ -1,36 +1,24 @@
import styles from '@/style/order-list.css';
import { ORDER_STATUS_ENUM } from '@/constants'; import { ORDER_STATUS_ENUM } from '@/constants';
import { HistoryOrder } from '@/pages/Statistics/Order'; import { HistoryOrder } from '@/pages/Statistics/Order';
import { import styles from '@/style/order-list.css';
ordercontrollerChangestatus, import { DeleteFilled, EllipsisOutlined } from '@ant-design/icons';
} from '@/servers/api/order';
import { formatShipmentState, formatSource } from '@/utils/format';
import {
EllipsisOutlined,
} from '@ant-design/icons';
import { import {
ActionType, ActionType,
ModalForm,
PageContainer, PageContainer,
ProColumns, ProColumns,
ProTable,
ModalForm,
ProFormTextArea, ProFormTextArea,
ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request, useParams } from '@umijs/max'; import { request, useParams } from '@umijs/max';
import { import { App, Button, Dropdown, Popconfirm, Tabs, TabsProps } from 'antd';
App,
Button,
Divider,
Dropdown,
Popconfirm,
Space,
Tabs,
TabsProps,
Tag,
} from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { BatchEditOrders, CreateOrder, EditOrder, OrderNote, Shipping } from '../components/Order/Forms'; import {
import { DeleteFilled } from '@ant-design/icons'; BatchEditOrders,
CreateOrder,
EditOrder,
OrderNote,
} from '../components/Order/Forms';
const OrdersPage: React.FC = () => { const OrdersPage: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
@ -70,10 +58,7 @@ const OrdersPage: React.FC = () => {
}; };
}); });
return [ return [{ key: 'all', label: `全部(${total})` }, ...tabs];
{ key: 'all', label: `全部(${total})` },
...tabs,
];
}, [count]); }, [count]);
const columns: ProColumns<API.Order>[] = [ const columns: ProColumns<API.Order>[] = [
@ -126,9 +111,7 @@ const OrdersPage: React.FC = () => {
return ( return (
<div> <div>
{record.line_items.map((item: any) => ( {record.line_items.map((item: any) => (
<div key={item.id}> <div key={item.id}>{`${item.name} x ${item.quantity}`}</div>
{`${item.name} x ${item.quantity}`}
</div>
))} ))}
</div> </div>
); );
@ -194,7 +177,9 @@ const OrdersPage: React.FC = () => {
}, },
{ {
key: 'note', key: 'note',
label: <OrderNote id={record.id as number} siteId={siteId} />, label: (
<OrderNote id={record.id as number} siteId={siteId} />
),
}, },
], ],
}} }}
@ -205,7 +190,10 @@ const OrdersPage: React.FC = () => {
title="确定删除订单?" title="确定删除订单?"
onConfirm={async () => { onConfirm={async () => {
try { try {
const res = await request(`/site-api/${siteId}/orders/${record.id}`, { method: 'DELETE' }); const res = await request(
`/site-api/${siteId}/orders/${record.id}`,
{ method: 'DELETE' },
);
if (res.success) { if (res.success) {
message.success('删除成功'); message.success('删除成功');
actionRef.current?.reload(); actionRef.current?.reload();
@ -272,15 +260,20 @@ const OrdersPage: React.FC = () => {
message.warning(res.message || '部分删除失败'); message.warning(res.message || '部分删除失败');
} }
}} }}
/> />,
,
<Button <Button
onClick={async () => { onClick={async () => {
if (!siteId) return; if (!siteId) return;
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; const idsParam = selectedRowKeys.length
const res = await request(`/site-api/${siteId}/orders/export`, { params: { ids: idsParam } }); ? (selectedRowKeys as any[]).join(',')
: undefined;
const res = await request(`/site-api/${siteId}/orders/export`, {
params: { ids: idsParam },
});
if (res?.success && res?.data?.csv) { if (res?.success && res?.data?.csv) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([res.data.csv], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@ -291,17 +284,26 @@ const OrdersPage: React.FC = () => {
message.error(res.message || '导出失败'); message.error(res.message || '导出失败');
} }
}} }}
></Button>, >
</Button>,
<ModalForm <ModalForm
title="批量导入订单" title="批量导入订单"
trigger={<Button type="primary" ghost></Button>} trigger={
<Button type="primary" ghost>
</Button>
}
width={600} width={600}
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
const csv = values.csv || ''; const csv = values.csv || '';
const items = values.items || []; const items = values.items || [];
const res = await request(`/site-api/${siteId}/orders/import`, { method: 'POST', data: { csv, items } }); const res = await request(`/site-api/${siteId}/orders/import`, {
method: 'POST',
data: { csv, items },
});
if (res.success) { if (res.success) {
message.success('导入完成'); message.success('导入完成');
actionRef.current?.reload(); actionRef.current?.reload();
@ -311,9 +313,12 @@ const OrdersPage: React.FC = () => {
return false; return false;
}} }}
> >
<ProFormTextArea name="csv" label="CSV文本" placeholder="粘贴CSV,首行为表头" /> <ProFormTextArea
</ModalForm> name="csv"
label="CSV文本"
placeholder="粘贴CSV,首行为表头"
/>
</ModalForm>,
]} ]}
request={async (params, sort, filter) => { request={async (params, sort, filter) => {
const p: any = params || {}; const p: any = params || {};
@ -321,7 +326,13 @@ const OrdersPage: React.FC = () => {
const pageSize = p.pageSize; const pageSize = p.pageSize;
const date = p.date; const date = p.date;
const status = p.status; const status = p.status;
const { current: _c, pageSize: _ps, date: _d, status: _s, ...rest } = p; const {
current: _c,
pageSize: _ps,
date: _d,
status: _s,
...rest
} = p;
const where: Record<string, any> = { ...(filter || {}), ...rest }; const where: Record<string, any> = { ...(filter || {}), ...rest };
if (status && status !== 'all') { if (status && status !== 'all') {
where.status = status; where.status = status;
@ -345,7 +356,7 @@ const OrdersPage: React.FC = () => {
page_size: pageSize, page_size: pageSize,
where, where,
...(orderObj ? { order: orderObj } : {}), ...(orderObj ? { order: orderObj } : {}),
} },
}); });
if (!response.success) { if (!response.success) {
@ -379,7 +390,7 @@ const OrdersPage: React.FC = () => {
const { status: _status, ...baseWhere } = where; const { status: _status, ...baseWhere } = where;
// 并发请求各状态的总数,对站点接口不支持的状态使用0 // 并发请求各状态的总数,对站点接口不支持的状态使用0
const results = await Promise.all( const results = await Promise.all(
statusKeys.map(async key => { statusKeys.map(async (key) => {
// 将前端退款状态映射为站点接口可能识别的原始状态 // 将前端退款状态映射为站点接口可能识别的原始状态
const mapToRawStatus: Record<string, string> = { const mapToRawStatus: Record<string, string> = {
refund_requested: 'return-requested', refund_requested: 'return-requested',
@ -388,7 +399,10 @@ const OrdersPage: React.FC = () => {
}; };
const rawStatus = mapToRawStatus[key] || key; const rawStatus = mapToRawStatus[key] || key;
// 对扩展状态直接返回0,减少不必要的请求 // 对扩展状态直接返回0,减少不必要的请求
const unsupported = ['after_sale_pending', 'pending_reshipment']; const unsupported = [
'after_sale_pending',
'pending_reshipment',
];
if (unsupported.includes(key)) { if (unsupported.includes(key)) {
return { status: key, count: 0 }; return { status: key, count: 0 };
} }
@ -406,7 +420,7 @@ const OrdersPage: React.FC = () => {
// 请求失败时该状态数量记为0 // 请求失败时该状态数量记为0
return { status: key, count: 0 }; return { status: key, count: 0 };
} }
}) }),
); );
setCount(results); setCount(results);
} catch (e) { } catch (e) {
@ -418,15 +432,14 @@ const OrdersPage: React.FC = () => {
return { return {
total: data?.total || 0, total: data?.total || 0,
data: data?.items || [], data: data?.items || [],
success: true success: true,
}; };
} }
return { return {
data: [], data: [],
success: false success: false,
}; };
}} }}
/> />
</PageContainer> </PageContainer>
); );

View File

@ -1,24 +1,24 @@
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants'; import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
import { request } from '@umijs/max'; import { DeleteFilled, LinkOutlined } from '@ant-design/icons';
import { useParams } from '@umijs/max';
import { import {
ActionType, ActionType,
PageContainer, PageContainer,
ProColumns, ProColumns,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request, useParams } from '@umijs/max';
import { App, Button, Divider, Popconfirm, Tag } from 'antd'; import { App, Button, Divider, Popconfirm, Tag } from 'antd';
import { DeleteFilled, LinkOutlined } from '@ant-design/icons';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { ErpProductBindModal } from '../components/Product/ErpProductBindModal';
import { import {
BatchDeleteProducts,
BatchEditProducts, BatchEditProducts,
CreateProduct,
ImportCsv, ImportCsv,
SetComponent, SetComponent,
UpdateForm, UpdateForm,
UpdateStatus, UpdateStatus,
UpdateVaritation, UpdateVaritation,
BatchDeleteProducts,
CreateProduct,
} from '../components/Product/Forms'; } from '../components/Product/Forms';
import { TagConfig } from '../components/Product/utils'; import { TagConfig } from '../components/Product/utils';
const ProductsPage: React.FC = () => { const ProductsPage: React.FC = () => {
@ -67,11 +67,22 @@ const ProductsPage: React.FC = () => {
if (!dict) { if (!dict) {
return []; return [];
} }
const res = await request('/dict/items', { params: { dictId: dict.id } }); const res = await request('/dict/items', {
params: { dictId: dict.id },
});
return res.map((item: any) => item.name); return res.map((item: any) => item.name);
}; };
const [brands, fruits, mints, flavors, strengths, sizes, humidities, categories] = await Promise.all([ const [
brands,
fruits,
mints,
flavors,
strengths,
sizes,
humidities,
categories,
] = await Promise.all([
getItems('brand'), getItems('brand'),
getItems('fruit'), getItems('fruit'),
getItems('mint'), getItems('mint'),
@ -90,7 +101,7 @@ const ProductsPage: React.FC = () => {
strengths, strengths,
sizes, sizes,
humidities, humidities,
categories categories,
}); });
} catch (error) { } catch (error) {
console.error('Failed to fetch configs:', error); console.error('Failed to fetch configs:', error);
@ -100,7 +111,7 @@ const ProductsPage: React.FC = () => {
fetchAllConfigs(); fetchAllConfigs();
}, []); }, []);
const columns: ProColumns<API.UnifiedProductDTO>[] = [ const columns: ProColumns<any>[] = [
{ {
// ID // ID
title: 'ID', title: 'ID',
@ -110,7 +121,7 @@ const ProductsPage: React.FC = () => {
copyable: true, copyable: true,
render: (_, record) => { render: (_, record) => {
return record?.id ?? '-'; return record?.id ?? '-';
} },
}, },
{ {
// sku // sku
@ -123,6 +134,11 @@ const ProductsPage: React.FC = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
}, },
{
// 产品类型
title: '产品类型',
dataIndex: 'type',
},
{ {
// 产品状态 // 产品状态
title: '产品状态', title: '产品状态',
@ -130,11 +146,7 @@ const ProductsPage: React.FC = () => {
valueType: 'select', valueType: 'select',
valueEnum: PRODUCT_STATUS_ENUM, valueEnum: PRODUCT_STATUS_ENUM,
}, },
{
// 产品类型
title: '产品类型',
dataIndex: 'type',
},
{ {
// 库存状态 // 库存状态
title: '库存状态', title: '库存状态',
@ -148,6 +160,43 @@ const ProductsPage: React.FC = () => {
dataIndex: 'stock_quantity', dataIndex: 'stock_quantity',
hideInSearch: true, hideInSearch: true,
}, },
{
// ERP产品信息
title: 'ERP产品',
dataIndex: 'erpProduct',
hideInSearch: true,
width: 200,
render: (_, record) => {
if (record.erpProduct) {
return (
<div
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
>
<div>
<strong>SKU:</strong> {record.erpProduct.sku}
</div>
<div>
<strong>:</strong> {record.erpProduct.name}
</div>
{record.erpProduct.nameCn && (
<div>
<strong>:</strong> {record.erpProduct.nameCn}
</div>
)}
{record.erpProduct.category && (
<div>
<strong>:</strong> {record.erpProduct.category.name}
</div>
)}
<div>
<strong>:</strong> {record.erpProduct.stock_quantity ?? '-'}
</div>
</div>
);
}
return <Tag color="orange"></Tag>;
},
},
{ {
// 图片 // 图片
title: '图片', title: '图片',
@ -172,29 +221,7 @@ const ProductsPage: React.FC = () => {
dataIndex: 'sale_price', dataIndex: 'sale_price',
hideInSearch: true, hideInSearch: true,
}, },
{
// 标签
title: '标签',
dataIndex: 'tags',
hideInSearch: true,
width: 250,
render: (_, record) => {
// 检查 record.tags 是否存在并且是一个数组
if (record.tags && Array.isArray(record.tags)) {
// 遍历 tags 数组并为每个 tag 对象渲染一个 Tag 组件
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{record.tags.map((tag: any) => (
// 使用 tag.name 作为 key, 因为 tag.id 可能是对象, 会导致 React key 错误
<Tag key={tag.name}>{tag.name}</Tag>
))}
</div>
);
}
// 如果 record.tags 不是一个有效的数组,则不渲染任何内容
return null;
},
},
{ {
// 分类 // 分类
title: '分类', title: '分类',
@ -228,10 +255,13 @@ const ProductsPage: React.FC = () => {
// 检查 record.attributes 是否存在并且是一个数组 // 检查 record.attributes 是否存在并且是一个数组
if (record.attributes && Array.isArray(record.attributes)) { if (record.attributes && Array.isArray(record.attributes)) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> <div
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
>
{(record.attributes as any[]).map((attr: any) => ( {(record.attributes as any[]).map((attr: any) => (
<div key={attr.name}> <div key={attr.name}>
<strong>{attr.name}:</strong> {Array.isArray(attr.options) ? attr.options.join(', ') : ''} <strong>{attr.name}:</strong>{' '}
{Array.isArray(attr.options) ? attr.options.join(', ') : ''}
</div> </div>
))} ))}
</div> </div>
@ -240,6 +270,29 @@ const ProductsPage: React.FC = () => {
return null; return null;
}, },
}, },
{
// 标签
title: '标签',
dataIndex: 'tags',
hideInSearch: true,
width: 250,
render: (_, record) => {
// 检查 record.tags 是否存在并且是一个数组
if (record.tags && Array.isArray(record.tags)) {
// 遍历 tags 数组并为每个 tag 对象渲染一个 Tag 组件
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{record.tags.map((tag: any) => (
// 使用 tag.name 作为 key, 因为 tag.id 可能是对象, 会导致 React key 错误
<Tag key={tag.name}>{tag.name}</Tag>
))}
</div>
);
}
// 如果 record.tags 不是一个有效的数组,则不渲染任何内容
return null;
},
},
{ {
// 创建时间 // 创建时间
title: '创建时间', title: '创建时间',
@ -263,16 +316,38 @@ const ProductsPage: React.FC = () => {
width: '200', width: '200',
render: (_, record) => ( render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<UpdateForm tableRef={actionRef} values={record} config={config} siteId={siteId} /> <UpdateForm
tableRef={actionRef}
values={record}
config={config}
siteId={siteId}
/>
<UpdateStatus tableRef={actionRef} values={record} siteId={siteId} /> <UpdateStatus tableRef={actionRef} values={record} siteId={siteId} />
{siteId && (
<ErpProductBindModal
trigger={
<Button
type="link"
title={record.erpProduct ? '换绑ERP产品' : '绑定ERP产品'}
>
{record.erpProduct ? '换绑' : '绑定'}
</Button>
}
siteProduct={record}
siteId={siteId}
onBindSuccess={() => {
actionRef.current?.reload();
}}
/>
)}
<Button <Button
type="link" type="link"
title="店铺链接" title="店铺链接"
icon={<LinkOutlined />} icon={<LinkOutlined />}
disabled={!record.frontendUrl} disabled={!record.permalink}
onClick={() => { onClick={() => {
if (record.frontendUrl) { if (record.permalink) {
window.open(record.frontendUrl, '_blank', 'noopener,noreferrer'); window.open(record.permalink, '_blank', 'noopener,noreferrer');
} else { } else {
message.warning('未能生成店铺链接'); message.warning('未能生成店铺链接');
} }
@ -284,7 +359,9 @@ const ProductsPage: React.FC = () => {
description="确认删除?" description="确认删除?"
onConfirm={async () => { onConfirm={async () => {
try { try {
await request(`/site-api/${siteId}/products/${record.id}`, { method: 'DELETE' }); await request(`/site-api/${siteId}/products/${record.id}`, {
method: 'DELETE',
});
message.success('删除成功'); message.success('删除成功');
actionRef.current?.reload(); actionRef.current?.reload();
} catch (e: any) { } catch (e: any) {
@ -295,11 +372,11 @@ const ProductsPage: React.FC = () => {
<Button type="link" danger title="删除" icon={<DeleteFilled />} /> <Button type="link" danger title="删除" icon={<DeleteFilled />} />
</Popconfirm> </Popconfirm>
{record.type === 'simple' && record.sku ? ( {record.type === 'simple' && record.sku ? (
<SetComponent <SetComponent
tableRef={actionRef} tableRef={actionRef}
values={record} values={record}
isProduct={true} isProduct={true}
/> />
) : ( ) : (
<></> <></>
)} )}
@ -315,7 +392,7 @@ const ProductsPage: React.FC = () => {
<ProTable<API.UnifiedProductDTO> <ProTable<API.UnifiedProductDTO>
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
pagination={{ pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000','2000'], pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
showSizeChanger: true, showSizeChanger: true,
defaultPageSize: 10, defaultPageSize: 10,
}} }}
@ -346,8 +423,13 @@ const ProductsPage: React.FC = () => {
page, page,
per_page: pageSize, per_page: pageSize,
...where, ...where,
...(orderObj ? { sortField: Object.keys(orderObj)[0], sortOrder: Object.values(orderObj)[0] } : {}), ...(orderObj
} ? {
sortField: Object.keys(orderObj)[0],
sortOrder: Object.values(orderObj)[0],
}
: {}),
},
}); });
if (!response.success) { if (!response.success) {
@ -385,24 +467,37 @@ const ProductsPage: React.FC = () => {
siteId={siteId} siteId={siteId}
/>, />,
<ImportCsv tableRef={actionRef} siteId={siteId} />, <ImportCsv tableRef={actionRef} siteId={siteId} />,
<Button onClick={async () => { <Button
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined; onClick={async () => {
const res = await request(`/site-api/${siteId}/products/export`, { params: { ids: idsParam } }); const idsParam = selectedRowKeys.length
if (res?.success && res?.data?.csv) { ? (selectedRowKeys as any[]).join(',')
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); : undefined;
const url = URL.createObjectURL(blob); const res = await request(`/site-api/${siteId}/products/export`, {
const a = document.createElement('a'); params: { ids: idsParam },
a.href = url; });
a.download = 'products.csv'; if (res?.success && res?.data?.csv) {
a.click(); const blob = new Blob([res.data.csv], {
URL.revokeObjectURL(url); type: 'text/csv;charset=utf-8;',
} });
}}></Button>, const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'products.csv';
a.click();
URL.revokeObjectURL(url);
}
}}
>
</Button>,
]} ]}
expandable={{ expandable={{
rowExpandable: (record) => record.type === 'variable', rowExpandable: (record) => record.type === 'variable',
expandedRowRender: (record) => { expandedRowRender: (record) => {
const productExternalId = (record as any).externalProductId || (record as any).external_product_id || record.id; const productExternalId =
(record as any).externalProductId ||
(record as any).external_product_id ||
record.id;
const innerColumns: ProColumns<any>[] = [ const innerColumns: ProColumns<any>[] = [
{ {
title: 'ID', title: 'ID',
@ -411,12 +506,20 @@ const ProductsPage: React.FC = () => {
width: 120, width: 120,
render: (_, row) => { render: (_, row) => {
return row?.id ?? '-'; return row?.id ?? '-';
} },
}, },
{ title: '变体名', dataIndex: 'name' }, { title: '变体名', dataIndex: 'name' },
{ title: 'sku', dataIndex: 'sku' }, { title: 'sku', dataIndex: 'sku' },
{ title: '常规价格', dataIndex: 'regular_price', hideInSearch: true }, {
{ title: '销售价格', dataIndex: 'sale_price', hideInSearch: true }, title: '常规价格',
dataIndex: 'regular_price',
hideInSearch: true,
},
{
title: '销售价格',
dataIndex: 'sale_price',
hideInSearch: true,
},
{ {
title: 'Attributes', title: 'Attributes',
dataIndex: 'attributes', dataIndex: 'attributes',
@ -425,7 +528,13 @@ const ProductsPage: React.FC = () => {
// 检查 row.attributes 是否存在并且是一个数组 // 检查 row.attributes 是否存在并且是一个数组
if (row.attributes && Array.isArray(row.attributes)) { if (row.attributes && Array.isArray(row.attributes)) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> <div
style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
}}
>
{(row.attributes as any[]).map((attr: any) => ( {(row.attributes as any[]).map((attr: any) => (
<div key={attr.name}> <div key={attr.name}>
<strong>{attr.name}:</strong> {attr.option} <strong>{attr.name}:</strong> {attr.option}
@ -443,11 +552,20 @@ const ProductsPage: React.FC = () => {
valueType: 'option', valueType: 'option',
render: (_, row) => ( render: (_, row) => (
<> <>
<UpdateVaritation tableRef={actionRef} values={row} siteId={siteId} productId={productExternalId} /> <UpdateVaritation
tableRef={actionRef}
values={row}
siteId={siteId}
productId={productExternalId}
/>
{row.sku ? ( {row.sku ? (
<> <>
<Divider type="vertical" /> <Divider type="vertical" />
<SetComponent tableRef={actionRef} values={row} isProduct={false} /> <SetComponent
tableRef={actionRef}
values={row}
isProduct={false}
/>
</> </>
) : ( ) : (
<></> <></>

View File

@ -1,4 +1,3 @@
import React from 'react'; import React from 'react';
interface ReviewFormProps { interface ReviewFormProps {
@ -9,7 +8,13 @@ interface ReviewFormProps {
onSuccess: () => void; onSuccess: () => void;
} }
const ReviewForm: React.FC<ReviewFormProps> = ({ open, editing, siteId, onClose, onSuccess }) => { const ReviewForm: React.FC<ReviewFormProps> = ({
open,
editing,
siteId,
onClose,
onSuccess,
}) => {
// // 这是一个临时的占位符组件 // // 这是一个临时的占位符组件
// // 你可以在这里实现表单逻辑 // // 你可以在这里实现表单逻辑
if (!open) { if (!open) {

View File

@ -1,9 +1,17 @@
import React, { useRef, useState } from 'react'; import {
import { ActionType, ProTable, ProColumns, ProCard } from '@ant-design/pro-components'; siteapicontrollerDeletereview,
import { Button, Popconfirm, message, Space } from 'antd'; siteapicontrollerGetreviews,
import { siteapicontrollerGetreviews, siteapicontrollerDeletereview } from '@/servers/api/siteApi'; } from '@/servers/api/siteApi';
import ReviewForm from './ReviewForm'; import {
ActionType,
ProCard,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { useParams } from '@umijs/max'; import { useParams } from '@umijs/max';
import { Button, message, Popconfirm, Space } from 'antd';
import React, { useRef, useState } from 'react';
import ReviewForm from './ReviewForm';
const ReviewsPage: React.FC = () => { const ReviewsPage: React.FC = () => {
const params = useParams(); const params = useParams();
@ -31,26 +39,40 @@ const ReviewsPage: React.FC = () => {
width: 150, width: 150,
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button type="link" style={{padding:0}} onClick={() => { <Button
setEditing(record); type="link"
setOpen(true); style={{ padding: 0 }}
}}></Button> onClick={() => {
<Popconfirm title="确定删除吗?" onConfirm={async () => { setEditing(record);
if (record.id) { setOpen(true);
try { }}
const response = await siteapicontrollerDeletereview({ siteId, id: String(record.id) }); >
if (response.success) {
message.success('删除成功'); </Button>
actionRef.current?.reload(); <Popconfirm
} else { title="确定删除吗?"
onConfirm={async () => {
if (record.id) {
try {
const response = await siteapicontrollerDeletereview({
siteId,
id: String(record.id),
});
if (response.success) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error('删除失败');
}
} catch (error) {
message.error('删除失败'); message.error('删除失败');
} }
} catch (error) {
message.error('删除失败');
} }
} }}
}}> >
<Button type='link' danger></Button> <Button type="link" danger>
</Button>
</Popconfirm> </Popconfirm>
</Space> </Space>
), ),
@ -63,7 +85,12 @@ const ReviewsPage: React.FC = () => {
columns={columns} columns={columns}
actionRef={actionRef} actionRef={actionRef}
request={async (params) => { request={async (params) => {
const response = await siteapicontrollerGetreviews({ ...params, siteId, page: params.current, per_page: params.pageSize }); const response = await siteapicontrollerGetreviews({
...params,
siteId,
page: params.current,
per_page: params.pageSize,
});
return { return {
data: response.data.items, data: response.data.items,
success: true, success: true,
@ -76,10 +103,13 @@ const ReviewsPage: React.FC = () => {
}} }}
headerTitle="评论列表" headerTitle="评论列表"
toolBarRender={() => [ toolBarRender={() => [
<Button type="primary" onClick={() => { <Button
setEditing(null); type="primary"
setOpen(true); onClick={() => {
}}> setEditing(null);
setOpen(true);
}}
>
</Button>, </Button>,
]} ]}

View File

@ -1,16 +1,13 @@
import { sitecontrollerAll } from '@/servers/api/site';
import {} from '@/servers/api/subscription'; import {} from '@/servers/api/subscription';
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
import { import {
ActionType, ActionType,
DrawerForm,
PageContainer, PageContainer,
ProColumns, ProColumns,
ProFormSelect,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { useParams } from '@umijs/max'; import { useParams } from '@umijs/max';
import { App, Button, Drawer, List, Popconfirm, Space, Tag } from 'antd'; import { App, Button, Drawer, List, Popconfirm, Space, Tag } from 'antd';
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { request } from 'umi'; import { request } from 'umi';
@ -116,8 +113,16 @@ const SubscriptionsPage: React.FC = () => {
valueType: 'option', valueType: 'option',
render: (_, row) => ( render: (_, row) => (
<Space> <Space>
<Button type="link" title="编辑" icon={<EditOutlined />} onClick={() => setEditing(row)} /> <Button
<Popconfirm title="确定删除?" onConfirm={() => message.info('订阅删除未实现')}> type="link"
title="编辑"
icon={<EditOutlined />}
onClick={() => setEditing(row)}
/>
<Popconfirm
title="确定删除?"
onConfirm={() => message.info('订阅删除未实现')}
>
<Button type="link" danger title="删除" icon={<DeleteFilled />} /> <Button type="link" danger title="删除" icon={<DeleteFilled />} />
</Popconfirm> </Popconfirm>
</Space> </Space>
@ -142,7 +147,7 @@ const SubscriptionsPage: React.FC = () => {
...params, ...params,
page: params.current, page: params.current,
per_page: params.pageSize, per_page: params.pageSize,
} },
}); });
if (!response.success) { if (!response.success) {
@ -165,15 +170,29 @@ const SubscriptionsPage: React.FC = () => {
// 工具栏:订阅同步入口 // 工具栏:订阅同步入口
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }} rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
toolBarRender={() => [ toolBarRender={() => [
<Button type="primary" title="新增" icon={<PlusOutlined />} onClick={() => message.info('订阅新增未实现')} />, <Button
<Button title="批量编辑" icon={<EditOutlined />} onClick={() => message.info('批量编辑未实现')} />, type="primary"
title="新增"
icon={<PlusOutlined />}
onClick={() => message.info('订阅新增未实现')}
/>,
<Button
title="批量编辑"
icon={<EditOutlined />}
onClick={() => message.info('批量编辑未实现')}
/>,
<Button <Button
title="批量导出" title="批量导出"
onClick={async () => { onClick={async () => {
if (!siteId) return; if (!siteId) return;
const res = await request(`/site-api/${siteId}/subscriptions/export`, { params: {} }); const res = await request(
`/site-api/${siteId}/subscriptions/export`,
{ params: {} },
);
if (res?.success && res?.data?.csv) { if (res?.success && res?.data?.csv) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([res.data.csv], {
type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@ -185,8 +204,14 @@ const SubscriptionsPage: React.FC = () => {
} }
}} }}
/>, />,
<Button title="批量删除" danger icon={<DeleteFilled />} onClick={() => message.info('订阅删除未实现')} /> <Button
]} /> title="批量删除"
danger
icon={<DeleteFilled />}
onClick={() => message.info('订阅删除未实现')}
/>,
]}
/>
<Drawer <Drawer
open={drawerOpen} open={drawerOpen}
title={drawerTitle} title={drawerTitle}

View File

@ -1,22 +1,13 @@
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
import { ORDER_STATUS_ENUM } from '@/constants'; import { ORDER_STATUS_ENUM } from '@/constants';
import { import {
logisticscontrollerCreateshipment, logisticscontrollerCreateshipment,
logisticscontrollerDelshipment,
logisticscontrollerGetshipmentfee,
logisticscontrollerGetshippingaddresslist, logisticscontrollerGetshippingaddresslist,
} from '@/servers/api/logistics'; } from '@/servers/api/logistics';
import { import { productcontrollerSearchproducts } from '@/servers/api/product';
productcontrollerSearchproducts,
} from '@/servers/api/product';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock'; import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { formatShipmentState, formatSource } from '@/utils/format';
import { import {
CodeSandboxOutlined, CodeSandboxOutlined,
CopyOutlined,
DeleteFilled,
EditOutlined, EditOutlined,
FileDoneOutlined,
PlusOutlined, PlusOutlined,
TagsOutlined, TagsOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@ -25,36 +16,20 @@ import {
DrawerForm, DrawerForm,
ModalForm, ModalForm,
ProColumns, ProColumns,
ProDescriptions,
ProForm, ProForm,
ProFormDatePicker, ProFormDatePicker,
ProFormDependency,
ProFormDigit, ProFormDigit,
ProFormInstance, ProFormInstance,
ProFormItem,
ProFormList, ProFormList,
ProFormRadio,
ProFormSelect, ProFormSelect,
ProFormText, ProFormText,
ProFormTextArea, ProFormTextArea,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { import { App, Button, Col, Divider, Row } from 'antd';
App,
Button,
Card,
Col,
Divider,
Drawer,
Empty,
Popconfirm,
Radio,
Row,
} from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import RelatedOrders from '@/pages/Subscription/Orders/RelatedOrders';
const region = { const region = {
AB: 'Alberta', AB: 'Alberta',
@ -88,22 +63,26 @@ export const OrderNote: React.FC<{
} }
onFinish={async (values: any) => { onFinish={async (values: any) => {
if (!siteId) { if (!siteId) {
message.error('缺少站点ID'); message.error('缺少站点ID');
return false; return false;
} }
try { try {
// Use new API for creating note // Use new API for creating note
const { success, data } = await request(`/site-api/${siteId}/orders/${id}/notes`, { const { success, data } = await request(
method: 'POST', `/site-api/${siteId}/orders/${id}/notes`,
data: { {
...values, method: 'POST',
orderId: id, // API might not need this in body if in URL, but keeping for compatibility if adapter needs it data: {
} ...values,
}); orderId: id, // API might not need this in body if in URL, but keeping for compatibility if adapter needs it
},
},
);
// Check success based on response structure // Check success based on response structure
if (success === false) { // Assuming response.util returns success: boolean if (success === false) {
throw new Error('提交失败'); // Assuming response.util returns success: boolean
throw new Error('提交失败');
} }
descRef?.current?.reload(); descRef?.current?.reload();
@ -255,7 +234,9 @@ export const Shipping: React.FC<{
request={async () => { request={async () => {
if (!siteId) return {}; if (!siteId) return {};
// Use site-api to get order detail // Use site-api to get order detail
const { data, success } = await request(`/site-api/${siteId}/orders/${id}`); const { data, success } = await request(
`/site-api/${siteId}/orders/${id}`,
);
if (!success || !data) return {}; if (!success || !data) return {};
@ -263,18 +244,15 @@ export const Shipping: React.FC<{
const sales = data.sales || []; const sales = data.sales || [];
// Logic for merging duplicate products // Logic for merging duplicate products
const mergedSales = sales.reduce( const mergedSales = sales.reduce((acc: any[], cur: any) => {
(acc: any[], cur: any) => { let idx = acc.findIndex((v: any) => v.productId === cur.productId);
let idx = acc.findIndex((v: any) => v.productId === cur.productId); if (idx === -1) {
if (idx === -1) { acc.push({ ...cur }); // clone
acc.push({ ...cur }); // clone } else {
} else { acc[idx].quantity += cur.quantity;
acc[idx].quantity += cur.quantity; }
} return acc;
return acc; }, []);
},
[],
);
// Update data.sales // Update data.sales
data.sales = mergedSales; data.sales = mergedSales;
@ -450,10 +428,7 @@ export const Shipping: React.FC<{
</ProFormList> </ProFormList>
</Col> </Col>
<Col span={12}> <Col span={12}>
<ProFormList <ProFormList label="发货产品" name="sales">
label="发货产品"
name="sales"
>
<ProForm.Group> <ProForm.Group>
<ProFormSelect <ProFormSelect
params={{ options }} params={{ options }}
@ -548,11 +523,11 @@ export const Shipping: React.FC<{
name={['details', 'origin', 'name']} name={['details', 'origin', 'name']}
rules={[{ required: true, message: '请输入公司名称' }]} rules={[{ required: true, message: '请输入公司名称' }]}
/> />
{/* Simplified for brevity - assume standard fields remain */} {/* Simplified for brevity - assume standard fields remain */}
</ProForm.Group> </ProForm.Group>
</Col> </Col>
</Row> </Row>
{/* ... Packaging fields ... */} {/* ... Packaging fields ... */}
</ModalForm> </ModalForm>
); );
}; };
@ -588,20 +563,23 @@ export const CreateOrder: React.FC<{
}} }}
onFinish={async ({ items, details, ...data }) => { onFinish={async ({ items, details, ...data }) => {
if (!siteId) { if (!siteId) {
message.error('缺少站点ID'); message.error('缺少站点ID');
return false; return false;
} }
try { try {
// Use site-api to create order // Use site-api to create order
const { success, message: errMsg } = await request(`/site-api/${siteId}/orders`, { const { success, message: errMsg } = await request(
method: 'POST', `/site-api/${siteId}/orders`,
data: { {
...data, method: 'POST',
customer_email: data?.billing?.email, data: {
billing_phone: data?.billing?.phone, ...data,
// map other fields if needed for Adapter customer_email: data?.billing?.email,
} billing_phone: data?.billing?.phone,
}); // map other fields if needed for Adapter
},
},
);
if (success === false) throw new Error(errMsg); // Check success if (success === false) throw new Error(errMsg); // Check success
@ -621,7 +599,7 @@ export const CreateOrder: React.FC<{
}} }}
> >
{/* ... Form fields ... same as before */} {/* ... Form fields ... same as before */}
<ProFormDigit <ProFormDigit
label="金额" label="金额"
name="total" name="total"
rules={[{ required: true, message: '请输入金额' }]} rules={[{ required: true, message: '请输入金额' }]}
@ -654,22 +632,27 @@ export const BatchEditOrders: React.FC<{
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
let ok = 0, fail = 0; let ok = 0,
fail = 0;
for (const id of selectedRowKeys) { for (const id of selectedRowKeys) {
try { try {
// Remove undefined values // Remove undefined values
const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== '')); const data = Object.fromEntries(
if (Object.keys(data).length === 0) continue; Object.entries(values).filter(
([_, v]) => v !== undefined && v !== '',
),
);
if (Object.keys(data).length === 0) continue;
const res = await request(`/site-api/${siteId}/orders/${id}`, { const res = await request(`/site-api/${siteId}/orders/${id}`, {
method: 'PUT', method: 'PUT',
data: data, data: data,
}); });
if (res.success) ok++; if (res.success) ok++;
else fail++; else fail++;
} catch (e) { } catch (e) {
fail++; fail++;
} }
} }
message.success(`成功 ${ok}, 失败 ${fail}`); message.success(`成功 ${ok}, 失败 ${fail}`);
tableRef.current?.reload(); tableRef.current?.reload();
@ -699,126 +682,133 @@ export const EditOrder: React.FC<{
return ( return (
<DrawerForm <DrawerForm
formRef={formRef} formRef={formRef}
title="编辑订单" title="编辑订单"
trigger={ trigger={
<Button <Button
type="primary" type="primary"
size="small" size="small"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => setActiveLine(record.id)} onClick={() => setActiveLine(record.id)}
>
</Button>
}
drawerProps={{
destroyOnHidden: true,
width: '60vw',
}}
request={async () => {
if (!siteId) return {};
const { data, success } = await request(`/site-api/${siteId}/orders/${orderId}`);
if (!success || !data) return {};
const sales = data.sales || [];
const mergedSales = sales.reduce(
(acc: any[], cur: any) => {
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
if (idx === -1) {
acc.push(cur);
} else {
acc[idx].quantity += cur.quantity;
}
return acc;
},
[],
);
data.sales = mergedSales;
return data;
}}
onFinish={async (values) => {
if (!siteId) return false;
try {
const res = await request(`/site-api/${siteId}/orders/${orderId}`, {
method: 'PUT',
data: values
});
if (res.success) {
message.success('更新成功');
tableRef.current?.reload();
return true;
}
message.error(res.message || '更新失败');
return false;
} catch(e: any) {
message.error(e.message || '更新失败');
return false;
}
}}
>
<ProForm.Group title="基本信息">
<ProFormText name="number" label="订单号" readonly />
<ProFormSelect name="status" label="状态" valueEnum={ORDER_STATUS_ENUM} />
<ProFormText name="currency" label="币种" readonly />
<ProFormText name="payment_method" label="支付方式" readonly />
<ProFormText name="transaction_id" label="交易ID" readonly />
<ProFormDatePicker name="date_created" label="创建时间" readonly fieldProps={{style: {width: '100%'}}} />
</ProForm.Group>
<Divider />
<ProForm.Group title="账单地址">
<ProFormText name={['billing', 'first_name']} label="名" />
<ProFormText name={['billing', 'last_name']} label="姓" />
<ProFormText name={['billing', 'company']} label="公司" />
<ProFormText name={['billing', 'address_1']} label="地址1" />
<ProFormText name={['billing', 'address_2']} label="地址2" />
<ProFormText name={['billing', 'city']} label="城市" />
<ProFormText name={['billing', 'state']} label="省/州" />
<ProFormText name={['billing', 'postcode']} label="邮编" />
<ProFormText name={['billing', 'country']} label="国家" />
<ProFormText name={['billing', 'email']} label="邮箱" />
<ProFormText name={['billing', 'phone']} label="电话" />
</ProForm.Group>
<Divider />
<ProForm.Group title="收货地址">
<ProFormText name={['shipping', 'first_name']} label="名" />
<ProFormText name={['shipping', 'last_name']} label="姓" />
<ProFormText name={['shipping', 'company']} label="公司" />
<ProFormText name={['shipping', 'address_1']} label="地址1" />
<ProFormText name={['shipping', 'address_2']} label="地址2" />
<ProFormText name={['shipping', 'city']} label="城市" />
<ProFormText name={['shipping', 'state']} label="省/州" />
<ProFormText name={['shipping', 'postcode']} label="邮编" />
<ProFormText name={['shipping', 'country']} label="国家" />
<ProFormText name={['shipping', 'phone']} label="电话" />
</ProForm.Group>
<Divider />
<ProFormTextArea name="customer_note" label="客户备注" />
<Divider />
<ProFormList
name="sales"
label="商品列表"
readonly
actionRender={() => []}
> >
<ProForm.Group>
<ProFormText name="name" label="商品名" /> </Button>
<ProFormText name="sku" label="SKU" /> }
<ProFormDigit name="quantity" label="数量" /> drawerProps={{
<ProFormText name="total" label="总价" /> destroyOnHidden: true,
</ProForm.Group> width: '60vw',
</ProFormList> }}
request={async () => {
if (!siteId) return {};
const { data, success } = await request(
`/site-api/${siteId}/orders/${orderId}`,
);
if (!success || !data) return {};
<ProFormText name="total" label="订单总额" readonly /> const sales = data.sales || [];
const mergedSales = sales.reduce((acc: any[], cur: any) => {
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
if (idx === -1) {
acc.push(cur);
} else {
acc[idx].quantity += cur.quantity;
}
return acc;
}, []);
data.sales = mergedSales;
return data;
}}
onFinish={async (values) => {
if (!siteId) return false;
try {
const res = await request(`/site-api/${siteId}/orders/${orderId}`, {
method: 'PUT',
data: values,
});
if (res.success) {
message.success('更新成功');
tableRef.current?.reload();
return true;
}
message.error(res.message || '更新失败');
return false;
} catch (e: any) {
message.error(e.message || '更新失败');
return false;
}
}}
>
<ProForm.Group title="基本信息">
<ProFormText name="number" label="订单号" readonly />
<ProFormSelect
name="status"
label="状态"
valueEnum={ORDER_STATUS_ENUM}
/>
<ProFormText name="currency" label="币种" readonly />
<ProFormText name="payment_method" label="支付方式" readonly />
<ProFormText name="transaction_id" label="交易ID" readonly />
<ProFormDatePicker
name="date_created"
label="创建时间"
readonly
fieldProps={{ style: { width: '100%' } }}
/>
</ProForm.Group>
<Divider />
<ProForm.Group title="账单地址">
<ProFormText name={['billing', 'first_name']} label="名" />
<ProFormText name={['billing', 'last_name']} label="姓" />
<ProFormText name={['billing', 'company']} label="公司" />
<ProFormText name={['billing', 'address_1']} label="地址1" />
<ProFormText name={['billing', 'address_2']} label="地址2" />
<ProFormText name={['billing', 'city']} label="城市" />
<ProFormText name={['billing', 'state']} label="省/州" />
<ProFormText name={['billing', 'postcode']} label="邮编" />
<ProFormText name={['billing', 'country']} label="国家" />
<ProFormText name={['billing', 'email']} label="邮箱" />
<ProFormText name={['billing', 'phone']} label="电话" />
</ProForm.Group>
<Divider />
<ProForm.Group title="收货地址">
<ProFormText name={['shipping', 'first_name']} label="名" />
<ProFormText name={['shipping', 'last_name']} label="姓" />
<ProFormText name={['shipping', 'company']} label="公司" />
<ProFormText name={['shipping', 'address_1']} label="地址1" />
<ProFormText name={['shipping', 'address_2']} label="地址2" />
<ProFormText name={['shipping', 'city']} label="城市" />
<ProFormText name={['shipping', 'state']} label="省/州" />
<ProFormText name={['shipping', 'postcode']} label="邮编" />
<ProFormText name={['shipping', 'country']} label="国家" />
<ProFormText name={['shipping', 'phone']} label="电话" />
</ProForm.Group>
<Divider />
<ProFormTextArea name="customer_note" label="客户备注" />
<Divider />
<ProFormList
name="sales"
label="商品列表"
readonly
actionRender={() => []}
>
<ProForm.Group>
<ProFormText name="name" label="商品名" />
<ProFormText name="sku" label="SKU" />
<ProFormDigit name="quantity" label="数量" />
<ProFormText name="total" label="总价" />
</ProForm.Group>
</ProFormList>
<ProFormText name="total" label="订单总额" readonly />
</DrawerForm> </DrawerForm>
); );
}; };

View File

@ -0,0 +1,177 @@
import { productcontrollerSearchproducts } from '@/servers/api/product';
import { ModalForm, ProTable } from '@ant-design/pro-components';
import { Form, message } from 'antd';
import React, { useState } from 'react';
interface ErpProductBindModalProps {
trigger: React.ReactNode;
siteProduct: any;
siteId: string;
onBindSuccess?: () => void;
}
export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
trigger,
siteProduct,
siteId,
onBindSuccess,
}) => {
const [form] = Form.useForm();
const [selectedProduct, setSelectedProduct] = useState<any>(null);
const handleBind = async (values: any) => {
if (!selectedProduct) {
message.error('请选择一个ERP产品');
return false;
}
try {
// 调用绑定API
const response = await fetch(
`/api/site-api/${siteId}/products/${siteProduct.id}/bind-erp`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
erpProductId: selectedProduct.id,
siteSku: siteProduct.sku,
}),
},
);
const result = await response.json();
if (result.success) {
message.success('ERP产品绑定成功');
onBindSuccess?.();
return true;
} else {
message.error(result.message || '绑定失败');
return false;
}
} catch (error) {
message.error('绑定请求失败');
return false;
}
};
return (
<ModalForm
title="绑定ERP产品"
trigger={trigger}
form={form}
modalProps={{
destroyOnClose: true,
width: 800,
}}
onFinish={handleBind}
>
<div style={{ marginBottom: 16 }}>
<strong></strong>
<div>SKU: {siteProduct.sku}</div>
<div>: {siteProduct.name}</div>
{siteProduct.erpProduct && (
<div style={{ color: '#ff4d4f' }}>
ERP产品{siteProduct.erpProduct.sku} -{' '}
{siteProduct.erpProduct.name}
</div>
)}
</div>
<ProTable
rowKey="id"
search={{
labelWidth: 'auto',
}}
request={async (params) => {
const response = await productcontrollerSearchproducts({
keyword: params.keyword,
page: params.current,
per_page: params.pageSize,
});
if (response.success) {
return {
data: response.data.items,
total: response.data.total,
success: true,
};
}
return {
data: [],
total: 0,
success: false,
};
}}
columns={[
{
title: 'ID',
dataIndex: 'id',
width: 80,
},
{
title: 'SKU',
dataIndex: 'sku',
copyable: true,
},
{
title: '产品名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '中文名称',
dataIndex: 'nameCn',
ellipsis: true,
},
{
title: '分类',
dataIndex: ['category', 'name'],
ellipsis: true,
},
{
title: '价格',
dataIndex: 'price',
width: 100,
render: (text) => `¥${text}`,
},
]}
rowSelection={{
type: 'radio',
onChange: (_, selectedRows) => {
setSelectedProduct(selectedRows[0]);
},
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `${total}`,
}}
toolBarRender={false}
options={false}
scroll={{ y: 400 }}
/>
{selectedProduct && (
<div
style={{
marginTop: 16,
padding: 12,
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
}}
>
<strong></strong>
<div>SKU: {selectedProduct.sku}</div>
<div>: {selectedProduct.name}</div>
{selectedProduct.nameCn && (
<div>: {selectedProduct.nameCn}</div>
)}
</div>
)}
</ModalForm>
);
};

View File

@ -1,10 +1,5 @@
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants'; import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
import { import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
productcontrollerProductbysku,
productcontrollerSearchproducts,
} from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { EditOutlined, PlusOutlined, DeleteFilled } from '@ant-design/icons';
import { import {
ActionType, ActionType,
DrawerForm, DrawerForm,
@ -14,7 +9,6 @@ import {
ProFormList, ProFormList,
ProFormSelect, ProFormSelect,
ProFormText, ProFormText,
ProFormUploadButton,
ProFormTextArea, ProFormTextArea,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
@ -33,15 +27,19 @@ export const CreateProduct: React.FC<{
<DrawerForm <DrawerForm
title="新增产品" title="新增产品"
form={form} form={form}
trigger={<Button type="primary" title="新增产品" icon={<PlusOutlined />}></Button>} trigger={
<Button type="primary" title="新增产品" icon={<PlusOutlined />}>
</Button>
}
autoFocusFirstInput autoFocusFirstInput
drawerProps={{ drawerProps={{
destroyOnHidden: true, destroyOnHidden: true,
}} }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) { if (!siteId) {
message.error('缺少站点ID'); message.error('缺少站点ID');
return false; return false;
} }
try { try {
// 将数字字段转换为字符串以匹配DTO // 将数字字段转换为字符串以匹配DTO
@ -50,7 +48,10 @@ export const CreateProduct: React.FC<{
type: values.type || 'simple', type: values.type || 'simple',
regular_price: values.regular_price?.toString() || '', regular_price: values.regular_price?.toString() || '',
sale_price: values.sale_price?.toString() || '', sale_price: values.sale_price?.toString() || '',
price: values.sale_price?.toString() || values.regular_price?.toString() || '', price:
values.sale_price?.toString() ||
values.regular_price?.toString() ||
'',
}; };
await request(`/site-api/${siteId}/products`, { await request(`/site-api/${siteId}/products`, {
@ -87,16 +88,8 @@ export const CreateProduct: React.FC<{
width="lg" width="lg"
rules={[{ required: true, message: '请输入SKU' }]} rules={[{ required: true, message: '请输入SKU' }]}
/> />
<ProFormTextArea <ProFormTextArea name="description" label="描述" width="lg" />
name="description" <ProFormTextArea name="short_description" label="简短描述" width="lg" />
label="描述"
width="lg"
/>
<ProFormTextArea
name="short_description"
label="简短描述"
width="lg"
/>
<ProFormDigit <ProFormDigit
name="regular_price" name="regular_price"
label="常规价格" label="常规价格"
@ -116,17 +109,17 @@ export const CreateProduct: React.FC<{
fieldProps={{ precision: 0 }} fieldProps={{ precision: 0 }}
/> />
<ProFormSelect <ProFormSelect
name="status" name="status"
label="产品状态" label="产品状态"
width="md" width="md"
valueEnum={PRODUCT_STATUS_ENUM} valueEnum={PRODUCT_STATUS_ENUM}
initialValue="publish" initialValue="publish"
/> />
<ProFormSelect <ProFormSelect
name="stock_status" name="stock_status"
label="库存状态" label="库存状态"
width="md" width="md"
valueEnum={PRODUCT_STOCK_STATUS_ENUM} valueEnum={PRODUCT_STOCK_STATUS_ENUM}
/> />
</ProForm.Group> </ProForm.Group>
<Divider /> <Divider />
@ -135,12 +128,12 @@ export const CreateProduct: React.FC<{
label="产品图片" label="产品图片"
initialValue={[{}]} initialValue={[{}]}
creatorButtonProps={{ creatorButtonProps={{
creatorButtonText: '添加图片', creatorButtonText: '添加图片',
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText name="src" label="图片URL" width="lg" /> <ProFormText name="src" label="图片URL" width="lg" />
<ProFormText name="alt" label="替代文本" width="md" /> <ProFormText name="alt" label="替代文本" width="md" />
</ProForm.Group> </ProForm.Group>
</ProFormList> </ProFormList>
<Divider /> <Divider />
@ -149,38 +142,38 @@ export const CreateProduct: React.FC<{
label="产品属性" label="产品属性"
initialValue={[]} initialValue={[]}
creatorButtonProps={{ creatorButtonProps={{
creatorButtonText: '添加属性', creatorButtonText: '添加属性',
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText name="name" label="属性名称" width="md" /> <ProFormText name="name" label="属性名称" width="md" />
<ProFormSelect <ProFormSelect
name="options" name="options"
label="选项" label="选项"
width="md" width="md"
mode="tags" mode="tags"
placeholder="输入选项并回车" placeholder="输入选项并回车"
/> />
<ProFormSelect <ProFormSelect
name="visible" name="visible"
label="可见性" label="可见性"
width="xs" width="xs"
options={[ options={[
{ label: '可见', value: true }, { label: '可见', value: true },
{ label: '隐藏', value: false }, { label: '隐藏', value: false },
]} ]}
initialValue={true} initialValue={true}
/> />
<ProFormSelect <ProFormSelect
name="variation" name="variation"
label="用于变体" label="用于变体"
width="xs" width="xs"
options={[ options={[
{ label: '是', value: true }, { label: '是', value: true },
{ label: '否', value: false }, { label: '否', value: false },
]} ]}
initialValue={false} initialValue={false}
/> />
</ProForm.Group> </ProForm.Group>
</ProFormList> </ProFormList>
</DrawerForm> </DrawerForm>
@ -197,7 +190,9 @@ export const UpdateStatus: React.FC<{
// 转换初始值,将字符串价格转换为数字以便编辑 // 转换初始值,将字符串价格转换为数字以便编辑
const formValues = { const formValues = {
...initialValues, ...initialValues,
stock_quantity: initialValues.stock_quantity ? parseInt(initialValues.stock_quantity) : 0, stock_quantity: initialValues.stock_quantity
? parseInt(initialValues.stock_quantity)
: 0,
}; };
return ( return (
@ -209,7 +204,9 @@ export const UpdateStatus: React.FC<{
title="修改产品状态" title="修改产品状态"
initialValues={formValues} initialValues={formValues}
trigger={ trigger={
<Button type="link" title="修改状态" icon={<EditOutlined />}></Button> <Button type="link" title="修改状态" icon={<EditOutlined />}>
</Button>
} }
autoFocusFirstInput autoFocusFirstInput
drawerProps={{ drawerProps={{
@ -217,17 +214,17 @@ export const UpdateStatus: React.FC<{
}} }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) { if (!siteId) {
message.error('缺少站点ID'); message.error('缺少站点ID');
return false; return false;
} }
try { try {
await request(`/site-api/${siteId}/products/${initialValues.id}`, { await request(`/site-api/${siteId}/products/${initialValues.id}`, {
method: 'PUT', method: 'PUT',
data: { data: {
status: values.status, status: values.status,
stock_status: values.stock_status, stock_status: values.stock_status,
stock_quantity: values.stock_quantity, stock_quantity: values.stock_quantity,
} },
}); });
message.success('状态更新成功'); message.success('状态更新成功');
tableRef.current?.reload(); tableRef.current?.reload();
@ -276,8 +273,12 @@ export const UpdateForm: React.FC<{
...initialValues, ...initialValues,
categories: initialValues.categories?.map((c: any) => c.name) || [], categories: initialValues.categories?.map((c: any) => c.name) || [],
tags: initialValues.tags?.map((t: any) => t.name) || [], tags: initialValues.tags?.map((t: any) => t.name) || [],
regular_price: initialValues.regular_price ? parseFloat(initialValues.regular_price) : 0, regular_price: initialValues.regular_price
sale_price: initialValues.sale_price ? parseFloat(initialValues.sale_price) : 0, ? parseFloat(initialValues.regular_price)
: 0,
sale_price: initialValues.sale_price
? parseFloat(initialValues.sale_price)
: 0,
}; };
const handleAutoGenerateTags = () => { const handleAutoGenerateTags = () => {
@ -289,7 +290,7 @@ export const UpdateForm: React.FC<{
const name = initialValues.name || ''; const name = initialValues.name || '';
const generatedTagsString = computeTags(name, sku, config); const generatedTagsString = computeTags(name, sku, config);
const generatedTags = generatedTagsString.split(', ').filter(t => t); const generatedTags = generatedTagsString.split(', ').filter((t) => t);
if (generatedTags.length > 0) { if (generatedTags.length > 0) {
const currentTags = form.getFieldValue('tags') || []; const currentTags = form.getFieldValue('tags') || [];
@ -307,7 +308,9 @@ export const UpdateForm: React.FC<{
form={form} form={form}
initialValues={formValues} initialValues={formValues}
trigger={ trigger={
<Button type="link" title="编辑详情" icon={<EditOutlined />}></Button> <Button type="link" title="编辑详情" icon={<EditOutlined />}>
</Button>
} }
autoFocusFirstInput autoFocusFirstInput
drawerProps={{ drawerProps={{
@ -315,8 +318,8 @@ export const UpdateForm: React.FC<{
}} }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) { if (!siteId) {
message.error('缺少站点ID'); message.error('缺少站点ID');
return false; return false;
} }
try { try {
// 将数字字段转换为字符串以匹配DTO // 将数字字段转换为字符串以匹配DTO
@ -324,12 +327,15 @@ export const UpdateForm: React.FC<{
...values, ...values,
regular_price: values.regular_price?.toString() || '', regular_price: values.regular_price?.toString() || '',
sale_price: values.sale_price?.toString() || '', sale_price: values.sale_price?.toString() || '',
price: values.sale_price?.toString() || values.regular_price?.toString() || '', price:
values.sale_price?.toString() ||
values.regular_price?.toString() ||
'',
}; };
await request(`/site-api/${siteId}/products/${initialValues.id}`, { await request(`/site-api/${siteId}/products/${initialValues.id}`, {
method: 'PUT', method: 'PUT',
data: updateData data: updateData,
}); });
message.success('提交成功'); message.success('提交成功');
tableRef.current?.reload(); tableRef.current?.reload();
@ -355,16 +361,8 @@ export const UpdateForm: React.FC<{
placeholder="请输入SKU" placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]} rules={[{ required: true, message: '请输入SKU' }]}
/> />
<ProFormTextArea <ProFormTextArea name="short_description" label="简短描述" width="lg" />
name="short_description" <ProFormTextArea name="description" label="描述" width="lg" />
label="简短描述"
width="lg"
/>
<ProFormTextArea
name="description"
label="描述"
width="lg"
/>
{initialValues.type === 'simple' ? ( {initialValues.type === 'simple' ? (
<> <>
@ -391,20 +389,20 @@ export const UpdateForm: React.FC<{
fieldProps={{ precision: 0 }} fieldProps={{ precision: 0 }}
/> />
<ProFormSelect <ProFormSelect
name="stock_status" name="stock_status"
label="库存状态" label="库存状态"
width="md" width="md"
valueEnum={PRODUCT_STOCK_STATUS_ENUM} valueEnum={PRODUCT_STOCK_STATUS_ENUM}
/> />
</> </>
) : ( ) : (
<></> <></>
)} )}
<ProFormSelect <ProFormSelect
name="status" name="status"
label="产品状态" label="产品状态"
width="md" width="md"
valueEnum={PRODUCT_STATUS_ENUM} valueEnum={PRODUCT_STATUS_ENUM}
/> />
<ProFormSelect <ProFormSelect
name="categories" name="categories"
@ -432,12 +430,12 @@ export const UpdateForm: React.FC<{
label="产品图片" label="产品图片"
initialValue={initialValues.images || [{}]} initialValue={initialValues.images || [{}]}
creatorButtonProps={{ creatorButtonProps={{
creatorButtonText: '添加图片', creatorButtonText: '添加图片',
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText name="src" label="图片URL" width="lg" /> <ProFormText name="src" label="图片URL" width="lg" />
<ProFormText name="alt" label="替代文本" width="md" /> <ProFormText name="alt" label="替代文本" width="md" />
</ProForm.Group> </ProForm.Group>
</ProFormList> </ProFormList>
<Divider /> <Divider />
@ -446,38 +444,38 @@ export const UpdateForm: React.FC<{
label="产品属性" label="产品属性"
initialValue={initialValues.attributes || []} initialValue={initialValues.attributes || []}
creatorButtonProps={{ creatorButtonProps={{
creatorButtonText: '添加属性', creatorButtonText: '添加属性',
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText name="name" label="属性名称" width="md" /> <ProFormText name="name" label="属性名称" width="md" />
<ProFormSelect <ProFormSelect
name="options" name="options"
label="选项" label="选项"
width="md" width="md"
mode="tags" mode="tags"
placeholder="输入选项并回车" placeholder="输入选项并回车"
/> />
<ProFormSelect <ProFormSelect
name="visible" name="visible"
label="可见性" label="可见性"
width="xs" width="xs"
options={[ options={[
{ label: '可见', value: true }, { label: '可见', value: true },
{ label: '隐藏', value: false }, { label: '隐藏', value: false },
]} ]}
initialValue={true} initialValue={true}
/> />
<ProFormSelect <ProFormSelect
name="variation" name="variation"
label="用于变体" label="用于变体"
width="xs" width="xs"
options={[ options={[
{ label: '是', value: true }, { label: '是', value: true },
{ label: '否', value: false }, { label: '否', value: false },
]} ]}
initialValue={false} initialValue={false}
/> />
</ProForm.Group> </ProForm.Group>
</ProFormList> </ProFormList>
</DrawerForm> </DrawerForm>
@ -489,15 +487,26 @@ export const UpdateVaritation: React.FC<{
values: any; values: any;
siteId?: string; siteId?: string;
productId?: string | number; productId?: string | number;
}> = ({ tableRef, values: initialValues, siteId, productId: propProductId }) => { }> = ({
tableRef,
values: initialValues,
siteId,
productId: propProductId,
}) => {
const { message } = App.useApp(); const { message } = App.useApp();
// 转换初始值,将字符串价格转换为数字以便编辑 // 转换初始值,将字符串价格转换为数字以便编辑
const formValues = { const formValues = {
...initialValues, ...initialValues,
regular_price: initialValues.regular_price ? parseFloat(initialValues.regular_price) : 0, regular_price: initialValues.regular_price
sale_price: initialValues.sale_price ? parseFloat(initialValues.sale_price) : 0, ? parseFloat(initialValues.regular_price)
stock_quantity: initialValues.stock_quantity ? parseInt(initialValues.stock_quantity) : 0, : 0,
sale_price: initialValues.sale_price
? parseFloat(initialValues.sale_price)
: 0,
stock_quantity: initialValues.stock_quantity
? parseInt(initialValues.stock_quantity)
: 0,
}; };
return ( return (
@ -505,18 +514,24 @@ export const UpdateVaritation: React.FC<{
title="编辑变体" title="编辑变体"
initialValues={formValues} initialValues={formValues}
trigger={ trigger={
<Button type="link" title="编辑变体" icon={<EditOutlined />}></Button> <Button type="link" title="编辑变体" icon={<EditOutlined />}>
</Button>
} }
autoFocusFirstInput autoFocusFirstInput
drawerProps={{ drawerProps={{
destroyOnHidden: true, destroyOnHidden: true,
}} }}
onFinish={async (values) => { onFinish={async (values) => {
const productId = propProductId || initialValues.externalProductId || initialValues.parent_id || initialValues.product_id; const productId =
propProductId ||
initialValues.externalProductId ||
initialValues.parent_id ||
initialValues.product_id;
if (!siteId || !productId) { if (!siteId || !productId) {
message.error('缺少站点ID或产品ID'); message.error('缺少站点ID或产品ID');
return false; return false;
} }
try { try {
@ -525,14 +540,21 @@ export const UpdateVaritation: React.FC<{
...values, ...values,
regular_price: values.regular_price?.toString() || '', regular_price: values.regular_price?.toString() || '',
sale_price: values.sale_price?.toString() || '', sale_price: values.sale_price?.toString() || '',
price: values.sale_price?.toString() || values.regular_price?.toString() || '', price:
values.sale_price?.toString() ||
values.regular_price?.toString() ||
'',
}; };
const variationId = initialValues.externalVariationId || initialValues.id; const variationId =
await request(`/site-api/${siteId}/products/${productId}/variations/${variationId}`, { initialValues.externalVariationId || initialValues.id;
method: 'PUT', await request(
data: variationData `/site-api/${siteId}/products/${productId}/variations/${variationId}`,
}); {
method: 'PUT',
data: variationData,
},
);
message.success('更新变体成功'); message.success('更新变体成功');
tableRef.current?.reload(); tableRef.current?.reload();
return true; return true;
@ -575,16 +597,16 @@ export const UpdateVaritation: React.FC<{
fieldProps={{ precision: 0 }} fieldProps={{ precision: 0 }}
/> />
<ProFormSelect <ProFormSelect
name="stock_status" name="stock_status"
label="库存状态" label="库存状态"
width="lg" width="lg"
valueEnum={PRODUCT_STOCK_STATUS_ENUM} valueEnum={PRODUCT_STOCK_STATUS_ENUM}
/> />
<ProFormSelect <ProFormSelect
name="status" name="status"
label="产品状态" label="产品状态"
width="lg" width="lg"
valueEnum={PRODUCT_STATUS_ENUM} valueEnum={PRODUCT_STATUS_ENUM}
/> />
</ProForm.Group> </ProForm.Group>
</DrawerForm> </DrawerForm>
@ -603,7 +625,6 @@ export const UpdateVaritation: React.FC<{
// I'll remove BatchEdit from ProductsPage toolbar for now or implement batch update in Controller. // I'll remove BatchEdit from ProductsPage toolbar for now or implement batch update in Controller.
// I'll update BatchDelete. // I'll update BatchDelete.
export const BatchDeleteProducts: React.FC<{ export const BatchDeleteProducts: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>; tableRef: React.MutableRefObject<ActionType | undefined>;
selectedRowKeys: React.Key[]; selectedRowKeys: React.Key[];
@ -639,30 +660,56 @@ export const BatchDeleteProducts: React.FC<{
}; };
return ( return (
<Button type="primary" danger title="批量删除" disabled={!hasSelection} onClick={handleBatchDelete} icon={<DeleteFilled />} /> <Button
type="primary"
danger
title="批量删除"
disabled={!hasSelection}
onClick={handleBatchDelete}
icon={<DeleteFilled />}
/>
); );
}; };
export const BatchEditProducts: React.FC<{ export const BatchEditProducts: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>; tableRef: React.MutableRefObject<ActionType | undefined>;
selectedRowKeys: React.Key[]; selectedRowKeys: React.Key[];
setSelectedRowKeys: (keys: React.Key[]) => void; setSelectedRowKeys: (keys: React.Key[]) => void;
selectedRows: any[]; selectedRows: any[];
siteId?: string; siteId?: string;
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, selectedRows, siteId }) => { }> = ({
tableRef,
selectedRowKeys,
setSelectedRowKeys,
selectedRows,
siteId,
}) => {
const { message } = App.useApp(); const { message } = App.useApp();
return ( return (
<ModalForm <ModalForm
title="批量编辑产品" title="批量编辑产品"
trigger={<Button disabled={!selectedRowKeys.length} type="primary" icon={<EditOutlined />}></Button>} trigger={
<Button
disabled={!selectedRowKeys.length}
type="primary"
icon={<EditOutlined />}
>
</Button>
}
width={600} width={600}
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
if (!siteId) return false; if (!siteId) return false;
const updatePayload = selectedRows.map((row) => ({ id: row.id, ...values })); const updatePayload = selectedRows.map((row) => ({
id: row.id,
...values,
}));
try { try {
const res = await request(`/site-api/${siteId}/products/batch`, { method: 'POST', data: { update: updatePayload } }); const res = await request(`/site-api/${siteId}/products/batch`, {
method: 'POST',
data: { update: updatePayload },
});
if (res.success) { if (res.success) {
message.success('批量编辑成功'); message.success('批量编辑成功');
tableRef.current?.reload(); tableRef.current?.reload();
@ -678,14 +725,26 @@ export const BatchEditProducts: React.FC<{
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormSelect name="status" label="产品状态" valueEnum={PRODUCT_STATUS_ENUM} /> <ProFormSelect
<ProFormSelect name="stock_status" label="库存状态" valueEnum={PRODUCT_STOCK_STATUS_ENUM} /> name="status"
<ProFormDigit name="stock_quantity" label="库存数量" fieldProps={{ precision: 0 }} /> label="产品状态"
valueEnum={PRODUCT_STATUS_ENUM}
/>
<ProFormSelect
name="stock_status"
label="库存状态"
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
/>
<ProFormDigit
name="stock_quantity"
label="库存数量"
fieldProps={{ precision: 0 }}
/>
</ProForm.Group> </ProForm.Group>
</ModalForm> </ModalForm>
); );
}; };
// Disable for now // Disable for now
export const SetComponent: React.FC<any> = () => null; // Disable for now (relies on local productcontrollerProductbysku?) export const SetComponent: React.FC<any> = () => null; // Disable for now (relies on local productcontrollerProductbysku?)
export const ImportCsv: React.FC<{ export const ImportCsv: React.FC<{
@ -696,7 +755,11 @@ export const ImportCsv: React.FC<{
return ( return (
<ModalForm <ModalForm
title="批量导入产品" title="批量导入产品"
trigger={<Button type="primary" ghost icon={<PlusOutlined />}></Button>} trigger={
<Button type="primary" ghost icon={<PlusOutlined />}>
</Button>
}
width={600} width={600}
modalProps={{ destroyOnHidden: true }} modalProps={{ destroyOnHidden: true }}
onFinish={async (values) => { onFinish={async (values) => {
@ -704,7 +767,10 @@ export const ImportCsv: React.FC<{
const csvText = values.csv || ''; const csvText = values.csv || '';
const itemsList = values.items || []; const itemsList = values.items || [];
try { try {
const res = await request(`/site-api/${siteId}/products/import`, { method: 'POST', data: { csv: csvText, items: itemsList } }); const res = await request(`/site-api/${siteId}/products/import`, {
method: 'POST',
data: { csv: csvText, items: itemsList },
});
if (res.success) { if (res.success) {
message.success('导入完成'); message.success('导入完成');
tableRef.current?.reload(); tableRef.current?.reload();
@ -718,16 +784,28 @@ export const ImportCsv: React.FC<{
} }
}} }}
> >
<ProFormTextArea name="csv" label="CSV文本" placeholder="粘贴CSV,首行为表头" /> <ProFormTextArea
name="csv"
label="CSV文本"
placeholder="粘贴CSV,首行为表头"
/>
<ProFormList name="items" label="或手动输入产品" initialValue={[]}> <ProFormList name="items" label="或手动输入产品" initialValue={[]}>
<ProForm.Group> <ProForm.Group>
<ProFormText name="name" label="名称" /> <ProFormText name="name" label="名称" />
<ProFormText name="sku" label="SKU" /> <ProFormText name="sku" label="SKU" />
<ProFormDigit name="regular_price" label="常规价" fieldProps={{ precision: 2 }} /> <ProFormDigit
<ProFormDigit name="sale_price" label="促销价" fieldProps={{ precision: 2 }} /> name="regular_price"
label="常规价"
fieldProps={{ precision: 2 }}
/>
<ProFormDigit
name="sale_price"
label="促销价"
fieldProps={{ precision: 2 }}
/>
</ProForm.Group> </ProForm.Group>
</ProFormList> </ProFormList>
</ModalForm> </ModalForm>
); );
}; };
// Disable for now // Disable for now

View File

@ -78,9 +78,10 @@ export const classifyExtraTags = (
const fLower = flavorPart.toLowerCase(); const fLower = flavorPart.toLowerCase();
const isFruit = const isFruit =
fruits.some((key) => fLower.includes(key.toLowerCase())) || fruits.some((key) => fLower.includes(key.toLowerCase())) ||
tokens.some((t) => fruits.map(k => k.toLowerCase()).includes(t)); tokens.some((t) => fruits.map((k) => k.toLowerCase()).includes(t));
const isMint = const isMint =
mints.some((key) => fLower.includes(key.toLowerCase())) || tokens.includes('mint'); mints.some((key) => fLower.includes(key.toLowerCase())) ||
tokens.includes('mint');
const extras: string[] = []; const extras: string[] = [];
if (isFruit) extras.push('Fruit'); if (isFruit) extras.push('Fruit');
@ -106,14 +107,18 @@ export const matchAttributes = (text: string, keys: string[]): string[] => {
/** /**
* @description Tags * @description Tags
*/ */
export const computeTags = (name: string, sku: string, config: TagConfig): string => { export const computeTags = (
name: string,
sku: string,
config: TagConfig,
): string => {
const [brand, flavorPart, mg, dryness] = parseName(name, config.brands); const [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
const tokens = splitFlavorTokens(flavorPart); const tokens = splitFlavorTokens(flavorPart);
const flavorKeysLower = config.flavors.map(k => k.toLowerCase()); const flavorKeysLower = config.flavors.map((k) => k.toLowerCase());
const tokensForFlavor = tokens.filter( const tokensForFlavor = tokens.filter((t) =>
(t) => flavorKeysLower.includes(t.toLowerCase()), flavorKeysLower.includes(t.toLowerCase()),
); );
const flavorTag = tokensForFlavor const flavorTag = tokensForFlavor
@ -125,7 +130,9 @@ export const computeTags = (name: string, sku: string, config: TagConfig): strin
if (flavorTag) tags.push(flavorTag); if (flavorTag) tags.push(flavorTag);
for (const t of tokensForFlavor) { for (const t of tokensForFlavor) {
const isFruitKey = config.fruits.some(k => k.toLowerCase() === t.toLowerCase()); const isFruitKey = config.fruits.some(
(k) => k.toLowerCase() === t.toLowerCase(),
);
if (isFruitKey && t.toLowerCase() !== 'fruit') { if (isFruitKey && t.toLowerCase() !== 'fruit') {
tags.push(t.charAt(0).toUpperCase() + t.slice(1)); tags.push(t.charAt(0).toUpperCase() + t.slice(1));
} }
@ -155,9 +162,7 @@ export const computeTags = (name: string, sku: string, config: TagConfig): strin
} }
} }
tags.push( tags.push(...classifyExtraTags(flavorPart, config.fruits, config.mints));
...classifyExtraTags(flavorPart, config.fruits, config.mints),
);
const seen = new Set<string>(); const seen = new Set<string>();
const finalTags = tags.filter((t) => { const finalTags = tags.filter((t) => {

View File

@ -1,53 +1,50 @@
{ {
admin_id: 0, "admin_id": 0,
admin_name: "", "admin_name": "",
birthday: 0, "birthday": 0,
contact: "", "contact": "",
country_id: 14, "country_id": 14,
created_at: 1765351077, "created_at": 1765351077,
domain: "auspouches.com", "domain": "auspouches.com",
email: "daniel.waring81@gmail.com", "email": "daniel.waring81@gmail.com",
first_name: "Dan", "first_name": "Dan",
first_pay_at: 1765351308, "first_pay_at": 1765351308,
gender: 0, "gender": 0,
id: 44898147, "id": 44898147,
ip: "1.146.111.163", "ip": "1.146.111.163",
is_cart: 0, "is_cart": 0,
is_event_sub: 1, "is_event_sub": 1,
is_sub: 1, "is_sub": 1,
is_verified: 1, "is_verified": 1,
last_name: "Waring", "last_name": "Waring",
last_order_id: 236122, "last_order_id": 236122,
login_at: 1765351340, "login_at": 1765351340,
note: "", "note": "",
order_at: 1765351224, "order_at": 1765351224,
orders_count: 1, "orders_count": 1,
pay_at: 1765351308, "pay_at": 1765351308,
source_device: "phone", "source_device": "phone",
tags: [ "tags": [],
], "total_spent": "203.81",
total_spent: "203.81", "updated_at": 1765351515,
updated_at: 1765351515, "utm_medium": "referral",
utm_medium: "referral", "utm_source": "checkout.cartadicreditopay.com",
utm_source: "checkout.cartadicreditopay.com", "visit_at": 1765351513,
visit_at: 1765351513, "country": {
country: { "chinese_name": "澳大利亚",
chinese_name: "澳大利亚", "country_code2": "AU",
country_code2: "AU", "country_name": "Australia"
country_name: "Australia",
}, },
sysinfo: { "sysinfo": {
user_agent: "Mozilla/5.0 (Linux; Android 16; Pixel 8 Pro Build/BP3A.251105.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.212 Mobile Safari/537.36 MetaIAB Facebook", "user_agent": "Mozilla/5.0 (Linux; Android 16; Pixel 8 Pro Build/BP3A.251105.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.212 Mobile Safari/537.36 MetaIAB Facebook",
timezone: "Etc/GMT-10", "timezone": "Etc/GMT-10",
os: "Android", "os": "Android",
browser: "Pixel 8", "browser": "Pixel 8",
language: "en-GB", "language": "en-GB",
screen_size: "528X1174", "screen_size": "528X1174",
viewport_size: "527X1026", "viewport_size": "527X1026",
ip: "1.146.111.163", "ip": "1.146.111.163"
}, },
default_address: [ "default_address": [],
], "addresses": []
addresses: [
],
} }

View File

@ -128,11 +128,7 @@ const ListPage: React.FC = () => {
const stockRow = points.map( const stockRow = points.map(
(p) => stockMap.get(p.id || 0) || 0, (p) => stockMap.get(p.id || 0) || 0,
); );
return [ return [item.productName || '', item.sku || '', ...stockRow];
item.productName || '',
item.sku || '',
...stockRow,
];
}); });
// 导出 // 导出

View File

@ -564,15 +564,13 @@ const DetailForm: React.FC<{
const [form] = Form.useForm(); const [form] = Form.useForm();
const initialValues = { const initialValues = {
...values, ...values,
items: values?.items?.map( items: values?.items?.map((item: { productName: string; sku: string }) => ({
(item: { productName: string; sku: string }) => ({ ...item,
...item, sku: {
sku: { label: item.productName,
label: item.productName, value: item.sku,
value: item.sku, },
}, })),
}),
),
}; };
return ( return (
<DrawerForm <DrawerForm

View File

@ -223,7 +223,8 @@ const UpdateForm: React.FC<{
title="编辑" title="编辑"
initialValues={{ initialValues={{
...initialValues, ...initialValues,
areas: ((initialValues as any).areas?.map((area: any) => area.code) ?? []), areas:
(initialValues as any).areas?.map((area: any) => area.code) ?? [],
}} }}
trigger={ trigger={
<Button type="primary"> <Button type="primary">

View File

@ -57,13 +57,13 @@ const TestModal: React.FC<{
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
try { try {
const res = await templatecontrollerRendertemplate( const res = await templatecontrollerRendertemplate(
{ name: template.name || '' }, { name: template.name || '' },
inputData inputData,
); );
if (res.success) { if (res.success) {
setRenderedResult(res.data as unknown as string); setRenderedResult(res.data as unknown as string);
} else { } else {
setRenderedResult(`Error: ${res.message}`); setRenderedResult(`Error: ${res.message}`);
} }
} catch (error: any) { } catch (error: any) {
setRenderedResult(`Error: ${error.message}`); setRenderedResult(`Error: ${error.message}`);
@ -88,9 +88,15 @@ const TestModal: React.FC<{
<Card bodyStyle={{ padding: 0, height: '300px', overflow: 'auto' }}> <Card bodyStyle={{ padding: 0, height: '300px', overflow: 'auto' }}>
<ReactJson <ReactJson
src={inputData} src={inputData}
onEdit={(edit) => setInputData(edit.updated_src as Record<string, any>)} onEdit={(edit) =>
onAdd={(add) => setInputData(add.updated_src as Record<string, any>)} setInputData(edit.updated_src as Record<string, any>)
onDelete={(del) => setInputData(del.updated_src as Record<string, any>)} }
onAdd={(add) =>
setInputData(add.updated_src as Record<string, any>)
}
onDelete={(del) =>
setInputData(del.updated_src as Record<string, any>)
}
name={false} name={false}
displayDataTypes={false} displayDataTypes={false}
/> />
@ -98,8 +104,17 @@ const TestModal: React.FC<{
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Typography.Title level={5}></Typography.Title> <Typography.Title level={5}></Typography.Title>
<Card bodyStyle={{ padding: '16px', height: '300px', overflow: 'auto', backgroundColor: '#f5f5f5' }}> <Card
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{renderedResult}</pre> bodyStyle={{
padding: '16px',
height: '300px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
{renderedResult}
</pre>
</Card> </Card>
</div> </div>
</div> </div>
@ -111,7 +126,9 @@ const List: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const { message } = App.useApp(); const { message } = App.useApp();
const [testModalVisible, setTestModalVisible] = useState(false); const [testModalVisible, setTestModalVisible] = useState(false);
const [currentTemplate, setCurrentTemplate] = useState<API.Template | null>(null); const [currentTemplate, setCurrentTemplate] = useState<API.Template | null>(
null,
);
const columns: ProColumns<API.Template>[] = [ const columns: ProColumns<API.Template>[] = [
{ {
@ -245,7 +262,6 @@ const CreateForm: React.FC<{
> >
<ProFormText <ProFormText
name="name" name="name"
label="模板名称" label="模板名称"
placeholder="请输入名称" placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]} rules={[{ required: true, message: '请输入名称' }]}
@ -323,13 +339,13 @@ const UpdateForm: React.FC<{
} }
}} }}
> >
<ProFormText <ProFormText
name="name" name="name"
label="模板名称" label="模板名称"
placeholder="请输入名称" placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]} rules={[{ required: true, message: '请输入名称' }]}
/> />
<ProForm.Item <ProForm.Item
name="value" name="value"
label="值" label="值"
rules={[{ required: true, message: '请输入值' }]} rules={[{ required: true, message: '请输入值' }]}
@ -345,24 +361,24 @@ const UpdateForm: React.FC<{
}} }}
/> />
</ProForm.Item> </ProForm.Item>
<ProFormTextArea <ProFormTextArea
name="testData" name="testData"
label="测试数据 (JSON)" label="测试数据 (JSON)"
placeholder="请输入JSON格式的测试数据" placeholder="请输入JSON格式的测试数据"
rules={[ rules={[
{ {
validator: (_, value) => { validator: (_, value) => {
if (!value) return Promise.resolve(); if (!value) return Promise.resolve();
try { try {
JSON.parse(value); JSON.parse(value);
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
return Promise.reject(new Error('请输入有效的JSON格式')); return Promise.reject(new Error('请输入有效的JSON格式'));
} }
},
}, },
]} },
/> ]}
/>
</DrawerForm> </DrawerForm>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,10 @@ import {
ProForm, ProForm,
ProFormSelect, ProFormSelect,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Card, Col, Input, message, Row, Upload } from 'antd'; import { Button, Card, Col, Input, message, Row, Upload } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { request } from '@umijs/max';
import { attributes } from '../../../Product/Attribute/consts';
// 定义配置接口 // 定义配置接口
interface TagConfig { interface TagConfig {
@ -95,9 +94,10 @@ const classifyExtraTags = (
const fLower = flavorPart.toLowerCase(); const fLower = flavorPart.toLowerCase();
const isFruit = const isFruit =
fruitKeys.some((key) => fLower.includes(key.toLowerCase())) || fruitKeys.some((key) => fLower.includes(key.toLowerCase())) ||
tokens.some((t) => fruitKeys.map(k => k.toLowerCase()).includes(t)); tokens.some((t) => fruitKeys.map((k) => k.toLowerCase()).includes(t));
const isMint = const isMint =
mintKeys.some((key) => fLower.includes(key.toLowerCase())) || tokens.includes('mint'); mintKeys.some((key) => fLower.includes(key.toLowerCase())) ||
tokens.includes('mint');
const extras: string[] = []; const extras: string[] = [];
if (isFruit) extras.push('Fruit'); if (isFruit) extras.push('Fruit');
@ -131,10 +131,10 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
// 白名单模式:只保留在 flavorKeys 中的 token // 白名单模式:只保留在 flavorKeys 中的 token
// 且对比时忽略大小写 // 且对比时忽略大小写
const flavorKeysLower = config.flavorKeys.map(k => k.toLowerCase()); const flavorKeysLower = config.flavorKeys.map((k) => k.toLowerCase());
const tokensForFlavor = tokens.filter( const tokensForFlavor = tokens.filter((t) =>
(t) => flavorKeysLower.includes(t.toLowerCase()), flavorKeysLower.includes(t.toLowerCase()),
); );
// 将匹配到的 token 转为首字母大写 // 将匹配到的 token 转为首字母大写
@ -149,12 +149,14 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
// 添加额外的口味描述词 // 添加额外的口味描述词
for (const t of tokensForFlavor) { for (const t of tokensForFlavor) {
// 检查是否在 fruitKeys 中 (忽略大小写) // 检查是否在 fruitKeys 中 (忽略大小写)
const isFruitKey = config.fruitKeys.some(k => k.toLowerCase() === t.toLowerCase()); const isFruitKey = config.fruitKeys.some(
(k) => k.toLowerCase() === t.toLowerCase(),
);
if (isFruitKey && t.toLowerCase() !== 'fruit') { if (isFruitKey && t.toLowerCase() !== 'fruit') {
tags.push(t.charAt(0).toUpperCase() + t.slice(1)); tags.push(t.charAt(0).toUpperCase() + t.slice(1));
} }
if (t.toLowerCase() === 'mint') { if (t.toLowerCase() === 'mint') {
tags.push('Mint'); tags.push('Mint');
} }
} }
@ -219,7 +221,8 @@ const WpToolPage: React.FC = () => {
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据 const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据 const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中 const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
const [config, setConfig] = useState<TagConfig>({ // 动态配置 const [config, setConfig] = useState<TagConfig>({
// 动态配置
brands: [], brands: [],
fruitKeys: [], fruitKeys: [],
mintKeys: [], mintKeys: [],
@ -244,14 +247,25 @@ const WpToolPage: React.FC = () => {
console.warn(`Dictionary ${dictName} not found`); console.warn(`Dictionary ${dictName} not found`);
return []; return [];
} }
const res = await request('/dict/items', { params: { dictId: dict.id } }); const res = await request('/dict/items', {
params: { dictId: dict.id },
});
return res.map((item: any) => item.name); return res.map((item: any) => item.name);
}; };
const [brands, fruitKeys, mintKeys, flavorKeys, strengthKeys, sizeKeys, humidityKeys, categoryKeys] = await Promise.all([ const [
brands,
fruitKeys,
mintKeys,
flavorKeys,
strengthKeys,
sizeKeys,
humidityKeys,
categoryKeys,
] = await Promise.all([
getItems('brand'), getItems('brand'),
getItems('fruit'), // 假设字典名为 fruit getItems('fruit'), // 假设字典名为 fruit
getItems('mint'), // 假设字典名为 mint getItems('mint'), // 假设字典名为 mint
getItems('flavor'), // 假设字典名为 flavor getItems('flavor'), // 假设字典名为 flavor
getItems('strength'), getItems('strength'),
getItems('size'), getItems('size'),
@ -267,7 +281,7 @@ const WpToolPage: React.FC = () => {
strengthKeys, strengthKeys,
sizeKeys, sizeKeys,
humidityKeys, humidityKeys,
categoryKeys categoryKeys,
}; };
setConfig(newConfig); setConfig(newConfig);
form.setFieldsValue(newConfig); form.setFieldsValue(newConfig);
@ -372,7 +386,16 @@ const WpToolPage: React.FC = () => {
// 获取表单中的最新配置 // 获取表单中的最新配置
const config = await form.validateFields(); const config = await form.validateFields();
const { brands, fruitKeys, mintKeys, flavorKeys, strengthKeys, sizeKeys, humidityKeys, categoryKeys } = config; const {
brands,
fruitKeys,
mintKeys,
flavorKeys,
strengthKeys,
sizeKeys,
humidityKeys,
categoryKeys,
} = config;
// 确保品牌按长度降序排序 // 确保品牌按长度降序排序
const sortedBrands = [...brands].sort((a, b) => b.length - a.length); const sortedBrands = [...brands].sort((a, b) => b.length - a.length);
@ -406,7 +429,6 @@ const WpToolPage: React.FC = () => {
// 自动下载 // 自动下载
downloadData(dataWithTags); downloadData(dataWithTags);
} catch (error) { } catch (error) {
message.error({ message.error({
content: '处理失败,请检查配置或文件.', content: '处理失败,请检查配置或文件.',

View File

@ -11,7 +11,6 @@ import * as logistics from './logistics';
import * as media from './media'; import * as media from './media';
import * as order from './order'; import * as order from './order';
import * as product from './product'; import * as product from './product';
import * as review from './review';
import * as site from './site'; import * as site from './site';
import * as siteApi from './siteApi'; import * as siteApi from './siteApi';
import * as statistics from './statistics'; import * as statistics from './statistics';
@ -31,7 +30,6 @@ export default {
media, media,
order, order,
product, product,
review,
siteApi, siteApi,
site, site,
statistics, statistics,

View File

@ -17,6 +17,20 @@ export async function productcontrollerCreateproduct(
}); });
} }
/** 此处后端没有提供注释 GET /product/${param0} */
export async function productcontrollerGetproductbyid(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetproductbyidParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.ProductRes>(`/product/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /product/${param0} */ /** 此处后端没有提供注释 PUT /product/${param0} */
export async function productcontrollerUpdateproduct( export async function productcontrollerUpdateproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -564,6 +578,20 @@ export async function productcontrollerSearchproducts(
}); });
} }
/** 此处后端没有提供注释 GET /product/site-sku/${param0} */
export async function productcontrollerGetproductbysitesku(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetproductbysiteskuParams,
options?: { [key: string]: any },
) {
const { siteSku: param0, ...queryParams } = params;
return request<API.ProductRes>(`/product/site-sku/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /product/size */ /** 此处后端没有提供注释 GET /product/size */
export async function productcontrollerCompatsize( export async function productcontrollerCompatsize(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -1,80 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 此处后端没有提供注释 POST /review/create */
export async function reviewcontrollerCreate(
body: API.CreateReviewDTO,
options?: { [key: string]: any },
) {
return request<any>('/review/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 DELETE /review/delete/${param0} */
export async function reviewcontrollerDelete(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.reviewcontrollerDeleteParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/review/delete/${param0}`, {
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /review/get/${param0} */
export async function reviewcontrollerGet(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.reviewcontrollerGetParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/review/get/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /review/list */
export async function reviewcontrollerList(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.reviewcontrollerListParams,
options?: { [key: string]: any },
) {
return request<any>('/review/list', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /review/update/${param0} */
export async function reviewcontrollerUpdate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.reviewcontrollerUpdateParams,
body: API.UpdateReviewDTO,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/review/update/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}

View File

@ -163,6 +163,28 @@ export async function siteapicontrollerConvertmediatowebp(
); );
} }
/** 此处后端没有提供注释 POST /site-api/${param0}/media/create */
export async function siteapicontrollerCreatemedia(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerCreatemediaParams,
body: API.UploadMediaDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.UnifiedMediaPaginationDTO>(
`/site-api/${param0}/media/create`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 GET /site-api/${param0}/media/export */ /** 此处后端没有提供注释 GET /site-api/${param0}/media/export */
export async function siteapicontrollerExportmedia( export async function siteapicontrollerExportmedia(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -185,22 +185,16 @@ declare namespace API {
}; };
type CreateReviewDTO = { type CreateReviewDTO = {
/** 站点ID */
siteId?: number;
/** 产品ID */ /** 产品ID */
productId?: number; product_id?: Record<string, any>;
/** 客户ID */
customerId?: number;
/** 评论者姓名 */
author?: string;
/** 评论者电子邮件 */
email?: string;
/** 评论内容 */ /** 评论内容 */
content?: string; review?: string;
/** 评分 (1-5) */ /** 评论者 */
reviewer?: string;
/** 评论者邮箱 */
reviewer_email?: string;
/** 评分 */
rating?: number; rating?: number;
/** 状态 */
status?: 'pending' | 'approved' | 'rejected';
}; };
type CreateSiteDTO = { type CreateSiteDTO = {
@ -834,18 +828,18 @@ declare namespace API {
type Product = { type Product = {
/** ID */ /** ID */
id: number; id: number;
/** sku */
sku?: string;
/** 类型 */ /** 类型 */
type?: string; type?: string;
/** 产品名称 */ /** 产品名称 */
name: string; name: string;
/** 产品中文名称 */ /** 产品中文名称 */
nameCn?: string; nameCn?: string;
/** 产品描述 */
description?: string;
/** 产品简短描述 */ /** 产品简短描述 */
shortDescription?: string; shortDescription?: string;
/** sku */ /** 产品描述 */
sku?: string; description?: string;
/** 价格 */ /** 价格 */
price?: number; price?: number;
/** 促销价格 */ /** 促销价格 */
@ -961,6 +955,14 @@ declare namespace API {
id: number; id: number;
}; };
type productcontrollerGetproductbyidParams = {
id: number;
};
type productcontrollerGetproductbysiteskuParams = {
siteSku: string;
};
type productcontrollerGetproductcomponentsParams = { type productcontrollerGetproductcomponentsParams = {
id: number; id: number;
}; };
@ -1047,7 +1049,7 @@ declare namespace API {
type ProductSiteSku = { type ProductSiteSku = {
/** 站点 SKU */ /** 站点 SKU */
code?: string; siteSku?: string;
}; };
type ProductsRes = { type ProductsRes = {
@ -1230,19 +1232,6 @@ declare namespace API {
stockPointId?: number; stockPointId?: number;
}; };
type QueryReviewDTO = {
/** 当前页码 */
current?: number;
/** 每页数量 */
pageSize?: number;
/** 站点ID */
siteId?: number;
/** 产品ID */
productId?: number;
/** 状态 */
status?: string;
};
type QueryServiceDTO = { type QueryServiceDTO = {
/** 页码 */ /** 页码 */
current?: number; current?: number;
@ -1348,31 +1337,6 @@ declare namespace API {
data?: RateDTO[]; data?: RateDTO[];
}; };
type reviewcontrollerDeleteParams = {
id: number;
};
type reviewcontrollerGetParams = {
id: number;
};
type reviewcontrollerListParams = {
/** 当前页码 */
current?: number;
/** 每页数量 */
pageSize?: number;
/** 站点ID */
siteId?: number;
/** 产品ID */
productId?: number;
/** 状态 */
status?: string;
};
type reviewcontrollerUpdateParams = {
id: number;
};
type Service = { type Service = {
id?: string; id?: string;
carrier_name?: string; carrier_name?: string;
@ -1486,6 +1450,10 @@ declare namespace API {
siteId: number; siteId: number;
}; };
type siteapicontrollerCreatemediaParams = {
siteId: number;
};
type siteapicontrollerCreateordernoteParams = { type siteapicontrollerCreateordernoteParams = {
id: string; id: string;
siteId: number; siteId: number;
@ -2537,9 +2505,11 @@ declare namespace API {
/** 更新时间 */ /** 更新时间 */
date_modified?: string; date_modified?: string;
/** 产品链接 */ /** 产品链接 */
frontendUrl?: string; permalink?: string;
/** 原始数据(保留备用) */ /** 原始数据(保留备用) */
raw?: Record<string, any>; raw?: Record<string, any>;
/** ERP产品信息 */
erpProduct?: Record<string, any>;
}; };
type UnifiedProductPaginationDTO = { type UnifiedProductPaginationDTO = {
@ -2728,11 +2698,11 @@ declare namespace API {
type UpdateReviewDTO = { type UpdateReviewDTO = {
/** 评论内容 */ /** 评论内容 */
content?: string; review?: string;
/** 评分 (1-5) */ /** 评分 */
rating?: number; rating?: number;
/** 状态 */ /** 状态 */
status?: 'pending' | 'approved' | 'rejected'; status?: string;
}; };
type UpdateSiteDTO = { type UpdateSiteDTO = {
@ -2740,6 +2710,8 @@ declare namespace API {
areas?: any; areas?: any;
/** 绑定仓库ID列表 */ /** 绑定仓库ID列表 */
stockPointIds?: any; stockPointIds?: any;
/** 站点网站URL */
websiteUrl?: string;
}; };
type UpdateStockDTO = { type UpdateStockDTO = {
@ -2805,6 +2777,13 @@ declare namespace API {
siteId?: number; siteId?: number;
}; };
type UploadMediaDTO = {
/** Base64 编码的文件内容 */
file?: string;
/** 文件名 */
filename?: string;
};
type usercontrollerUpdateuserParams = { type usercontrollerUpdateuserParams = {
id?: number; id?: number;
}; };