Add aftersales work order frontend pages

- Public scan-to-confirm page (/aftersales/:sn) with phone last-4 verification
- Admin list + detail pages with state machine, QR generation, reassign, force-close
- PublicLayout extracted from PublicQuery so both pages share logo + 备案 chrome
- PublicQuery auto-redirects scanned zjbf-sh-* serials to the aftersales page
- AdminLayout: new 售后工单 menu entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frudrax Cheng
2026-05-26 10:51:25 +08:00
parent 11f3eda668
commit 6fef517556
12 changed files with 1505 additions and 51 deletions
+118 -1
View File
@@ -1,5 +1,16 @@
import axios from 'axios';
import type { ApiResponse, AuthResponse, User, EmployeeSerial, EmployeeSerialResponse } from '@/types';
import type {
User,
EmployeeSerial,
EmployeeSerialResponse,
AftersalesOrder,
AftersalesPublicView,
AftersalesListFilter,
AftersalesListResponse,
CreateAftersalesRequest,
UpdateAftersalesRequest,
CustomerConfirmRequest,
} from '@/types';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
@@ -316,4 +327,110 @@ export const employeeSerialApi = {
}
throw new Error(response.data.error || '删除员工序列号失败');
},
};
export const aftersalesApi = {
create: async (data: CreateAftersalesRequest) => {
const response = await apiClient.post('/aftersales', data);
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '创建工单失败');
},
list: async (filter?: AftersalesListFilter) => {
const params = new URLSearchParams();
if (filter?.page && filter.page > 1) params.append('page', String(filter.page));
if (filter?.limit && filter.limit !== 20) params.append('limit', String(filter.limit));
if (filter?.search) params.append('search', filter.search);
if (filter?.workOrderStatus) params.append('workOrderStatus', filter.workOrderStatus);
if (filter?.serviceType) params.append('serviceType', filter.serviceType);
if (filter?.technicianId) params.append('technicianId', String(filter.technicianId));
if (filter?.mine) params.append('mine', 'true');
const url = params.toString() ? `/aftersales?${params.toString()}` : '/aftersales';
const response = await apiClient.get(url);
if (response.data) {
return response.data as AftersalesListResponse;
}
throw new Error('获取售后工单列表失败');
},
get: async (serialNumber: string) => {
const response = await apiClient.get(`/aftersales/${encodeURIComponent(serialNumber)}`);
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '查询工单失败');
},
update: async (serialNumber: string, data: UpdateAftersalesRequest) => {
const response = await apiClient.patch(`/aftersales/${encodeURIComponent(serialNumber)}`, data);
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '更新工单失败');
},
submit: async (serialNumber: string, resolutionNote: string) => {
const response = await apiClient.post(`/aftersales/${encodeURIComponent(serialNumber)}/submit`, {
resolutionNote,
});
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '提交客户确认失败');
},
generateQrCode: async (serialNumber: string, baseUrl?: string) => {
const response = await apiClient.post(`/aftersales/${encodeURIComponent(serialNumber)}/qrcode`, {
baseUrl,
});
if (response.data.qrCodeData) {
return response.data as { qrCodeData: string; queryUrl: string };
}
throw new Error(response.data.error || '生成二维码失败');
},
reassign: async (serialNumber: string, technicianId: number) => {
const response = await apiClient.post(`/aftersales/${encodeURIComponent(serialNumber)}/reassign`, {
technicianId,
});
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '重新分配失败');
},
forceClose: async (serialNumber: string) => {
const response = await apiClient.post(`/aftersales/${encodeURIComponent(serialNumber)}/force-close`);
if (response.data.order) {
return response.data.order as AftersalesOrder;
}
throw new Error(response.data.error || '强制关闭失败');
},
delete: async (serialNumber: string) => {
const response = await apiClient.delete(`/aftersales/${encodeURIComponent(serialNumber)}`);
if (response.data.message) {
return true;
}
throw new Error(response.data.error || '删除工单失败');
},
publicQuery: async (serialNumber: string) => {
const response = await apiClient.get(`/aftersales/${encodeURIComponent(serialNumber)}/query`);
if (response.data.order) {
return response.data.order as AftersalesPublicView;
}
throw new Error(response.data.error || '查询工单失败');
},
customerConfirm: async (serialNumber: string, data: CustomerConfirmRequest) => {
const response = await apiClient.post(`/aftersales/${encodeURIComponent(serialNumber)}/confirm`, data);
if (response.data.order) {
return response.data.order as AftersalesPublicView;
}
throw new Error(response.data.error || '提交确认失败');
},
};