feat: 移动端开发完成 - 包含完整的移动端应用和Web管理后台

This commit is contained in:
2025-06-28 12:07:25 +08:00
commit 1a3e922235
75 changed files with 23857 additions and 0 deletions
+276
View File
@@ -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;
+363
View File
@@ -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;
+202
View File
@@ -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;
+391
View File
@@ -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;
+8
View File
@@ -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';