后台管理端调整
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",
|
"@ant-design/plots": "^2.5.0",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
"@tanstack/react-query": "^5.8.4",
|
"@tanstack/react-query": "^5.8.4",
|
||||||
|
"@types/moment": "^2.13.0",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"antd": "^5.12.5",
|
"antd": "^5.12.5",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@ -2238,6 +2240,15 @@
|
|||||||
"@types/unist": "*"
|
"@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": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@ -6342,6 +6353,14 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
|
@ -15,12 +15,14 @@
|
|||||||
"@ant-design/plots": "^2.5.0",
|
"@ant-design/plots": "^2.5.0",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
"@tanstack/react-query": "^5.8.4",
|
"@tanstack/react-query": "^5.8.4",
|
||||||
|
"@types/moment": "^2.13.0",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"antd": "^5.12.5",
|
"antd": "^5.12.5",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"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