refactor: 优化代码结构并修复类型问题
- 移除未使用的TabPane组件 - 修复类型定义和导入方式 - 优化mock数据源的环境变量判断逻辑 - 更新文档结构并归档旧文件 - 添加新的UI组件和Memo组件 - 调整API路径和响应处理
This commit is contained in:
@@ -36,12 +36,57 @@ const { Option } = Select;
|
||||
const { Password } = Input;
|
||||
|
||||
const PLATFORM_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
AMAZON: { color: 'orange', text: 'Amazon', icon: <ShopOutlined /> },
|
||||
EBAY: { color: 'blue', text: 'eBay', icon: <ShopOutlined /> },
|
||||
SHOPIFY: { color: 'green', text: 'Shopify', icon: <ShopOutlined /> },
|
||||
// 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 }> = {
|
||||
|
||||
448
dashboard/src/pages/Settings/PlatformAuth.tsx
Normal file
448
dashboard/src/pages/Settings/PlatformAuth.tsx
Normal file
@@ -0,0 +1,448 @@
|
||||
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;
|
||||
355
dashboard/src/pages/Settings/ServiceManager.tsx
Normal file
355
dashboard/src/pages/Settings/ServiceManager.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
message,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Badge,
|
||||
Typography,
|
||||
Divider,
|
||||
Alert,
|
||||
Spin,
|
||||
Tabs,
|
||||
Modal,
|
||||
Progress,
|
||||
} from 'antd';
|
||||
import type { TabsProps } from 'antd';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
DashboardOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { serviceManagerDataSource, ServiceInfo, ServiceStats, ServicePreset } from '@/services/serviceManagerDataSource';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const MEMORY_IMPACT_COLORS: Record<string, string> = {
|
||||
low: 'green',
|
||||
medium: 'orange',
|
||||
high: 'red',
|
||||
};
|
||||
|
||||
const MEMORY_IMPACT_TEXT: Record<string, string> = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
};
|
||||
|
||||
const ServiceManager: React.FC = () => {
|
||||
const [services, setServices] = useState<ServiceInfo[]>([]);
|
||||
const [groupedServices, setGroupedServices] = useState<Record<string, ServiceInfo[]>>({});
|
||||
const [stats, setStats] = useState<ServiceStats | null>(null);
|
||||
const [presets, setPresets] = useState<ServicePreset[]>([]);
|
||||
const [categories, setCategories] = useState<Record<string, { label: string; color: string }>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [presetModalVisible, setPresetModalVisible] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [listData, presetsData, categoriesData] = await Promise.all([
|
||||
serviceManagerDataSource.list(),
|
||||
serviceManagerDataSource.getPresets(),
|
||||
serviceManagerDataSource.getCategories(),
|
||||
]);
|
||||
setServices(listData.services);
|
||||
setGroupedServices(listData.groupedServices);
|
||||
setStats(listData.stats);
|
||||
setPresets(presetsData);
|
||||
setCategories(categoriesData);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
message.error(`加载失败: ${errorMessage}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async (name: string, enabled: boolean) => {
|
||||
try {
|
||||
const result = await serviceManagerDataSource.toggle(name, enabled);
|
||||
message.success(result.message);
|
||||
loadData();
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
message.error(`操作失败: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyPreset = async (preset: string) => {
|
||||
try {
|
||||
const result = await serviceManagerDataSource.applyPreset(preset);
|
||||
message.success(result.message);
|
||||
setPresetModalVisible(false);
|
||||
loadData();
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
message.error(`应用预设失败: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchToggle = async (category: string, enabled: boolean) => {
|
||||
const categoryServices = groupedServices[category] || [];
|
||||
const serviceNames = categoryServices.map(s => s.name);
|
||||
|
||||
Modal.confirm({
|
||||
title: `确认${enabled ? '启用' : '禁用'}所有 ${categories[category]?.label || category} 服务?`,
|
||||
content: `将影响 ${serviceNames.length} 个服务`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const result = await serviceManagerDataSource.batchToggle(serviceNames, enabled);
|
||||
message.success(result.message);
|
||||
loadData();
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
message.error(`批量操作失败: ${errorMessage}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<ServiceInfo> = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
render: (name: string) => <Text strong>{name}</Text>,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: ['config', 'description'],
|
||||
key: 'description',
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: ['config', 'category'],
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (category: string) => {
|
||||
const config = categories[category] || { color: 'default', label: category };
|
||||
return <Tag color={config.color}>{config.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '内存影响',
|
||||
dataIndex: ['config', 'memoryImpact'],
|
||||
key: 'memoryImpact',
|
||||
width: 100,
|
||||
render: (impact: string) => (
|
||||
<Tag color={MEMORY_IMPACT_COLORS[impact]}>
|
||||
{MEMORY_IMPACT_TEXT[impact]}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'initialized',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (initialized: boolean, record: ServiceInfo) => {
|
||||
if (record.error) {
|
||||
return <Badge status="error" text="错误" />;
|
||||
}
|
||||
if (!record.enabled) {
|
||||
return <Badge status="default" text="已禁用" />;
|
||||
}
|
||||
if (initialized) {
|
||||
return <Badge status="success" text="已启动" />;
|
||||
}
|
||||
return <Badge status="processing" text="启动中" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '开关',
|
||||
key: 'toggle',
|
||||
width: 80,
|
||||
render: (_, record: ServiceInfo) => (
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
onChange={(checked) => handleToggle(record.name, checked)}
|
||||
checkedChildren="开"
|
||||
unCheckedChildren="关"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const filteredServices = selectedCategory === 'all'
|
||||
? services
|
||||
: groupedServices[selectedCategory] || [];
|
||||
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'all',
|
||||
label: `全部 (${services.length})`,
|
||||
},
|
||||
...Object.entries(groupedServices).map(([category, categoryServices]) => ({
|
||||
key: category,
|
||||
label: `${categories[category]?.label || category} (${categoryServices.length})`,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4}>服务管理</Title>
|
||||
<Text type="secondary">管理系统后台服务,控制内存占用</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
{stats && (
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总服务数"
|
||||
value={stats.total}
|
||||
prefix={<DashboardOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已启用"
|
||||
value={stats.enabled}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已禁用"
|
||||
value={stats.disabled}
|
||||
valueStyle={{ color: '#999' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已启动"
|
||||
value={stats.initialized}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<ThunderboltOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="启动失败"
|
||||
value={stats.failed}
|
||||
valueStyle={{ color: stats.failed > 0 ? '#cf1322' : '#999' }}
|
||||
prefix={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="内存影响(高)"
|
||||
value={stats.byMemoryImpact?.high?.enabled || 0}
|
||||
suffix={`/ ${stats.byMemoryImpact?.high?.total || 0}`}
|
||||
valueStyle={{ color: (stats.byMemoryImpact?.high?.enabled || 0) > 0 ? '#cf1322' : '#999' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Alert
|
||||
message="提示:修改服务配置后需要重启服务器才能生效"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => setPresetModalVisible(true)}
|
||||
>
|
||||
应用预设
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={loadData}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<Tabs
|
||||
activeKey={selectedCategory}
|
||||
onChange={setSelectedCategory}
|
||||
items={tabItems}
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredServices}
|
||||
rowKey="name"
|
||||
pagination={{ pageSize: 20 }}
|
||||
size="small"
|
||||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="选择预设配置"
|
||||
open={presetModalVisible}
|
||||
onCancel={() => setPresetModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
{presets.map(preset => (
|
||||
<Col span={12} key={preset.id}>
|
||||
<Card
|
||||
hoverable
|
||||
onClick={() => handleApplyPreset(preset.id)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Title level={5}>{preset.name}</Title>
|
||||
<Text type="secondary">{preset.description}</Text>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Progress
|
||||
percent={Math.round((preset.serviceCount / (stats?.total || 100)) * 100)}
|
||||
format={() => `${preset.serviceCount} 个服务`}
|
||||
strokeColor={preset.id === 'minimal' ? '#52c41a' : preset.id === 'full' ? '#f5222d' : '#1890ff'}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceManager;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
dashboard/src/pages/Settings/temp.txt
Normal file
1
dashboard/src/pages/Settings/temp.txt
Normal file
@@ -0,0 +1 @@
|
||||
{/* 修改密码模态框 */}
|
||||
Reference in New Issue
Block a user