449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import {
|
|||
|
|
Card,
|
|||
|
|
Table,
|
|||
|
|
Button,
|
|||
|
|
Space,
|
|||
|
|
Modal,
|
|||
|
|
Form,
|
|||
|
|
Input,
|
|||
|
|
Select,
|
|||
|
|
Tag,
|
|||
|
|
message,
|
|||
|
|
Row,
|
|||
|
|
Col,
|
|||
|
|
Statistic,
|
|||
|
|
Badge,
|
|||
|
|
Typography,
|
|||
|
|
Divider,
|
|||
|
|
Alert,
|
|||
|
|
Spin,
|
|||
|
|
} from 'antd';
|
|||
|
|
import {
|
|||
|
|
PlusOutlined,
|
|||
|
|
CheckCircleOutlined,
|
|||
|
|
CloseCircleOutlined,
|
|||
|
|
SyncOutlined,
|
|||
|
|
ShopOutlined,
|
|||
|
|
LinkOutlined,
|
|||
|
|
ApiOutlined,
|
|||
|
|
ReloadOutlined,
|
|||
|
|
DeleteOutlined,
|
|||
|
|
} from '@ant-design/icons';
|
|||
|
|
import type { ColumnsType } from 'antd/es/table';
|
|||
|
|
import { platformAuthDataSource } from '@/services/platformAuthDataSource';
|
|||
|
|
import { PlatformAccount, PlatformAccountStats, Platform } from '@/types/platform';
|
|||
|
|
|
|||
|
|
const { Title, Text } = Typography;
|
|||
|
|
const { Option } = Select;
|
|||
|
|
|
|||
|
|
const PLATFORM_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
|||
|
|
// TikTok系列
|
|||
|
|
TIKTOK: { color: 'cyan', text: 'TikTok', icon: <ShopOutlined /> },
|
|||
|
|
TIKTOK_FULL: { color: 'cyan', text: 'TikTok全托管', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// Shopee系列
|
|||
|
|
SHOPEE: { color: 'red', text: 'Shopee', icon: <ShopOutlined /> },
|
|||
|
|
SHOPEE_FULL: { color: 'red', text: 'Shopee全托管', icon: <ShopOutlined /> },
|
|||
|
|
SHOPEE_LIGHT: { color: 'red', text: 'Shopee轻出海', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// Lazada系列
|
|||
|
|
LAZADA: { color: 'purple', text: 'Lazada', icon: <ShopOutlined /> },
|
|||
|
|
LAZADA_FULL: { color: 'purple', text: 'Lazada全托管', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// Temu
|
|||
|
|
TEMU_FULL: { color: 'green', text: 'Temu全托管', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// SHEIN系列
|
|||
|
|
SHEIN: { color: 'pink', text: 'SHEIN', icon: <ShopOutlined /> },
|
|||
|
|
SHEIN_HALF: { color: 'pink', text: 'SHEIN半托管', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// 其他平台
|
|||
|
|
OZON: { color: 'yellow', text: 'Ozon', icon: <ShopOutlined /> },
|
|||
|
|
YANDEX: { color: 'blue', text: 'Yandex', icon: <ShopOutlined /> },
|
|||
|
|
ALIEXPRESS: { color: 'orange', text: 'AliExpress', icon: <ShopOutlined /> },
|
|||
|
|
ALIEXPRESS_HALF: { color: 'orange', text: '速卖通半托管', icon: <ShopOutlined /> },
|
|||
|
|
ALIEXPRESS_POP: { color: 'orange', text: '速卖通本土POP', icon: <ShopOutlined /> },
|
|||
|
|
COUPANG: { color: 'red', text: 'Coupang', icon: <ShopOutlined /> },
|
|||
|
|
WALMART: { color: 'blue', text: 'Walmart', icon: <ShopOutlined /> },
|
|||
|
|
WILDBERRIES: { color: 'purple', text: 'Wildberries', icon: <ShopOutlined /> },
|
|||
|
|
ALLEGRO: { color: 'green', text: 'Allegro', icon: <ShopOutlined /> },
|
|||
|
|
MERCADO_LIBRE: { color: 'yellow', text: 'Mercado Libre', icon: <ShopOutlined /> },
|
|||
|
|
JUMIA: { color: 'blue', text: 'Jumia', icon: <ShopOutlined /> },
|
|||
|
|
JOOM: { color: 'purple', text: 'Joom', icon: <ShopOutlined /> },
|
|||
|
|
AMAZON: { color: 'orange', text: 'Amazon', icon: <ShopOutlined /> },
|
|||
|
|
WISH: { color: 'blue', text: 'Wish', icon: <ShopOutlined /> },
|
|||
|
|
EMAG: { color: 'green', text: 'eMAG', icon: <ShopOutlined /> },
|
|||
|
|
MIRAVIA: { color: 'pink', text: 'Miravia', icon: <ShopOutlined /> },
|
|||
|
|
DARAZ: { color: 'blue', text: 'Daraz', icon: <ShopOutlined /> },
|
|||
|
|
JOYBUY: { color: 'red', text: 'Joybuy', icon: <ShopOutlined /> },
|
|||
|
|
ALIBABA: { color: 'orange', text: 'Alibaba', icon: <ShopOutlined /> },
|
|||
|
|
QOO10: { color: 'red', text: 'Qoo10', icon: <ShopOutlined /> },
|
|||
|
|
SHOPIFY: { color: 'green', text: 'Shopify', icon: <ShopOutlined /> },
|
|||
|
|
SHOPLAZZA: { color: 'blue', text: 'Shoplazza', icon: <ShopOutlined /> },
|
|||
|
|
SHOPYY_V1: { color: 'purple', text: 'SHOPYY v1.0', icon: <ShopOutlined /> },
|
|||
|
|
SHOPYY_V2: { color: 'purple', text: 'SHOPYY v2.0', icon: <ShopOutlined /> },
|
|||
|
|
SHOPLINE: { color: 'green', text: 'SHOPLINE', icon: <ShopOutlined /> },
|
|||
|
|
GREATBOSS: { color: 'blue', text: 'GreatBoss', icon: <ShopOutlined /> },
|
|||
|
|
OTHER: { color: 'default', text: '其他', icon: <ShopOutlined /> },
|
|||
|
|
|
|||
|
|
// 原有平台
|
|||
|
|
EBAY: { color: 'blue', text: 'eBay', icon: <ShopOutlined /> },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
|
|||
|
|
ACTIVE: { color: 'success', text: '已连接' },
|
|||
|
|
INACTIVE: { color: 'default', text: '未连接' },
|
|||
|
|
EXPIRED: { color: 'warning', text: '已过期' },
|
|||
|
|
ERROR: { color: 'error', text: '错误' },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const PlatformAuth: React.FC = () => {
|
|||
|
|
const [accounts, setAccounts] = useState<PlatformAccount[]>([]);
|
|||
|
|
const [stats, setStats] = useState<PlatformAccountStats>({ total: 0, active: 0, inactive: 0, expired: 0, error: 0 });
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|||
|
|
const [form] = Form.useForm();
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadData();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const loadData = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const [accountList, accountStats] = await Promise.all([
|
|||
|
|
platformAuthDataSource.list(),
|
|||
|
|
platformAuthDataSource.getStats(),
|
|||
|
|
]);
|
|||
|
|
setAccounts(accountList);
|
|||
|
|
setStats(accountStats);
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(`加载失败: ${error.message}`);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleConnect = async (values: { platform: Platform; accountName: string; accountId: string; shopId?: string }) => {
|
|||
|
|
try {
|
|||
|
|
const result = await platformAuthDataSource.connect({
|
|||
|
|
platform: values.platform,
|
|||
|
|
accountName: values.accountName,
|
|||
|
|
accountId: values.accountId,
|
|||
|
|
shopId: values.shopId,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (result.authUrl) {
|
|||
|
|
message.success('正在跳转到授权页面...');
|
|||
|
|
window.open(result.authUrl, '_blank');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setModalVisible(false);
|
|||
|
|
form.resetFields();
|
|||
|
|
loadData();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(`连接失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDisconnect = async (id: string) => {
|
|||
|
|
Modal.confirm({
|
|||
|
|
title: '确认断开连接?',
|
|||
|
|
content: '断开后将无法同步该平台的数据',
|
|||
|
|
onOk: async () => {
|
|||
|
|
try {
|
|||
|
|
await platformAuthDataSource.disconnect(id);
|
|||
|
|
message.success('已断开连接');
|
|||
|
|
loadData();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(`断开失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRefresh = async (id: string) => {
|
|||
|
|
try {
|
|||
|
|
message.loading({ content: '正在刷新授权...', key: 'refresh' });
|
|||
|
|
const result = await platformAuthDataSource.refresh(id);
|
|||
|
|
if (result.success) {
|
|||
|
|
message.success({ content: '授权已刷新', key: 'refresh' });
|
|||
|
|
loadData();
|
|||
|
|
} else {
|
|||
|
|
message.error({ content: result.message, key: 'refresh' });
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error({ content: `刷新失败: ${error.message}`, key: 'refresh' });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDelete = async (id: string) => {
|
|||
|
|
Modal.confirm({
|
|||
|
|
title: '确认删除?',
|
|||
|
|
content: '删除后需要重新授权才能使用该平台',
|
|||
|
|
okType: 'danger',
|
|||
|
|
onOk: async () => {
|
|||
|
|
try {
|
|||
|
|
await platformAuthDataSource.delete(id);
|
|||
|
|
message.success('已删除');
|
|||
|
|
loadData();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(`删除失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const columns: ColumnsType<PlatformAccount> = [
|
|||
|
|
{
|
|||
|
|
title: '平台',
|
|||
|
|
dataIndex: 'platform',
|
|||
|
|
key: 'platform',
|
|||
|
|
width: 150,
|
|||
|
|
render: (platform: string) => {
|
|||
|
|
const config = PLATFORM_CONFIG[platform] || { color: 'default', text: platform, icon: <ShopOutlined /> };
|
|||
|
|
return (
|
|||
|
|
<Space>
|
|||
|
|
{config.icon}
|
|||
|
|
<Tag color={config.color}>{config.text}</Tag>
|
|||
|
|
</Space>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '店铺名称',
|
|||
|
|
dataIndex: 'accountName',
|
|||
|
|
key: 'accountName',
|
|||
|
|
width: 200,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '账号ID',
|
|||
|
|
dataIndex: 'accountId',
|
|||
|
|
key: 'accountId',
|
|||
|
|
width: 150,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '状态',
|
|||
|
|
dataIndex: 'status',
|
|||
|
|
key: 'status',
|
|||
|
|
width: 120,
|
|||
|
|
render: (status: string) => {
|
|||
|
|
const config = STATUS_CONFIG[status] || { color: 'default', text: status };
|
|||
|
|
return <Badge status={config.color as any} text={config.text} />;
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '最后同步',
|
|||
|
|
dataIndex: 'lastSyncAt',
|
|||
|
|
key: 'lastSyncAt',
|
|||
|
|
width: 180,
|
|||
|
|
render: (lastSyncAt?: string) => lastSyncAt ? new Date(lastSyncAt).toLocaleString() : '-',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '授权过期',
|
|||
|
|
dataIndex: 'expiresAt',
|
|||
|
|
key: 'expiresAt',
|
|||
|
|
width: 180,
|
|||
|
|
render: (expiresAt?: string) => expiresAt ? new Date(expiresAt).toLocaleString() : '-',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '操作',
|
|||
|
|
key: 'action',
|
|||
|
|
width: 220,
|
|||
|
|
render: (_, record) => (
|
|||
|
|
<Space>
|
|||
|
|
{record.status === 'ACTIVE' && (
|
|||
|
|
<>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<ReloadOutlined />}
|
|||
|
|
onClick={() => handleRefresh(record.id)}
|
|||
|
|
>
|
|||
|
|
刷新
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
danger
|
|||
|
|
onClick={() => handleDisconnect(record.id)}
|
|||
|
|
>
|
|||
|
|
断开
|
|||
|
|
</Button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
{record.status === 'EXPIRED' && (
|
|||
|
|
<>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<SyncOutlined />}
|
|||
|
|
onClick={() => handleRefresh(record.id)}
|
|||
|
|
>
|
|||
|
|
重新授权
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
danger
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={() => handleDelete(record.id)}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
{record.status === 'INACTIVE' && (
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<LinkOutlined />}
|
|||
|
|
onClick={() => {
|
|||
|
|
form.setFieldsValue({
|
|||
|
|
platform: record.platform,
|
|||
|
|
accountName: record.accountName,
|
|||
|
|
accountId: record.accountId,
|
|||
|
|
});
|
|||
|
|
setModalVisible(true);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
去授权
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
{record.status === 'ERROR' && (
|
|||
|
|
<>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<SyncOutlined />}
|
|||
|
|
onClick={() => handleRefresh(record.id)}
|
|||
|
|
>
|
|||
|
|
重试
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
danger
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
onClick={() => handleDelete(record.id)}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<Title level={4}>平台授权管理</Title>
|
|||
|
|
<Text type="secondary">管理各电商平台的API授权,确保数据同步正常</Text>
|
|||
|
|
|
|||
|
|
<Divider />
|
|||
|
|
|
|||
|
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
|||
|
|
<Col span={6}>
|
|||
|
|
<Card>
|
|||
|
|
<Statistic title="总平台数" value={stats.total} prefix={<ShopOutlined />} />
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
<Col span={6}>
|
|||
|
|
<Card>
|
|||
|
|
<Statistic
|
|||
|
|
title="已连接"
|
|||
|
|
value={stats.active}
|
|||
|
|
valueStyle={{ color: '#3f8600' }}
|
|||
|
|
prefix={<CheckCircleOutlined />}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
<Col span={6}>
|
|||
|
|
<Card>
|
|||
|
|
<Statistic
|
|||
|
|
title="已过期"
|
|||
|
|
value={stats.expired}
|
|||
|
|
valueStyle={{ color: '#faad14' }}
|
|||
|
|
prefix={<CloseCircleOutlined />}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
<Col span={6}>
|
|||
|
|
<Card>
|
|||
|
|
<Statistic
|
|||
|
|
title="错误"
|
|||
|
|
value={stats.error}
|
|||
|
|
valueStyle={{ color: '#cf1322' }}
|
|||
|
|
prefix={<SyncOutlined spin />}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
|
|||
|
|
{stats.expired > 0 && (
|
|||
|
|
<Alert
|
|||
|
|
message="部分平台授权已过期,请及时刷新授权以避免数据同步中断"
|
|||
|
|
type="warning"
|
|||
|
|
showIcon
|
|||
|
|
style={{ marginBottom: 16 }}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Card>
|
|||
|
|
<div style={{ marginBottom: 16 }}>
|
|||
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalVisible(true)}>
|
|||
|
|
添加平台
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Spin spinning={loading}>
|
|||
|
|
<Table
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={accounts}
|
|||
|
|
rowKey="id"
|
|||
|
|
pagination={{ pageSize: 10 }}
|
|||
|
|
/>
|
|||
|
|
</Spin>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title="添加平台授权"
|
|||
|
|
open={modalVisible}
|
|||
|
|
onCancel={() => {
|
|||
|
|
setModalVisible(false);
|
|||
|
|
form.resetFields();
|
|||
|
|
}}
|
|||
|
|
onOk={() => {
|
|||
|
|
form.validateFields().then(values => {
|
|||
|
|
handleConnect(values);
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Form form={form} layout="vertical">
|
|||
|
|
<Form.Item name="platform" label="平台" rules={[{ required: true, message: '请选择平台' }]}>
|
|||
|
|
<Select placeholder="选择平台">
|
|||
|
|
{Object.entries(PLATFORM_CONFIG).map(([key, config]) => (
|
|||
|
|
<Option key={key} value={key}>
|
|||
|
|
<Space>
|
|||
|
|
{config.icon}
|
|||
|
|
{config.text}
|
|||
|
|
</Space>
|
|||
|
|
</Option>
|
|||
|
|
))}
|
|||
|
|
</Select>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item name="accountName" label="店铺名称" rules={[{ required: true, message: '请输入店铺名称' }]}>
|
|||
|
|
<Input placeholder="输入店铺名称" />
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item name="accountId" label="账号ID" rules={[{ required: true, message: '请输入账号ID' }]}>
|
|||
|
|
<Input placeholder="输入平台账号ID" />
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item name="shopId" label="店铺ID(可选)">
|
|||
|
|
<Input placeholder="输入店铺ID" />
|
|||
|
|
</Form.Item>
|
|||
|
|
</Form>
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default PlatformAuth;
|