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:
@@ -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[]>([]);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
553
dashboard/src/pages/Monitoring/SystemStatus.tsx
Normal file
553
dashboard/src/pages/Monitoring/SystemStatus.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
324
dashboard/src/pages/Settings/MyShops.tsx
Normal file
324
dashboard/src/pages/Settings/MyShops.tsx
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLocale } from '@/contexts/LocaleContext';
|
||||
import {
|
||||
useLocale,
|
||||
useState,
|
||||
useEffect,
|
||||
Card,
|
||||
|
||||
Reference in New Issue
Block a user