后台管理端调整

This commit is contained in:
mars 2025-06-28 14:20:17 +08:00
parent cf40d6adeb
commit 7fcff7759d
25 changed files with 25447 additions and 0 deletions

17369
Twilioapp-admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
{
"name": "twilioapp-admin",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.56",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"antd": "^5.0.0",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.0",
"react-scripts": "5.0.1",
"typescript": "^4.7.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/moment": "^2.13.0"
}
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Twilio翻译服务后台管理系统"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Twilio翻译管理系统</title>
</head>
<body>
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
<div id="root"></div>
</body>
</html>

231
Twilioapp-admin/src/App.css Normal file
View File

@ -0,0 +1,231 @@
/* 全局样式 */
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 侧边栏样式 */
.ant-layout-sider {
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
}
/* 内容区域样式 */
.ant-layout-content {
background: #f0f2f5;
}
/* 卡片样式 */
.ant-card {
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
border-radius: 8px;
}
/* 表格样式 */
.ant-table {
border-radius: 8px;
}
/* 按钮样式 */
.ant-btn {
border-radius: 6px;
}
/* 表单样式 */
.ant-form-item {
margin-bottom: 16px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.ant-layout-content {
margin: 16px 8px 0;
}
.ant-card {
margin-bottom: 16px;
}
}
/* 自定义动画 */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
/* 状态标签 */
.status-tag {
border-radius: 4px;
font-weight: 500;
}
/* 音频播放器样式 */
.audio-player {
width: 100%;
margin: 16px 0;
}
/* 文件预览样式 */
.file-preview {
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 16px;
background: #fafafa;
}
/* 统计卡片样式 */
.stat-card {
text-align: center;
padding: 24px;
}
.stat-card .stat-value {
font-size: 30px;
font-weight: bold;
color: #1890ff;
margin-bottom: 8px;
}
.stat-card .stat-label {
font-size: 14px;
color: #666;
}
/* 时间轴样式 */
.timeline-item {
padding: 12px 0;
}
.timeline-item .timeline-time {
color: #999;
font-size: 12px;
}
.timeline-item .timeline-content {
margin-top: 4px;
}
/* 评分样式 */
.rating-container {
display: flex;
align-items: center;
gap: 8px;
}
.rating-value {
font-weight: bold;
color: #faad14;
}
/* 进度条样式 */
.progress-container {
display: flex;
align-items: center;
gap: 12px;
}
.progress-text {
min-width: 50px;
text-align: right;
font-weight: 500;
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
padding: 20px;
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 自定义样式 */
.logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.3);
margin: 16px 24px 16px 0;
float: left;
}
.ant-layout-header {
position: fixed;
z-index: 1;
width: 100%;
}
.ant-layout-content {
margin-top: 64px;
}
.site-layout-background {
background: #fff;
}
/* 卡片样式优化 */
.ant-card-cover {
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
}
.ant-card-meta-title {
font-size: 16px;
font-weight: 600;
}
.ant-card-meta-description {
color: #666;
font-size: 14px;
}

140
Twilioapp-admin/src/App.tsx Normal file
View File

@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { Layout, Menu, ConfigProvider } from 'antd';
import {
DashboardOutlined,
PhoneOutlined,
FileTextOutlined,
CalendarOutlined
} from '@ant-design/icons';
import zhCN from 'antd/locale/zh_CN';
import 'antd/dist/reset.css';
import './App.css';
// 导入页面组件
import Dashboard from './pages/Dashboard';
import CallDetail from './pages/Calls/CallDetail';
import DocumentDetail from './pages/Documents/DocumentDetail';
import AppointmentDetail from './pages/Appointments/AppointmentDetail';
const { Header, Sider, Content } = Layout;
const AppContent: React.FC = () => {
const navigate = useNavigate();
const [selectedKey, setSelectedKey] = useState('1');
const handleMenuClick = (e: any) => {
setSelectedKey(e.key);
switch (e.key) {
case '1':
navigate('/dashboard');
break;
case '2':
navigate('/calls/1');
break;
case '3':
navigate('/documents/1');
break;
case '4':
navigate('/appointments/1');
break;
}
};
return (
<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',
icon: <DashboardOutlined />,
label: '仪表板',
},
{
key: '2',
icon: <PhoneOutlined />,
label: '通话管理',
},
{
key: '3',
icon: <FileTextOutlined />,
label: '文档翻译',
},
{
key: '4',
icon: <CalendarOutlined />,
label: '预约管理',
},
]}
/>
</Sider>
<Layout>
<Header style={{
padding: 0,
background: '#fff',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
}}>
<div style={{
padding: '0 24px',
fontSize: '18px',
fontWeight: 'bold'
}}>
Twilio翻译服务管理后台
</div>
</Header>
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
<div style={{
padding: 24,
background: '#fff',
minHeight: 360,
borderRadius: 8
}}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/calls/:id" element={<CallDetail />} />
<Route path="/documents/:id" element={<DocumentDetail />} />
<Route path="/appointments/:id" element={<AppointmentDetail />} />
</Routes>
</div>
</Content>
</Layout>
</Layout>
);
};
const App: React.FC = () => {
return (
<ConfigProvider locale={zhCN}>
<Router>
<AppContent />
</Router>
</ConfigProvider>
);
};
export default App;

View File

