refactor: 优化代码格式和导入顺序,移除未使用的导入和代码
fix: 修复JSON文件格式错误 feat(api): 添加产品相关API接口和类型定义 feat(product): 新增ERP产品绑定功能组件 style: 统一代码缩进和格式化 chore: 更新依赖和配置文件 perf: 优化字典数据导出功能 docs: 更新类型定义文件 test: 移除临时测试代码 ci: 更新CI配置 build: 调整构建配置
This commit is contained in:
parent
8bb082187b
commit
54fa1b7ca2
44
.umirc.ts
44
.umirc.ts
|
|
@ -44,8 +44,7 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
{
|
||||
name: '地区管理',
|
||||
path: '/area',
|
||||
access: 'canSeeArea',
|
||||
|
|
@ -55,7 +54,7 @@ export default defineConfig({
|
|||
path: '/area/list',
|
||||
component: './Area/List',
|
||||
},
|
||||
{
|
||||
{
|
||||
name: '地区地图',
|
||||
path: '/area/map',
|
||||
component: './Area/Map',
|
||||
|
|
@ -77,18 +76,31 @@ export default defineConfig({
|
|||
path: '/site/shop',
|
||||
component: './Site/Shop/Layout',
|
||||
routes: [
|
||||
{ path: '/site/shop/:siteId/products', component: './Site/Shop/Products' },
|
||||
{ path: '/site/shop/:siteId/orders', component: './Site/Shop/Orders' },
|
||||
{ path: '/site/shop/:siteId/subscriptions', component: './Site/Shop/Subscriptions' },
|
||||
{ path: '/site/shop/:siteId/logistics', component: './Site/Shop/Logistics' },
|
||||
{ path: '/site/shop/:siteId/media', component: './Site/Shop/Media' },
|
||||
{ path: '/site/shop/:siteId/customers', component: './Site/Shop/Customers' },
|
||||
{
|
||||
path: '/site/shop/:siteId/products',
|
||||
component: './Site/Shop/Products',
|
||||
},
|
||||
{
|
||||
path: '/site/shop/:siteId/orders',
|
||||
component: './Site/Shop/Orders',
|
||||
},
|
||||
{
|
||||
path: '/site/shop/:siteId/subscriptions',
|
||||
component: './Site/Shop/Subscriptions',
|
||||
},
|
||||
{
|
||||
path: '/site/shop/:siteId/logistics',
|
||||
component: './Site/Shop/Logistics',
|
||||
},
|
||||
{
|
||||
path: '/site/shop/:siteId/media',
|
||||
component: './Site/Shop/Media',
|
||||
},
|
||||
{
|
||||
path: '/site/shop/:siteId/customers',
|
||||
component: './Site/Shop/Customers',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Woo产品列表',
|
||||
path: '/site/woocommerce/product/list',
|
||||
component: './Woo/Product/List',
|
||||
},
|
||||
{
|
||||
name: 'Woo标签工具',
|
||||
|
|
@ -97,7 +109,7 @@ export default defineConfig({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
{
|
||||
name: '客户管理',
|
||||
path: '/customer',
|
||||
access: 'canSeeCustomer',
|
||||
|
|
@ -125,7 +137,7 @@ export default defineConfig({
|
|||
component: './Product/Permutation',
|
||||
},
|
||||
{
|
||||
name: "产品分类",
|
||||
name: '产品分类',
|
||||
path: '/product/category',
|
||||
component: './Product/Category',
|
||||
},
|
||||
|
|
|
|||
32128
public/world.json
32128
public/world.json
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
// 运行时配置
|
||||
|
||||
import { GlobalOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ProLayoutProps,
|
||||
ProSchemaValueEnumObj,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Hook: 获取设备指纹(visitorId)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
|
||||
import {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
ProColumns,
|
||||
ProFormInstance,
|
||||
ProFormSelect,
|
||||
ProTable
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { Button, message, Popconfirm, Space } from 'antd';
|
||||
|
|
@ -181,7 +180,10 @@ const AreaList: React.FC = () => {
|
|||
<ProFormSelect
|
||||
name="code"
|
||||
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="请选择国家/地区"
|
||||
rules={[{ required: true, message: '国家/地区为必填项' }]}
|
||||
showSearch
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { request } from '@umijs/max';
|
||||
import { Spin, message } from 'antd';
|
||||
import * as echarts from 'echarts/core';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { MapChart } from 'echarts/charts';
|
||||
import { TooltipComponent, VisualMapComponent } from 'echarts/components';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// 注册 ECharts 组件
|
||||
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
|
||||
|
|
@ -46,7 +46,7 @@ const AreaMap: React.FC = () => {
|
|||
const savedAreas: AreaItem[] = areaResponse.data?.list || [];
|
||||
|
||||
// 3. 将后端数据转换为 ECharts 需要的格式
|
||||
const mapData = savedAreas.map(area => {
|
||||
const mapData = savedAreas.map((area) => {
|
||||
let nameEn = countries.getName(area.code, 'en');
|
||||
return {
|
||||
name: nameEn || area.code,
|
||||
|
|
@ -55,7 +55,6 @@ const AreaMap: React.FC = () => {
|
|||
};
|
||||
});
|
||||
|
||||
|
||||
// 4. 配置 ECharts 地图选项
|
||||
const mapOption = {
|
||||
tooltip: {
|
||||
|
|
@ -108,12 +107,20 @@ const AreaMap: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
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 (
|
||||
|
||||
<ReactECharts
|
||||
echarts={echarts}
|
||||
option={option}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PageContainer,
|
||||
} from '@ant-design/pro-components';
|
||||
productcontrollerCreatecategory,
|
||||
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 {
|
||||
Button,
|
||||
|
|
@ -16,15 +23,6 @@ import {
|
|||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
productcontrollerCreatecategory,
|
||||
productcontrollerCreatecategoryattribute,
|
||||
productcontrollerDeletecategory,
|
||||
productcontrollerDeletecategoryattribute,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerUpdatecategory,
|
||||
} from '@/servers/api/product';
|
||||
import { attributes } from '../Attribute/consts';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
|
@ -62,7 +60,9 @@ const CategoryPage: React.FC = () => {
|
|||
const fetchCategoryAttributes = async (categoryId: number) => {
|
||||
setLoadingAttributes(true);
|
||||
try {
|
||||
const res = await productcontrollerGetcategoryattributes({ categoryItemId: categoryId });
|
||||
const res = await productcontrollerGetcategoryattributes({
|
||||
categoryItemId: categoryId,
|
||||
});
|
||||
setCategoryAttributes(res || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
|
|
@ -81,7 +81,10 @@ const CategoryPage: React.FC = () => {
|
|||
const handleCategorySubmit = async (values: any) => {
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await productcontrollerUpdatecategory({ id: editingCategory.id }, values);
|
||||
await productcontrollerUpdatecategory(
|
||||
{ id: editingCategory.id },
|
||||
values,
|
||||
);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await productcontrollerCreatecategory(values);
|
||||
|
|
@ -113,7 +116,9 @@ const CategoryPage: React.FC = () => {
|
|||
const res = await request('/dict/list');
|
||||
const filtered = (res || []).filter((d: any) => attributes.has(d.name));
|
||||
// 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));
|
||||
setAvailableDicts(available);
|
||||
setSelectedDictIds([]);
|
||||
|
|
@ -154,8 +159,22 @@ const CategoryPage: React.FC = () => {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
|
||||
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0', padding: '16px' }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Sider
|
||||
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>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -175,10 +194,15 @@ const CategoryPage: React.FC = () => {
|
|||
dataSource={categories}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
className={selectedCategory?.id === item.id ? 'ant-list-item-active' : ''}
|
||||
className={
|
||||
selectedCategory?.id === item.id ? 'ant-list-item-active' : ''
|
||||
}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background: selectedCategory?.id === item.id ? '#e6f7ff' : 'transparent',
|
||||
background:
|
||||
selectedCategory?.id === item.id
|
||||
? '#e6f7ff'
|
||||
: 'transparent',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
|
|
@ -204,21 +228,30 @@ const CategoryPage: React.FC = () => {
|
|||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<a onClick={(e) => e.stopPropagation()} style={{ color: 'red' }}>删除</a>
|
||||
<a
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={item.title}
|
||||
description={item.name}
|
||||
/>
|
||||
<List.Item.Meta title={item.title} description={item.name} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Sider>
|
||||
<Content style={{ padding: '24px' }}>
|
||||
{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
|
||||
loading={loadingAttributes}
|
||||
dataSource={categoryAttributes}
|
||||
|
|
@ -229,8 +262,10 @@ const CategoryPage: React.FC = () => {
|
|||
title="确定移除该属性吗?"
|
||||
onConfirm={() => handleDeleteAttribute(item.id)}
|
||||
>
|
||||
<Button type="link" danger>移除</Button>
|
||||
</Popconfirm>
|
||||
<Button type="link" danger>
|
||||
移除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
|
|
@ -242,7 +277,15 @@ const CategoryPage: React.FC = () => {
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
|
|
@ -255,11 +298,19 @@ const CategoryPage: React.FC = () => {
|
|||
onOk={() => categoryForm.submit()}
|
||||
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 }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="标识 (Code)" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="标识 (Code)"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -279,7 +330,10 @@ const CategoryPage: React.FC = () => {
|
|||
placeholder="请选择要关联的属性"
|
||||
value={selectedDictIds}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ const ListPage: React.FC = () => {
|
|||
return (
|
||||
<PageContainer ghost>
|
||||
<ProTable
|
||||
scroll={{ x: 'max-content' }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
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 {
|
||||
Button,
|
||||
|
|
@ -98,14 +102,30 @@ const DictPage: React.FC = () => {
|
|||
};
|
||||
|
||||
// 下载字典导入模板
|
||||
const handleDownloadDictTemplate = () => {
|
||||
// 创建一个空的 a 标签用于下载
|
||||
const link = document.createElement('a');
|
||||
link.href = '/dict/template'; // 指向后端的模板下载接口
|
||||
link.setAttribute('download', 'dict_template.xlsx');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
const handleDownloadDictTemplate = async () => {
|
||||
try {
|
||||
// 使用 request 函数获取带认证的文件数据
|
||||
const response = await request('/dict/template', {
|
||||
method: 'GET',
|
||||
responseType: 'blob', // 指定响应类型为 blob
|
||||
skipErrorHandler: true, // 跳过默认错误处理,自己处理错误
|
||||
});
|
||||
|
||||
// 创建 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 link = document.createElement('a');
|
||||
link.href = '/dict/item/template';
|
||||
link.setAttribute('download', 'dict_item_template.xlsx');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// 导出字典项数据
|
||||
const handleExportDictItems = async () => {
|
||||
if (!selectedDict) {
|
||||
message.warning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -176,7 +254,11 @@ const DictPage: React.FC = () => {
|
|||
key: 'action',
|
||||
render: (_: any, record: any) => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" onClick={() => handleEditDict(record)}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleEditDict(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
|
|
@ -291,7 +373,7 @@ const DictPage: React.FC = () => {
|
|||
</Button>
|
||||
</Upload>
|
||||
<Button size="small" onClick={handleDownloadDictTemplate}>
|
||||
下载模板
|
||||
导出模板
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
|
|
@ -359,11 +441,11 @@ const DictPage: React.FC = () => {
|
|||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
onClick={handleDownloadDictItemTemplate}
|
||||
onClick={handleExportDictItems}
|
||||
disabled={!selectedDict}
|
||||
size="small"
|
||||
>
|
||||
下载模板
|
||||
导出数据
|
||||
</Button>
|
||||
</div>
|
||||
<ProTable
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@ const AttributePage: React.FC = () => {
|
|||
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) {
|
||||
message.error('删除失败');
|
||||
} else {
|
||||
|
|
@ -147,7 +148,13 @@ const AttributePage: React.FC = () => {
|
|||
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
|
||||
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
|
||||
{ title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true },
|
||||
{ title: '图片', dataIndex: 'image', key: 'image', valueType: 'image', width: 80 },
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'image',
|
||||
key: 'image',
|
||||
valueType: 'image',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
|
|
@ -195,7 +202,13 @@ const AttributePage: React.FC = () => {
|
|||
size="small"
|
||||
/>
|
||||
</Space>
|
||||
<div style={{ marginTop: '8px', overflowY: 'auto', height: 'calc(100vh - 150px)' }}>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
overflowY: 'auto',
|
||||
height: 'calc(100vh - 150px)',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
dataSource={dicts}
|
||||
columns={dictColumns}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PageContainer,
|
||||
} from '@ant-design/pro-components';
|
||||
productcontrollerCreatecategory,
|
||||
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 {
|
||||
Button,
|
||||
|
|
@ -16,15 +23,6 @@ import {
|
|||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
productcontrollerCreatecategory,
|
||||
productcontrollerCreatecategoryattribute,
|
||||
productcontrollerDeletecategory,
|
||||
productcontrollerDeletecategoryattribute,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerUpdatecategory,
|
||||
} from '@/servers/api/product';
|
||||
import { attributes } from '../Attribute/consts';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
|
@ -62,7 +60,9 @@ const CategoryPage: React.FC = () => {
|
|||
const fetchCategoryAttributes = async (categoryId: number) => {
|
||||
setLoadingAttributes(true);
|
||||
try {
|
||||
const res = await productcontrollerGetcategoryattributes({ id: categoryId });
|
||||
const res = await productcontrollerGetcategoryattributes({
|
||||
id: categoryId,
|
||||
});
|
||||
setCategoryAttributes(res?.data || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
|
|
@ -81,7 +81,10 @@ const CategoryPage: React.FC = () => {
|
|||
const handleCategorySubmit = async (values: any) => {
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await productcontrollerUpdatecategory({ id: editingCategory.id }, values);
|
||||
await productcontrollerUpdatecategory(
|
||||
{ id: editingCategory.id },
|
||||
values,
|
||||
);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await productcontrollerCreatecategory(values);
|
||||
|
|
@ -112,10 +115,12 @@ const CategoryPage: React.FC = () => {
|
|||
try {
|
||||
const res = await request('/dict/list');
|
||||
// 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));
|
||||
// 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));
|
||||
setAvailableDicts(available);
|
||||
setSelectedDictIds([]);
|
||||
|
|
@ -132,12 +137,14 @@ const CategoryPage: React.FC = () => {
|
|||
}
|
||||
try {
|
||||
// Loop through selected IDs and create attribute for each
|
||||
await Promise.all(selectedDictIds.map(dictId =>
|
||||
productcontrollerCreatecategoryattribute({
|
||||
categoryId: selectedCategory.id,
|
||||
dictId: dictId,
|
||||
})
|
||||
));
|
||||
await Promise.all(
|
||||
selectedDictIds.map((dictId) =>
|
||||
productcontrollerCreatecategoryattribute({
|
||||
categoryId: selectedCategory.id,
|
||||
dictId: dictId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
message.success('添加属性成功');
|
||||
setIsAttributeModalVisible(false);
|
||||
fetchCategoryAttributes(selectedCategory.id);
|
||||
|
|
@ -159,8 +166,22 @@ const CategoryPage: React.FC = () => {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Layout style={{ background: '#fff', height: 'calc(100vh - 200px)' }}>
|
||||
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0', padding: '16px' }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Sider
|
||||
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>
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -180,10 +201,15 @@ const CategoryPage: React.FC = () => {
|
|||
dataSource={categories}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
className={selectedCategory?.id === item.id ? 'ant-list-item-active' : ''}
|
||||
className={
|
||||
selectedCategory?.id === item.id ? 'ant-list-item-active' : ''
|
||||
}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background: selectedCategory?.id === item.id ? '#e6f7ff' : 'transparent',
|
||||
background:
|
||||
selectedCategory?.id === item.id
|
||||
? '#e6f7ff'
|
||||
: 'transparent',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
|
|
@ -209,21 +235,30 @@ const CategoryPage: React.FC = () => {
|
|||
}}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<a onClick={(e) => e.stopPropagation()} style={{ color: 'red' }}>删除</a>
|
||||
<a
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ color: 'red' }}
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={item.title}
|
||||
description={item.name}
|
||||
/>
|
||||
<List.Item.Meta title={item.title} description={item.name} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Sider>
|
||||
<Content style={{ padding: '24px' }}>
|
||||
{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
|
||||
loading={loadingAttributes}
|
||||
dataSource={categoryAttributes}
|
||||
|
|
@ -234,8 +269,10 @@ const CategoryPage: React.FC = () => {
|
|||
title="确定移除该属性吗?"
|
||||
onConfirm={() => handleDeleteAttribute(item.id)}
|
||||
>
|
||||
<Button type="link" danger>移除</Button>
|
||||
</Popconfirm>
|
||||
<Button type="link" danger>
|
||||
移除
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
|
|
@ -247,7 +284,15 @@ const CategoryPage: React.FC = () => {
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
|
|
@ -260,11 +305,19 @@ const CategoryPage: React.FC = () => {
|
|||
onOk={() => categoryForm.submit()}
|
||||
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 }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="标识 (Code)" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="标识 (Code)"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
@ -284,7 +337,10 @@ const CategoryPage: React.FC = () => {
|
|||
placeholder="请选择要关联的属性"
|
||||
value={selectedDictIds}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
import {
|
||||
productcontrollerCreateproduct,
|
||||
productcontrollerGetcategoriesall,
|
||||
|
|
@ -18,7 +19,6 @@ import {
|
|||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Tag } from 'antd';
|
||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const capitalize = (s: string) => s.charAt(0).toLocaleUpperCase() + s.slice(1);
|
||||
|
|
@ -49,7 +49,9 @@ const CreateForm: React.FC<{
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const res: any = await productcontrollerGetcategoryattributes({ id: categoryId });
|
||||
const res: any = await productcontrollerGetcategoryattributes({
|
||||
id: categoryId,
|
||||
});
|
||||
setActiveAttributes(res?.data || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
|
|
@ -135,8 +137,8 @@ const CreateForm: React.FC<{
|
|||
humidityName === 'dry'
|
||||
? 'Dry'
|
||||
: humidityName === 'moisture'
|
||||
? 'Moisture'
|
||||
: capitalize(humidityName),
|
||||
? 'Moisture'
|
||||
: capitalize(humidityName),
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
|
|
@ -310,14 +312,16 @@ const CreateForm: React.FC<{
|
|||
placeholder="请输入子产品SKU"
|
||||
rules={[{ required: true, message: '请输入子产品SKU' }]}
|
||||
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);
|
||||
if (!data || !data.items) {
|
||||
return [];
|
||||
}
|
||||
return data.items
|
||||
.filter(item => item.sku)
|
||||
.map(item => ({
|
||||
.filter((item) => item.sku)
|
||||
.map((item) => ({
|
||||
label: `${item.sku} - ${item.name}`,
|
||||
value: item.sku,
|
||||
}));
|
||||
|
|
@ -341,7 +345,7 @@ const CreateForm: React.FC<{
|
|||
name="categoryId"
|
||||
label="分类"
|
||||
width="md"
|
||||
options={categories.map(c => ({ label: c.title, value: c.id }))}
|
||||
options={categories.map((c) => ({ label: c.title, value: c.id }))}
|
||||
placeholder="请选择分类"
|
||||
rules={[{ required: true, message: '请选择分类' }]}
|
||||
/>
|
||||
|
|
@ -372,17 +376,16 @@ const CreateForm: React.FC<{
|
|||
/>
|
||||
<ProFormTextArea
|
||||
name="shortDescription"
|
||||
style={{width: '100%'}}
|
||||
style={{ width: '100%' }}
|
||||
label="产品简短描述"
|
||||
placeholder="请输入产品简短描述"
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
style={{width: '100%'}}
|
||||
style={{ width: '100%' }}
|
||||
label="产品描述"
|
||||
placeholder="请输入产品描述"
|
||||
/>
|
||||
|
||||
</DrawerForm>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
import {
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
|
|
@ -18,7 +19,6 @@ import {
|
|||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Tag } from 'antd';
|
||||
import AttributeFormItem from '@/pages/Product/Attribute/components/AttributeFormItem';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
const EditForm: React.FC<{
|
||||
|
|
@ -35,7 +35,7 @@ const EditForm: React.FC<{
|
|||
const [stockStatus, setStockStatus] = useState<
|
||||
'in-stock' | 'out-of-stock' | null
|
||||
>(null);
|
||||
const [siteSkuEntries, setSiteSkuEntries] = useState<any[]>([]);
|
||||
const [siteSkuCodes, setSiteSkuCodes] = useState<string[]>([]);
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
|
|
@ -52,11 +52,14 @@ const EditForm: React.FC<{
|
|||
}, []);
|
||||
|
||||
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) {
|
||||
productcontrollerGetcategoryattributes({ id: categoryId }).then((res: any) => {
|
||||
setActiveAttributes(res?.data || []);
|
||||
});
|
||||
productcontrollerGetcategoryattributes({ id: categoryId }).then(
|
||||
(res: any) => {
|
||||
setActiveAttributes(res?.data || []);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setActiveAttributes([]);
|
||||
}
|
||||
|
|
@ -68,7 +71,9 @@ const EditForm: React.FC<{
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const res: any = await productcontrollerGetcategoryattributes({ id: categoryId });
|
||||
const res: any = await productcontrollerGetcategoryattributes({
|
||||
id: categoryId,
|
||||
});
|
||||
setActiveAttributes(res?.data || []);
|
||||
} catch (error) {
|
||||
message.error('获取分类属性失败');
|
||||
|
|
@ -95,8 +100,14 @@ const EditForm: React.FC<{
|
|||
await productcontrollerGetproductcomponents({ id: record.id });
|
||||
setComponents(componentsData || []);
|
||||
// 获取站点SKU详细信息
|
||||
const { data: siteSkusData } = await productcontrollerGetproductsiteskus({ id: record.id });
|
||||
setSiteSkuEntries(siteSkusData || []);
|
||||
const { data: siteSkusData } = await productcontrollerGetproductsiteskus({
|
||||
id: record.id,
|
||||
});
|
||||
// 只提取code字段组成字符串数组
|
||||
const codes = siteSkusData
|
||||
? siteSkusData.map((item: any) => item.code)
|
||||
: [];
|
||||
setSiteSkuCodes(codes);
|
||||
})();
|
||||
}, [record]);
|
||||
|
||||
|
|
@ -117,10 +128,10 @@ const EditForm: React.FC<{
|
|||
components: components,
|
||||
type: type,
|
||||
categoryId: (record as any).categoryId || (record as any).category?.id,
|
||||
// 初始化站点SKU列表为对象形式
|
||||
siteSkus: siteSkuEntries && siteSkuEntries.length ? siteSkuEntries : [],
|
||||
// 初始化站点SKU为字符串数组
|
||||
siteSkus: siteSkuCodes,
|
||||
};
|
||||
}, [record, components, type, siteSkuEntries]);
|
||||
}, [record, components, type, siteSkuCodes]);
|
||||
|
||||
return (
|
||||
<DrawerForm<any>
|
||||
|
|
@ -186,12 +197,15 @@ const EditForm: React.FC<{
|
|||
attributes,
|
||||
type: values.type, // 直接使用 type
|
||||
categoryId: values.categoryId,
|
||||
siteSkus: values.siteSkus,
|
||||
siteSkus: values.siteSkus || [], // 直接传递字符串数组
|
||||
// 连带更新 components
|
||||
components: values.type === 'bundle' ? (values.components || []).map((c: any) => ({
|
||||
sku: c.sku,
|
||||
quantity: Number(c.quantity),
|
||||
})) : [],
|
||||
components:
|
||||
values.type === 'bundle'
|
||||
? (values.components || []).map((c: any) => ({
|
||||
sku: c.sku,
|
||||
quantity: Number(c.quantity),
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -223,43 +237,35 @@ const EditForm: React.FC<{
|
|||
</Tag>
|
||||
)}
|
||||
</ProForm.Group>
|
||||
<ProFormList
|
||||
name="siteSkus"
|
||||
label="站点SKU"
|
||||
creatorButtonProps={{ position: 'bottom', creatorButtonText: '新增站点SKU' }}
|
||||
itemRender={({ listDom, action }) => (
|
||||
<div style={{ marginBottom: 8, display: 'flex', flexDirection: 'row', alignItems: 'end' }}>
|
||||
{listDom}
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
name="siteId"
|
||||
label="站点"
|
||||
width="md"
|
||||
options={sites.map((site) => ({ label: site.name, value: site.id }))}
|
||||
placeholder="请选择站点"
|
||||
rules={[{ required: true, message: '请选择站点' }]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="code"
|
||||
label="站点SKU"
|
||||
width="md"
|
||||
placeholder="请输入站点SKU"
|
||||
rules={[{ required: true, message: '请输入站点SKU' }]}
|
||||
/>
|
||||
<ProFormText
|
||||
name="quantity"
|
||||
label="数量"
|
||||
width="md"
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
<ProFormList
|
||||
name="siteSkus"
|
||||
label="站点SKU"
|
||||
creatorButtonProps={{
|
||||
position: 'bottom',
|
||||
creatorButtonText: '新增站点SKU',
|
||||
}}
|
||||
itemRender={({ listDom, action }) => (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'end',
|
||||
}}
|
||||
>
|
||||
{listDom}
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ProFormText
|
||||
name="code"
|
||||
width="md"
|
||||
placeholder="请输入站点SKU"
|
||||
rules={[{ required: true, message: '请输入站点SKU' }]}
|
||||
/>
|
||||
</ProFormList>
|
||||
<ProForm.Group>
|
||||
|
||||
<ProFormText
|
||||
name="name"
|
||||
label="名称"
|
||||
|
|
@ -316,14 +322,16 @@ const EditForm: React.FC<{
|
|||
placeholder="请输入库存SKU"
|
||||
rules={[{ required: true, message: '请输入库存SKU' }]}
|
||||
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);
|
||||
if (!data || !data.items) {
|
||||
return [];
|
||||
}
|
||||
return data.items
|
||||
.filter(item => item.sku)
|
||||
.map(item => ({
|
||||
.filter((item) => item.sku)
|
||||
.map((item) => ({
|
||||
label: `${item.sku} - ${item.name}`,
|
||||
value: item.sku,
|
||||
}));
|
||||
|
|
@ -362,7 +370,7 @@ const EditForm: React.FC<{
|
|||
name="categoryId"
|
||||
label="分类"
|
||||
width="md"
|
||||
options={categories.map(c => ({ label: c.title, value: c.id }))}
|
||||
options={categories.map((c) => ({ label: c.title, value: c.id }))}
|
||||
placeholder="请选择分类"
|
||||
rules={[{ required: true, message: '请选择分类' }]}
|
||||
/>
|
||||
|
|
@ -388,7 +396,6 @@ const EditForm: React.FC<{
|
|||
label="产品描述"
|
||||
placeholder="请输入产品描述"
|
||||
/>
|
||||
|
||||
</DrawerForm>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import {
|
||||
productcontrollerBatchupdateproduct,
|
||||
productcontrollerDeleteproduct,
|
||||
productcontrollerBatchdeleteproduct,
|
||||
productcontrollerBatchupdateproduct,
|
||||
productcontrollerBindproductsiteskus,
|
||||
productcontrollerDeleteproduct,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetproductcomponents,
|
||||
productcontrollerGetproductlist,
|
||||
productcontrollerUpdatenamecn,
|
||||
productcontrollerBindproductsiteskus,
|
||||
} from '@/servers/api/product';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
|
||||
import {
|
||||
wpproductcontrollerBatchsynctosite,
|
||||
wpproductcontrollerGetwpproducts,
|
||||
wpproductcontrollerSynctoproduct,
|
||||
} from '@/servers/api/wpProduct';
|
||||
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
|
||||
import { ActionType, ModalForm, PageContainer, ProColumns, ProFormSelect, ProFormText, ProTable } from '@ant-design/pro-components';
|
||||
import {
|
||||
ActionType,
|
||||
ModalForm,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -125,15 +132,17 @@ const BatchEditModal: React.FC<{
|
|||
const updateData: any = { ids };
|
||||
// 只有当用户输入了值才进行更新
|
||||
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 (Object.keys(updateData).length <= 1) {
|
||||
message.warning('未修改任何属性');
|
||||
return false;
|
||||
message.warning('未修改任何属性');
|
||||
return false;
|
||||
}
|
||||
|
||||
const { success, message: errMsg } = await productcontrollerBatchupdateproduct(updateData);
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerBatchupdateproduct(updateData);
|
||||
if (success) {
|
||||
message.success('批量修改成功');
|
||||
onSuccess();
|
||||
|
|
@ -145,11 +154,7 @@ const BatchEditModal: React.FC<{
|
|||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="price"
|
||||
label="价格"
|
||||
placeholder="不修改请留空"
|
||||
/>
|
||||
<ProFormText name="price" label="价格" placeholder="不修改请留空" />
|
||||
<ProFormText
|
||||
name="promotionPrice"
|
||||
label="促销价格"
|
||||
|
|
@ -158,7 +163,7 @@ const BatchEditModal: React.FC<{
|
|||
<ProFormSelect
|
||||
name="categoryId"
|
||||
label="分类"
|
||||
options={categories.map(c => ({ label: c.title, value: c.id }))}
|
||||
options={categories.map((c) => ({ label: c.title, value: c.id }))}
|
||||
placeholder="不修改请留空"
|
||||
/>
|
||||
</ModalForm>
|
||||
|
|
@ -211,7 +216,7 @@ const SyncToSiteModal: React.FC<{
|
|||
try {
|
||||
await wpproductcontrollerBatchsynctosite(
|
||||
{ siteId: values.siteId },
|
||||
{ productIds }
|
||||
{ productIds },
|
||||
);
|
||||
const map = values.productSiteSkus || {};
|
||||
for (const currentProductId of productIds) {
|
||||
|
|
@ -219,7 +224,15 @@ const SyncToSiteModal: React.FC<{
|
|||
if (entry && entry.code) {
|
||||
await productcontrollerBindproductsiteskus(
|
||||
{ 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: '请选择站点' }]}
|
||||
/>
|
||||
{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>
|
||||
<ProFormText
|
||||
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> }> = ({
|
||||
skus,
|
||||
record,
|
||||
parentTableRef,
|
||||
}) => {
|
||||
const WpProductInfo: React.FC<{
|
||||
skus: string[];
|
||||
record: API.Product;
|
||||
parentTableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
}> = ({ skus, record, parentTableRef }) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
|
||||
|
|
@ -295,12 +311,13 @@ const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTable
|
|||
// 遍历每一个SKU在当前站点进行搜索
|
||||
for (const skuCode of skus) {
|
||||
// 直接调用站点API根据搜索关键字获取产品列表
|
||||
const { data: productPage } = await siteapicontrollerGetproducts({
|
||||
siteId: siteItem.id,
|
||||
const response = await siteapicontrollerGetproducts({
|
||||
siteId: Number(siteItem.id),
|
||||
per_page: 100,
|
||||
search: skuCode,
|
||||
});
|
||||
const siteProducts = productPage?.items || [];
|
||||
const productPage = response as any;
|
||||
const siteProducts = productPage?.data?.items || [];
|
||||
// 将站点信息附加到产品数据中便于展示
|
||||
siteProducts.forEach((p: any) => {
|
||||
aggregatedProducts.push({
|
||||
|
|
@ -382,7 +399,9 @@ const WpProductInfo: React.FC<{ skus: string[]; record: API.Product; parentTable
|
|||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await request(`/wp_product/${wpRow.id}`, { method: 'DELETE' });
|
||||
await request(`/wp_product/${wpRow.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
|
|
@ -443,9 +462,9 @@ const List: React.FC = () => {
|
|||
dataIndex: 'siteSkus',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.siteSkus?.map((item, index) => (
|
||||
{record.siteSkus?.map((code, index) => (
|
||||
<Tag key={index} color="cyan">
|
||||
{item.code}
|
||||
{code}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -471,7 +490,7 @@ const List: React.FC = () => {
|
|||
dataIndex: 'category',
|
||||
render: (_, record: any) => {
|
||||
return record.category?.title || record.category?.name || '-';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
|
|
@ -584,7 +603,6 @@ const List: React.FC = () => {
|
|||
<PageContainer header={{ title: '产品列表' }}>
|
||||
<ProTable<API.Product>
|
||||
scroll={{ x: 'max-content' }}
|
||||
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
|
|
@ -592,8 +610,12 @@ const List: React.FC = () => {
|
|||
// 新建按钮
|
||||
<CreateForm tableRef={actionRef} />,
|
||||
// 批量编辑按钮
|
||||
<Button disabled={selectedRows.length <= 0} onClick={() => setBatchEditModalVisible(true)}>批量修改</Button>
|
||||
,
|
||||
<Button
|
||||
disabled={selectedRows.length <= 0}
|
||||
onClick={() => setBatchEditModalVisible(true)}
|
||||
>
|
||||
批量修改
|
||||
</Button>,
|
||||
// 批量同步按钮
|
||||
<Button
|
||||
disabled={selectedRows.length <= 0}
|
||||
|
|
@ -614,9 +636,10 @@ const List: React.FC = () => {
|
|||
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const { success, message: errMsg } = await productcontrollerBatchdeleteproduct({
|
||||
ids: selectedRows.map((row) => row.id),
|
||||
});
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerBatchdeleteproduct({
|
||||
ids: selectedRows.map((row) => row.id),
|
||||
});
|
||||
if (success) {
|
||||
message.success('批量删除成功');
|
||||
setSelectedRows([]);
|
||||
|
|
@ -652,7 +675,11 @@ const List: React.FC = () => {
|
|||
requestType: 'form',
|
||||
});
|
||||
|
||||
const { created = 0, updated = 0, errors = [] } = res.data || {};
|
||||
const {
|
||||
created = 0,
|
||||
updated = 0,
|
||||
errors = [],
|
||||
} = res.data || {};
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
Modal.warning({
|
||||
|
|
@ -662,18 +689,31 @@ const List: React.FC = () => {
|
|||
<div>
|
||||
<p>创建成功: {created}</p>
|
||||
<p>更新成功: {updated}</p>
|
||||
<p style={{ color: 'red', fontWeight: 'bold' }}>失败数量: {errors.length}</p>
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
background: '#f5f5f5',
|
||||
padding: '8px',
|
||||
marginTop: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}>
|
||||
<p style={{ color: 'red', fontWeight: 'bold' }}>
|
||||
失败数量: {errors.length}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
background: '#f5f5f5',
|
||||
padding: '8px',
|
||||
marginTop: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9',
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -721,7 +761,7 @@ const List: React.FC = () => {
|
|||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<WpProductInfo
|
||||
skus={record.siteSkus?.map((s) => s.code) || []}
|
||||
skus={(record.siteSkus as string[]) || []}
|
||||
record={record}
|
||||
parentTableRef={actionRef}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ const CreateModal: React.FC<CreateModalProps> = ({
|
|||
|
||||
// Helper to generate default name based on attributes
|
||||
const generateDefaultName = () => {
|
||||
if (!category) return '';
|
||||
const parts = [category.name];
|
||||
attributes.forEach(attr => {
|
||||
const val = permutation[attr.name];
|
||||
if (val) parts.push(val.name);
|
||||
});
|
||||
return parts.join(' - ');
|
||||
if (!category) return '';
|
||||
const parts = [category.name];
|
||||
attributes.forEach((attr) => {
|
||||
const val = permutation[attr.name];
|
||||
if (val) parts.push(val.name);
|
||||
});
|
||||
return parts.join(' - ');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -57,10 +57,11 @@ const CreateModal: React.FC<CreateModalProps> = ({
|
|||
humidity: humidity ? capitalize(humidity) : '',
|
||||
};
|
||||
|
||||
const { success, data: rendered } = await templatecontrollerRendertemplate(
|
||||
{ name: 'product.sku' },
|
||||
variables
|
||||
);
|
||||
const { success, data: rendered } =
|
||||
await templatecontrollerRendertemplate(
|
||||
{ name: 'product.sku' },
|
||||
variables,
|
||||
);
|
||||
|
||||
if (success && rendered) {
|
||||
form.setFieldValue('sku', rendered);
|
||||
|
|
@ -90,8 +91,8 @@ const CreateModal: React.FC<CreateModalProps> = ({
|
|||
// Construct attributes payload
|
||||
// Expected format: [{ dictName: 'Size', name: 'S' }, ...]
|
||||
const payloadAttributes = attributes
|
||||
.filter(attr => permutation[attr.name])
|
||||
.map(attr => ({
|
||||
.filter((attr) => permutation[attr.name])
|
||||
.map((attr) => ({
|
||||
dictName: attr.name,
|
||||
name: permutation[attr.name].name,
|
||||
}));
|
||||
|
|
@ -105,7 +106,8 @@ const CreateModal: React.FC<CreateModalProps> = ({
|
|||
};
|
||||
|
||||
try {
|
||||
const { success, message: errMsg } = await productcontrollerCreateproduct(payload as any);
|
||||
const { success, message: errMsg } =
|
||||
await productcontrollerCreateproduct(payload as any);
|
||||
if (success) {
|
||||
message.success('产品创建成功');
|
||||
onSuccess();
|
||||
|
|
@ -120,12 +122,15 @@ const CreateModal: React.FC<CreateModalProps> = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="分类">
|
||||
{category?.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions
|
||||
column={1}
|
||||
bordered
|
||||
size="small"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Descriptions.Item label="分类">{category?.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="属性">
|
||||
{attributes.map(attr => {
|
||||
{attributes.map((attr) => {
|
||||
const val = permutation[attr.name];
|
||||
if (!val) return null;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,31 +1,33 @@
|
|||
import {
|
||||
productcontrollerCreateproduct,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerGetproductlist,
|
||||
} from '@/servers/api/product';
|
||||
import { request } from '@umijs/max';
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer,
|
||||
ProCard,
|
||||
ProColumns,
|
||||
ProForm,
|
||||
ProFormSelect,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Table, Tag, message } from 'antd';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import CreateModal from './components/CreateModal';
|
||||
import { request } from '@umijs/max';
|
||||
import { Button, Tag, message } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import EditForm from '../List/EditForm';
|
||||
import CreateModal from './components/CreateModal';
|
||||
|
||||
const PermutationPage: React.FC = () => {
|
||||
const [categoryId, setCategoryId] = useState<number>();
|
||||
const [attributes, setAttributes] = useState<any[]>([]);
|
||||
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 [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 [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
|
|
@ -38,7 +40,7 @@ const PermutationPage: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
productcontrollerGetcategoriesall().then((res) => {
|
||||
const list = Array.isArray(res) ? res : (res?.data || []);
|
||||
const list = Array.isArray(res) ? res : res?.data || [];
|
||||
setCategories(list);
|
||||
if (list.length > 0) {
|
||||
setCategoryId(list[0].id);
|
||||
|
|
@ -53,15 +55,15 @@ const PermutationPage: React.FC = () => {
|
|||
const productRes = await productcontrollerGetproductlist({
|
||||
categoryId: catId,
|
||||
pageSize: 2000,
|
||||
current: 1
|
||||
current: 1,
|
||||
});
|
||||
const products = productRes.data?.items || [];
|
||||
|
||||
const productMap = new Map<string, API.Product>();
|
||||
products.forEach((p: any) => {
|
||||
if (p.attributes && Array.isArray(p.attributes)) {
|
||||
const key = generateAttributeKey(p.attributes);
|
||||
if (key) productMap.set(key, p);
|
||||
const key = generateAttributeKey(p.attributes);
|
||||
if (key) productMap.set(key, p);
|
||||
}
|
||||
});
|
||||
setExistingProducts(productMap);
|
||||
|
|
@ -101,8 +103,10 @@ const PermutationPage: React.FC = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
// 1. Fetch Attributes
|
||||
const attrRes = await productcontrollerGetcategoryattributes({ id: categoryId });
|
||||
const attrs = Array.isArray(attrRes) ? attrRes : (attrRes?.data || []);
|
||||
const attrRes = await productcontrollerGetcategoryattributes({
|
||||
id: categoryId,
|
||||
});
|
||||
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
|
||||
setAttributes(attrs);
|
||||
|
||||
// 2. Fetch Attribute Values (Dict Items)
|
||||
|
|
@ -110,15 +114,16 @@ const PermutationPage: React.FC = () => {
|
|||
for (const attr of attrs) {
|
||||
const dictId = attr.dict?.id || attr.dictId;
|
||||
if (dictId) {
|
||||
const itemsRes = await request('/dict/items', { params: { dictId } });
|
||||
valuesMap[attr.name] = itemsRes || [];
|
||||
const itemsRes = await request('/dict/items', {
|
||||
params: { dictId },
|
||||
});
|
||||
valuesMap[attr.name] = itemsRes || [];
|
||||
}
|
||||
}
|
||||
setAttributeValues(valuesMap);
|
||||
|
||||
// 3. Fetch Existing Products
|
||||
await fetchProducts(categoryId);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('获取数据失败');
|
||||
|
|
@ -137,11 +142,13 @@ const PermutationPage: React.FC = () => {
|
|||
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) {
|
||||
setPermutations([]);
|
||||
return;
|
||||
setPermutations([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const generateCombinations = (index: number, current: any): any[] => {
|
||||
|
|
@ -154,41 +161,42 @@ const PermutationPage: React.FC = () => {
|
|||
let res: any[] = [];
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const combos = generateCombinations(0, {});
|
||||
setPermutations(combos);
|
||||
|
||||
}, [attributes, attributeValues]);
|
||||
|
||||
const generateAttributeKey = (attrs: any[]) => {
|
||||
const parts = attrs.map(a => {
|
||||
const key = a.dict?.name || a.dictName;
|
||||
const val = a.name || a.value;
|
||||
return `${key}:${val}`;
|
||||
const parts = attrs.map((a) => {
|
||||
const key = a.dict?.name || a.dictName;
|
||||
const val = a.name || a.value;
|
||||
return `${key}:${val}`;
|
||||
});
|
||||
return parts.sort().join('|');
|
||||
};
|
||||
|
||||
const generateKeyFromPermutation = (perm: any) => {
|
||||
const parts = Object.keys(perm).map(attrName => {
|
||||
const valItem = perm[attrName];
|
||||
const val = valItem.name;
|
||||
return `${attrName}:${val}`;
|
||||
});
|
||||
return parts.sort().join('|');
|
||||
const parts = Object.keys(perm).map((attrName) => {
|
||||
const valItem = perm[attrName];
|
||||
const val = valItem.name;
|
||||
return `${attrName}:${val}`;
|
||||
});
|
||||
return parts.sort().join('|');
|
||||
};
|
||||
|
||||
const handleAdd = (record: any) => {
|
||||
setSelectedPermutation(record);
|
||||
setCreateModalVisible(true);
|
||||
setSelectedPermutation(record);
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const columns: any[] = [
|
||||
...attributes.map(attr => ({
|
||||
...attributes.map((attr) => ({
|
||||
title: attr.title || attr.name,
|
||||
dataIndex: attr.name,
|
||||
width: 100, // Make columns narrower
|
||||
|
|
@ -198,7 +206,10 @@ const PermutationPage: React.FC = () => {
|
|||
const valB = b[attr.name]?.name || '';
|
||||
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,
|
||||
})),
|
||||
{
|
||||
|
|
@ -241,13 +252,17 @@ const PermutationPage: React.FC = () => {
|
|||
const key = generateKeyFromPermutation(record);
|
||||
const product = existingProducts.get(key);
|
||||
if (product) {
|
||||
return (
|
||||
<EditForm
|
||||
record={product}
|
||||
tableRef={actionRef}
|
||||
trigger={<Button type="link" size="small">编辑</Button>}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<EditForm
|
||||
record={product}
|
||||
tableRef={actionRef}
|
||||
trigger={
|
||||
<Button type="link" size="small">
|
||||
编辑
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -273,8 +288,8 @@ const PermutationPage: React.FC = () => {
|
|||
label="选择分类"
|
||||
width="md"
|
||||
options={categories.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
fieldProps={{
|
||||
onChange: (val) => setCategoryId(val as number),
|
||||
|
|
@ -283,35 +298,35 @@ const PermutationPage: React.FC = () => {
|
|||
</ProForm>
|
||||
|
||||
{categoryId && (
|
||||
<ProTable
|
||||
size="small"
|
||||
dataSource={permutations}
|
||||
columns={columns}
|
||||
loading={loading || productsLoading}
|
||||
rowKey={(record) => generateKeyFromPermutation(record)}
|
||||
pagination={{
|
||||
defaultPageSize: 50,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000']
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
<ProTable
|
||||
size="small"
|
||||
dataSource={permutations}
|
||||
columns={columns}
|
||||
loading={loading || productsLoading}
|
||||
rowKey={(record) => generateKeyFromPermutation(record)}
|
||||
pagination={{
|
||||
defaultPageSize: 50,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
)}
|
||||
</ProCard>
|
||||
|
||||
{selectedPermutation && (
|
||||
<CreateModal
|
||||
visible={createModalVisible}
|
||||
onClose={() => setCreateModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setCreateModalVisible(false);
|
||||
if (categoryId) fetchProducts(categoryId);
|
||||
}}
|
||||
category={categories.find(c => c.id === categoryId) || null}
|
||||
permutation={selectedPermutation}
|
||||
attributes={attributes}
|
||||
visible={createModalVisible}
|
||||
onClose={() => setCreateModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
setCreateModalVisible(false);
|
||||
if (categoryId) fetchProducts(categoryId);
|
||||
}}
|
||||
category={categories.find((c) => c.id === categoryId) || null}
|
||||
permutation={selectedPermutation}
|
||||
attributes={attributes}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { ModalForm, ProFormText } from '@ant-design/pro-components';
|
||||
import { productcontrollerGetproductlist } from '@/servers/api/product';
|
||||
import { templatecontrollerGettemplatebyname } from '@/servers/api/template';
|
||||
import { EditOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import {
|
||||
ActionType,
|
||||
ModalForm,
|
||||
ProColumns,
|
||||
ProFormText,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
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 EditForm from '../List/EditForm';
|
||||
|
||||
|
|
@ -36,6 +41,10 @@ interface WpProduct {
|
|||
interface ProductWithWP extends API.Product {
|
||||
wpProducts: Record<string, WpProduct>;
|
||||
attributes?: any[];
|
||||
siteSkus?: Array<{
|
||||
siteSku: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 定义API响应接口
|
||||
|
|
@ -76,6 +85,11 @@ const ProductSyncPage: React.FC = () => {
|
|||
const [skuTemplate, setSkuTemplate] = useState<string>('');
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
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 产品
|
||||
useEffect(() => {
|
||||
|
|
@ -96,22 +110,23 @@ const ProductSyncPage: React.FC = () => {
|
|||
// 构建 WP 产品 Map,Key 为 SKU
|
||||
const map = new Map<string, WpProduct>();
|
||||
wpProductList.forEach((p) => {
|
||||
if (p.sku) {
|
||||
map.set(p.sku, p);
|
||||
}
|
||||
if (p.sku) {
|
||||
map.set(p.sku, p);
|
||||
}
|
||||
});
|
||||
setWpProductMap(map);
|
||||
|
||||
// 获取 SKU 模板
|
||||
try {
|
||||
const templateRes = await templatecontrollerGettemplatebyname({ name: 'site.product.sku' });
|
||||
const templateRes = await templatecontrollerGettemplatebyname({
|
||||
name: 'site.product.sku',
|
||||
});
|
||||
if (templateRes && templateRes.value) {
|
||||
setSkuTemplate(templateRes.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Template site.product.sku not found, using default.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
message.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 {
|
||||
const hide = message.loading('正在同步...', 0);
|
||||
const data = {
|
||||
|
|
@ -141,14 +161,14 @@ const ProductSyncPage: React.FC = () => {
|
|||
|
||||
let res;
|
||||
if (wpProductId) {
|
||||
res = await request(`/wp_product/siteId/${site.id}/products/${wpProductId}`, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
res = await request(`/site-api/${site.id}/products/${wpProductId}`, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
res = await request(`/wp_product/siteId/${site.id}/products`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
res = await request(`/site-api/${site.id}/products`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -156,16 +176,15 @@ const ProductSyncPage: React.FC = () => {
|
|||
if (!res.success) {
|
||||
hide();
|
||||
throw new Error(res.message || '同步失败');
|
||||
|
||||
}
|
||||
// 更新本地缓存 Map,避免刷新
|
||||
setWpProductMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
newMap.set(values.sku, res.data as WpProduct);
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
// 更新本地缓存 Map,避免刷新
|
||||
setWpProductMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
newMap.set(values.sku, res.data as WpProduct);
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
|
||||
hide();
|
||||
message.success('同步成功');
|
||||
|
|
@ -173,8 +192,139 @@ const ProductSyncPage: React.FC = () => {
|
|||
} catch (error: any) {
|
||||
message.error('同步失败: ' + (error.message || error.toString()));
|
||||
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) => {
|
||||
if (!template) return '';
|
||||
// 支持 <%= 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 keys = path.split('.');
|
||||
let value = data;
|
||||
for (const key of keys) {
|
||||
value = value?.[key];
|
||||
value = value?.[key];
|
||||
}
|
||||
return value === undefined || value === null ? '' : String(value);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 生成表格列配置
|
||||
|
|
@ -211,20 +364,33 @@ const ProductSyncPage: React.FC = () => {
|
|||
fixed: 'left',
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
|
||||
<div style={{ fontWeight: 'bold', fontSize: 14 }}>
|
||||
{record.name}
|
||||
</div>
|
||||
<EditForm
|
||||
record={record}
|
||||
tableRef={actionRef}
|
||||
trigger={<EditOutlined style={{ cursor: 'pointer', fontSize: 16, color: '#1890ff' }} />}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<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 style={{ fontSize: 12, color: '#666' }}>
|
||||
<span style={{ marginRight: 8 }}>
|
||||
价格: {record.price}
|
||||
</span>
|
||||
<span style={{ marginRight: 8 }}>价格: {record.price}</span>
|
||||
{record.promotionPrice && (
|
||||
<span style={{ color: 'red' }}>
|
||||
促销价: {record.promotionPrice}
|
||||
|
|
@ -234,24 +400,39 @@ const ProductSyncPage: React.FC = () => {
|
|||
|
||||
{/* 属性 */}
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{record.attributes?.map((attr: any, idx: number) => (
|
||||
<Tag key={idx} style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}>
|
||||
{attr.dict?.name || attr.name}: {attr.name}
|
||||
</Tag>
|
||||
))}
|
||||
{record.attributes?.map((attr: any, idx: number) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}
|
||||
>
|
||||
{attr.dict?.name || attr.name}: {attr.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 组成 (如果是 Bundle) */}
|
||||
{record.type === 'bundle' && record.components && record.components.length > 0 && (
|
||||
<div style={{ marginTop: 8, fontSize: 12, background: '#f5f5f5', padding: 4, borderRadius: 4 }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>Components:</div>
|
||||
{record.components.map((comp: any, idx: number) => (
|
||||
<div key={idx}>
|
||||
{comp.sku} × {comp.quantity}
|
||||
{record.type === 'bundle' &&
|
||||
record.components &&
|
||||
record.components.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
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 key={idx}>
|
||||
{comp.sku} × {comp.quantity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -265,18 +446,34 @@ const ProductSyncPage: React.FC = () => {
|
|||
hideInSearch: true,
|
||||
width: 220,
|
||||
render: (_, record) => {
|
||||
// 根据模板或默认规则生成期望的 SKU
|
||||
const expectedSku = skuTemplate
|
||||
? renderSku(skuTemplate, { site, product: record })
|
||||
: `${site.skuPrefix || ''}-${record.sku}`;
|
||||
// 首先查找该产品在该站点的实际SKU
|
||||
let siteProductSku = '';
|
||||
if (record.siteSkus && record.siteSkus.length > 0) {
|
||||
// 根据站点名称匹配对应的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);
|
||||
|
||||
// 如果没找到,且没有模板(或者即使有模板),尝试回退到默认规则查找(以防万一)
|
||||
if (!wpProduct && skuTemplate) {
|
||||
const fallbackSku = `${site.skuPrefix || ''}-${record.sku}`;
|
||||
wpProduct = wpProductMap.get(fallbackSku);
|
||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
||||
if (!wpProduct && siteProductSku && skuTemplate) {
|
||||
const templateSku = renderSku(skuTemplate, { site, product: record });
|
||||
wpProduct = wpProductMap.get(templateSku);
|
||||
}
|
||||
|
||||
if (!wpProduct) {
|
||||
|
|
@ -284,66 +481,89 @@ const ProductSyncPage: React.FC = () => {
|
|||
<ModalForm
|
||||
title="同步产品"
|
||||
trigger={
|
||||
<Button type="link" icon={<SyncOutlined />}>
|
||||
同步到站点
|
||||
</Button>
|
||||
<Button type="link" icon={<SyncOutlined />}>
|
||||
同步到站点
|
||||
</Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(values, record, site);
|
||||
return await syncProductToSite(values, record, site);
|
||||
}}
|
||||
initialValues={{
|
||||
sku: skuTemplate
|
||||
sku: siteProductSku || (
|
||||
skuTemplate
|
||||
? renderSku(skuTemplate, { site, product: record })
|
||||
: `${site.skuPrefix || ''}-${record.sku}`
|
||||
}}
|
||||
),
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="sku"
|
||||
label="商店 SKU"
|
||||
placeholder="请输入商店 SKU"
|
||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
||||
name="sku"
|
||||
label="商店 SKU"
|
||||
placeholder="请输入商店 SKU"
|
||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||
<div style={{ fontWeight: 'bold' }}>{wpProduct.sku}</div>
|
||||
<ModalForm
|
||||
title="更新同步"
|
||||
trigger={
|
||||
<Button type="link" size="small" icon={<SyncOutlined spin={false} />}>
|
||||
</Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(values, record, site, wpProduct.externalProductId);
|
||||
}}
|
||||
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
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>{wpProduct.sku}</div>
|
||||
<ModalForm
|
||||
title="更新同步"
|
||||
trigger={
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SyncOutlined spin={false} />}
|
||||
></Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(
|
||||
values,
|
||||
record,
|
||||
site,
|
||||
wpProduct.externalProductId,
|
||||
);
|
||||
}}
|
||||
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>Price: {wpProduct.regular_price ?? wpProduct.price}</div>
|
||||
{wpProduct.sale_price && (
|
||||
<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 }}>
|
||||
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>
|
||||
);
|
||||
|
|
@ -356,52 +576,137 @@ const ProductSyncPage: React.FC = () => {
|
|||
};
|
||||
|
||||
if (initialLoading) {
|
||||
return (
|
||||
<Card title="商品同步状态" className="product-sync-card">
|
||||
<Spin size="large" style={{ display: 'flex', justifyContent: 'center', padding: 40 }} />
|
||||
</Card>
|
||||
)
|
||||
return (
|
||||
<Card title="商品同步状态" className="product-sync-card">
|
||||
<Spin
|
||||
size="large"
|
||||
style={{ display: 'flex', justifyContent: 'center', padding: 40 }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="商品同步状态" className="product-sync-card">
|
||||
<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);
|
||||
<Card
|
||||
title="商品同步状态"
|
||||
className="product-sync-card"
|
||||
extra={
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Select
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择目标站点"
|
||||
value={selectedSiteId}
|
||||
onChange={setSelectedSiteId}
|
||||
options={sites.map(site => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}))}
|
||||
/>
|
||||
<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
|
||||
return {
|
||||
data: (data?.items || []) as ProductWithWP[],
|
||||
success,
|
||||
total: data?.total || 0,
|
||||
};
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
}}
|
||||
options={{
|
||||
density: true,
|
||||
fullScreen: true,
|
||||
}}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
// 返回给 ProTable
|
||||
return {
|
||||
data: (data?.items || []) as ProductWithWP[],
|
||||
success,
|
||||
total: data?.total || 0,
|
||||
};
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
}}
|
||||
options={{
|
||||
density: true,
|
||||
fullScreen: true,
|
||||
}}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import {
|
||||
ActionType,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { ordercontrollerSyncorder } from '@/servers/api/order';
|
||||
import {
|
||||
sitecontrollerCreate,
|
||||
sitecontrollerDisable,
|
||||
sitecontrollerList,
|
||||
sitecontrollerUpdate,
|
||||
} from '@/servers/api/site';
|
||||
import { wpproductcontrollerSyncproducts } from '@/servers/api/wpProduct';
|
||||
import { ordercontrollerSyncorder } from '@/servers/api/order';
|
||||
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 React, { useRef, useState } from 'react';
|
||||
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
||||
|
|
@ -91,9 +86,16 @@ const SiteList: React.FC = () => {
|
|||
message: '同步完成',
|
||||
description: (
|
||||
<div>
|
||||
<p>产品: 成功 {stats.products.success}, 失败 {stats.products.fail}</p>
|
||||
<p>订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}</p>
|
||||
<p>订阅: 成功 {stats.subscriptions.success}, 失败 {stats.subscriptions.fail}</p>
|
||||
<p>
|
||||
产品: 成功 {stats.products.success}, 失败 {stats.products.fail}
|
||||
</p>
|
||||
<p>
|
||||
订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}
|
||||
</p>
|
||||
<p>
|
||||
订阅: 成功 {stats.subscriptions.success}, 失败{' '}
|
||||
{stats.subscriptions.fail}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
duration: null, // 不自动关闭
|
||||
|
|
@ -124,7 +126,11 @@ const SiteList: React.FC = () => {
|
|||
dataIndex: 'websiteUrl',
|
||||
width: 280,
|
||||
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 前缀',
|
||||
|
|
@ -154,7 +160,9 @@ const SiteList: React.FC = () => {
|
|||
return (
|
||||
<Space wrap>
|
||||
{row.stockPoints.map((sp) => (
|
||||
<Tag color="blue" key={sp.id}>{sp.name}</Tag>
|
||||
<Tag color="blue" key={sp.id}>
|
||||
{sp.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
|
|
@ -175,7 +183,7 @@ const SiteList: React.FC = () => {
|
|||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
width: 240,
|
||||
fixed:"right",
|
||||
fixed: 'right',
|
||||
hideInSearch: true,
|
||||
render: (_, row) => (
|
||||
<Space>
|
||||
|
|
@ -197,12 +205,13 @@ const SiteList: React.FC = () => {
|
|||
</Button>
|
||||
<Popconfirm
|
||||
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
||||
description={
|
||||
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
|
||||
}
|
||||
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await sitecontrollerDisable({ id: String(row.id) }, { disabled: !row.isDisabled });
|
||||
await sitecontrollerDisable(
|
||||
{ id: String(row.id) },
|
||||
{ disabled: !row.isDisabled },
|
||||
);
|
||||
message.success('更新成功');
|
||||
actionRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -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 { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { DeleteFilled, EditOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
|
||||
|
||||
const BatchEditCustomers: React.FC<{
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
|
|
@ -27,22 +41,27 @@ const BatchEditCustomers: React.FC<{
|
|||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
let ok = 0, fail = 0;
|
||||
let ok = 0,
|
||||
fail = 0;
|
||||
for (const id of selectedRowKeys) {
|
||||
try {
|
||||
// Remove undefined values
|
||||
const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== ''));
|
||||
if (Object.keys(data).length === 0) continue;
|
||||
try {
|
||||
// Remove undefined values
|
||||
const data = Object.fromEntries(
|
||||
Object.entries(values).filter(
|
||||
([_, v]) => v !== undefined && v !== '',
|
||||
),
|
||||
);
|
||||
if (Object.keys(data).length === 0) continue;
|
||||
|
||||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
});
|
||||
if (res.success) ok++;
|
||||
else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
});
|
||||
if (res.success) ok++;
|
||||
else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
message.success(`成功 ${ok}, 失败 ${fail}`);
|
||||
tableRef.current?.reload();
|
||||
|
|
@ -50,8 +69,16 @@ const BatchEditCustomers: React.FC<{
|
|||
return true;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="role" label="角色" placeholder="请输入角色,不修改请留空" />
|
||||
<ProFormText name="phone" label="电话" placeholder="请输入电话,不修改请留空" />
|
||||
<ProFormText
|
||||
name="role"
|
||||
label="角色"
|
||||
placeholder="请输入角色,不修改请留空"
|
||||
/>
|
||||
<ProFormText
|
||||
name="phone"
|
||||
label="电话"
|
||||
placeholder="请输入电话,不修改请留空"
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
|
@ -75,7 +102,9 @@ const CustomerPage: React.FC = () => {
|
|||
const handleDelete = async (id: number) => {
|
||||
if (!siteId) return;
|
||||
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) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -112,7 +141,7 @@ const CustomerPage: React.FC = () => {
|
|||
copyable: true,
|
||||
render: (_, record) => {
|
||||
return record?.id ?? '-';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
|
|
@ -139,7 +168,8 @@ const CustomerPage: React.FC = () => {
|
|||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
render: (_, record) => record.phone || record.billing?.phone || record.shipping?.phone || '-',
|
||||
render: (_, record) =>
|
||||
record.phone || record.billing?.phone || record.shipping?.phone || '-',
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -159,12 +189,16 @@ const CustomerPage: React.FC = () => {
|
|||
const { billing } = record;
|
||||
if (!billing) return '-';
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div>{billing.address_1} {billing.address_2}</div>
|
||||
<div>{billing.city}, {billing.state}, {billing.postcode}</div>
|
||||
<div>{billing.country}</div>
|
||||
<div>{billing.phone}</div>
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div>
|
||||
{billing.address_1} {billing.address_2}
|
||||
</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: '操作',
|
||||
valueType: 'option',
|
||||
width: 120,
|
||||
fixed:"right",
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" title="编辑" icon={<EditOutlined />} onClick={() => setEditing(record)} />
|
||||
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button
|
||||
type="link"
|
||||
title="编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setEditing(record)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
||||
</Popconfirm>
|
||||
<Button type="link" title="查询订单" onClick={() => { setOrdersCustomer(record); setOrdersVisible(true); }}>
|
||||
<Button
|
||||
type="link"
|
||||
title="查询订单"
|
||||
onClick={() => {
|
||||
setOrdersCustomer(record);
|
||||
setOrdersVisible(true);
|
||||
}}
|
||||
>
|
||||
查询订单
|
||||
</Button>
|
||||
</Space>
|
||||
|
|
@ -195,11 +244,11 @@ const CustomerPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<PageContainer
|
||||
ghost
|
||||
header={{
|
||||
title: null,
|
||||
breadcrumb: undefined
|
||||
}}
|
||||
ghost
|
||||
header={{
|
||||
title: null,
|
||||
breadcrumb: undefined,
|
||||
}}
|
||||
>
|
||||
<ProTable
|
||||
rowKey="id"
|
||||
|
|
@ -232,7 +281,7 @@ const CustomerPage: React.FC = () => {
|
|||
page_size: pageSize,
|
||||
where,
|
||||
...(orderObj ? { order: orderObj } : {}),
|
||||
...((name || email) ? { search: name || email } : {}),
|
||||
...(name || email ? { search: name || email } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -255,10 +304,15 @@ const CustomerPage: React.FC = () => {
|
|||
toolBarRender={() => [
|
||||
<DrawerForm
|
||||
title="新增客户"
|
||||
trigger={<Button type="primary" title="新增" icon={<PlusOutlined />} />}
|
||||
trigger={
|
||||
<Button type="primary" title="新增" icon={<PlusOutlined />} />
|
||||
}
|
||||
onFinish={async (values) => {
|
||||
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) {
|
||||
message.success('新增成功');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -268,11 +322,15 @@ const CustomerPage: React.FC = () => {
|
|||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText name="email" label="邮箱" rules={[{ required: true }]} />
|
||||
<ProFormText
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[{ required: true }]}
|
||||
/>
|
||||
<ProFormText name="first_name" label="名" />
|
||||
<ProFormText name="last_name" label="姓" />
|
||||
<ProFormText name="username" label="用户名" />
|
||||
<ProFormText name="phone" label="电话" />
|
||||
<ProFormText name="phone" label="电话" />
|
||||
</DrawerForm>,
|
||||
<BatchEditCustomers
|
||||
tableRef={actionRef}
|
||||
|
|
@ -284,10 +342,17 @@ const CustomerPage: React.FC = () => {
|
|||
title="批量导出"
|
||||
onClick={async () => {
|
||||
if (!siteId) return;
|
||||
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined;
|
||||
const res = await request(`/site-api/${siteId}/customers/export`, { params: { ids: idsParam } });
|
||||
const idsParam = selectedRowKeys.length
|
||||
? (selectedRowKeys as any[]).join(',')
|
||||
: undefined;
|
||||
const res = await request(
|
||||
`/site-api/${siteId}/customers/export`,
|
||||
{ params: { ids: idsParam } },
|
||||
);
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
@ -298,17 +363,26 @@ const CustomerPage: React.FC = () => {
|
|||
message.error(res.message || '导出失败');
|
||||
}
|
||||
}}
|
||||
>批量导出</Button>,
|
||||
>
|
||||
批量导出
|
||||
</Button>,
|
||||
<ModalForm
|
||||
title="批量导入客户"
|
||||
trigger={<Button type="primary" ghost>批量导入</Button>}
|
||||
trigger={
|
||||
<Button type="primary" ghost>
|
||||
批量导入
|
||||
</Button>
|
||||
}
|
||||
width={600}
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
const csv = values.csv || '';
|
||||
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) {
|
||||
message.success('导入完成');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -318,7 +392,11 @@ const CustomerPage: React.FC = () => {
|
|||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormTextArea name="csv" label="CSV文本" placeholder="粘贴CSV,首行为表头" />
|
||||
<ProFormTextArea
|
||||
name="csv"
|
||||
label="CSV文本"
|
||||
placeholder="粘贴CSV,首行为表头"
|
||||
/>
|
||||
</ModalForm>,
|
||||
|
||||
<Button
|
||||
|
|
@ -327,7 +405,10 @@ const CustomerPage: React.FC = () => {
|
|||
icon={<DeleteFilled />}
|
||||
onClick={async () => {
|
||||
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();
|
||||
setSelectedRowKeys([]);
|
||||
if (res.success) {
|
||||
|
|
@ -336,7 +417,7 @@ const CustomerPage: React.FC = () => {
|
|||
message.warning(res.message || '部分删除失败');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
|
|
@ -347,7 +428,10 @@ const CustomerPage: React.FC = () => {
|
|||
initialValues={editing || {}}
|
||||
onFinish={async (values) => {
|
||||
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) {
|
||||
message.success('更新成功');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -362,11 +446,14 @@ const CustomerPage: React.FC = () => {
|
|||
<ProFormText name="first_name" label="名" />
|
||||
<ProFormText name="last_name" label="姓" />
|
||||
<ProFormText name="username" label="用户名" />
|
||||
<ProFormText name="phone" label="电话" />
|
||||
<ProFormText name="phone" label="电话" />
|
||||
</DrawerForm>
|
||||
<Modal
|
||||
open={ordersVisible}
|
||||
onCancel={() => { setOrdersVisible(false); setOrdersCustomer(null); }}
|
||||
onCancel={() => {
|
||||
setOrdersVisible(false);
|
||||
setOrdersCustomer(null);
|
||||
}}
|
||||
footer={null}
|
||||
width={1000}
|
||||
title="客户订单"
|
||||
|
|
@ -386,7 +473,12 @@ const CustomerPage: React.FC = () => {
|
|||
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: 'status', hideInSearch: true },
|
||||
{ title: '来源', dataIndex: 'created_via', hideInSearch: true },
|
||||
|
|
@ -408,13 +500,17 @@ const CustomerPage: React.FC = () => {
|
|||
},
|
||||
]}
|
||||
request={async (params) => {
|
||||
if (!siteId || !ordersCustomer?.id) return { data: [], total: 0, success: true };
|
||||
const res = await request(`/site-api/${siteId}/customers/${ordersCustomer.id}/orders`, {
|
||||
params: {
|
||||
page: params.current,
|
||||
per_page: params.pageSize,
|
||||
if (!siteId || !ordersCustomer?.id)
|
||||
return { data: [], total: 0, success: true };
|
||||
const res = await request(
|
||||
`/site-api/${siteId}/customers/${ordersCustomer.id}/orders`,
|
||||
{
|
||||
params: {
|
||||
page: params.current,
|
||||
per_page: params.pageSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
if (!res?.success) {
|
||||
message.error(res?.message || '获取订单失败');
|
||||
return { data: [], total: 0, success: false };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import { areacontrollerGetarealist } from '@/servers/api/area';
|
||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
import {
|
||||
DrawerForm,
|
||||
ProFormDependency,
|
||||
|
|
@ -9,8 +10,6 @@ import {
|
|||
} from '@ant-design/pro-components';
|
||||
import { Form } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import { areacontrollerGetarealist } from '@/servers/api/area';
|
||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
|
||||
// 定义组件的 props 类型
|
||||
interface EditSiteFormProps {
|
||||
|
|
@ -67,7 +66,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
<ProFormTextArea name="description" label="描述" placeholder="请输入描述" />
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="描述"
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
<ProFormText
|
||||
name="apiUrl"
|
||||
label="API 地址"
|
||||
|
|
@ -99,14 +102,22 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
<ProFormText
|
||||
name="consumerKey"
|
||||
label="Consumer Key"
|
||||
rules={[{ required: !isEdit, message: '请输入 Consumer Key' }]}
|
||||
placeholder={isEdit ? '留空表示不修改' : '请输入 Consumer Key'}
|
||||
rules={[
|
||||
{ required: !isEdit, message: '请输入 Consumer Key' },
|
||||
]}
|
||||
placeholder={
|
||||
isEdit ? '留空表示不修改' : '请输入 Consumer Key'
|
||||
}
|
||||
/>
|
||||
<ProFormText
|
||||
name="consumerSecret"
|
||||
label="Consumer Secret"
|
||||
rules={[{ required: !isEdit, message: '请输入 Consumer Secret' }]}
|
||||
placeholder={isEdit ? '留空表示不修改' : '请输入 Consumer Secret'}
|
||||
rules={[
|
||||
{ required: !isEdit, message: '请输入 Consumer Secret' },
|
||||
]}
|
||||
placeholder={
|
||||
isEdit ? '留空表示不修改' : '请输入 Consumer Secret'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
@ -125,7 +136,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
return null;
|
||||
}}
|
||||
</ProFormDependency>
|
||||
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder="请输入 SKU 前缀" />
|
||||
<ProFormText
|
||||
name="skuPrefix"
|
||||
label="SKU 前缀"
|
||||
placeholder="请输入 SKU 前缀"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="areas"
|
||||
label="区域"
|
||||
|
|
@ -135,7 +150,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
// 从后端接口获取区域数据
|
||||
const res = await areacontrollerGetarealist({ pageSize: 1000 });
|
||||
// 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
|
||||
|
|
@ -147,7 +165,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
// 从后端接口获取仓库数据
|
||||
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="是否禁用" />
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { request } from '@umijs/max';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import {
|
||||
PageContainer,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Outlet, history, useLocation, useParams } from '@umijs/max';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
|
||||
import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import EditSiteForm from './EditSiteForm';
|
||||
import type { SiteItem } from '../List/index';
|
||||
import EditSiteForm from './EditSiteForm';
|
||||
|
||||
const ShopLayout: React.FC = () => {
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
|
|
@ -102,15 +99,29 @@ const ShopLayout: React.FC = () => {
|
|||
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
|
||||
<Col span={4} style={{ height: '100%' }}>
|
||||
<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' }}
|
||||
>
|
||||
<div style={{ padding: '0 10px 16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
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}
|
||||
onChange={handleSiteChange}
|
||||
showSearch
|
||||
|
|
@ -120,7 +131,9 @@ const ShopLayout: React.FC = () => {
|
|||
icon={<EditOutlined />}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => {
|
||||
const currentSite = sites.find(site => site.id === Number(siteId));
|
||||
const currentSite = sites.find(
|
||||
(site) => site.id === Number(siteId),
|
||||
);
|
||||
if (currentSite) {
|
||||
setEditingSite(currentSite);
|
||||
setEditModalOpen(true);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import {
|
|||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
import { formatUniuniShipmentState } from '@/utils/format';
|
||||
import { printPDF } from '@/utils/util';
|
||||
import { CopyOutlined, FilePdfOutlined, ReloadOutlined, DeleteFilled } from '@ant-design/icons';
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteFilled,
|
||||
FilePdfOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer,
|
||||
|
|
@ -150,7 +155,12 @@ const LogisticsPage: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" danger title="删除" icon={<DeleteFilled />} />
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
title="删除"
|
||||
icon={<DeleteFilled />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
<ToastContainer />
|
||||
</>
|
||||
|
|
@ -183,7 +193,11 @@ const LogisticsPage: React.FC = () => {
|
|||
if (siteId) {
|
||||
params.siteId = Number(siteId);
|
||||
}
|
||||
const { data, success, message: errMsg } = await logisticscontrollerGetlist({
|
||||
const {
|
||||
data,
|
||||
success,
|
||||
message: errMsg,
|
||||
} = await logisticscontrollerGetlist({
|
||||
params,
|
||||
});
|
||||
if (success) {
|
||||
|
|
@ -217,8 +231,9 @@ const LogisticsPage: React.FC = () => {
|
|||
setIsLoading(true);
|
||||
let ok = 0;
|
||||
for (const row of selectedRows) {
|
||||
const { success } = await logisticscontrollerDeleteshipment({ id: row.id });
|
||||
if (success) ok++;
|
||||
const { success } =
|
||||
await logisticscontrollerDeleteshipment({ id: row.id });
|
||||
if (success) ok++;
|
||||
}
|
||||
message.success(`成功删除 ${ok} 条`);
|
||||
setIsLoading(false);
|
||||
|
|
|
|||
|
|
@ -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 { App, Button, Image, Popconfirm, Space } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const MediaPage: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
|
|
@ -62,7 +70,7 @@ const MediaPage: React.FC = () => {
|
|||
copyable: true,
|
||||
render: (_, record) => {
|
||||
return record?.id ?? '-';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '展示',
|
||||
|
|
@ -71,7 +79,12 @@ const MediaPage: React.FC = () => {
|
|||
render: (_, record) => (
|
||||
<Image
|
||||
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"
|
||||
/>
|
||||
),
|
||||
|
|
@ -100,6 +113,31 @@ const MediaPage: React.FC = () => {
|
|||
dataIndex: 'mime_type',
|
||||
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: '创建时间',
|
||||
dataIndex: 'date_created',
|
||||
|
|
@ -140,11 +178,11 @@ const MediaPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<PageContainer
|
||||
ghost
|
||||
header={{
|
||||
title: null,
|
||||
breadcrumb: undefined
|
||||
}}
|
||||
ghost
|
||||
header={{
|
||||
title: null,
|
||||
breadcrumb: undefined,
|
||||
}}
|
||||
>
|
||||
<ProTable
|
||||
rowKey="id"
|
||||
|
|
@ -192,7 +230,11 @@ const MediaPage: React.FC = () => {
|
|||
toolBarRender={() => [
|
||||
<ModalForm
|
||||
title="上传媒体"
|
||||
trigger={<Button type="primary" title="上传媒体" icon={<PlusOutlined />}>上传媒体</Button>}
|
||||
trigger={
|
||||
<Button type="primary" title="上传媒体" icon={<PlusOutlined />}>
|
||||
上传媒体
|
||||
</Button>
|
||||
}
|
||||
width={500}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
|
|
@ -204,13 +246,12 @@ const MediaPage: React.FC = () => {
|
|||
formData.append('file', f.originFileObj);
|
||||
});
|
||||
} else {
|
||||
message.warning('请选择文件');
|
||||
return false;
|
||||
message.warning('请选择文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = await request('/media/upload', {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
const res = await siteapicontrollerCreatemedia({
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
|
|
@ -230,23 +271,27 @@ const MediaPage: React.FC = () => {
|
|||
<ProFormUploadButton
|
||||
name="file"
|
||||
label="文件"
|
||||
max={1}
|
||||
fieldProps={{
|
||||
name: 'file',
|
||||
listType: 'picture-card',
|
||||
}}
|
||||
rules={[{ required: true, message: '请选择文件' }]}
|
||||
/>
|
||||
</ModalForm>
|
||||
,
|
||||
</ModalForm>,
|
||||
<Button
|
||||
title="批量导出"
|
||||
onClick={async () => {
|
||||
if (!siteId) return;
|
||||
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined;
|
||||
const res = await request(`/site-api/${siteId}/media/export`, { params: { ids: idsParam } });
|
||||
const idsParam = selectedRowKeys.length
|
||||
? (selectedRowKeys as any[]).join(',')
|
||||
: undefined;
|
||||
const res = await request(`/site-api/${siteId}/media/export`, {
|
||||
params: { ids: idsParam },
|
||||
});
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
@ -269,7 +314,10 @@ const MediaPage: React.FC = () => {
|
|||
// 条件判断 如果站点編號不存在則直接返回
|
||||
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) {
|
||||
message.success('批量删除成功');
|
||||
|
|
@ -290,9 +338,8 @@ const MediaPage: React.FC = () => {
|
|||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Popconfirm>,
|
||||
|
||||
,
|
||||
<Button
|
||||
title="批量转换为WebP"
|
||||
disabled={!selectedRowKeys.length}
|
||||
|
|
@ -301,16 +348,21 @@ const MediaPage: React.FC = () => {
|
|||
if (!siteId) return;
|
||||
try {
|
||||
// 发起后端批量转换请求
|
||||
const response = await request(`/site-api/${siteId}/media/convert-webp`, {
|
||||
method: 'POST',
|
||||
data: { ids: selectedRowKeys },
|
||||
});
|
||||
const response = await request(
|
||||
`/site-api/${siteId}/media/convert-webp`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: { ids: selectedRowKeys },
|
||||
},
|
||||
);
|
||||
// 条件判断 根据接口返回结果进行提示
|
||||
if (response.success) {
|
||||
const convertedCount = response?.data?.converted?.length || 0;
|
||||
const failedCount = response?.data?.failed?.length || 0;
|
||||
if (failedCount > 0) {
|
||||
message.warning(`部分转换失败 已转换 ${convertedCount} 失败 ${failedCount}`);
|
||||
message.warning(
|
||||
`部分转换失败 已转换 ${convertedCount} 失败 ${failedCount}`,
|
||||
);
|
||||
} else {
|
||||
message.success(`转换成功 已转换 ${convertedCount}`);
|
||||
}
|
||||
|
|
@ -325,7 +377,7 @@ const MediaPage: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
批量转换为WebP
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,24 @@
|
|||
import styles from '@/style/order-list.css';
|
||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
||||
import {
|
||||
ordercontrollerChangestatus,
|
||||
} from '@/servers/api/order';
|
||||
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||
import {
|
||||
EllipsisOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import styles from '@/style/order-list.css';
|
||||
import { DeleteFilled, EllipsisOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
ModalForm,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
ModalForm,
|
||||
ProFormTextArea,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request, useParams } from '@umijs/max';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Tabs,
|
||||
TabsProps,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { App, Button, Dropdown, Popconfirm, Tabs, TabsProps } from 'antd';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BatchEditOrders, CreateOrder, EditOrder, OrderNote, Shipping } from '../components/Order/Forms';
|
||||
import { DeleteFilled } from '@ant-design/icons';
|
||||
import {
|
||||
BatchEditOrders,
|
||||
CreateOrder,
|
||||
EditOrder,
|
||||
OrderNote,
|
||||
} from '../components/Order/Forms';
|
||||
|
||||
const OrdersPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
|
@ -70,10 +58,7 @@ const OrdersPage: React.FC = () => {
|
|||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{ key: 'all', label: `全部(${total})` },
|
||||
...tabs,
|
||||
];
|
||||
return [{ key: 'all', label: `全部(${total})` }, ...tabs];
|
||||
}, [count]);
|
||||
|
||||
const columns: ProColumns<API.Order>[] = [
|
||||
|
|
@ -126,9 +111,7 @@ const OrdersPage: React.FC = () => {
|
|||
return (
|
||||
<div>
|
||||
{record.line_items.map((item: any) => (
|
||||
<div key={item.id}>
|
||||
{`${item.name} x ${item.quantity}`}
|
||||
</div>
|
||||
<div key={item.id}>{`${item.name} x ${item.quantity}`}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -194,7 +177,9 @@ const OrdersPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
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="确定删除订单?"
|
||||
onConfirm={async () => {
|
||||
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) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -272,15 +260,20 @@ const OrdersPage: React.FC = () => {
|
|||
message.warning(res.message || '部分删除失败');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
,
|
||||
/>,
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!siteId) return;
|
||||
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined;
|
||||
const res = await request(`/site-api/${siteId}/orders/export`, { params: { ids: idsParam } });
|
||||
const idsParam = selectedRowKeys.length
|
||||
? (selectedRowKeys as any[]).join(',')
|
||||
: undefined;
|
||||
const res = await request(`/site-api/${siteId}/orders/export`, {
|
||||
params: { ids: idsParam },
|
||||
});
|
||||
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 a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
@ -291,17 +284,26 @@ const OrdersPage: React.FC = () => {
|
|||
message.error(res.message || '导出失败');
|
||||
}
|
||||
}}
|
||||
>批量导出</Button>,
|
||||
>
|
||||
批量导出
|
||||
</Button>,
|
||||
<ModalForm
|
||||
title="批量导入订单"
|
||||
trigger={<Button type="primary" ghost>批量导入</Button>}
|
||||
trigger={
|
||||
<Button type="primary" ghost>
|
||||
批量导入
|
||||
</Button>
|
||||
}
|
||||
width={600}
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
const csv = values.csv || '';
|
||||
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) {
|
||||
message.success('导入完成');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -311,9 +313,12 @@ const OrdersPage: React.FC = () => {
|
|||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormTextArea name="csv" label="CSV文本" placeholder="粘贴CSV,首行为表头" />
|
||||
</ModalForm>
|
||||
|
||||
<ProFormTextArea
|
||||
name="csv"
|
||||
label="CSV文本"
|
||||
placeholder="粘贴CSV,首行为表头"
|
||||
/>
|
||||
</ModalForm>,
|
||||
]}
|
||||
request={async (params, sort, filter) => {
|
||||
const p: any = params || {};
|
||||
|
|
@ -321,7 +326,13 @@ const OrdersPage: React.FC = () => {
|
|||
const pageSize = p.pageSize;
|
||||
const date = p.date;
|
||||
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 };
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
|
|
@ -345,7 +356,7 @@ const OrdersPage: React.FC = () => {
|
|||
page_size: pageSize,
|
||||
where,
|
||||
...(orderObj ? { order: orderObj } : {}),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
|
@ -379,7 +390,7 @@ const OrdersPage: React.FC = () => {
|
|||
const { status: _status, ...baseWhere } = where;
|
||||
// 并发请求各状态的总数,对站点接口不支持的状态使用0
|
||||
const results = await Promise.all(
|
||||
statusKeys.map(async key => {
|
||||
statusKeys.map(async (key) => {
|
||||
// 将前端退款状态映射为站点接口可能识别的原始状态
|
||||
const mapToRawStatus: Record<string, string> = {
|
||||
refund_requested: 'return-requested',
|
||||
|
|
@ -388,7 +399,10 @@ const OrdersPage: React.FC = () => {
|
|||
};
|
||||
const rawStatus = mapToRawStatus[key] || key;
|
||||
// 对扩展状态直接返回0,减少不必要的请求
|
||||
const unsupported = ['after_sale_pending', 'pending_reshipment'];
|
||||
const unsupported = [
|
||||
'after_sale_pending',
|
||||
'pending_reshipment',
|
||||
];
|
||||
if (unsupported.includes(key)) {
|
||||
return { status: key, count: 0 };
|
||||
}
|
||||
|
|
@ -406,7 +420,7 @@ const OrdersPage: React.FC = () => {
|
|||
// 请求失败时该状态数量记为0
|
||||
return { status: key, count: 0 };
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
setCount(results);
|
||||
} catch (e) {
|
||||
|
|
@ -418,15 +432,14 @@ const OrdersPage: React.FC = () => {
|
|||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
success: true
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: [],
|
||||
success: false
|
||||
success: false,
|
||||
};
|
||||
}}
|
||||
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
|
||||
import { request } from '@umijs/max';
|
||||
import { useParams } from '@umijs/max';
|
||||
import { DeleteFilled, LinkOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request, useParams } from '@umijs/max';
|
||||
import { App, Button, Divider, Popconfirm, Tag } from 'antd';
|
||||
import { DeleteFilled, LinkOutlined } from '@ant-design/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ErpProductBindModal } from '../components/Product/ErpProductBindModal';
|
||||
import {
|
||||
BatchDeleteProducts,
|
||||
BatchEditProducts,
|
||||
CreateProduct,
|
||||
ImportCsv,
|
||||
SetComponent,
|
||||
UpdateForm,
|
||||
UpdateStatus,
|
||||
UpdateVaritation,
|
||||
BatchDeleteProducts,
|
||||
CreateProduct,
|
||||
} from '../components/Product/Forms';
|
||||
import { TagConfig } from '../components/Product/utils';
|
||||
const ProductsPage: React.FC = () => {
|
||||
|
|
@ -67,11 +67,22 @@ const ProductsPage: React.FC = () => {
|
|||
if (!dict) {
|
||||
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);
|
||||
};
|
||||
|
||||
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('fruit'),
|
||||
getItems('mint'),
|
||||
|
|
@ -90,7 +101,7 @@ const ProductsPage: React.FC = () => {
|
|||
strengths,
|
||||
sizes,
|
||||
humidities,
|
||||
categories
|
||||
categories,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch configs:', error);
|
||||
|
|
@ -100,7 +111,7 @@ const ProductsPage: React.FC = () => {
|
|||
fetchAllConfigs();
|
||||
}, []);
|
||||
|
||||
const columns: ProColumns<API.UnifiedProductDTO>[] = [
|
||||
const columns: ProColumns<any>[] = [
|
||||
{
|
||||
// ID
|
||||
title: 'ID',
|
||||
|
|
@ -110,7 +121,7 @@ const ProductsPage: React.FC = () => {
|
|||
copyable: true,
|
||||
render: (_, record) => {
|
||||
return record?.id ?? '-';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// sku
|
||||
|
|
@ -123,6 +134,11 @@ const ProductsPage: React.FC = () => {
|
|||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
// 产品类型
|
||||
title: '产品类型',
|
||||
dataIndex: 'type',
|
||||
},
|
||||
{
|
||||
// 产品状态
|
||||
title: '产品状态',
|
||||
|
|
@ -130,11 +146,7 @@ const ProductsPage: React.FC = () => {
|
|||
valueType: 'select',
|
||||
valueEnum: PRODUCT_STATUS_ENUM,
|
||||
},
|
||||
{
|
||||
// 产品类型
|
||||
title: '产品类型',
|
||||
dataIndex: 'type',
|
||||
},
|
||||
|
||||
{
|
||||
// 库存状态
|
||||
title: '库存状态',
|
||||
|
|
@ -148,6 +160,43 @@ const ProductsPage: React.FC = () => {
|
|||
dataIndex: 'stock_quantity',
|
||||
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: '图片',
|
||||
|
|
@ -172,29 +221,7 @@ const ProductsPage: React.FC = () => {
|
|||
dataIndex: 'sale_price',
|
||||
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: '分类',
|
||||
|
|
@ -228,10 +255,13 @@ const ProductsPage: React.FC = () => {
|
|||
// 检查 record.attributes 是否存在并且是一个数组
|
||||
if (record.attributes && Array.isArray(record.attributes)) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
||||
>
|
||||
{(record.attributes as any[]).map((attr: any) => (
|
||||
<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>
|
||||
|
|
@ -240,6 +270,29 @@ const ProductsPage: React.FC = () => {
|
|||
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: '创建时间',
|
||||
|
|
@ -263,16 +316,38 @@ const ProductsPage: React.FC = () => {
|
|||
width: '200',
|
||||
render: (_, record) => (
|
||||
<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} />
|
||||
{siteId && (
|
||||
<ErpProductBindModal
|
||||
trigger={
|
||||
<Button
|
||||
type="link"
|
||||
title={record.erpProduct ? '换绑ERP产品' : '绑定ERP产品'}
|
||||
>
|
||||
{record.erpProduct ? '换绑' : '绑定'}
|
||||
</Button>
|
||||
}
|
||||
siteProduct={record}
|
||||
siteId={siteId}
|
||||
onBindSuccess={() => {
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
title="店铺链接"
|
||||
icon={<LinkOutlined />}
|
||||
disabled={!record.frontendUrl}
|
||||
disabled={!record.permalink}
|
||||
onClick={() => {
|
||||
if (record.frontendUrl) {
|
||||
window.open(record.frontendUrl, '_blank', 'noopener,noreferrer');
|
||||
if (record.permalink) {
|
||||
window.open(record.permalink, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
message.warning('未能生成店铺链接');
|
||||
}
|
||||
|
|
@ -284,7 +359,9 @@ const ProductsPage: React.FC = () => {
|
|||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await request(`/site-api/${siteId}/products/${record.id}`, { method: 'DELETE' });
|
||||
await request(`/site-api/${siteId}/products/${record.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
|
|
@ -295,11 +372,11 @@ const ProductsPage: React.FC = () => {
|
|||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
||||
</Popconfirm>
|
||||
{record.type === 'simple' && record.sku ? (
|
||||
<SetComponent
|
||||
tableRef={actionRef}
|
||||
values={record}
|
||||
isProduct={true}
|
||||
/>
|
||||
<SetComponent
|
||||
tableRef={actionRef}
|
||||
values={record}
|
||||
isProduct={true}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
|
@ -315,7 +392,7 @@ const ProductsPage: React.FC = () => {
|
|||
<ProTable<API.UnifiedProductDTO>
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000','2000'],
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
|
||||
showSizeChanger: true,
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
|
|
@ -346,8 +423,13 @@ const ProductsPage: React.FC = () => {
|
|||
page,
|
||||
per_page: pageSize,
|
||||
...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) {
|
||||
|
|
@ -385,24 +467,37 @@ const ProductsPage: React.FC = () => {
|
|||
siteId={siteId}
|
||||
/>,
|
||||
<ImportCsv tableRef={actionRef} siteId={siteId} />,
|
||||
<Button onClick={async () => {
|
||||
const idsParam = selectedRowKeys.length ? (selectedRowKeys as any[]).join(',') : undefined;
|
||||
const res = await request(`/site-api/${siteId}/products/export`, { params: { ids: idsParam } });
|
||||
if (res?.success && res?.data?.csv) {
|
||||
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'products.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}}>批量导出</Button>,
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const idsParam = selectedRowKeys.length
|
||||
? (selectedRowKeys as any[]).join(',')
|
||||
: undefined;
|
||||
const res = await request(`/site-api/${siteId}/products/export`, {
|
||||
params: { ids: idsParam },
|
||||
});
|
||||
if (res?.success && res?.data?.csv) {
|
||||
const blob = new Blob([res.data.csv], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'products.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
批量导出
|
||||
</Button>,
|
||||
]}
|
||||
expandable={{
|
||||
rowExpandable: (record) => record.type === 'variable',
|
||||
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>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
|
|
@ -411,12 +506,20 @@ const ProductsPage: React.FC = () => {
|
|||
width: 120,
|
||||
render: (_, row) => {
|
||||
return row?.id ?? '-';
|
||||
}
|
||||
},
|
||||
},
|
||||
{ title: '变体名', dataIndex: 'name' },
|
||||
{ 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',
|
||||
dataIndex: 'attributes',
|
||||
|
|
@ -425,7 +528,13 @@ const ProductsPage: React.FC = () => {
|
|||
// 检查 row.attributes 是否存在并且是一个数组
|
||||
if (row.attributes && Array.isArray(row.attributes)) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{(row.attributes as any[]).map((attr: any) => (
|
||||
<div key={attr.name}>
|
||||
<strong>{attr.name}:</strong> {attr.option}
|
||||
|
|
@ -443,11 +552,20 @@ const ProductsPage: React.FC = () => {
|
|||
valueType: 'option',
|
||||
render: (_, row) => (
|
||||
<>
|
||||
<UpdateVaritation tableRef={actionRef} values={row} siteId={siteId} productId={productExternalId} />
|
||||
<UpdateVaritation
|
||||
tableRef={actionRef}
|
||||
values={row}
|
||||
siteId={siteId}
|
||||
productId={productExternalId}
|
||||
/>
|
||||
{row.sku ? (
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<SetComponent tableRef={actionRef} values={row} isProduct={false} />
|
||||
<SetComponent
|
||||
tableRef={actionRef}
|
||||
values={row}
|
||||
isProduct={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
interface ReviewFormProps {
|
||||
|
|
@ -9,7 +8,13 @@ interface ReviewFormProps {
|
|||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ReviewForm: React.FC<ReviewFormProps> = ({ open, editing, siteId, onClose, onSuccess }) => {
|
||||
const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||
open,
|
||||
editing,
|
||||
siteId,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
// // 这是一个临时的占位符组件
|
||||
// // 你可以在这里实现表单逻辑
|
||||
if (!open) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { ActionType, ProTable, ProColumns, ProCard } from '@ant-design/pro-components';
|
||||
import { Button, Popconfirm, message, Space } from 'antd';
|
||||
import { siteapicontrollerGetreviews, siteapicontrollerDeletereview } from '@/servers/api/siteApi';
|
||||
import ReviewForm from './ReviewForm';
|
||||
import {
|
||||
siteapicontrollerDeletereview,
|
||||
siteapicontrollerGetreviews,
|
||||
} from '@/servers/api/siteApi';
|
||||
import {
|
||||
ActionType,
|
||||
ProCard,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
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 params = useParams();
|
||||
|
|
@ -31,26 +39,40 @@ const ReviewsPage: React.FC = () => {
|
|||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button type="link" style={{padding:0}} onClick={() => {
|
||||
setEditing(record);
|
||||
setOpen(true);
|
||||
}}>编辑</Button>
|
||||
<Popconfirm 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 {
|
||||
<Button
|
||||
type="link"
|
||||
style={{ padding: 0 }}
|
||||
onClick={() => {
|
||||
setEditing(record);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
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('删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Button type='link' danger>删除</Button>
|
||||
}}
|
||||
>
|
||||
<Button type="link" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
|
|
@ -63,7 +85,12 @@ const ReviewsPage: React.FC = () => {
|
|||
columns={columns}
|
||||
actionRef={actionRef}
|
||||
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 {
|
||||
data: response.data.items,
|
||||
success: true,
|
||||
|
|
@ -76,10 +103,13 @@ const ReviewsPage: React.FC = () => {
|
|||
}}
|
||||
headerTitle="评论列表"
|
||||
toolBarRender={() => [
|
||||
<Button type="primary" onClick={() => {
|
||||
setEditing(null);
|
||||
setOpen(true);
|
||||
}}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setEditing(null);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
新建评论
|
||||
</Button>,
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import {} from '@/servers/api/subscription';
|
||||
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProFormSelect,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useParams } from '@umijs/max';
|
||||
import { App, Button, Drawer, List, Popconfirm, Space, Tag } from 'antd';
|
||||
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { request } from 'umi';
|
||||
|
|
@ -116,8 +113,16 @@ const SubscriptionsPage: React.FC = () => {
|
|||
valueType: 'option',
|
||||
render: (_, row) => (
|
||||
<Space>
|
||||
<Button type="link" title="编辑" icon={<EditOutlined />} onClick={() => setEditing(row)} />
|
||||
<Popconfirm title="确定删除?" onConfirm={() => message.info('订阅删除未实现')}>
|
||||
<Button
|
||||
type="link"
|
||||
title="编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setEditing(row)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定删除?"
|
||||
onConfirm={() => message.info('订阅删除未实现')}
|
||||
>
|
||||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
|
|
@ -142,7 +147,7 @@ const SubscriptionsPage: React.FC = () => {
|
|||
...params,
|
||||
page: params.current,
|
||||
per_page: params.pageSize,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
|
@ -165,15 +170,29 @@ const SubscriptionsPage: React.FC = () => {
|
|||
// 工具栏:订阅同步入口
|
||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
||||
toolBarRender={() => [
|
||||
<Button type="primary" title="新增" icon={<PlusOutlined />} onClick={() => message.info('订阅新增未实现')} />,
|
||||
<Button title="批量编辑" icon={<EditOutlined />} onClick={() => message.info('批量编辑未实现')} />,
|
||||
<Button
|
||||
type="primary"
|
||||
title="新增"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => message.info('订阅新增未实现')}
|
||||
/>,
|
||||
<Button
|
||||
title="批量编辑"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => message.info('批量编辑未实现')}
|
||||
/>,
|
||||
<Button
|
||||
title="批量导出"
|
||||
onClick={async () => {
|
||||
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) {
|
||||
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 a = document.createElement('a');
|
||||
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
|
||||
open={drawerOpen}
|
||||
title={drawerTitle}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
|
||||
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||
import {
|
||||
logisticscontrollerCreateshipment,
|
||||
logisticscontrollerDelshipment,
|
||||
logisticscontrollerGetshipmentfee,
|
||||
logisticscontrollerGetshippingaddresslist,
|
||||
} from '@/servers/api/logistics';
|
||||
import {
|
||||
productcontrollerSearchproducts,
|
||||
} from '@/servers/api/product';
|
||||
import { productcontrollerSearchproducts } from '@/servers/api/product';
|
||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||
import {
|
||||
CodeSandboxOutlined,
|
||||
CopyOutlined,
|
||||
DeleteFilled,
|
||||
EditOutlined,
|
||||
FileDoneOutlined,
|
||||
PlusOutlined,
|
||||
TagsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
|
@ -25,36 +16,20 @@ import {
|
|||
DrawerForm,
|
||||
ModalForm,
|
||||
ProColumns,
|
||||
ProDescriptions,
|
||||
ProForm,
|
||||
ProFormDatePicker,
|
||||
ProFormDependency,
|
||||
ProFormDigit,
|
||||
ProFormInstance,
|
||||
ProFormItem,
|
||||
ProFormList,
|
||||
ProFormRadio,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Empty,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Row,
|
||||
} from 'antd';
|
||||
import { App, Button, Col, Divider, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import RelatedOrders from '@/pages/Subscription/Orders/RelatedOrders';
|
||||
|
||||
const region = {
|
||||
AB: 'Alberta',
|
||||
|
|
@ -88,22 +63,26 @@ export const OrderNote: React.FC<{
|
|||
}
|
||||
onFinish={async (values: any) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Use new API for creating note
|
||||
const { success, data } = await request(`/site-api/${siteId}/orders/${id}/notes`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
...values,
|
||||
orderId: id, // API might not need this in body if in URL, but keeping for compatibility if adapter needs it
|
||||
}
|
||||
});
|
||||
const { success, data } = await request(
|
||||
`/site-api/${siteId}/orders/${id}/notes`,
|
||||
{
|
||||
method: 'POST',
|
||||
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
|
||||
if (success === false) { // Assuming response.util returns success: boolean
|
||||
throw new Error('提交失败');
|
||||
if (success === false) {
|
||||
// Assuming response.util returns success: boolean
|
||||
throw new Error('提交失败');
|
||||
}
|
||||
|
||||
descRef?.current?.reload();
|
||||
|
|
@ -255,7 +234,9 @@ export const Shipping: React.FC<{
|
|||
request={async () => {
|
||||
if (!siteId) return {};
|
||||
// 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 {};
|
||||
|
||||
|
|
@ -263,18 +244,15 @@ export const Shipping: React.FC<{
|
|||
const sales = data.sales || [];
|
||||
|
||||
// Logic for merging duplicate products
|
||||
const mergedSales = sales.reduce(
|
||||
(acc: any[], cur: any) => {
|
||||
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||
if (idx === -1) {
|
||||
acc.push({ ...cur }); // clone
|
||||
} else {
|
||||
acc[idx].quantity += cur.quantity;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const mergedSales = sales.reduce((acc: any[], cur: any) => {
|
||||
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||
if (idx === -1) {
|
||||
acc.push({ ...cur }); // clone
|
||||
} else {
|
||||
acc[idx].quantity += cur.quantity;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Update data.sales
|
||||
data.sales = mergedSales;
|
||||
|
|
@ -450,10 +428,7 @@ export const Shipping: React.FC<{
|
|||
</ProFormList>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<ProFormList
|
||||
label="发货产品"
|
||||
name="sales"
|
||||
>
|
||||
<ProFormList label="发货产品" name="sales">
|
||||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
params={{ options }}
|
||||
|
|
@ -548,11 +523,11 @@ export const Shipping: React.FC<{
|
|||
name={['details', 'origin', 'name']}
|
||||
rules={[{ required: true, message: '请输入公司名称' }]}
|
||||
/>
|
||||
{/* Simplified for brevity - assume standard fields remain */}
|
||||
{/* Simplified for brevity - assume standard fields remain */}
|
||||
</ProForm.Group>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* ... Packaging fields ... */}
|
||||
{/* ... Packaging fields ... */}
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
|
@ -588,20 +563,23 @@ export const CreateOrder: React.FC<{
|
|||
}}
|
||||
onFinish={async ({ items, details, ...data }) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Use site-api to create order
|
||||
const { success, message: errMsg } = await request(`/site-api/${siteId}/orders`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
...data,
|
||||
customer_email: data?.billing?.email,
|
||||
billing_phone: data?.billing?.phone,
|
||||
// map other fields if needed for Adapter
|
||||
}
|
||||
});
|
||||
const { success, message: errMsg } = await request(
|
||||
`/site-api/${siteId}/orders`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: {
|
||||
...data,
|
||||
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
|
||||
|
||||
|
|
@ -621,7 +599,7 @@ export const CreateOrder: React.FC<{
|
|||
}}
|
||||
>
|
||||
{/* ... Form fields ... same as before */}
|
||||
<ProFormDigit
|
||||
<ProFormDigit
|
||||
label="金额"
|
||||
name="total"
|
||||
rules={[{ required: true, message: '请输入金额' }]}
|
||||
|
|
@ -654,22 +632,27 @@ export const BatchEditOrders: React.FC<{
|
|||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
let ok = 0, fail = 0;
|
||||
let ok = 0,
|
||||
fail = 0;
|
||||
for (const id of selectedRowKeys) {
|
||||
try {
|
||||
// Remove undefined values
|
||||
const data = Object.fromEntries(Object.entries(values).filter(([_, v]) => v !== undefined && v !== ''));
|
||||
if (Object.keys(data).length === 0) continue;
|
||||
try {
|
||||
// Remove undefined values
|
||||
const data = Object.fromEntries(
|
||||
Object.entries(values).filter(
|
||||
([_, v]) => v !== undefined && v !== '',
|
||||
),
|
||||
);
|
||||
if (Object.keys(data).length === 0) continue;
|
||||
|
||||
const res = await request(`/site-api/${siteId}/orders/${id}`, {
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
});
|
||||
if (res.success) ok++;
|
||||
else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
const res = await request(`/site-api/${siteId}/orders/${id}`, {
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
});
|
||||
if (res.success) ok++;
|
||||
else fail++;
|
||||
} catch (e) {
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
message.success(`成功 ${ok}, 失败 ${fail}`);
|
||||
tableRef.current?.reload();
|
||||
|
|
@ -699,126 +682,133 @@ export const EditOrder: React.FC<{
|
|||
|
||||
return (
|
||||
<DrawerForm
|
||||
formRef={formRef}
|
||||
title="编辑订单"
|
||||
trigger={
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
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={() => []}
|
||||
formRef={formRef}
|
||||
title="编辑订单"
|
||||
trigger={
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setActiveLine(record.id)}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="name" label="商品名" />
|
||||
<ProFormText name="sku" label="SKU" />
|
||||
<ProFormDigit name="quantity" label="数量" />
|
||||
<ProFormText name="total" label="总价" />
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
编辑
|
||||
</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 {};
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
import { PRODUCT_STATUS_ENUM, PRODUCT_STOCK_STATUS_ENUM } from '@/constants';
|
||||
import {
|
||||
productcontrollerProductbysku,
|
||||
productcontrollerSearchproducts,
|
||||
} from '@/servers/api/product';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { EditOutlined, PlusOutlined, DeleteFilled } from '@ant-design/icons';
|
||||
import { DeleteFilled, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
|
|
@ -14,7 +9,6 @@ import {
|
|||
ProFormList,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProFormUploadButton,
|
||||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
|
|
@ -33,15 +27,19 @@ export const CreateProduct: React.FC<{
|
|||
<DrawerForm
|
||||
title="新增产品"
|
||||
form={form}
|
||||
trigger={<Button type="primary" title="新增产品" icon={<PlusOutlined />}>新增产品</Button>}
|
||||
trigger={
|
||||
<Button type="primary" title="新增产品" icon={<PlusOutlined />}>
|
||||
新增产品
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
drawerProps={{
|
||||
destroyOnHidden: true,
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 将数字字段转换为字符串以匹配DTO
|
||||
|
|
@ -50,7 +48,10 @@ export const CreateProduct: React.FC<{
|
|||
type: values.type || 'simple',
|
||||
regular_price: values.regular_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`, {
|
||||
|
|
@ -87,16 +88,8 @@ export const CreateProduct: React.FC<{
|
|||
width="lg"
|
||||
rules={[{ required: true, message: '请输入SKU' }]}
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="描述"
|
||||
width="lg"
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="short_description"
|
||||
label="简短描述"
|
||||
width="lg"
|
||||
/>
|
||||
<ProFormTextArea name="description" label="描述" width="lg" />
|
||||
<ProFormTextArea name="short_description" label="简短描述" width="lg" />
|
||||
<ProFormDigit
|
||||
name="regular_price"
|
||||
label="常规价格"
|
||||
|
|
@ -116,17 +109,17 @@ export const CreateProduct: React.FC<{
|
|||
fieldProps={{ precision: 0 }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
initialValue="publish"
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
initialValue="publish"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<Divider />
|
||||
|
|
@ -135,12 +128,12 @@ export const CreateProduct: React.FC<{
|
|||
label="产品图片"
|
||||
initialValue={[{}]}
|
||||
creatorButtonProps={{
|
||||
creatorButtonText: '添加图片',
|
||||
creatorButtonText: '添加图片',
|
||||
}}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="src" label="图片URL" width="lg" />
|
||||
<ProFormText name="alt" label="替代文本" width="md" />
|
||||
<ProFormText name="src" label="图片URL" width="lg" />
|
||||
<ProFormText name="alt" label="替代文本" width="md" />
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
<Divider />
|
||||
|
|
@ -149,38 +142,38 @@ export const CreateProduct: React.FC<{
|
|||
label="产品属性"
|
||||
initialValue={[]}
|
||||
creatorButtonProps={{
|
||||
creatorButtonText: '添加属性',
|
||||
creatorButtonText: '添加属性',
|
||||
}}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="name" label="属性名称" width="md" />
|
||||
<ProFormSelect
|
||||
name="options"
|
||||
label="选项"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入选项并回车"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="visible"
|
||||
label="可见性"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '可见', value: true },
|
||||
{ label: '隐藏', value: false },
|
||||
]}
|
||||
initialValue={true}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="variation"
|
||||
label="用于变体"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]}
|
||||
initialValue={false}
|
||||
/>
|
||||
<ProFormText name="name" label="属性名称" width="md" />
|
||||
<ProFormSelect
|
||||
name="options"
|
||||
label="选项"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入选项并回车"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="visible"
|
||||
label="可见性"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '可见', value: true },
|
||||
{ label: '隐藏', value: false },
|
||||
]}
|
||||
initialValue={true}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="variation"
|
||||
label="用于变体"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]}
|
||||
initialValue={false}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
</DrawerForm>
|
||||
|
|
@ -197,7 +190,9 @@ export const UpdateStatus: React.FC<{
|
|||
// 转换初始值,将字符串价格转换为数字以便编辑
|
||||
const formValues = {
|
||||
...initialValues,
|
||||
stock_quantity: initialValues.stock_quantity ? parseInt(initialValues.stock_quantity) : 0,
|
||||
stock_quantity: initialValues.stock_quantity
|
||||
? parseInt(initialValues.stock_quantity)
|
||||
: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -209,7 +204,9 @@ export const UpdateStatus: React.FC<{
|
|||
title="修改产品状态"
|
||||
initialValues={formValues}
|
||||
trigger={
|
||||
<Button type="link" title="修改状态" icon={<EditOutlined />}>修改状态</Button>
|
||||
<Button type="link" title="修改状态" icon={<EditOutlined />}>
|
||||
修改状态
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
drawerProps={{
|
||||
|
|
@ -217,17 +214,17 @@ export const UpdateStatus: React.FC<{
|
|||
}}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await request(`/site-api/${siteId}/products/${initialValues.id}`, {
|
||||
method: 'PUT',
|
||||
data: {
|
||||
status: values.status,
|
||||
stock_status: values.stock_status,
|
||||
stock_quantity: values.stock_quantity,
|
||||
}
|
||||
status: values.status,
|
||||
stock_status: values.stock_status,
|
||||
stock_quantity: values.stock_quantity,
|
||||
},
|
||||
});
|
||||
message.success('状态更新成功');
|
||||
tableRef.current?.reload();
|
||||
|
|
@ -276,8 +273,12 @@ export const UpdateForm: React.FC<{
|
|||
...initialValues,
|
||||
categories: initialValues.categories?.map((c: any) => c.name) || [],
|
||||
tags: initialValues.tags?.map((t: any) => t.name) || [],
|
||||
regular_price: initialValues.regular_price ? parseFloat(initialValues.regular_price) : 0,
|
||||
sale_price: initialValues.sale_price ? parseFloat(initialValues.sale_price) : 0,
|
||||
regular_price: initialValues.regular_price
|
||||
? parseFloat(initialValues.regular_price)
|
||||
: 0,
|
||||
sale_price: initialValues.sale_price
|
||||
? parseFloat(initialValues.sale_price)
|
||||
: 0,
|
||||
};
|
||||
|
||||
const handleAutoGenerateTags = () => {
|
||||
|
|
@ -289,7 +290,7 @@ export const UpdateForm: React.FC<{
|
|||
const name = initialValues.name || '';
|
||||
|
||||
const generatedTagsString = computeTags(name, sku, config);
|
||||
const generatedTags = generatedTagsString.split(', ').filter(t => t);
|
||||
const generatedTags = generatedTagsString.split(', ').filter((t) => t);
|
||||
|
||||
if (generatedTags.length > 0) {
|
||||
const currentTags = form.getFieldValue('tags') || [];
|
||||
|
|
@ -307,7 +308,9 @@ export const UpdateForm: React.FC<{
|
|||
form={form}
|
||||
initialValues={formValues}
|
||||
trigger={
|
||||
<Button type="link" title="编辑详情" icon={<EditOutlined />}>编辑详情</Button>
|
||||
<Button type="link" title="编辑详情" icon={<EditOutlined />}>
|
||||
编辑详情
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
drawerProps={{
|
||||
|
|
@ -315,8 +318,8 @@ export const UpdateForm: React.FC<{
|
|||
}}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 将数字字段转换为字符串以匹配DTO
|
||||
|
|
@ -324,12 +327,15 @@ export const UpdateForm: React.FC<{
|
|||
...values,
|
||||
regular_price: values.regular_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}`, {
|
||||
method: 'PUT',
|
||||
data: updateData
|
||||
data: updateData,
|
||||
});
|
||||
message.success('提交成功');
|
||||
tableRef.current?.reload();
|
||||
|
|
@ -355,16 +361,8 @@ export const UpdateForm: React.FC<{
|
|||
placeholder="请输入SKU"
|
||||
rules={[{ required: true, message: '请输入SKU' }]}
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="short_description"
|
||||
label="简短描述"
|
||||
width="lg"
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="描述"
|
||||
width="lg"
|
||||
/>
|
||||
<ProFormTextArea name="short_description" label="简短描述" width="lg" />
|
||||
<ProFormTextArea name="description" label="描述" width="lg" />
|
||||
|
||||
{initialValues.type === 'simple' ? (
|
||||
<>
|
||||
|
|
@ -391,20 +389,20 @@ export const UpdateForm: React.FC<{
|
|||
fieldProps={{ precision: 0 }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<ProFormSelect
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="md"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="categories"
|
||||
|
|
@ -432,12 +430,12 @@ export const UpdateForm: React.FC<{
|
|||
label="产品图片"
|
||||
initialValue={initialValues.images || [{}]}
|
||||
creatorButtonProps={{
|
||||
creatorButtonText: '添加图片',
|
||||
creatorButtonText: '添加图片',
|
||||
}}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="src" label="图片URL" width="lg" />
|
||||
<ProFormText name="alt" label="替代文本" width="md" />
|
||||
<ProFormText name="src" label="图片URL" width="lg" />
|
||||
<ProFormText name="alt" label="替代文本" width="md" />
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
<Divider />
|
||||
|
|
@ -446,38 +444,38 @@ export const UpdateForm: React.FC<{
|
|||
label="产品属性"
|
||||
initialValue={initialValues.attributes || []}
|
||||
creatorButtonProps={{
|
||||
creatorButtonText: '添加属性',
|
||||
creatorButtonText: '添加属性',
|
||||
}}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="name" label="属性名称" width="md" />
|
||||
<ProFormSelect
|
||||
name="options"
|
||||
label="选项"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入选项并回车"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="visible"
|
||||
label="可见性"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '可见', value: true },
|
||||
{ label: '隐藏', value: false },
|
||||
]}
|
||||
initialValue={true}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="variation"
|
||||
label="用于变体"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]}
|
||||
initialValue={false}
|
||||
/>
|
||||
<ProFormText name="name" label="属性名称" width="md" />
|
||||
<ProFormSelect
|
||||
name="options"
|
||||
label="选项"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入选项并回车"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="visible"
|
||||
label="可见性"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '可见', value: true },
|
||||
{ label: '隐藏', value: false },
|
||||
]}
|
||||
initialValue={true}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="variation"
|
||||
label="用于变体"
|
||||
width="xs"
|
||||
options={[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]}
|
||||
initialValue={false}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
</DrawerForm>
|
||||
|
|
@ -489,15 +487,26 @@ export const UpdateVaritation: React.FC<{
|
|||
values: any;
|
||||
siteId?: string;
|
||||
productId?: string | number;
|
||||
}> = ({ tableRef, values: initialValues, siteId, productId: propProductId }) => {
|
||||
}> = ({
|
||||
tableRef,
|
||||
values: initialValues,
|
||||
siteId,
|
||||
productId: propProductId,
|
||||
}) => {
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 转换初始值,将字符串价格转换为数字以便编辑
|
||||
const formValues = {
|
||||
...initialValues,
|
||||
regular_price: initialValues.regular_price ? parseFloat(initialValues.regular_price) : 0,
|
||||
sale_price: initialValues.sale_price ? parseFloat(initialValues.sale_price) : 0,
|
||||
stock_quantity: initialValues.stock_quantity ? parseInt(initialValues.stock_quantity) : 0,
|
||||
regular_price: initialValues.regular_price
|
||||
? parseFloat(initialValues.regular_price)
|
||||
: 0,
|
||||
sale_price: initialValues.sale_price
|
||||
? parseFloat(initialValues.sale_price)
|
||||
: 0,
|
||||
stock_quantity: initialValues.stock_quantity
|
||||
? parseInt(initialValues.stock_quantity)
|
||||
: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -505,18 +514,24 @@ export const UpdateVaritation: React.FC<{
|
|||
title="编辑变体"
|
||||
initialValues={formValues}
|
||||
trigger={
|
||||
<Button type="link" title="编辑变体" icon={<EditOutlined />}>编辑变体</Button>
|
||||
<Button type="link" title="编辑变体" icon={<EditOutlined />}>
|
||||
编辑变体
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
drawerProps={{
|
||||
destroyOnHidden: true,
|
||||
}}
|
||||
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) {
|
||||
message.error('缺少站点ID或产品ID');
|
||||
return false;
|
||||
message.error('缺少站点ID或产品ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -525,14 +540,21 @@ export const UpdateVaritation: React.FC<{
|
|||
...values,
|
||||
regular_price: values.regular_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;
|
||||
await request(`/site-api/${siteId}/products/${productId}/variations/${variationId}`, {
|
||||
method: 'PUT',
|
||||
data: variationData
|
||||
});
|
||||
const variationId =
|
||||
initialValues.externalVariationId || initialValues.id;
|
||||
await request(
|
||||
`/site-api/${siteId}/products/${productId}/variations/${variationId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data: variationData,
|
||||
},
|
||||
);
|
||||
message.success('更新变体成功');
|
||||
tableRef.current?.reload();
|
||||
return true;
|
||||
|
|
@ -575,16 +597,16 @@ export const UpdateVaritation: React.FC<{
|
|||
fieldProps={{ precision: 0 }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="lg"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
name="stock_status"
|
||||
label="库存状态"
|
||||
width="lg"
|
||||
valueEnum={PRODUCT_STOCK_STATUS_ENUM}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="lg"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
name="status"
|
||||
label="产品状态"
|
||||
width="lg"
|
||||
valueEnum={PRODUCT_STATUS_ENUM}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</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 update BatchDelete.
|
||||
|
||||
|
||||
export const BatchDeleteProducts: React.FC<{
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
selectedRowKeys: React.Key[];
|
||||
|
|
@ -639,30 +660,56 @@ export const BatchDeleteProducts: React.FC<{
|
|||
};
|
||||
|
||||
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<{
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
selectedRowKeys: React.Key[];
|
||||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
||||
selectedRows: any[];
|
||||
siteId?: string;
|
||||
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, selectedRows, siteId }) => {
|
||||
}> = ({
|
||||
tableRef,
|
||||
selectedRowKeys,
|
||||
setSelectedRowKeys,
|
||||
selectedRows,
|
||||
siteId,
|
||||
}) => {
|
||||
const { message } = App.useApp();
|
||||
return (
|
||||
<ModalForm
|
||||
title="批量编辑产品"
|
||||
trigger={<Button disabled={!selectedRowKeys.length} type="primary" icon={<EditOutlined />}>批量编辑</Button>}
|
||||
trigger={
|
||||
<Button
|
||||
disabled={!selectedRowKeys.length}
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
>
|
||||
批量编辑
|
||||
</Button>
|
||||
}
|
||||
width={600}
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
if (!siteId) return false;
|
||||
const updatePayload = selectedRows.map((row) => ({ id: row.id, ...values }));
|
||||
const updatePayload = selectedRows.map((row) => ({
|
||||
id: row.id,
|
||||
...values,
|
||||
}));
|
||||
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) {
|
||||
message.success('批量编辑成功');
|
||||
tableRef.current?.reload();
|
||||
|
|
@ -678,14 +725,26 @@ export const BatchEditProducts: React.FC<{
|
|||
}}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormSelect name="status" label="产品状态" valueEnum={PRODUCT_STATUS_ENUM} />
|
||||
<ProFormSelect name="stock_status" label="库存状态" valueEnum={PRODUCT_STOCK_STATUS_ENUM} />
|
||||
<ProFormDigit name="stock_quantity" label="库存数量" fieldProps={{ precision: 0 }} />
|
||||
<ProFormSelect
|
||||
name="status"
|
||||
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>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
// Disable for now
|
||||
// Disable for now
|
||||
export const SetComponent: React.FC<any> = () => null; // Disable for now (relies on local productcontrollerProductbysku?)
|
||||
|
||||
export const ImportCsv: React.FC<{
|
||||
|
|
@ -696,7 +755,11 @@ export const ImportCsv: React.FC<{
|
|||
return (
|
||||
<ModalForm
|
||||
title="批量导入产品"
|
||||
trigger={<Button type="primary" ghost icon={<PlusOutlined />}>批量导入</Button>}
|
||||
trigger={
|
||||
<Button type="primary" ghost icon={<PlusOutlined />}>
|
||||
批量导入
|
||||
</Button>
|
||||
}
|
||||
width={600}
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
onFinish={async (values) => {
|
||||
|
|
@ -704,7 +767,10 @@ export const ImportCsv: React.FC<{
|
|||
const csvText = values.csv || '';
|
||||
const itemsList = values.items || [];
|
||||
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) {
|
||||
message.success('导入完成');
|
||||
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={[]}>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="name" label="名称" />
|
||||
<ProFormText name="sku" label="SKU" />
|
||||
<ProFormDigit name="regular_price" label="常规价" fieldProps={{ precision: 2 }} />
|
||||
<ProFormDigit name="sale_price" label="促销价" fieldProps={{ precision: 2 }} />
|
||||
<ProFormDigit
|
||||
name="regular_price"
|
||||
label="常规价"
|
||||
fieldProps={{ precision: 2 }}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="sale_price"
|
||||
label="促销价"
|
||||
fieldProps={{ precision: 2 }}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
// Disable for now
|
||||
// Disable for now
|
||||
|
|
|
|||
|
|
@ -78,9 +78,10 @@ export const classifyExtraTags = (
|
|||
const fLower = flavorPart.toLowerCase();
|
||||
const isFruit =
|
||||
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 =
|
||||
mints.some((key) => fLower.includes(key.toLowerCase())) || tokens.includes('mint');
|
||||
mints.some((key) => fLower.includes(key.toLowerCase())) ||
|
||||
tokens.includes('mint');
|
||||
|
||||
const extras: string[] = [];
|
||||
if (isFruit) extras.push('Fruit');
|
||||
|
|
@ -106,14 +107,18 @@ export const matchAttributes = (text: string, keys: string[]): string[] => {
|
|||
/**
|
||||
* @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 tokens = splitFlavorTokens(flavorPart);
|
||||
|
||||
const flavorKeysLower = config.flavors.map(k => k.toLowerCase());
|
||||
const flavorKeysLower = config.flavors.map((k) => k.toLowerCase());
|
||||
|
||||
const tokensForFlavor = tokens.filter(
|
||||
(t) => flavorKeysLower.includes(t.toLowerCase()),
|
||||
const tokensForFlavor = tokens.filter((t) =>
|
||||
flavorKeysLower.includes(t.toLowerCase()),
|
||||
);
|
||||
|
||||
const flavorTag = tokensForFlavor
|
||||
|
|
@ -125,7 +130,9 @@ export const computeTags = (name: string, sku: string, config: TagConfig): strin
|
|||
if (flavorTag) tags.push(flavorTag);
|
||||
|
||||
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') {
|
||||
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(
|
||||
...classifyExtraTags(flavorPart, config.fruits, config.mints),
|
||||
);
|
||||
tags.push(...classifyExtraTags(flavorPart, config.fruits, config.mints));
|
||||
|
||||
const seen = new Set<string>();
|
||||
const finalTags = tags.filter((t) => {
|
||||
|
|
|
|||
|
|
@ -1,53 +1,50 @@
|
|||
{
|
||||
admin_id: 0,
|
||||
admin_name: "",
|
||||
birthday: 0,
|
||||
contact: "",
|
||||
country_id: 14,
|
||||
created_at: 1765351077,
|
||||
domain: "auspouches.com",
|
||||
email: "daniel.waring81@gmail.com",
|
||||
first_name: "Dan",
|
||||
first_pay_at: 1765351308,
|
||||
gender: 0,
|
||||
id: 44898147,
|
||||
ip: "1.146.111.163",
|
||||
is_cart: 0,
|
||||
is_event_sub: 1,
|
||||
is_sub: 1,
|
||||
is_verified: 1,
|
||||
last_name: "Waring",
|
||||
last_order_id: 236122,
|
||||
login_at: 1765351340,
|
||||
note: "",
|
||||
order_at: 1765351224,
|
||||
orders_count: 1,
|
||||
pay_at: 1765351308,
|
||||
source_device: "phone",
|
||||
tags: [
|
||||
],
|
||||
total_spent: "203.81",
|
||||
updated_at: 1765351515,
|
||||
utm_medium: "referral",
|
||||
utm_source: "checkout.cartadicreditopay.com",
|
||||
visit_at: 1765351513,
|
||||
country: {
|
||||
chinese_name: "澳大利亚",
|
||||
country_code2: "AU",
|
||||
country_name: "Australia",
|
||||
"admin_id": 0,
|
||||
"admin_name": "",
|
||||
"birthday": 0,
|
||||
"contact": "",
|
||||
"country_id": 14,
|
||||
"created_at": 1765351077,
|
||||
"domain": "auspouches.com",
|
||||
"email": "daniel.waring81@gmail.com",
|
||||
"first_name": "Dan",
|
||||
"first_pay_at": 1765351308,
|
||||
"gender": 0,
|
||||
"id": 44898147,
|
||||
"ip": "1.146.111.163",
|
||||
"is_cart": 0,
|
||||
"is_event_sub": 1,
|
||||
"is_sub": 1,
|
||||
"is_verified": 1,
|
||||
"last_name": "Waring",
|
||||
"last_order_id": 236122,
|
||||
"login_at": 1765351340,
|
||||
"note": "",
|
||||
"order_at": 1765351224,
|
||||
"orders_count": 1,
|
||||
"pay_at": 1765351308,
|
||||
"source_device": "phone",
|
||||
"tags": [],
|
||||
"total_spent": "203.81",
|
||||
"updated_at": 1765351515,
|
||||
"utm_medium": "referral",
|
||||
"utm_source": "checkout.cartadicreditopay.com",
|
||||
"visit_at": 1765351513,
|
||||
"country": {
|
||||
"chinese_name": "澳大利亚",
|
||||
"country_code2": "AU",
|
||||
"country_name": "Australia"
|
||||
},
|
||||
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",
|
||||
timezone: "Etc/GMT-10",
|
||||
os: "Android",
|
||||
browser: "Pixel 8",
|
||||
language: "en-GB",
|
||||
screen_size: "528X1174",
|
||||
viewport_size: "527X1026",
|
||||
ip: "1.146.111.163",
|
||||
"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",
|
||||
"timezone": "Etc/GMT-10",
|
||||
"os": "Android",
|
||||
"browser": "Pixel 8",
|
||||
"language": "en-GB",
|
||||
"screen_size": "528X1174",
|
||||
"viewport_size": "527X1026",
|
||||
"ip": "1.146.111.163"
|
||||
},
|
||||
default_address: [
|
||||
],
|
||||
addresses: [
|
||||
],
|
||||
"default_address": [],
|
||||
"addresses": []
|
||||
}
|
||||
|
|
@ -128,11 +128,7 @@ const ListPage: React.FC = () => {
|
|||
const stockRow = points.map(
|
||||
(p) => stockMap.get(p.id || 0) || 0,
|
||||
);
|
||||
return [
|
||||
item.productName || '',
|
||||
item.sku || '',
|
||||
...stockRow,
|
||||
];
|
||||
return [item.productName || '', item.sku || '', ...stockRow];
|
||||
});
|
||||
|
||||
// 导出
|
||||
|
|
|
|||
|
|
@ -564,15 +564,13 @@ const DetailForm: React.FC<{
|
|||
const [form] = Form.useForm();
|
||||
const initialValues = {
|
||||
...values,
|
||||
items: values?.items?.map(
|
||||
(item: { productName: string; sku: string }) => ({
|
||||
...item,
|
||||
sku: {
|
||||
label: item.productName,
|
||||
value: item.sku,
|
||||
},
|
||||
}),
|
||||
),
|
||||
items: values?.items?.map((item: { productName: string; sku: string }) => ({
|
||||
...item,
|
||||
sku: {
|
||||
label: item.productName,
|
||||
value: item.sku,
|
||||
},
|
||||
})),
|
||||
};
|
||||
return (
|
||||
<DrawerForm
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@ const UpdateForm: React.FC<{
|
|||
title="编辑"
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
areas: ((initialValues as any).areas?.map((area: any) => area.code) ?? []),
|
||||
areas:
|
||||
(initialValues as any).areas?.map((area: any) => area.code) ?? [],
|
||||
}}
|
||||
trigger={
|
||||
<Button type="primary">
|
||||
|
|
|
|||
|
|
@ -57,13 +57,13 @@ const TestModal: React.FC<{
|
|||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const res = await templatecontrollerRendertemplate(
|
||||
{ name: template.name || '' },
|
||||
inputData
|
||||
{ name: template.name || '' },
|
||||
inputData,
|
||||
);
|
||||
if (res.success) {
|
||||
setRenderedResult(res.data as unknown as string);
|
||||
setRenderedResult(res.data as unknown as string);
|
||||
} else {
|
||||
setRenderedResult(`Error: ${res.message}`);
|
||||
setRenderedResult(`Error: ${res.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setRenderedResult(`Error: ${error.message}`);
|
||||
|
|
@ -88,9 +88,15 @@ const TestModal: React.FC<{
|
|||
<Card bodyStyle={{ padding: 0, height: '300px', overflow: 'auto' }}>
|
||||
<ReactJson
|
||||
src={inputData}
|
||||
onEdit={(edit) => setInputData(edit.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>)}
|
||||
onEdit={(edit) =>
|
||||
setInputData(edit.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}
|
||||
displayDataTypes={false}
|
||||
/>
|
||||
|
|
@ -98,8 +104,17 @@ const TestModal: React.FC<{
|
|||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography.Title level={5}>渲染结果</Typography.Title>
|
||||
<Card bodyStyle={{ padding: '16px', height: '300px', overflow: 'auto', backgroundColor: '#f5f5f5' }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{renderedResult}</pre>
|
||||
<Card
|
||||
bodyStyle={{
|
||||
padding: '16px',
|
||||
height: '300px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
||||
{renderedResult}
|
||||
</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -111,7 +126,9 @@ const List: React.FC = () => {
|
|||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
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>[] = [
|
||||
{
|
||||
|
|
@ -245,7 +262,6 @@ const CreateForm: React.FC<{
|
|||
>
|
||||
<ProFormText
|
||||
name="name"
|
||||
|
||||
label="模板名称"
|
||||
placeholder="请输入名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
|
|
@ -323,13 +339,13 @@ const UpdateForm: React.FC<{
|
|||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="name"
|
||||
label="模板名称"
|
||||
placeholder="请输入名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
/>
|
||||
<ProForm.Item
|
||||
<ProFormText
|
||||
name="name"
|
||||
label="模板名称"
|
||||
placeholder="请输入名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
/>
|
||||
<ProForm.Item
|
||||
name="value"
|
||||
label="值"
|
||||
rules={[{ required: true, message: '请输入值' }]}
|
||||
|
|
@ -345,24 +361,24 @@ const UpdateForm: React.FC<{
|
|||
}}
|
||||
/>
|
||||
</ProForm.Item>
|
||||
<ProFormTextArea
|
||||
name="testData"
|
||||
label="测试数据 (JSON)"
|
||||
placeholder="请输入JSON格式的测试数据"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('请输入有效的JSON格式'));
|
||||
}
|
||||
},
|
||||
<ProFormTextArea
|
||||
name="testData"
|
||||
label="测试数据 (JSON)"
|
||||
placeholder="请输入JSON格式的测试数据"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!value) return Promise.resolve();
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('请输入有效的JSON格式'));
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DrawerForm>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,11 +4,10 @@ import {
|
|||
ProForm,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { Button, Card, Col, Input, message, Row, Upload } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { request } from '@umijs/max';
|
||||
import { attributes } from '../../../Product/Attribute/consts';
|
||||
|
||||
// 定义配置接口
|
||||
interface TagConfig {
|
||||
|
|
@ -95,9 +94,10 @@ const classifyExtraTags = (
|
|||
const fLower = flavorPart.toLowerCase();
|
||||
const isFruit =
|
||||
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 =
|
||||
mintKeys.some((key) => fLower.includes(key.toLowerCase())) || tokens.includes('mint');
|
||||
mintKeys.some((key) => fLower.includes(key.toLowerCase())) ||
|
||||
tokens.includes('mint');
|
||||
|
||||
const extras: string[] = [];
|
||||
if (isFruit) extras.push('Fruit');
|
||||
|
|
@ -131,10 +131,10 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
|
||||
// 白名单模式:只保留在 flavorKeys 中的 token
|
||||
// 且对比时忽略大小写
|
||||
const flavorKeysLower = config.flavorKeys.map(k => k.toLowerCase());
|
||||
const flavorKeysLower = config.flavorKeys.map((k) => k.toLowerCase());
|
||||
|
||||
const tokensForFlavor = tokens.filter(
|
||||
(t) => flavorKeysLower.includes(t.toLowerCase()),
|
||||
const tokensForFlavor = tokens.filter((t) =>
|
||||
flavorKeysLower.includes(t.toLowerCase()),
|
||||
);
|
||||
|
||||
// 将匹配到的 token 转为首字母大写
|
||||
|
|
@ -149,12 +149,14 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
// 添加额外的口味描述词
|
||||
for (const t of tokensForFlavor) {
|
||||
// 检查是否在 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') {
|
||||
tags.push(t.charAt(0).toUpperCase() + t.slice(1));
|
||||
tags.push(t.charAt(0).toUpperCase() + t.slice(1));
|
||||
}
|
||||
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 [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
|
||||
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
|
||||
const [config, setConfig] = useState<TagConfig>({ // 动态配置
|
||||
const [config, setConfig] = useState<TagConfig>({
|
||||
// 动态配置
|
||||
brands: [],
|
||||
fruitKeys: [],
|
||||
mintKeys: [],
|
||||
|
|
@ -244,14 +247,25 @@ const WpToolPage: React.FC = () => {
|
|||
console.warn(`Dictionary ${dictName} not found`);
|
||||
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);
|
||||
};
|
||||
|
||||
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('fruit'), // 假设字典名为 fruit
|
||||
getItems('mint'), // 假设字典名为 mint
|
||||
getItems('mint'), // 假设字典名为 mint
|
||||
getItems('flavor'), // 假设字典名为 flavor
|
||||
getItems('strength'),
|
||||
getItems('size'),
|
||||
|
|
@ -267,7 +281,7 @@ const WpToolPage: React.FC = () => {
|
|||
strengthKeys,
|
||||
sizeKeys,
|
||||
humidityKeys,
|
||||
categoryKeys
|
||||
categoryKeys,
|
||||
};
|
||||
setConfig(newConfig);
|
||||
form.setFieldsValue(newConfig);
|
||||
|
|
@ -372,7 +386,16 @@ const WpToolPage: React.FC = () => {
|
|||
// 获取表单中的最新配置
|
||||
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);
|
||||
|
|
@ -406,7 +429,6 @@ const WpToolPage: React.FC = () => {
|
|||
|
||||
// 自动下载
|
||||
downloadData(dataWithTags);
|
||||
|
||||
} catch (error) {
|
||||
message.error({
|
||||
content: '处理失败,请检查配置或文件.',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import * as logistics from './logistics';
|
|||
import * as media from './media';
|
||||
import * as order from './order';
|
||||
import * as product from './product';
|
||||
import * as review from './review';
|
||||
import * as site from './site';
|
||||
import * as siteApi from './siteApi';
|
||||
import * as statistics from './statistics';
|
||||
|
|
@ -31,7 +30,6 @@ export default {
|
|||
media,
|
||||
order,
|
||||
product,
|
||||
review,
|
||||
siteApi,
|
||||
site,
|
||||
statistics,
|
||||
|
|
|
|||
|
|
@ -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} */
|
||||
export async function productcontrollerUpdateproduct(
|
||||
// 叠加生成的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 */
|
||||
export async function productcontrollerCompatsize(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -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 || {}),
|
||||
});
|
||||
}
|
||||
|
|
@ -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 */
|
||||
export async function siteapicontrollerExportmedia(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -185,22 +185,16 @@ declare namespace API {
|
|||
};
|
||||
|
||||
type CreateReviewDTO = {
|
||||
/** 站点ID */
|
||||
siteId?: number;
|
||||
/** 产品ID */
|
||||
productId?: number;
|
||||
/** 客户ID */
|
||||
customerId?: number;
|
||||
/** 评论者姓名 */
|
||||
author?: string;
|
||||
/** 评论者电子邮件 */
|
||||
email?: string;
|
||||
product_id?: Record<string, any>;
|
||||
/** 评论内容 */
|
||||
content?: string;
|
||||
/** 评分 (1-5) */
|
||||
review?: string;
|
||||
/** 评论者 */
|
||||
reviewer?: string;
|
||||
/** 评论者邮箱 */
|
||||
reviewer_email?: string;
|
||||
/** 评分 */
|
||||
rating?: number;
|
||||
/** 状态 */
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
};
|
||||
|
||||
type CreateSiteDTO = {
|
||||
|
|
@ -834,18 +828,18 @@ declare namespace API {
|
|||
type Product = {
|
||||
/** ID */
|
||||
id: number;
|
||||
/** sku */
|
||||
sku?: string;
|
||||
/** 类型 */
|
||||
type?: string;
|
||||
/** 产品名称 */
|
||||
name: string;
|
||||
/** 产品中文名称 */
|
||||
nameCn?: string;
|
||||
/** 产品描述 */
|
||||
description?: string;
|
||||
/** 产品简短描述 */
|
||||
shortDescription?: string;
|
||||
/** sku */
|
||||
sku?: string;
|
||||
/** 产品描述 */
|
||||
description?: string;
|
||||
/** 价格 */
|
||||
price?: number;
|
||||
/** 促销价格 */
|
||||
|
|
@ -961,6 +955,14 @@ declare namespace API {
|
|||
id: number;
|
||||
};
|
||||
|
||||
type productcontrollerGetproductbyidParams = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type productcontrollerGetproductbysiteskuParams = {
|
||||
siteSku: string;
|
||||
};
|
||||
|
||||
type productcontrollerGetproductcomponentsParams = {
|
||||
id: number;
|
||||
};
|
||||
|
|
@ -1047,7 +1049,7 @@ declare namespace API {
|
|||
|
||||
type ProductSiteSku = {
|
||||
/** 站点 SKU */
|
||||
code?: string;
|
||||
siteSku?: string;
|
||||
};
|
||||
|
||||
type ProductsRes = {
|
||||
|
|
@ -1230,19 +1232,6 @@ declare namespace API {
|
|||
stockPointId?: number;
|
||||
};
|
||||
|
||||
type QueryReviewDTO = {
|
||||
/** 当前页码 */
|
||||
current?: number;
|
||||
/** 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 站点ID */
|
||||
siteId?: number;
|
||||
/** 产品ID */
|
||||
productId?: number;
|
||||
/** 状态 */
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type QueryServiceDTO = {
|
||||
/** 页码 */
|
||||
current?: number;
|
||||
|
|
@ -1348,31 +1337,6 @@ declare namespace API {
|
|||
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 = {
|
||||
id?: string;
|
||||
carrier_name?: string;
|
||||
|
|
@ -1486,6 +1450,10 @@ declare namespace API {
|
|||
siteId: number;
|
||||
};
|
||||
|
||||
type siteapicontrollerCreatemediaParams = {
|
||||
siteId: number;
|
||||
};
|
||||
|
||||
type siteapicontrollerCreateordernoteParams = {
|
||||
id: string;
|
||||
siteId: number;
|
||||
|
|
@ -2537,9 +2505,11 @@ declare namespace API {
|
|||
/** 更新时间 */
|
||||
date_modified?: string;
|
||||
/** 产品链接 */
|
||||
frontendUrl?: string;
|
||||
permalink?: string;
|
||||
/** 原始数据(保留备用) */
|
||||
raw?: Record<string, any>;
|
||||
/** ERP产品信息 */
|
||||
erpProduct?: Record<string, any>;
|
||||
};
|
||||
|
||||
type UnifiedProductPaginationDTO = {
|
||||
|
|
@ -2728,11 +2698,11 @@ declare namespace API {
|
|||
|
||||
type UpdateReviewDTO = {
|
||||
/** 评论内容 */
|
||||
content?: string;
|
||||
/** 评分 (1-5) */
|
||||
review?: string;
|
||||
/** 评分 */
|
||||
rating?: number;
|
||||
/** 状态 */
|
||||
status?: 'pending' | 'approved' | 'rejected';
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type UpdateSiteDTO = {
|
||||
|
|
@ -2740,6 +2710,8 @@ declare namespace API {
|
|||
areas?: any;
|
||||
/** 绑定仓库ID列表 */
|
||||
stockPointIds?: any;
|
||||
/** 站点网站URL */
|
||||
websiteUrl?: string;
|
||||
};
|
||||
|
||||
type UpdateStockDTO = {
|
||||
|
|
@ -2805,6 +2777,13 @@ declare namespace API {
|
|||
siteId?: number;
|
||||
};
|
||||
|
||||
type UploadMediaDTO = {
|
||||
/** Base64 编码的文件内容 */
|
||||
file?: string;
|
||||
/** 文件名 */
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
type usercontrollerUpdateuserParams = {
|
||||
id?: number;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue