后台管理端调整
This commit is contained in:
parent
cf40d6adeb
commit
7fcff7759d
17369
Twilioapp-admin/package-lock.json
generated
Normal file
17369
Twilioapp-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
Twilioapp-admin/package.json
Normal file
49
Twilioapp-admin/package.json
Normal 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"
|
||||
}
|
||||
}
|
20
Twilioapp-admin/public/index.html
Normal file
20
Twilioapp-admin/public/index.html
Normal 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
231
Twilioapp-admin/src/App.css
Normal 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
140
Twilioapp-admin/src/App.tsx
Normal 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;
|
21
Twilioapp-admin/src/index.css
Normal file
21
Twilioapp-admin/src/index.css
Normal 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%;
|
||||
}
|
15
Twilioapp-admin/src/index.tsx
Normal file
15
Twilioapp-admin/src/index.tsx
Normal 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>
|
||||
);
|
1123
Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx
Normal file
1123
Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx
Normal file
File diff suppressed because it is too large
Load Diff
805
Twilioapp-admin/src/pages/Calls/CallDetail.tsx
Normal file
805
Twilioapp-admin/src/pages/Calls/CallDetail.tsx
Normal 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;
|
72
Twilioapp-admin/src/pages/Dashboard.tsx
Normal file
72
Twilioapp-admin/src/pages/Dashboard.tsx
Normal 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;
|
1052
Twilioapp-admin/src/pages/Documents/DocumentDetail.tsx
Normal file
1052
Twilioapp-admin/src/pages/Documents/DocumentDetail.tsx
Normal file
File diff suppressed because it is too large
Load Diff
156
Twilioapp-admin/src/types/index.ts
Normal file
156
Twilioapp-admin/src/types/index.ts
Normal 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;
|
||||
}
|
220
Twilioapp-admin/src/utils/api.ts
Normal file
220
Twilioapp-admin/src/utils/api.ts
Normal 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;
|
290
Twilioapp-admin/src/utils/database.ts
Normal file
290
Twilioapp-admin/src/utils/database.ts
Normal 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;
|
26
Twilioapp-admin/tsconfig.json
Normal file
26
Twilioapp-admin/tsconfig.json
Normal 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
19
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
530
src/components/Forms/NewAppointmentModal.tsx
Normal file
530
src/components/Forms/NewAppointmentModal.tsx
Normal 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;
|
326
src/components/Forms/NewCallModal.tsx
Normal file
326
src/components/Forms/NewCallModal.tsx
Normal 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;
|
442
src/components/Forms/NewDocumentModal.tsx
Normal file
442
src/components/Forms/NewDocumentModal.tsx
Normal 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">
|
||||
支持 PDF、Word、PowerPoint、文本文件,单个文件不超过 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;
|
654
src/pages/Appointments/AppointmentDetail.tsx
Normal file
654
src/pages/Appointments/AppointmentDetail.tsx
Normal 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;
|
504
src/pages/Calls/CallDetail.tsx
Normal file
504
src/pages/Calls/CallDetail.tsx
Normal 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;
|
525
src/pages/Documents/DocumentDetail.tsx
Normal file
525
src/pages/Documents/DocumentDetail.tsx
Normal 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企业
|
||||
|
||||
第一条 合作内容
|
||||
双方就以下事项达成合作协议...
|
||||
|
||||
第二条 合作期限
|
||||
本合同有效期自2024年1月1日至2024年12月31日...
|
||||
|
||||
第三条 费用条款
|
||||
合作费用总计人民币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
498
src/utils/api.ts
Normal 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
358
src/utils/database.ts
Normal 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 };
|
Loading…
x
Reference in New Issue
Block a user