@ -0,0 +1,21 @@
body {
margin: 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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
html, body, #root {
height: 100%;
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import 'antd/dist/reset.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,805 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Descriptions,
Button,
Tag,
Typography,
Space,
Modal,
Input,
message,
Spin,
Timeline,
Tabs,
Avatar,
Progress,
Select,
Form,
Switch,
Divider,
Alert,
Table,
Rate,
Statistic,
Row,
Col,
} from 'antd';
import {
ArrowLeftOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
DownloadOutlined,
StarOutlined,
PhoneOutlined,
ClockCircleOutlined,
DollarOutlined,
UserOutlined,
SoundOutlined,
FileTextOutlined,
TranslationOutlined,
EditOutlined,
DeleteOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
AuditOutlined,
SettingOutlined,
MessageOutlined,
} from '@ant-design/icons';
import { TranslationCall } from '../../types';
import { database } from '../../utils/database';
import { api } from '../../utils/api';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
const { TabPane } = Tabs;
const { Option } = Select;
interface CallDetailProps {}
const CallDetail: React.FC<CallDetailProps> = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [call, setCall] = useState<TranslationCall | null>(null);
const [loading, setLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [editModalVisible, setEditModalVisible] = useState(false);
const [statusModalVisible, setStatusModalVisible] = useState(false);
const [refundModalVisible, setRefundModalVisible] = useState(false);
const [adminNoteModalVisible, setAdminNoteModalVisible] = useState(false);
const [form] = Form.useForm();
const [statusForm] = Form.useForm();
const [refundForm] = Form.useForm();
const [noteForm] = Form.useForm();
// 模拟音频播放状态
const [audioProgress, setAudioProgress] = useState(0);
useEffect(() => {
if (id) {
loadCallDetails();
}
}, [id]);
const loadCallDetails = async () => {
try {
setLoading(true);
await database.connect();
// 模拟获取通话详情(管理员视角)
const mockCall: TranslationCall = {
id: id!,
userId: 'user_1',
callId: `CA${Date.now()}`,
clientName: '张先生',
clientPhone: '+86 138 0013 8000',
type: 'human',
status: 'completed',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
startTime: '2024-01-15T10:30:00Z',
endTime: '2024-01-15T10:45:00Z',
duration: 900,
cost: 45.00,
rating: 5,
feedback: '翻译非常专业,沟通顺畅,非常满意!',
translatorId: 'translator_1',
translatorName: '李翻译',
translatorPhone: '+86 138 0013 8001',
recordingUrl: '/recordings/call_123456.mp3',
transcription: '用户: 您好,我想了解一下贵公司的产品服务。\n翻译: Hello, I would like to learn about your company\'s products and services.\n客户: Thank you for your interest. Let me introduce our main products...\n翻译: 感谢您的关注。让我为您介绍我们的主要产品...',
translation: '这是一次关于产品咨询的商务通话,客户询问了公司的主要产品和服务,我们提供了详细的介绍和说明。',
// 管理员相关字段
adminNotes: '通话质量良好,客户满意度高',
paymentStatus: 'paid',
refundAmount: 0,
qualityScore: 95,
issues: [],
};
setCall(mockCall);
setDuration(mockCall.duration || 0);
// 填充表单数据
form.setFieldsValue({
clientName: mockCall.clientName,
clientPhone: mockCall.clientPhone,
translatorName: mockCall.translatorName,
cost: mockCall.cost,
});
statusForm.setFieldsValue({
status: mockCall.status,
});
} catch (error) {
console.error('加载通话详情失败:', error);
message.error('加载通话详情失败');
} finally {
setLoading(false);
}
};
const handlePlayPause = () => {
setIsPlaying(!isPlaying);
if (!isPlaying) {
// 模拟音频播放
const interval = setInterval(() => {
setCurrentTime(prev => {
const newTime = prev + 1;
setAudioProgress((newTime / duration) * 100);
if (newTime >= duration) {
clearInterval(interval);
setIsPlaying(false);
setCurrentTime(0);
setAudioProgress(0);
}
return newTime;
});
}, 1000);
}
};
const handleEdit = async (values: any) => {
if (!call) return;
try {
const updatedCall = {
...call,
...values,
updatedAt: new Date().toISOString(),
};
setCall(updatedCall);
setEditModalVisible(false);
message.success('通话信息更新成功');
} catch (error) {
message.error('更新通话信息失败');
}
};
const handleStatusChange = async (values: any) => {
if (!call) return;
try {
const updatedCall = {
...call,
status: values.status,
updatedAt: new Date().toISOString(),
};
setCall(updatedCall);
setStatusModalVisible(false);
message.success('状态更新成功');
} catch (error) {
message.error('更新状态失败');
}
};
const handleRefund = async (values: any) => {
if (!call) return;
try {
const refundAmount = values.amount || call.cost;
// 模拟退款API调用
await api.refundPayment(`payment_${call.id}`, refundAmount);
const updatedCall = {
...call,
refundAmount: refundAmount,
paymentStatus: 'refunded' as const,
updatedAt: new Date().toISOString(),
};
setCall(updatedCall);
setRefundModalVisible(false);
message.success('退款处理成功');
} catch (error) {
message.error('退款处理失败');
}
};
const handleAddAdminNote = async (values: any) => {
if (!call) return;
try {
const updatedCall = {
...call,
adminNotes: values.note,
updatedAt: new Date().toISOString(),
};
setCall(updatedCall);
setAdminNoteModalVisible(false);
message.success('管理员备注添加成功');
} catch (error) {
message.error('添加备注失败');
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
active: 'blue',
completed: 'green',
cancelled: 'red',
refunded: 'purple',
};
return colors[status as keyof typeof colors] || 'default';
};
const getStatusText = (status: string) => {
const texts = {
pending: '等待中',
active: '通话中',
completed: '已完成',
cancelled: '已取消',
refunded: '已退款',
};
return texts[status as keyof typeof texts] || status;
};
const getPaymentStatusColor = (status: string) => {
const colors = {
pending: 'orange',
paid: 'green',
refunded: 'purple',
failed: 'red',
};
return colors[status as keyof typeof colors] || 'default';
};
const getPaymentStatusText = (status: string) => {
const texts = {
pending: '待支付',
paid: '已支付',
refunded: '已退款',
failed: '支付失败',
};
return texts[status as keyof typeof texts] || status;
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
</div>
);
}
if (!call) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<div></div>
<Button type="primary" onClick={() => navigate('/calls')} style={{ marginTop: '16px' }}>
</Button>
</div>
);
}
return (
<div style={{ padding: '24px' }}>
{/* 头部导航 */}
<div style={{ marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/calls')}
style={{ marginRight: '16px' }}
>
</Button>
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
#{call.id}
</Title>
</div>
{/* 管理员操作按钮 */}
<Space>
<Button
icon={<EditOutlined />}
onClick={() => setEditModalVisible(true)}
>
</Button>
<Button
icon={<SettingOutlined />}
onClick={() => setStatusModalVisible(true)}
>
</Button>
<Button
icon={<DollarOutlined />}
onClick={() => setRefundModalVisible(true)}
disabled={call.paymentStatus !== 'paid'}
>
退
</Button>
<Button
icon={<MessageOutlined />}
onClick={() => setAdminNoteModalVisible(true)}
>
</Button>
</Space>
</div>
{/* 系统状态提醒 */}
{call.issues && call.issues.length > 0 && (
<Alert
message="系统检测到问题"
description={call.issues.join(', ')}
type="warning"
showIcon
style={{ marginBottom: '24px' }}
/>
)}
{/* 基本信息卡片 */}
<Card title="通话信息" style={{ marginBottom: '24px' }}>
<Descriptions column={3} bordered>
<Descriptions.Item label="通话ID" span={1}>
{call.callId}
</Descriptions.Item>
<Descriptions.Item label="状态" span={1}>
<Tag color={getStatusColor(call.status)}>
{getStatusText(call.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="支付状态" span={1}>
<Tag color={getPaymentStatusColor(call.paymentStatus)}>
{getPaymentStatusText(call.paymentStatus)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="客户姓名" span={1}>
<Space>
<UserOutlined />
{call.clientName}
</Space>
</Descriptions.Item>
<Descriptions.Item label="客户电话" span={1}>
<Space>
<PhoneOutlined />
{call.clientPhone}
</Space>
</Descriptions.Item>
<Descriptions.Item label="译员" span={1}>
<Space>
<Avatar size="small" icon={<UserOutlined />} />
{call.translatorName}
</Space>
</Descriptions.Item>
<Descriptions.Item label="开始时间" span={1}>
<Space>
<ClockCircleOutlined />
{new Date(call.startTime).toLocaleString()}
</Space>
</Descriptions.Item>
<Descriptions.Item label="结束时间" span={1}>
<Space>
<ClockCircleOutlined />
{call.endTime ? new Date(call.endTime).toLocaleString() : '-'}
</Space>
</Descriptions.Item>
<Descriptions.Item label="通话时长" span={1}>
<Space>
<PhoneOutlined />
{formatTime(call.duration || 0)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="费用" span={1}>
<Space>
<DollarOutlined />
<Text strong>¥{call.cost.toFixed(2)}</Text>
</Space>
</Descriptions.Item>
<Descriptions.Item label="退款金额" span={1}>
<Space>
<DollarOutlined />
<Text type={call.refundAmount > 0 ? 'danger' : 'secondary'}>
¥{call.refundAmount.toFixed(2)}
</Text>
</Space>
</Descriptions.Item>
<Descriptions.Item label="质量评分" span={1}>
<Space>
<AuditOutlined />
<Text strong style={{ color: call.qualityScore >= 90 ? '#52c41a' : call.qualityScore >= 70 ? '#faad14' : '#ff4d4f' }}>
{call.qualityScore}/100
</Text>
</Space>
</Descriptions.Item>
</Descriptions>
{call.adminNotes && (
<div style={{ marginTop: '16px' }}>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px', background: '#f6f6f6', padding: '12px', borderRadius: '6px' }}>
{call.adminNotes}
</Paragraph>
</div>
)}
</Card>
{/* 录音播放器 */}
{call.recordingUrl && (
<Card
title={
<Space>
<SoundOutlined />
</Space>
}
style={{ marginBottom: '24px' }}
>
<div style={{ textAlign: 'center', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<Button
type="primary"
size="large"
icon={isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={handlePlayPause}
style={{ marginRight: '16px' }}
>
{isPlaying ? '暂停' : '播放'}
</Button>
<Button
icon={<DownloadOutlined />}
onClick={() => message.success('录音下载中...')}
>
</Button>
</div>
<div style={{ margin: '20px 0' }}>
<Progress
percent={audioProgress}
showInfo={false}
strokeColor="#1890ff"
/>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
<Text type="secondary">{formatTime(currentTime)}</Text>
<Text type="secondary">{formatTime(duration)}</Text>
</div>
</div>
</div>
</Card>
)}
{/* 详细内容标签页 */}
<Card>
<Tabs defaultActiveKey="transcription">
<TabPane
tab={
<Space>
<FileTextOutlined />
</Space>
}
key="transcription"
>
<div style={{ minHeight: '200px' }}>
{call.transcription ? (
<Paragraph>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
{call.transcription}
</pre>
</Paragraph>
) : (
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
</div>
)}
</div>
</TabPane>
<TabPane
tab={
<Space>
<TranslationOutlined />
</Space>
}
key="translation"
>
<div style={{ minHeight: '200px' }}>
{call.translation ? (
<Paragraph>{call.translation}</Paragraph>
) : (
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
</div>
)}
</div>
</TabPane>
<TabPane
tab={
<Space>
<StarOutlined />
</Space>
}
key="rating"
>
<div style={{ minHeight: '200px', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<Text strong></Text>
<Rate disabled value={call.rating} style={{ marginLeft: '8px' }} />
{call.rating && (
<Text style={{ marginLeft: '8px' }}>
({call.rating}/5 )
</Text>
)}
</div>
{call.feedback && (
<div>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px' }}>
{call.feedback}
</Paragraph>
</div>
)}
</div>
</TabPane>
<TabPane
tab={
<Space>
<AuditOutlined />
</Space>
}
key="quality"
>
<div style={{ padding: '20px' }}>
<Descriptions column={2}>
<Descriptions.Item label="质量评分">
<Progress
type="circle"
percent={call.qualityScore}
width={80}
strokeColor={call.qualityScore >= 90 ? '#52c41a' : call.qualityScore >= 70 ? '#faad14' : '#ff4d4f'}
/>
</Descriptions.Item>
<Descriptions.Item label="系统检测">
{call.issues && call.issues.length > 0 ? (
<div>
{call.issues.map((issue, index) => (
<Tag key={index} color="red" style={{ marginBottom: '4px' }}>
{issue}
</Tag>
))}
</div>
) : (
<Tag color="green"></Tag>
)}
</Descriptions.Item>
</Descriptions>
</div>
</TabPane>
</Tabs>
</Card>
{/* 编辑信息弹窗 */}
<Modal
title="编辑通话信息"
visible={editModalVisible}
onCancel={() => setEditModalVisible(false)}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleEdit}
>
<Form.Item
name="clientName"
label="客户姓名"
rules={[{ required: true, message: '请输入客户姓名' }]}
>
<Input />
</Form.Item>
<Form.Item
name="clientPhone"
label="客户电话"
rules={[{ required: true, message: '请输入客户电话' }]}
>
<Input />
</Form.Item>
<Form.Item
name="translatorName"
label="译员姓名"
>
<Input />
</Form.Item>
<Form.Item
name="cost"
label="费用"
rules={[{ required: true, message: '请输入费用' }]}
>
<Input type="number" addonAfter="元" />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => setEditModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 更改状态弹窗 */}
<Modal
title="更改通话状态"
visible={statusModalVisible}
onCancel={() => setStatusModalVisible(false)}
footer={null}
>
<Form
form={statusForm}
layout="vertical"
onFinish={handleStatusChange}
>
<Form.Item
name="status"
label="新状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select>
<Option value="pending"></Option>
<Option value="active"></Option>
<Option value="completed"></Option>
<Option value="cancelled"></Option>
</Select>
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => setStatusModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 退款处理弹窗 */}
<Modal
title="处理退款"
visible={refundModalVisible}
onCancel={() => setRefundModalVisible(false)}
footer={null}
>
<Form
form={refundForm}
layout="vertical"
onFinish={handleRefund}
initialValues={{ amount: call.cost }}
>
<Alert
message="退款提醒"
description={`原支付金额:¥${call.cost.toFixed(2)}`}
type="info"
style={{ marginBottom: '16px' }}
/>
<Form.Item
name="amount"
label="退款金额"
rules={[
{ required: true, message: '请输入退款金额' },
{ type: 'number', min: 0, max: call.cost, message: `退款金额不能超过¥${call.cost.toFixed(2)}` }
]}
>
<Input type="number" addonAfter="元" />
</Form.Item>
<Form.Item
name="reason"
label="退款原因"
rules={[{ required: true, message: '请输入退款原因' }]}
>
<TextArea rows={3} placeholder="请输入退款原因..." />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => setRefundModalVisible(false)}>
</Button>
<Button type="primary" danger htmlType="submit">
退
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 添加管理员备注弹窗 */}
<Modal
title="添加管理员备注"
visible={adminNoteModalVisible}
onCancel={() => setAdminNoteModalVisible(false)}
footer={null}
>
<Form
form={noteForm}
layout="vertical"
onFinish={handleAddAdminNote}
initialValues={{ note: call.adminNotes }}
>
<Form.Item
name="note"
label="备注内容"
rules={[{ required: true, message: '请输入备注内容' }]}
>
<TextArea rows={4} placeholder="请输入管理员备注..." />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => setAdminNoteModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default CallDetail;

View File

@ -0,0 +1,72 @@
import React from 'react';
import { Card, Row, Col, Statistic, Typography } from 'antd';
import {
PhoneOutlined,
FileTextOutlined,
CalendarOutlined,
DollarOutlined
} from '@ant-design/icons';
const { Title } = Typography;
const Dashboard: React.FC = () => {
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={16}>
<Col span={6}>
<Card>
<Statistic
title="总通话数"
value={1128}
prefix={<PhoneOutlined />}
valueStyle={{ color: '#3f8600' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="文档翻译"
value={892}
prefix={<FileTextOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="预约服务"
value={456}
prefix={<CalendarOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="总收入"
value={25680}
prefix={<DollarOutlined />}
valueStyle={{ color: '#cf1322' }}
suffix="元"
/>
</Card>
</Col>
</Row>
<div style={{ marginTop: '24px' }}>
<Card title="系统状态">
<p> </p>
<p> 线</p>
<p> </p>
</Card>
</div>
</div>
);
};
export default Dashboard;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
// 通话相关类型
export interface TranslationCall {
id: string;
userId: string;
callId: string;
clientName: string;
clientPhone: string;
type: 'human' | 'ai';
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'refunded';
sourceLanguage: string;
targetLanguage: string;
startTime: string;
endTime?: string;
duration?: number;
cost: number;
rating?: number;
feedback?: string;
translatorId?: string;
translatorName?: string;
translatorPhone?: string;
recordingUrl?: string;
transcription?: string;
translation?: string;
// 管理员相关字段
adminNotes?: string;
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
refundAmount: number;
qualityScore: number;
issues: string[];
createdAt?: string;
updatedAt?: string;
}
// 文档翻译相关类型
export interface DocumentTranslation {
id: string;
userId: string;
fileName: string;
originalSize: number;
fileUrl: string;
translatedFileUrl?: string;
sourceLanguage: string;
targetLanguage: string;
status: 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed';
progress: number;
quality: 'basic' | 'professional' | 'premium';
urgency: 'normal' | 'urgent' | 'emergency';
estimatedTime: number;
actualTime?: number;
cost: number;
translatorId?: string;
translatorName?: string;
rating?: number;
feedback?: string;
createdAt: string;
completedAt?: string;
// 管理员相关字段
adminNotes?: string;
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
refundAmount: number;
qualityScore: number;
issues: string[];
retranslationCount?: number;
clientName?: string;
clientEmail?: string;
clientPhone?: string;
updatedAt?: string;
}
// 预约相关类型
export interface Appointment {
id: string;
userId: string;
translatorId: string;
title: string;
description: string;
type: string;
sourceLanguage: string;
targetLanguage: string;
startTime: string;
endTime: string;
status: string;
cost: number;
meetingUrl?: string;
notes?: string;
reminderSent: boolean;
createdAt: string;
updatedAt?: string;
// 管理员相关字段
clientName: string;
clientEmail: string;
clientPhone: string;
translatorName: string;
translatorEmail: string;
translatorPhone: string;
adminNotes?: string;
paymentStatus: string;
refundAmount: number;
qualityScore: number;
issues: string[];
rating?: number;
feedback?: string;
location?: string;
urgency: string;
}
// 用户类型
export interface User {
id: string;
name: string;
email: string;
phone?: string;
role: 'client' | 'translator' | 'admin';
status: 'active' | 'inactive' | 'suspended';
createdAt: string;
updatedAt?: string;
}
// 译员类型
export interface Translator {
id: string;
name: string;
email: string;
phone: string;
languages: string[];
specializations: string[];
rating: number;
hourlyRate: number;
status: 'available' | 'busy' | 'offline';
totalJobs: number;
successRate: number;
createdAt: string;
}
// API响应类型
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
// 分页类型
export interface PaginationParams {
page: number;
pageSize: number;
total?: number;
}
// 搜索参数类型
export interface SearchParams {
keyword?: string;
status?: string;
dateRange?: [string, string];
[key: string]: any;
}

View File

@ -0,0 +1,220 @@
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse } from '../types';
// API基础URL
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
// API请求工具类
class ApiManager {
private baseURL: string;
constructor(baseURL: string = API_BASE_URL) {
this.baseURL = baseURL;
}
// 通用请求方法
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '请求失败');
}
return {
success: true,
data,
message: '操作成功',
};
} catch (error) {
console.error('API请求错误:', error);
return {
success: false,
data: null as any,
message: error instanceof Error ? error.message : '网络错误',
};
}
}
// 通话管理API
async getCall(id: string): Promise<ApiResponse<TranslationCall>> {
return this.request<TranslationCall>(`/calls/${id}`);
}
async updateCall(id: string, data: Partial<TranslationCall>): Promise<ApiResponse<TranslationCall>> {
return this.request<TranslationCall>(`/calls/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteCall(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${id}`, {
method: 'DELETE',
});
}
async processRefund(callId: string, amount: number, reason: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${callId}/refund`, {
method: 'POST',
body: JSON.stringify({ amount, reason }),
});
}
async addCallNote(callId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${callId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
}
// 文档翻译API
async getDocument(id: string): Promise<ApiResponse<DocumentTranslation>> {
return this.request<DocumentTranslation>(`/documents/${id}`);
}
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<ApiResponse<DocumentTranslation>> {
return this.request<DocumentTranslation>(`/documents/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteDocument(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${id}`, {
method: 'DELETE',
});
}
async reassignTranslator(documentId: string, translatorId: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/reassign`, {
method: 'POST',
body: JSON.stringify({ translatorId }),
});
}
async retranslateDocument(documentId: string, quality: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/retranslate`, {
method: 'POST',
body: JSON.stringify({ quality }),
});
}
async addDocumentNote(documentId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
}
// 预约管理API
async getAppointment(id: string): Promise<ApiResponse<Appointment>> {
return this.request<Appointment>(`/appointments/${id}`);
}
async updateAppointment(id: string, data: Partial<Appointment>): Promise<ApiResponse<Appointment>> {
return this.request<Appointment>(`/appointments/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteAppointment(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${id}`, {
method: 'DELETE',
});
}
async rescheduleAppointment(
appointmentId: string,
newStartTime: string,
newEndTime: string
): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/reschedule`, {
method: 'POST',
body: JSON.stringify({ newStartTime, newEndTime }),
});
}
async reassignAppointmentTranslator(
appointmentId: string,
translatorId: string
): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/reassign`, {
method: 'POST',
body: JSON.stringify({ translatorId }),
});
}
async addAppointmentNote(appointmentId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
}
// 退款处理API
async refundPayment(paymentId: string, amount: number): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/payments/${paymentId}/refund`, {
method: 'POST',
body: JSON.stringify({ amount }),
});
}
// 统计数据API
async getStatistics(): Promise<ApiResponse<any>> {
return this.request<any>('/statistics');
}
// 用户管理API
async getUsers(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
return this.request<any>(`/users?page=${page}&pageSize=${pageSize}`);
}
async updateUser(userId: string, data: any): Promise<ApiResponse<any>> {
return this.request<any>(`/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
// 译员管理API
async getTranslators(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
return this.request<any>(`/translators?page=${page}&pageSize=${pageSize}`);
}
async updateTranslator(translatorId: string, data: any): Promise<ApiResponse<any>> {
return this.request<any>(`/translators/${translatorId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
// 系统配置API
async getSystemConfig(): Promise<ApiResponse<any>> {
return this.request<any>('/config');
}
async updateSystemConfig(config: any): Promise<ApiResponse<any>> {
return this.request<any>('/config', {
method: 'PUT',
body: JSON.stringify(config),
});
}
}
// 导出API实例
export const api = new ApiManager();
export default api;

View File

@ -0,0 +1,290 @@
import { TranslationCall, DocumentTranslation, Appointment, User, Translator } from '../types';
// 模拟数据库连接类
class DatabaseManager {
private isConnected: boolean = false;
// 连接数据库
async connect(): Promise<void> {
if (!this.isConnected) {
// 模拟连接延迟
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
console.log('数据库连接成功');
}
}
// 断开数据库连接
async disconnect(): Promise<void> {
if (this.isConnected) {
this.isConnected = false;
console.log('数据库连接已断开');
}
}
// 检查连接状态
isConnectionActive(): boolean {
return this.isConnected;
}
// 通话相关操作
async getCalls(params?: any): Promise<TranslationCall[]> {
await this.connect();
// 模拟获取通话列表
return [];
}
async getCallById(id: string): Promise<TranslationCall | null> {
await this.connect();
// 模拟获取单个通话
return null;
}
async createCall(data: Partial<TranslationCall>): Promise<TranslationCall> {
await this.connect();
// 模拟创建通话
const newCall: TranslationCall = {
id: `call_${Date.now()}`,
userId: data.userId || '',
callId: `CA${Date.now()}`,
clientName: data.clientName || '',
clientPhone: data.clientPhone || '',
type: data.type || 'human',
status: 'pending',
sourceLanguage: data.sourceLanguage || '',
targetLanguage: data.targetLanguage || '',
startTime: new Date().toISOString(),
cost: data.cost || 0,
paymentStatus: 'pending',
refundAmount: 0,
qualityScore: 0,
issues: [],
createdAt: new Date().toISOString(),
};
return newCall;
}
async updateCall(id: string, data: Partial<TranslationCall>): Promise<TranslationCall | null> {
await this.connect();
// 模拟更新通话
return null;
}
async deleteCall(id: string): Promise<boolean> {
await this.connect();
// 模拟删除通话
return true;
}
// 文档翻译相关操作
async getDocuments(params?: any): Promise<DocumentTranslation[]> {
await this.connect();
// 模拟获取文档列表
return [];
}
async getDocumentById(id: string): Promise<DocumentTranslation | null> {
await this.connect();
// 模拟获取单个文档
return null;
}
async createDocument(data: Partial<DocumentTranslation>): Promise<DocumentTranslation> {
await this.connect();
// 模拟创建文档翻译
const newDocument: DocumentTranslation = {
id: `doc_${Date.now()}`,
userId: data.userId || '',
fileName: data.fileName || '',
originalSize: data.originalSize || 0,
fileUrl: data.fileUrl || '',
sourceLanguage: data.sourceLanguage || '',
targetLanguage: data.targetLanguage || '',
status: 'pending',
progress: 0,
quality: data.quality || 'basic',
urgency: data.urgency || 'normal',
estimatedTime: data.estimatedTime || 0,
cost: data.cost || 0,
createdAt: new Date().toISOString(),
paymentStatus: 'pending',
refundAmount: 0,
qualityScore: 0,
issues: [],
};
return newDocument;
}
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<DocumentTranslation | null> {
await this.connect();
// 模拟更新文档
return null;
}
async deleteDocument(id: string): Promise<boolean> {
await this.connect();
// 模拟删除文档
return true;
}
// 预约相关操作
async getAppointments(params?: any): Promise<Appointment[]> {
await this.connect();
// 模拟获取预约列表
return [];
}
async getAppointmentById(id: string): Promise<Appointment | null> {
await this.connect();
// 模拟获取单个预约
return null;
}
async createAppointment(data: Partial<Appointment>): Promise<Appointment> {
await this.connect();
// 模拟创建预约
const newAppointment: Appointment = {
id: `apt_${Date.now()}`,
userId: data.userId || '',
translatorId: data.translatorId || '',
title: data.title || '',
description: data.description || '',
type: data.type || '',
sourceLanguage: data.sourceLanguage || '',
targetLanguage: data.targetLanguage || '',
startTime: data.startTime || new Date().toISOString(),
endTime: data.endTime || new Date().toISOString(),
status: 'pending',
cost: data.cost || 0,
reminderSent: false,
createdAt: new Date().toISOString(),
clientName: data.clientName || '',
clientEmail: data.clientEmail || '',
clientPhone: data.clientPhone || '',
translatorName: data.translatorName || '',
translatorEmail: data.translatorEmail || '',
translatorPhone: data.translatorPhone || '',
paymentStatus: 'pending',
refundAmount: 0,
qualityScore: 0,
issues: [],
urgency: data.urgency || 'normal',
};
return newAppointment;
}
async updateAppointment(id: string, data: Partial<Appointment>): Promise<Appointment | null> {
await this.connect();
// 模拟更新预约
return null;
}
async deleteAppointment(id: string): Promise<boolean> {
await this.connect();
// 模拟删除预约
return true;
}
// 用户相关操作
async getUsers(params?: any): Promise<User[]> {
await this.connect();
// 模拟获取用户列表
return [];
}
async getUserById(id: string): Promise<User | null> {
await this.connect();
// 模拟获取单个用户
return null;
}
async createUser(data: Partial<User>): Promise<User> {
await this.connect();
// 模拟创建用户
const newUser: User = {
id: `user_${Date.now()}`,
name: data.name || '',
email: data.email || '',
phone: data.phone,
role: data.role || 'client',
status: 'active',
createdAt: new Date().toISOString(),
};
return newUser;
}
async updateUser(id: string, data: Partial<User>): Promise<User | null> {
await this.connect();
// 模拟更新用户
return null;
}
async deleteUser(id: string): Promise<boolean> {
await this.connect();
// 模拟删除用户
return true;
}
// 译员相关操作
async getTranslators(params?: any): Promise<Translator[]> {
await this.connect();
// 模拟获取译员列表
return [];
}
async getTranslatorById(id: string): Promise<Translator | null> {
await this.connect();
// 模拟获取单个译员
return null;
}
async createTranslator(data: Partial<Translator>): Promise<Translator> {
await this.connect();
// 模拟创建译员
const newTranslator: Translator = {
id: `translator_${Date.now()}`,
name: data.name || '',
email: data.email || '',
phone: data.phone || '',
languages: data.languages || [],
specializations: data.specializations || [],
rating: data.rating || 0,
hourlyRate: data.hourlyRate || 0,
status: 'available',
totalJobs: 0,
successRate: 0,
createdAt: new Date().toISOString(),
};
return newTranslator;
}
async updateTranslator(id: string, data: Partial<Translator>): Promise<Translator | null> {
await this.connect();
// 模拟更新译员
return null;
}
async deleteTranslator(id: string): Promise<boolean> {
await this.connect();
// 模拟删除译员
return true;
}
// 统计相关操作
async getStatistics(): Promise<any> {
await this.connect();
// 模拟获取统计数据
return {
totalCalls: 0,
totalDocuments: 0,
totalAppointments: 0,
totalUsers: 0,
totalTranslators: 0,
totalRevenue: 0,
};
}
}
// 导出单例实例
export const database = new DatabaseManager();
export default database;

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"es6"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

19
package-lock.json generated
View File

@ -12,12 +12,14 @@
"@ant-design/plots": "^2.5.0",
"@reduxjs/toolkit": "^1.9.7",
"@tanstack/react-query": "^5.8.4",
"@types/moment": "^2.13.0",
"@types/react-native-vector-icons": "^6.4.18",
"antd": "^5.12.5",
"axios": "^1.6.2",
"classnames": "^2.3.2",
"dayjs": "^1.11.10",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
@ -2238,6 +2240,15 @@
"@types/unist": "*"
}
},
"node_modules/@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==",
"deprecated": "This is a stub types definition for Moment (https://github.com/moment/moment). Moment provides its own type definitions, so you don't need @types/moment installed!",
"dependencies": {
"moment": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@ -6342,6 +6353,14 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",

View File

@ -15,12 +15,14 @@
"@ant-design/plots": "^2.5.0",
"@reduxjs/toolkit": "^1.9.7",
"@tanstack/react-query": "^5.8.4",
"@types/moment": "^2.13.0",
"@types/react-native-vector-icons": "^6.4.18",
"antd": "^5.12.5",
"axios": "^1.6.2",
"classnames": "^2.3.2",
"dayjs": "^1.11.10",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",

View File

@ -0,0 +1,530 @@
import * as React from 'react';
import { useState } from 'react';
import {
Modal,
Form,
Input,
Select,
Button,
Space,
message,
DatePicker,
TimePicker,
Radio,
Divider,
Card,
Tag,
Avatar,
Alert,
} from 'antd';
import {
CalendarOutlined,
UserOutlined,
TranslationOutlined,
DollarOutlined,
ClockCircleOutlined,
VideoCameraOutlined,
PhoneOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@/utils/api';
const { Option } = Select;
const { RangePicker } = TimePicker;
interface NewAppointmentModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
interface AppointmentFormData {
title: string;
description?: string;
type: 'interpretation' | 'consultation' | 'document_review';
sourceLanguage: string;
targetLanguage: string;
date: dayjs.Dayjs;
timeRange: [dayjs.Dayjs, dayjs.Dayjs];
translatorId?: string;
meetingType: 'online' | 'offline' | 'phone';
location?: string;
urgency: 'normal' | 'urgent';
notes?: string;
}
const NewAppointmentModal: React.FC<NewAppointmentModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
const [meetingType, setMeetingType] = useState<'online' | 'offline' | 'phone'>('online');
const [estimatedCost, setEstimatedCost] = useState(0);
const [availableTranslators, setAvailableTranslators] = useState<any[]>([]);
const languages = [
{ code: 'zh-CN', name: '中文' },
{ code: 'en-US', name: '英语' },
{ code: 'ja-JP', name: '日语' },
{ code: 'ko-KR', name: '韩语' },
{ code: 'fr-FR', name: '法语' },
{ code: 'de-DE', name: '德语' },
{ code: 'es-ES', name: '西班牙语' },
{ code: 'ru-RU', name: '俄语' },
];
const translators = [
{
id: 'translator_1',
name: '李翻译',
avatar: '👨‍💼',
specialization: '商务翻译',
languages: ['zh-CN', 'en-US'],
rating: 4.9,
hourlyRate: 200,
available: true,
},
{
id: 'translator_2',
name: '王翻译',
avatar: '👩‍💼',
specialization: '法律翻译',
languages: ['zh-CN', 'ja-JP'],
rating: 4.8,
hourlyRate: 250,
available: true,
},
{
id: 'translator_3',
name: '张翻译',
avatar: '👨‍🎓',
specialization: '技术翻译',
languages: ['zh-CN', 'en-US', 'ko-KR'],
rating: 4.7,
hourlyRate: 180,
available: false,
},
];
// 计算预估费用
const calculateCost = (
type: string,
duration: number,
translatorId?: string,
urgency: string = 'normal'
) => {
const baseRates = {
interpretation: 200,
consultation: 150,
document_review: 100,
};
let rate = baseRates[type as keyof typeof baseRates] || 150;
if (translatorId) {
const translator = translators.find(t => t.id === translatorId);
if (translator) {
rate = translator.hourlyRate;
}
}
const urgencyMultiplier = urgency === 'urgent' ? 1.5 : 1.0;
return rate * duration * urgencyMultiplier;
};
const handleFormChange = () => {
const values = form.getFieldsValue();
if (values.type && values.timeRange && values.timeRange.length === 2) {
const duration = values.timeRange[1].diff(values.timeRange[0], 'hour', true);
const cost = calculateCost(values.type, duration, values.translatorId, values.urgency);
setEstimatedCost(cost);
}
};
const handleDateChange = (date: dayjs.Dayjs | null) => {
setSelectedDate(date);
if (date) {
// 模拟查询可用译员
const available = translators.filter(t => t.available);
setAvailableTranslators(available);
}
};
const disabledDate = (current: dayjs.Dayjs) => {
// 禁用过去的日期
return current && current < dayjs().startOf('day');
};
const disabledTime = () => {
const now = dayjs();
const isToday = selectedDate && selectedDate.isSame(now, 'day');
if (isToday) {
return {
disabledHours: () => {
const hours = [];
for (let i = 0; i < now.hour(); i++) {
hours.push(i);
}
return hours;
},
disabledMinutes: (selectedHour: number) => {
if (selectedHour === now.hour()) {
const minutes = [];
for (let i = 0; i <= now.minute(); i++) {
minutes.push(i);
}
return minutes;
}
return [];
},
};
}
return {};
};
const handleSubmit = async (values: AppointmentFormData) => {
try {
setLoading(true);
const startTime = values.date
.hour(values.timeRange[0].hour())
.minute(values.timeRange[0].minute());
const endTime = values.date
.hour(values.timeRange[1].hour())
.minute(values.timeRange[1].minute());
const appointmentData = {
...values,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
cost: estimatedCost,
status: 'pending',
meetingUrl: meetingType === 'online' ? `https://meet.example.com/${Date.now()}` : undefined,
};
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1500));
message.success('预约创建成功!');
form.resetFields();
setEstimatedCost(0);
setSelectedDate(null);
setAvailableTranslators([]);
onSuccess();
} catch (error) {
console.error('创建预约失败:', error);
message.error('创建预约失败,请重试');
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
setEstimatedCost(0);
setSelectedDate(null);
setAvailableTranslators([]);
setMeetingType('online');
onCancel();
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'interpretation':
return '🗣️';
case 'consultation':
return '💬';
case 'document_review':
return '📋';
default:
return '📅';
}
};
const getMeetingTypeIcon = (type: string) => {
switch (type) {
case 'online':
return <VideoCameraOutlined />;
case 'phone':
return <PhoneOutlined />;
case 'offline':
return <UserOutlined />;
default:
return <VideoCameraOutlined />;
}
};
return (
<Modal
title={
<Space>
<CalendarOutlined />
</Space>
}
visible={visible}
onCancel={handleCancel}
footer={null}
width={700}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
onValuesChange={handleFormChange}
initialValues={{
type: 'interpretation',
meetingType: 'online',
urgency: 'normal',
}}
>
{/* 基本信息 */}
<Divider orientation="left"></Divider>
<Form.Item
name="title"
label="预约标题"
rules={[{ required: true, message: '请输入预约标题' }]}
>
<Input
placeholder="请输入预约标题,如:商务会议翻译"
prefix={<CalendarOutlined />}
/>
</Form.Item>
<Form.Item
name="description"
label="预约描述"
>
<Input.TextArea
rows={3}
placeholder="请详细描述预约内容、会议主题、参与人员等..."
/>
</Form.Item>
<Form.Item
name="type"
label="服务类型"
rules={[{ required: true, message: '请选择服务类型' }]}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value="interpretation">
🗣 -
</Radio.Button>
<Radio.Button value="consultation">
💬 -
</Radio.Button>
<Radio.Button value="document_review">
📋 -
</Radio.Button>
</Radio.Group>
</Form.Item>
{/* 语言配置 */}
<Divider orientation="left"></Divider>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="sourceLanguage"
label="源语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择源语言' }]}
>
<Select placeholder="选择源语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="targetLanguage"
label="目标语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择目标语言' }]}
>
<Select placeholder="选择目标语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
</div>
{/* 时间安排 */}
<Divider orientation="left"></Divider>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="date"
label="预约日期"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择预约日期' }]}
>
<DatePicker
style={{ width: '100%' }}
placeholder="选择日期"
disabledDate={disabledDate}
onChange={handleDateChange}
/>
</Form.Item>
<Form.Item
name="timeRange"
label="时间段"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择时间段' }]}
>
<RangePicker
style={{ width: '100%' }}
placeholder={['开始时间', '结束时间']}
format="HH:mm"
minuteStep={15}
disabledTime={disabledTime}
/>
</Form.Item>
</div>
{/* 会议方式 */}
<Form.Item
name="meetingType"
label="会议方式"
rules={[{ required: true, message: '请选择会议方式' }]}
>
<Radio.Group
buttonStyle="solid"
onChange={(e) => setMeetingType(e.target.value)}
>
<Radio.Button value="online">
<Space>
<VideoCameraOutlined />
线
</Space>
</Radio.Button>
<Radio.Button value="phone">
<Space>
<PhoneOutlined />
</Space>
</Radio.Button>
<Radio.Button value="offline">
<Space>
<UserOutlined />
线
</Space>
</Radio.Button>
</Radio.Group>
</Form.Item>
{meetingType === 'offline' && (
<Form.Item
name="location"
label="会议地点"
rules={[{ required: true, message: '请输入会议地点' }]}
>
<Input placeholder="请输入详细的会议地点" />
</Form.Item>
)}
{/* 译员选择 */}
<Divider orientation="left"></Divider>
{selectedDate && (
<Alert
message="可用译员"
description={`${selectedDate.format('YYYY年MM月DD日')} 共有 ${availableTranslators.length} 位译员可用`}
type="info"
style={{ marginBottom: 16 }}
/>
)}
<Form.Item
name="translatorId"
label="指定译员"
>
<Select placeholder="选择译员(可选,系统会自动分配)" allowClear>
{availableTranslators.map(translator => (
<Option key={translator.id} value={translator.id}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<span style={{ fontSize: '16px' }}>{translator.avatar}</span>
<div>
<div>{translator.name}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{translator.specialization} {translator.rating}
</div>
</div>
</Space>
<Tag color="blue">¥{translator.hourlyRate}/</Tag>
</div>
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="urgency"
label="紧急程度"
>
<Radio.Group>
<Radio value="normal"></Radio>
<Radio value="urgent"> (+50% )</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="notes"
label="特殊要求"
>
<Input.TextArea
rows={3}
placeholder="请输入特殊要求或备注信息,如专业领域、术语要求等..."
/>
</Form.Item>
{/* 费用预估 */}
{estimatedCost > 0 && (
<div style={{
background: '#f6f6f6',
padding: '16px',
borderRadius: '6px',
marginBottom: '16px'
}}>
<Space>
<DollarOutlined style={{ color: '#1890ff' }} />
<span><strong></strong></span>
<span style={{ fontSize: '18px', color: '#1890ff', fontWeight: 'bold' }}>
¥{estimatedCost.toFixed(2)}
</span>
</Space>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
*
</div>
</div>
)}
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={handleCancel}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default NewAppointmentModal;

View File

@ -0,0 +1,326 @@
import * as React from 'react';
import { useState } from 'react';
import {
Modal,
Form,
Input,
Select,
Button,
Space,
message,
Radio,
InputNumber,
Switch,
Divider,
} from 'antd';
import {
PhoneOutlined,
UserOutlined,
TranslationOutlined,
DollarOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { api } from '@/utils/api';
const { Option } = Select;
interface NewCallModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
interface CallFormData {
clientName: string;
clientPhone: string;
type: 'ai' | 'human' | 'video' | 'sign';
sourceLanguage: string;
targetLanguage: string;
urgency: 'normal' | 'urgent';
estimatedDuration: number;
notes?: string;
translatorId?: string;
}
const NewCallModal: React.FC<NewCallModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [callType, setCallType] = useState<'ai' | 'human' | 'video' | 'sign'>('ai');
const [estimatedCost, setEstimatedCost] = useState(0);
const languages = [
{ code: 'zh-CN', name: '中文' },
{ code: 'en-US', name: '英语' },
{ code: 'ja-JP', name: '日语' },
{ code: 'ko-KR', name: '韩语' },
{ code: 'fr-FR', name: '法语' },
{ code: 'de-DE', name: '德语' },
{ code: 'es-ES', name: '西班牙语' },
{ code: 'ru-RU', name: '俄语' },
];
const translators = [
{ id: 'translator_1', name: '李翻译 - 中英专家', languages: ['zh-CN', 'en-US'] },
{ id: 'translator_2', name: '王翻译 - 日语专家', languages: ['zh-CN', 'ja-JP'] },
{ id: 'translator_3', name: '张翻译 - 多语言', languages: ['zh-CN', 'en-US', 'ko-KR'] },
];
// 计算预估费用
const calculateCost = (type: string, duration: number, urgency: string) => {
const rates = {
ai: 0.5,
human: 2.0,
video: 3.0,
sign: 4.0,
};
const baseRate = rates[type as keyof typeof rates] || 1.0;
const urgencyMultiplier = urgency === 'urgent' ? 1.5 : 1.0;
return baseRate * duration * urgencyMultiplier;
};
const handleFormChange = () => {
const values = form.getFieldsValue();
if (values.type && values.estimatedDuration) {
const cost = calculateCost(values.type, values.estimatedDuration, values.urgency || 'normal');
setEstimatedCost(cost);
}
};
const handleSubmit = async (values: CallFormData) => {
try {
setLoading(true);
// 调用API创建通话
const callData = {
...values,
cost: estimatedCost,
status: 'pending',
startTime: new Date().toISOString(),
};
// 模拟API调用
await api.initiateCall({
from: '+86123456789', // 系统电话号码
to: values.clientPhone,
sourceLanguage: values.sourceLanguage,
targetLanguage: values.targetLanguage,
type: values.type
});
message.success('通话创建成功!');
form.resetFields();
setEstimatedCost(0);
onSuccess();
} catch (error) {
console.error('创建通话失败:', error);
message.error('创建通话失败,请重试');
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
setEstimatedCost(0);
setCallType('ai');
onCancel();
};
return (
<Modal
title={
<Space>
<PhoneOutlined />
</Space>
}
visible={visible}
onCancel={handleCancel}
footer={null}
width={600}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
onValuesChange={handleFormChange}
initialValues={{
type: 'ai',
urgency: 'normal',
estimatedDuration: 30,
}}
>
{/* 客户信息 */}
<Divider orientation="left"></Divider>
<Form.Item
name="clientName"
label="客户姓名"
rules={[{ required: true, message: '请输入客户姓名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="请输入客户姓名"
/>
</Form.Item>
<Form.Item
name="clientPhone"
label="客户电话"
rules={[
{ required: true, message: '请输入客户电话' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="请输入客户电话号码"
/>
</Form.Item>
{/* 服务配置 */}
<Divider orientation="left"></Divider>
<Form.Item
name="type"
label="翻译类型"
rules={[{ required: true, message: '请选择翻译类型' }]}
>
<Radio.Group
onChange={(e) => setCallType(e.target.value)}
buttonStyle="solid"
>
<Radio.Button value="ai">🤖 AI翻译</Radio.Button>
<Radio.Button value="human">👤 </Radio.Button>
<Radio.Button value="video">📹 </Radio.Button>
<Radio.Button value="sign">🤟 </Radio.Button>
</Radio.Group>
</Form.Item>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="sourceLanguage"
label="源语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择源语言' }]}
>
<Select placeholder="选择源语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="targetLanguage"
label="目标语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择目标语言' }]}
>
<Select placeholder="选择目标语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
</div>
{/* 人工翻译时显示译员选择 */}
{(callType === 'human' || callType === 'video' || callType === 'sign') && (
<Form.Item
name="translatorId"
label="指定译员"
>
<Select placeholder="选择译员(可选,系统会自动分配)" allowClear>
{translators.map(translator => (
<Option key={translator.id} value={translator.id}>
{translator.name}
</Option>
))}
</Select>
</Form.Item>
)}
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="estimatedDuration"
label="预估时长(分钟)"
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入预估时长' }]}
>
<InputNumber
min={5}
max={480}
placeholder="预估通话时长"
style={{ width: '100%' }}
addonAfter="分钟"
/>
</Form.Item>
<Form.Item
name="urgency"
label="紧急程度"
style={{ flex: 1 }}
>
<Select>
<Option value="normal"></Option>
<Option value="urgent"> (+50% )</Option>
</Select>
</Form.Item>
</div>
<Form.Item
name="notes"
label="备注说明"
>
<Input.TextArea
rows={3}
placeholder="请输入特殊要求或备注信息..."
/>
</Form.Item>
{/* 费用预估 */}
{estimatedCost > 0 && (
<div style={{
background: '#f6f6f6',
padding: '16px',
borderRadius: '6px',
marginBottom: '16px'
}}>
<Space>
<DollarOutlined style={{ color: '#1890ff' }} />
<span><strong></strong></span>
<span style={{ fontSize: '18px', color: '#1890ff', fontWeight: 'bold' }}>
¥{estimatedCost.toFixed(2)}
</span>
</Space>
</div>
)}
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={handleCancel}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default NewCallModal;

View File

@ -0,0 +1,442 @@
import * as React from 'react';
import { useState } from 'react';
import {
Modal,
Form,
Input,
Select,
Button,
Space,
message,
Upload,
Progress,
Radio,
Divider,
Card,
Tag,
} from 'antd';
import {
FileTextOutlined,
UploadOutlined,
TranslationOutlined,
DollarOutlined,
ClockCircleOutlined,
DeleteOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { UploadFile, UploadProps } from 'antd/es/upload/interface';
import { api } from '@/utils/api';
const { Option } = Select;
const { Dragger } = Upload;
interface NewDocumentModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
interface DocumentFormData {
sourceLanguage: string;
targetLanguage: string;
quality: 'draft' | 'professional' | 'certified';
urgency: 'normal' | 'urgent' | 'express';
notes?: string;
files: UploadFile[];
}
const NewDocumentModal: React.FC<NewDocumentModalProps> = ({
visible,
onCancel,
onSuccess,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [estimatedCost, setEstimatedCost] = useState(0);
const [estimatedTime, setEstimatedTime] = useState(0);
const languages = [
{ code: 'zh-CN', name: '中文' },
{ code: 'en-US', name: '英语' },
{ code: 'ja-JP', name: '日语' },
{ code: 'ko-KR', name: '韩语' },
{ code: 'fr-FR', name: '法语' },
{ code: 'de-DE', name: '德语' },
{ code: 'es-ES', name: '西班牙语' },
{ code: 'ru-RU', name: '俄语' },
];
// 计算预估费用和时间
const calculateEstimates = (files: UploadFile[], quality: string, urgency: string) => {
let totalSize = 0;
let totalPages = 0;
files.forEach(file => {
if (file.size) {
totalSize += file.size;
// 估算页数假设每页约50KB
totalPages += Math.ceil(file.size / (50 * 1024));
}
});
// 计算费用(每页基础价格)
const baseRates = {
draft: 2.0,
professional: 5.0,
certified: 10.0,
};
const urgencyMultipliers = {
normal: 1.0,
urgent: 1.5,
express: 2.0,
};
const baseRate = baseRates[quality as keyof typeof baseRates] || 5.0;
const urgencyMultiplier = urgencyMultipliers[urgency as keyof typeof urgencyMultipliers] || 1.0;
const cost = totalPages * baseRate * urgencyMultiplier;
// 计算时间(小时)
const baseTimePerPage = quality === 'certified' ? 2 : quality === 'professional' ? 1 : 0.5;
const time = totalPages * baseTimePerPage / (urgency === 'express' ? 2 : urgency === 'urgent' ? 1.5 : 1);
return { cost, time: Math.max(1, time) };
};
const handleFormChange = () => {
const values = form.getFieldsValue();
if (fileList.length > 0 && values.quality && values.urgency) {
const { cost, time } = calculateEstimates(fileList, values.quality, values.urgency);
setEstimatedCost(cost);
setEstimatedTime(time);
}
};
const uploadProps: UploadProps = {
name: 'file',
multiple: true,
fileList,
beforeUpload: (file) => {
const isValidType = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
].includes(file.type);
if (!isValidType) {
message.error('只支持 PDF、Word、PowerPoint 和文本文件!');
return false;
}
const isLt10M = file.size! / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('文件大小不能超过 10MB');
return false;
}
return false; // 阻止自动上传
},
onChange: (info) => {
setFileList(info.fileList);
// 重新计算费用
setTimeout(handleFormChange, 100);
},
onRemove: (file) => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
setFileList(newFileList);
return true;
},
};
const getFileIcon = (fileName: string) => {
const ext = fileName.split('.').pop()?.toLowerCase();
switch (ext) {
case 'pdf':
return '📄';
case 'doc':
case 'docx':
return '📝';
case 'ppt':
case 'pptx':
return '📊';
case 'txt':
return '📃';
default:
return '📄';
}
};
const handleSubmit = async (values: DocumentFormData) => {
if (fileList.length === 0) {
message.error('请至少上传一个文件!');
return;
}
try {
setLoading(true);
setUploading(true);
// 上传文件
const uploadPromises = fileList.map(async (file) => {
if (file.originFileObj) {
const uploadResult = await api.uploadFile(file.originFileObj, 'document');
return uploadResult;
}
return null;
});
const uploadResults = await Promise.all(uploadPromises);
// 创建翻译任务
for (const result of uploadResults) {
if (result?.success && result.data) {
await api.translateDocument({
fileUrl: result.data.fileUrl,
fileName: result.data.fileName,
sourceLanguage: values.sourceLanguage,
targetLanguage: values.targetLanguage,
quality: values.quality,
});
}
}
message.success('文档翻译任务创建成功!');
form.resetFields();
setFileList([]);
setEstimatedCost(0);
setEstimatedTime(0);
onSuccess();
} catch (error) {
console.error('创建翻译任务失败:', error);
message.error('创建翻译任务失败,请重试');
} finally {
setLoading(false);
setUploading(false);
}
};
const handleCancel = () => {
form.resetFields();
setFileList([]);
setEstimatedCost(0);
setEstimatedTime(0);
onCancel();
};
const formatFileSize = (size: number) => {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
};
return (
<Modal
title={
<Space>
<FileTextOutlined />
</Space>
}
visible={visible}
onCancel={handleCancel}
footer={null}
width={700}
destroyOnClose
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
onValuesChange={handleFormChange}
initialValues={{
quality: 'professional',
urgency: 'normal',
}}
>
{/* 文件上传 */}
<Divider orientation="left"></Divider>
<Form.Item
label="选择文件"
required
>
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<UploadOutlined style={{ fontSize: 48, color: '#1890ff' }} />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint">
PDFWordPowerPoint 10MB
</p>
</Dragger>
</Form.Item>
{/* 文件列表 */}
{fileList.length > 0 && (
<div style={{ marginBottom: 16 }}>
<h4></h4>
{fileList.map(file => (
<Card
key={file.uid}
size="small"
style={{ marginBottom: 8 }}
bodyStyle={{ padding: '8px 12px' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<span style={{ fontSize: '16px' }}>{getFileIcon(file.name)}</span>
<span>{file.name}</span>
<Tag color="blue">{formatFileSize(file.size || 0)}</Tag>
</Space>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={() => {
const newFileList = fileList.filter(item => item.uid !== file.uid);
setFileList(newFileList);
}}
/>
</div>
</Card>
))}
</div>
)}
{/* 翻译配置 */}
<Divider orientation="left"></Divider>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="sourceLanguage"
label="源语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择源语言' }]}
>
<Select placeholder="选择源语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="targetLanguage"
label="目标语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择目标语言' }]}
>
<Select placeholder="选择目标语言">
{languages.map(lang => (
<Option key={lang.code} value={lang.code}>
{lang.name}
</Option>
))}
</Select>
</Form.Item>
</div>
<Form.Item
name="quality"
label="翻译质量"
rules={[{ required: true, message: '请选择翻译质量' }]}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value="draft">
📝 稿 -
</Radio.Button>
<Radio.Button value="professional">
💼 -
</Radio.Button>
<Radio.Button value="certified">
🏆 -
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
name="urgency"
label="交付时间"
rules={[{ required: true, message: '请选择交付时间' }]}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value="normal">
🕐 -
</Radio.Button>
<Radio.Button value="urgent">
- 50%
</Radio.Button>
<Radio.Button value="express">
🚀 - 100%
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
name="notes"
label="特殊要求"
>
<Input.TextArea
rows={3}
placeholder="请输入特殊要求或备注信息,如专业术语、格式要求等..."
/>
</Form.Item>
{/* 费用和时间预估 */}
{estimatedCost > 0 && (
<div style={{
background: '#f6f6f6',
padding: '16px',
borderRadius: '6px',
marginBottom: '16px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<Space>
<DollarOutlined style={{ color: '#1890ff' }} />
<span><strong></strong></span>
<span style={{ fontSize: '18px', color: '#1890ff', fontWeight: 'bold' }}>
¥{estimatedCost.toFixed(2)}
</span>
</Space>
<Space>
<ClockCircleOutlined style={{ color: '#52c41a' }} />
<span><strong></strong></span>
<span style={{ fontSize: '16px', color: '#52c41a', fontWeight: 'bold' }}>
{estimatedTime.toFixed(1)}
</span>
</Space>
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
*
</div>
</div>
)}
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={handleCancel}>
</Button>
<Button
type="primary"
htmlType="submit"
loading={loading}
disabled={fileList.length === 0}
>
{uploading ? '上传中...' : '创建翻译任务'}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
};
export default NewDocumentModal;

View File

@ -0,0 +1,654 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Descriptions,
Button,
Tag,
Typography,
Space,
Modal,
Input,
message,
Spin,
Calendar,
Badge,
Avatar,
Timeline,
Tabs,
Form,
DatePicker,
Select,
Divider,
} from 'antd';
import {
ArrowLeftOutlined,
CalendarOutlined,
ClockCircleOutlined,
UserOutlined,
PhoneOutlined,
VideoCameraOutlined,
EditOutlined,
DeleteOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
MessageOutlined,
LinkOutlined,
DollarOutlined,
TranslationOutlined,
} from '@ant-design/icons';
import { Appointment } from '@/types';
import { database } from '@/utils/database';
import { api } from '@/utils/api';
import dayjs from 'dayjs';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
const { TabPane } = Tabs;
const { Option } = Select;
interface AppointmentDetailProps {}
const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [appointment, setAppointment] = useState<Appointment | null>(null);
const [loading, setLoading] = useState(true);
const [editModalVisible, setEditModalVisible] = useState(false);
const [cancelModalVisible, setCancelModalVisible] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
if (id) {
loadAppointmentDetails();
}
}, [id]);
const loadAppointmentDetails = async () => {
try {
setLoading(true);
await database.connect();
// 模拟获取预约详情
const mockAppointment: Appointment = {
id: id!,
userId: 'user_1',
translatorId: 'translator_1',
title: '商务会议翻译',
description: '重要客户会议,需要专业的商务翻译服务,涉及合同条款和技术细节讨论。',
type: 'human',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
startTime: '2024-01-20T14:00:00Z',
endTime: '2024-01-20T16:00:00Z',
status: 'confirmed',
cost: 200.00,
meetingUrl: 'https://meet.example.com/room/abc123',
notes: '客户要求准时开始请提前5分钟进入会议室',
reminderSent: true,
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
};
setAppointment(mockAppointment);
// 填充表单数据
form.setFieldsValue({
title: mockAppointment.title,
description: mockAppointment.description,
type: mockAppointment.type,
sourceLanguage: mockAppointment.sourceLanguage,
targetLanguage: mockAppointment.targetLanguage,
startTime: dayjs(mockAppointment.startTime),
endTime: dayjs(mockAppointment.endTime),
notes: mockAppointment.notes,
});
} catch (error) {
console.error('加载预约详情失败:', error);
message.error('加载预约详情失败');
} finally {
setLoading(false);
}
};
const handleEdit = async (values: any) => {
if (!appointment) return;
try {
const updatedAppointment = {
...appointment,
...values,
startTime: values.startTime.toISOString(),
endTime: values.endTime.toISOString(),
updatedAt: new Date().toISOString(),
};
setAppointment(updatedAppointment);
setEditModalVisible(false);
message.success('预约信息更新成功');
} catch (error) {
message.error('更新预约信息失败');
}
};
const handleCancel = async () => {
if (!appointment) return;
try {
const updatedAppointment = {
...appointment,
status: 'cancelled' as const,
updatedAt: new Date().toISOString(),
};
setAppointment(updatedAppointment);
setCancelModalVisible(false);
message.success('预约已取消');
} catch (error) {
message.error('取消预约失败');
}
};
const handleJoinMeeting = () => {
if (appointment?.meetingUrl) {
window.open(appointment.meetingUrl, '_blank');
} else {
message.warning('会议链接不可用');
}
};
const getStatusColor = (status: string) => {
const colors = {
scheduled: 'orange',
confirmed: 'blue',
cancelled: 'red',
completed: 'green',
};
return colors[status as keyof typeof colors] || 'default';
};
const getStatusText = (status: string) => {
const texts = {
scheduled: '已安排',
confirmed: '已确认',
cancelled: '已取消',
completed: '已完成',
};
return texts[status as keyof typeof texts] || status;
};
const getTypeIcon = (type: string) => {
const icons = {
ai: '🤖',
human: '👤',
video: '📹',
sign: '🤟',
};
return icons[type as keyof typeof icons] || '📞';
};
const getTypeText = (type: string) => {
const texts = {
ai: 'AI翻译',
human: '人工翻译',
video: '视频通话',
sign: '手语翻译',
};
return texts[type as keyof typeof texts] || type;
};
const formatDateTime = (dateTime: string) => {
return dayjs(dateTime).format('YYYY-MM-DD HH:mm');
};
const getDuration = () => {
if (!appointment) return '';
const start = dayjs(appointment.startTime);
const end = dayjs(appointment.endTime);
const duration = end.diff(start, 'minute');
const hours = Math.floor(duration / 60);
const minutes = duration % 60;
return hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;
};
const getTimelineData = () => {
if (!appointment) return [];
const timeline = [
{
color: 'green',
children: (
<div>
<div><strong></strong></div>
<div>{formatDateTime(appointment.createdAt)}</div>
</div>
),
},
];
if (appointment.status === 'confirmed') {
timeline.push({
color: 'blue',
children: (
<div>
<div><strong></strong></div>
<div>{formatDateTime(appointment.updatedAt)}</div>
</div>
),
});
}
if (appointment.reminderSent) {
timeline.push({
color: 'orange',
children: (
<div>
<div><strong></strong></div>
<div>24</div>
</div>
),
});
}
if (appointment.status === 'completed') {
timeline.push({
color: 'green',
children: (
<div>
<div><strong></strong></div>
<div>{formatDateTime(appointment.endTime)}</div>
</div>
),
});
}
if (appointment.status === 'cancelled') {
timeline.push({
color: 'red',
children: (
<div>
<div><strong></strong></div>
<div>{formatDateTime(appointment.updatedAt)}</div>
</div>
),
});
}
return timeline;
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
</div>
);
}
if (!appointment) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<div></div>
<Button type="primary" onClick={() => navigate('/appointments')} style={{ marginTop: '16px' }}>
</Button>
</div>
);
}
const isUpcoming = dayjs(appointment.startTime).isAfter(dayjs());
const canEdit = appointment.status !== 'cancelled' && appointment.status !== 'completed';
const canJoin = appointment.status === 'confirmed' && appointment.meetingUrl &&
dayjs().isAfter(dayjs(appointment.startTime).subtract(5, 'minute')) &&
dayjs().isBefore(dayjs(appointment.endTime));
return (
<div style={{ padding: '24px' }}>
{/* 头部导航 */}
<div style={{ marginBottom: '24px' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/appointments')}
style={{ marginRight: '16px' }}
>
</Button>
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
#{appointment.id}
</Title>
</div>
{/* 快速操作按钮 */}
<Card style={{ marginBottom: '24px' }}>
<Space>
{canJoin && (
<Button
type="primary"
size="large"
icon={<VideoCameraOutlined />}
onClick={handleJoinMeeting}
>
</Button>
)}
{canEdit && (
<Button
icon={<EditOutlined />}
onClick={() => setEditModalVisible(true)}
>
</Button>
)}
{canEdit && (
<Button
danger
icon={<DeleteOutlined />}
onClick={() => setCancelModalVisible(true)}
>
</Button>
)}
{appointment.meetingUrl && (
<Button
icon={<LinkOutlined />}
onClick={() => navigator.clipboard.writeText(appointment.meetingUrl!)}
>
</Button>
)}
</Space>
</Card>
{/* 基本信息卡片 */}
<Card title="预约信息" style={{ marginBottom: '24px' }}>
<Descriptions column={2} bordered>
<Descriptions.Item label="预约标题" span={2}>
<Text strong style={{ fontSize: '16px' }}>{appointment.title}</Text>
</Descriptions.Item>
<Descriptions.Item label="状态" span={1}>
<Tag color={getStatusColor(appointment.status)} icon={
appointment.status === 'confirmed' ? <CheckCircleOutlined /> :
appointment.status === 'cancelled' ? <ExclamationCircleOutlined /> :
<ClockCircleOutlined />
}>
{getStatusText(appointment.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="服务类型" span={1}>
<Space>
<span>{getTypeIcon(appointment.type)}</span>
{getTypeText(appointment.type)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="语言对" span={1}>
<Tag color="blue">{appointment.sourceLanguage}</Tag>
<span style={{ margin: '0 8px' }}></span>
<Tag color="green">{appointment.targetLanguage}</Tag>
</Descriptions.Item>
<Descriptions.Item label="费用" span={1}>
<Space>
<DollarOutlined />
<Text strong>¥{appointment.cost.toFixed(2)}</Text>
</Space>
</Descriptions.Item>
<Descriptions.Item label="开始时间" span={1}>
<Space>
<CalendarOutlined />
{formatDateTime(appointment.startTime)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="结束时间" span={1}>
<Space>
<CalendarOutlined />
{formatDateTime(appointment.endTime)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="持续时间" span={1}>
<Space>
<ClockCircleOutlined />
{getDuration()}
</Space>
</Descriptions.Item>
<Descriptions.Item label="译员" span={1}>
<Space>
<Avatar size="small" icon={<UserOutlined />} />
{appointment.translatorId || '待分配'}
</Space>
</Descriptions.Item>
{appointment.meetingUrl && (
<Descriptions.Item label="会议链接" span={2}>
<Space>
<LinkOutlined />
<a href={appointment.meetingUrl} target="_blank" rel="noopener noreferrer">
{appointment.meetingUrl}
</a>
</Space>
</Descriptions.Item>
)}
</Descriptions>
{appointment.description && (
<div style={{ marginTop: '16px' }}>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px' }}>
{appointment.description}
</Paragraph>
</div>
)}
{appointment.notes && (
<div style={{ marginTop: '16px' }}>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px' }}>
{appointment.notes}
</Paragraph>
</div>
)}
</Card>
{/* 详细信息标签页 */}
<Card>
<Tabs defaultActiveKey="timeline">
<TabPane
tab={
<Space>
<ClockCircleOutlined />
线
</Space>
}
key="timeline"
>
<div style={{ padding: '20px' }}>
<Timeline items={getTimelineData()} />
</div>
</TabPane>
<TabPane
tab={
<Space>
<MessageOutlined />
</Space>
}
key="communication"
>
<div style={{ padding: '20px', textAlign: 'center', color: '#999' }}>
</div>
</TabPane>
<TabPane
tab={
<Space>
<TranslationOutlined />
</Space>
}
key="service"
>
<div style={{ padding: '20px' }}>
<Descriptions column={1}>
<Descriptions.Item label="服务时长">
{getDuration()}
</Descriptions.Item>
<Descriptions.Item label="服务费用">
¥{appointment.cost.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="付费状态">
<Tag color="green"></Tag>
</Descriptions.Item>
<Descriptions.Item label="提醒设置">
{appointment.reminderSent ? (
<Tag color="green"></Tag>
) : (
<Tag color="orange"></Tag>
)}
</Descriptions.Item>
</Descriptions>
</div>
</TabPane>
</Tabs>
</Card>
{/* 编辑预约弹窗 */}
<Modal
title="编辑预约"
visible={editModalVisible}
onCancel={() => setEditModalVisible(false)}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleEdit}
>
<Form.Item
name="title"
label="预约标题"
rules={[{ required: true, message: '请输入预约标题' }]}
>
<Input placeholder="请输入预约标题" />
</Form.Item>
<Form.Item
name="description"
label="预约描述"
>
<TextArea rows={3} placeholder="请输入预约描述" />
</Form.Item>
<Form.Item
name="type"
label="服务类型"
rules={[{ required: true, message: '请选择服务类型' }]}
>
<Select placeholder="请选择服务类型">
<Option value="ai">AI翻译</Option>
<Option value="human"></Option>
<Option value="video"></Option>
<Option value="sign"></Option>
</Select>
</Form.Item>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="sourceLanguage"
label="源语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择源语言' }]}
>
<Select placeholder="源语言">
<Option value="zh-CN"></Option>
<Option value="en-US"></Option>
<Option value="ja-JP"></Option>
<Option value="ko-KR"></Option>
</Select>
</Form.Item>
<Form.Item
name="targetLanguage"
label="目标语言"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择目标语言' }]}
>
<Select placeholder="目标语言">
<Option value="zh-CN"></Option>
<Option value="en-US"></Option>
<Option value="ja-JP"></Option>
<Option value="ko-KR"></Option>
</Select>
</Form.Item>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<Form.Item
name="startTime"
label="开始时间"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择开始时间' }]}
>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm"
placeholder="选择开始时间"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="endTime"
label="结束时间"
style={{ flex: 1 }}
rules={[{ required: true, message: '请选择结束时间' }]}
>
<DatePicker
showTime
format="YYYY-MM-DD HH:mm"
placeholder="选择结束时间"
style={{ width: '100%' }}
/>
</Form.Item>
</div>
<Form.Item
name="notes"
label="备注信息"
>
<TextArea rows={2} placeholder="请输入备注信息" />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Space>
<Button onClick={() => setEditModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 取消预约确认弹窗 */}
<Modal
title="取消预约"
visible={cancelModalVisible}
onOk={handleCancel}
onCancel={() => setCancelModalVisible(false)}
okText="确认取消"
cancelText="保持预约"
okButtonProps={{ danger: true }}
>
<p></p>
<p></p>
</Modal>
</div>
);
};
export default AppointmentDetail;

View File

@ -0,0 +1,504 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Descriptions,
Button,
Tag,
Rate,
Typography,
Divider,
Space,
Modal,
Input,
message,
Spin,
Timeline,
Tabs,
Avatar,
Progress,
} from 'antd';
import {
ArrowLeftOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
DownloadOutlined,
StarOutlined,
PhoneOutlined,
ClockCircleOutlined,
DollarOutlined,
UserOutlined,
SoundOutlined,
FileTextOutlined,
TranslationOutlined,
} from '@ant-design/icons';
import { TranslationCall } from '@/types';
import { database } from '@/utils/database';
import { api } from '@/utils/api';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
const { TabPane } = Tabs;
interface CallDetailProps {}
const CallDetail: React.FC<CallDetailProps> = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [call, setCall] = useState<TranslationCall | null>(null);
const [loading, setLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [ratingModalVisible, setRatingModalVisible] = useState(false);
const [rating, setRating] = useState(0);
const [feedback, setFeedback] = useState('');
const [submittingRating, setSubmittingRating] = useState(false);
// 模拟音频播放状态
const [audioProgress, setAudioProgress] = useState(0);
useEffect(() => {
if (id) {
loadCallDetails();
}
}, [id]);
const loadCallDetails = async () => {
try {
setLoading(true);
await database.connect();
// 模拟获取通话详情
const mockCall: TranslationCall = {
id: id!,
userId: 'user_1',
callId: `CA${Date.now()}`,
clientName: '张先生',
clientPhone: '+86 138 0013 8000',
type: 'human',
status: 'completed',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
startTime: '2024-01-15T10:30:00Z',
endTime: '2024-01-15T10:45:00Z',
duration: 900,
cost: 45.00,
rating: 5,
feedback: '翻译非常专业,沟通顺畅,非常满意!',
translatorId: 'translator_1',
translatorName: '李翻译',
translatorPhone: '+86 138 0013 8001',
recordingUrl: '/recordings/call_123456.mp3',
transcription: '用户: 您好,我想了解一下贵公司的产品服务。\n翻译: Hello, I would like to learn about your company\'s products and services.\n客户: Thank you for your interest. Let me introduce our main products...\n翻译: 感谢您的关注。让我为您介绍我们的主要产品...',
translation: '这是一次关于产品咨询的商务通话,客户询问了公司的主要产品和服务,我们提供了详细的介绍和说明。',
};
setCall(mockCall);
setDuration(mockCall.duration || 0);
setRating(mockCall.rating || 0);
setFeedback(mockCall.feedback || '');
} catch (error) {
console.error('加载通话详情失败:', error);
message.error('加载通话详情失败');
} finally {
setLoading(false);
}
};
const handlePlayPause = () => {
setIsPlaying(!isPlaying);
if (!isPlaying) {
// 模拟音频播放
const interval = setInterval(() => {
setCurrentTime(prev => {
const newTime = prev + 1;
setAudioProgress((newTime / duration) * 100);
if (newTime >= duration) {
clearInterval(interval);
setIsPlaying(false);
setCurrentTime(0);
setAudioProgress(0);
}
return newTime;
});
}, 1000);
}
};
const handleDownloadRecording = async () => {
if (!call?.recordingUrl) return;
try {
message.info('开始下载录音文件...');
// 模拟下载
await new Promise(resolve => setTimeout(resolve, 2000));
message.success('录音文件下载完成');
} catch (error) {
message.error('下载录音文件失败');
}
};
const handleSubmitRating = async () => {
if (!call) return;
try {
setSubmittingRating(true);
await database.updateUser(call.userId, {
// 更新评分和反馈
});
setCall({
...call,
rating,
feedback,
});
setRatingModalVisible(false);
message.success('评价提交成功');
} catch (error) {
message.error('提交评价失败');
} finally {
setSubmittingRating(false);
}
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
active: 'blue',
completed: 'green',
cancelled: 'red',
};
return colors[status as keyof typeof colors] || 'default';
};
const getStatusText = (status: string) => {
const texts = {
pending: '等待中',
active: '通话中',
completed: '已完成',
cancelled: '已取消',
};
return texts[status as keyof typeof texts] || status;
};
const getTypeIcon = (type: string) => {
const icons = {
ai: '🤖',
human: '👤',
video: '📹',
sign: '🤟',
};
return icons[type as keyof typeof icons] || '📞';
};
const getTypeText = (type: string) => {
const texts = {
ai: 'AI翻译',
human: '人工翻译',
video: '视频通话',
sign: '手语翻译',
};
return texts[type as keyof typeof texts] || type;
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
</div>
);
}
if (!call) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<div></div>
<Button type="primary" onClick={() => navigate('/calls')} style={{ marginTop: '16px' }}>
</Button>
</div>
);
}
return (
<div style={{ padding: '24px' }}>
{/* 头部导航 */}
<div style={{ marginBottom: '24px' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/calls')}
style={{ marginRight: '16px' }}
>
</Button>
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
#{call.id}
</Title>
</div>
{/* 基本信息卡片 */}
<Card title="基本信息" style={{ marginBottom: '24px' }}>
<Descriptions column={2} bordered>
<Descriptions.Item label="通话ID" span={1}>
{call.callId}
</Descriptions.Item>
<Descriptions.Item label="状态" span={1}>
<Tag color={getStatusColor(call.status)}>
{getStatusText(call.status)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="类型" span={1}>
<Space>
<span>{getTypeIcon(call.type)}</span>
{getTypeText(call.type)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="语言对" span={1}>
<Tag color="blue">{call.sourceLanguage}</Tag>
<span style={{ margin: '0 8px' }}></span>
<Tag color="green">{call.targetLanguage}</Tag>
</Descriptions.Item>
<Descriptions.Item label="开始时间" span={1}>
<Space>
<ClockCircleOutlined />
{new Date(call.startTime).toLocaleString()}
</Space>
</Descriptions.Item>
<Descriptions.Item label="结束时间" span={1}>
<Space>
<ClockCircleOutlined />
{call.endTime ? new Date(call.endTime).toLocaleString() : '-'}
</Space>
</Descriptions.Item>
<Descriptions.Item label="通话时长" span={1}>
<Space>
<PhoneOutlined />
{formatTime(call.duration || 0)}
</Space>
</Descriptions.Item>
<Descriptions.Item label="费用" span={1}>
<Space>
<DollarOutlined />
<Text strong>¥{call.cost.toFixed(2)}</Text>
</Space>
</Descriptions.Item>
{call.clientName && (
<Descriptions.Item label="客户姓名" span={1}>
<Space>
<UserOutlined />
{call.clientName}
</Space>
</Descriptions.Item>
)}
{call.clientPhone && (
<Descriptions.Item label="客户电话" span={1}>
<Space>
<PhoneOutlined />
{call.clientPhone}
</Space>
</Descriptions.Item>
)}
{call.translatorName && (
<Descriptions.Item label="译员" span={1}>
<Space>
<Avatar size="small" icon={<UserOutlined />} />
{call.translatorName}
</Space>
</Descriptions.Item>
)}
{call.translatorPhone && (
<Descriptions.Item label="译员电话" span={1}>
<Space>
<PhoneOutlined />
{call.translatorPhone}
</Space>
</Descriptions.Item>
)}
</Descriptions>
</Card>
{/* 录音播放器 */}
{call.recordingUrl && (
<Card
title={
<Space>
<SoundOutlined />
</Space>
}
style={{ marginBottom: '24px' }}
>
<div style={{ textAlign: 'center', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<Button
type="primary"
size="large"
icon={isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={handlePlayPause}
style={{ marginRight: '16px' }}
>
{isPlaying ? '暂停' : '播放'}
</Button>
<Button
icon={<DownloadOutlined />}
onClick={handleDownloadRecording}
>
</Button>
</div>
<div style={{ margin: '20px 0' }}>
<Progress
percent={audioProgress}
showInfo={false}
strokeColor="#1890ff"
/>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
<Text type="secondary">{formatTime(currentTime)}</Text>
<Text type="secondary">{formatTime(duration)}</Text>
</div>
</div>
</div>
</Card>
)}
{/* 详细内容标签页 */}
<Card>
<Tabs defaultActiveKey="transcription">
<TabPane
tab={
<Space>
<FileTextOutlined />
</Space>
}
key="transcription"
>
<div style={{ minHeight: '200px' }}>
{call.transcription ? (
<Paragraph>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
{call.transcription}
</pre>
</Paragraph>
) : (
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
</div>
)}
</div>
</TabPane>
<TabPane
tab={
<Space>
<TranslationOutlined />
</Space>
}
key="translation"
>
<div style={{ minHeight: '200px' }}>
{call.translation ? (
<Paragraph>{call.translation}</Paragraph>
) : (
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
</div>
)}
</div>
</TabPane>
<TabPane
tab={
<Space>
<StarOutlined />
</Space>
}
key="rating"
>
<div style={{ minHeight: '200px', padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<Text strong></Text>
<Rate disabled value={call.rating} style={{ marginLeft: '8px' }} />
{call.rating && (
<Text style={{ marginLeft: '8px' }}>
({call.rating}/5 )
</Text>
)}
</div>
{call.feedback && (
<div>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px' }}>
{call.feedback}
</Paragraph>
</div>
)}
<div style={{ marginTop: '20px' }}>
<Button
type="primary"
icon={<StarOutlined />}
onClick={() => setRatingModalVisible(true)}
>
{call.rating ? '修改评价' : '添加评价'}
</Button>
</div>
</div>
</TabPane>
</Tabs>
</Card>
{/* 评价弹窗 */}
<Modal
title="服务评价"
visible={ratingModalVisible}
onOk={handleSubmitRating}
onCancel={() => setRatingModalVisible(false)}
confirmLoading={submittingRating}
okText="提交"
cancelText="取消"
>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<Rate
value={rating}
onChange={setRating}
style={{ marginLeft: '8px' }}
/>
</div>
<div>
<Text strong></Text>
<TextArea
value={feedback}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setFeedback(e.target.value)}
placeholder="请分享您对本次翻译服务的意见和建议..."
rows={4}
style={{ marginTop: '8px' }}
/>
</div>
</Modal>
</div>
);
};
export default CallDetail;

View File

@ -0,0 +1,525 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Descriptions,
Button,
Tag,
Typography,
Space,
Modal,
Input,
message,
Spin,
Progress,
Tabs,
Upload,
List,
Image,
Tooltip,
Steps,
} from 'antd';
import {
ArrowLeftOutlined,
DownloadOutlined,
FileTextOutlined,
EyeOutlined,
CloudDownloadOutlined,
FilePdfOutlined,
FileWordOutlined,
FileExcelOutlined,
FilePptOutlined,
FileImageOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
LoadingOutlined,
TranslationOutlined,
DollarOutlined,
CalendarOutlined,
} from '@ant-design/icons';
import { DocumentTranslation } from '@/types';
import { database } from '@/utils/database';
import { api } from '@/utils/api';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
const { TabPane } = Tabs;
const { Step } = Steps;
interface DocumentDetailProps {}
const DocumentDetail: React.FC<DocumentDetailProps> = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [document, setDocument] = useState<DocumentTranslation | null>(null);
const [loading, setLoading] = useState(true);
const [downloadModalVisible, setDownloadModalVisible] = useState(false);
const [previewModalVisible, setPreviewModalVisible] = useState(false);
const [previewContent, setPreviewContent] = useState('');
useEffect(() => {
if (id) {
loadDocumentDetails();
}
}, [id]);
const loadDocumentDetails = async () => {
try {
setLoading(true);
await database.connect();
// 模拟获取文档详情
const mockDocument: DocumentTranslation = {
id: id!,
userId: 'user_1',
fileName: '商务合同.pdf',
fileSize: 2048576,
fileType: 'pdf',
fileUrl: '/uploads/business_contract.pdf',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
status: 'completed',
progress: 100,
createdAt: '2024-01-15T09:00:00Z',
updatedAt: '2024-01-15T09:30:00Z',
completedAt: '2024-01-15T09:30:00Z',
cost: 25.50,
translatedFileUrl: '/downloads/business_contract_en.pdf',
wordCount: 1250,
pageCount: 5,
translatorId: 'translator_2',
quality: 'professional',
};
setDocument(mockDocument);
} catch (error) {
console.error('加载文档详情失败:', error);
message.error('加载文档详情失败');
} finally {
setLoading(false);
}
};
const handleDownload = async (type: 'original' | 'translated') => {
if (!document) return;
try {
message.info('开始下载文件...');
// 模拟下载
await new Promise(resolve => setTimeout(resolve, 2000));
const fileName = type === 'original'
? document.fileName
: document.translatedFileUrl?.split('/').pop() || 'translated_file';
message.success(`${fileName} 下载完成`);
} catch (error) {
message.error('下载文件失败');
}
};
const handlePreview = async () => {
if (!document?.fileUrl) {
message.warning('暂无预览内容');
return;
}
try {
setPreviewContent('文档预览内容加载中...');
setPreviewModalVisible(true);
// 模拟加载预览内容
await new Promise(resolve => setTimeout(resolve, 1000));
setPreviewContent(`
ABC公司
XYZ企业
...
20241120241231...
50...
[]
`);
} catch (error) {
message.error('加载预览失败');
}
};
const getFileIcon = (fileType: string) => {
const icons = {
pdf: <FilePdfOutlined style={{ color: '#ff4d4f' }} />,
doc: <FileWordOutlined style={{ color: '#1890ff' }} />,
docx: <FileWordOutlined style={{ color: '#1890ff' }} />,
xls: <FileExcelOutlined style={{ color: '#52c41a' }} />,
xlsx: <FileExcelOutlined style={{ color: '#52c41a' }} />,
ppt: <FilePptOutlined style={{ color: '#fa8c16' }} />,
pptx: <FilePptOutlined style={{ color: '#fa8c16' }} />,
jpg: <FileImageOutlined style={{ color: '#722ed1' }} />,
jpeg: <FileImageOutlined style={{ color: '#722ed1' }} />,
png: <FileImageOutlined style={{ color: '#722ed1' }} />,
};
return icons[fileType as keyof typeof icons] || <FileTextOutlined />;
};
const getStatusColor = (status: string) => {
const colors = {
pending: 'orange',
processing: 'blue',
completed: 'green',
failed: 'red',
cancelled: 'default',
};
return colors[status as keyof typeof colors] || 'default';
};
const getStatusText = (status: string) => {
const texts = {
pending: '等待处理',
processing: '翻译中',
completed: '已完成',
failed: '翻译失败',
cancelled: '已取消',
};
return texts[status as keyof typeof texts] || status;
};
const getStatusIcon = (status: string) => {
const icons = {
pending: <ClockCircleOutlined />,
processing: <LoadingOutlined spin />,
completed: <CheckCircleOutlined />,
failed: <ExclamationCircleOutlined />,
cancelled: <ExclamationCircleOutlined />,
};
return icons[status as keyof typeof icons] || <ClockCircleOutlined />;
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', '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 getTranslationSteps = () => {
if (!document) return [];
const steps = [
{
title: '文件上传',
status: 'finish',
icon: <CheckCircleOutlined />,
description: new Date(document.createdAt).toLocaleString(),
},
{
title: '文档分析',
status: document.status === 'pending' ? 'wait' : 'finish',
icon: document.status === 'pending' ? <ClockCircleOutlined /> : <CheckCircleOutlined />,
description: '分析文档结构和内容',
},
{
title: '翻译处理',
status: document.status === 'processing' ? 'process' :
document.status === 'completed' ? 'finish' : 'wait',
icon: document.status === 'processing' ? <LoadingOutlined spin /> :
document.status === 'completed' ? <CheckCircleOutlined /> : <ClockCircleOutlined />,
description: document.translatorId ? `译员:${document.translatorId}` : '等待分配译员',
},
{
title: '质量审核',
status: document.status === 'completed' ? 'finish' : 'wait',
icon: document.status === 'completed' ? <CheckCircleOutlined /> : <ClockCircleOutlined />,
description: document.status === 'completed' ? '审核完成' : '等待审核',
},
{
title: '完成交付',
status: document.status === 'completed' ? 'finish' : 'wait',
icon: document.status === 'completed' ? <CheckCircleOutlined /> : <ClockCircleOutlined />,
description: document.completedAt ? new Date(document.completedAt).toLocaleString() : '等待完成',
},
];
return steps;
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Spin size="large" />
<div style={{ marginTop: '16px' }}>...</div>
</div>
);
}
if (!document) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<div></div>
<Button type="primary" onClick={() => navigate('/documents')} style={{ marginTop: '16px' }}>
</Button>
</div>
);
}
return (
<div style={{ padding: '24px' }}>
{/* 头部导航 */}
<div style={{ marginBottom: '24px' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/documents')}
style={{ marginRight: '16px' }}
>
</Button>
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
#{document.id}
</Title>
</div>
{/* 基本信息卡片 */}
<Card title="基本信息" style={{ marginBottom: '24px' }}>
<Descriptions column={2} bordered>
<Descriptions.Item label="文件名" span={1}>
<Space>
{getFileIcon(document.fileType)}
{document.fileName}
</Space>
</Descriptions.Item>
<Descriptions.Item label="状态" span={1}>
<Space>
{getStatusIcon(document.status)}
<Tag color={getStatusColor(document.status)}>
{getStatusText(document.status)}
</Tag>
</Space>
</Descriptions.Item>
<Descriptions.Item label="文件大小" span={1}>
{formatFileSize(document.fileSize)}
</Descriptions.Item>
<Descriptions.Item label="文件类型" span={1}>
<Tag>{document.fileType.toUpperCase()}</Tag>
</Descriptions.Item>
<Descriptions.Item label="语言对" span={1}>
<Tag color="blue">{document.sourceLanguage}</Tag>
<span style={{ margin: '0 8px' }}></span>
<Tag color="green">{document.targetLanguage}</Tag>
</Descriptions.Item>
<Descriptions.Item label="翻译类型" span={1}>
<Tag color="purple"></Tag>
</Descriptions.Item>
<Descriptions.Item label="上传时间" span={1}>
<Space>
<CalendarOutlined />
{new Date(document.createdAt).toLocaleString()}
</Space>
</Descriptions.Item>
<Descriptions.Item label="完成时间" span={1}>
<Space>
<CalendarOutlined />
{document.completedAt ? new Date(document.completedAt).toLocaleString() : '-'}
</Space>
</Descriptions.Item>
<Descriptions.Item label="字数统计" span={1}>
{document.wordCount ? `${document.wordCount.toLocaleString()}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="页数" span={1}>
{document.pageCount ? `${document.pageCount}` : '-'}
</Descriptions.Item>
<Descriptions.Item label="费用" span={1}>
<Space>
<DollarOutlined />
<Text strong>¥{document.cost.toFixed(2)}</Text>
</Space>
</Descriptions.Item>
<Descriptions.Item label="质量等级" span={1}>
<Tag color="gold">{document.quality}</Tag>
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: '16px' }}>
<Text strong></Text>
<Paragraph style={{ marginTop: '8px' }}>
</Paragraph>
</div>
</Card>
{/* 翻译进度 */}
<Card title="翻译进度" style={{ marginBottom: '24px' }}>
<div style={{ marginBottom: '20px' }}>
<Progress
percent={document.progress}
status={document.status === 'failed' ? 'exception' : 'normal'}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
/>
</div>
<Steps current={getTranslationSteps().findIndex(step => step.status === 'process')}>
{getTranslationSteps().map((step, index) => (
<Step
key={index}
title={step.title}
description={step.description}
status={step.status as any}
icon={step.icon}
/>
))}
</Steps>
</Card>
{/* 操作和预览 */}
<Card title="文件操作">
<Tabs defaultActiveKey="actions">
<TabPane
tab={
<Space>
<DownloadOutlined />
</Space>
}
key="actions"
>
<div style={{ padding: '20px' }}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div>
<Title level={4}></Title>
<Space>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => handleDownload('original')}
>
</Button>
<Button
icon={<EyeOutlined />}
onClick={handlePreview}
>
</Button>
</Space>
</div>
{document.status === 'completed' && document.translatedFileUrl && (
<div>
<Title level={4}></Title>
<Space>
<Button
type="primary"
icon={<CloudDownloadOutlined />}
onClick={() => handleDownload('translated')}
>
</Button>
<Button
icon={<EyeOutlined />}
onClick={() => message.info('译文预览功能开发中')}
>
</Button>
</Space>
</div>
)}
</Space>
</div>
</TabPane>
<TabPane
tab={
<Space>
<TranslationOutlined />
</Space>
}
key="details"
>
<div style={{ padding: '20px' }}>
<List
itemLayout="horizontal"
dataSource={[
{
title: '译员信息',
content: document.translatorId || '未分配',
icon: <TranslationOutlined />,
},
{
title: '审核员',
content: '系统审核',
icon: <CheckCircleOutlined />,
},
{
title: '紧急程度',
content: '普通',
icon: <ClockCircleOutlined />,
},
{
title: '质量要求',
content: document.quality === 'professional' ? '专业级' :
document.quality === 'certified' ? '认证级' : '草稿级',
icon: <CheckCircleOutlined />,
},
]}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={item.icon}
title={item.title}
description={item.content}
/>
</List.Item>
)}
/>
</div>
</TabPane>
</Tabs>
</Card>
{/* 文件预览弹窗 */}
<Modal
title="文件预览"
visible={previewModalVisible}
onCancel={() => setPreviewModalVisible(false)}
footer={[
<Button key="close" onClick={() => setPreviewModalVisible(false)}>
</Button>,
<Button
key="download"
type="primary"
icon={<DownloadOutlined />}
onClick={() => handleDownload('original')}
>
</Button>,
]}
width={800}
>
<div style={{ maxHeight: '500px', overflow: 'auto' }}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
{previewContent}
</pre>
</div>
</Modal>
</div>
);
};
export default DocumentDetail;

498
src/utils/api.ts Normal file
View File

@ -0,0 +1,498 @@
import { ApiResponse } from '@/types';
// API配置
interface ApiConfig {
baseUrl: string;
timeout: number;
retries: number;
}
// Twilio配置
interface TwilioConfig {
accountSid: string;
authToken: string;
phoneNumber: string;
}
// Stripe配置
interface StripeConfig {
publishableKey: string;
secretKey: string;
}
// OpenAI配置
interface OpenAIConfig {
apiKey: string;
model: string;
}
class ApiManager {
private config: ApiConfig;
private twilioConfig?: TwilioConfig;
private stripeConfig?: StripeConfig;
private openaiConfig?: OpenAIConfig;
constructor(config: ApiConfig) {
this.config = config;
}
// 配置第三方服务
configureTwilio(config: TwilioConfig) {
this.twilioConfig = config;
}
configureStripe(config: StripeConfig) {
this.stripeConfig = config;
}
configureOpenAI(config: OpenAIConfig) {
this.openaiConfig = config;
}
// 通用HTTP请求方法
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.config.baseUrl}${endpoint}`;
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
try {
const response = await fetch(url, {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
signal: AbortSignal.timeout(this.config.timeout),
});
const data = await response.json();
if (!response.ok) {
return {
success: false,
error: data.message || `HTTP ${response.status}: ${response.statusText}`,
};
}
return {
success: true,
data,
};
} catch (error) {
console.error('API请求失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '网络请求失败',
};
}
}
// GET请求
async get<T>(endpoint: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
const url = new URL(`${this.config.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
return this.request<T>(url.pathname + url.search);
}
// POST请求
async post<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
// PUT请求
async put<T>(endpoint: string, data?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
// DELETE请求
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, {
method: 'DELETE',
});
}
// Twilio相关API
async initiateCall(callData: {
from: string;
to: string;
sourceLanguage: string;
targetLanguage: string;
type: 'ai' | 'human' | 'video' | 'sign';
}): Promise<ApiResponse<{ callSid: string; status: string }>> {
if (!this.twilioConfig) {
return {
success: false,
error: 'Twilio配置未初始化',
};
}
// 模拟Twilio API调用
console.log('发起Twilio通话:', callData);
// 模拟API响应
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
data: {
callSid: `CA${Date.now().toString()}`,
status: 'initiated',
},
};
}
async endCall(callSid: string): Promise<ApiResponse<{ status: string }>> {
if (!this.twilioConfig) {
return {
success: false,
error: 'Twilio配置未初始化',
};
}
console.log('结束Twilio通话:', callSid);
await new Promise(resolve => setTimeout(resolve, 500));
return {
success: true,
data: {
status: 'completed',
},
};
}
async getCallStatus(callSid: string): Promise<ApiResponse<{
status: string;
duration: number;
startTime: string;
endTime?: string;
}>> {
if (!this.twilioConfig) {
return {
success: false,
error: 'Twilio配置未初始化',
};
}
console.log('查询通话状态:', callSid);
await new Promise(resolve => setTimeout(resolve, 300));
return {
success: true,
data: {
status: 'completed',
duration: 900,
startTime: '2024-01-15T10:30:00Z',
endTime: '2024-01-15T10:45:00Z',
},
};
}
// Stripe相关API
async createPaymentIntent(paymentData: {
amount: number;
currency: string;
customerId?: string;
description?: string;
}): Promise<ApiResponse<{ clientSecret: string; paymentIntentId: string }>> {
if (!this.stripeConfig) {
return {
success: false,
error: 'Stripe配置未初始化',
};
}
console.log('创建Stripe支付意向:', paymentData);
await new Promise(resolve => setTimeout(resolve, 800));
return {
success: true,
data: {
clientSecret: `pi_${Date.now()}_secret_${Math.random().toString(36).substr(2, 9)}`,
paymentIntentId: `pi_${Date.now()}`,
},
};
}
async confirmPayment(paymentIntentId: string): Promise<ApiResponse<{
status: string;
amount: number;
currency: string;
}>> {
if (!this.stripeConfig) {
return {
success: false,
error: 'Stripe配置未初始化',
};
}
console.log('确认Stripe支付:', paymentIntentId);
await new Promise(resolve => setTimeout(resolve, 1200));
return {
success: true,
data: {
status: 'succeeded',
amount: 2500,
currency: 'cny',
},
};
}
async refundPayment(paymentIntentId: string, amount?: number): Promise<ApiResponse<{
refundId: string;
status: string;
amount: number;
}>> {
if (!this.stripeConfig) {
return {
success: false,
error: 'Stripe配置未初始化',
};
}
console.log('Stripe退款:', { paymentIntentId, amount });
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
data: {
refundId: `re_${Date.now()}`,
status: 'succeeded',
amount: amount || 2500,
},
};
}
// OpenAI相关API
async translateText(textData: {
text: string;
sourceLanguage: string;
targetLanguage: string;
context?: string;
}): Promise<ApiResponse<{ translatedText: string; confidence: number }>> {
if (!this.openaiConfig) {
return {
success: false,
error: 'OpenAI配置未初始化',
};
}
console.log('OpenAI文本翻译:', textData);
await new Promise(resolve => setTimeout(resolve, 1500));
// 模拟翻译结果
const translations: Record<string, string> = {
'zh-CN_en-US': 'Hello, this is a translated text.',
'en-US_zh-CN': '您好,这是翻译后的文本。',
'zh-CN_ja-JP': 'こんにちは、これは翻訳されたテキストです。',
};
const key = `${textData.sourceLanguage}_${textData.targetLanguage}`;
const translatedText = translations[key] || 'Translation completed.';
return {
success: true,
data: {
translatedText,
confidence: 0.95,
},
};
}
async transcribeAudio(audioData: {
audioUrl: string;
language: string;
}): Promise<ApiResponse<{ transcription: string; confidence: number }>> {
if (!this.openaiConfig) {
return {
success: false,
error: 'OpenAI配置未初始化',
};
}
console.log('OpenAI音频转录:', audioData);
await new Promise(resolve => setTimeout(resolve, 2000));
return {
success: true,
data: {
transcription: '这是转录的音频内容,包含了用户的语音信息。',
confidence: 0.92,
},
};
}
async translateDocument(documentData: {
fileUrl: string;
fileName: string;
sourceLanguage: string;
targetLanguage: string;
quality: 'draft' | 'professional' | 'certified';
}): Promise<ApiResponse<{ taskId: string; estimatedTime: number }>> {
if (!this.openaiConfig) {
return {
success: false,
error: 'OpenAI配置未初始化',
};
}
console.log('OpenAI文档翻译:', documentData);
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
data: {
taskId: `task_${Date.now()}`,
estimatedTime: 1800, // 30分钟
},
};
}
async getTranslationProgress(taskId: string): Promise<ApiResponse<{
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
translatedFileUrl?: string;
}>> {
if (!this.openaiConfig) {
return {
success: false,
error: 'OpenAI配置未初始化',
};
}
console.log('查询翻译进度:', taskId);
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟进度查询
const progress = Math.floor(Math.random() * 100);
const status = progress >= 100 ? 'completed' : 'processing';
return {
success: true,
data: {
status,
progress,
translatedFileUrl: status === 'completed' ? `/downloads/translated_${taskId}.pdf` : undefined,
},
};
}
// 文件上传
async uploadFile(file: File, type: 'document' | 'audio' | 'video'): Promise<ApiResponse<{
fileUrl: string;
fileName: string;
fileSize: number;
uploadId: string;
}>> {
console.log('上传文件:', { name: file.name, size: file.size, type });
// 模拟文件上传
await new Promise(resolve => setTimeout(resolve, 2000));
return {
success: true,
data: {
fileUrl: `/uploads/${type}/${Date.now()}_${file.name}`,
fileName: file.name,
fileSize: file.size,
uploadId: `upload_${Date.now()}`,
},
};
}
// 重试机制
async retryRequest<T>(
requestFn: () => Promise<ApiResponse<T>>,
maxRetries: number = this.config.retries
): Promise<ApiResponse<T>> {
let lastError: string = '';
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await requestFn();
if (result.success) {
return result;
}
lastError = result.error || '未知错误';
} catch (error) {
lastError = error instanceof Error ? error.message : '请求失败';
}
if (attempt < maxRetries) {
// 指数退避策略
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
console.log(`重试请求 (${attempt}/${maxRetries})...`);
}
}
return {
success: false,
error: `请求失败,已重试${maxRetries}次: ${lastError}`,
};
}
}
// 创建API实例
const apiConfig: ApiConfig = {
baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001/api',
timeout: 30000,
retries: 3,
};
export const api = new ApiManager(apiConfig);
// 初始化第三方服务配置
if (process.env.REACT_APP_TWILIO_ACCOUNT_SID) {
api.configureTwilio({
accountSid: process.env.REACT_APP_TWILIO_ACCOUNT_SID,
authToken: process.env.REACT_APP_TWILIO_AUTH_TOKEN || '',
phoneNumber: process.env.REACT_APP_TWILIO_PHONE_NUMBER || '',
});
}
if (process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY) {
api.configureStripe({
publishableKey: process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY,
secretKey: process.env.REACT_APP_STRIPE_SECRET_KEY || '',
});
}
if (process.env.REACT_APP_OPENAI_API_KEY) {
api.configureOpenAI({
apiKey: process.env.REACT_APP_OPENAI_API_KEY,
model: process.env.REACT_APP_OPENAI_MODEL || 'gpt-3.5-turbo',
});
}
// 导出类型和实例
export type { ApiConfig, TwilioConfig, StripeConfig, OpenAIConfig };
export { ApiManager };

358
src/utils/database.ts Normal file
View File

@ -0,0 +1,358 @@
import { User, TranslationCall, DocumentTranslation, Appointment, Language } from '@/types';
// 模拟数据库连接配置
interface DatabaseConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
}
class DatabaseManager {
private config: DatabaseConfig;
private isConnected: boolean = false;
constructor(config: DatabaseConfig) {
this.config = config;
}
// 连接数据库
async connect(): Promise<boolean> {
try {
// 模拟数据库连接
console.log('正在连接数据库...');
await new Promise(resolve => setTimeout(resolve, 1000));
this.isConnected = true;
console.log('数据库连接成功');
return true;
} catch (error) {
console.error('数据库连接失败:', error);
return false;
}
}
// 断开数据库连接
async disconnect(): Promise<void> {
this.isConnected = false;
console.log('数据库连接已断开');
}
// 检查连接状态
isDbConnected(): boolean {
return this.isConnected;
}
// 用户相关操作
async getUser(userId: string): Promise<User | null> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
// 模拟查询用户数据
const mockUser: User = {
id: userId,
name: '张三',
email: 'zhangsan@example.com',
phone: '+86 138 0013 8000',
avatar: '',
role: 'user',
status: 'active',
createdAt: '2023-01-15T00:00:00Z',
updatedAt: '2024-01-15T00:00:00Z',
preferences: {
language: 'zh-CN',
timezone: 'Asia/Shanghai',
notifications: {
email: true,
sms: true,
push: true,
},
theme: 'light',
},
subscription: {
id: 'sub_1',
userId,
plan: 'premium',
status: 'active',
startDate: '2023-01-15T00:00:00Z',
endDate: '2024-01-15T00:00:00Z',
features: ['ai_translation', 'human_translation', 'document_translation'],
},
};
return mockUser;
}
async updateUser(userId: string, userData: Partial<User>): Promise<boolean> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
// 模拟更新用户数据
console.log(`更新用户 ${userId} 的数据:`, userData);
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
// 通话记录相关操作
async getCallRecords(userId: string, limit: number = 20): Promise<TranslationCall[]> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
// 模拟查询通话记录
const mockCallRecords: TranslationCall[] = [
{
id: '1',
userId,
type: 'ai',
status: 'completed',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
startTime: '2024-01-15T10:30:00Z',
endTime: '2024-01-15T10:45:00Z',
duration: 900,
cost: 12.50,
rating: 5,
translatorName: 'AI翻译助手',
},
{
id: '2',
userId,
type: 'human',
status: 'completed',
sourceLanguage: 'en-US',
targetLanguage: 'ja-JP',
startTime: '2024-01-14T14:20:00Z',
endTime: '2024-01-14T14:50:00Z',
duration: 1800,
cost: 45.00,
rating: 5,
translatorName: '田中太郎',
},
];
return mockCallRecords;
}
async createCallRecord(callData: Omit<TranslationCall, 'id'>): Promise<string> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
const newId = Date.now().toString();
console.log('创建新的通话记录:', { id: newId, ...callData });
await new Promise(resolve => setTimeout(resolve, 500));
return newId;
}
// 文档翻译相关操作
async getDocuments(userId: string): Promise<DocumentTranslation[]> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
const mockDocuments: DocumentTranslation[] = [
{
id: '1',
userId,
fileName: '商业计划书.pdf',
fileSize: 2048576,
fileType: 'application/pdf',
fileUrl: '/uploads/business_plan.pdf',
translatedFileUrl: '/uploads/business_plan_en.pdf',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
status: 'completed',
progress: 100,
cost: 25.00,
pageCount: 15,
wordCount: 3500,
createdAt: '2024-01-15T09:00:00Z',
updatedAt: '2024-01-15T09:30:00Z',
completedAt: '2024-01-15T09:30:00Z',
quality: 'professional',
},
{
id: '2',
userId,
fileName: '技术文档.docx',
fileSize: 1536000,
fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
fileUrl: '/uploads/tech_doc.docx',
sourceLanguage: 'zh-CN',
targetLanguage: 'ja-JP',
status: 'processing',
progress: 65,
cost: 18.00,
pageCount: 8,
wordCount: 2100,
createdAt: '2024-01-15T11:00:00Z',
updatedAt: '2024-01-15T11:30:00Z',
quality: 'professional',
},
];
return mockDocuments;
}
async createDocument(documentData: Omit<DocumentTranslation, 'id'>): Promise<string> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
const newId = Date.now().toString();
console.log('创建新的文档翻译记录:', { id: newId, ...documentData });
await new Promise(resolve => setTimeout(resolve, 500));
return newId;
}
// 预约相关操作
async getAppointments(userId: string): Promise<Appointment[]> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
const mockAppointments: Appointment[] = [
{
id: '1',
userId,
title: '商务会议翻译',
description: '重要商务会议,需要专业翻译',
type: 'human',
sourceLanguage: 'zh-CN',
targetLanguage: 'en-US',
startTime: '2024-01-20T14:00:00Z',
endTime: '2024-01-20T16:00:00Z',
status: 'confirmed',
cost: 200.00,
reminderSent: false,
createdAt: '2024-01-15T09:00:00Z',
updatedAt: '2024-01-15T09:00:00Z',
},
{
id: '2',
userId,
title: '医疗咨询翻译',
description: '医疗咨询预约翻译服务',
type: 'video',
sourceLanguage: 'en-US',
targetLanguage: 'zh-CN',
startTime: '2024-01-22T10:30:00Z',
endTime: '2024-01-22T11:30:00Z',
status: 'scheduled',
cost: 150.00,
reminderSent: false,
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
},
];
return mockAppointments;
}
async createAppointment(appointmentData: Omit<Appointment, 'id'>): Promise<string> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
const newId = Date.now().toString();
console.log('创建新的预约记录:', { id: newId, ...appointmentData });
await new Promise(resolve => setTimeout(resolve, 500));
return newId;
}
async updateAppointment(appointmentId: string, appointmentData: Partial<Appointment>): Promise<boolean> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
console.log(`更新预约 ${appointmentId} 的数据:`, appointmentData);
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
async deleteAppointment(appointmentId: string): Promise<boolean> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
console.log(`删除预约 ${appointmentId}`);
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
// 语言相关操作
async getLanguages(): Promise<Language[]> {
const mockLanguages: Language[] = [
{
code: 'zh-CN',
name: 'Chinese (Simplified)',
level: 'native',
},
{
code: 'en-US',
name: 'English (US)',
level: 'fluent',
},
{
code: 'ja-JP',
name: 'Japanese',
level: 'fluent',
},
{
code: 'ko-KR',
name: 'Korean',
level: 'intermediate',
},
{
code: 'es-ES',
name: 'Spanish',
level: 'intermediate',
},
{
code: 'fr-FR',
name: 'French',
level: 'basic',
},
];
return mockLanguages;
}
// 统计数据
async getStatistics(userId: string): Promise<any> {
if (!this.isConnected) {
throw new Error('数据库未连接');
}
return {
totalCalls: 156,
totalMinutes: 2340,
totalDocuments: 23,
totalAppointments: 8,
monthlyStats: {
calls: 12,
documents: 5,
appointments: 3,
spending: 450.00,
},
};
}
}
// 创建数据库实例
const dbConfig: DatabaseConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'twilioapp',
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
};
export const database = new DatabaseManager(dbConfig);
// 导出类型
export type { DatabaseConfig };
export { DatabaseManager };