refactor: 优化代码结构和类型定义

feat(types): 添加express.d.ts类型引用
style: 格式化express.d.ts中的接口定义
refactor: 移除未使用的AntFC类型导入
chore: 删除自动生成的.umi-production文件
feat: 添加店铺管理相关表和初始化脚本
docs: 更新安全规则和交互指南文档
refactor: 统一使用FC类型替代React.FC
perf: 优化图表组件导入方式
style: 添加.prettierrc配置文件
refactor: 调整组件导入顺序和结构
feat: 添加平台库存管理路由
fix: 修复订单同步时的库存检查逻辑
docs: 更新RBAC设计和租户管理文档
refactor: 优化部门控制器代码
This commit is contained in:
2026-03-30 01:20:57 +08:00
parent d327706087
commit 1b14947e7b
106 changed files with 11251 additions and 38565 deletions

View File

@@ -1,6 +1,8 @@
import { useLocale,
import { useLocale } from '@/contexts/LocaleContext';
import {
useState,
useEffect,
FC,
Card,
Table,
Tag,
@@ -54,7 +56,6 @@ import { useLocale,
Option,
RangePicker,
Step,
FC,
} from '@/imports';
import type { AITask } from '@/services/aiActionTaskDataSource';
import type { ColumnsType } from 'antd/es/table';
@@ -86,7 +87,7 @@ const PRIORITY_MAP: Record<string, { color: string; text: string }> = {
URGENT: { color: 'red', text: 'Urgent' },
};
const AIActionTaskManager: React.FC = () => {
const AIActionTaskManager: FC = () => {
const { t } = useLocale();
const [loading, setLoading] = useState(false);
const [tasks, setTasks] = useState<AITask[]>([]);

View File

@@ -38,8 +38,6 @@ import {
ClockCircleOutlined,
BarChartOutlined,
LineChartOutlined,
Line,
Pie,
RangePicker,
Option,
Search,
@@ -47,6 +45,7 @@ import {
Text,
FC,
} from '@/imports';
import { Line, Pie } from '@ant-design/charts';
import { useLocale } from '@/contexts/LocaleContext';
import moment from 'moment';

View File

@@ -1,4 +1,5 @@
import { useLocale,
import { useLocale } from '@/contexts/LocaleContext';
import {
useState,
useEffect,
Card,
@@ -16,10 +17,10 @@ import { useLocale,
FilterOutlined,
SearchOutlined,
PlusOutlined,
aiSuggestionDataSource,
Option,
FC,
} from '@/imports';
import { aiSuggestionDataSource } from '@/services/aiSuggestionDataSource';
import type { AISuggestion } from '../services/aiSuggestionDataSource';
import AISuggestionDisplay from '../components/ai-suggestion/AISuggestionDisplay';

View File

@@ -20,8 +20,8 @@ import {
PieChartOutlined,
UserOutlined,
ShopOutlined,
KeyOutlined,
ShieldOutlined,
LockOutlined,
SafetyOutlined,
ReloadOutlined,
DownloadOutlined,
RangePicker,
@@ -404,7 +404,7 @@ const AnalyticsDashboard: FC<AnalyticsDashboardProps> = ({ tenantId }) => {
)}
</TabPane>
<TabPane tab={<><KeyOutlined /> 使</>} key="permissionUsage">
<TabPane tab={<><LockOutlined /> 使</>} key="permissionUsage">
<Card title="权限使用情况">
<Table
columns={permissionUsageColumns}
@@ -419,7 +419,7 @@ const AnalyticsDashboard: FC<AnalyticsDashboardProps> = ({ tenantId }) => {
</Card>
</TabPane>
<TabPane tab={<><ShieldOutlined /> </>} key="securityAudit">
<TabPane tab={<><SafetyOutlined /> </>} key="securityAudit">
<Card title="安全审计记录">
<Table
columns={securityAuditColumns}

View File

@@ -76,19 +76,14 @@ import {
MailOutlined,
BellOutlined,
ClockCircleOutlined,
Line,
Column,
Pie,
Area,
DualAxes,
Title,
Text,
RangePicker,
Option,
Panel,
Step,
FC,
} from '@/imports';
import { Line, Column, Pie, Area, DualAxes } from '@ant-design/charts';
import type { ColumnsType } from 'antd/es/table';
import { analyticsDataSource, ProductAnalytics, OrderAnalytics, ProfitAnalytics, RealtimeMetrics, AlertRule, Dashboard } from '@/services/analyticsDataSource';

View File

@@ -1,4 +1,5 @@
import { useLocale,
import { useLocale } from '@/contexts/LocaleContext';
import {
useState,
useEffect,
Card,
@@ -39,10 +40,10 @@ import { useLocale,
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
clientDataSource,
Option,
FC,
} from '@/imports';
import { clientDataSource } from '@/services/clientDataSource';
import type { Client, ClientGroup } from '../services/clientDataSource';
const ClientManagement: FC = () => {

View File

@@ -18,20 +18,14 @@ import {
OrderedListOutlined,
LineChartOutlined,
ShoppingOutlined,
RechartsArea,
AreaChart,
CartesianGrid,
ResponsiveContainer,
RechartsTooltip,
XAxis,
YAxis,
RangePicker,
Option,
Table,
Typography,
dashboardDataSource,
FC,
} from '@/imports';
import { Area as RechartsArea, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip as RechartsTooltip, XAxis, YAxis } from 'recharts';
import { dashboardDataSource } from '@/services/dashboardDataSource';
import { useLocale } from '../contexts/LocaleContext';
const DashboardPage: FC = () => {

View File

@@ -1,5 +1,5 @@
import { useLocale } from '@/contexts/LocaleContext';
import {
useLocale,
useState,
useEffect,
Card,
@@ -33,11 +33,11 @@ import {
CalendarOutlined,
BarChartOutlined,
InfoCircleOutlined,
executionResultDataSource,
Option,
RangePicker,
FC,
} from '@/imports';
import { executionResultDataSource } from '@/services/executionResultDataSource';
import type { ExecutionResult, StatData } from '../services/executionResultDataSource';
import dayjs from 'dayjs';

View File

@@ -32,16 +32,6 @@ import {
ArrowUpOutlined,
ArrowDownOutlined,
ReloadOutlined,
LineChart,
Line,
Bar,
RechartsPie,
ResponsiveContainer,
Legend,
XAxis,
YAxis,
CartesianGrid,
Cell,
useNavigate,
Option,
RangePicker,
@@ -50,6 +40,7 @@ import {
Text,
FC,
} from '@/imports';
import { LineChart, Line, Bar, Pie as RechartsPie, ResponsiveContainer, Legend, XAxis, YAxis, CartesianGrid, Cell } from 'recharts';
import moment from 'moment';
import { financeDataSource } from '@/services/financeDataSource';
import type { Transaction } from '@/services/financeDataSource';

View File

@@ -23,9 +23,6 @@ import {
ReloadOutlined,
DownloadOutlined,
useNavigate,
Line,
Column,
Pie,
Title,
Text,
RangePicker,
@@ -33,6 +30,7 @@ import {
message,
FC,
} from '@/imports';
import { Line, Column, Pie } from '@ant-design/charts';
import type { ColumnsType } from 'antd/es/table';
import moment from 'moment';
import { financeDataSource } from '@/services/financeDataSource';

View File

@@ -1,4 +1,5 @@
import { useLocale,
import { useLocale } from '@/contexts/LocaleContext';
import {
useState,
useEffect,
Card,
@@ -21,10 +22,10 @@ import { useLocale,
SearchOutlined,
FilterOutlined,
ReloadOutlined,
humanApprovalDataSource,
Option,
FC,
} from '@/imports';
import { humanApprovalDataSource } from '@/services/humanApprovalDataSource';
import type { Task } from '../services/humanApprovalDataSource';
import HumanApprovalForm from '../components/human-approval/HumanApprovalForm';
import AISuggestionDisplay from '../components/ai-suggestion/AISuggestionDisplay';

View File

@@ -15,11 +15,6 @@ import {
Tag,
Space,
Typography,
Line,
Column,
Pie,
Area,
DualAxes,
ReloadOutlined,
ExportOutlined,
DollarOutlined,
@@ -32,6 +27,7 @@ import {
Text,
FC,
} from '@/imports';
import { Line, Column, Pie, DualAxes } from '@ant-design/charts';
interface SalesData {
date: string;

View File

@@ -0,0 +1,553 @@
/**
* [FE-MON004] 系统状态监控页面
* @description 展示系统健康状态、服务状态、性能指标、告警信息
* @version 1.0
*/
import { useState, useEffect, useCallback, FC } from '@/imports';
import {
Card,
Row,
Col,
Statistic,
Table,
Tag,
Alert,
Progress,
Badge,
Button,
Space,
Typography,
Tooltip,
Spin,
Descriptions,
Timeline,
Tabs,
} from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
WarningOutlined,
SyncOutlined,
ReloadOutlined,
ServerOutlined,
DatabaseOutlined,
CloudServerOutlined,
AlertOutlined,
DashboardOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
systemStatusDataSource,
MonitoringDashboardData,
ServiceStatus,
AlertInfo,
PerformanceDataPoint,
} from '@/services/systemStatusDataSource';
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const SystemMonitoringPage: FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<MonitoringDashboardData | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
const loadData = useCallback(async () => {
try {
const result = await systemStatusDataSource.getDashboardData();
setData(result);
} catch (error) {
console.error('Failed to load monitoring data:', error);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
loadData();
let unsubscribe: (() => void) | null = null;
if (autoRefresh) {
unsubscribe = systemStatusDataSource.subscribeToUpdates((newData) => {
setData(newData);
});
}
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, [loadData, autoRefresh]);
const handleRefresh = () => {
setRefreshing(true);
loadData();
};
const handleAcknowledgeAlert = async (alertId: string) => {
await systemStatusDataSource.acknowledgeAlert(alertId);
loadData();
};
const handleRestartService = async (serviceId: string) => {
try {
const result = await systemStatusDataSource.restartService(serviceId);
if (result.success) {
loadData();
}
} catch (error) {
console.error('Failed to restart service:', error);
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'healthy':
case 'up':
case 'running':
return <Badge status="success" text="正常" />;
case 'degraded':
return <Badge status="warning" text="降级" />;
case 'unhealthy':
case 'down':
case 'error':
return <Badge status="error" text="异常" />;
case 'stopped':
return <Badge status="default" text="停止" />;
case 'pending':
return <Badge status="processing" text="启动中" />;
default:
return <Badge status="default" text={status} />;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'healthy':
case 'up':
case 'running':
return <CheckCircleOutlined style={{ color: '#52c41a', fontSize: 24 }} />;
case 'degraded':
return <WarningOutlined style={{ color: '#faad14', fontSize: 24 }} />;
case 'unhealthy':
case 'down':
case 'error':
return <CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: 24 }} />;
default:
return <SyncOutlined spin style={{ color: '#1890ff', fontSize: 24 }} />;
}
};
const serviceColumns: ColumnsType<ServiceStatus> = [
{
title: '服务名称',
dataIndex: 'name',
key: 'name',
render: (name: string, record) => (
<Space>
<CloudServerOutlined />
<span>{name}</span>
{record.version && <Tag color="blue">v{record.version}</Tag>}
</Space>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status: string) => getStatusBadge(status),
},
{
title: '运行时间',
dataIndex: 'uptime',
key: 'uptime',
width: 120,
render: (uptime: number) => formatUptime(uptime),
},
{
title: 'CPU',
dataIndex: 'cpu',
key: 'cpu',
width: 120,
render: (cpu: number) => (
<Progress
percent={Math.round(cpu)}
size="small"
status={cpu > 80 ? 'exception' : 'normal'}
format={(percent) => `${percent}%`}
/>
),
},
{
title: '内存',
dataIndex: 'memory',
key: 'memory',
width: 120,
render: (memory: number) => (
<Progress
percent={Math.round(memory)}
size="small"
status={memory > 80 ? 'exception' : 'normal'}
format={(percent) => `${percent}%`}
/>
),
},
{
title: '最后心跳',
dataIndex: 'lastHeartbeat',
key: 'lastHeartbeat',
width: 180,
render: (time: string) => new Date(time).toLocaleString(),
},
{
title: '操作',
key: 'action',
width: 100,
render: (_: unknown, record: ServiceStatus) => (
<Button
type="link"
size="small"
onClick={() => handleRestartService(record.id)}
disabled={record.status !== 'running'}
>
</Button>
),
},
];
const alertColumns: ColumnsType<AlertInfo> = [
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 80,
render: (type: string) => {
const colors: Record<string, string> = {
error: 'red',
warning: 'orange',
info: 'blue',
};
const icons: Record<string, React.ReactNode> = {
error: <CloseCircleOutlined />,
warning: <WarningOutlined />,
info: <AlertOutlined />,
};
return (
<Tag color={colors[type]} icon={icons[type]}>
{type.toUpperCase()}
</Tag>
);
},
},
{
title: '消息',
dataIndex: 'message',
key: 'message',
},
{
title: '来源',
dataIndex: 'source',
key: 'source',
width: 120,
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: 180,
render: (time: string) => new Date(time).toLocaleString(),
},
{
title: '状态',
dataIndex: 'acknowledged',
key: 'acknowledged',
width: 100,
render: (acknowledged: boolean) => (
<Tag color={acknowledged ? 'green' : 'orange'}>
{acknowledged ? '已确认' : '待处理'}
</Tag>
),
},
{
title: '操作',
key: 'action',
width: 80,
render: (_: unknown, record: AlertInfo) => (
<Button
type="link"
size="small"
onClick={() => handleAcknowledgeAlert(record.id)}
disabled={record.acknowledged}
>
</Button>
),
},
];
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" tip="加载系统状态..." />
</div>
);
}
if (!data) {
return (
<Alert
message="无法加载系统状态"
description="请检查后端服务是否正常运行"
type="error"
showIcon
/>
);
}
return (
<div>
<Row gutter={[16, 16]}>
<Col span={24}>
<Card>
<Row justify="space-between" align="middle">
<Col>
<Space>
<DashboardOutlined style={{ fontSize: 24 }} />
<Title level={4} style={{ margin: 0 }}></Title>
</Space>
</Col>
<Col>
<Space>
<Button
icon={<SyncOutlined spin={refreshing} />}
onClick={handleRefresh}
loading={refreshing}
>
</Button>
<Button
type={autoRefresh ? 'primary' : 'default'}
icon={<ReloadOutlined />}
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? '停止自动刷新' : '开启自动刷新'}
</Button>
</Space>
</Col>
</Row>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="系统状态"
value={data.health.status === 'healthy' ? '正常' : data.health.status === 'degraded' ? '降级' : '异常'}
prefix={getStatusIcon(data.health.status)}
valueStyle={{
color: data.health.status === 'healthy' ? '#52c41a' :
data.health.status === 'degraded' ? '#faad14' : '#ff4d4f'
}}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="CPU 使用率"
value={Math.round(data.health.metrics.cpu)}
suffix="%"
prefix={<ServerOutlined />}
valueStyle={{
color: data.health.metrics.cpu > 80 ? '#ff4d4f' :
data.health.metrics.cpu > 60 ? '#faad14' : '#52c41a'
}}
/>
<Progress
percent={Math.round(data.health.metrics.cpu)}
showInfo={false}
status={data.health.metrics.cpu > 80 ? 'exception' : 'normal'}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="内存使用率"
value={Math.round(data.health.metrics.memory)}
suffix="%"
prefix={<DatabaseOutlined />}
valueStyle={{
color: data.health.metrics.memory > 80 ? '#ff4d4f' :
data.health.metrics.memory > 60 ? '#faad14' : '#52c41a'
}}
/>
<Progress
percent={Math.round(data.health.metrics.memory)}
showInfo={false}
status={data.health.metrics.memory > 80 ? 'exception' : 'normal'}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="活跃连接"
value={data.activeConnections}
prefix={<CloudServerOutlined />}
/>
<div style={{ marginTop: 8 }}>
<Text type="secondary">: {data.queueSize}</Text>
</div>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="请求速率"
value={Math.round(data.health.metrics.requestRate)}
suffix="req/s"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="平均响应时间"
value={Math.round(data.health.metrics.avgResponseTime)}
suffix="ms"
valueStyle={{
color: data.health.metrics.avgResponseTime > 500 ? '#ff4d4f' :
data.health.metrics.avgResponseTime > 200 ? '#faad14' : '#52c41a'
}}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="错误率"
value={data.health.metrics.errorRate.toFixed(2)}
suffix="%"
valueStyle={{
color: data.health.metrics.errorRate > 5 ? '#ff4d4f' :
data.health.metrics.errorRate > 1 ? '#faad14' : '#52c41a'
}}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="缓存命中率"
value={(data.cacheHitRate * 100).toFixed(1)}
suffix="%"
/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="组件状态">
<Row gutter={[16, 16]}>
{Object.entries(data.health.components).map(([name, component]) => (
<Col span={4} key={name}>
<Tooltip title={`响应时间: ${component.responseTime}ms`}>
<Card size="small">
<div style={{ textAlign: 'center' }}>
{getStatusIcon(component.status)}
<div style={{ marginTop: 8 }}>
<Text strong>{name.toUpperCase()}</Text>
</div>
<div style={{ marginTop: 4 }}>
{getStatusBadge(component.status)}
</div>
</div>
</Card>
</Tooltip>
</Col>
))}
</Row>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="服务状态">
<Table
columns={serviceColumns}
dataSource={data.services}
rowKey="id"
pagination={false}
size="small"
/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="告警信息">
<Table
columns={alertColumns}
dataSource={data.alerts}
rowKey="id"
pagination={{ pageSize: 5 }}
size="small"
/>
</Card>
</Col>
</Row>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="系统信息">
<Descriptions column={4}>
<Descriptions.Item label="系统运行时间">
{formatUptime(data.health.metrics.uptime)}
</Descriptions.Item>
<Descriptions.Item label="磁盘使用率">
{Math.round(data.health.metrics.disk)}%
</Descriptions.Item>
<Descriptions.Item label="最后更新">
{new Date(data.health.timestamp).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="数据刷新">
{autoRefresh ? '自动 (5秒)' : '手动'}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
</div>
);
};
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}${hours}小时`;
}
if (hours > 0) {
return `${hours}小时 ${mins}分钟`;
}
return `${mins}分钟`;
}
export default SystemMonitoringPage;

View File

@@ -1,172 +1,5 @@
import {
useState,
useEffect,
Card,
Table,
Button,
Space,
Input,
message,
Tag,
Modal,
Form,
Select,
DatePicker,
Row,
Col,
Statistic,
ReloadOutlined,
PlusOutlined,
SearchOutlined,
Option,
RangePicker,
FC,
} from '@/imports';
import { MonitoringDataSource } from '@/services/monitoringDataSource';
import type { MonitoringItem } from '@/services/monitoringDataSource';
const MonitoringPage: FC = () => {
const [data, setData] = useState<MonitoringItem[]>([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const result = await MonitoringDataSource.list();
setData(result);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
message.error(`加载失败: ${errorMessage}`);
} finally {
setLoading(false);
}
};
const handleCreate = async (values: any) => {
try {
await MonitoringDataSource.create(values);
message.success('创建成功');
setModalVisible(false);
form.resetFields();
loadData();
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
message.error(`创建失败: ${errorMessage}`);
}
};
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 200,
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const color = status === 'ACTIVE' ? 'green' : status === 'INACTIVE' ? 'red' : 'default';
return <Tag color={color}>{status}</Tag>;
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
},
{
title: '操作',
key: 'action',
width: 150,
render: (_: any, record: MonitoringItem) => (
<Space>
<Button type="link" size="small"></Button>
<Button type="link" size="small"></Button>
</Space>
),
},
];
return (
<div>
<Card>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Statistic title="总数" value={data.length} />
</Col>
<Col span={6}>
<Statistic
title="活跃"
value={data.filter(item => item.status === 'ACTIVE').length}
valueStyle={{ color: '#3f8600' }}
/>
</Col>
</Row>
<div style={{ marginBottom: 16 }}>
<Space>
<Input
placeholder="搜索..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: 200 }}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalVisible(true)}>
</Button>
<Button icon={<ReloadOutlined />} onClick={loadData}>
</Button>
</Space>
</div>
<Table
columns={columns}
dataSource={data.filter(item =>
item.name?.toLowerCase().includes(searchText.toLowerCase())
)}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20 }}
/>
</Card>
<Modal
title="新建"
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={() => form.submit()}
>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="请输入名称" />
</Form.Item>
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select placeholder="请选择状态">
<Option value="ACTIVE"></Option>
<Option value="INACTIVE"></Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default MonitoringPage;
/**
* 监控模块入口
* @description 包含监控配置和系统状态监控页面
*/
export { default as SystemStatus } from './SystemStatus';

View File

@@ -208,7 +208,7 @@ const MOCK_CONVERSION_REPORT = {
})),
};
const MultiShopReport: React.FC = () => {
const MultiShopReport: FC = () => {
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('sales');
const [salesReport, setSalesReport] = useState<any>(null);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { FC } from '@/imports';
import { Modal, Row, Col, Text, Tag, Divider, Paragraph, Timeline, Card, Space, Button } from '@/imports';
import dayjs from 'dayjs';
import type { OperationLogItem } from '@/services/operationLogDataSource';
@@ -13,7 +13,7 @@ interface DetailModalProps {
getStateChange: (log: OperationLogItem) => React.ReactNode;
}
const DetailModal: React.FC<DetailModalProps> = ({
const DetailModal: FC<DetailModalProps> = ({
visible,
log,
relatedLogs,

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { FC } from '@/imports';
import { Card, Row, Col, Select, Input, RangePicker, Button, Space, SearchOutlined, FilterOutlined } from '@/imports';
import type { OperationLogQuery } from '@/services/operationLogDataSource';
import { LOOP_OPTIONS, STAGE_OPTIONS, ACTOR_OPTIONS, ACTION_OPTIONS } from '@/services/operationLogDataSource';
@@ -11,7 +11,7 @@ interface FilterPanelProps {
onReset: () => void;
}
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, onSearch, onReset }) => {
const FilterPanel: FC<FilterPanelProps> = ({ filters, onFilterChange, onSearch, onReset }) => {
const { t } = useLocale();
return (
<Card style={{ marginBottom: 16 }}>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useMemo, moment } from '@/imports';
import { useState, useEffect, useCallback, useMemo, moment, FC } from '@/imports';
import {
Card,
Table,
@@ -415,7 +415,7 @@ const MOCK_ORDERS: Order[] = [
},
];
const OrderList: React.FC = () => {
const OrderList: FC = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Order[]>([]);

View File

@@ -22,15 +22,13 @@ import {
CheckCircleOutlined,
CloseCircleOutlined,
Link,
Area,
Pie,
Column,
orderDataSource,
Title,
Text,
Paragraph,
FC,
} from '@/imports';
import { Area, Pie, Column } from '@ant-design/charts';
import moment from 'moment';
const OrderManagement: FC = () => {

View File

@@ -4,6 +4,7 @@ import {
useCallback,
useMemo,
useNavigate,
FC,
Card,
Table,
Button,
@@ -137,7 +138,7 @@ const PLATFORM_CONFIG: Record<string, { icon: React.ReactNode; color: string }>
eBay: { icon: <GlobalOutlined />, color: '#e53238' },
};
const ProductList: React.FC = () => {
const ProductList: FC = () => {
const navigate = useNavigate();
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(false);

View File

@@ -13,16 +13,6 @@ import {
message,
Alert,
Badge,
LineChart,
Line,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
AlertOutlined,
UpOutlined,
DownOutlined,
@@ -34,6 +24,7 @@ import {
Option,
FC,
} from '@/imports';
import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { productDataSource } from '@/services/productDataSource';
interface ProfitData {

View File

@@ -0,0 +1,324 @@
import { useState, useEffect, FC } from '@/imports';
import {
Card,
Row,
Col,
Button,
Space,
Tag,
Badge,
Statistic,
Modal,
Form,
Input,
Select,
message,
Typography,
Alert,
Spin,
PlusOutlined,
EditOutlined,
ReloadOutlined,
DeleteOutlined,
TeamOutlined,
ShopOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
} from '@/imports';
import { shopDataSource } from '@/services/shopDataSource';
import { Shop, ShopMember, ShopStats } from '@/services/shopDataSource';
import { PLATFORM_CONFIGS } from '@/types/platform';
import { useNavigate } from 'react-router-dom';
const { Title, Text } = Typography;
const MyShops: FC = () => {
const [shops, setShops] = useState<Shop[]>([]);
const [stats, setStats] = useState<ShopStats>({ total: 0, active: 0, inactive: 0, expired: 0, error: 0 });
const [loading, setLoading] = useState(true);
const [editModalVisible, setEditModalVisible] = useState(false);
const [currentShop, setCurrentShop] = useState<Shop | null>(null);
const [form] = Form.useForm();
const navigate = useNavigate();
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const [shopList, shopStats] = await Promise.all([
shopDataSource.getMyShops(),
shopDataSource.getStats(),
]);
setShops(shopList);
setStats(shopStats);
} catch (error: any) {
message.error(`加载失败: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleEditShop = (shop: Shop) => {
setCurrentShop(shop);
form.setFieldsValue({
name: shop.name,
platform: shop.platform,
});
setEditModalVisible(true);
};
const handleUpdateShop = async () => {
if (!currentShop) return;
try {
const values = await form.validateFields();
await shopDataSource.updateShop(currentShop.id, {
name: values.name,
});
message.success('店铺信息已更新');
setEditModalVisible(false);
form.resetFields();
setCurrentShop(null);
loadData();
} catch (error: any) {
if (error.errorFields) {
message.error('请填写完整信息');
} else {
message.error(`更新失败: ${error.message}`);
}
}
};
const handleRefreshAuth = async (shop: Shop) => {
try {
message.loading({ content: '正在刷新授权...', key: 'refresh' });
const result = await shopDataSource.refreshAuth(shop.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 handleDeleteShop = async (shop: Shop) => {
Modal.confirm({
title: '确认删除店铺?',
content: '删除后该店铺的所有成员关系将被移除,此操作不可恢复',
okType: 'danger',
onOk: async () => {
try {
await shopDataSource.deleteShop(shop.id);
message.success('店铺已删除');
loadData();
} catch (error: any) {
message.error(`删除失败: ${error.message}`);
}
},
});
};
const getStatusConfig = (status: string) => {
switch (status) {
case 'ACTIVE':
return { color: 'success', text: '已连接', icon: <CheckCircleOutlined /> };
case 'INACTIVE':
return { color: 'default', text: '未连接', icon: <CloseCircleOutlined /> };
case 'EXPIRED':
return { color: 'warning', text: '已过期', icon: <SyncOutlined /> };
case 'ERROR':
return { color: 'error', text: '错误', icon: <SyncOutlined spin /> };
default:
return { color: 'default', text: status, icon: null };
}
};
const getPlatformConfig = (platform: string) => {
return PLATFORM_CONFIGS[platform as keyof typeof PLATFORM_CONFIGS] || { name: platform, icon: 'shop', color: 'default' };
};
return (
<div style={{ padding: '24px' }}>
<Title level={4}></Title>
<Text type="secondary"></Text>
<Row gutter={16} style={{ marginTop: 24, 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={<SyncOutlined />}
/>
</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: 24 }}
/>
)}
<Spin spinning={loading}>
<Row gutter={[16, 16]}>
{shops.map(shop => {
const statusConfig = getStatusConfig(shop.status);
const platformConfig = getPlatformConfig(shop.platform);
return (
<Col xs={24} sm={12} lg={8} xl={6} key={shop.id}>
<Card
hoverable
actions={[
<Button
type="link"
icon={<TeamOutlined />}
onClick={() => navigate(`/settings/shop-members/${shop.id}`)}
>
</Button>,
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEditShop(shop)}
>
</Button>,
shop.status === 'ACTIVE' ? (
<Button
type="link"
icon={<ReloadOutlined />}
onClick={() => handleRefreshAuth(shop)}
>
</Button>
) : shop.status === 'EXPIRED' ? (
<Button
type="link"
icon={<ReloadOutlined />}
onClick={() => handleRefreshAuth(shop)}
>
</Button>
) : null,
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDeleteShop(shop)}
>
</Button>,
]}
>
<Card.Meta
avatar={
<Badge status={statusConfig.color as any}>
<ShopOutlined style={{ fontSize: 32, color: platformConfig.color === 'default' ? '#999' : undefined }} />
</Badge>
}
title={
<div>
<div style={{ fontWeight: 'bold' }}>{shop.name}</div>
<Tag color={platformConfig.color} style={{ marginTop: 4 }}>
{platformConfig.name}
</Tag>
</div>
}
description={
<div>
<div style={{ marginBottom: 8 }}>
<Badge status={statusConfig.color as any} text={statusConfig.text} />
</div>
<div style={{ color: '#666', fontSize: 12 }}>
<div>: {new Date(shop.createdAt).toLocaleDateString()}</div>
{shop.lastSyncAt && (
<div>: {new Date(shop.lastSyncAt).toLocaleString()}</div>
)}
</div>
</div>
}
/>
</Card>
</Col>
);
})}
{shops.length === 0 && !loading && (
<Col span={24}>
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
<ShopOutlined style={{ fontSize: 64, marginBottom: 16 }} />
<div style={{ fontSize: 16, marginBottom: 8 }}></div>
<div></div>
</div>
</Col>
)}
</Row>
</Spin>
<Modal
title="编辑店铺"
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false);
form.resetFields();
setCurrentShop(null);
}}
onOk={handleUpdateShop}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="店铺名称" rules={[{ required: true, message: '请输入店铺名称' }]}>
<Input placeholder="输入店铺名称" />
</Form.Item>
<Form.Item name="platform" label="平台">
<Select disabled>
{Object.entries(PLATFORM_CONFIGS).map(([key, config]) => (
<Select.Option key={key} value={key}>
{config.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default MyShops;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { FC } from '@/imports';
import {
useState,
useEffect,
@@ -96,7 +96,7 @@ const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
error: { color: 'error', text: 'Error' },
};
const PlatformAccountConfig: React.FC = () => {
const PlatformAccountConfig: FC = () => {
const [loading, setLoading] = useState(false);
const [accounts, setAccounts] = useState<PlatformAccount[]>([]);
const [modalVisible, setModalVisible] = useState(false);

View File

@@ -1,6 +1,7 @@
import {
useState,
useEffect,
FC,
Card,
Table,
Button,
@@ -31,7 +32,6 @@ import {
Title,
Text,
Option,
FC,
} from '@/imports';
import type { ColumnsType } from 'antd/es/table';
import { platformAuthDataSource } from '@/services/platformAuthDataSource';
@@ -98,7 +98,7 @@ const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
error: { color: 'error', text: '错误' },
};
const PlatformAuth: React.FC = () => {
const PlatformAuth: 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);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect, FC } from '@/imports';
import {
Card,
Table,
@@ -33,7 +33,7 @@ const { Option } = Select;
const { TreeNode } = Tree;
const { Panel } = Collapse;
const RoleManagement: React.FC = () => {
const RoleManagement: FC = () => {
const [roles, setRoles] = useState<any[]>([]);
const [permissions, setPermissions] = useState<any[]>([]);
const [selectedRole, setSelectedRole] = useState<any>(null);

View File

@@ -1,30 +1,35 @@
import { useState, useEffect, FC } from '@/imports';
import {
Layout,
Menu,
Table,
Button,
Space,
Modal,
Form,
Select,
Tag,
message,
Spin,
PlusOutlined,
DeleteOutlined,
TeamOutlined,
ShopOutlined,
UserOutlined,
} from '@/imports';
import type { ColumnsType } from 'antd/es/table';
import { shopDataSource } from '@/services/shopDataSource';
import { Shop, ShopMember } from '@/services/shopDataSource';
import { PLATFORM_CONFIGS } from '@/types/platform';
import { useNavigate, useParams } from 'react-router-dom';
interface ShopMember {
id: string;
shopId: string;
userId: string;
userName: string;
userEmail: string;
role: 'owner' | 'admin' | 'operator' | 'viewer';
permissions: string[];
assignedBy: string;
assignedAt: string;
}
interface Shop {
id: string;
name: string;
platform: string;
departmentId: string;
}
const { Sider, Content } = Layout;
interface User {
id: string;
name: string;
email: string;
role: string;
departments: string[];
}
const ShopMemberManagement: FC = () => {
@@ -32,124 +37,164 @@ const ShopMemberManagement: FC = () => {
const [selectedShop, setSelectedShop] = useState<Shop | null>(null);
const [members, setMembers] = useState<ShopMember[]>([]);
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [selectedRole, setSelectedRole] = useState<'admin' | 'operator' | 'viewer'>('operator');
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
const [memberCounts, setMemberCounts] = useState<Record<string, number>>({});
const navigate = useNavigate();
const { shopId } = useParams<{ shopId: string }>();
useEffect(() => {
fetchShops();
loadShops();
}, []);
useEffect(() => {
if (shopId && shops.length > 0) {
const shop = shops.find(s => s.id === shopId);
if (shop) {
setSelectedShop(shop);
}
}
}, [shopId, shops]);
useEffect(() => {
if (selectedShop) {
fetchShopMembers(selectedShop.id);
fetchAvailableUsers();
loadShopMembers(selectedShop.id);
loadAvailableUsers();
loadMemberCounts();
}
}, [selectedShop]);
const fetchShops = async () => {
const loadShops = async () => {
setLoading(true);
setTimeout(() => {
const mockShops: Shop[] = [
{ id: 'shop_1', name: 'TikTok店铺A', platform: 'TIKTOK', departmentId: 'dept_1_1' },
{ id: 'shop_2', name: 'Shopee店铺B', platform: 'SHOPEE', departmentId: 'dept_1_2' },
{ id: 'shop_3', name: 'Amazon店铺C', platform: 'AMAZON', departmentId: 'dept_2' },
{ id: 'shop_4', name: 'Shopify店铺D', platform: 'SHOPIFY', departmentId: 'dept_4_1' },
];
setShops(mockShops);
try {
const shopList = await shopDataSource.getMyShops();
setShops(shopList);
if (shopId) {
const shop = shopList.find(s => s.id === shopId);
if (shop) {
setSelectedShop(shop);
} else if (shopList.length > 0) {
setSelectedShop(shopList[0]);
}
} else if (shopList.length > 0) {
setSelectedShop(shopList[0]);
}
} catch (error: any) {
message.error(`加载店铺列表失败: ${error.message}`);
} finally {
setLoading(false);
}, 500);
}
};
const fetchShopMembers = async (shopId: string) => {
const loadShopMembers = async (shopId: string) => {
setLoading(true);
setTimeout(() => {
const mockMembers: ShopMember[] = [
{
id: 'member_1',
shopId,
userId: 'user_manager',
userName: '李主管',
userEmail: 'manager@crawlful.com',
role: 'admin',
permissions: ['product:read', 'product:write', 'order:read', 'order:write', 'inventory:read', 'inventory:write'],
assignedBy: 'system',
assignedAt: '2026-03-20T10:00:00Z',
},
{
id: 'member_2',
shopId,
userId: 'user_operator',
userName: '王专员',
userEmail: 'operator@crawlful.com',
role: 'operator',
permissions: ['product:read', 'product:write', 'order:read', 'inventory:read'],
assignedBy: 'user_manager',
assignedAt: '2026-03-21T14:30:00Z',
},
];
setMembers(mockMembers);
try {
const memberList = await shopDataSource.getShopMembers(shopId);
setMembers(memberList);
} catch (error: any) {
message.error(`加载成员列表失败: ${error.message}`);
} finally {
setLoading(false);
}, 500);
}
};
const fetchAvailableUsers = async () => {
setTimeout(() => {
const mockUsers: User[] = [
{ id: 'user_manager', name: '李主管', email: 'manager@crawlful.com', role: 'MANAGER', departments: ['dept_1', 'dept_1_1', 'dept_1_2'] },
{ id: 'user_operator', name: '王专员', email: 'operator@crawlful.com', role: 'OPERATOR', departments: ['dept_1_1'] },
{ id: 'user_finance', name: '赵财务', email: 'finance@crawlful.com', role: 'FINANCE', departments: ['dept_3'] },
{ id: 'user_sourcing', name: '孙采购', email: 'sourcing@crawlful.com', role: 'SOURCING', departments: ['dept_1', 'dept_2'] },
{ id: 'user_logistics', name: '周物流', email: 'logistics@crawlful.com', role: 'LOGISTICS', departments: ['dept_5'] },
];
setAvailableUsers(mockUsers);
}, 300);
const loadAvailableUsers = async () => {
try {
const response = await fetch('/api/users/list');
const data = await response.json();
setAvailableUsers(data.data || []);
} catch (error: any) {
message.error(`加载用户列表失败: ${error.message}`);
}
};
const handleAddMembers = () => {
const loadMemberCounts = async () => {
const counts: Record<string, number> = {};
for (const shop of shops) {
try {
const shopMembers = await shopDataSource.getShopMembers(shop.id);
counts[shop.id] = shopMembers.length;
} catch (error) {
counts[shop.id] = 0;
}
}
setMemberCounts(counts);
};
const handleAddMembers = async () => {
if (selectedUsers.length === 0) {
alert('请选择要添加的成员');
message.warning('请选择要添加的成员');
return;
}
const newMembers = selectedUsers.map(userId => {
const user = availableUsers.find(u => u.id === userId);
return {
id: `member_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
shopId: selectedShop!.id,
userId,
userName: user!.name,
userEmail: user!.email,
role: selectedRole,
permissions: selectedPermissions.length > 0 ? selectedPermissions : getDefaultPermissions(selectedRole),
assignedBy: 'current_user',
assignedAt: new Date().toISOString(),
};
});
if (!selectedShop) return;
setMembers([...members, ...newMembers]);
setIsModalVisible(false);
setSelectedUsers([]);
setSelectedPermissions([]);
alert(`成功添加 ${newMembers.length} 个成员`);
};
const handleRemoveMember = (memberId: string) => {
if (confirm('确定要移除该成员吗?')) {
setMembers(members.filter(m => m.id !== memberId));
alert('成员已移除');
try {
for (const userId of selectedUsers) {
await shopDataSource.addMember({
shopId: selectedShop.id,
userId,
role: selectedRole,
permissions: selectedPermissions.length > 0 ? selectedPermissions : getDefaultPermissions(selectedRole),
assignedBy: 'current_user',
});
}
message.success(`成功添加 ${selectedUsers.length} 个成员`);
setModalVisible(false);
setSelectedUsers([]);
setSelectedPermissions([]);
loadShopMembers(selectedShop.id);
loadMemberCounts();
} catch (error: any) {
message.error(`添加成员失败: ${error.message}`);
}
};
const handleUpdateMemberRole = (memberId: string, newRole: 'admin' | 'operator' | 'viewer') => {
setMembers(members.map(m =>
m.id === memberId
? { ...m, role: newRole, permissions: getDefaultPermissions(newRole) }
: m
));
alert('成员角色已更新');
const handleRemoveMember = async (memberId: string) => {
if (!selectedShop) return;
Modal.confirm({
title: '确认移除该成员?',
content: '移除后该用户将无法访问该店铺',
onOk: async () => {
try {
const member = members.find(m => m.id === memberId);
if (member) {
await shopDataSource.removeMember(selectedShop.id, member.userId);
message.success('成员已移除');
loadShopMembers(selectedShop.id);
loadMemberCounts();
}
} catch (error: any) {
message.error(`移除成员失败: ${error.message}`);
}
},
});
};
const handleUpdateMemberRole = async (memberId: string, newRole: 'owner' | 'admin' | 'operator' | 'viewer') => {
if (!selectedShop) return;
try {
const member = members.find(m => m.id === memberId);
if (member) {
await shopDataSource.updateMemberRole(
selectedShop.id,
member.userId,
newRole,
getDefaultPermissions(newRole)
);
message.success('成员角色已更新');
loadShopMembers(selectedShop.id);
}
} catch (error: any) {
message.error(`更新角色失败: ${error.message}`);
}
};
const getDefaultPermissions = (role: 'admin' | 'operator' | 'viewer'): string[] => {
@@ -185,197 +230,225 @@ const ShopMemberManagement: FC = () => {
}
};
return (
<div style={{ padding: '24px' }}>
<h2></h2>
<div style={{ marginBottom: '24px' }}>
<label style={{ marginRight: '12px' }}></label>
<select
value={selectedShop?.id || ''}
onChange={(e) => setSelectedShop(shops.find(s => s.id === e.target.value) || null)}
style={{ padding: '8px', minWidth: '200px' }}
const getPlatformConfig = (platform: string) => {
return PLATFORM_CONFIGS[platform as keyof typeof PLATFORM_CONFIGS] || { name: platform, icon: 'shop', color: 'default' };
};
const columns: ColumnsType<ShopMember> = [
{
title: '用户',
dataIndex: 'userName',
key: 'userName',
render: (userName: string, record: ShopMember) => (
<Space direction="vertical" size={0}>
<span style={{ fontWeight: 'bold' }}>{userName}</span>
<span style={{ color: '#666', fontSize: 12 }}>{record.userEmail}</span>
</Space>
),
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
width: 120,
render: (role: string, record: ShopMember) => (
<Select
value={role}
onChange={(value) => handleUpdateMemberRole(record.id, value)}
style={{ width: '100%' }}
>
<option value=""></option>
{shops.map(shop => (
<option key={shop.id} value={shop.id}>
{shop.name} ({shop.platform})
</option>
<Select.Option value="owner"></Select.Option>
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="viewer"></Select.Option>
</Select>
),
},
{
title: '权限',
dataIndex: 'permissions',
key: 'permissions',
render: (permissions: string[]) => (
<Space wrap>
{permissions.map(perm => (
<Tag key={perm} style={{ margin: 2 }}>
{perm}
</Tag>
))}
</select>
</div>
</Space>
),
},
{
title: '分配时间',
dataIndex: 'assignedAt',
key: 'assignedAt',
width: 180,
render: (assignedAt: string) => new Date(assignedAt).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'action',
width: 100,
render: (_, record: ShopMember) => (
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveMember(record.id)}
>
</Button>
),
},
];
{selectedShop && (
<>
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h3>{selectedShop.name} - </h3>
<p style={{ color: '#666' }}> {members.length} </p>
</div>
<button
onClick={() => setIsModalVisible(true)}
style={{
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
+
</button>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>...</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#f5f5f5' }}>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}></th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}></th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}></th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}></th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}></th>
</tr>
</thead>
<tbody>
{members.map(member => (
<tr key={member.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}>
<div>
<div style={{ fontWeight: 'bold' }}>{member.userName}</div>
<div style={{ color: '#666', fontSize: '12px' }}>{member.userEmail}</div>
return (
<Layout style={{ minHeight: 'calc(100vh - 64px)' }}>
<Sider width={280} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
<h3 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
<ShopOutlined />
</h3>
</div>
<Spin spinning={loading}>
<Menu
mode="inline"
selectedKeys={selectedShop ? [selectedShop.id] : []}
style={{ borderRight: 0 }}
items={shops.map(shop => {
const platformConfig = getPlatformConfig(shop.platform);
return {
key: shop.id,
icon: <ShopOutlined />,
label: (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ flex: 1, overflow: 'hidden' }}>
<div style={{ fontWeight: 'bold', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{shop.name}
</div>
</td>
<td style={{ padding: '12px' }}>
<select
value={member.role}
onChange={(e) => handleUpdateMemberRole(member.id, e.target.value as any)}
style={{ padding: '4px 8px' }}
>
<option value="owner"></option>
<option value="admin"></option>
<option value="operator"></option>
<option value="viewer"></option>
</select>
</td>
<td style={{ padding: '12px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{member.permissions.map(perm => (
<span key={perm} style={{
padding: '2px 8px',
backgroundColor: '#f0f2f5',
borderRadius: '4px',
fontSize: '12px'
}}>
{perm}
</span>
))}
<div style={{ fontSize: 12, color: '#666' }}>
{platformConfig.name}
</div>
</td>
<td style={{ padding: '12px', color: '#666' }}>
{new Date(member.assignedAt).toLocaleString('zh-CN')}
</td>
<td style={{ padding: '12px' }}>
<button
onClick={() => handleRemoveMember(member.id)}
style={{
padding: '4px 8px',
backgroundColor: '#ff4d4f',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
</button>
</td>
</tr>
))}
{members.length === 0 && (
<tr>
<td colSpan={5} style={{ padding: '40px', textAlign: 'center', color: '#999' }}>
"添加成员"
</td>
</tr>
)}
</tbody>
</table>
)}
</>
)}
{isModalVisible && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}}>
<div style={{
backgroundColor: 'white',
padding: '24px',
borderRadius: '8px',
width: '600px',
maxHeight: '80vh',
overflowY: 'auto',
}}>
<h3 style={{ marginTop: 0 }}></h3>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px' }}></label>
<div style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ddd', padding: '8px' }}>
{availableUsers.filter(user => !members.some(m => m.userId === user.id)).map(user => (
<div key={user.id} style={{ marginBottom: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={selectedUsers.includes(user.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedUsers([...selectedUsers, user.id]);
} else {
setSelectedUsers(selectedUsers.filter(id => id !== user.id));
}
}}
style={{ marginRight: '8px' }}
/>
<span>{user.name} ({user.email}) - {user.role}</span>
</label>
</div>
<Tag color="blue" style={{ marginLeft: 8 }}>
{memberCounts[shop.id] || 0}
</Tag>
</div>
))}
),
onClick: () => {
setSelectedShop(shop);
navigate(`/settings/shop-members/${shop.id}`);
},
};
})}
/>
</Spin>
{shops.length === 0 && !loading && (
<div style={{ padding: '24px', textAlign: 'center', color: '#999' }}>
<ShopOutlined style={{ fontSize: 48, marginBottom: 16 }} />
<div></div>
<Button type="link" onClick={() => navigate('/settings/my-shops')}>
</Button>
</div>
)}
</Sider>
<Content style={{ padding: '24px', background: '#f5f5f5' }}>
{selectedShop ? (
<>
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{ margin: 0, marginBottom: 8 }}>
{selectedShop.name} -
</h2>
<p style={{ margin: 0, color: '#666' }}>
{members.length}
</p>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalVisible(true)}>
</Button>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px' }}></label>
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value as any)}
style={{ padding: '8px', width: '100%' }}
>
<option value="admin"></option>
<option value="operator"></option>
<option value="viewer"></option>
</select>
</div>
<Spin spinning={loading}>
<Table
columns={columns}
dataSource={members}
rowKey="id"
pagination={false}
/>
</Spin>
</>
) : (
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
<TeamOutlined style={{ fontSize: 64, marginBottom: 16 }} />
<div style={{ fontSize: 16, marginBottom: 8 }}></div>
<div></div>
</div>
)}
</Content>
<div style={{ marginBottom: '24px' }}>
<label style={{ display: 'block', marginBottom: '8px' }}></label>
<div style={{ maxHeight: '150px', overflowY: 'auto', border: '1px solid #ddd', padding: '8px' }}>
{['product:read', 'product:write', 'product:delete', 'order:read', 'order:write', 'order:approve', 'inventory:read', 'inventory:write'].map(perm => (
<label key={perm} style={{ display: 'block', marginBottom: '4px' }}>
<input
type="checkbox"
<Modal
title="添加店铺成员"
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setSelectedUsers([]);
setSelectedPermissions([]);
}}
onOk={handleAddMembers}
width={600}
>
<Form layout="vertical">
<Form.Item label="选择用户">
<div style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #d9d9d9', borderRadius: 4, padding: 8 }}>
{availableUsers.filter(user => !members.some(m => m.userId === user.id)).map(user => (
<div key={user.id} style={{ marginBottom: 8 }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={selectedUsers.includes(user.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedUsers([...selectedUsers, user.id]);
} else {
setSelectedUsers(selectedUsers.filter(id => id !== user.id));
}
}}
style={{ marginRight: 8 }}
/>
<Space>
<UserOutlined />
<span>{user.name}</span>
<span style={{ color: '#999' }}>({user.email})</span>
<Tag>{user.role}</Tag>
</Space>
</label>
</div>
))}
</div>
</Form.Item>
<Form.Item label="角色">
<Select
value={selectedRole}
onChange={setSelectedRole}
>
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="viewer"></Select.Option>
</Select>
</Form.Item>
<Form.Item label="权限">
<div style={{ maxHeight: '150px', overflowY: 'auto', border: '1px solid #d9d9d9', borderRadius: 4, padding: 8 }}>
{['product:read', 'product:write', 'product:delete', 'order:read', 'order:write', 'order:approve', 'inventory:read', 'inventory:write'].map(perm => (
<div key={perm} style={{ marginBottom: 4 }}>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={selectedPermissions.includes(perm)}
onChange={(e) => {
if (e.target.checked) {
@@ -384,45 +457,17 @@ const ShopMemberManagement: FC = () => {
setSelectedPermissions(selectedPermissions.filter(p => p !== perm));
}
}}
style={{ marginRight: '8px' }}
style={{ marginRight: 8 }}
/>
{perm}
</label>
))}
</div>
</div>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
onClick={() => setIsModalVisible(false)}
style={{
padding: '8px 16px',
backgroundColor: '#f5f5f5',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
</button>
<button
onClick={handleAddMembers}
style={{
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
</button>
</div>
</div>
</div>
)}
</div>
</Form.Item>
</Form>
</Modal>
</Layout>
);
};

View File

@@ -50,7 +50,6 @@ import {
Title,
Text,
Paragraph,
FC,
} from '@/imports';
import InstancePurchase from './InstancePurchase';
import type { ColumnsType } from 'antd/es/table';
@@ -267,7 +266,7 @@ interface PlanComparisonData {
current: boolean;
}
const SubscriptionManage: React.FC = () => {
const SubscriptionManage: FC = () => {
const { currentUser, hasFeature, isPaidUser, getPlanLabel } = useUser();
const [previewFeature, setPreviewFeature] = useState<FeatureConfig | null>(null);
const [simulatedPlan, setSimulatedPlan] = useState<string | null>(null);

View File

@@ -34,6 +34,8 @@ import {
FC,
} from '@/imports';
import { useUser, ROLE_CONFIG, FEATURES, PERMISSIONS, UserRole, User, Department } from '@/contexts/UserContext';
import { shopDataSource } from '@/services/shopDataSource';
import { Shop, ShopMember } from '@/services/shopDataSource';
interface ManagedUser extends User {
status: 'active' | 'inactive';
@@ -388,45 +390,60 @@ const UserManagement: FC = () => {
setIsSubscriptionModalVisible(false);
};
const handleManageShops = (user: ManagedUser) => {
const handleManageShops = async (user: ManagedUser) => {
setSelectedUser(user);
setUserShops(user.shops || []);
const mockShops = [
{ id: 'shop_1', name: 'TikTok店铺A', platform: 'TIKTOK', departmentId: 'dept_1_1' },
{ id: 'shop_2', name: 'TikTok店铺B', platform: 'TIKTOK', departmentId: 'dept_1_2' },
{ id: 'shop_3', name: 'Shopee店铺A', platform: 'SHOPEE', departmentId: 'dept_2' },
{ id: 'shop_4', name: 'Shopee店铺B', platform: 'SHOPEE', departmentId: 'dept_2' },
{ id: 'shop_5', name: 'Amazon店铺A', platform: 'AMAZON', departmentId: 'dept_3' },
{ id: 'shop_6', name: 'Amazon店铺B', platform: 'AMAZON', departmentId: 'dept_3' },
{ id: 'shop_7', name: 'Shopify店铺A', platform: 'SHOPIFY', departmentId: 'dept_4_1' },
{ id: 'shop_8', name: 'Shopify店铺B', platform: 'SHOPIFY', departmentId: 'dept_4_2' },
{ id: 'shop_9', name: 'eBay店铺A', platform: 'EBAY', departmentId: 'dept_5' },
{ id: 'shop_10', name: 'eBay店铺B', platform: 'EBAY', departmentId: 'dept_5' },
];
setAvailableShops(mockShops);
shopForm.setFieldsValue({
shops: user.shops?.map((s: any) => s.id) || [],
});
setIsShopModalVisible(true);
try {
const userShopMembers = await shopDataSource.getUserShops(user.id);
const allShops = await shopDataSource.getMyShops();
setUserShops(userShopMembers);
setAvailableShops(allShops);
shopForm.setFieldsValue({
shops: userShopMembers.map((sm: ShopMember) => sm.shopId) || [],
});
setIsShopModalVisible(true);
} catch (error: any) {
message.error(`加载店铺列表失败: ${error.message}`);
}
};
const handleShopSubmit = async (values: any) => {
if (!selectedUser) return;
const selectedShopObjects = availableShops.filter(shop =>
values.shops.includes(shop.id)
);
setUsers(users.map(u =>
u.id === selectedUser.id
? { ...u, shops: selectedShopObjects }
: u
));
message.success(`成功分配 ${selectedShopObjects.length} 个店铺`);
setIsShopModalVisible(false);
try {
const currentShopMembers = await shopDataSource.getUserShops(selectedUser.id);
const selectedShopIds = values.shops || [];
for (const member of currentShopMembers) {
if (!selectedShopIds.includes(member.shopId)) {
await shopDataSource.removeMember(member.shopId, member.userId);
}
}
for (const shopId of selectedShopIds) {
const existingMember = currentShopMembers.find(m => m.shopId === shopId);
if (!existingMember) {
await shopDataSource.addMember({
shopId,
userId: selectedUser.id,
role: 'viewer',
permissions: ['product:read', 'order:read', 'inventory:read'],
assignedBy: 'current_user',
});
}
}
message.success(`成功分配 ${selectedShopIds.length} 个店铺`);
setIsShopModalVisible(false);
const updatedUserShopMembers = await shopDataSource.getUserShops(selectedUser.id);
setUserShops(updatedUserShopMembers);
} catch (error: any) {
message.error(`保存店铺分配失败: ${error.message}`);
}
};
const handleBack = () => {

View File

@@ -1,5 +1,5 @@
import { useLocale } from '@/contexts/LocaleContext';
import {
useLocale,
useState,
useEffect,
Card,