重构移动端和管理端应用,整合路由和页面组件,优化样式,添加新功能和页面,更新API配置,提升用户体验。
This commit is contained in:
parent
240dd5d2a4
commit
deb2900acc
15
.expo/README.md
Normal file
15
.expo/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
> Why do I have a folder named ".expo" in my project?
|
||||||
|
|
||||||
|
The ".expo" folder is created when an Expo project is started using "expo start" command.
|
||||||
|
|
||||||
|
> What do the files contain?
|
||||||
|
|
||||||
|
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
|
||||||
|
- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
|
||||||
|
- "settings.json": contains the server configuration that is used to serve the application manifest.
|
||||||
|
|
||||||
|
> Should I commit the ".expo" folder?
|
||||||
|
|
||||||
|
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
|
||||||
|
|
||||||
|
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
|
8
.expo/settings.json
Normal file
8
.expo/settings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hostType": "lan",
|
||||||
|
"lanType": "ip",
|
||||||
|
"dev": true,
|
||||||
|
"minify": false,
|
||||||
|
"urlRandomness": null,
|
||||||
|
"https": false
|
||||||
|
}
|
46
App.tsx
46
App.tsx
@ -1,19 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { StatusBar } from 'react-native';
|
import { ConfigProvider } from 'antd';
|
||||||
import { store } from '@/store';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import AppNavigator from '@/navigation/AppNavigator';
|
import './src/styles/global.css';
|
||||||
|
|
||||||
|
// 导入页面组件
|
||||||
|
import HomeScreen from './src/screens/HomeScreen';
|
||||||
|
import CallScreen from './src/screens/CallScreen';
|
||||||
|
import DocumentScreen from './src/screens/DocumentScreen';
|
||||||
|
import SettingsScreen from './src/screens/SettingsScreen';
|
||||||
|
|
||||||
|
// 导入移动端导航组件
|
||||||
|
import MobileNavigation from './src/components/MobileNavigation.web';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<StatusBar
|
<Router
|
||||||
barStyle="dark-content"
|
future={{
|
||||||
backgroundColor="#fff"
|
v7_startTransition: true,
|
||||||
translucent={false}
|
v7_relativeSplatPath: true
|
||||||
/>
|
}}
|
||||||
<AppNavigator />
|
>
|
||||||
</Provider>
|
<div className="app-container">
|
||||||
|
<div className="app-content">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||||
|
<Route path="/home" element={<HomeScreen />} />
|
||||||
|
<Route path="/call" element={<CallScreen />} />
|
||||||
|
<Route path="/documents" element={<DocumentScreen />} />
|
||||||
|
<Route path="/settings" element={<SettingsScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
<MobileNavigation />
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
1
Twilioapp-admin/package-lock.json
generated
1
Twilioapp-admin/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"antd": "^5.0.0",
|
"antd": "^5.0.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"antd": "^5.0.0",
|
"antd": "^5.0.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
import { Layout, Menu, ConfigProvider } from 'antd';
|
import { Layout, Menu, Typography, ConfigProvider } from 'antd';
|
||||||
import {
|
import {
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CalendarOutlined
|
CalendarOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
SettingOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
@ -13,114 +17,159 @@ import './App.css';
|
|||||||
|
|
||||||
// 导入页面组件
|
// 导入页面组件
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import CallList from './pages/Calls/CallList';
|
||||||
import CallDetail from './pages/Calls/CallDetail';
|
import CallDetail from './pages/Calls/CallDetail';
|
||||||
|
import DocumentList from './pages/Documents/DocumentList';
|
||||||
import DocumentDetail from './pages/Documents/DocumentDetail';
|
import DocumentDetail from './pages/Documents/DocumentDetail';
|
||||||
|
import AppointmentList from './pages/Appointments/AppointmentList';
|
||||||
import AppointmentDetail from './pages/Appointments/AppointmentDetail';
|
import AppointmentDetail from './pages/Appointments/AppointmentDetail';
|
||||||
|
import UserList from './pages/Users/UserList';
|
||||||
|
import TranslatorList from './pages/Translators/TranslatorList';
|
||||||
|
import PaymentList from './pages/Payments/PaymentList';
|
||||||
|
import SystemSettings from './pages/Settings/SystemSettings';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedKey, setSelectedKey] = useState('1');
|
|
||||||
|
|
||||||
const handleMenuClick = (e: any) => {
|
const handleMenuClick = ({ key }: { key: string }) => {
|
||||||
setSelectedKey(e.key);
|
switch (key) {
|
||||||
switch (e.key) {
|
case 'dashboard':
|
||||||
case '1':
|
navigate('/');
|
||||||
navigate('/dashboard');
|
|
||||||
break;
|
break;
|
||||||
case '2':
|
case 'calls':
|
||||||
navigate('/calls/1');
|
navigate('/calls');
|
||||||
break;
|
break;
|
||||||
case '3':
|
case 'documents':
|
||||||
navigate('/documents/1');
|
navigate('/documents');
|
||||||
break;
|
break;
|
||||||
case '4':
|
case 'appointments':
|
||||||
navigate('/appointments/1');
|
navigate('/appointments');
|
||||||
break;
|
break;
|
||||||
|
case 'users':
|
||||||
|
navigate('/users');
|
||||||
|
break;
|
||||||
|
case 'translators':
|
||||||
|
navigate('/translators');
|
||||||
|
break;
|
||||||
|
case 'payments':
|
||||||
|
navigate('/payments');
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
navigate('/settings');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
navigate('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const menuItems = [
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
|
||||||
<Sider
|
|
||||||
breakpoint="lg"
|
|
||||||
collapsedWidth="0"
|
|
||||||
style={{
|
|
||||||
background: '#001529',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{
|
|
||||||
height: 32,
|
|
||||||
margin: 16,
|
|
||||||
background: 'rgba(255,255,255,.2)',
|
|
||||||
borderRadius: 4,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}>
|
|
||||||
Twilio管理系统
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
theme="dark"
|
|
||||||
mode="inline"
|
|
||||||
selectedKeys={[selectedKey]}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
items={[
|
|
||||||
{
|
{
|
||||||
key: '1',
|
key: 'dashboard',
|
||||||
icon: <DashboardOutlined />,
|
icon: <DashboardOutlined />,
|
||||||
label: '仪表板',
|
label: '仪表板',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: 'calls',
|
||||||
icon: <PhoneOutlined />,
|
icon: <PhoneOutlined />,
|
||||||
label: '通话管理',
|
label: '通话记录',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: 'documents',
|
||||||
icon: <FileTextOutlined />,
|
icon: <FileTextOutlined />,
|
||||||
label: '文档翻译',
|
label: '文档翻译',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: 'appointments',
|
||||||
icon: <CalendarOutlined />,
|
icon: <CalendarOutlined />,
|
||||||
label: '预约管理',
|
label: '预约管理',
|
||||||
},
|
},
|
||||||
]}
|
{
|
||||||
/>
|
key: 'users',
|
||||||
</Sider>
|
icon: <UserOutlined />,
|
||||||
<Layout>
|
label: '用户管理',
|
||||||
<Header style={{
|
},
|
||||||
padding: 0,
|
{
|
||||||
background: '#fff',
|
key: 'translators',
|
||||||
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
|
icon: <TeamOutlined />,
|
||||||
}}>
|
label: '译员管理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'payments',
|
||||||
|
icon: <DollarOutlined />,
|
||||||
|
label: '支付记录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'settings',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
label: '系统设置',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
|
<Sider
|
||||||
|
collapsible
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={setCollapsed}
|
||||||
|
theme="dark"
|
||||||
|
width={250}
|
||||||
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '0 24px',
|
height: '64px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'white',
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>
|
}}>
|
||||||
Twilio翻译服务管理后台
|
{collapsed ? 'T' : 'Twilio管理后台'}
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
<Menu
|
||||||
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
|
theme="dark"
|
||||||
<div style={{
|
defaultSelectedKeys={['dashboard']}
|
||||||
padding: 24,
|
mode="inline"
|
||||||
|
items={menuItems}
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Header style={{
|
||||||
|
padding: '0 24px',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
minHeight: 360,
|
display: 'flex',
|
||||||
borderRadius: 8
|
alignItems: 'center',
|
||||||
|
borderBottom: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
|
Twilio翻译服务管理系统
|
||||||
|
</Title>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Content style={{
|
||||||
|
margin: '0',
|
||||||
|
background: '#f0f2f5',
|
||||||
|
minHeight: 'calc(100vh - 64px)'
|
||||||
}}>
|
}}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/calls" element={<CallList />} />
|
||||||
<Route path="/calls/:id" element={<CallDetail />} />
|
<Route path="/calls/:id" element={<CallDetail />} />
|
||||||
|
<Route path="/documents" element={<DocumentList />} />
|
||||||
<Route path="/documents/:id" element={<DocumentDetail />} />
|
<Route path="/documents/:id" element={<DocumentDetail />} />
|
||||||
|
<Route path="/appointments" element={<AppointmentList />} />
|
||||||
<Route path="/appointments/:id" element={<AppointmentDetail />} />
|
<Route path="/appointments/:id" element={<AppointmentDetail />} />
|
||||||
|
<Route path="/users" element={<UserList />} />
|
||||||
|
<Route path="/translators" element={<TranslatorList />} />
|
||||||
|
<Route path="/payments" element={<PaymentList />} />
|
||||||
|
<Route path="/settings" element={<SystemSettings />} />
|
||||||
|
<Route path="*" element={<Dashboard />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -18,9 +18,7 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Alert,
|
Alert,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
TimePicker,
|
|
||||||
Rate,
|
Rate,
|
||||||
Divider,
|
|
||||||
Statistic,
|
Statistic,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
@ -34,19 +32,14 @@ import {
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
DollarOutlined,
|
DollarOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
|
||||||
TranslationOutlined,
|
|
||||||
StarOutlined,
|
StarOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
FileTextOutlined,
|
|
||||||
EnvironmentOutlined,
|
EnvironmentOutlined,
|
||||||
SwapOutlined,
|
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
@ -236,11 +229,11 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
try {
|
try {
|
||||||
const refundAmount = values.amount || appointment.cost;
|
const refundAmount = values.amount || appointment.cost;
|
||||||
|
|
||||||
const updatedAppointment = {
|
const updatedAppointment: Appointment = {
|
||||||
...appointment,
|
...appointment,
|
||||||
refundAmount: refundAmount,
|
refundAmount: refundAmount,
|
||||||
paymentStatus: 'refunded',
|
paymentStatus: 'refunded' as const,
|
||||||
status: 'cancelled',
|
status: 'cancelled' as const,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -326,8 +319,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
const getUrgencyText = (urgency: string) => {
|
const getUrgencyText = (urgency: string) => {
|
||||||
const texts = {
|
const texts = {
|
||||||
normal: '普通',
|
normal: '普通',
|
||||||
|
low: '低',
|
||||||
|
high: '高',
|
||||||
urgent: '加急',
|
urgent: '加急',
|
||||||
emergency: '特急',
|
|
||||||
};
|
};
|
||||||
return texts[urgency as keyof typeof texts] || urgency;
|
return texts[urgency as keyof typeof texts] || urgency;
|
||||||
};
|
};
|
||||||
@ -469,9 +463,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="质量评分"
|
title="质量评分"
|
||||||
value={appointment.qualityScore}
|
value={appointment.qualityScore || 0}
|
||||||
suffix="/100"
|
suffix="/100"
|
||||||
valueStyle={{ color: appointment.qualityScore >= 90 ? '#3f8600' : '#faad14' }}
|
valueStyle={{ color: (appointment.qualityScore || 0) >= 90 ? '#3f8600' : '#faad14' }}
|
||||||
prefix={<AuditOutlined />}
|
prefix={<AuditOutlined />}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@ -498,7 +492,7 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
</Tag>
|
</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="紧急程度" span={1}>
|
<Descriptions.Item label="紧急程度" span={1}>
|
||||||
<Tag color={appointment.urgency === 'emergency' ? 'red' : appointment.urgency === 'urgent' ? 'orange' : 'default'}>
|
<Tag color={appointment.urgency === 'urgent' ? 'orange' : appointment.urgency === 'high' ? 'red' : 'default'}>
|
||||||
{getUrgencyText(appointment.urgency)}
|
{getUrgencyText(appointment.urgency)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@ -577,8 +571,8 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
<Descriptions.Item label="退款金额" span={1}>
|
<Descriptions.Item label="退款金额" span={1}>
|
||||||
<Space>
|
<Space>
|
||||||
<DollarOutlined />
|
<DollarOutlined />
|
||||||
<Text type={appointment.refundAmount > 0 ? 'danger' : 'secondary'}>
|
<Text type={(appointment.refundAmount || 0) > 0 ? 'danger' : 'secondary'}>
|
||||||
¥{appointment.refundAmount.toFixed(2)}
|
¥{(appointment.refundAmount || 0).toFixed(2)}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@ -762,8 +756,8 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
<Descriptions column={2}>
|
<Descriptions column={2}>
|
||||||
<Descriptions.Item label="质量评分">
|
<Descriptions.Item label="质量评分">
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<div style={{ fontSize: '24px', fontWeight: 'bold', color: appointment.qualityScore >= 90 ? '#52c41a' : appointment.qualityScore >= 70 ? '#faad14' : '#ff4d4f' }}>
|
<div style={{ fontSize: '24px', fontWeight: 'bold', color: (appointment.qualityScore || 0) >= 90 ? '#52c41a' : (appointment.qualityScore || 0) >= 70 ? '#faad14' : '#ff4d4f' }}>
|
||||||
{appointment.qualityScore}/100
|
{appointment.qualityScore || 0}/100
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#999', fontSize: '12px' }}>系统评分</div>
|
<div style={{ color: '#999', fontSize: '12px' }}>系统评分</div>
|
||||||
</div>
|
</div>
|
||||||
@ -844,8 +838,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
|||||||
>
|
>
|
||||||
<Select>
|
<Select>
|
||||||
<Option value="normal">普通</Option>
|
<Option value="normal">普通</Option>
|
||||||
|
<Option value="low">低</Option>
|
||||||
|
<Option value="high">高</Option>
|
||||||
<Option value="urgent">加急</Option>
|
<Option value="urgent">加急</Option>
|
||||||
<Option value="emergency">特急</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
737
Twilioapp-admin/src/pages/Appointments/AppointmentList.tsx
Normal file
737
Twilioapp-admin/src/pages/Appointments/AppointmentList.tsx
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
DatePicker,
|
||||||
|
TimePicker,
|
||||||
|
Form,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Tooltip,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Calendar
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
interface Appointment {
|
||||||
|
id: string;
|
||||||
|
clientName: string;
|
||||||
|
clientPhone: string;
|
||||||
|
clientEmail: string;
|
||||||
|
appointmentDate: string;
|
||||||
|
appointmentTime: string;
|
||||||
|
duration: number; // 分钟
|
||||||
|
serviceType: 'voice' | 'video' | 'document';
|
||||||
|
sourceLanguage: string;
|
||||||
|
targetLanguage: string;
|
||||||
|
translator?: string;
|
||||||
|
status: 'pending' | 'confirmed' | 'in-progress' | 'completed' | 'cancelled';
|
||||||
|
notes?: string;
|
||||||
|
cost: number;
|
||||||
|
createdTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppointmentList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||||
|
const [filteredAppointments, setFilteredAppointments] = useState<Appointment[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [serviceTypeFilter, setServiceTypeFilter] = useState<string>('all');
|
||||||
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editingAppointment, setEditingAppointment] = useState<Appointment | null>(null);
|
||||||
|
const [calendarVisible, setCalendarVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockAppointments: Appointment[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
clientName: '张先生',
|
||||||
|
clientPhone: '13800138001',
|
||||||
|
clientEmail: 'zhang@example.com',
|
||||||
|
appointmentDate: '2024-01-16',
|
||||||
|
appointmentTime: '10:00',
|
||||||
|
duration: 60,
|
||||||
|
serviceType: 'video',
|
||||||
|
sourceLanguage: '中文',
|
||||||
|
targetLanguage: '英文',
|
||||||
|
translator: '王译员',
|
||||||
|
status: 'confirmed',
|
||||||
|
notes: '商务会议翻译',
|
||||||
|
cost: 300,
|
||||||
|
createdTime: '2024-01-15 14:30:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
clientName: '李女士',
|
||||||
|
clientPhone: '13800138002',
|
||||||
|
clientEmail: 'li@example.com',
|
||||||
|
appointmentDate: '2024-01-16',
|
||||||
|
appointmentTime: '14:30',
|
||||||
|
duration: 90,
|
||||||
|
serviceType: 'voice',
|
||||||
|
sourceLanguage: '英文',
|
||||||
|
targetLanguage: '中文',
|
||||||
|
translator: '李译员',
|
||||||
|
status: 'in-progress',
|
||||||
|
notes: '医疗咨询翻译',
|
||||||
|
cost: 450,
|
||||||
|
createdTime: '2024-01-15 14:25:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
clientName: '王总',
|
||||||
|
clientPhone: '13800138003',
|
||||||
|
clientEmail: 'wang@example.com',
|
||||||
|
appointmentDate: '2024-01-17',
|
||||||
|
appointmentTime: '09:00',
|
||||||
|
duration: 120,
|
||||||
|
serviceType: 'video',
|
||||||
|
sourceLanguage: '中文',
|
||||||
|
targetLanguage: '日文',
|
||||||
|
translator: '张译员',
|
||||||
|
status: 'pending',
|
||||||
|
notes: '技术交流会议',
|
||||||
|
cost: 600,
|
||||||
|
createdTime: '2024-01-15 14:20:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
clientName: '陈先生',
|
||||||
|
clientPhone: '13800138004',
|
||||||
|
clientEmail: 'chen@example.com',
|
||||||
|
appointmentDate: '2024-01-15',
|
||||||
|
appointmentTime: '16:00',
|
||||||
|
duration: 45,
|
||||||
|
serviceType: 'document',
|
||||||
|
sourceLanguage: '德文',
|
||||||
|
targetLanguage: '中文',
|
||||||
|
translator: '赵译员',
|
||||||
|
status: 'completed',
|
||||||
|
notes: '合同翻译讨论',
|
||||||
|
cost: 225,
|
||||||
|
createdTime: '2024-01-15 14:15:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
clientName: '刘女士',
|
||||||
|
clientPhone: '13800138005',
|
||||||
|
clientEmail: 'liu@example.com',
|
||||||
|
appointmentDate: '2024-01-18',
|
||||||
|
appointmentTime: '11:00',
|
||||||
|
duration: 30,
|
||||||
|
serviceType: 'voice',
|
||||||
|
sourceLanguage: '法文',
|
||||||
|
targetLanguage: '中文',
|
||||||
|
status: 'cancelled',
|
||||||
|
notes: '客户临时取消',
|
||||||
|
cost: 0,
|
||||||
|
createdTime: '2024-01-15 14:10:00'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchAppointments = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setAppointments(mockAppointments);
|
||||||
|
setFilteredAppointments(mockAppointments);
|
||||||
|
message.success('预约列表加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载预约列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAppointments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = appointments;
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(apt =>
|
||||||
|
apt.clientName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
apt.clientPhone.includes(searchText) ||
|
||||||
|
apt.sourceLanguage.includes(searchText) ||
|
||||||
|
apt.targetLanguage.includes(searchText) ||
|
||||||
|
(apt.translator && apt.translator.includes(searchText))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(apt => apt.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务类型过滤
|
||||||
|
if (serviceTypeFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(apt => apt.serviceType === serviceTypeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期范围过滤
|
||||||
|
if (dateRange) {
|
||||||
|
const [startDate, endDate] = dateRange;
|
||||||
|
filtered = filtered.filter(apt => {
|
||||||
|
const aptDate = dayjs(apt.appointmentDate);
|
||||||
|
return aptDate.isAfter(startDate.subtract(1, 'day')) &&
|
||||||
|
aptDate.isBefore(endDate.add(1, 'day'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredAppointments(filtered);
|
||||||
|
}, [appointments, searchText, statusFilter, serviceTypeFilter, dateRange]);
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
pending: { color: 'orange', text: '待确认' },
|
||||||
|
confirmed: { color: 'blue', text: '已确认' },
|
||||||
|
'in-progress': { color: 'green', text: '进行中' },
|
||||||
|
completed: { color: 'cyan', text: '已完成' },
|
||||||
|
cancelled: { color: 'red', text: '已取消' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServiceTypeTag = (type: string) => {
|
||||||
|
const typeConfig = {
|
||||||
|
voice: { color: 'blue', text: '语音翻译', icon: <PhoneOutlined /> },
|
||||||
|
video: { color: 'green', text: '视频翻译', icon: <VideoCameraOutlined /> },
|
||||||
|
document: { color: 'purple', text: '文档讨论', icon: <EyeOutlined /> }
|
||||||
|
};
|
||||||
|
const config = typeConfig[type as keyof typeof typeConfig];
|
||||||
|
return (
|
||||||
|
<Tag color={config.color} icon={config.icon}>
|
||||||
|
{config.text}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = (appointmentId: string, newStatus: string) => {
|
||||||
|
const updatedAppointments = appointments.map(apt =>
|
||||||
|
apt.id === appointmentId ? { ...apt, status: newStatus as Appointment['status'] } : apt
|
||||||
|
);
|
||||||
|
setAppointments(updatedAppointments);
|
||||||
|
message.success('状态更新成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (appointment: Appointment) => {
|
||||||
|
setEditingAppointment(appointment);
|
||||||
|
form.setFieldsValue({
|
||||||
|
...appointment,
|
||||||
|
appointmentDate: dayjs(appointment.appointmentDate),
|
||||||
|
appointmentTime: dayjs(appointment.appointmentTime, 'HH:mm')
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (appointment: Appointment) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除 ${appointment.clientName} 的预约吗?`,
|
||||||
|
onOk: () => {
|
||||||
|
const newAppointments = appointments.filter(apt => apt.id !== appointment.id);
|
||||||
|
setAppointments(newAppointments);
|
||||||
|
message.success('预约删除成功');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (values: any) => {
|
||||||
|
try {
|
||||||
|
const appointmentData = {
|
||||||
|
...values,
|
||||||
|
appointmentDate: values.appointmentDate.format('YYYY-MM-DD'),
|
||||||
|
appointmentTime: values.appointmentTime.format('HH:mm'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingAppointment) {
|
||||||
|
// 更新预约
|
||||||
|
const updatedAppointments = appointments.map(apt =>
|
||||||
|
apt.id === editingAppointment.id ? { ...apt, ...appointmentData } : apt
|
||||||
|
);
|
||||||
|
setAppointments(updatedAppointments);
|
||||||
|
message.success('预约更新成功');
|
||||||
|
} else {
|
||||||
|
// 新增预约
|
||||||
|
const newAppointment: Appointment = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
...appointmentData,
|
||||||
|
status: 'pending',
|
||||||
|
createdTime: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
setAppointments([...appointments, newAppointment]);
|
||||||
|
message.success('预约创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingAppointment(null);
|
||||||
|
form.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<Appointment> = [
|
||||||
|
{
|
||||||
|
title: '客户信息',
|
||||||
|
key: 'client',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
|
||||||
|
<Avatar size="small" icon={<UserOutlined />} />
|
||||||
|
<span style={{ marginLeft: 8, fontWeight: 'bold' }}>{record.clientName}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
{record.clientPhone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '预约时间',
|
||||||
|
key: 'datetime',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
|
||||||
|
<CalendarOutlined style={{ marginRight: 4 }} />
|
||||||
|
{record.appointmentDate}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||||
|
{record.appointmentTime} ({record.duration}分钟)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '服务类型',
|
||||||
|
dataIndex: 'serviceType',
|
||||||
|
key: 'serviceType',
|
||||||
|
width: 120,
|
||||||
|
render: getServiceTypeTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '语言对',
|
||||||
|
key: 'languages',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<Tag color="blue">{record.sourceLanguage}</Tag>
|
||||||
|
<span style={{ margin: '0 4px' }}>→</span>
|
||||||
|
<Tag color="green">{record.targetLanguage}</Tag>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '译员',
|
||||||
|
dataIndex: 'translator',
|
||||||
|
key: 'translator',
|
||||||
|
width: 100,
|
||||||
|
render: (text) => text || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '费用(元)',
|
||||||
|
dataIndex: 'cost',
|
||||||
|
key: 'cost',
|
||||||
|
width: 100,
|
||||||
|
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{record.status === 'pending' && (
|
||||||
|
<Tooltip title="确认">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
style={{ color: 'green' }}
|
||||||
|
onClick={() => handleStatusChange(record.id, 'confirmed')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{record.status !== 'cancelled' && record.status !== 'completed' && (
|
||||||
|
<Tooltip title="取消">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
danger
|
||||||
|
onClick={() => handleStatusChange(record.id, 'cancelled')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = {
|
||||||
|
total: filteredAppointments.length,
|
||||||
|
pending: filteredAppointments.filter(a => a.status === 'pending').length,
|
||||||
|
confirmed: filteredAppointments.filter(a => a.status === 'confirmed').length,
|
||||||
|
inProgress: filteredAppointments.filter(a => a.status === 'in-progress').length,
|
||||||
|
completed: filteredAppointments.filter(a => a.status === 'completed').length,
|
||||||
|
totalRevenue: filteredAppointments.filter(a => a.status === 'completed').reduce((sum, a) => sum + a.cost, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Title level={2}>预约管理</Title>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总预约数"
|
||||||
|
value={stats.total}
|
||||||
|
prefix={<CalendarOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="待确认"
|
||||||
|
value={stats.pending}
|
||||||
|
valueStyle={{ color: '#faad14' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="已确认"
|
||||||
|
value={stats.confirmed}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="进行中"
|
||||||
|
value={stats.inProgress}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="已完成"
|
||||||
|
value={stats.completed}
|
||||||
|
valueStyle={{ color: '#13c2c2' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总收入"
|
||||||
|
value={stats.totalRevenue}
|
||||||
|
precision={2}
|
||||||
|
prefix="¥"
|
||||||
|
valueStyle={{ color: '#cf1322' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
{/* 搜索和筛选 */}
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索客户、电话、语言..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="pending">待确认</Option>
|
||||||
|
<Option value="confirmed">已确认</Option>
|
||||||
|
<Option value="in-progress">进行中</Option>
|
||||||
|
<Option value="completed">已完成</Option>
|
||||||
|
<Option value="cancelled">已取消</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={serviceTypeFilter}
|
||||||
|
onChange={setServiceTypeFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="服务类型"
|
||||||
|
>
|
||||||
|
<Option value="all">全部类型</Option>
|
||||||
|
<Option value="voice">语音翻译</Option>
|
||||||
|
<Option value="video">视频翻译</Option>
|
||||||
|
<Option value="document">文档讨论</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<RangePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingAppointment(null);
|
||||||
|
form.resetFields();
|
||||||
|
setModalVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新增预约
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchAppointments}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 预约列表表格 */}
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredAppointments}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredAppointments.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 预约编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title={editingAppointment ? '编辑预约' : '新增预约'}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingAppointment(null);
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSave}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="clientName"
|
||||||
|
label="客户姓名"
|
||||||
|
rules={[{ required: true, message: '请输入客户姓名' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="clientPhone"
|
||||||
|
label="联系电话"
|
||||||
|
rules={[{ required: true, message: '请输入联系电话' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="appointmentDate"
|
||||||
|
label="预约日期"
|
||||||
|
rules={[{ required: true, message: '请选择预约日期' }]}
|
||||||
|
>
|
||||||
|
<DatePicker style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="appointmentTime"
|
||||||
|
label="预约时间"
|
||||||
|
rules={[{ required: true, message: '请选择预约时间' }]}
|
||||||
|
>
|
||||||
|
<TimePicker format="HH:mm" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
name="duration"
|
||||||
|
label="时长(分钟)"
|
||||||
|
rules={[{ required: true, message: '请输入时长' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
name="serviceType"
|
||||||
|
label="服务类型"
|
||||||
|
rules={[{ required: true, message: '请选择服务类型' }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="voice">语音翻译</Option>
|
||||||
|
<Option value="video">视频翻译</Option>
|
||||||
|
<Option value="document">文档讨论</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Form.Item
|
||||||
|
name="cost"
|
||||||
|
label="费用(元)"
|
||||||
|
rules={[{ required: true, message: '请输入费用' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="sourceLanguage"
|
||||||
|
label="源语言"
|
||||||
|
rules={[{ required: true, message: '请选择源语言' }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="中文">中文</Option>
|
||||||
|
<Option value="英文">英文</Option>
|
||||||
|
<Option value="日文">日文</Option>
|
||||||
|
<Option value="韩文">韩文</Option>
|
||||||
|
<Option value="法文">法文</Option>
|
||||||
|
<Option value="德文">德文</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="targetLanguage"
|
||||||
|
label="目标语言"
|
||||||
|
rules={[{ required: true, message: '请选择目标语言' }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="中文">中文</Option>
|
||||||
|
<Option value="英文">英文</Option>
|
||||||
|
<Option value="日文">日文</Option>
|
||||||
|
<Option value="韩文">韩文</Option>
|
||||||
|
<Option value="法文">法文</Option>
|
||||||
|
<Option value="德文">德文</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item
|
||||||
|
name="notes"
|
||||||
|
label="备注"
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppointmentList;
|
470
Twilioapp-admin/src/pages/Calls/CallList.tsx
Normal file
470
Twilioapp-admin/src/pages/Calls/CallList.tsx
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
DatePicker,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface CallRecord {
|
||||||
|
id: string;
|
||||||
|
caller: string;
|
||||||
|
callee: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
duration: string;
|
||||||
|
status: 'completed' | 'ongoing' | 'failed' | 'missed';
|
||||||
|
type: 'voice' | 'video';
|
||||||
|
language: string;
|
||||||
|
translator?: string;
|
||||||
|
quality: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [calls, setCalls] = useState<CallRecord[]>([]);
|
||||||
|
const [filteredCalls, setFilteredCalls] = useState<CallRecord[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||||
|
const [selectedCall, setSelectedCall] = useState<CallRecord | null>(null);
|
||||||
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockCalls: CallRecord[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
caller: '张三 (+86 138****1234)',
|
||||||
|
callee: '李四 (+1 555****5678)',
|
||||||
|
startTime: '2024-01-15 14:30:00',
|
||||||
|
endTime: '2024-01-15 14:45:30',
|
||||||
|
duration: '15:30',
|
||||||
|
status: 'completed',
|
||||||
|
type: 'video',
|
||||||
|
language: '中文-英文',
|
||||||
|
translator: '王译员',
|
||||||
|
quality: 4.8,
|
||||||
|
cost: 45.50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
caller: '李四 (+1 555****5678)',
|
||||||
|
callee: '王五 (+86 139****5678)',
|
||||||
|
startTime: '2024-01-15 14:25:00',
|
||||||
|
endTime: '',
|
||||||
|
duration: '08:45',
|
||||||
|
status: 'ongoing',
|
||||||
|
type: 'voice',
|
||||||
|
language: '英文-中文',
|
||||||
|
translator: '赵译员',
|
||||||
|
quality: 0,
|
||||||
|
cost: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
caller: '王五 (+86 139****5678)',
|
||||||
|
callee: '赵六 (+81 90****1234)',
|
||||||
|
startTime: '2024-01-15 14:20:00',
|
||||||
|
endTime: '2024-01-15 14:42:10',
|
||||||
|
duration: '22:10',
|
||||||
|
status: 'completed',
|
||||||
|
type: 'video',
|
||||||
|
language: '中文-日文',
|
||||||
|
translator: '孙译员',
|
||||||
|
quality: 4.9,
|
||||||
|
cost: 66.30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
caller: '赵六 (+81 90****1234)',
|
||||||
|
callee: '孙七 (+86 137****9876)',
|
||||||
|
startTime: '2024-01-15 14:15:00',
|
||||||
|
endTime: '2024-01-15 14:20:15',
|
||||||
|
duration: '05:15',
|
||||||
|
status: 'failed',
|
||||||
|
type: 'voice',
|
||||||
|
language: '日文-中文',
|
||||||
|
translator: '',
|
||||||
|
quality: 0,
|
||||||
|
cost: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
caller: '孙七 (+86 137****9876)',
|
||||||
|
callee: '周八 (+49 30****5678)',
|
||||||
|
startTime: '2024-01-15 14:10:00',
|
||||||
|
endTime: '',
|
||||||
|
duration: '00:00',
|
||||||
|
status: 'missed',
|
||||||
|
type: 'voice',
|
||||||
|
language: '中文-德文',
|
||||||
|
translator: '',
|
||||||
|
quality: 0,
|
||||||
|
cost: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchCalls = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setCalls(mockCalls);
|
||||||
|
setFilteredCalls(mockCalls);
|
||||||
|
message.success('通话记录加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载通话记录失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCalls();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = calls;
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(call =>
|
||||||
|
call.caller.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
call.callee.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
call.language.includes(searchText) ||
|
||||||
|
(call.translator && call.translator.includes(searchText))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(call => call.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型过滤
|
||||||
|
if (typeFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(call => call.type === typeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredCalls(filtered);
|
||||||
|
}, [calls, searchText, statusFilter, typeFilter]);
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
completed: { color: 'green', text: '已完成' },
|
||||||
|
ongoing: { color: 'blue', text: '进行中' },
|
||||||
|
failed: { color: 'red', text: '失败' },
|
||||||
|
missed: { color: 'orange', text: '未接听' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeTag = (type: string) => {
|
||||||
|
return type === 'video' ?
|
||||||
|
<Tag color="purple">视频通话</Tag> :
|
||||||
|
<Tag color="cyan">语音通话</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<CallRecord> = [
|
||||||
|
{
|
||||||
|
title: '通话ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '呼叫方',
|
||||||
|
dataIndex: 'caller',
|
||||||
|
key: 'caller',
|
||||||
|
width: 200,
|
||||||
|
render: (text) => (
|
||||||
|
<div>
|
||||||
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '接听方',
|
||||||
|
dataIndex: 'callee',
|
||||||
|
key: 'callee',
|
||||||
|
width: 200,
|
||||||
|
render: (text) => (
|
||||||
|
<div>
|
||||||
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开始时间',
|
||||||
|
dataIndex: 'startTime',
|
||||||
|
key: 'startTime',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通话时长',
|
||||||
|
dataIndex: 'duration',
|
||||||
|
key: 'duration',
|
||||||
|
width: 100,
|
||||||
|
render: (text) => (
|
||||||
|
<div>
|
||||||
|
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
width: 100,
|
||||||
|
render: getTypeTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '语言',
|
||||||
|
dataIndex: 'language',
|
||||||
|
key: 'language',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '译员',
|
||||||
|
dataIndex: 'translator',
|
||||||
|
key: 'translator',
|
||||||
|
width: 100,
|
||||||
|
render: (text) => text || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '评分',
|
||||||
|
dataIndex: 'quality',
|
||||||
|
key: 'quality',
|
||||||
|
width: 80,
|
||||||
|
render: (score) => score > 0 ? `${score}/5` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '费用(元)',
|
||||||
|
dataIndex: 'cost',
|
||||||
|
key: 'cost',
|
||||||
|
width: 100,
|
||||||
|
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCall(record);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = {
|
||||||
|
total: filteredCalls.length,
|
||||||
|
completed: filteredCalls.filter(c => c.status === 'completed').length,
|
||||||
|
ongoing: filteredCalls.filter(c => c.status === 'ongoing').length,
|
||||||
|
failed: filteredCalls.filter(c => c.status === 'failed').length,
|
||||||
|
totalRevenue: filteredCalls.reduce((sum, c) => sum + c.cost, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
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={<PhoneOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="已完成"
|
||||||
|
value={stats.completed}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="进行中"
|
||||||
|
value={stats.ongoing}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总收入"
|
||||||
|
value={stats.totalRevenue}
|
||||||
|
precision={2}
|
||||||
|
prefix="¥"
|
||||||
|
valueStyle={{ color: '#cf1322' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
{/* 搜索和筛选 */}
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索呼叫方、接听方、译员..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="completed">已完成</Option>
|
||||||
|
<Option value="ongoing">进行中</Option>
|
||||||
|
<Option value="failed">失败</Option>
|
||||||
|
<Option value="missed">未接听</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={typeFilter}
|
||||||
|
onChange={setTypeFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="类型筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部类型</Option>
|
||||||
|
<Option value="voice">语音通话</Option>
|
||||||
|
<Option value="video">视频通话</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<RangePicker style={{ width: '100%' }} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchCalls}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 通话记录表格 */}
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredCalls}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredCalls.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 详情弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="通话详情"
|
||||||
|
open={detailModalVisible}
|
||||||
|
onCancel={() => setDetailModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedCall && (
|
||||||
|
<div>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<p><strong>通话ID:</strong> {selectedCall.id}</p>
|
||||||
|
<p><strong>呼叫方:</strong> {selectedCall.caller}</p>
|
||||||
|
<p><strong>接听方:</strong> {selectedCall.callee}</p>
|
||||||
|
<p><strong>开始时间:</strong> {selectedCall.startTime}</p>
|
||||||
|
<p><strong>结束时间:</strong> {selectedCall.endTime || '进行中'}</p>
|
||||||
|
<p><strong>通话时长:</strong> {selectedCall.duration}</p>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<p><strong>状态:</strong> {getStatusTag(selectedCall.status)}</p>
|
||||||
|
<p><strong>类型:</strong> {getTypeTag(selectedCall.type)}</p>
|
||||||
|
<p><strong>语言:</strong> {selectedCall.language}</p>
|
||||||
|
<p><strong>译员:</strong> {selectedCall.translator || '无'}</p>
|
||||||
|
<p><strong>服务评分:</strong> {selectedCall.quality > 0 ? `${selectedCall.quality}/5` : '未评分'}</p>
|
||||||
|
<p><strong>费用:</strong> {selectedCall.cost > 0 ? `¥${selectedCall.cost.toFixed(2)}` : '免费'}</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CallList;
|
@ -1,55 +1,231 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, Row, Col, Statistic, Typography } from 'antd';
|
import {
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Typography,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Progress,
|
||||||
|
Spin,
|
||||||
|
message,
|
||||||
|
Space,
|
||||||
|
Button
|
||||||
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
DollarOutlined
|
DollarOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
TrophyOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface DashboardData {
|
||||||
|
totalCalls: number;
|
||||||
|
totalDocuments: number;
|
||||||
|
totalAppointments: number;
|
||||||
|
totalRevenue: number;
|
||||||
|
activeUsers: number;
|
||||||
|
videoCalls: number;
|
||||||
|
recentCalls: Array<{
|
||||||
|
id: string;
|
||||||
|
caller: string;
|
||||||
|
duration: string;
|
||||||
|
status: 'completed' | 'ongoing' | 'failed';
|
||||||
|
time: string;
|
||||||
|
}>;
|
||||||
|
systemStatus: {
|
||||||
|
api: 'online' | 'offline';
|
||||||
|
database: 'online' | 'offline';
|
||||||
|
twilio: 'online' | 'offline';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [data, setData] = useState<DashboardData>({
|
||||||
|
totalCalls: 0,
|
||||||
|
totalDocuments: 0,
|
||||||
|
totalAppointments: 0,
|
||||||
|
totalRevenue: 0,
|
||||||
|
activeUsers: 0,
|
||||||
|
videoCalls: 0,
|
||||||
|
recentCalls: [],
|
||||||
|
systemStatus: {
|
||||||
|
api: 'online',
|
||||||
|
database: 'online',
|
||||||
|
twilio: 'online'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchDashboardData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const mockData: DashboardData = {
|
||||||
|
totalCalls: 1128,
|
||||||
|
totalDocuments: 892,
|
||||||
|
totalAppointments: 456,
|
||||||
|
totalRevenue: 25680,
|
||||||
|
activeUsers: 89,
|
||||||
|
videoCalls: 234,
|
||||||
|
recentCalls: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
caller: '张三',
|
||||||
|
duration: '15:30',
|
||||||
|
status: 'completed',
|
||||||
|
time: '2024-01-15 14:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
caller: '李四',
|
||||||
|
duration: '08:45',
|
||||||
|
status: 'ongoing',
|
||||||
|
time: '2024-01-15 14:25'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
caller: '王五',
|
||||||
|
duration: '22:10',
|
||||||
|
status: 'completed',
|
||||||
|
time: '2024-01-15 14:20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
caller: '赵六',
|
||||||
|
duration: '05:15',
|
||||||
|
status: 'failed',
|
||||||
|
time: '2024-01-15 14:15'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
systemStatus: {
|
||||||
|
api: 'online',
|
||||||
|
database: 'online',
|
||||||
|
twilio: 'online'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setData(mockData);
|
||||||
|
message.success('仪表板数据加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取仪表板数据失败:', error);
|
||||||
|
message.error('获取仪表板数据失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDashboardData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const recentCallsColumns = [
|
||||||
|
{
|
||||||
|
title: '呼叫者',
|
||||||
|
dataIndex: 'caller',
|
||||||
|
key: 'caller',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通话时长',
|
||||||
|
dataIndex: 'duration',
|
||||||
|
key: 'duration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
render: (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
completed: { color: 'green', text: '已完成' },
|
||||||
|
ongoing: { color: 'blue', text: '进行中' },
|
||||||
|
failed: { color: 'red', text: '失败' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'time',
|
||||||
|
key: 'time',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
padding: '24px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '400px'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" tip="加载仪表板数据中..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px' }}>
|
<div style={{ padding: '24px' }}>
|
||||||
<Title level={2}>仪表板</Title>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
|
||||||
|
<Title level={2} style={{ margin: 0 }}>仪表板</Title>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchDashboardData}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Row gutter={16}>
|
{/* 统计卡片 */}
|
||||||
<Col span={6}>
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总通话数"
|
title="总通话数"
|
||||||
value={1128}
|
value={data.totalCalls}
|
||||||
prefix={<PhoneOutlined />}
|
prefix={<PhoneOutlined />}
|
||||||
valueStyle={{ color: '#3f8600' }}
|
valueStyle={{ color: '#3f8600' }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="文档翻译"
|
title="文档翻译"
|
||||||
value={892}
|
value={data.totalDocuments}
|
||||||
prefix={<FileTextOutlined />}
|
prefix={<FileTextOutlined />}
|
||||||
valueStyle={{ color: '#1890ff' }}
|
valueStyle={{ color: '#1890ff' }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="预约服务"
|
title="预约服务"
|
||||||
value={456}
|
value={data.totalAppointments}
|
||||||
prefix={<CalendarOutlined />}
|
prefix={<CalendarOutlined />}
|
||||||
valueStyle={{ color: '#faad14' }}
|
valueStyle={{ color: '#faad14' }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总收入"
|
title="总收入"
|
||||||
value={25680}
|
value={data.totalRevenue}
|
||||||
prefix={<DollarOutlined />}
|
prefix={<DollarOutlined />}
|
||||||
valueStyle={{ color: '#cf1322' }}
|
valueStyle={{ color: '#cf1322' }}
|
||||||
suffix="元"
|
suffix="元"
|
||||||
@ -58,13 +234,112 @@ const Dashboard: React.FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<div style={{ marginTop: '24px' }}>
|
{/* 第二行统计 */}
|
||||||
<Card title="系统状态">
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
<p>✅ 系统运行正常</p>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<p>✅ 所有服务在线</p>
|
<Card>
|
||||||
<p>✅ 数据库连接正常</p>
|
<Statistic
|
||||||
|
title="活跃用户"
|
||||||
|
value={data.activeUsers}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
valueStyle={{ color: '#722ed1' }}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="视频通话"
|
||||||
|
value={data.videoCalls}
|
||||||
|
prefix={<VideoCameraOutlined />}
|
||||||
|
valueStyle={{ color: '#eb2f96' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="成功率"
|
||||||
|
value={94.5}
|
||||||
|
prefix={<TrophyOutlined />}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Text type="secondary">系统负载</Text>
|
||||||
|
<Progress
|
||||||
|
type="circle"
|
||||||
|
percent={75}
|
||||||
|
size={80}
|
||||||
|
strokeColor={{
|
||||||
|
'0%': '#108ee9',
|
||||||
|
'100%': '#87d068',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
{/* 最近通话记录 */}
|
||||||
|
<Col xs={24} lg={16}>
|
||||||
|
<Card title="最近通话记录" style={{ marginBottom: '24px' }}>
|
||||||
|
<Table
|
||||||
|
columns={recentCallsColumns}
|
||||||
|
dataSource={data.recentCalls}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 系统状态 */}
|
||||||
|
<Col xs={24} lg={8}>
|
||||||
|
<Card title="系统状态" style={{ marginBottom: '24px' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text>API服务</Text>
|
||||||
|
<Tag color={data.systemStatus.api === 'online' ? 'green' : 'red'}>
|
||||||
|
{data.systemStatus.api === 'online' ? '在线' : '离线'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text>数据库</Text>
|
||||||
|
<Tag color={data.systemStatus.database === 'online' ? 'green' : 'red'}>
|
||||||
|
{data.systemStatus.database === 'online' ? '在线' : '离线'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text>Twilio服务</Text>
|
||||||
|
<Tag color={data.systemStatus.twilio === 'online' ? 'green' : 'red'}>
|
||||||
|
{data.systemStatus.twilio === 'online' ? '在线' : '离线'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 快速操作 */}
|
||||||
|
<Card title="快速操作">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Button type="primary" block icon={<PhoneOutlined />}>
|
||||||
|
查看通话记录
|
||||||
|
</Button>
|
||||||
|
<Button block icon={<FileTextOutlined />}>
|
||||||
|
管理文档翻译
|
||||||
|
</Button>
|
||||||
|
<Button block icon={<CalendarOutlined />}>
|
||||||
|
预约管理
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
404
Twilioapp-admin/src/pages/Documents/DocumentList.tsx
Normal file
404
Twilioapp-admin/src/pages/Documents/DocumentList.tsx
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Upload,
|
||||||
|
Progress,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Tooltip
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FilePdfOutlined,
|
||||||
|
FileWordOutlined,
|
||||||
|
FileExcelOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
TranslationOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import type { UploadProps } from 'antd';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface Document {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
fileType: string;
|
||||||
|
fileSize: number;
|
||||||
|
uploadTime: string;
|
||||||
|
status: 'pending' | 'translating' | 'completed' | 'failed';
|
||||||
|
sourceLanguage: string;
|
||||||
|
targetLanguage: string;
|
||||||
|
translator?: string;
|
||||||
|
progress: number;
|
||||||
|
downloadCount: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [documents, setDocuments] = useState<Document[]>([]);
|
||||||
|
const [filteredDocuments, setFilteredDocuments] = useState<Document[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [uploadModalVisible, setUploadModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockDocuments: Document[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
fileName: '商业合同.pdf',
|
||||||
|
fileType: 'pdf',
|
||||||
|
fileSize: 2048576,
|
||||||
|
uploadTime: '2024-01-15 14:30:00',
|
||||||
|
status: 'completed',
|
||||||
|
sourceLanguage: '中文',
|
||||||
|
targetLanguage: '英文',
|
||||||
|
translator: '王译员',
|
||||||
|
progress: 100,
|
||||||
|
downloadCount: 5,
|
||||||
|
cost: 128.50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
fileName: '技术文档.docx',
|
||||||
|
fileType: 'docx',
|
||||||
|
fileSize: 1536000,
|
||||||
|
uploadTime: '2024-01-15 14:25:00',
|
||||||
|
status: 'translating',
|
||||||
|
sourceLanguage: '英文',
|
||||||
|
targetLanguage: '中文',
|
||||||
|
translator: '李译员',
|
||||||
|
progress: 65,
|
||||||
|
downloadCount: 0,
|
||||||
|
cost: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
fileName: '财务报表.xlsx',
|
||||||
|
fileType: 'xlsx',
|
||||||
|
fileSize: 512000,
|
||||||
|
uploadTime: '2024-01-15 14:20:00',
|
||||||
|
status: 'completed',
|
||||||
|
sourceLanguage: '中文',
|
||||||
|
targetLanguage: '日文',
|
||||||
|
translator: '张译员',
|
||||||
|
progress: 100,
|
||||||
|
downloadCount: 12,
|
||||||
|
cost: 85.30
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchDocuments = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setDocuments(mockDocuments);
|
||||||
|
setFilteredDocuments(mockDocuments);
|
||||||
|
message.success('文档列表加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载文档列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDocuments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = documents;
|
||||||
|
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(doc =>
|
||||||
|
doc.fileName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
doc.sourceLanguage.includes(searchText) ||
|
||||||
|
doc.targetLanguage.includes(searchText) ||
|
||||||
|
(doc.translator && doc.translator.includes(searchText))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(doc => doc.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredDocuments(filtered);
|
||||||
|
}, [documents, searchText, statusFilter]);
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
pending: { color: 'orange', text: '待处理' },
|
||||||
|
translating: { color: 'blue', text: '翻译中' },
|
||||||
|
completed: { color: 'green', text: '已完成' },
|
||||||
|
failed: { color: 'red', text: '失败' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (fileType: string) => {
|
||||||
|
const iconMap = {
|
||||||
|
pdf: <FilePdfOutlined style={{ color: '#ff4d4f' }} />,
|
||||||
|
docx: <FileWordOutlined style={{ color: '#1890ff' }} />,
|
||||||
|
xlsx: <FileExcelOutlined style={{ color: '#52c41a' }} />,
|
||||||
|
txt: <FileTextOutlined style={{ color: '#722ed1' }} />
|
||||||
|
};
|
||||||
|
return iconMap[fileType as keyof typeof iconMap] || <FileTextOutlined />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = (document: Document) => {
|
||||||
|
if (document.status !== 'completed') {
|
||||||
|
message.warning('文档尚未翻译完成,无法下载');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.success(`开始下载:${document.fileName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (document: Document) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除文档 "${document.fileName}" 吗?`,
|
||||||
|
onOk: () => {
|
||||||
|
const newDocuments = documents.filter(doc => doc.id !== document.id);
|
||||||
|
setDocuments(newDocuments);
|
||||||
|
message.success('文档删除成功');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<Document> = [
|
||||||
|
{
|
||||||
|
title: '文件名',
|
||||||
|
dataIndex: 'fileName',
|
||||||
|
key: 'fileName',
|
||||||
|
width: 250,
|
||||||
|
render: (text, record) => (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
{getFileIcon(record.fileType)}
|
||||||
|
<span style={{ marginLeft: 8 }}>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件大小',
|
||||||
|
dataIndex: 'fileSize',
|
||||||
|
key: 'fileSize',
|
||||||
|
width: 120,
|
||||||
|
render: formatFileSize
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '翻译进度',
|
||||||
|
dataIndex: 'progress',
|
||||||
|
key: 'progress',
|
||||||
|
width: 120,
|
||||||
|
render: (progress, record) => (
|
||||||
|
<Progress
|
||||||
|
percent={progress}
|
||||||
|
size="small"
|
||||||
|
status={record.status === 'failed' ? 'exception' : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '源语言',
|
||||||
|
dataIndex: 'sourceLanguage',
|
||||||
|
key: 'sourceLanguage',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '目标语言',
|
||||||
|
dataIndex: 'targetLanguage',
|
||||||
|
key: 'targetLanguage',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '费用(元)',
|
||||||
|
dataIndex: 'cost',
|
||||||
|
key: 'cost',
|
||||||
|
width: 100,
|
||||||
|
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 160,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="下载">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
disabled={record.status !== 'completed'}
|
||||||
|
onClick={() => handleDownload(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: filteredDocuments.length,
|
||||||
|
completed: filteredDocuments.filter(d => d.status === 'completed').length,
|
||||||
|
translating: filteredDocuments.filter(d => d.status === 'translating').length,
|
||||||
|
totalRevenue: filteredDocuments.reduce((sum, d) => sum + d.cost, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
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={<FileTextOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="已完成"
|
||||||
|
value={stats.completed}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="翻译中"
|
||||||
|
value={stats.translating}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总收入"
|
||||||
|
value={stats.totalRevenue}
|
||||||
|
precision={2}
|
||||||
|
prefix="¥"
|
||||||
|
valueStyle={{ color: '#cf1322' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索文件名、语言、译员..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="pending">待处理</Option>
|
||||||
|
<Option value="translating">翻译中</Option>
|
||||||
|
<Option value="completed">已完成</Option>
|
||||||
|
<Option value="failed">失败</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={10}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
onClick={() => setUploadModalVisible(true)}
|
||||||
|
>
|
||||||
|
上传文档
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchDocuments}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredDocuments}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredDocuments.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentList;
|
525
Twilioapp-admin/src/pages/Payments/PaymentList.tsx
Normal file
525
Twilioapp-admin/src/pages/Payments/PaymentList.tsx
Normal file
@ -0,0 +1,525 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Tooltip,
|
||||||
|
DatePicker,
|
||||||
|
Descriptions,
|
||||||
|
Divider
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
CreditCardOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
UndoOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
interface Payment {
|
||||||
|
id: string;
|
||||||
|
orderId: string;
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
amount: number;
|
||||||
|
paymentMethod: 'credit_card' | 'alipay' | 'wechat' | 'paypal';
|
||||||
|
status: 'pending' | 'completed' | 'failed' | 'refunded' | 'cancelled';
|
||||||
|
transactionId: string;
|
||||||
|
serviceType: 'voice_call' | 'video_call' | 'document_translation' | 'appointment';
|
||||||
|
serviceName: string;
|
||||||
|
createdAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
refundAmount?: number;
|
||||||
|
refundReason?: string;
|
||||||
|
currency: string;
|
||||||
|
fee: number; // 手续费
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [payments, setPayments] = useState<Payment[]>([]);
|
||||||
|
const [filteredPayments, setFilteredPayments] = useState<Payment[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [methodFilter, setMethodFilter] = useState<string>('all');
|
||||||
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
|
||||||
|
const [selectedPayment, setSelectedPayment] = useState<Payment | null>(null);
|
||||||
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockPayments: Payment[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
orderId: 'ORD-2024-001',
|
||||||
|
userId: 'U001',
|
||||||
|
userName: '张三',
|
||||||
|
amount: 150.00,
|
||||||
|
paymentMethod: 'alipay',
|
||||||
|
status: 'completed',
|
||||||
|
transactionId: 'TXN-20240115-001',
|
||||||
|
serviceType: 'voice_call',
|
||||||
|
serviceName: '中英文语音翻译',
|
||||||
|
createdAt: '2024-01-15 14:30:00',
|
||||||
|
completedAt: '2024-01-15 14:31:00',
|
||||||
|
currency: 'CNY',
|
||||||
|
fee: 4.50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
orderId: 'ORD-2024-002',
|
||||||
|
userId: 'U002',
|
||||||
|
userName: '李四',
|
||||||
|
amount: 280.00,
|
||||||
|
paymentMethod: 'wechat',
|
||||||
|
status: 'completed',
|
||||||
|
transactionId: 'TXN-20240115-002',
|
||||||
|
serviceType: 'document_translation',
|
||||||
|
serviceName: '商务文档翻译',
|
||||||
|
createdAt: '2024-01-15 13:45:00',
|
||||||
|
completedAt: '2024-01-15 13:46:00',
|
||||||
|
currency: 'CNY',
|
||||||
|
fee: 8.40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
orderId: 'ORD-2024-003',
|
||||||
|
userId: 'U003',
|
||||||
|
userName: '王五',
|
||||||
|
amount: 320.00,
|
||||||
|
paymentMethod: 'credit_card',
|
||||||
|
status: 'refunded',
|
||||||
|
transactionId: 'TXN-20240115-003',
|
||||||
|
serviceType: 'video_call',
|
||||||
|
serviceName: '视频会议翻译',
|
||||||
|
createdAt: '2024-01-15 12:20:00',
|
||||||
|
completedAt: '2024-01-15 12:21:00',
|
||||||
|
refundAmount: 320.00,
|
||||||
|
refundReason: '服务质量问题',
|
||||||
|
currency: 'CNY',
|
||||||
|
fee: 9.60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
orderId: 'ORD-2024-004',
|
||||||
|
userId: 'U004',
|
||||||
|
userName: '赵六',
|
||||||
|
amount: 450.00,
|
||||||
|
paymentMethod: 'paypal',
|
||||||
|
status: 'pending',
|
||||||
|
transactionId: 'TXN-20240115-004',
|
||||||
|
serviceType: 'appointment',
|
||||||
|
serviceName: '专业咨询预约',
|
||||||
|
createdAt: '2024-01-15 16:10:00',
|
||||||
|
currency: 'USD',
|
||||||
|
fee: 13.50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
orderId: 'ORD-2024-005',
|
||||||
|
userId: 'U005',
|
||||||
|
userName: '孙七',
|
||||||
|
amount: 180.00,
|
||||||
|
paymentMethod: 'alipay',
|
||||||
|
status: 'failed',
|
||||||
|
transactionId: 'TXN-20240115-005',
|
||||||
|
serviceType: 'voice_call',
|
||||||
|
serviceName: '法语口译服务',
|
||||||
|
createdAt: '2024-01-15 11:30:00',
|
||||||
|
currency: 'CNY',
|
||||||
|
fee: 5.40
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchPayments = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setPayments(mockPayments);
|
||||||
|
setFilteredPayments(mockPayments);
|
||||||
|
message.success('支付记录加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载支付记录失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPayments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = payments;
|
||||||
|
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(payment =>
|
||||||
|
payment.orderId.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
payment.userName.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
payment.transactionId.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
payment.serviceName.includes(searchText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(payment => payment.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (methodFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(payment => payment.paymentMethod === methodFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
const [start, end] = dateRange;
|
||||||
|
filtered = filtered.filter(payment => {
|
||||||
|
const paymentDate = dayjs(payment.createdAt);
|
||||||
|
return paymentDate.isAfter(start.startOf('day')) && paymentDate.isBefore(end.endOf('day'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredPayments(filtered);
|
||||||
|
}, [payments, searchText, statusFilter, methodFilter, dateRange]);
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
pending: { color: 'orange', text: '待支付', icon: <ExclamationCircleOutlined /> },
|
||||||
|
completed: { color: 'green', text: '已完成', icon: <CheckCircleOutlined /> },
|
||||||
|
failed: { color: 'red', text: '支付失败', icon: <CloseCircleOutlined /> },
|
||||||
|
refunded: { color: 'purple', text: '已退款', icon: <UndoOutlined /> },
|
||||||
|
cancelled: { color: 'gray', text: '已取消', icon: <CloseCircleOutlined /> }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentMethodTag = (method: string) => {
|
||||||
|
const methodConfig = {
|
||||||
|
credit_card: { color: 'blue', text: '信用卡' },
|
||||||
|
alipay: { color: 'green', text: '支付宝' },
|
||||||
|
wechat: { color: 'lime', text: '微信支付' },
|
||||||
|
paypal: { color: 'gold', text: 'PayPal' }
|
||||||
|
};
|
||||||
|
const config = methodConfig[method as keyof typeof methodConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetail = (payment: Payment) => {
|
||||||
|
setSelectedPayment(payment);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<Payment> = [
|
||||||
|
{
|
||||||
|
title: '订单号',
|
||||||
|
dataIndex: 'orderId',
|
||||||
|
key: 'orderId',
|
||||||
|
width: 140,
|
||||||
|
render: (orderId) => (
|
||||||
|
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>
|
||||||
|
{orderId}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
dataIndex: 'userName',
|
||||||
|
key: 'userName',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
key: 'amount',
|
||||||
|
width: 120,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 'bold' }}>
|
||||||
|
{record.currency} {record.amount.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
手续费: {record.currency} {record.fee.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支付方式',
|
||||||
|
dataIndex: 'paymentMethod',
|
||||||
|
key: 'paymentMethod',
|
||||||
|
width: 120,
|
||||||
|
render: getPaymentMethodTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 120,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '服务类型',
|
||||||
|
dataIndex: 'serviceName',
|
||||||
|
key: 'serviceName',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '交易时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 120,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleViewDetail(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="下载凭证">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
disabled={record.status !== 'completed'}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: filteredPayments.length,
|
||||||
|
completed: filteredPayments.filter(p => p.status === 'completed').length,
|
||||||
|
pending: filteredPayments.filter(p => p.status === 'pending').length,
|
||||||
|
failed: filteredPayments.filter(p => p.status === 'failed').length,
|
||||||
|
refunded: filteredPayments.filter(p => p.status === 'refunded').length,
|
||||||
|
totalAmount: filteredPayments
|
||||||
|
.filter(p => p.status === 'completed')
|
||||||
|
.reduce((sum, p) => sum + p.amount, 0),
|
||||||
|
totalFee: filteredPayments
|
||||||
|
.filter(p => p.status === 'completed')
|
||||||
|
.reduce((sum, p) => sum + p.fee, 0),
|
||||||
|
refundAmount: filteredPayments
|
||||||
|
.filter(p => p.status === 'refunded')
|
||||||
|
.reduce((sum, p) => sum + (p.refundAmount || 0), 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
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={<CreditCardOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="成功交易"
|
||||||
|
value={stats.completed}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="交易总额"
|
||||||
|
value={stats.totalAmount}
|
||||||
|
precision={2}
|
||||||
|
prefix={<DollarOutlined />}
|
||||||
|
valueStyle={{ color: '#fa8c16' }}
|
||||||
|
suffix="CNY"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="手续费收入"
|
||||||
|
value={stats.totalFee}
|
||||||
|
precision={2}
|
||||||
|
valueStyle={{ color: '#722ed1' }}
|
||||||
|
suffix="CNY"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索订单号、用户、交易号..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="pending">待支付</Option>
|
||||||
|
<Option value="completed">已完成</Option>
|
||||||
|
<Option value="failed">支付失败</Option>
|
||||||
|
<Option value="refunded">已退款</Option>
|
||||||
|
<Option value="cancelled">已取消</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={methodFilter}
|
||||||
|
onChange={setMethodFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="支付方式"
|
||||||
|
>
|
||||||
|
<Option value="all">全部方式</Option>
|
||||||
|
<Option value="credit_card">信用卡</Option>
|
||||||
|
<Option value="alipay">支付宝</Option>
|
||||||
|
<Option value="wechat">微信支付</Option>
|
||||||
|
<Option value="paypal">PayPal</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<RangePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchPayments}
|
||||||
|
loading={loading}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredPayments}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredPayments.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="支付详情"
|
||||||
|
open={detailModalVisible}
|
||||||
|
onCancel={() => setDetailModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedPayment && (
|
||||||
|
<Descriptions column={2} bordered>
|
||||||
|
<Descriptions.Item label="订单号" span={2}>
|
||||||
|
{selectedPayment.orderId}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="交易号" span={2}>
|
||||||
|
{selectedPayment.transactionId}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户">
|
||||||
|
{selectedPayment.userName}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户ID">
|
||||||
|
{selectedPayment.userId}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="服务类型" span={2}>
|
||||||
|
{selectedPayment.serviceName}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="支付金额">
|
||||||
|
{selectedPayment.currency} {selectedPayment.amount.toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="手续费">
|
||||||
|
{selectedPayment.currency} {selectedPayment.fee.toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="支付方式">
|
||||||
|
{getPaymentMethodTag(selectedPayment.paymentMethod)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态">
|
||||||
|
{getStatusTag(selectedPayment.status)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="创建时间" span={2}>
|
||||||
|
{dayjs(selectedPayment.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
{selectedPayment.completedAt && (
|
||||||
|
<Descriptions.Item label="完成时间" span={2}>
|
||||||
|
{dayjs(selectedPayment.completedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
{selectedPayment.refundAmount && (
|
||||||
|
<>
|
||||||
|
<Descriptions.Item label="退款金额">
|
||||||
|
{selectedPayment.currency} {selectedPayment.refundAmount.toFixed(2)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="退款原因">
|
||||||
|
{selectedPayment.refundReason}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentList;
|
637
Twilioapp-admin/src/pages/Settings/SystemSettings.tsx
Normal file
637
Twilioapp-admin/src/pages/Settings/SystemSettings.tsx
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Space,
|
||||||
|
Alert,
|
||||||
|
Badge
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SaveOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
SecurityScanOutlined,
|
||||||
|
NotificationOutlined,
|
||||||
|
GlobalOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface SystemConfig {
|
||||||
|
// 基础设置
|
||||||
|
siteName: string;
|
||||||
|
siteDescription: string;
|
||||||
|
supportEmail: string;
|
||||||
|
supportPhone: string;
|
||||||
|
defaultLanguage: string;
|
||||||
|
timezone: string;
|
||||||
|
|
||||||
|
// Twilio 设置
|
||||||
|
twilioAccountSid: string;
|
||||||
|
twilioAuthToken: string;
|
||||||
|
twilioPhoneNumber: string;
|
||||||
|
twilioWebhookUrl: string;
|
||||||
|
enableVideoCall: boolean;
|
||||||
|
enableVoiceCall: boolean;
|
||||||
|
|
||||||
|
// 支付设置
|
||||||
|
enableAlipay: boolean;
|
||||||
|
enableWechatPay: boolean;
|
||||||
|
enableCreditCard: boolean;
|
||||||
|
enablePaypal: boolean;
|
||||||
|
paymentFeeRate: number;
|
||||||
|
minimumPayment: number;
|
||||||
|
|
||||||
|
// 业务设置
|
||||||
|
defaultCallDuration: number;
|
||||||
|
maxCallDuration: number;
|
||||||
|
translatorCommissionRate: number;
|
||||||
|
autoAssignTranslator: boolean;
|
||||||
|
requirePaymentUpfront: boolean;
|
||||||
|
|
||||||
|
// 通知设置
|
||||||
|
emailNotifications: boolean;
|
||||||
|
smsNotifications: boolean;
|
||||||
|
systemMaintenanceMode: boolean;
|
||||||
|
|
||||||
|
// 安全设置
|
||||||
|
enableTwoFactorAuth: boolean;
|
||||||
|
sessionTimeout: number;
|
||||||
|
maxLoginAttempts: number;
|
||||||
|
passwordMinLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SystemSettings: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [config, setConfig] = useState<SystemConfig | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState('basic');
|
||||||
|
|
||||||
|
// 模拟配置数据
|
||||||
|
const mockConfig: SystemConfig = {
|
||||||
|
siteName: 'Twilio翻译平台',
|
||||||
|
siteDescription: '专业的实时翻译服务平台',
|
||||||
|
supportEmail: 'support@twiliotranslate.com',
|
||||||
|
supportPhone: '400-123-4567',
|
||||||
|
defaultLanguage: 'zh-CN',
|
||||||
|
timezone: 'Asia/Shanghai',
|
||||||
|
|
||||||
|
twilioAccountSid: 'AC1234567890abcdef1234567890abcdef',
|
||||||
|
twilioAuthToken: '********************************',
|
||||||
|
twilioPhoneNumber: '+86-138-0013-8000',
|
||||||
|
twilioWebhookUrl: 'https://api.twiliotranslate.com/webhook',
|
||||||
|
enableVideoCall: true,
|
||||||
|
enableVoiceCall: true,
|
||||||
|
|
||||||
|
enableAlipay: true,
|
||||||
|
enableWechatPay: true,
|
||||||
|
enableCreditCard: true,
|
||||||
|
enablePaypal: false,
|
||||||
|
paymentFeeRate: 3.0,
|
||||||
|
minimumPayment: 10.0,
|
||||||
|
|
||||||
|
defaultCallDuration: 30,
|
||||||
|
maxCallDuration: 120,
|
||||||
|
translatorCommissionRate: 70.0,
|
||||||
|
autoAssignTranslator: true,
|
||||||
|
requirePaymentUpfront: true,
|
||||||
|
|
||||||
|
emailNotifications: true,
|
||||||
|
smsNotifications: false,
|
||||||
|
systemMaintenanceMode: false,
|
||||||
|
|
||||||
|
enableTwoFactorAuth: true,
|
||||||
|
sessionTimeout: 30,
|
||||||
|
maxLoginAttempts: 5,
|
||||||
|
passwordMinLength: 8
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setConfig(mockConfig);
|
||||||
|
form.setFieldsValue(mockConfig);
|
||||||
|
message.success('配置加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载配置失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (values: SystemConfig) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
setConfig(values);
|
||||||
|
message.success('配置保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存配置失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testTwilioConnection = async () => {
|
||||||
|
message.loading('测试Twilio连接中...', 2);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
message.success('Twilio连接测试成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderBasicSettings = () => (
|
||||||
|
<Card title="基础设置" extra={<SettingOutlined />}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="网站名称"
|
||||||
|
name="siteName"
|
||||||
|
rules={[{ required: true, message: '请输入网站名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入网站名称" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="默认语言"
|
||||||
|
name="defaultLanguage"
|
||||||
|
rules={[{ required: true, message: '请选择默认语言' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择默认语言">
|
||||||
|
<Option value="zh-CN">简体中文</Option>
|
||||||
|
<Option value="en-US">English</Option>
|
||||||
|
<Option value="ja-JP">日本語</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="网站描述"
|
||||||
|
name="siteDescription"
|
||||||
|
>
|
||||||
|
<TextArea rows={3} placeholder="请输入网站描述" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="客服邮箱"
|
||||||
|
name="supportEmail"
|
||||||
|
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入客服邮箱" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="客服电话"
|
||||||
|
name="supportPhone"
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入客服电话" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="时区"
|
||||||
|
name="timezone"
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择时区">
|
||||||
|
<Option value="Asia/Shanghai">Asia/Shanghai (UTC+8)</Option>
|
||||||
|
<Option value="America/New_York">America/New_York (UTC-5)</Option>
|
||||||
|
<Option value="Europe/London">Europe/London (UTC+0)</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTwilioSettings = () => (
|
||||||
|
<Card
|
||||||
|
title="Twilio配置"
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Badge status="success" text="已连接" />
|
||||||
|
<Button size="small" onClick={testTwilioConnection}>
|
||||||
|
测试连接
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
message="Twilio配置说明"
|
||||||
|
description="请确保您的Twilio账户有足够的余额,并且已经验证了电话号码。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="Account SID"
|
||||||
|
name="twilioAccountSid"
|
||||||
|
rules={[{ required: true, message: '请输入Account SID' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入Twilio Account SID" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="Auth Token"
|
||||||
|
name="twilioAuthToken"
|
||||||
|
rules={[{ required: true, message: '请输入Auth Token' }]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入Twilio Auth Token" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="电话号码"
|
||||||
|
name="twilioPhoneNumber"
|
||||||
|
rules={[{ required: true, message: '请输入电话号码' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入Twilio电话号码" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="Webhook URL"
|
||||||
|
name="twilioWebhookUrl"
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入Webhook URL" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="启用语音通话"
|
||||||
|
name="enableVoiceCall"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="启用视频通话"
|
||||||
|
name="enableVideoCall"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPaymentSettings = () => (
|
||||||
|
<Card title="支付配置" extra={<DollarOutlined />}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
label="支付宝"
|
||||||
|
name="enableAlipay"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
label="微信支付"
|
||||||
|
name="enableWechatPay"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
label="信用卡"
|
||||||
|
name="enableCreditCard"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
label="PayPal"
|
||||||
|
name="enablePaypal"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="手续费率 (%)"
|
||||||
|
name="paymentFeeRate"
|
||||||
|
rules={[{ required: true, message: '请输入手续费率' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
step={0.1}
|
||||||
|
precision={1}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入手续费率"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="最低支付金额"
|
||||||
|
name="minimumPayment"
|
||||||
|
rules={[{ required: true, message: '请输入最低支付金额' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入最低支付金额"
|
||||||
|
addonAfter="CNY"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBusinessSettings = () => (
|
||||||
|
<Card title="业务配置" extra={<GlobalOutlined />}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="默认通话时长 (分钟)"
|
||||||
|
name="defaultCallDuration"
|
||||||
|
rules={[{ required: true, message: '请输入默认通话时长' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={5}
|
||||||
|
max={180}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入默认通话时长"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="最大通话时长 (分钟)"
|
||||||
|
name="maxCallDuration"
|
||||||
|
rules={[{ required: true, message: '请输入最大通话时长' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={10}
|
||||||
|
max={300}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入最大通话时长"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="译员佣金比例 (%)"
|
||||||
|
name="translatorCommissionRate"
|
||||||
|
rules={[{ required: true, message: '请输入译员佣金比例' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={50}
|
||||||
|
max={90}
|
||||||
|
step={1}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入译员佣金比例"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="自动分配译员"
|
||||||
|
name="autoAssignTranslator"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="要求预付费"
|
||||||
|
name="requirePaymentUpfront"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNotificationSettings = () => (
|
||||||
|
<Card title="通知设置" extra={<NotificationOutlined />}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="邮件通知"
|
||||||
|
name="emailNotifications"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="短信通知"
|
||||||
|
name="smsNotifications"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="系统维护模式"
|
||||||
|
name="systemMaintenanceMode"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{config?.systemMaintenanceMode && (
|
||||||
|
<Alert
|
||||||
|
message="维护模式已启用"
|
||||||
|
description="系统当前处于维护模式,用户无法正常使用服务。"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSecuritySettings = () => (
|
||||||
|
<Card title="安全设置" extra={<SecurityScanOutlined />}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="启用双因素认证"
|
||||||
|
name="enableTwoFactorAuth"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="会话超时 (分钟)"
|
||||||
|
name="sessionTimeout"
|
||||||
|
rules={[{ required: true, message: '请输入会话超时时间' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={5}
|
||||||
|
max={120}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入会话超时时间"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="最大登录尝试次数"
|
||||||
|
name="maxLoginAttempts"
|
||||||
|
rules={[{ required: true, message: '请输入最大登录尝试次数' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={3}
|
||||||
|
max={10}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入最大登录尝试次数"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
label="密码最小长度"
|
||||||
|
name="passwordMinLength"
|
||||||
|
rules={[{ required: true, message: '请输入密码最小长度' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={6}
|
||||||
|
max={20}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入密码最小长度"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabItems = [
|
||||||
|
{
|
||||||
|
key: 'basic',
|
||||||
|
label: '基础设置',
|
||||||
|
children: renderBasicSettings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'twilio',
|
||||||
|
label: 'Twilio配置',
|
||||||
|
children: renderTwilioSettings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'payment',
|
||||||
|
label: '支付配置',
|
||||||
|
children: renderPaymentSettings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'business',
|
||||||
|
label: '业务配置',
|
||||||
|
children: renderBusinessSettings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notification',
|
||||||
|
label: '通知设置',
|
||||||
|
children: renderNotificationSettings()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'security',
|
||||||
|
label: '安全设置',
|
||||||
|
children: renderSecuritySettings()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Title level={2}>系统设置</Title>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSave}
|
||||||
|
initialValues={config || {}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
items={tabItems}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={fetchConfig}
|
||||||
|
loading={loading}
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSettings;
|
477
Twilioapp-admin/src/pages/Translators/TranslatorList.tsx
Normal file
477
Twilioapp-admin/src/pages/Translators/TranslatorList.tsx
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Tooltip,
|
||||||
|
Avatar,
|
||||||
|
Rate,
|
||||||
|
Progress,
|
||||||
|
Badge
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
TrophyOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
FileTextOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface Translator {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
avatar?: string;
|
||||||
|
languages: string[];
|
||||||
|
specialties: string[];
|
||||||
|
rating: number;
|
||||||
|
totalCalls: number;
|
||||||
|
completedCalls: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
status: 'available' | 'busy' | 'offline';
|
||||||
|
experience: number; // 年
|
||||||
|
certifications: string[];
|
||||||
|
joinDate: string;
|
||||||
|
lastActiveTime: string;
|
||||||
|
hourlyRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TranslatorList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [translators, setTranslators] = useState<Translator[]>([]);
|
||||||
|
const [filteredTranslators, setFilteredTranslators] = useState<Translator[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [languageFilter, setLanguageFilter] = useState<string>('all');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockTranslators: Translator[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '王译员',
|
||||||
|
email: 'wang@translator.com',
|
||||||
|
phone: '13800138001',
|
||||||
|
languages: ['中文', '英文', '日文'],
|
||||||
|
specialties: ['商务', '技术', '医疗'],
|
||||||
|
rating: 4.8,
|
||||||
|
totalCalls: 156,
|
||||||
|
completedCalls: 152,
|
||||||
|
totalEarnings: 15600,
|
||||||
|
status: 'available',
|
||||||
|
experience: 5,
|
||||||
|
certifications: ['CATTI二级', '商务英语高级'],
|
||||||
|
joinDate: '2023-06-15',
|
||||||
|
lastActiveTime: '2024-01-15 14:45:00',
|
||||||
|
hourlyRate: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '李译员',
|
||||||
|
email: 'li@translator.com',
|
||||||
|
phone: '13800138002',
|
||||||
|
languages: ['中文', '英文', '法文'],
|
||||||
|
specialties: ['法律', '文学', '艺术'],
|
||||||
|
rating: 4.9,
|
||||||
|
totalCalls: 89,
|
||||||
|
completedCalls: 87,
|
||||||
|
totalEarnings: 12400,
|
||||||
|
status: 'busy',
|
||||||
|
experience: 7,
|
||||||
|
certifications: ['CATTI一级', '法语专业八级'],
|
||||||
|
joinDate: '2023-08-20',
|
||||||
|
lastActiveTime: '2024-01-15 13:15:00',
|
||||||
|
hourlyRate: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '张译员',
|
||||||
|
email: 'zhang@translator.com',
|
||||||
|
phone: '13800138003',
|
||||||
|
languages: ['中文', '德文', '俄文'],
|
||||||
|
specialties: ['工程', '科技', '学术'],
|
||||||
|
rating: 4.7,
|
||||||
|
totalCalls: 67,
|
||||||
|
completedCalls: 65,
|
||||||
|
totalEarnings: 8900,
|
||||||
|
status: 'available',
|
||||||
|
experience: 3,
|
||||||
|
certifications: ['德语专业八级', '俄语专业六级'],
|
||||||
|
joinDate: '2023-10-01',
|
||||||
|
lastActiveTime: '2024-01-15 16:20:00',
|
||||||
|
hourlyRate: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: '赵译员',
|
||||||
|
email: 'zhao@translator.com',
|
||||||
|
phone: '13800138004',
|
||||||
|
languages: ['中文', '韩文'],
|
||||||
|
specialties: ['娱乐', '时尚', '旅游'],
|
||||||
|
rating: 4.6,
|
||||||
|
totalCalls: 45,
|
||||||
|
completedCalls: 43,
|
||||||
|
totalEarnings: 5400,
|
||||||
|
status: 'offline',
|
||||||
|
experience: 2,
|
||||||
|
certifications: ['韩语TOPIK6级'],
|
||||||
|
joinDate: '2023-11-15',
|
||||||
|
lastActiveTime: '2024-01-14 18:30:00',
|
||||||
|
hourlyRate: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: '孙译员',
|
||||||
|
email: 'sun@translator.com',
|
||||||
|
phone: '13800138005',
|
||||||
|
languages: ['中文', '西班牙文', '葡萄牙文'],
|
||||||
|
specialties: ['体育', '新闻', '政治'],
|
||||||
|
rating: 4.5,
|
||||||
|
totalCalls: 78,
|
||||||
|
completedCalls: 74,
|
||||||
|
totalEarnings: 9200,
|
||||||
|
status: 'available',
|
||||||
|
experience: 4,
|
||||||
|
certifications: ['西语专业八级', 'DELE C2'],
|
||||||
|
joinDate: '2023-09-10',
|
||||||
|
lastActiveTime: '2024-01-15 15:10:00',
|
||||||
|
hourlyRate: 130
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchTranslators = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setTranslators(mockTranslators);
|
||||||
|
setFilteredTranslators(mockTranslators);
|
||||||
|
message.success('译员列表加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载译员列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTranslators();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = translators;
|
||||||
|
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(translator =>
|
||||||
|
translator.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
translator.email.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
translator.languages.some(lang => lang.includes(searchText)) ||
|
||||||
|
translator.specialties.some(spec => spec.includes(searchText))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(translator =>
|
||||||
|
translator.languages.includes(languageFilter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(translator => translator.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredTranslators(filtered);
|
||||||
|
}, [translators, searchText, languageFilter, statusFilter]);
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
available: { color: 'green', text: '可用' },
|
||||||
|
busy: { color: 'orange', text: '忙碌' },
|
||||||
|
offline: { color: 'red', text: '离线' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Badge status={status === 'available' ? 'success' : status === 'busy' ? 'processing' : 'error'} text={config.text} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<Translator> = [
|
||||||
|
{
|
||||||
|
title: '译员信息',
|
||||||
|
key: 'translatorInfo',
|
||||||
|
width: 250,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
size={50}
|
||||||
|
src={record.avatar}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
style={{ marginRight: 12 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
|
||||||
|
{record.name}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginBottom: 4 }}>
|
||||||
|
{record.email}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
{record.experience} 年经验
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '语言能力',
|
||||||
|
dataIndex: 'languages',
|
||||||
|
key: 'languages',
|
||||||
|
width: 200,
|
||||||
|
render: (languages) => (
|
||||||
|
<div>
|
||||||
|
{languages.map((lang: string) => (
|
||||||
|
<Tag key={lang} color="blue" style={{ marginBottom: 4 }}>
|
||||||
|
{lang}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '专业领域',
|
||||||
|
dataIndex: 'specialties',
|
||||||
|
key: 'specialties',
|
||||||
|
width: 180,
|
||||||
|
render: (specialties) => (
|
||||||
|
<div>
|
||||||
|
{specialties.map((spec: string) => (
|
||||||
|
<Tag key={spec} color="purple" style={{ marginBottom: 4 }}>
|
||||||
|
{spec}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '评分',
|
||||||
|
dataIndex: 'rating',
|
||||||
|
key: 'rating',
|
||||||
|
width: 120,
|
||||||
|
render: (rating) => (
|
||||||
|
<div>
|
||||||
|
<Rate disabled defaultValue={rating} style={{ fontSize: '14px' }} />
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
{rating}/5.0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工作统计',
|
||||||
|
key: 'stats',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '12px', marginBottom: 4 }}>
|
||||||
|
总通话: {record.totalCalls} 次
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', marginBottom: 4 }}>
|
||||||
|
完成率: {((record.completedCalls / record.totalCalls) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px' }}>
|
||||||
|
收入: ¥{record.totalEarnings.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时薪',
|
||||||
|
dataIndex: 'hourlyRate',
|
||||||
|
key: 'hourlyRate',
|
||||||
|
width: 100,
|
||||||
|
render: (rate) => `¥${rate}/小时`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="分配任务">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<PhoneOutlined />}
|
||||||
|
disabled={record.status !== 'available'}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: filteredTranslators.length,
|
||||||
|
available: filteredTranslators.filter(t => t.status === 'available').length,
|
||||||
|
busy: filteredTranslators.filter(t => t.status === 'busy').length,
|
||||||
|
averageRating: filteredTranslators.reduce((sum, t) => sum + t.rating, 0) / filteredTranslators.length || 0,
|
||||||
|
totalEarnings: filteredTranslators.reduce((sum, t) => sum + t.totalEarnings, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
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 />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="可用译员"
|
||||||
|
value={stats.available}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="平均评分"
|
||||||
|
value={stats.averageRating}
|
||||||
|
precision={1}
|
||||||
|
prefix={<StarOutlined />}
|
||||||
|
valueStyle={{ color: '#faad14' }}
|
||||||
|
suffix="/5.0"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总收入"
|
||||||
|
value={stats.totalEarnings}
|
||||||
|
prefix="¥"
|
||||||
|
valueStyle={{ color: '#cf1322' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索译员姓名、邮箱、语言、专业..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Select
|
||||||
|
value={languageFilter}
|
||||||
|
onChange={setLanguageFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="语言筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部语言</Option>
|
||||||
|
<Option value="英文">英文</Option>
|
||||||
|
<Option value="日文">日文</Option>
|
||||||
|
<Option value="法文">法文</Option>
|
||||||
|
<Option value="德文">德文</Option>
|
||||||
|
<Option value="韩文">韩文</Option>
|
||||||
|
<Option value="西班牙文">西班牙文</Option>
|
||||||
|
<Option value="俄文">俄文</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="available">可用</Option>
|
||||||
|
<Option value="busy">忙碌</Option>
|
||||||
|
<Option value="offline">离线</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchTranslators}
|
||||||
|
loading={loading}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredTranslators}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredTranslators.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TranslatorList;
|
654
Twilioapp-admin/src/pages/Users/UserList.tsx
Normal file
654
Twilioapp-admin/src/pages/Users/UserList.tsx
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
DatePicker,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic,
|
||||||
|
Tooltip,
|
||||||
|
Avatar,
|
||||||
|
Form,
|
||||||
|
Switch
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
MailOutlined,
|
||||||
|
PhoneOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
UnlockOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
realName: string;
|
||||||
|
role: 'admin' | 'translator' | 'customer' | 'manager';
|
||||||
|
status: 'active' | 'inactive' | 'banned';
|
||||||
|
avatar?: string;
|
||||||
|
lastLoginTime?: string;
|
||||||
|
registrationTime: string;
|
||||||
|
totalCalls: number;
|
||||||
|
totalSpent: number;
|
||||||
|
preferredLanguages: string[];
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserList: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [roleFilter, setRoleFilter] = useState<string>('all');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockUsers: User[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
username: 'admin001',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
phone: '13800138001',
|
||||||
|
realName: '系统管理员',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
lastLoginTime: '2024-01-15 15:30:00',
|
||||||
|
registrationTime: '2023-01-01 10:00:00',
|
||||||
|
totalCalls: 0,
|
||||||
|
totalSpent: 0,
|
||||||
|
preferredLanguages: ['中文', '英文'],
|
||||||
|
notes: '系统管理员账户'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
username: 'translator_wang',
|
||||||
|
email: 'wang@translator.com',
|
||||||
|
phone: '13800138002',
|
||||||
|
realName: '王译员',
|
||||||
|
role: 'translator',
|
||||||
|
status: 'active',
|
||||||
|
lastLoginTime: '2024-01-15 14:45:00',
|
||||||
|
registrationTime: '2023-06-15 09:30:00',
|
||||||
|
totalCalls: 156,
|
||||||
|
totalSpent: 0,
|
||||||
|
preferredLanguages: ['中文', '英文', '日文'],
|
||||||
|
notes: '资深英日翻译,5年经验'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
username: 'customer_zhang',
|
||||||
|
email: 'zhang@customer.com',
|
||||||
|
phone: '13800138003',
|
||||||
|
realName: '张先生',
|
||||||
|
role: 'customer',
|
||||||
|
status: 'active',
|
||||||
|
lastLoginTime: '2024-01-15 16:20:00',
|
||||||
|
registrationTime: '2023-12-01 14:20:00',
|
||||||
|
totalCalls: 23,
|
||||||
|
totalSpent: 1580.50,
|
||||||
|
preferredLanguages: ['中文', '英文'],
|
||||||
|
notes: '企业客户,经常需要商务翻译'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
username: 'translator_li',
|
||||||
|
email: 'li@translator.com',
|
||||||
|
phone: '13800138004',
|
||||||
|
realName: '李译员',
|
||||||
|
role: 'translator',
|
||||||
|
status: 'active',
|
||||||
|
lastLoginTime: '2024-01-15 13:15:00',
|
||||||
|
registrationTime: '2023-08-20 11:45:00',
|
||||||
|
totalCalls: 89,
|
||||||
|
totalSpent: 0,
|
||||||
|
preferredLanguages: ['中文', '英文', '法文'],
|
||||||
|
notes: '法语专业译员'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
username: 'customer_li',
|
||||||
|
email: 'li_customer@example.com',
|
||||||
|
phone: '13800138005',
|
||||||
|
realName: '李女士',
|
||||||
|
role: 'customer',
|
||||||
|
status: 'inactive',
|
||||||
|
lastLoginTime: '2024-01-10 10:30:00',
|
||||||
|
registrationTime: '2023-11-15 16:00:00',
|
||||||
|
totalCalls: 8,
|
||||||
|
totalSpent: 420.00,
|
||||||
|
preferredLanguages: ['中文', '韩文'],
|
||||||
|
notes: '个人用户,偶尔使用'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
username: 'manager001',
|
||||||
|
email: 'manager@example.com',
|
||||||
|
phone: '13800138006',
|
||||||
|
realName: '业务经理',
|
||||||
|
role: 'manager',
|
||||||
|
status: 'active',
|
||||||
|
lastLoginTime: '2024-01-15 17:00:00',
|
||||||
|
registrationTime: '2023-03-01 08:00:00',
|
||||||
|
totalCalls: 0,
|
||||||
|
totalSpent: 0,
|
||||||
|
preferredLanguages: ['中文', '英文'],
|
||||||
|
notes: '负责客户关系管理'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setUsers(mockUsers);
|
||||||
|
setFilteredUsers(mockUsers);
|
||||||
|
message.success('用户列表加载成功');
|
||||||
|
} catch (error) {
|
||||||
|
message.error('加载用户列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = users;
|
||||||
|
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(user =>
|
||||||
|
user.username.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
user.realName.includes(searchText) ||
|
||||||
|
user.phone.includes(searchText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(user => user.role === roleFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(user => user.status === statusFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredUsers(filtered);
|
||||||
|
}, [users, searchText, roleFilter, statusFilter]);
|
||||||
|
|
||||||
|
const getRoleTag = (role: string) => {
|
||||||
|
const roleConfig = {
|
||||||
|
admin: { color: 'red', text: '管理员' },
|
||||||
|
manager: { color: 'purple', text: '经理' },
|
||||||
|
translator: { color: 'blue', text: '译员' },
|
||||||
|
customer: { color: 'green', text: '客户' }
|
||||||
|
};
|
||||||
|
const config = roleConfig[role as keyof typeof roleConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusConfig = {
|
||||||
|
active: { color: 'green', text: '活跃' },
|
||||||
|
inactive: { color: 'orange', text: '非活跃' },
|
||||||
|
banned: { color: 'red', text: '已禁用' }
|
||||||
|
};
|
||||||
|
const config = statusConfig[status as keyof typeof statusConfig];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusToggle = (userId: string, newStatus: User['status']) => {
|
||||||
|
const updatedUsers = users.map(user =>
|
||||||
|
user.id === userId ? { ...user, status: newStatus } : user
|
||||||
|
);
|
||||||
|
setUsers(updatedUsers);
|
||||||
|
message.success('用户状态更新成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (user: User) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
form.setFieldsValue(user);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (user: User) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除用户 "${user.realName}" 吗?此操作不可恢复。`,
|
||||||
|
onOk: () => {
|
||||||
|
const newUsers = users.filter(u => u.id !== user.id);
|
||||||
|
setUsers(newUsers);
|
||||||
|
message.success('用户删除成功');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (values: any) => {
|
||||||
|
try {
|
||||||
|
if (editingUser) {
|
||||||
|
const updatedUsers = users.map(user =>
|
||||||
|
user.id === editingUser.id ? { ...user, ...values } : user
|
||||||
|
);
|
||||||
|
setUsers(updatedUsers);
|
||||||
|
message.success('用户信息更新成功');
|
||||||
|
} else {
|
||||||
|
const newUser: User = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
...values,
|
||||||
|
registrationTime: new Date().toLocaleString(),
|
||||||
|
totalCalls: 0,
|
||||||
|
totalSpent: 0
|
||||||
|
};
|
||||||
|
setUsers([...users, newUser]);
|
||||||
|
message.success('用户创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingUser(null);
|
||||||
|
form.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnsType<User> = [
|
||||||
|
{
|
||||||
|
title: '用户信息',
|
||||||
|
key: 'userInfo',
|
||||||
|
width: 250,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
size={40}
|
||||||
|
src={record.avatar}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
style={{ marginRight: 12 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
|
||||||
|
{record.realName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
@{record.username}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '联系方式',
|
||||||
|
key: 'contact',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
|
||||||
|
<MailOutlined style={{ marginRight: 4, color: '#666' }} />
|
||||||
|
<span style={{ fontSize: '12px' }}>{record.email}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<PhoneOutlined style={{ marginRight: 4, color: '#666' }} />
|
||||||
|
<span style={{ fontSize: '12px' }}>{record.phone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
dataIndex: 'role',
|
||||||
|
key: 'role',
|
||||||
|
width: 100,
|
||||||
|
render: getRoleTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: getStatusTag
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '统计信息',
|
||||||
|
key: 'stats',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '12px', marginBottom: 4 }}>
|
||||||
|
通话: {record.totalCalls} 次
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px' }}>
|
||||||
|
消费: ¥{record.totalSpent.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后登录',
|
||||||
|
dataIndex: 'lastLoginTime',
|
||||||
|
key: 'lastLoginTime',
|
||||||
|
width: 150,
|
||||||
|
render: (time) => time || '从未登录'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={record.status === 'active' ? '禁用' : '启用'}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
|
||||||
|
onClick={() => handleStatusToggle(
|
||||||
|
record.id,
|
||||||
|
record.status === 'active' ? 'banned' : 'active'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
total: filteredUsers.length,
|
||||||
|
admin: filteredUsers.filter(u => u.role === 'admin').length,
|
||||||
|
translator: filteredUsers.filter(u => u.role === 'translator').length,
|
||||||
|
customer: filteredUsers.filter(u => u.role === 'customer').length,
|
||||||
|
active: filteredUsers.filter(u => u.status === 'active').length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<Title level={2}>用户管理</Title>
|
||||||
|
|
||||||
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="总用户数"
|
||||||
|
value={stats.total}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="管理员"
|
||||||
|
value={stats.admin}
|
||||||
|
valueStyle={{ color: '#cf1322' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="译员"
|
||||||
|
value={stats.translator}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="客户"
|
||||||
|
value={stats.customer}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="活跃用户"
|
||||||
|
value={stats.active}
|
||||||
|
valueStyle={{ color: '#722ed1' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Row gutter={16} style={{ marginBottom: '16px' }}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索用户名、邮箱、姓名、电话..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={roleFilter}
|
||||||
|
onChange={setRoleFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="角色筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部角色</Option>
|
||||||
|
<Option value="admin">管理员</Option>
|
||||||
|
<Option value="manager">经理</Option>
|
||||||
|
<Option value="translator">译员</Option>
|
||||||
|
<Option value="customer">客户</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="状态筛选"
|
||||||
|
>
|
||||||
|
<Option value="all">全部状态</Option>
|
||||||
|
<Option value="active">活跃</Option>
|
||||||
|
<Option value="inactive">非活跃</Option>
|
||||||
|
<Option value="banned">已禁用</Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<RangePicker style={{ width: '100%' }} placeholder={['注册开始日期', '注册结束日期']} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingUser(null);
|
||||||
|
form.resetFields();
|
||||||
|
setModalVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新增用户
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={fetchUsers}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredUsers}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
|
pagination={{
|
||||||
|
total: filteredUsers.length,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingUser ? '编辑用户' : '新增用户'}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setModalVisible(false);
|
||||||
|
setEditingUser(null);
|
||||||
|
form.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSave}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="realName"
|
||||||
|
label="真实姓名"
|
||||||
|
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
label="邮箱"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入邮箱' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
label="电话"
|
||||||
|
rules={[{ required: true, message: '请输入电话号码' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="role"
|
||||||
|
label="角色"
|
||||||
|
rules={[{ required: true, message: '请选择角色' }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="admin">管理员</Option>
|
||||||
|
<Option value="manager">经理</Option>
|
||||||
|
<Option value="translator">译员</Option>
|
||||||
|
<Option value="customer">客户</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="状态"
|
||||||
|
rules={[{ required: true, message: '请选择状态' }]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Option value="active">活跃</Option>
|
||||||
|
<Option value="inactive">非活跃</Option>
|
||||||
|
<Option value="banned">已禁用</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item
|
||||||
|
name="preferredLanguages"
|
||||||
|
label="偏好语言"
|
||||||
|
>
|
||||||
|
<Select mode="multiple" placeholder="选择偏好语言">
|
||||||
|
<Option value="中文">中文</Option>
|
||||||
|
<Option value="英文">英文</Option>
|
||||||
|
<Option value="日文">日文</Option>
|
||||||
|
<Option value="韩文">韩文</Option>
|
||||||
|
<Option value="法文">法文</Option>
|
||||||
|
<Option value="德文">德文</Option>
|
||||||
|
<Option value="西班牙文">西班牙文</Option>
|
||||||
|
<Option value="俄文">俄文</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="notes"
|
||||||
|
label="备注"
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={3} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserList;
|
@ -1,3 +1,69 @@
|
|||||||
|
// 用户相关类型
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
fullName: string;
|
||||||
|
avatar?: string;
|
||||||
|
role: 'user' | 'translator' | 'admin';
|
||||||
|
status: 'active' | 'inactive' | 'suspended';
|
||||||
|
preferredLanguages: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastLoginAt?: string;
|
||||||
|
totalCalls: number;
|
||||||
|
totalSpent: number;
|
||||||
|
rating: number;
|
||||||
|
verificationStatus: 'pending' | 'verified' | 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 译员相关类型
|
||||||
|
export interface Translator {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
avatar?: string;
|
||||||
|
languages: string[];
|
||||||
|
specializations: string[];
|
||||||
|
status: 'available' | 'busy' | 'offline' | 'suspended';
|
||||||
|
rating: number;
|
||||||
|
totalCalls: number;
|
||||||
|
totalEarnings: number;
|
||||||
|
hourlyRate: number;
|
||||||
|
certifications: Certification[];
|
||||||
|
workingHours: WorkingHours;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Certification {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
issuer: string;
|
||||||
|
issuedAt: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
documentUrl?: string;
|
||||||
|
verified: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkingHours {
|
||||||
|
monday: TimeSlot[];
|
||||||
|
tuesday: TimeSlot[];
|
||||||
|
wednesday: TimeSlot[];
|
||||||
|
thursday: TimeSlot[];
|
||||||
|
friday: TimeSlot[];
|
||||||
|
saturday: TimeSlot[];
|
||||||
|
sunday: TimeSlot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSlot {
|
||||||
|
start: string; // HH:mm
|
||||||
|
end: string; // HH:mm
|
||||||
|
}
|
||||||
|
|
||||||
// 通话相关类型
|
// 通话相关类型
|
||||||
export interface TranslationCall {
|
export interface TranslationCall {
|
||||||
id: string;
|
id: string;
|
||||||
@ -5,13 +71,13 @@ export interface TranslationCall {
|
|||||||
callId: string;
|
callId: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
clientPhone: string;
|
clientPhone: string;
|
||||||
type: 'human' | 'ai';
|
type: 'ai' | 'human';
|
||||||
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'refunded';
|
status: 'pending' | 'connecting' | 'ongoing' | 'completed' | 'failed' | 'cancelled';
|
||||||
sourceLanguage: string;
|
sourceLanguage: string;
|
||||||
targetLanguage: string;
|
targetLanguage: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
duration?: number;
|
duration?: number; // seconds
|
||||||
cost: number;
|
cost: number;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
feedback?: string;
|
feedback?: string;
|
||||||
@ -21,14 +87,12 @@ export interface TranslationCall {
|
|||||||
recordingUrl?: string;
|
recordingUrl?: string;
|
||||||
transcription?: string;
|
transcription?: string;
|
||||||
translation?: string;
|
translation?: string;
|
||||||
// 管理员相关字段
|
// 管理员字段
|
||||||
adminNotes?: string;
|
adminNotes?: string;
|
||||||
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
||||||
refundAmount: number;
|
refundAmount?: number;
|
||||||
qualityScore: number;
|
qualityScore?: number;
|
||||||
issues: string[];
|
issues?: string[];
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文档翻译相关类型
|
// 文档翻译相关类型
|
||||||
@ -41,11 +105,11 @@ export interface DocumentTranslation {
|
|||||||
translatedFileUrl?: string;
|
translatedFileUrl?: string;
|
||||||
sourceLanguage: string;
|
sourceLanguage: string;
|
||||||
targetLanguage: string;
|
targetLanguage: string;
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed';
|
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
|
||||||
progress: number;
|
progress: number;
|
||||||
quality: 'basic' | 'professional' | 'premium';
|
quality: 'basic' | 'professional' | 'premium';
|
||||||
urgency: 'normal' | 'urgent' | 'emergency';
|
urgency: 'low' | 'normal' | 'high' | 'urgent';
|
||||||
estimatedTime: number;
|
estimatedTime?: number; // minutes
|
||||||
actualTime?: number;
|
actualTime?: number;
|
||||||
cost: number;
|
cost: number;
|
||||||
translatorId?: string;
|
translatorId?: string;
|
||||||
@ -54,82 +118,113 @@ export interface DocumentTranslation {
|
|||||||
feedback?: string;
|
feedback?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
completedAt?: string;
|
completedAt?: string;
|
||||||
// 管理员相关字段
|
// 管理员字段
|
||||||
adminNotes?: string;
|
adminNotes?: string;
|
||||||
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
||||||
refundAmount: number;
|
refundAmount?: number;
|
||||||
qualityScore: number;
|
qualityScore?: number;
|
||||||
issues: string[];
|
issues?: string[];
|
||||||
retranslationCount?: number;
|
retranslationCount?: number;
|
||||||
clientName?: string;
|
clientName?: string;
|
||||||
clientEmail?: string;
|
clientEmail?: string;
|
||||||
clientPhone?: string;
|
clientPhone?: string;
|
||||||
updatedAt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预约相关类型
|
// 预约相关类型
|
||||||
export interface Appointment {
|
export interface Appointment {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
translatorId: string;
|
translatorId?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description?: string;
|
||||||
type: string;
|
type: 'interpretation' | 'translation' | 'consultation';
|
||||||
sourceLanguage: string;
|
sourceLanguage: string;
|
||||||
targetLanguage: string;
|
targetLanguage: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
status: string;
|
status: 'pending' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled';
|
||||||
cost: number;
|
cost: number;
|
||||||
meetingUrl?: string;
|
meetingUrl?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
reminderSent: boolean;
|
reminderSent: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt: string;
|
||||||
// 管理员相关字段
|
// 管理员字段
|
||||||
clientName: string;
|
clientName?: string;
|
||||||
clientEmail: string;
|
clientEmail?: string;
|
||||||
clientPhone: string;
|
clientPhone?: string;
|
||||||
translatorName: string;
|
translatorName?: string;
|
||||||
translatorEmail: string;
|
translatorEmail?: string;
|
||||||
translatorPhone: string;
|
translatorPhone?: string;
|
||||||
adminNotes?: string;
|
adminNotes?: string;
|
||||||
paymentStatus: string;
|
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
||||||
refundAmount: number;
|
refundAmount?: number;
|
||||||
qualityScore: number;
|
qualityScore?: number;
|
||||||
issues: string[];
|
issues?: string[];
|
||||||
rating?: number;
|
rating?: number;
|
||||||
feedback?: string;
|
feedback?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
urgency: string;
|
urgency: 'low' | 'normal' | 'high' | 'urgent';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户类型
|
// 支付相关类型
|
||||||
export interface User {
|
export interface Payment {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
userId: string;
|
||||||
email: string;
|
type: 'call' | 'document' | 'appointment';
|
||||||
phone?: string;
|
relatedId: string; // callId, documentId, or appointmentId
|
||||||
role: 'client' | 'translator' | 'admin';
|
amount: number;
|
||||||
status: 'active' | 'inactive' | 'suspended';
|
currency: 'CNY' | 'USD' | 'EUR';
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed' | 'refunded';
|
||||||
|
paymentMethod: 'wechat' | 'alipay' | 'credit_card' | 'bank_transfer';
|
||||||
|
transactionId?: string;
|
||||||
|
refundAmount?: number;
|
||||||
|
refundReason?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
completedAt?: string;
|
||||||
|
// 管理员字段
|
||||||
|
adminNotes?: string;
|
||||||
|
clientName?: string;
|
||||||
|
clientEmail?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 译员类型
|
// 系统配置类型
|
||||||
export interface Translator {
|
export interface SystemConfig {
|
||||||
id: string;
|
// 基本设置
|
||||||
name: string;
|
siteName: string;
|
||||||
email: string;
|
siteDescription: string;
|
||||||
phone: string;
|
supportEmail: string;
|
||||||
languages: string[];
|
supportPhone: string;
|
||||||
specializations: string[];
|
|
||||||
rating: number;
|
// Twilio设置
|
||||||
hourlyRate: number;
|
twilioAccountSid: string;
|
||||||
status: 'available' | 'busy' | 'offline';
|
twilioAuthToken: string;
|
||||||
totalJobs: number;
|
twilioPhoneNumber: string;
|
||||||
successRate: number;
|
|
||||||
createdAt: string;
|
// 支付设置
|
||||||
|
stripePublishableKey: string;
|
||||||
|
stripeSecretKey: string;
|
||||||
|
wechatPayMerchantId: string;
|
||||||
|
alipayAppId: string;
|
||||||
|
|
||||||
|
// 业务设置
|
||||||
|
defaultCallRate: number;
|
||||||
|
defaultDocumentRate: number;
|
||||||
|
maxCallDuration: number;
|
||||||
|
maxFileSize: number;
|
||||||
|
supportedLanguages: string[];
|
||||||
|
|
||||||
|
// 通知设置
|
||||||
|
emailNotifications: boolean;
|
||||||
|
smsNotifications: boolean;
|
||||||
|
pushNotifications: boolean;
|
||||||
|
|
||||||
|
// 安全设置
|
||||||
|
requireEmailVerification: boolean;
|
||||||
|
requirePhoneVerification: boolean;
|
||||||
|
maxLoginAttempts: number;
|
||||||
|
sessionTimeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API响应类型
|
// API响应类型
|
||||||
@ -141,16 +236,23 @@ export interface ApiResponse<T = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分页类型
|
// 分页类型
|
||||||
export interface PaginationParams {
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
total?: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索参数类型
|
// 统计数据类型
|
||||||
export interface SearchParams {
|
export interface DashboardStats {
|
||||||
keyword?: string;
|
totalUsers: number;
|
||||||
status?: string;
|
totalTranslators: number;
|
||||||
dateRange?: [string, string];
|
totalCalls: number;
|
||||||
[key: string]: any;
|
totalDocuments: number;
|
||||||
|
totalRevenue: number;
|
||||||
|
activeUsers: number;
|
||||||
|
onlineTranslators: number;
|
||||||
|
ongoingCalls: number;
|
||||||
|
pendingDocuments: number;
|
||||||
}
|
}
|
@ -1,140 +1,226 @@
|
|||||||
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse } from '../types';
|
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse, PaginatedResponse } from '../types';
|
||||||
|
|
||||||
// API基础URL
|
// API基础URL配置
|
||||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api';
|
||||||
|
|
||||||
// API请求工具类
|
// HTTP请求方法
|
||||||
class ApiManager {
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
private baseURL: string;
|
|
||||||
|
|
||||||
constructor(baseURL: string = API_BASE_URL) {
|
// 请求选项
|
||||||
this.baseURL = baseURL;
|
interface RequestOptions {
|
||||||
|
method?: HttpMethod;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: any;
|
||||||
|
params?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用请求方法
|
// API客户端类
|
||||||
|
export class ApiClient {
|
||||||
|
private baseURL: string;
|
||||||
|
private defaultHeaders: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(baseURL = API_BASE_URL) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
this.defaultHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置授权令牌
|
||||||
|
setAuthToken(token: string): void {
|
||||||
|
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除授权令牌
|
||||||
|
removeAuthToken(): void {
|
||||||
|
delete this.defaultHeaders['Authorization'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建URL参数
|
||||||
|
private buildURL(endpoint: string, params?: Record<string, any>): string {
|
||||||
|
const url = new URL(`${this.baseURL}${endpoint}`);
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
url.searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送HTTP请求
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {}
|
options: RequestOptions = {}
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
const url = `${this.baseURL}${endpoint}`;
|
try {
|
||||||
const config: RequestInit = {
|
const { method = 'GET', headers = {}, body, params } = options;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
const url = this.buildURL(endpoint, params);
|
||||||
...options.headers,
|
|
||||||
},
|
const requestHeaders = {
|
||||||
...options,
|
...this.defaultHeaders,
|
||||||
|
...headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const requestInit: RequestInit = {
|
||||||
const response = await fetch(url, config);
|
method,
|
||||||
const data = await response.json();
|
headers: requestHeaders,
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok) {
|
if (body && method !== 'GET') {
|
||||||
throw new Error(data.message || '请求失败');
|
if (body instanceof FormData) {
|
||||||
|
// 对于FormData,不设置Content-Type,让浏览器自动设置
|
||||||
|
delete requestHeaders['Content-Type'];
|
||||||
|
requestInit.body = body;
|
||||||
|
} else {
|
||||||
|
requestInit.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, requestInit);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
message: '操作成功',
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API请求错误:', error);
|
console.error('API请求失败:', error);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
data: null as any,
|
error: error instanceof Error ? error.message : '未知错误',
|
||||||
message: error instanceof Error ? error.message : '网络错误',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET请求
|
||||||
|
async get<T>(endpoint: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(endpoint, { method: 'GET', params });
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST请求
|
||||||
|
async post<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(endpoint, { method: 'POST', body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT请求
|
||||||
|
async put<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(endpoint, { method: 'PUT', body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH请求
|
||||||
|
async patch<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(endpoint, { method: 'PATCH', body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE请求
|
||||||
|
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
async upload<T>(endpoint: string, file: File, additionalData?: Record<string, any>): Promise<ApiResponse<T>> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
if (additionalData) {
|
||||||
|
Object.entries(additionalData).forEach(([key, value]) => {
|
||||||
|
formData.append(key, String(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.request<T>(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页数据
|
||||||
|
async getPaginated<T>(
|
||||||
|
endpoint: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
params?: Record<string, any>
|
||||||
|
): Promise<ApiResponse<PaginatedResponse<T>>> {
|
||||||
|
const paginationParams = {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.get<PaginatedResponse<T>>(endpoint, paginationParams);
|
||||||
|
}
|
||||||
|
|
||||||
// 通话管理API
|
// 通话管理API
|
||||||
async getCall(id: string): Promise<ApiResponse<TranslationCall>> {
|
async getCall(id: string): Promise<ApiResponse<TranslationCall>> {
|
||||||
return this.request<TranslationCall>(`/calls/${id}`);
|
return this.get<TranslationCall>(`/calls/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCall(id: string, data: Partial<TranslationCall>): Promise<ApiResponse<TranslationCall>> {
|
async updateCall(id: string, data: Partial<TranslationCall>): Promise<ApiResponse<TranslationCall>> {
|
||||||
return this.request<TranslationCall>(`/calls/${id}`, {
|
return this.post<TranslationCall>(`/calls/${id}`, data);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCall(id: string): Promise<ApiResponse<boolean>> {
|
async deleteCall(id: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/calls/${id}`, {
|
return this.delete<boolean>(`/calls/${id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async processRefund(callId: string, amount: number, reason: string): Promise<ApiResponse<boolean>> {
|
async processRefund(callId: string, amount: number, reason: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/calls/${callId}/refund`, {
|
return this.post<boolean>(`/calls/${callId}/refund`, { amount, reason });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ amount, reason }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCallNote(callId: string, note: string): Promise<ApiResponse<boolean>> {
|
async addCallNote(callId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/calls/${callId}/notes`, {
|
return this.post<boolean>(`/calls/${callId}/notes`, { note });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ note }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文档翻译API
|
// 文档翻译API
|
||||||
async getDocument(id: string): Promise<ApiResponse<DocumentTranslation>> {
|
async getDocument(id: string): Promise<ApiResponse<DocumentTranslation>> {
|
||||||
return this.request<DocumentTranslation>(`/documents/${id}`);
|
return this.get<DocumentTranslation>(`/documents/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<ApiResponse<DocumentTranslation>> {
|
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<ApiResponse<DocumentTranslation>> {
|
||||||
return this.request<DocumentTranslation>(`/documents/${id}`, {
|
return this.put<DocumentTranslation>(`/documents/${id}`, data);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDocument(id: string): Promise<ApiResponse<boolean>> {
|
async deleteDocument(id: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/documents/${id}`, {
|
return this.delete<boolean>(`/documents/${id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async reassignTranslator(documentId: string, translatorId: string): Promise<ApiResponse<boolean>> {
|
async reassignTranslator(documentId: string, translatorId: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/documents/${documentId}/reassign`, {
|
return this.post<boolean>(`/documents/${documentId}/reassign`, { translatorId });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ translatorId }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async retranslateDocument(documentId: string, quality: string): Promise<ApiResponse<boolean>> {
|
async retranslateDocument(documentId: string, quality: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/documents/${documentId}/retranslate`, {
|
return this.post<boolean>(`/documents/${documentId}/retranslate`, { quality });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ quality }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDocumentNote(documentId: string, note: string): Promise<ApiResponse<boolean>> {
|
async addDocumentNote(documentId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/documents/${documentId}/notes`, {
|
return this.post<boolean>(`/documents/${documentId}/notes`, { note });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ note }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预约管理API
|
// 预约管理API
|
||||||
async getAppointment(id: string): Promise<ApiResponse<Appointment>> {
|
async getAppointment(id: string): Promise<ApiResponse<Appointment>> {
|
||||||
return this.request<Appointment>(`/appointments/${id}`);
|
return this.get<Appointment>(`/appointments/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAppointment(id: string, data: Partial<Appointment>): Promise<ApiResponse<Appointment>> {
|
async updateAppointment(id: string, data: Partial<Appointment>): Promise<ApiResponse<Appointment>> {
|
||||||
return this.request<Appointment>(`/appointments/${id}`, {
|
return this.put<Appointment>(`/appointments/${id}`, data);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAppointment(id: string): Promise<ApiResponse<boolean>> {
|
async deleteAppointment(id: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/appointments/${id}`, {
|
return this.delete<boolean>(`/appointments/${id}`);
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async rescheduleAppointment(
|
async rescheduleAppointment(
|
||||||
@ -142,79 +228,68 @@ class ApiManager {
|
|||||||
newStartTime: string,
|
newStartTime: string,
|
||||||
newEndTime: string
|
newEndTime: string
|
||||||
): Promise<ApiResponse<boolean>> {
|
): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/appointments/${appointmentId}/reschedule`, {
|
return this.post<boolean>(`/appointments/${appointmentId}/reschedule`, { newStartTime, newEndTime });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ newStartTime, newEndTime }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async reassignAppointmentTranslator(
|
async reassignAppointmentTranslator(
|
||||||
appointmentId: string,
|
appointmentId: string,
|
||||||
translatorId: string
|
translatorId: string
|
||||||
): Promise<ApiResponse<boolean>> {
|
): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/appointments/${appointmentId}/reassign`, {
|
return this.post<boolean>(`/appointments/${appointmentId}/reassign`, { translatorId });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ translatorId }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAppointmentNote(appointmentId: string, note: string): Promise<ApiResponse<boolean>> {
|
async addAppointmentNote(appointmentId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/appointments/${appointmentId}/notes`, {
|
return this.post<boolean>(`/appointments/${appointmentId}/notes`, { note });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ note }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退款处理API
|
// 退款处理API
|
||||||
async refundPayment(paymentId: string, amount: number): Promise<ApiResponse<boolean>> {
|
async refundPayment(paymentId: string, amount: number): Promise<ApiResponse<boolean>> {
|
||||||
return this.request<boolean>(`/payments/${paymentId}/refund`, {
|
return this.post<boolean>(`/payments/${paymentId}/refund`, { amount });
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ amount }),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据API
|
// 统计数据API
|
||||||
async getStatistics(): Promise<ApiResponse<any>> {
|
async getStatistics(): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>('/statistics');
|
return this.get<any>('/statistics');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户管理API
|
// 用户管理API
|
||||||
async getUsers(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
async getUsers(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>(`/users?page=${page}&pageSize=${pageSize}`);
|
return this.get<any>(`/users?page=${page}&pageSize=${pageSize}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(userId: string, data: any): Promise<ApiResponse<any>> {
|
async updateUser(userId: string, data: any): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>(`/users/${userId}`, {
|
return this.put<any>(`/users/${userId}`, data);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 译员管理API
|
// 译员管理API
|
||||||
async getTranslators(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
async getTranslators(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>(`/translators?page=${page}&pageSize=${pageSize}`);
|
return this.get<any>(`/translators?page=${page}&pageSize=${pageSize}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTranslator(translatorId: string, data: any): Promise<ApiResponse<any>> {
|
async updateTranslator(translatorId: string, data: any): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>(`/translators/${translatorId}`, {
|
return this.put<any>(`/translators/${translatorId}`, data);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统配置API
|
// 系统配置API
|
||||||
async getSystemConfig(): Promise<ApiResponse<any>> {
|
async getSystemConfig(): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>('/config');
|
return this.get<any>('/config');
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSystemConfig(config: any): Promise<ApiResponse<any>> {
|
async updateSystemConfig(config: any): Promise<ApiResponse<any>> {
|
||||||
return this.request<any>('/config', {
|
return this.put<any>('/config', config);
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(config),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出API实例
|
// 导出默认API客户端实例
|
||||||
export const api = new ApiManager();
|
export const api = new ApiClient();
|
||||||
export default api;
|
|
||||||
|
// 导出常用的API方法
|
||||||
|
export const {
|
||||||
|
get,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
patch,
|
||||||
|
delete: del,
|
||||||
|
upload,
|
||||||
|
getPaginated,
|
||||||
|
} = api;
|
@ -1,30 +1,76 @@
|
|||||||
import { TranslationCall, DocumentTranslation, Appointment, User, Translator } from '../types';
|
import { TranslationCall, DocumentTranslation, Appointment, User, Translator } from '../types';
|
||||||
|
|
||||||
// 模拟数据库连接类
|
// 模拟数据库连接和操作
|
||||||
class DatabaseManager {
|
export class Database {
|
||||||
private isConnected: boolean = false;
|
private connected = false;
|
||||||
|
|
||||||
// 连接数据库
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (!this.isConnected) {
|
// 模拟数据库连接
|
||||||
// 模拟连接延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
this.isConnected = true;
|
this.connected = true;
|
||||||
console.log('数据库连接成功');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开数据库连接
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
if (this.isConnected) {
|
this.connected = false;
|
||||||
this.isConnected = false;
|
|
||||||
console.log('数据库连接已断开');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查连接状态
|
isConnected(): boolean {
|
||||||
isConnectionActive(): boolean {
|
return this.connected;
|
||||||
return this.isConnected;
|
}
|
||||||
|
|
||||||
|
// 模拟查询操作
|
||||||
|
async query<T>(sql: string, params?: any[]): Promise<T[]> {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Database not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟查询延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
// 这里可以添加具体的查询逻辑
|
||||||
|
return [] as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟插入操作
|
||||||
|
async insert<T>(table: string, data: Partial<T>): Promise<T> {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Database not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
// 模拟返回插入的数据
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: `${table}_${Date.now()}`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟更新操作
|
||||||
|
async update<T>(table: string, id: string, data: Partial<T>): Promise<T> {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Database not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
// 模拟返回更新的数据
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟删除操作
|
||||||
|
async delete(table: string, id: string): Promise<boolean> {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Database not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通话相关操作
|
// 通话相关操作
|
||||||
@ -59,7 +105,6 @@ class DatabaseManager {
|
|||||||
refundAmount: 0,
|
refundAmount: 0,
|
||||||
qualityScore: 0,
|
qualityScore: 0,
|
||||||
issues: [],
|
issues: [],
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
};
|
};
|
||||||
return newCall;
|
return newCall;
|
||||||
}
|
}
|
||||||
@ -149,7 +194,7 @@ class DatabaseManager {
|
|||||||
translatorId: data.translatorId || '',
|
translatorId: data.translatorId || '',
|
||||||
title: data.title || '',
|
title: data.title || '',
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
type: data.type || '',
|
type: data.type || 'interpretation',
|
||||||
sourceLanguage: data.sourceLanguage || '',
|
sourceLanguage: data.sourceLanguage || '',
|
||||||
targetLanguage: data.targetLanguage || '',
|
targetLanguage: data.targetLanguage || '',
|
||||||
startTime: data.startTime || new Date().toISOString(),
|
startTime: data.startTime || new Date().toISOString(),
|
||||||
@ -158,6 +203,7 @@ class DatabaseManager {
|
|||||||
cost: data.cost || 0,
|
cost: data.cost || 0,
|
||||||
reminderSent: false,
|
reminderSent: false,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
clientName: data.clientName || '',
|
clientName: data.clientName || '',
|
||||||
clientEmail: data.clientEmail || '',
|
clientEmail: data.clientEmail || '',
|
||||||
clientPhone: data.clientPhone || '',
|
clientPhone: data.clientPhone || '',
|
||||||
@ -203,12 +249,21 @@ class DatabaseManager {
|
|||||||
// 模拟创建用户
|
// 模拟创建用户
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
id: `user_${Date.now()}`,
|
id: `user_${Date.now()}`,
|
||||||
name: data.name || '',
|
username: data.username || '',
|
||||||
email: data.email || '',
|
email: data.email || '',
|
||||||
phone: data.phone,
|
phone: data.phone || '',
|
||||||
role: data.role || 'client',
|
fullName: data.fullName || '',
|
||||||
|
avatar: data.avatar,
|
||||||
|
role: data.role || 'user',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
preferredLanguages: data.preferredLanguages || [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
lastLoginAt: data.lastLoginAt,
|
||||||
|
totalCalls: data.totalCalls || 0,
|
||||||
|
totalSpent: data.totalSpent || 0,
|
||||||
|
rating: data.rating || 0,
|
||||||
|
verificationStatus: data.verificationStatus || 'pending',
|
||||||
};
|
};
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
@ -243,17 +298,30 @@ class DatabaseManager {
|
|||||||
// 模拟创建译员
|
// 模拟创建译员
|
||||||
const newTranslator: Translator = {
|
const newTranslator: Translator = {
|
||||||
id: `translator_${Date.now()}`,
|
id: `translator_${Date.now()}`,
|
||||||
name: data.name || '',
|
userId: data.userId || '',
|
||||||
|
fullName: data.fullName || '',
|
||||||
email: data.email || '',
|
email: data.email || '',
|
||||||
phone: data.phone || '',
|
phone: data.phone || '',
|
||||||
|
avatar: data.avatar,
|
||||||
languages: data.languages || [],
|
languages: data.languages || [],
|
||||||
specializations: data.specializations || [],
|
specializations: data.specializations || [],
|
||||||
|
status: data.status || 'available',
|
||||||
rating: data.rating || 0,
|
rating: data.rating || 0,
|
||||||
|
totalCalls: data.totalCalls || 0,
|
||||||
|
totalEarnings: data.totalEarnings || 0,
|
||||||
hourlyRate: data.hourlyRate || 0,
|
hourlyRate: data.hourlyRate || 0,
|
||||||
status: 'available',
|
certifications: data.certifications || [],
|
||||||
totalJobs: 0,
|
workingHours: data.workingHours || {
|
||||||
successRate: 0,
|
monday: [],
|
||||||
|
tuesday: [],
|
||||||
|
wednesday: [],
|
||||||
|
thursday: [],
|
||||||
|
friday: [],
|
||||||
|
saturday: [],
|
||||||
|
sunday: [],
|
||||||
|
},
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
return newTranslator;
|
return newTranslator;
|
||||||
}
|
}
|
||||||
@ -286,5 +354,5 @@ class DatabaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出单例实例
|
// 导出单例实例
|
||||||
export const database = new DatabaseManager();
|
export const database = new Database();
|
||||||
export default database;
|
export default database;
|
34
index.html
34
index.html
@ -4,8 +4,38 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Twilio 翻译服务管理后台</title>
|
<title>翻译通 - 移动端</title>
|
||||||
<meta name="description" content="Twilio 翻译服务管理后台系统" />
|
<meta name="description" content="专业的翻译服务平台" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@ -11,8 +11,7 @@ const navItems: NavItem[] = [
|
|||||||
{ path: '/mobile/home', label: '首页', icon: '🏠' },
|
{ path: '/mobile/home', label: '首页', icon: '🏠' },
|
||||||
{ path: '/mobile/call', label: '通话', icon: '📞' },
|
{ path: '/mobile/call', label: '通话', icon: '📞' },
|
||||||
{ path: '/mobile/documents', label: '文档', icon: '📄' },
|
{ path: '/mobile/documents', label: '文档', icon: '📄' },
|
||||||
{ path: '/mobile/appointments', label: '预约', icon: '📅' },
|
{ path: '/mobile/settings', label: '我的', icon: '👤' },
|
||||||
{ path: '/mobile/settings', label: '设置', icon: '⚙️' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const MobileNavigation: FC = () => {
|
const MobileNavigation: FC = () => {
|
||||||
|
@ -10,10 +10,8 @@ interface NavItem {
|
|||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ path: '/mobile/home', label: '首页', icon: '🏠' },
|
{ path: '/mobile/home', label: '首页', icon: '🏠' },
|
||||||
{ path: '/mobile/call', label: '通话', icon: '📞' },
|
{ path: '/mobile/call', label: '通话', icon: '📞' },
|
||||||
{ path: '/mobile/video-call', label: '视频', icon: '📹' },
|
|
||||||
{ path: '/mobile/documents', label: '文档', icon: '📄' },
|
{ path: '/mobile/documents', label: '文档', icon: '📄' },
|
||||||
{ path: '/mobile/appointments', label: '预约', icon: '📅' },
|
{ path: '/mobile/settings', label: '我的', icon: '👤' },
|
||||||
{ path: '/mobile/settings', label: '设置', icon: '⚙️' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const MobileNavigation: FC = () => {
|
const MobileNavigation: FC = () => {
|
||||||
|
13
src/main.tsx
13
src/main.tsx
@ -1,15 +1,16 @@
|
|||||||
import { StrictMode } from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from '../App.tsx';
|
||||||
|
import './styles/global.css';
|
||||||
|
|
||||||
// 创建根元素
|
// 创建根元素
|
||||||
const root = createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
// 渲染应用
|
// 渲染应用
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
@ -8,23 +8,17 @@ import { View, Text, StyleSheet } from 'react-native';
|
|||||||
import HomeScreen from '@/screens/HomeScreen';
|
import HomeScreen from '@/screens/HomeScreen';
|
||||||
import CallScreen from '@/screens/CallScreen';
|
import CallScreen from '@/screens/CallScreen';
|
||||||
import DocumentScreen from '@/screens/DocumentScreen';
|
import DocumentScreen from '@/screens/DocumentScreen';
|
||||||
import AppointmentScreen from '@/screens/AppointmentScreen';
|
|
||||||
import SettingsScreen from '@/screens/SettingsScreen';
|
import SettingsScreen from '@/screens/SettingsScreen';
|
||||||
|
|
||||||
// 导航类型定义
|
// 导航类型定义
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
MainTabs: undefined;
|
MainTabs: undefined;
|
||||||
Call: {
|
|
||||||
mode: 'ai' | 'human' | 'video' | 'sign';
|
|
||||||
sourceLanguage: string;
|
|
||||||
targetLanguage: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TabParamList = {
|
export type TabParamList = {
|
||||||
Home: undefined;
|
Home: undefined;
|
||||||
|
Call: undefined;
|
||||||
Documents: undefined;
|
Documents: undefined;
|
||||||
Appointments: undefined;
|
|
||||||
Settings: undefined;
|
Settings: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,12 +31,12 @@ const TabIcon: React.FC<{ name: string; focused: boolean }> = ({ name, focused }
|
|||||||
switch (iconName) {
|
switch (iconName) {
|
||||||
case 'home':
|
case 'home':
|
||||||
return '🏠';
|
return '🏠';
|
||||||
|
case 'call':
|
||||||
|
return '📞';
|
||||||
case 'documents':
|
case 'documents':
|
||||||
return '📄';
|
return '📄';
|
||||||
case 'appointments':
|
|
||||||
return '📅';
|
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return '⚙️';
|
return '👤';
|
||||||
default:
|
default:
|
||||||
return '❓';
|
return '❓';
|
||||||
}
|
}
|
||||||
@ -79,6 +73,13 @@ const TabNavigator: React.FC = () => {
|
|||||||
tabBarLabel: '首页',
|
tabBarLabel: '首页',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Call"
|
||||||
|
component={CallScreen}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: '通话',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Documents"
|
name="Documents"
|
||||||
component={DocumentScreen}
|
component={DocumentScreen}
|
||||||
@ -86,18 +87,11 @@ const TabNavigator: React.FC = () => {
|
|||||||
tabBarLabel: '文档',
|
tabBarLabel: '文档',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
|
||||||
name="Appointments"
|
|
||||||
component={AppointmentScreen}
|
|
||||||
options={{
|
|
||||||
tabBarLabel: '预约',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Settings"
|
name="Settings"
|
||||||
component={SettingsScreen}
|
component={SettingsScreen}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: '设置',
|
tabBarLabel: '我的',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
@ -118,14 +112,6 @@ const AppNavigator: React.FC = () => {
|
|||||||
name="MainTabs"
|
name="MainTabs"
|
||||||
component={TabNavigator}
|
component={TabNavigator}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
|
||||||
name="Call"
|
|
||||||
component={CallScreen}
|
|
||||||
options={{
|
|
||||||
presentation: 'fullScreenModal',
|
|
||||||
gestureEnabled: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,6 @@ import { useAuth } from '@/store';
|
|||||||
import HomeScreen from '@/screens/HomeScreen.web';
|
import HomeScreen from '@/screens/HomeScreen.web';
|
||||||
import CallScreen from '@/screens/CallScreen.web';
|
import CallScreen from '@/screens/CallScreen.web';
|
||||||
import DocumentScreen from '@/screens/DocumentScreen.web';
|
import DocumentScreen from '@/screens/DocumentScreen.web';
|
||||||
import AppointmentScreen from '@/screens/AppointmentScreen.web';
|
|
||||||
import SettingsScreen from '@/screens/SettingsScreen.web';
|
import SettingsScreen from '@/screens/SettingsScreen.web';
|
||||||
import MobileNavigation from '@/components/MobileNavigation.web';
|
import MobileNavigation from '@/components/MobileNavigation.web';
|
||||||
|
|
||||||
@ -101,9 +100,7 @@ const AppRoutes = () => {
|
|||||||
<Route path="/home" element={<HomeScreen />} />
|
<Route path="/home" element={<HomeScreen />} />
|
||||||
<Route path="/call" element={<CallScreen />} />
|
<Route path="/call" element={<CallScreen />} />
|
||||||
<Route path="/documents" element={<DocumentScreen />} />
|
<Route path="/documents" element={<DocumentScreen />} />
|
||||||
<Route path="/appointments" element={<AppointmentScreen />} />
|
|
||||||
<Route path="/settings" element={<SettingsScreen />} />
|
<Route path="/settings" element={<SettingsScreen />} />
|
||||||
<Route path="/video-call" element={<VideoCallPage />} />
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</MobileLayout>
|
</MobileLayout>
|
||||||
|
@ -8,7 +8,7 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
// React Native Web 别名配置
|
// React Native Web 别名配置
|
||||||
'react-native$': 'react-native-web',
|
'react-native': 'react-native-web',
|
||||||
'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter',
|
'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter',
|
||||||
'react-native/Libraries/vendor/emitter/EventEmitter$': 'react-native-web/dist/vendor/react-native/emitter/EventEmitter',
|
'react-native/Libraries/vendor/emitter/EventEmitter$': 'react-native-web/dist/vendor/react-native/emitter/EventEmitter',
|
||||||
'react-native/Libraries/EventEmitter/NativeEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter',
|
'react-native/Libraries/EventEmitter/NativeEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter',
|
||||||
@ -26,8 +26,9 @@ export default defineConfig({
|
|||||||
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
|
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 5173,
|
||||||
host: true,
|
host: true,
|
||||||
|
open: true
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user