654 lines
19 KiB
TypeScript
654 lines
19 KiB
TypeScript
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;
|