Files
Twilioapp/src/pages/Appointments/AppointmentDetail.tsx
T
2025-06-28 14:20:17 +08:00

654 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;