|
|
@@ -0,0 +1,342 @@
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
+import { useParams, useNavigate } from 'react-router-dom';
|
|
|
+import Card from 'antd/es/card';
|
|
|
+import Form from 'antd/es/form';
|
|
|
+import Input from 'antd/es/input';
|
|
|
+import Select from 'antd/es/select';
|
|
|
+import Button from 'antd/es/button';
|
|
|
+import DatePicker from 'antd/es/date-picker';
|
|
|
+import Space from 'antd/es/space';
|
|
|
+import Typography from 'antd/es/typography';
|
|
|
+import message from 'antd/es/message';
|
|
|
+import TextArea from 'antd/es/input/TextArea';
|
|
|
+import Spin from 'antd/es/spin';
|
|
|
+import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
|
|
|
+import { z } from 'zod';
|
|
|
+import { ticketsClient } from '@/client/api';
|
|
|
+import type { InferRequestType, InferResponseType } from 'hono/client';
|
|
|
+import { logger } from '@/client/utils/logger';
|
|
|
+
|
|
|
+const { Title } = Typography;
|
|
|
+const { Option } = Select;
|
|
|
+
|
|
|
+// 表单验证Schema
|
|
|
+const EditTicketSchema = z.object({
|
|
|
+ title: z.string().min(3, '标题至少3个字符').max(100, '标题最多100个字符'),
|
|
|
+ customerId: z.number().int().positive('请选择客户'),
|
|
|
+ contactId: z.number().int().positive().optional(),
|
|
|
+ type: z.string().min(1, '请选择工单类型'),
|
|
|
+ priority: z.string().min(1, '请选择优先级'),
|
|
|
+ description: z.string().min(10, '描述至少10个字符').max(2000, '描述最多2000个字符'),
|
|
|
+ dueDate: z.date().optional(),
|
|
|
+ assigneeId: z.number().int().positive().optional(),
|
|
|
+ status: z.string().min(1, '请选择工单状态'),
|
|
|
+});
|
|
|
+
|
|
|
+// 定义请求和响应类型
|
|
|
+type UpdateTicketRequest = InferRequestType<typeof ticketsClient['$put']>['json'];
|
|
|
+type TicketDetailResponse = InferResponseType<typeof ticketsClient['$get'], 200>;
|
|
|
+
|
|
|
+const EditTicketPage = () => {
|
|
|
+ const { id } = useParams<{ id: string }>();
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const [loading, setLoading] = useState<boolean>(false);
|
|
|
+ const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
|
|
+ const [form] = Form.useForm<{
|
|
|
+ title: string;
|
|
|
+ customerId: string;
|
|
|
+ contactId?: string;
|
|
|
+ type: string;
|
|
|
+ priority: string;
|
|
|
+ description: string;
|
|
|
+ dueDate?: Date;
|
|
|
+ assigneeId?: string;
|
|
|
+ status: string;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ // 模拟客户数据 - 实际项目中应从API获取
|
|
|
+ const customers = [
|
|
|
+ { id: 1, name: 'ABC公司' },
|
|
|
+ { id: 2, name: 'XYZ企业' },
|
|
|
+ { id: 3, name: '123集团' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 模拟联系人数据 - 实际项目中应根据选择的客户动态获取
|
|
|
+ const contacts = [
|
|
|
+ { id: 1, name: '张三', customerId: 1 },
|
|
|
+ { id: 2, name: '李四', customerId: 1 },
|
|
|
+ { id: 3, name: '王五', customerId: 2 },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 模拟负责人数据 - 实际项目中应从API获取
|
|
|
+ const assignees = [
|
|
|
+ { id: 1, name: '技术支持-小明' },
|
|
|
+ { id: 2, name: '技术支持-小红' },
|
|
|
+ { id: 3, name: '技术支持-小刚' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 工单状态选项
|
|
|
+ const statusOptions = [
|
|
|
+ { value: 'new', label: '新建' },
|
|
|
+ { value: 'in_progress', label: '处理中' },
|
|
|
+ { value: 'pending', label: '待处理' },
|
|
|
+ { value: 'resolved', label: '已解决' },
|
|
|
+ { value: 'closed', label: '已关闭' },
|
|
|
+ { value: 'reopened', label: '已重新打开' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 获取工单详情数据
|
|
|
+ const fetchTicketDetail = async () => {
|
|
|
+ if (!id) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ setInitialLoading(true);
|
|
|
+ const response = await ticketsClient.$get({
|
|
|
+ param: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch ticket details');
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json() as TicketDetailResponse;
|
|
|
+
|
|
|
+ // 填充表单数据
|
|
|
+ form.setFieldsValue({
|
|
|
+ title: data.title,
|
|
|
+ customerId: data.customerId.toString(),
|
|
|
+ contactId: data.contactId?.toString(),
|
|
|
+ type: data.type,
|
|
|
+ priority: data.priority,
|
|
|
+ description: data.description,
|
|
|
+ status: data.status,
|
|
|
+ dueDate: data.dueDate ? new Date(data.dueDate) : undefined,
|
|
|
+ assigneeId: data.assigneeId?.toString(),
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Error fetching ticket details:', error);
|
|
|
+ message.error('加载工单详情失败');
|
|
|
+ } finally {
|
|
|
+ setInitialLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 初始加载
|
|
|
+ useEffect(() => {
|
|
|
+ fetchTicketDetail();
|
|
|
+ }, [id, form]);
|
|
|
+
|
|
|
+ // 处理表单提交
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ if (!id) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 验证表单
|
|
|
+ const values = await form.validateFields();
|
|
|
+
|
|
|
+ // 转换表单数据为API请求格式
|
|
|
+ const ticketData: UpdateTicketRequest = {
|
|
|
+ title: values.title,
|
|
|
+ customerId: parseInt(values.customerId, 10),
|
|
|
+ contactId: values.contactId ? parseInt(values.contactId, 10) : undefined,
|
|
|
+ type: values.type,
|
|
|
+ priority: values.priority,
|
|
|
+ description: values.description,
|
|
|
+ status: values.status,
|
|
|
+ dueDate: values.dueDate ? values.dueDate.toISOString() : undefined,
|
|
|
+ assigneeId: values.assigneeId ? parseInt(values.assigneeId, 10) : undefined,
|
|
|
+ };
|
|
|
+
|
|
|
+ setLoading(true);
|
|
|
+
|
|
|
+ // 调用API更新工单
|
|
|
+ const response = await ticketsClient.$put({
|
|
|
+ param: { id },
|
|
|
+ json: ticketData
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('更新工单失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ message.success('工单更新成功');
|
|
|
+
|
|
|
+ // 跳转到工单详情页
|
|
|
+ navigate(`/tickets/${id}`);
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('更新工单失败:', error);
|
|
|
+ message.error(error instanceof Error ? error.message : '更新工单时发生错误');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 返回详情页
|
|
|
+ const handleBack = () => {
|
|
|
+ navigate(`/tickets/${id}`);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理客户选择变化,过滤联系人
|
|
|
+ const handleCustomerChange = (customerId: string) => {
|
|
|
+ form.setFieldValue('contactId', undefined);
|
|
|
+ // 在实际项目中,这里应该通过API获取该客户的联系人列表
|
|
|
+ };
|
|
|
+
|
|
|
+ if (initialLoading) {
|
|
|
+ return (
|
|
|
+ <div className="page-loading">
|
|
|
+ <Spin size="large" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="page-container">
|
|
|
+ <div className="page-header" style={{ marginBottom: 24 }}>
|
|
|
+ <Button onClick={handleBack} style={{ marginRight: 16 }}>
|
|
|
+ <ArrowLeftOutlined /> 返回详情
|
|
|
+ </Button>
|
|
|
+ <Title level={2} style={{ margin: 0 }}>编辑工单</Title>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card>
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="vertical"
|
|
|
+ >
|
|
|
+ <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
|
|
|
+ <Form.Item
|
|
|
+ name="title"
|
|
|
+ label="工单标题"
|
|
|
+ rules={[{ required: true, message: '请输入工单标题' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入工单标题" maxLength={100} />
|
|
|
+ </Form.Item>
|
|
|
+ </Space.Compact>
|
|
|
+
|
|
|
+ <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
|
|
|
+ <Form.Item
|
|
|
+ name="customerId"
|
|
|
+ label="客户"
|
|
|
+ rules={[{ required: true, message: '请选择客户' }]}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ placeholder="请选择客户"
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ onChange={handleCustomerChange}
|
|
|
+ >
|
|
|
+ {customers.map(customer => (
|
|
|
+ <Option key={customer.id} value={customer.id.toString()}>
|
|
|
+ {customer.name}
|
|
|
+ </Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="contactId"
|
|
|
+ label="联系人"
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择联系人" style={{ width: '100%' }}>
|
|
|
+ {contacts.map(contact => (
|
|
|
+ <Option key={contact.id} value={contact.id.toString()}>
|
|
|
+ {contact.name}
|
|
|
+ </Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ </Space.Compact>
|
|
|
+
|
|
|
+ <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
|
|
|
+ <Form.Item
|
|
|
+ name="type"
|
|
|
+ label="工单类型"
|
|
|
+ rules={[{ required: true, message: '请选择工单类型' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择工单类型" style={{ width: '100%' }}>
|
|
|
+ <Option value="technical_support">技术支持</Option>
|
|
|
+ <Option value="service_request">服务请求</Option>
|
|
|
+ <Option value="complaint">投诉</Option>
|
|
|
+ <Option value="consultation">咨询</Option>
|
|
|
+ <Option value="other">其他</Option>
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="priority"
|
|
|
+ label="优先级"
|
|
|
+ rules={[{ required: true, message: '请选择优先级' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择优先级" style={{ width: '100%' }}>
|
|
|
+ <Option value="low">低</Option>
|
|
|
+ <Option value="medium">中</Option>
|
|
|
+ <Option value="high">高</Option>
|
|
|
+ <Option value="urgent">紧急</Option>
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="status"
|
|
|
+ label="工单状态"
|
|
|
+ rules={[{ required: true, message: '请选择工单状态' }]}
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择工单状态" style={{ width: '100%' }}>
|
|
|
+ {statusOptions.map(option => (
|
|
|
+ <Option key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ </Space.Compact>
|
|
|
+
|
|
|
+ <Space.Compact size="large" style={{ width: '100%', marginBottom: 16 }}>
|
|
|
+ <Form.Item
|
|
|
+ name="dueDate"
|
|
|
+ label="截止日期"
|
|
|
+ >
|
|
|
+ <DatePicker style={{ width: '100%' }} placeholder="选择截止日期" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="assigneeId"
|
|
|
+ label="负责人"
|
|
|
+ >
|
|
|
+ <Select placeholder="请选择负责人" style={{ width: '100%' }}>
|
|
|
+ {assignees.map(assignee => (
|
|
|
+ <Option key={assignee.id} value={assignee.id.toString()}>
|
|
|
+ {assignee.name}
|
|
|
+ </Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </Form.Item>
|
|
|
+ </Space.Compact>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="description"
|
|
|
+ label="问题描述"
|
|
|
+ rules={[{ required: true, message: '请输入问题描述' }]}
|
|
|
+ >
|
|
|
+ <TextArea
|
|
|
+ rows={6}
|
|
|
+ placeholder="请详细描述问题..."
|
|
|
+ maxLength={2000}
|
|
|
+ showCount
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <div style={{ textAlign: 'right', marginTop: 24 }}>
|
|
|
+ <Space>
|
|
|
+ <Button onClick={handleBack}>取消</Button>
|
|
|
+ <Button type="primary" icon={<SaveOutlined />} onClick={handleSubmit} loading={loading}>
|
|
|
+ 保存修改
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </Form>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default EditTicketPage;
|