feat: 添加产品工具, 重构产品 #31
|
|
@ -15,7 +15,7 @@ import { usercontrollerGetuser } from './servers/api/user';
|
|||
// 设置 dayjs 全局语言为中文
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
export async function getInitialState(): Promise<{
|
||||
user?: Record<string, any>;
|
||||
|
|
@ -102,11 +102,11 @@ export const request: RequestConfig = {
|
|||
export const onRouteChange = ({ location }: { location: Location }) => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// 白名单,不需要登录的页面
|
||||
// 白名单,不需要登录的页面
|
||||
const whiteList = ['/login', '/track'];
|
||||
|
||||
if (!token && !whiteList.includes(location.pathname)) {
|
||||
// 没有 token 且不在白名单内,跳转到登录页
|
||||
// 没有 token 且不在白名单内,跳转到登录页
|
||||
history.push('/login');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish }) => {
|
|||
return (
|
||||
<DrawerForm<API.ordercontrollerSyncorderParams>
|
||||
title="同步订单"
|
||||
// 表单的触发器,一个带图标的按钮
|
||||
// 表单的触发器,一个带图标的按钮
|
||||
trigger={
|
||||
<Button key="syncSite" type="primary">
|
||||
<SyncOutlined />
|
||||
|
|
|
|||
|
|
@ -29,5 +29,5 @@ export function useDeviceFingerprint() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return fingerprint; // 初始为 null,加载后返回指纹 ID
|
||||
return fingerprint; // 初始为 null,加载后返回指纹 ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ const AreaList: React.FC = () => {
|
|||
</Button>
|
||||
<Popconfirm
|
||||
title="删除区域"
|
||||
description="确认删除该区域?"
|
||||
description="确认删除该区域?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await request(`/area/${row.id}`, {
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ const CategoryPage: React.FC = () => {
|
|||
</a>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定删除该分类吗?"
|
||||
title="确定删除该分类吗?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteCategory(item.id);
|
||||
|
|
@ -218,7 +218,7 @@ const CategoryPage: React.FC = () => {
|
|||
</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}
|
||||
|
|
@ -226,7 +226,7 @@ const CategoryPage: React.FC = () => {
|
|||
<List.Item
|
||||
actions={[
|
||||
<Popconfirm
|
||||
title="确定移除该属性吗?"
|
||||
title="确定移除该属性吗?"
|
||||
onConfirm={() => handleDeleteAttribute(item.id)}
|
||||
>
|
||||
<Button type="link" danger>移除</Button>
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ const DictPage: React.FC = () => {
|
|||
size="small"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
// 如果点击的是当前已选中的行,则取消选择
|
||||
// 如果点击的是当前已选中的行,则取消选择
|
||||
if (selectedDict?.id === record.id) {
|
||||
setSelectedDict(null);
|
||||
} else {
|
||||
|
|
@ -356,7 +356,7 @@ const DictPage: React.FC = () => {
|
|||
<ProTable
|
||||
columns={dictItemColumns}
|
||||
request={async (params) => {
|
||||
// 当没有选择字典时,不发起请求
|
||||
// 当没有选择字典时,不发起请求
|
||||
if (!selectedDict?.id) {
|
||||
return {
|
||||
data: [],
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ const Page = () => {
|
|||
/>
|
||||
),
|
||||
}}
|
||||
placeholder={'请输入密码!'}
|
||||
placeholder={'请输入密码!'}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码!',
|
||||
message: '请输入密码!',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const ListPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ const ListPage: React.FC = () => {
|
|||
await navigator.clipboard.writeText(
|
||||
record.return_tracking_number,
|
||||
);
|
||||
message.success('复制成功!');
|
||||
message.success('复制成功!');
|
||||
} catch (err) {
|
||||
message.error('复制失败!');
|
||||
message.error('复制失败!');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -140,7 +140,7 @@ const ListPage: React.FC = () => {
|
|||
<Popconfirm
|
||||
disabled={isLoading}
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const OrderItemsPage: React.FC = () => {
|
|||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
|
||||
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
|
||||
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
|
||||
const columns: ProColumns<OrderItemAggRow>[] = [
|
||||
{
|
||||
title: '商品名称',
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ const ListPage: React.FC = () => {
|
|||
label: (
|
||||
<Popconfirm
|
||||
title="转至售后"
|
||||
description="确认转至售后?"
|
||||
description="确认转至售后?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
|
|
@ -513,7 +513,7 @@ const Detail: React.FC<{
|
|||
orderId,
|
||||
});
|
||||
if (!success || !data) return { data: {} };
|
||||
// 合并订单中相同的sku,只显示一次记录总数
|
||||
// 合并订单中相同的sku,只显示一次记录总数
|
||||
data.sales = data.sales?.reduce(
|
||||
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||
|
|
@ -605,7 +605,7 @@ const Detail: React.FC<{
|
|||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至售后"
|
||||
description="确认转至售后?"
|
||||
description="确认转至售后?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
|
|
@ -641,7 +641,7 @@ const Detail: React.FC<{
|
|||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至取消"
|
||||
description="确认转至取消?"
|
||||
description="确认转至取消?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
|
|
@ -668,7 +668,7 @@ const Detail: React.FC<{
|
|||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至退款"
|
||||
description="确认转至退款?"
|
||||
description="确认转至退款?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
|
|
@ -695,7 +695,7 @@ const Detail: React.FC<{
|
|||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至完成"
|
||||
description="确认转至完成?"
|
||||
description="确认转至完成?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
|
|
@ -722,7 +722,7 @@ const Detail: React.FC<{
|
|||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至待补发"
|
||||
description="确认转至待补发?"
|
||||
description="确认转至待补发?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -999,9 +999,9 @@ const Detail: React.FC<{
|
|||
await navigator.clipboard.writeText(
|
||||
v.tracking_url,
|
||||
);
|
||||
message.success('复制成功!');
|
||||
message.success('复制成功!');
|
||||
} catch (err) {
|
||||
message.error('复制失败!');
|
||||
message.error('复制失败!');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -1013,7 +1013,7 @@ const Detail: React.FC<{
|
|||
? [
|
||||
<Popconfirm
|
||||
title="取消运单"
|
||||
description="确认取消运单?"
|
||||
description="确认取消运单?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -1461,7 +1461,7 @@ const Shipping: React.FC<{
|
|||
showSearch: true,
|
||||
filterOption: false,
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
|
|
@ -2083,7 +2083,7 @@ const SalesChange: React.FC<{
|
|||
showSearch: true,
|
||||
filterOption: false,
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择订单' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
|
|
@ -2126,7 +2126,7 @@ const SalesChange: React.FC<{
|
|||
showSearch: true,
|
||||
filterOption: false,
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
|
|
@ -2226,7 +2226,7 @@ const CreateOrder: React.FC<{
|
|||
showSearch: true,
|
||||
filterOption: false,
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const ListPage: React.FC = () => {
|
|||
hideInSearch: true,
|
||||
width: 800,
|
||||
render: (_, record) => {
|
||||
return record?.numbers?.join?.('、');
|
||||
return record?.numbers?.join?.(',');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -72,7 +72,7 @@ const ListPage: React.FC = () => {
|
|||
|
||||
// 数据行
|
||||
const rows = (data?.items || []).map((item) => {
|
||||
return [item.name, item.quantity, item.numbers?.join('、')];
|
||||
return [item.name, item.quantity, item.numbers?.join(',')];
|
||||
});
|
||||
|
||||
// 导出
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const ListPage: React.FC = () => {
|
|||
danger={record.isActive}
|
||||
type="link"
|
||||
onClick={async () => {
|
||||
// 中文注释:软删除为禁用(isActive=false),再次点击可启用
|
||||
// 软删除为禁用(isActive=false),再次点击可启用
|
||||
const next = !record.isActive;
|
||||
const { success, message: errMsg } =
|
||||
await usercontrollerToggleactive({
|
||||
|
|
@ -204,7 +204,7 @@ const EditForm: React.FC<{
|
|||
}}
|
||||
onFinish={async (values: any) => {
|
||||
try {
|
||||
// 中文注释:更新用户,密码可选填
|
||||
// 更新用户,密码可选填
|
||||
const { success, message: err } = await usercontrollerUpdateuser(
|
||||
{ id: record.id },
|
||||
values,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// 中文注释:限定允许管理的字典名称集合
|
||||
// 限定允许管理的字典名称集合
|
||||
export const attributes = new Set([
|
||||
'brand',
|
||||
'strength',
|
||||
|
|
|
|||
|
|
@ -23,16 +23,16 @@ const { Sider, Content } = Layout;
|
|||
import { attributes } from './consts';
|
||||
|
||||
const AttributePage: React.FC = () => {
|
||||
// 中文注释:左侧字典列表状态
|
||||
// 左侧字典列表状态
|
||||
const [dicts, setDicts] = useState<any[]>([]);
|
||||
const [loadingDicts, setLoadingDicts] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedDict, setSelectedDict] = useState<any>(null);
|
||||
|
||||
// 中文注释:右侧字典项 ProTable 的引用
|
||||
// 右侧字典项 ProTable 的引用
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
// 中文注释:字典项新增/编辑模态框控制
|
||||
// 字典项新增/编辑模态框控制
|
||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
||||
const [editingDictItem, setEditingDictItem] = useState<any>(null);
|
||||
const [dictItemForm] = Form.useForm();
|
||||
|
|
@ -41,7 +41,7 @@ const AttributePage: React.FC = () => {
|
|||
setLoadingDicts(true);
|
||||
try {
|
||||
const res = await request('/dict/list', { params: { title } });
|
||||
// 中文注释:条件判断,过滤只保留 allowedDictNames 中的字典
|
||||
// 条件判断,过滤只保留 allowedDictNames 中的字典
|
||||
const filtered = (res || []).filter((d: any) => attributes.has(d?.name));
|
||||
setDicts(filtered);
|
||||
} catch (error) {
|
||||
|
|
@ -50,42 +50,42 @@ const AttributePage: React.FC = () => {
|
|||
setLoadingDicts(false);
|
||||
};
|
||||
|
||||
// 中文注释:组件挂载时初始化数据
|
||||
// 组件挂载时初始化数据
|
||||
useEffect(() => {
|
||||
fetchDicts();
|
||||
}, []);
|
||||
|
||||
// 中文注释:搜索触发过滤
|
||||
// 搜索触发过滤
|
||||
const handleSearch = (value: string) => {
|
||||
fetchDicts(value);
|
||||
};
|
||||
|
||||
// 中文注释:打开添加字典项模态框
|
||||
// 打开添加字典项模态框
|
||||
const handleAddDictItem = () => {
|
||||
setEditingDictItem(null);
|
||||
dictItemForm.resetFields();
|
||||
setIsDictItemModalVisible(true);
|
||||
};
|
||||
|
||||
// 中文注释:打开编辑字典项模态框
|
||||
// 打开编辑字典项模态框
|
||||
const handleEditDictItem = (item: any) => {
|
||||
setEditingDictItem(item);
|
||||
dictItemForm.setFieldsValue(item);
|
||||
setIsDictItemModalVisible(true);
|
||||
};
|
||||
|
||||
// 中文注释:字典项表单提交(新增或编辑)
|
||||
// 字典项表单提交(新增或编辑)
|
||||
const handleDictItemFormSubmit = async (values: any) => {
|
||||
try {
|
||||
if (editingDictItem) {
|
||||
// 中文注释:条件判断,存在编辑项则执行更新
|
||||
// 条件判断,存在编辑项则执行更新
|
||||
await request(`/dict/item/${editingDictItem.id}`, {
|
||||
method: 'PUT',
|
||||
data: values,
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
// 中文注释:否则执行新增,绑定到当前选择的字典
|
||||
// 否则执行新增,绑定到当前选择的字典
|
||||
await request('/dict/item', {
|
||||
method: 'POST',
|
||||
data: { ...values, dictId: selectedDict.id },
|
||||
|
|
@ -93,30 +93,30 @@ const AttributePage: React.FC = () => {
|
|||
message.success('添加成功');
|
||||
}
|
||||
setIsDictItemModalVisible(false);
|
||||
actionRef.current?.reload(); // 中文注释:刷新 ProTable
|
||||
actionRef.current?.reload(); // 刷新 ProTable
|
||||
} catch (error) {
|
||||
message.error(editingDictItem ? '更新失败' : '添加失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 中文注释:删除字典项
|
||||
// 删除字典项
|
||||
const handleDeleteDictItem = async (itemId: number) => {
|
||||
try {
|
||||
await request(`/dict/item/${itemId}`, { method: 'DELETE' });
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload(); // 中文注释:刷新 ProTable
|
||||
actionRef.current?.reload(); // 刷新 ProTable
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 中文注释:左侧字典列表列定义(紧凑样式)
|
||||
// 左侧字典列表列定义(紧凑样式)
|
||||
const dictColumns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '标题', dataIndex: 'title', key: 'title' },
|
||||
];
|
||||
|
||||
// 中文注释:右侧字典项列表列定义(紧凑样式)
|
||||
// 右侧字典项列表列定义(紧凑样式)
|
||||
const dictItemColumns: any[] = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', copyable: true },
|
||||
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
|
||||
|
|
@ -177,7 +177,7 @@ const AttributePage: React.FC = () => {
|
|||
size="small"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
// 中文注释:条件判断,重复点击同一行则取消选择
|
||||
// 条件判断,重复点击同一行则取消选择
|
||||
if (selectedDict?.id === record.id) {
|
||||
setSelectedDict(null);
|
||||
} else {
|
||||
|
|
@ -197,7 +197,7 @@ const AttributePage: React.FC = () => {
|
|||
columns={dictItemColumns}
|
||||
actionRef={actionRef}
|
||||
request={async (params) => {
|
||||
// 中文注释:当没有选择字典时,不发起请求
|
||||
// 当没有选择字典时,不发起请求
|
||||
if (!selectedDict?.id) {
|
||||
return {
|
||||
data: [],
|
||||
|
|
@ -242,7 +242,7 @@ const AttributePage: React.FC = () => {
|
|||
showUploadList={false}
|
||||
disabled={!selectedDict}
|
||||
onChange={(info) => {
|
||||
// 中文注释:条件判断,上传状态处理
|
||||
// 条件判断,上传状态处理
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功`);
|
||||
actionRef.current?.reload();
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ const CategoryPage: React.FC = () => {
|
|||
</a>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定删除该分类吗?"
|
||||
title="确定删除该分类吗?"
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDeleteCategory(item.id);
|
||||
|
|
@ -223,7 +223,7 @@ const CategoryPage: React.FC = () => {
|
|||
</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}
|
||||
|
|
@ -231,7 +231,7 @@ const CategoryPage: React.FC = () => {
|
|||
<List.Item
|
||||
actions={[
|
||||
<Popconfirm
|
||||
title="确定移除该属性吗?"
|
||||
title="确定移除该属性吗?"
|
||||
onConfirm={() => handleDeleteAttribute(item.id)}
|
||||
>
|
||||
<Button type="link" danger>移除</Button>
|
||||
|
|
|
|||
|
|
@ -94,11 +94,11 @@ const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
|
|||
components.map((component: any) => (
|
||||
<Tag key={component.id} color="blue" style={{ marginBottom: 4 }}>
|
||||
{component.sku || `#${component.id}`} × {component.quantity}
|
||||
(库存:
|
||||
(库存:
|
||||
{component.stock
|
||||
?.map((s: any) => `${s.name}:${s.quantity}`)
|
||||
.join(', ') || '-'}
|
||||
)
|
||||
)
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -110,22 +110,22 @@ const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
|
|||
|
||||
const List: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
// 状态:存储当前选中的行
|
||||
// 状态:存储当前选中的行
|
||||
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
|
||||
|
||||
const { message } = App.useApp();
|
||||
// 中文注释:导出产品 CSV(带认证请求)
|
||||
// 导出产品 CSV(带认证请求)
|
||||
const handleDownloadProductsCSV = async () => {
|
||||
try {
|
||||
// 中文注释:发起认证请求获取 CSV Blob
|
||||
// 发起认证请求获取 CSV Blob
|
||||
const blob = await request('/product/export', { responseType: 'blob' });
|
||||
// 中文注释:构建下载文件名
|
||||
// 构建下载文件名
|
||||
const d = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const filename = `products-${d.getFullYear()}${pad(
|
||||
d.getMonth() + 1,
|
||||
)}${pad(d.getDate())}.csv`;
|
||||
// 中文注释:创建临时链接并触发下载
|
||||
// 创建临时链接并触发下载
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
@ -142,10 +142,12 @@ const List: React.FC = () => {
|
|||
{
|
||||
title: 'sku',
|
||||
dataIndex: 'sku',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '中文名',
|
||||
|
|
@ -156,27 +158,7 @@ const List: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '产品类型',
|
||||
dataIndex: 'type',
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
single: { text: '单品' },
|
||||
bundle: { text: '套装' },
|
||||
},
|
||||
render: (_, record) => {
|
||||
// 如果类型不存在,则返回-
|
||||
if (!record.type) return '-';
|
||||
// 判断是否为单品
|
||||
const isSingle = record.type === 'single';
|
||||
// 根据类型显示不同颜色的标签
|
||||
return (
|
||||
<Tag color={isSingle ? 'green' : 'orange'}>
|
||||
{isSingle ? '单品' : '套装'}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '商品类型',
|
||||
dataIndex: 'category',
|
||||
|
|
@ -188,17 +170,40 @@ const List: React.FC = () => {
|
|||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '促销价',
|
||||
dataIndex: 'promotionPrice',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '属性',
|
||||
dataIndex: 'attributes',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => <AttributesCell record={record} />,
|
||||
},
|
||||
{
|
||||
title: '产品类型',
|
||||
dataIndex: 'type',
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
single: { text: '单品' },
|
||||
bundle: { text: '套装' },
|
||||
},
|
||||
render: (_, record) => {
|
||||
// 如果类型不存在,则返回-
|
||||
if (!record.type) return '-';
|
||||
// 判断是否为单品
|
||||
const isSingle = record.type === 'single';
|
||||
// 根据类型显示不同颜色的标签
|
||||
return (
|
||||
<Tag color={isSingle ? 'green' : 'orange'}>
|
||||
{isSingle ? '单品' : '套装'}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '构成',
|
||||
|
|
@ -217,12 +222,14 @@ const List: React.FC = () => {
|
|||
dataIndex: 'updatedAt',
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
|
@ -234,7 +241,7 @@ const List: React.FC = () => {
|
|||
<EditForm record={record} tableRef={actionRef} />
|
||||
<Popconfirm
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -264,33 +271,53 @@ const List: React.FC = () => {
|
|||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
toolBarRender={() => [
|
||||
// 中文注释:新建按钮
|
||||
// 新建按钮
|
||||
<CreateForm tableRef={actionRef} />,
|
||||
// 中文注释:导出 CSV(后端返回 text/csv,直接新窗口下载)
|
||||
// 导出 CSV(后端返回 text/csv,直接新窗口下载)
|
||||
<Button onClick={handleDownloadProductsCSV}>导出CSV</Button>,
|
||||
// 中文注释:导入 CSV(上传文件成功后刷新表格)
|
||||
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
|
||||
<Upload
|
||||
name="file"
|
||||
action="/product/import"
|
||||
accept=".csv"
|
||||
showUploadList={false}
|
||||
maxCount={1}
|
||||
onChange={({ file }) => {
|
||||
if (file.status === 'done') {
|
||||
customRequest={async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
await request('/product/import', {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
requestType: 'form',
|
||||
});
|
||||
message.success('导入完成');
|
||||
onSuccess?.('ok');
|
||||
actionRef.current?.reload();
|
||||
} else if (file.status === 'error') {
|
||||
} catch (error: any) {
|
||||
message.error('导入失败');
|
||||
onError?.(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button>导入CSV</Button>
|
||||
</Upload>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const { data, success } = await productcontrollerGetproductlist(
|
||||
params,
|
||||
);
|
||||
request={async (params, sort) => {
|
||||
let sortField = undefined;
|
||||
let sortOrder = undefined;
|
||||
|
||||
if (sort && Object.keys(sort).length > 0) {
|
||||
const field = Object.keys(sort)[0];
|
||||
sortField = field;
|
||||
sortOrder = sort[field];
|
||||
}
|
||||
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
...params,
|
||||
sortField,
|
||||
sortOrder,
|
||||
} as any);
|
||||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
|
|
@ -355,10 +382,10 @@ const CreateForm: React.FC<{
|
|||
const { humidityValues, brandValues, strengthValues, flavorValues } =
|
||||
formValues;
|
||||
// 检查是否所有必需的字段都已选择
|
||||
// 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑
|
||||
// 暂时保持原样,假设常用属性会被配置
|
||||
// 注意:这里仅检查标准属性,如果当前分类没有这些属性,可能需要调整逻辑
|
||||
// 暂时保持原样,假设常用属性会被配置
|
||||
|
||||
// 所选值(用于 SKU 模板传入 name)
|
||||
// 所选值(用于 SKU 模板传入 name)
|
||||
const brandName: string = String(brandValues?.[0] || '');
|
||||
const strengthName: string = String(strengthValues?.[0] || '');
|
||||
const flavorName: string = String(flavorValues?.[0] || '');
|
||||
|
|
@ -480,14 +507,14 @@ const CreateForm: React.FC<{
|
|||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
||||
}
|
||||
} else {
|
||||
// 如果 sku 不存在,则重置状态
|
||||
// 如果 sku 不存在,则重置状态
|
||||
setStockStatus(null);
|
||||
formRef.current?.setFieldsValue({ type: null });
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFinish={async (values: any) => {
|
||||
// 中文注释:组装 attributes(根据 activeAttributes 动态组装)
|
||||
// 组装 attributes(根据 activeAttributes 动态组装)
|
||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.name;
|
||||
const key = `${dictName}Values`;
|
||||
|
|
@ -715,12 +742,12 @@ const EditForm: React.FC<{
|
|||
sku: record.sku,
|
||||
} as any);
|
||||
if (stockData && stockData.items && stockData.items.length > 0) {
|
||||
// 如果有库存,则为单品
|
||||
// 如果有库存,则为单品
|
||||
setType('single');
|
||||
setStockStatus('in-stock');
|
||||
formRef.current?.setFieldsValue({ type: 'single' });
|
||||
} else {
|
||||
// 如果没有库存,则为套装
|
||||
// 如果没有库存,则为套装
|
||||
setType('bundle');
|
||||
setStockStatus('out-of-stock');
|
||||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
||||
|
|
@ -785,13 +812,13 @@ const EditForm: React.FC<{
|
|||
formRef.current?.setFieldsValue({ type: 'bundle' });
|
||||
}
|
||||
} else {
|
||||
// 如果 sku 不存在,则重置状态
|
||||
// 如果 sku 不存在,则重置状态
|
||||
formRef.current?.setFieldsValue({ type: null });
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
// 中文注释:组装 attributes
|
||||
// 组装 attributes
|
||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.name;
|
||||
const key = `${dictName}Values`;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface Site {
|
|||
id: string;
|
||||
name: string;
|
||||
prefix?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
// 定义WordPress商品接口
|
||||
|
|
@ -80,7 +81,9 @@ const ProductSyncPage: React.FC = () => {
|
|||
setLoading(true);
|
||||
// 获取所有站点
|
||||
const sitesResponse = await getSites();
|
||||
const siteList: Site[] = sitesResponse.data || [];
|
||||
const rawSiteList = sitesResponse.data || [];
|
||||
// 过滤掉已禁用的站点
|
||||
const siteList: Site[] = rawSiteList.filter(site => !site.isDisabled);
|
||||
setSites(siteList);
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
|
|
@ -120,7 +123,7 @@ const ProductSyncPage: React.FC = () => {
|
|||
// 转换为数组
|
||||
setProducts(Array.from(productMap.values()));
|
||||
} catch (error) {
|
||||
message.error('获取数据失败,请重试');
|
||||
message.error('获取数据失败,请重试');
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
ActionType,
|
||||
DrawerForm,
|
||||
ProColumns,
|
||||
ProFormDependency,
|
||||
ProFormInstance,
|
||||
ProFormSelect,
|
||||
ProFormSwitch,
|
||||
|
|
@ -18,7 +19,7 @@ interface AreaItem {
|
|||
name: string;
|
||||
}
|
||||
|
||||
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
||||
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
||||
interface SiteItem {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -29,7 +30,7 @@ interface SiteItem {
|
|||
areas?: AreaItem[];
|
||||
}
|
||||
|
||||
// 创建/更新表单的值类型,包含可选的密钥字段
|
||||
// 创建/更新表单的值类型,包含可选的密钥字段
|
||||
interface SiteFormValues {
|
||||
name: string;
|
||||
apiUrl?: string;
|
||||
|
|
@ -37,6 +38,7 @@ interface SiteFormValues {
|
|||
isDisabled?: boolean;
|
||||
consumerKey?: string; // WooCommerce REST API 的 consumer key
|
||||
consumerSecret?: string; // WooCommerce REST API 的 consumer secret
|
||||
token?: string; // Shopyy token
|
||||
skuPrefix?: string;
|
||||
areas?: string[];
|
||||
}
|
||||
|
|
@ -58,6 +60,7 @@ const SiteList: React.FC = () => {
|
|||
isDisabled: !!editing.isDisabled,
|
||||
consumerKey: undefined,
|
||||
consumerSecret: undefined,
|
||||
token: undefined,
|
||||
areas: editing.areas?.map((area) => area.code) ?? [],
|
||||
});
|
||||
} else {
|
||||
|
|
@ -69,6 +72,7 @@ const SiteList: React.FC = () => {
|
|||
isDisabled: false,
|
||||
consumerKey: undefined,
|
||||
consumerSecret: undefined,
|
||||
token: undefined,
|
||||
});
|
||||
}
|
||||
}, [open, editing]);
|
||||
|
|
@ -148,7 +152,7 @@ const SiteList: React.FC = () => {
|
|||
<Popconfirm
|
||||
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
||||
description={
|
||||
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
|
||||
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
|
||||
}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
|
|
@ -198,14 +202,17 @@ const SiteList: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 提交创建/更新逻辑;编辑时未填写密钥则不提交(保持原值)
|
||||
// 提交创建/更新逻辑;编辑时未填写密钥则不提交(保持原值)
|
||||
const handleSubmit = async (values: SiteFormValues) => {
|
||||
try {
|
||||
const isShopyy = values.type === 'shopyy';
|
||||
const apiUrl = isShopyy ? 'https://openapi.oemapps.com' : values.apiUrl;
|
||||
|
||||
if (editing) {
|
||||
const payload: Record<string, any> = {
|
||||
// 仅提交存在的字段,避免覆盖为 null/空
|
||||
// 仅提交存在的字段,避免覆盖为 null/空
|
||||
...(values.name ? { name: values.name } : {}),
|
||||
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
|
||||
...(apiUrl ? { apiUrl: apiUrl } : {}),
|
||||
...(values.type ? { type: values.type } : {}),
|
||||
...(typeof values.isDisabled === 'boolean'
|
||||
? { isDisabled: values.isDisabled }
|
||||
|
|
@ -213,30 +220,46 @@ const SiteList: React.FC = () => {
|
|||
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
|
||||
areas: values.areas ?? [],
|
||||
};
|
||||
// 仅当输入了新密钥时才提交,未输入则保持原本值
|
||||
|
||||
if (isShopyy) {
|
||||
if (values.token && values.token.trim()) {
|
||||
payload.token = values.token.trim();
|
||||
}
|
||||
} else {
|
||||
// 仅当输入了新密钥时才提交,未输入则保持原本值
|
||||
if (values.consumerKey && values.consumerKey.trim()) {
|
||||
payload.consumerKey = values.consumerKey.trim();
|
||||
}
|
||||
if (values.consumerSecret && values.consumerSecret.trim()) {
|
||||
payload.consumerSecret = values.consumerSecret.trim();
|
||||
}
|
||||
}
|
||||
|
||||
await request(`/site/update/${editing.id}`, {
|
||||
method: 'PUT',
|
||||
data: payload,
|
||||
});
|
||||
} else {
|
||||
if (isShopyy) {
|
||||
if (!values.token) {
|
||||
throw new Error('Token is required for Shopyy');
|
||||
}
|
||||
} else {
|
||||
// 新增站点时要求填写 consumerKey 和 consumerSecret
|
||||
if (!values.consumerKey || !values.consumerSecret) {
|
||||
throw new Error('Consumer Key and Secret are required');
|
||||
throw new Error('Consumer Key and Secret are required for WooCommerce');
|
||||
}
|
||||
}
|
||||
|
||||
await request('/site/create', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
name: values.name,
|
||||
apiUrl: values.apiUrl,
|
||||
apiUrl: apiUrl,
|
||||
type: values.type || 'woocommerce',
|
||||
consumerKey: values.consumerKey,
|
||||
consumerSecret: values.consumerSecret,
|
||||
consumerKey: isShopyy ? undefined : values.consumerKey,
|
||||
consumerSecret: isShopyy ? undefined : values.consumerSecret,
|
||||
token: isShopyy ? values.token : undefined,
|
||||
skuPrefix: values.skuPrefix,
|
||||
areas: values.areas ?? [],
|
||||
},
|
||||
|
|
@ -286,30 +309,13 @@ const SiteList: React.FC = () => {
|
|||
formRef={formRef}
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 站点名称,必填 */}
|
||||
{/* 站点名称,必填 */}
|
||||
<ProFormText
|
||||
name="name"
|
||||
label="站点名称"
|
||||
placeholder="例如:本地商店"
|
||||
placeholder="例如:本地商店"
|
||||
rules={[{ required: true, message: '站点名称为必填项' }]}
|
||||
/>
|
||||
{/* API 地址,可选 */}
|
||||
<ProFormText
|
||||
name="apiUrl"
|
||||
label="API 地址"
|
||||
placeholder="例如:https://shop.example.com"
|
||||
/>
|
||||
{/* 平台类型选择 */}
|
||||
<ProFormSelect
|
||||
name="type"
|
||||
label="平台"
|
||||
options={[
|
||||
{ label: 'WooCommerce', value: 'woocommerce' },
|
||||
{ label: 'Shopyy', value: 'shopyy' },
|
||||
]}
|
||||
/>
|
||||
{/* 是否禁用 */}
|
||||
<ProFormSwitch name="isDisabled" label="禁用" />
|
||||
{/* 区域选择 */}
|
||||
<ProFormSelect
|
||||
name="areas"
|
||||
|
|
@ -334,26 +340,68 @@ const SiteList: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<ProFormText
|
||||
name="skuPrefix"
|
||||
label="SKU 前缀"
|
||||
placeholder={editing ? '留空表示不修改' : '可选'}
|
||||
{/* 平台类型选择 */}
|
||||
<ProFormSelect
|
||||
name="type"
|
||||
label="平台"
|
||||
options={[
|
||||
{ label: 'WooCommerce', value: 'woocommerce' },
|
||||
{ label: 'Shopyy', value: 'shopyy' },
|
||||
]}
|
||||
/>
|
||||
{/* WooCommerce REST consumer key;新增必填,编辑不填则保持原值 */}
|
||||
|
||||
<ProFormDependency name={['type']}>
|
||||
{({ type }) => {
|
||||
const isShopyy = type === 'shopyy';
|
||||
return isShopyy ? (
|
||||
<>
|
||||
<ProFormText
|
||||
name="apiUrl"
|
||||
label="API 地址"
|
||||
disabled
|
||||
initialValue="https://openapi.oemapps.com"
|
||||
placeholder="https://openapi.oemapps.com"
|
||||
/>
|
||||
<ProFormText
|
||||
name="token"
|
||||
label="Token"
|
||||
placeholder={editing ? '留空表示不修改' : '必填'}
|
||||
rules={editing ? [] : [{ required: true, message: 'Token 为必填项' }]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ProFormText
|
||||
name="apiUrl"
|
||||
label="API 地址"
|
||||
placeholder="例如:https://shop.example.com"
|
||||
rules={[{ required: true, message: 'API 地址为必填项' }]}
|
||||
/>
|
||||
{/* WooCommerce REST consumer key */}
|
||||
<ProFormText
|
||||
name="consumerKey"
|
||||
label="Key"
|
||||
placeholder={editing ? '留空表示不修改' : '必填'}
|
||||
rules={editing ? [] : [{ required: true, message: 'Key 为必填项' }]}
|
||||
/>
|
||||
{/* WooCommerce REST consumer secret;新增必填,编辑不填则保持原值 */}
|
||||
{/* WooCommerce REST consumer secret */}
|
||||
<ProFormText
|
||||
name="consumerSecret"
|
||||
label="Secret"
|
||||
placeholder={editing ? '留空表示不修改' : '必填'}
|
||||
rules={
|
||||
editing ? [] : [{ required: true, message: 'Secret 为必填项' }]
|
||||
}
|
||||
rules={editing ? [] : [{ required: true, message: 'Secret 为必填项' }]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ProFormDependency>
|
||||
|
||||
{editing && <ProFormSwitch name="isDisabled" label="禁用" />}
|
||||
|
||||
<ProFormText
|
||||
name="skuPrefix"
|
||||
label="SKU 前缀"
|
||||
placeholder={editing ? '留空表示不修改' : '可选'}
|
||||
/>
|
||||
</DrawerForm>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const ListPage: React.FC = () => {
|
|||
|
||||
// 数据行
|
||||
const rows = (data?.items || []).map((item: API.StockDTO) => {
|
||||
// 处理stockPoint可能为undefined的情况,并正确定义类型
|
||||
// 处理stockPoint可能为undefined的情况,并正确定义类型
|
||||
const stockMap = new Map<number, number>(
|
||||
(item.stockPoint || []).map((sp: any) => [
|
||||
sp.id || 0,
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -120,7 +120,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="入库"
|
||||
description="确认已到达?"
|
||||
description="确认已到达?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -297,7 +297,7 @@ const CreateForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
@ -478,7 +478,7 @@ const UpdateForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
@ -643,7 +643,7 @@ const DetailForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const TransferPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="入库"
|
||||
description="确认已到达?"
|
||||
description="确认已到达?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -116,7 +116,7 @@ const TransferPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="丢失"
|
||||
description="确认该批货已丢失?"
|
||||
description="确认该批货已丢失?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -137,7 +137,7 @@ const TransferPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="取消"
|
||||
description="确认取消?"
|
||||
description="确认取消?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -354,7 +354,7 @@ const CreateForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
@ -530,7 +530,7 @@ const UpdateForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
@ -687,7 +687,7 @@ const DetailForm: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
onChange={(_, option) => {
|
||||
form.setFieldValue(
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const ListPage: React.FC = () => {
|
|||
<Divider type="vertical" />
|
||||
<Popconfirm
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
|
|||
};
|
||||
|
||||
/**
|
||||
* 订阅列表页:展示、筛选、触发订阅同步
|
||||
* 订阅列表页:展示,筛选,触发订阅同步
|
||||
*/
|
||||
const ListPage: React.FC = () => {
|
||||
// 表格操作引用:用于在同步后触发表格刷新
|
||||
|
|
@ -75,7 +75,7 @@ const ListPage: React.FC = () => {
|
|||
dataIndex: 'status',
|
||||
valueType: 'select',
|
||||
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
||||
// 以 Tag 形式展示,更易辨识
|
||||
// 以 Tag 形式展示,更易辨识
|
||||
render: (_, row) =>
|
||||
row?.status ? (
|
||||
<Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag>
|
||||
|
|
@ -159,7 +159,7 @@ const ListPage: React.FC = () => {
|
|||
const candidates: any[] = (
|
||||
Array.isArray(data) ? data : []
|
||||
).filter((c: any) => String(c?.externalOrderId) === parentNumber);
|
||||
// 拉取详情,补充状态、金额、时间
|
||||
// 拉取详情,补充状态,金额,时间
|
||||
const details = [] as any[];
|
||||
for (const c of candidates) {
|
||||
const d = await request(`/order/${c.id}`, { method: 'GET' });
|
||||
|
|
@ -205,7 +205,7 @@ const ListPage: React.FC = () => {
|
|||
rowKey="id"
|
||||
actionRef={actionRef}
|
||||
/**
|
||||
* 列表数据请求;保持与后端分页参数一致
|
||||
* 列表数据请求;保持与后端分页参数一致
|
||||
* 兼容后端 data.items 或 data.list 返回字段
|
||||
*/
|
||||
request={async (params) => {
|
||||
|
|
@ -220,7 +220,7 @@ const ListPage: React.FC = () => {
|
|||
// 工具栏:订阅同步入口
|
||||
toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]}
|
||||
/>
|
||||
{/* 关联订单抽屉:展示订单号、关系、时间、状态与金额 */}
|
||||
{/* 关联订单抽屉:展示订单号,关系,时间,状态与金额 */}
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
title={drawerTitle}
|
||||
|
|
@ -234,7 +234,7 @@ const ListPage: React.FC = () => {
|
|||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={`#${item?.externalOrderId || '-'}`}
|
||||
description={`关系:${item?.relationship || '-'},站点:${
|
||||
description={`关系:${item?.relationship || '-'},站点:${
|
||||
item?.siteName || '-'
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -281,7 +281,7 @@ const SyncForm: React.FC<{
|
|||
/**
|
||||
* 提交逻辑:
|
||||
* 1. 必填校验由 ProForm + rules 保证
|
||||
* 2. 调用同步接口,失败时友好提示
|
||||
* 2. 调用同步接口,失败时友好提示
|
||||
* 3. 成功后刷新列表
|
||||
*/
|
||||
onFinish={async (values) => {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import { ORDER_STATUS_ENUM } from '@/constants';
|
|||
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||
import RelatedOrders from './RelatedOrders';
|
||||
|
||||
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
||||
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
||||
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
||||
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
||||
// 当前按你的要求仅抽离详情 Drawer
|
||||
|
||||
type OrderRecord = API.Order;
|
||||
|
|
@ -118,7 +118,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
<Popconfirm
|
||||
key="btn-after-sale"
|
||||
title="转至售后"
|
||||
description="确认转至售后?"
|
||||
description="确认转至售后?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -145,7 +145,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
<Popconfirm
|
||||
key="btn-cancel"
|
||||
title="转至取消"
|
||||
description="确认转至取消?"
|
||||
description="确认转至取消?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -168,7 +168,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
<Popconfirm
|
||||
key="btn-refund"
|
||||
title="转至退款"
|
||||
description="确认转至退款?"
|
||||
description="确认转至退款?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -191,7 +191,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
<Popconfirm
|
||||
key="btn-completed"
|
||||
title="转至完成"
|
||||
description="确认转至完成?"
|
||||
description="确认转至完成?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
@ -425,9 +425,9 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
await navigator.clipboard.writeText(
|
||||
v.tracking_url,
|
||||
);
|
||||
message.success('复制成功!');
|
||||
message.success('复制成功!');
|
||||
} catch {
|
||||
message.error('复制失败!');
|
||||
message.error('复制失败!');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -440,7 +440,7 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|||
<Popconfirm
|
||||
key="action-cancel"
|
||||
title="取消运单"
|
||||
description="确认取消运单?"
|
||||
description="确认取消运单?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ dayjs.extend(relativeTime);
|
|||
|
||||
/**
|
||||
* RelatedOrders 表格组件
|
||||
* 用于展示订单详情中的关联数据(订阅/订单),按统一表格样式渲染
|
||||
* 本组件将订阅与订单统一归一化为五列展示,便于快速浏览
|
||||
* 用于展示订单详情中的关联数据(订阅/订单),按统一表格样式渲染
|
||||
* 本组件将订阅与订单统一归一化为五列展示,便于快速浏览
|
||||
*/
|
||||
const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
||||
const rows = (Array.isArray(data) ? data : []).map((it: any) => {
|
||||
|
|
@ -48,7 +48,7 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
|||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
||||
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ const List: React.FC = () => {
|
|||
<UpdateForm tableRef={actionRef} values={record} />
|
||||
<Popconfirm
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
if (!record.id) return;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ const SetComponent: React.FC<{
|
|||
transform={(value) => {
|
||||
return value?.value || value;
|
||||
}}
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
debounceTime={300} // 防抖,减少请求频率
|
||||
rules={[{ required: true, message: '请选择产品' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ interface TagConfig {
|
|||
// 移植 Python 脚本中的核心函数
|
||||
|
||||
/**
|
||||
* @description 从产品名称中解析出品牌、口味、毫克含量和干燥度
|
||||
* @description 从产品名称中解析出品牌,口味,毫克含量和干燥度
|
||||
*/
|
||||
const parseName = (
|
||||
name: string,
|
||||
|
|
@ -38,8 +38,8 @@ const parseName = (
|
|||
const mgMatch = nm.match(/(\d+)\s*MG/i);
|
||||
const mg = mgMatch ? mgMatch[1] : '';
|
||||
|
||||
// 确保品牌按长度降序排序,避免部分匹配(如匹配到 VELO 而不是 VELO MAX)
|
||||
// 这一步其实应该在传入 brands 之前就做好了,但这里再保险一下
|
||||
// 确保品牌按长度降序排序,避免部分匹配(如匹配到 VELO 而不是 VELO MAX)
|
||||
// 这一步其实应该在传入 brands 之前就做好了,但这里再保险一下
|
||||
// 实际调用时 sortedBrands 已经排好序了
|
||||
for (const b of brands) {
|
||||
if (nm.toUpperCase().startsWith(b.toUpperCase())) {
|
||||
|
|
@ -84,7 +84,7 @@ const splitFlavorTokens = (flavorPart: string): string[] => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @description 根据口味分类额外的标签(如 Fruit, Mint)
|
||||
* @description 根据口味分类额外的标签(如 Fruit, Mint)
|
||||
*/
|
||||
const classifyExtraTags = (
|
||||
flavorPart: string,
|
||||
|
|
@ -111,7 +111,7 @@ const classifyExtraTags = (
|
|||
const matchAttributes = (text: string, keys: string[]): string[] => {
|
||||
const matched = new Set<string>();
|
||||
for (const key of keys) {
|
||||
// 使用单词边界匹配,避免部分匹配
|
||||
// 使用单词边界匹配,避免部分匹配
|
||||
// 转义正则特殊字符
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`\\b${escapedKey}\\b`, 'i');
|
||||
|
|
@ -129,7 +129,7 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
const [brand, flavorPart, mg, dryness] = parseName(name, config.brands);
|
||||
const tokens = splitFlavorTokens(flavorPart);
|
||||
|
||||
// 白名单模式:只保留在 flavorKeys 中的 token
|
||||
// 白名单模式:只保留在 flavorKeys 中的 token
|
||||
// 且对比时忽略大小写
|
||||
const flavorKeysLower = config.flavorKeys.map(k => k.toLowerCase());
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
}
|
||||
|
||||
// 保留原有的 dryness 提取逻辑 (从括号中提取)
|
||||
// 如果 dict 匹配已经覆盖了,去重时会处理
|
||||
// 如果 dict 匹配已经覆盖了,去重时会处理
|
||||
if (dryness) {
|
||||
if (/moist/i.test(dryness)) {
|
||||
tags.push('Moisture');
|
||||
|
|
@ -197,8 +197,8 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
// 去重并保留顺序
|
||||
const seen = new Set<string>();
|
||||
const finalTags = tags.filter((t) => {
|
||||
// 简单的去重,忽略大小写差异? 或者完全匹配
|
||||
// 这里使用完全匹配,因为前面已经做了一些格式化
|
||||
// 简单的去重,忽略大小写差异? 或者完全匹配
|
||||
// 这里使用完全匹配,因为前面已经做了一些格式化
|
||||
if (t && !seen.has(t)) {
|
||||
seen.add(t);
|
||||
return true;
|
||||
|
|
@ -210,7 +210,7 @@ const computeTags = (name: string, sku: string, config: TagConfig): string => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @description WordPress 产品工具页面,用于处理产品 CSV 并生成 Tags
|
||||
* @description WordPress 产品工具页面,用于处理产品 CSV 并生成 Tags
|
||||
*/
|
||||
const WpToolPage: React.FC = () => {
|
||||
// 状态管理
|
||||
|
|
@ -285,9 +285,9 @@ const WpToolPage: React.FC = () => {
|
|||
* @param {File} uploadedFile - 用户上传的文件
|
||||
*/
|
||||
const handleFileUpload = (uploadedFile: File) => {
|
||||
// 检查文件类型,虽然 xlsx 库更宽容,但最好还是保留基本验证
|
||||
// 检查文件类型,虽然 xlsx 库更宽容,但最好还是保留基本验证
|
||||
if (!uploadedFile.name.match(/\.(csv|xlsx|xls)$/)) {
|
||||
message.error('请上传 CSV 或 Excel 格式的文件!');
|
||||
message.error('请上传 CSV 或 Excel 格式的文件!');
|
||||
return false;
|
||||
}
|
||||
setFile(uploadedFile);
|
||||
|
|
@ -302,7 +302,7 @@ const WpToolPage: React.FC = () => {
|
|||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||
|
||||
if (jsonData.length < 2) {
|
||||
message.error('文件为空或缺少表头!');
|
||||
message.error('文件为空或缺少表头!');
|
||||
setCsvData([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -317,17 +317,17 @@ const WpToolPage: React.FC = () => {
|
|||
return rowData;
|
||||
});
|
||||
|
||||
message.success(`成功解析 ${rows.length} 条数据。`);
|
||||
message.success(`成功解析 ${rows.length} 条数据.`);
|
||||
setCsvData(rows);
|
||||
setProcessedData([]); // 清空旧的处理结果
|
||||
} catch (error) {
|
||||
message.error('文件解析失败,请检查文件格式!');
|
||||
message.error('文件解析失败,请检查文件格式!');
|
||||
console.error('File Parse Error:', error);
|
||||
setCsvData([]);
|
||||
}
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
message.error('文件读取失败!');
|
||||
message.error('文件读取失败!');
|
||||
console.error('File Read Error:', error);
|
||||
};
|
||||
reader.readAsBinaryString(uploadedFile);
|
||||
|
|
@ -352,16 +352,16 @@ const WpToolPage: React.FC = () => {
|
|||
const fileName = `products_with_tags_${Date.now()}.xlsx`;
|
||||
XLSX.writeFile(workbook, fileName);
|
||||
|
||||
message.success('下载任务已开始!');
|
||||
message.success('下载任务已开始!');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 核心逻辑:根据配置处理 CSV 数据并生成 Tags
|
||||
* @description 核心逻辑:根据配置处理 CSV 数据并生成 Tags
|
||||
*/
|
||||
const handleProcessData = async () => {
|
||||
// 验证是否已上传并解析了数据
|
||||
if (csvData.length === 0) {
|
||||
message.warning('请先上传并成功解析一个 CSV 文件。');
|
||||
message.warning('请先上传并成功解析一个 CSV 文件.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -400,7 +400,7 @@ const WpToolPage: React.FC = () => {
|
|||
|
||||
setProcessedData(dataWithTags);
|
||||
message.success({
|
||||
content: 'Tags 生成成功!正在自动下载...',
|
||||
content: 'Tags 生成成功!正在自动下载...',
|
||||
key: 'processing',
|
||||
});
|
||||
|
||||
|
|
@ -409,7 +409,7 @@ const WpToolPage: React.FC = () => {
|
|||
|
||||
} catch (error) {
|
||||
message.error({
|
||||
content: '处理失败,请检查配置或文件。',
|
||||
content: '处理失败,请检查配置或文件.',
|
||||
key: 'processing',
|
||||
});
|
||||
console.error('Processing Error:', error);
|
||||
|
|
@ -421,7 +421,7 @@ const WpToolPage: React.FC = () => {
|
|||
return (
|
||||
<PageContainer title="WordPress 产品工具">
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 左侧:配置表单 */}
|
||||
{/* 左侧:配置表单 */}
|
||||
<Col xs={24} md={10}>
|
||||
<Card title="1. 配置映射规则">
|
||||
<ProForm
|
||||
|
|
@ -434,58 +434,58 @@ const WpToolPage: React.FC = () => {
|
|||
name="brands"
|
||||
label="品牌列表"
|
||||
mode="tags"
|
||||
placeholder="请输入品牌,按回车确认"
|
||||
placeholder="请输入品牌,按回车确认"
|
||||
rules={[{ required: true, message: '至少需要一个品牌' }]}
|
||||
tooltip="按品牌名称长度倒序匹配,请将较长的品牌(如 WHITE FOX)放在前面。"
|
||||
tooltip="按品牌名称长度倒序匹配,请将较长的品牌(如 WHITE FOX)放在前面."
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="fruitKeys"
|
||||
label="水果关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="mintKeys"
|
||||
label="薄荷关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="flavorKeys"
|
||||
label="口味白名单"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
tooltip="只有在白名单中的词才会被识别为口味。"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
tooltip="只有在白名单中的词才会被识别为口味."
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="strengthKeys"
|
||||
label="强度关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="sizeKeys"
|
||||
label="尺寸关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="humidityKeys"
|
||||
label="湿度关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="categoryKeys"
|
||||
label="分类关键词"
|
||||
mode="tags"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
placeholder="请输入关键词,按回车确认"
|
||||
/>
|
||||
</ProForm>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:文件上传与操作 */}
|
||||
{/* 右侧:文件上传与操作 */}
|
||||
<Col xs={24} md={14}>
|
||||
<Card title="2. 上传文件并操作">
|
||||
<Upload
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
import { request } from 'umi';
|
||||
|
||||
/** 获取区域列表(分页) GET /area/ */
|
||||
/** 获取区域列表(分页) GET /area/ */
|
||||
export async function areacontrollerGetarealist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.areacontrollerGetarealistParams,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-ignore
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
import * as area from './area';
|
||||
import * as category from './category';
|
||||
import * as customer from './customer';
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ declare namespace API {
|
|||
currentPage?: number;
|
||||
/** 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 关键词(名称或编码) */
|
||||
/** 关键词(名称或编码) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
|
|
@ -443,7 +443,7 @@ declare namespace API {
|
|||
| 'return-approved'
|
||||
| 'return-cancelled';
|
||||
payment_method?: string;
|
||||
/** 仅订阅订单(父订阅订单或包含订阅商品) */
|
||||
/** 仅订阅订单(父订阅订单或包含订阅商品) */
|
||||
isSubscriptionOnly?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -994,7 +994,7 @@ declare namespace API {
|
|||
currentPage?: number;
|
||||
/** 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 关键词(名称或编码) */
|
||||
/** 关键词(名称或编码) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
|
|
@ -1036,7 +1036,7 @@ declare namespace API {
|
|||
| 'return-approved'
|
||||
| 'return-cancelled';
|
||||
payment_method?: string;
|
||||
/** 仅订阅订单(父订阅订单或包含订阅商品) */
|
||||
/** 仅订阅订单(父订阅订单或包含订阅商品) */
|
||||
isSubscriptionOnly?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -1115,7 +1115,7 @@ declare namespace API {
|
|||
sku?: string;
|
||||
/** 按库存点ID排序 */
|
||||
sortPointId?: number;
|
||||
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
|
||||
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
|
||||
order?: Record<string, any>;
|
||||
};
|
||||
|
||||
|
|
@ -1149,7 +1149,7 @@ declare namespace API {
|
|||
| 'pending-cancel';
|
||||
/** 客户邮箱 */
|
||||
customer_email?: string;
|
||||
/** 关键字(订阅ID、邮箱等) */
|
||||
/** 关键字(订阅ID,邮箱等) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
|
|
@ -1383,7 +1383,7 @@ declare namespace API {
|
|||
sku?: string;
|
||||
/** 按库存点ID排序 */
|
||||
sortPointId?: number;
|
||||
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
|
||||
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */
|
||||
order?: Record<string, any>;
|
||||
};
|
||||
|
||||
|
|
@ -1548,7 +1548,7 @@ declare namespace API {
|
|||
billing_interval?: number;
|
||||
customer_id?: number;
|
||||
customer_email?: string;
|
||||
/** 父订单/父订阅ID(如有) */
|
||||
/** 父订单/父订阅ID(如有) */
|
||||
parent_id?: number;
|
||||
start_date?: string;
|
||||
trial_end?: string;
|
||||
|
|
@ -1579,7 +1579,7 @@ declare namespace API {
|
|||
| 'pending-cancel';
|
||||
/** 客户邮箱 */
|
||||
customer_email?: string;
|
||||
/** 关键字(订阅ID、邮箱等) */
|
||||
/** 关键字(订阅ID,邮箱等) */
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue