后台管理端调整
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user