feat: 移动端开发完成 - 包含完整的移动端应用和Web管理后台
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Space, message, Tag, Tooltip, Typography } from 'antd';
|
||||
import { EyeOutlined, PhoneOutlined, DeleteOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { DataTable, StatusTag, ConfirmDialog } from '@/components/Common';
|
||||
import { useAppState } from '@/store';
|
||||
import { apiService } from '@/services/api';
|
||||
import { formatDateTime, formatDuration, formatCurrency } from '@/utils';
|
||||
import type { Call, TableParams } from '@/types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const CallList: React.FC = () => {
|
||||
const { loading, setLoading } = useAppState();
|
||||
const [calls, setCalls] = useState<Call[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
// 获取通话记录列表
|
||||
const fetchCalls = async (params?: TableParams) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiService.getCalls(params);
|
||||
setCalls(response.data);
|
||||
setTotal(response.total);
|
||||
} catch (error) {
|
||||
message.error('获取通话记录失败');
|
||||
console.error('获取通话记录失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除通话记录
|
||||
const handleDelete = async (id: string) => {
|
||||
ConfirmDialog.delete({
|
||||
content: '删除通话记录后将无法恢复,确定要删除吗?',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiService.deleteCall(id);
|
||||
message.success('删除成功');
|
||||
fetchCalls();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
console.error('删除通话记录失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请选择要删除的通话记录');
|
||||
return;
|
||||
}
|
||||
|
||||
ConfirmDialog.batchDelete(selectedRowKeys.length, {
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiService.batchDeleteCalls(selectedRowKeys as string[]);
|
||||
message.success('批量删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
fetchCalls();
|
||||
} catch (error) {
|
||||
message.error('批量删除失败');
|
||||
console.error('批量删除通话记录失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 下载录音
|
||||
const handleDownloadRecording = async (callId: string, fileName: string) => {
|
||||
try {
|
||||
const url = await apiService.getCallRecordingUrl(callId);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
message.success('录音下载开始');
|
||||
} catch (error) {
|
||||
message.error('录音下载失败');
|
||||
console.error('下载录音失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<Call> = [
|
||||
{
|
||||
title: '通话信息',
|
||||
key: 'callInfo',
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
|
||||
<PhoneOutlined style={{ marginRight: 4, color: '#1890ff' }} />
|
||||
{record.callId}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.type === 'video' ? '视频通话' : '语音通话'}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户信息',
|
||||
key: 'client',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>{record.clientName}</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.clientPhone}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '译员信息',
|
||||
key: 'translator',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold' }}>{record.translatorName}</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.translatorPhone}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '语言对',
|
||||
key: 'languages',
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Tag color="blue">
|
||||
{record.sourceLanguage} → {record.targetLanguage}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '通话时长',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
width: 100,
|
||||
render: (duration: number) => formatDuration(duration),
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '费用',
|
||||
dataIndex: 'cost',
|
||||
key: 'cost',
|
||||
width: 100,
|
||||
render: (cost: number) => (
|
||||
<Text strong style={{ color: '#f5222d' }}>
|
||||
{formatCurrency(cost)}
|
||||
</Text>
|
||||
),
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
<StatusTag type="call" status={status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 150,
|
||||
render: (date: string) => formatDateTime(date),
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
width: 150,
|
||||
render: (date: string) => date ? formatDateTime(date) : '-',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleView(record.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{record.recordingUrl && (
|
||||
<Tooltip title="下载录音">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownloadRecording(
|
||||
record.id,
|
||||
`${record.callId}_recording.mp3`
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 查看详情
|
||||
const handleView = (id: string) => {
|
||||
// TODO: 实现查看详情功能
|
||||
console.log('查看通话详情:', id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCalls();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
danger
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
批量删除 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
dataSource={calls}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||
}}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
}}
|
||||
searchable
|
||||
filterable
|
||||
onTableChange={fetchCalls}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallList;
|
||||
@@ -0,0 +1,363 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Card, Statistic, Progress, List, Avatar, Tag, Typography, Space, Button } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
PhoneOutlined,
|
||||
FileTextOutlined,
|
||||
DollarOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
ClockCircleOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Line, Column, Pie } from '@ant-design/plots';
|
||||
import { useAppState } from '@/store';
|
||||
import { apiService } from '@/services/api';
|
||||
import { formatCurrency, formatNumber } from '@/utils';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
totalCalls: number;
|
||||
totalRevenue: number;
|
||||
totalDocuments: number;
|
||||
userGrowth: number;
|
||||
callGrowth: number;
|
||||
revenueGrowth: number;
|
||||
documentGrowth: number;
|
||||
activeTranslators: number;
|
||||
avgCallDuration: number;
|
||||
completionRate: number;
|
||||
satisfactionRate: number;
|
||||
}
|
||||
|
||||
interface RecentActivity {
|
||||
id: string;
|
||||
type: 'call' | 'document' | 'user';
|
||||
title: string;
|
||||
description: string;
|
||||
time: string;
|
||||
status: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { loading, setLoading } = useAppState();
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [recentActivities, setRecentActivities] = useState<RecentActivity[]>([]);
|
||||
const [callTrends, setCallTrends] = useState<any[]>([]);
|
||||
const [revenueTrends, setRevenueTrends] = useState<any[]>([]);
|
||||
const [languageDistribution, setLanguageDistribution] = useState<any[]>([]);
|
||||
|
||||
// 获取仪表板数据
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [statsRes, activitiesRes, trendsRes] = await Promise.all([
|
||||
apiService.getDashboardStats(),
|
||||
apiService.getRecentActivities(),
|
||||
apiService.getDashboardTrends(),
|
||||
]);
|
||||
|
||||
setStats(statsRes);
|
||||
setRecentActivities(activitiesRes);
|
||||
setCallTrends(trendsRes.callTrends);
|
||||
setRevenueTrends(trendsRes.revenueTrends);
|
||||
setLanguageDistribution(trendsRes.languageDistribution);
|
||||
} catch (error) {
|
||||
console.error('获取仪表板数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 统计卡片配置
|
||||
const getStatisticCards = () => {
|
||||
if (!stats) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: '总用户数',
|
||||
value: stats.totalUsers,
|
||||
icon: <UserOutlined style={{ color: '#1890ff' }} />,
|
||||
growth: stats.userGrowth,
|
||||
color: '#1890ff',
|
||||
},
|
||||
{
|
||||
title: '总通话数',
|
||||
value: stats.totalCalls,
|
||||
icon: <PhoneOutlined style={{ color: '#52c41a' }} />,
|
||||
growth: stats.callGrowth,
|
||||
color: '#52c41a',
|
||||
},
|
||||
{
|
||||
title: '总收入',
|
||||
value: stats.totalRevenue,
|
||||
prefix: '¥',
|
||||
formatter: (value: number) => formatCurrency(value),
|
||||
icon: <DollarOutlined style={{ color: '#faad14' }} />,
|
||||
growth: stats.revenueGrowth,
|
||||
color: '#faad14',
|
||||
},
|
||||
{
|
||||
title: '文档数量',
|
||||
value: stats.totalDocuments,
|
||||
icon: <FileTextOutlined style={{ color: '#722ed1' }} />,
|
||||
growth: stats.documentGrowth,
|
||||
color: '#722ed1',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 性能指标配置
|
||||
const getPerformanceMetrics = () => {
|
||||
if (!stats) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: '活跃译员',
|
||||
value: stats.activeTranslators,
|
||||
icon: <TeamOutlined />,
|
||||
color: '#1890ff',
|
||||
},
|
||||
{
|
||||
title: '平均通话时长',
|
||||
value: `${Math.round(stats.avgCallDuration / 60)}分钟`,
|
||||
icon: <ClockCircleOutlined />,
|
||||
color: '#52c41a',
|
||||
},
|
||||
{
|
||||
title: '完成率',
|
||||
value: stats.completionRate,
|
||||
suffix: '%',
|
||||
icon: <ArrowUpOutlined />,
|
||||
color: '#faad14',
|
||||
},
|
||||
{
|
||||
title: '满意度',
|
||||
value: stats.satisfactionRate,
|
||||
suffix: '%',
|
||||
icon: <ArrowUpOutlined />,
|
||||
color: '#f5222d',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 通话趋势图配置
|
||||
const callTrendConfig = {
|
||||
data: callTrends,
|
||||
xField: 'date',
|
||||
yField: 'count',
|
||||
point: {
|
||||
size: 3,
|
||||
shape: 'circle',
|
||||
},
|
||||
color: '#1890ff',
|
||||
smooth: true,
|
||||
};
|
||||
|
||||
// 收入趋势图配置
|
||||
const revenueTrendConfig = {
|
||||
data: revenueTrends,
|
||||
xField: 'date',
|
||||
yField: 'revenue',
|
||||
color: '#52c41a',
|
||||
columnStyle: {
|
||||
radius: [4, 4, 0, 0],
|
||||
},
|
||||
};
|
||||
|
||||
// 语言分布饼图配置
|
||||
const languageDistributionConfig = {
|
||||
data: languageDistribution,
|
||||
angleField: 'value',
|
||||
colorField: 'language',
|
||||
radius: 0.8,
|
||||
innerRadius: 0.6,
|
||||
label: {
|
||||
type: 'inner',
|
||||
offset: '-30%',
|
||||
content: ({ percent }: any) => `${(percent * 100).toFixed(0)}%`,
|
||||
style: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 获取活动类型图标
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'call':
|
||||
return <PhoneOutlined style={{ color: '#1890ff' }} />;
|
||||
case 'document':
|
||||
return <FileTextOutlined style={{ color: '#722ed1' }} />;
|
||||
case 'user':
|
||||
return <UserOutlined style={{ color: '#52c41a' }} />;
|
||||
default:
|
||||
return <UserOutlined />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态标签颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'pending':
|
||||
return 'warning';
|
||||
case 'failed':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{getStatisticCards().map((card, index) => (
|
||||
<Col xs={24} sm={12} lg={6} key={index}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
prefix={card.prefix}
|
||||
formatter={card.formatter}
|
||||
valueStyle={{ color: card.color }}
|
||||
/>
|
||||
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center' }}>
|
||||
{card.icon}
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
{card.growth > 0 ? (
|
||||
<Text type="success">
|
||||
<ArrowUpOutlined /> {card.growth}%
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="danger">
|
||||
<ArrowDownOutlined /> {Math.abs(card.growth)}%
|
||||
</Text>
|
||||
)}
|
||||
<Text type="secondary" style={{ marginLeft: 4 }}>
|
||||
较上月
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 性能指标 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{getPerformanceMetrics().map((metric, index) => (
|
||||
<Col xs={24} sm={12} lg={6} key={index}>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text type="secondary">{metric.title}</Text>
|
||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: metric.color }}>
|
||||
{metric.value}{metric.suffix}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '24px', color: metric.color }}>
|
||||
{metric.icon}
|
||||
</div>
|
||||
</div>
|
||||
{metric.title === '完成率' && (
|
||||
<Progress
|
||||
percent={stats?.completionRate || 0}
|
||||
showInfo={false}
|
||||
strokeColor={metric.color}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
)}
|
||||
{metric.title === '满意度' && (
|
||||
<Progress
|
||||
percent={stats?.satisfactionRate || 0}
|
||||
showInfo={false}
|
||||
strokeColor={metric.color}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="通话趋势" loading={loading}>
|
||||
<Line {...callTrendConfig} height={300} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="收入趋势" loading={loading}>
|
||||
<Column {...revenueTrendConfig} height={300} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="语言分布" loading={loading}>
|
||||
<Pie {...languageDistributionConfig} height={300} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={16}>
|
||||
<Card
|
||||
title="最近活动"
|
||||
loading={loading}
|
||||
extra={
|
||||
<Button type="link" onClick={fetchDashboardData}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={recentActivities}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
src={item.avatar}
|
||||
icon={getActivityIcon(item.type)}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span>{item.title}</span>
|
||||
<Tag color={getStatusColor(item.status)}>
|
||||
{item.status}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<div>{item.description}</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{item.time}
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Row, Col, Card, Statistic, Typography, Table, Progress, Tag, Space } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
PhoneOutlined,
|
||||
DollarOutlined,
|
||||
RiseOutlined,
|
||||
DownOutlined,
|
||||
UpOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useEffect } from 'react';
|
||||
import { useLoading } from '@/store';
|
||||
import { formatCurrency, formatDate } from '@/utils';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
totalCalls: number;
|
||||
totalRevenue: number;
|
||||
monthlyGrowth: number;
|
||||
}
|
||||
|
||||
interface RecentCall {
|
||||
id: string;
|
||||
caller: string;
|
||||
translator: string;
|
||||
language: string;
|
||||
duration: number;
|
||||
status: 'completed' | 'in-progress' | 'failed';
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const Dashboard = () => {
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const stats: DashboardStats = {
|
||||
totalUsers: 1234,
|
||||
totalCalls: 5678,
|
||||
totalRevenue: 123456,
|
||||
monthlyGrowth: 12.5,
|
||||
};
|
||||
|
||||
const recentCalls: RecentCall[] = [
|
||||
{
|
||||
id: '1',
|
||||
caller: '张三',
|
||||
translator: '李四',
|
||||
language: '英语',
|
||||
duration: 1800,
|
||||
status: 'completed',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
caller: '王五',
|
||||
translator: '赵六',
|
||||
language: '日语',
|
||||
duration: 2400,
|
||||
status: 'in-progress',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '呼叫者',
|
||||
dataIndex: 'caller',
|
||||
key: 'caller',
|
||||
},
|
||||
{
|
||||
title: '译员',
|
||||
dataIndex: 'translator',
|
||||
key: 'translator',
|
||||
},
|
||||
{
|
||||
title: '语言',
|
||||
dataIndex: 'language',
|
||||
key: 'language',
|
||||
},
|
||||
{
|
||||
title: '时长',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
render: (duration: number) => `${Math.floor(duration / 60)}分钟`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const statusMap = {
|
||||
completed: { color: 'green', text: '已完成' },
|
||||
'in-progress': { color: 'blue', text: '进行中' },
|
||||
failed: { color: 'red', text: '失败' },
|
||||
};
|
||||
const statusInfo = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (timestamp: Date) => formatDate(timestamp),
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, [setLoading]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Title level={2}>仪表板</Title>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={stats.totalUsers}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总通话数"
|
||||
value={stats.totalCalls}
|
||||
prefix={<PhoneOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总收入"
|
||||
value={stats.totalRevenue}
|
||||
prefix={<DollarOutlined />}
|
||||
formatter={(value) => formatCurrency(Number(value))}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月增长率"
|
||||
value={stats.monthlyGrowth}
|
||||
prefix={stats.monthlyGrowth > 0 ? <RiseOutlined /> : <DownOutlined />}
|
||||
suffix="%"
|
||||
valueStyle={{
|
||||
color: stats.monthlyGrowth > 0 ? '#3f8600' : '#cf1322'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} lg={16}>
|
||||
<Card title="最近通话记录" loading={loading}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentCalls}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="系统状态">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Text>CPU 使用率</Text>
|
||||
<Progress percent={30} size="small" />
|
||||
</div>
|
||||
<div>
|
||||
<Text>内存使用率</Text>
|
||||
<Progress percent={60} size="small" />
|
||||
</div>
|
||||
<div>
|
||||
<Text>磁盘使用率</Text>
|
||||
<Progress percent={80} size="small" />
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,391 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Statistic
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
CrownOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useLoading } from '@/store';
|
||||
import { formatDate } from '@/utils';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: 'admin' | 'translator' | 'user';
|
||||
status: 'active' | 'inactive' | 'suspended';
|
||||
registeredAt: Date;
|
||||
lastLoginAt: Date;
|
||||
}
|
||||
|
||||
const UserList = () => {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
const { setLoading: setGlobalLoading } = useLoading();
|
||||
|
||||
// 模拟数据
|
||||
const mockUsers: User[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
phone: '13800138001',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
registeredAt: new Date('2023-01-15'),
|
||||
lastLoginAt: new Date('2024-01-15'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '李四',
|
||||
email: 'lisi@example.com',
|
||||
phone: '13800138002',
|
||||
role: 'translator',
|
||||
status: 'active',
|
||||
registeredAt: new Date('2023-02-20'),
|
||||
lastLoginAt: new Date('2024-01-14'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '王五',
|
||||
email: 'wangwu@example.com',
|
||||
phone: '13800138003',
|
||||
role: 'user',
|
||||
status: 'inactive',
|
||||
registeredAt: new Date('2023-03-10'),
|
||||
lastLoginAt: new Date('2024-01-10'),
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnsType<User> = [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
filteredValue: searchText ? [searchText] : null,
|
||||
onFilter: (value, record) =>
|
||||
record.name.toLowerCase().includes(String(value).toLowerCase()) ||
|
||||
record.email.toLowerCase().includes(String(value).toLowerCase()),
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: string) => {
|
||||
const roleMap = {
|
||||
admin: { color: 'red', text: '管理员', icon: <CrownOutlined /> },
|
||||
translator: { color: 'blue', text: '译员', icon: <TeamOutlined /> },
|
||||
user: { color: 'green', text: '用户', icon: <UserOutlined /> },
|
||||
};
|
||||
const roleInfo = roleMap[role as keyof typeof roleMap];
|
||||
return (
|
||||
<Tag color={roleInfo.color} icon={roleInfo.icon}>
|
||||
{roleInfo.text}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const statusMap = {
|
||||
active: { color: 'green', text: '活跃' },
|
||||
inactive: { color: 'orange', text: '不活跃' },
|
||||
suspended: { color: 'red', text: '已暂停' },
|
||||
};
|
||||
const statusInfo = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '注册时间',
|
||||
dataIndex: 'registeredAt',
|
||||
key: 'registeredAt',
|
||||
render: (date: Date) => formatDate(date),
|
||||
},
|
||||
{
|
||||
title: '最后登录',
|
||||
dataIndex: 'lastLoginAt',
|
||||
key: 'lastLoginAt',
|
||||
render: (date: Date) => formatDate(date),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser(user);
|
||||
form.setFieldsValue(user);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (userId: string) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个用户吗?',
|
||||
onOk: () => {
|
||||
setUsers(users.filter(user => user.id !== userId));
|
||||
message.success('删除成功');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (editingUser) {
|
||||
// 编辑用户
|
||||
setUsers(users.map(user =>
|
||||
user.id === editingUser.id ? { ...user, ...values } : user
|
||||
));
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
// 新增用户
|
||||
const newUser: User = {
|
||||
id: Date.now().toString(),
|
||||
...values,
|
||||
registeredAt: new Date(),
|
||||
lastLoginAt: new Date(),
|
||||
};
|
||||
setUsers([...users, newUser]);
|
||||
message.success('添加成功');
|
||||
}
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
setEditingUser(null);
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
setEditingUser(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalLoading(true);
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
setUsers(mockUsers);
|
||||
setGlobalLoading(false);
|
||||
}, 1000);
|
||||
}, [setGlobalLoading]);
|
||||
|
||||
const stats = {
|
||||
total: users.length,
|
||||
active: users.filter(u => u.status === 'active').length,
|
||||
translators: users.filter(u => u.role === 'translator').length,
|
||||
admins: users.filter(u => u.role === 'admin').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Title level={2}>用户管理</Title>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={stats.total}
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="活跃用户"
|
||||
value={stats.active}
|
||||
prefix={<TeamOutlined />}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="译员数量"
|
||||
value={stats.translators}
|
||||
prefix={<TeamOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="管理员"
|
||||
value={stats.admins}
|
||||
prefix={<CrownOutlined />}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="搜索用户..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchText(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
</Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setModalVisible(true)}
|
||||
>
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingUser ? '编辑用户' : '添加用户'}
|
||||
open={modalVisible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={handleCancel}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="姓名"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入姓名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效邮箱' },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="电话"
|
||||
name="phone"
|
||||
rules={[{ required: true, message: '请输入电话' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="角色"
|
||||
name="role"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="user">用户</Option>
|
||||
<Option value="translator">译员</Option>
|
||||
<Option value="admin">管理员</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="状态"
|
||||
name="status"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="active">活跃</Option>
|
||||
<Option value="inactive">不活跃</Option>
|
||||
<Option value="suspended">已暂停</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserList;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Dashboard
|
||||
export { default as Dashboard } from './Dashboard/Dashboard';
|
||||
|
||||
// Users
|
||||
export { default as UserList } from './Users/UserList';
|
||||
|
||||
// Calls
|
||||
export { default as CallList } from './Calls/CallList';
|
||||
Reference in New Issue
Block a user