412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import {
|
|
ActionType,
|
|
DrawerForm,
|
|
ProColumns,
|
|
ProFormDependency,
|
|
ProFormInstance,
|
|
ProFormSelect,
|
|
ProFormSwitch,
|
|
ProFormText,
|
|
ProTable,
|
|
} from '@ant-design/pro-components';
|
|
import { request } from '@umijs/max';
|
|
import { Button, message, Popconfirm, Space, Tag } from 'antd';
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
|
|
// 区域数据项类型
|
|
interface AreaItem {
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
|
interface SiteItem {
|
|
id: number;
|
|
name: string;
|
|
apiUrl?: string;
|
|
type?: 'woocommerce' | 'shopyy';
|
|
skuPrefix?: string;
|
|
isDisabled: number;
|
|
areas?: AreaItem[];
|
|
}
|
|
|
|
// 创建/更新表单的值类型,包含可选的密钥字段
|
|
interface SiteFormValues {
|
|
name: string;
|
|
apiUrl?: string;
|
|
type?: 'woocommerce' | 'shopyy';
|
|
isDisabled?: boolean;
|
|
consumerKey?: string; // WooCommerce REST API 的 consumer key
|
|
consumerSecret?: string; // WooCommerce REST API 的 consumer secret
|
|
token?: string; // Shopyy token
|
|
skuPrefix?: string;
|
|
areas?: string[];
|
|
}
|
|
|
|
const SiteList: React.FC = () => {
|
|
const actionRef = useRef<ActionType>();
|
|
const formRef = useRef<ProFormInstance>();
|
|
const [open, setOpen] = useState(false);
|
|
const [editing, setEditing] = useState<SiteItem | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
if (editing) {
|
|
formRef.current?.setFieldsValue({
|
|
name: editing.name,
|
|
apiUrl: editing.apiUrl,
|
|
type: editing.type,
|
|
skuPrefix: editing.skuPrefix,
|
|
isDisabled: !!editing.isDisabled,
|
|
consumerKey: undefined,
|
|
consumerSecret: undefined,
|
|
token: undefined,
|
|
areas: editing.areas?.map((area) => area.code) ?? [],
|
|
});
|
|
} else {
|
|
formRef.current?.setFieldsValue({
|
|
name: undefined,
|
|
apiUrl: undefined,
|
|
type: 'woocommerce',
|
|
skuPrefix: undefined,
|
|
isDisabled: false,
|
|
consumerKey: undefined,
|
|
consumerSecret: undefined,
|
|
token: undefined,
|
|
});
|
|
}
|
|
}, [open, editing]);
|
|
|
|
// 表格列定义
|
|
const columns: ProColumns<SiteItem>[] = [
|
|
{
|
|
title: 'ID',
|
|
dataIndex: 'id',
|
|
width: 80,
|
|
sorter: true,
|
|
hideInSearch: true,
|
|
},
|
|
{ title: '名称', dataIndex: 'name', width: 220 },
|
|
{ title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true },
|
|
{
|
|
title: 'SKU 前缀',
|
|
dataIndex: 'skuPrefix',
|
|
width: 160,
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '平台',
|
|
dataIndex: 'type',
|
|
width: 140,
|
|
valueType: 'select',
|
|
request: async () => [
|
|
{ label: 'WooCommerce', value: 'woocommerce' },
|
|
{ label: 'Shopyy', value: 'shopyy' },
|
|
],
|
|
},
|
|
{
|
|
title: '区域',
|
|
dataIndex: 'areas',
|
|
width: 200,
|
|
hideInSearch: true,
|
|
render: (_, row) => {
|
|
if (!row.areas || row.areas.length === 0) {
|
|
return <Tag color="blue">全球</Tag>;
|
|
}
|
|
return (
|
|
<Space wrap>
|
|
{row.areas.map((area) => (
|
|
<Tag key={area.code}>{area.name}</Tag>
|
|
))}
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'isDisabled',
|
|
width: 120,
|
|
hideInSearch: true,
|
|
render: (_, row) => (
|
|
<Tag color={row.isDisabled ? 'red' : 'green'}>
|
|
{row.isDisabled ? '已禁用' : '启用中'}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
dataIndex: 'actions',
|
|
width: 240,
|
|
hideInSearch: true,
|
|
render: (_, row) => (
|
|
<Space>
|
|
<Button
|
|
size="small"
|
|
onClick={() => {
|
|
setEditing(row);
|
|
setOpen(true);
|
|
}}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
|
description={
|
|
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
|
|
}
|
|
onConfirm={async () => {
|
|
try {
|
|
await request(`/site/disable/${row.id}`, {
|
|
method: 'PUT',
|
|
data: { disabled: !row.isDisabled },
|
|
});
|
|
message.success('更新成功');
|
|
actionRef.current?.reload();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '更新失败');
|
|
}
|
|
}}
|
|
>
|
|
<Button size="small" type="primary" danger={!row.isDisabled}>
|
|
{row.isDisabled ? '启用' : '禁用'}
|
|
</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
// 表格数据请求
|
|
const tableRequest = async (params: Record<string, any>) => {
|
|
try {
|
|
const { current = 1, pageSize = 10, name, type } = params;
|
|
const resp = await request('/site/list', {
|
|
method: 'GET',
|
|
params: {
|
|
current,
|
|
pageSize,
|
|
keyword: name || undefined,
|
|
type: type || undefined,
|
|
},
|
|
});
|
|
const { success, data, message: errMsg } = resp as any;
|
|
if (!success) throw new Error(errMsg || '获取失败');
|
|
return {
|
|
data: (data?.items ?? []) as SiteItem[],
|
|
total: data?.total ?? 0,
|
|
success: true,
|
|
};
|
|
} catch (e: any) {
|
|
message.error(e?.message || '获取失败');
|
|
return { data: [], total: 0, success: false };
|
|
}
|
|
};
|
|
|
|
// 提交创建/更新逻辑;编辑时未填写密钥则不提交(保持原值)
|
|
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/空
|
|
...(values.name ? { name: values.name } : {}),
|
|
...(apiUrl ? { apiUrl: apiUrl } : {}),
|
|
...(values.type ? { type: values.type } : {}),
|
|
...(typeof values.isDisabled === 'boolean'
|
|
? { isDisabled: values.isDisabled }
|
|
: {}),
|
|
...(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 for WooCommerce');
|
|
}
|
|
}
|
|
|
|
await request('/site/create', {
|
|
method: 'POST',
|
|
data: {
|
|
name: values.name,
|
|
apiUrl: apiUrl,
|
|
type: values.type || 'woocommerce',
|
|
consumerKey: isShopyy ? undefined : values.consumerKey,
|
|
consumerSecret: isShopyy ? undefined : values.consumerSecret,
|
|
token: isShopyy ? values.token : undefined,
|
|
skuPrefix: values.skuPrefix,
|
|
areas: values.areas ?? [],
|
|
},
|
|
});
|
|
}
|
|
message.success('提交成功');
|
|
setOpen(false);
|
|
setEditing(null);
|
|
actionRef.current?.reload();
|
|
return true;
|
|
} catch (e: any) {
|
|
message.error(e?.message || '提交失败');
|
|
return false;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ProTable<SiteItem>
|
|
actionRef={actionRef}
|
|
rowKey="id"
|
|
columns={columns}
|
|
request={tableRequest}
|
|
toolBarRender={() => [
|
|
<Button
|
|
key="new"
|
|
type="primary"
|
|
onClick={() => {
|
|
setEditing(null);
|
|
setOpen(true);
|
|
}}
|
|
>
|
|
新增站点
|
|
</Button>,
|
|
// 同步包括 orders subscriptions 等等
|
|
<Button key='new' disabled type='primary' onClick={()=> {
|
|
}}>
|
|
同步站点数据
|
|
</Button>
|
|
]}
|
|
/>
|
|
|
|
<DrawerForm<SiteFormValues>
|
|
title={editing ? '编辑站点' : '新增站点'}
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
formRef={formRef}
|
|
onFinish={handleSubmit}
|
|
>
|
|
{/* 站点名称,必填 */}
|
|
<ProFormText
|
|
name="name"
|
|
label="站点名称"
|
|
placeholder="例如:本地商店"
|
|
rules={[{ required: true, message: '站点名称为必填项' }]}
|
|
/>
|
|
{/* 区域选择 */}
|
|
<ProFormSelect
|
|
name="areas"
|
|
label="区域"
|
|
mode="multiple"
|
|
placeholder="留空表示全球"
|
|
request={async () => {
|
|
try {
|
|
const resp = await request('/area', {
|
|
method: 'GET',
|
|
params: { pageSize: 1000 },
|
|
});
|
|
if (resp.success) {
|
|
return resp.data.list.map((area: AreaItem) => ({
|
|
label: area.name,
|
|
value: area.code,
|
|
}));
|
|
}
|
|
return [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}}
|
|
/>
|
|
{/* 平台类型选择 */}
|
|
<ProFormSelect
|
|
name="type"
|
|
label="平台"
|
|
options={[
|
|
{ label: 'WooCommerce', value: 'woocommerce' },
|
|
{ label: 'Shopyy', value: 'shopyy' },
|
|
]}
|
|
/>
|
|
|
|
<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 */}
|
|
<ProFormText
|
|
name="consumerSecret"
|
|
label="Secret"
|
|
placeholder={editing ? '留空表示不修改' : '必填'}
|
|
rules={editing ? [] : [{ required: true, message: 'Secret 为必填项' }]}
|
|
/>
|
|
</>
|
|
);
|
|
}}
|
|
</ProFormDependency>
|
|
|
|
{editing && <ProFormSwitch name="isDisabled" label="禁用" />}
|
|
|
|
<ProFormText
|
|
name="skuPrefix"
|
|
label="SKU 前缀"
|
|
placeholder={editing ? '留空表示不修改' : '可选'}
|
|
/>
|
|
</DrawerForm>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SiteList;
|