forked from yoone/WEB
450 lines
12 KiB
TypeScript
450 lines
12 KiB
TypeScript
import { ordercontrollerSyncorders } from '@/servers/api/order';
|
|
import {
|
|
sitecontrollerCreate,
|
|
sitecontrollerDisable,
|
|
sitecontrollerList,
|
|
sitecontrollerUpdate,
|
|
} from '@/servers/api/site';
|
|
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
|
|
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
|
import { ActionType, ProColumns, ProTable, DrawerForm, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
|
|
import { Button, message, notification, Popconfirm, Space, Tag, Form } from 'antd';
|
|
import React, { useRef, useState, useEffect } from 'react';
|
|
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
|
import * as countries from 'i18n-iso-countries';
|
|
import zhCN from 'i18n-iso-countries/langs/zh';
|
|
|
|
// 区域数据项类型
|
|
interface AreaItem {
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
// 仓库数据项类型
|
|
interface StockPointItem {
|
|
id: number;
|
|
name: string;
|
|
}
|
|
|
|
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
|
export interface SiteItem {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
apiUrl?: string;
|
|
websiteUrl?: string; // 网站地址
|
|
type?: 'woocommerce' | 'shopyy';
|
|
skuPrefix?: string;
|
|
isDisabled: number;
|
|
areas?: AreaItem[];
|
|
stockPoints?: StockPointItem[];
|
|
}
|
|
|
|
const SiteList: React.FC = () => {
|
|
const actionRef = useRef<ActionType>();
|
|
const [open, setOpen] = useState(false);
|
|
const [editing, setEditing] = useState<SiteItem | null>(null);
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
const [batchEditOpen, setBatchEditOpen] = useState(false);
|
|
const [batchEditForm] = Form.useForm();
|
|
countries.registerLocale(zhCN);
|
|
|
|
|
|
const handleSync = async (ids: number[]) => {
|
|
if (!ids.length) return;
|
|
const hide = message.loading('正在同步...', 0);
|
|
|
|
const stats = {
|
|
orders: { success: 0, fail: 0 },
|
|
subscriptions: { success: 0, fail: 0 },
|
|
};
|
|
|
|
try {
|
|
for (const id of ids) {
|
|
// 同步订单
|
|
const orderRes = await ordercontrollerSyncorders({ siteId: id });
|
|
if (orderRes.success) {
|
|
stats.orders.success += 1;
|
|
} else {
|
|
stats.orders.fail += 1;
|
|
}
|
|
|
|
// 同步订阅
|
|
const subRes = await subscriptioncontrollerSync({ siteId: id });
|
|
if (subRes.success) {
|
|
stats.subscriptions.success += 1;
|
|
} else {
|
|
stats.subscriptions.fail += 1;
|
|
}
|
|
}
|
|
hide();
|
|
|
|
notification.success({
|
|
message: '同步完成',
|
|
description: (
|
|
<div>
|
|
<p>
|
|
订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}
|
|
</p>
|
|
<p>
|
|
订阅: 成功 {stats.subscriptions.success}, 失败{' '}
|
|
{stats.subscriptions.fail}
|
|
</p>
|
|
</div>
|
|
),
|
|
duration: null, // 不自动关闭
|
|
});
|
|
|
|
setSelectedRowKeys([]);
|
|
actionRef.current?.reload();
|
|
} catch (error: any) {
|
|
hide();
|
|
message.error(error.message || '同步失败');
|
|
}
|
|
};
|
|
|
|
// 获取所有国家/地区的选项
|
|
const getCountryOptions = () => {
|
|
// 获取所有国家的 ISO 代码
|
|
const countryCodes = countries.getAlpha2Codes();
|
|
// 将国家代码转换为选项数组
|
|
return Object.keys(countryCodes).map((code) => ({
|
|
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
|
value: code,
|
|
}));
|
|
};
|
|
|
|
// 处理批量编辑提交
|
|
const handleBatchEditFinish = async (values: any) => {
|
|
if (!selectedRowKeys.length) return;
|
|
const hide = message.loading('正在批量更新...', 0);
|
|
|
|
try {
|
|
// 遍历所有选中的站点 ID
|
|
for (const id of selectedRowKeys) {
|
|
// 构建更新数据对象,只包含用户填写了值的字段
|
|
const updateData: any = {};
|
|
|
|
// 如果用户选择了区域,则更新区域
|
|
if (values.areas && values.areas.length > 0) {
|
|
updateData.areas = values.areas;
|
|
}
|
|
|
|
// 如果用户选择了仓库,则更新仓库
|
|
if (values.stockPointIds && values.stockPointIds.length > 0) {
|
|
updateData.stockPointIds = values.stockPointIds;
|
|
}
|
|
|
|
// 如果用户设置了禁用状态,则更新状态
|
|
if (values.isDisabled !== undefined) {
|
|
updateData.isDisabled = values.isDisabled;
|
|
}
|
|
|
|
// 如果有需要更新的字段,则调用更新接口
|
|
if (Object.keys(updateData).length > 0) {
|
|
await sitecontrollerUpdate({ id: String(id) }, updateData);
|
|
}
|
|
}
|
|
|
|
hide();
|
|
message.success('批量更新成功');
|
|
setBatchEditOpen(false);
|
|
setSelectedRowKeys([]);
|
|
batchEditForm.resetFields();
|
|
actionRef.current?.reload();
|
|
} catch (error: any) {
|
|
hide();
|
|
message.error(error.message || '批量更新失败');
|
|
}
|
|
};
|
|
|
|
// 当批量编辑弹窗打开时,重置表单
|
|
useEffect(() => {
|
|
if (batchEditOpen) {
|
|
batchEditForm.resetFields();
|
|
}
|
|
}, [batchEditOpen, batchEditForm]);
|
|
|
|
// 表格列定义
|
|
const columns: ProColumns<SiteItem>[] = [
|
|
{
|
|
title: 'ID',
|
|
dataIndex: 'id',
|
|
width: 80,
|
|
sorter: true,
|
|
hideInSearch: true,
|
|
},
|
|
{ title: '名称', dataIndex: 'name', width: 220 },
|
|
{ title: '描述', dataIndex: 'description', width: 220, hideInSearch: true },
|
|
{ title: 'API 地址', dataIndex: 'apiUrl', width: 280, hideInSearch: true },
|
|
{
|
|
title: '网站地址',
|
|
dataIndex: 'websiteUrl',
|
|
width: 280,
|
|
hideInSearch: true,
|
|
render: (text) => (
|
|
<a href={text as string} target="_blank" rel="noopener noreferrer">
|
|
{text}
|
|
</a>
|
|
),
|
|
},
|
|
{
|
|
title: 'SKU 前缀',
|
|
dataIndex: 'skuPrefix',
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '平台',
|
|
dataIndex: 'type',
|
|
valueType: 'select',
|
|
request: async () => [
|
|
{ label: 'WooCommerce', value: 'woocommerce' },
|
|
{ label: 'Shopyy', value: 'shopyy' },
|
|
],
|
|
},
|
|
{
|
|
// 地区列配置
|
|
title: "地区",
|
|
dataIndex: "areas",
|
|
hideInSearch: true,
|
|
render: (_, row) => {
|
|
// 如果没有关联地区,显示"全局"标签
|
|
if (!row.areas || row.areas.length === 0) {
|
|
return <Tag color="default">全局</Tag>;
|
|
}
|
|
// 遍历显示所有关联的地区名称
|
|
return (
|
|
<Space wrap>
|
|
{row.areas.map((area) => (
|
|
<Tag color="geekblue" key={area.code}>
|
|
{area.name}
|
|
</Tag>
|
|
))}
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '关联仓库',
|
|
dataIndex: 'stockPoints',
|
|
hideInSearch: true,
|
|
render: (_, row) => {
|
|
if (!row.stockPoints || row.stockPoints.length === 0) {
|
|
return <Tag>无</Tag>;
|
|
}
|
|
return (
|
|
<Space wrap>
|
|
{row.stockPoints.map((sp) => (
|
|
<Tag color="blue" key={sp.id}>
|
|
{sp.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,
|
|
fixed: 'right',
|
|
hideInSearch: true,
|
|
render: (_, row) => (
|
|
<Space>
|
|
<Button
|
|
size="small"
|
|
type="primary"
|
|
onClick={() => handleSync([row.id])}
|
|
>
|
|
同步
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
onClick={() => {
|
|
setEditing(row);
|
|
setOpen(true);
|
|
}}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
|
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
|
onConfirm={async () => {
|
|
try {
|
|
await sitecontrollerDisable(
|
|
{ id: String(row.id) },
|
|
{ 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, pageSize, name, type } = params;
|
|
const resp = await sitecontrollerList({
|
|
current,
|
|
pageSize,
|
|
keyword: name || undefined,
|
|
type: type || undefined,
|
|
});
|
|
// 假设 resp 直接就是后端返回的结构,包含 items 和 total
|
|
return {
|
|
data: (resp?.data?.items ?? []) as SiteItem[],
|
|
total: resp?.data?.total ?? 0,
|
|
success: true,
|
|
};
|
|
} catch (e: any) {
|
|
message.error(e?.message || '获取失败');
|
|
return { data: [], total: 0, success: false };
|
|
}
|
|
};
|
|
|
|
const handleFinish = async (values: any) => {
|
|
try {
|
|
if (editing) {
|
|
await sitecontrollerUpdate({ id: String(editing.id) }, values);
|
|
message.success('更新成功');
|
|
} else {
|
|
await sitecontrollerCreate(values);
|
|
message.success('创建成功');
|
|
}
|
|
setOpen(false);
|
|
actionRef.current?.reload();
|
|
return true;
|
|
} catch (error: any) {
|
|
message.error(error.message || '操作失败');
|
|
return false;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ProTable<SiteItem>
|
|
scroll={{ x: 'max-content' }}
|
|
actionRef={actionRef}
|
|
rowKey="id"
|
|
columns={columns}
|
|
request={tableRequest}
|
|
rowSelection={{
|
|
selectedRowKeys,
|
|
onChange: setSelectedRowKeys,
|
|
}}
|
|
pagination={{
|
|
defaultPageSize: 20,
|
|
showSizeChanger: true,
|
|
showQuickJumper: true,
|
|
}}
|
|
toolBarRender={() => [
|
|
<Button
|
|
type="primary"
|
|
onClick={() => {
|
|
setEditing(null);
|
|
setOpen(true);
|
|
}}
|
|
>
|
|
新建站点
|
|
</Button>,
|
|
<Button
|
|
disabled={!selectedRowKeys.length}
|
|
onClick={() => setBatchEditOpen(true)}
|
|
>
|
|
批量编辑
|
|
</Button>,
|
|
<Button
|
|
disabled={!selectedRowKeys.length}
|
|
onClick={() => handleSync(selectedRowKeys as number[])}
|
|
>
|
|
批量同步
|
|
</Button>,
|
|
]}
|
|
/>
|
|
|
|
<EditSiteForm
|
|
open={open}
|
|
onOpenChange={(visible) => {
|
|
setOpen(visible);
|
|
if (!visible) {
|
|
setEditing(null);
|
|
}
|
|
}}
|
|
initialValues={editing}
|
|
isEdit={!!editing}
|
|
onFinish={handleFinish}
|
|
/>
|
|
|
|
{/* 批量编辑弹窗 */}
|
|
<DrawerForm
|
|
title={`批量编辑站点 (${selectedRowKeys.length} 个)`}
|
|
form={batchEditForm}
|
|
open={batchEditOpen}
|
|
onOpenChange={setBatchEditOpen}
|
|
onFinish={handleBatchEditFinish}
|
|
layout="vertical"
|
|
>
|
|
<ProFormSelect
|
|
name="areas"
|
|
label="区域"
|
|
mode="multiple"
|
|
placeholder="请选择区域(留空表示不修改)"
|
|
showSearch
|
|
filterOption={(input, option) =>
|
|
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
|
}
|
|
options={getCountryOptions()}
|
|
/>
|
|
<ProFormSelect
|
|
name="stockPointIds"
|
|
label="关联仓库"
|
|
mode="multiple"
|
|
placeholder="请选择关联仓库(留空表示不修改)"
|
|
request={async () => {
|
|
// 从后端接口获取仓库数据
|
|
const res = await stockcontrollerGetallstockpoints();
|
|
// 使用可选链和空值合并运算符来安全地处理可能未定义的数据
|
|
return (
|
|
res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ??
|
|
[]
|
|
);
|
|
}}
|
|
/>
|
|
<ProFormSwitch
|
|
name="isDisabled"
|
|
label="是否禁用"
|
|
fieldProps={{
|
|
checkedChildren: '是',
|
|
unCheckedChildren: '否',
|
|
}}
|
|
/>
|
|
</DrawerForm>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SiteList;
|