后台管理端调整

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