import axios from 'axios'; import type { User, EmployeeSerial, EmployeeSerialResponse, AftersalesOrder, AftersalesPublicView, AftersalesListFilter, AftersalesListResponse, CreateAftersalesRequest, UpdateAftersalesRequest, CustomerConfirmRequest, ProjectOrder, ProjectOrderPublicView, ProjectOrderListFilter, ProjectOrderListResponse, CreateProjectOrderRequest, UpdateProjectOrderRequest, ProjectEngineerCompleteRequest, CreateUserRequest, UpdateUserRequest, UserListFilter, UserListResponse, } from '@/types'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); apiClient.interceptors.request.use((config) => { if (config.data instanceof FormData) { delete config.headers['Content-Type']; } const token = localStorage.getItem('authToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { const url = error.config?.url || ''; if (!url.includes('/auth/login')) { localStorage.removeItem('authToken'); localStorage.removeItem('currentUser'); window.location.href = '/login'; } } if (error.response?.status === 404) { const url = error.config?.url || ''; if (url.includes('/query')) { const customError = new Error('未找到该序列号,请检查输入是否正确'); customError.name = 'NotFoundError'; return Promise.reject(customError); } } if (error.response?.data?.error) { const customError = new Error(error.response.data.error); customError.name = 'ApiError'; return Promise.reject(customError); } return Promise.reject(error); } ); export const authApi = { login: async (username: string, password: string) => { const response = await apiClient.post('/auth/login', { username, password }); // 后端返回的是 accessToken,前端期望的是 token const token = response.data.accessToken || response.data.token; const user = response.data.user; if (token && user) { localStorage.setItem('authToken', token); localStorage.setItem('currentUser', JSON.stringify(user)); return user; } throw new Error('登录失败'); }, logout: async () => { try { await apiClient.post('/auth/logout'); } catch (error) { console.error('Logout error:', error); } localStorage.removeItem('authToken'); localStorage.removeItem('currentUser'); }, getCurrentUser: (): User | null => { const user = localStorage.getItem('currentUser'); return user ? JSON.parse(user) : null; }, updateProfile: async (data: { name?: string; email?: string }) => { const response = await apiClient.put('/auth/profile', data); if (response.data.user) { localStorage.setItem('currentUser', JSON.stringify(response.data.user)); return response.data.user as User; } throw new Error('更新资料失败'); }, changePassword: async (currentPassword: string, newPassword: string) => { const response = await apiClient.post('/auth/change-password', { currentPassword, newPassword }); if (response.data.message) { return true; } if (response.data.error) { throw new Error(response.data.error); } throw new Error('修改密码失败'); }, }; export const serialApi = { generate: async (data: { companyName: string; serialOption: 'auto' | 'custom'; serialPrefix?: string; quantity: number; validOption: 'days' | 'date'; validDays?: number; validUntil?: string; }) => { // 根据后端接口调整参数 const payload = { companyName: data.companyName, quantity: data.quantity, validDays: data.validOption === 'days' ? data.validDays : undefined, serialPrefix: data.serialOption === 'custom' ? data.serialPrefix : undefined, }; const response = await apiClient.post('/serials/generate', payload); if (response.data.serials) { return response.data; } throw new Error(response.data.error || '生成序列号失败'); }, query: async (serialNumber: string) => { // 后端路径是正确的: /api/serials/:serialNumber/query const response = await apiClient.get(`/serials/${encodeURIComponent(serialNumber)}/query`); if (response.data.serial) { return response.data.serial; } throw new Error(response.data.error || '查询序列号失败'); }, list: async (companyId?: number) => { let url = '/serials'; if (companyId) url += `?companyId=${companyId}`; const response = await apiClient.get(url); if (response.data.data) { return response.data.data; } throw new Error('获取序列号列表失败'); }, delete: async (id: number) => { // 后端没有单个删除接口,需要使用企业接口下的删除 const response = await apiClient.delete(`/serials/${id}`); if (response.data) { return true; } throw new Error(response.data.error || '删除序列号失败'); }, }; export const companyApi = { list: async (filter?: { search?: string; status?: 'all' | 'active' | 'expired' }) => { let url = '/companies'; if (filter?.search || filter?.status) { const params = new URLSearchParams(); if (filter.search) params.append('search', filter.search); if (filter.status && filter.status !== 'all') params.append('status', filter.status); url += `?${params.toString()}`; } const response = await apiClient.get(url); if (response.data.data) { return response.data.data; } throw new Error('获取企业列表失败'); }, get: async (companyName: string) => { const response = await apiClient.get(`/companies/${encodeURIComponent(companyName)}`); if (response.data.data) { return response.data.data; } throw new Error('获取企业详情失败'); }, delete: async (companyName: string) => { const response = await apiClient.delete(`/companies/${encodeURIComponent(companyName)}`); if (response.data) { return true; } throw new Error(response.data.error || '删除企业失败'); }, }; export const dashboardApi = { getStats: async () => { // 后端路径是 /api/companies/stats/overview const response = await apiClient.get('/companies/stats/overview'); if (response.data.data) { const data = response.data.data; // 转换数据格式以匹配前端期望 return { totalCompanies: data.overview?.totalCompanies || 0, totalSerials: data.overview?.totalSerials || 0, totalEmployeeSerials: data.overview?.totalEmployeeSerials || 0, activeSerials: data.overview?.activeSerials || 0, inactiveSerials: data.overview?.inactiveSerials || 0, totalAftersales: data.overview?.totalAftersales || 0, pendingConfirmation: data.overview?.pendingConfirmation || 0, closedAftersales: data.overview?.closedAftersales || 0, rejectedAftersales: data.overview?.rejectedAftersales || 0, monthlyData: data.monthlyStats || [], recentCompanies: data.recentCompanies?.map((c: any) => ({ id: c.companyName, name: c.companyName, status: 'active' as const, createdAt: c.lastCreated, })) || [], recentSerials: data.recentSerials?.map((s: any) => ({ id: s.serialNumber, serialNumber: s.serialNumber, companyName: s.companyName, status: s.isActive ? 'active' : 'inactive', createdAt: s.createdAt, type: s.type, })) || [], recentAftersales: data.recentAftersales || [], }; } throw new Error('获取统计数据失败'); }, }; export const employeeSerialApi = { generate: async (data: { companyName: string; position: string; employeeName: string; quantity: number; serialPrefix?: string; }) => { const response = await apiClient.post('/employee-serials/generate', data); if (response.data.serials) { return response.data; } throw new Error(response.data.error || '生成员工序列号失败'); }, list: async (filter?: { page?: number; limit?: number; search?: string }) => { let url = '/employee-serials'; 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 (params.toString()) url += `?${params.toString()}`; const response = await apiClient.get(url); if (response.data) { return response.data as EmployeeSerialResponse; } throw new Error('获取员工序列号列表失败'); }, query: async (serialNumber: string) => { const response = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`); if (response.data.serial) { return response.data.serial as EmployeeSerial; } throw new Error(response.data.error || '查询员工序列号失败'); }, queryAll: async (serialNumber: string) => { // 先查企业序列号 try { const companyResponse = await apiClient.get(`/serials/${encodeURIComponent(serialNumber)}/query`); if (companyResponse.data.serial) { return { type: 'company', data: companyResponse.data.serial }; } } catch (e: any) { // 企业序列号不存在,继续查员工序列号 } // 再查员工序列号 try { const employeeResponse = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`); if (employeeResponse.data.serial) { return { type: 'employee', data: employeeResponse.data.serial }; } } catch (e: any) { throw new Error('序列号不存在'); } throw new Error('序列号不存在'); }, generateQrCode: async (serialNumber: string, baseUrl?: string) => { const response = await apiClient.post(`/employee-serials/${encodeURIComponent(serialNumber)}/qrcode`, { baseUrl, }); if (response.data.qrCodeData) { return response.data; } throw new Error(response.data.error || '生成二维码失败'); }, update: async (serialNumber: string, data: { companyName?: string; position?: string; employeeName?: string; isActive?: boolean; }) => { const response = await apiClient.put(`/employee-serials/${encodeURIComponent(serialNumber)}`, data); if (response.data.serial) { return response.data.serial as EmployeeSerial; } throw new Error(response.data.error || '更新员工序列号失败'); }, revoke: async (serialNumber: string) => { const response = await apiClient.post(`/employee-serials/${encodeURIComponent(serialNumber)}/revoke`); if (response.data.message) { return true; } throw new Error(response.data.error || '吊销员工序列号失败'); }, delete: async (serialNumber: string) => { const response = await apiClient.delete(`/employee-serials/${encodeURIComponent(serialNumber)}`); if (response.data.message) { return true; } 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 || '提交确认失败'); }, uploadSiteImages: async (serialNumber: string, files: File[]) => { const formData = new FormData(); files.forEach((file) => formData.append('files', file)); const response = await apiClient.post( `/aftersales/${encodeURIComponent(serialNumber)}/site-images`, formData, ); if (response.data.siteImages) { return response.data.siteImages as string[]; } throw new Error(response.data.error || '上传现场图片失败'); }, }; export const projectOrdersApi = { create: async (data: CreateProjectOrderRequest) => { const response = await apiClient.post('/project-orders', data); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '创建项目工单失败'); }, list: async (filter?: ProjectOrderListFilter) => { 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?.projectType) params.append('serviceType', filter.projectType); if (filter?.technicianId) params.append('technicianId', String(filter.technicianId)); if (filter?.mine) params.append('mine', 'true'); const url = params.toString() ? `/project-orders?${params.toString()}` : '/project-orders'; const response = await apiClient.get(url); if (response.data) { return response.data as ProjectOrderListResponse; } throw new Error('获取项目工单列表失败'); }, get: async (serialNumber: string) => { const response = await apiClient.get(`/project-orders/${encodeURIComponent(serialNumber)}`); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '查询项目工单失败'); }, update: async (serialNumber: string, data: UpdateProjectOrderRequest) => { const response = await apiClient.patch(`/project-orders/${encodeURIComponent(serialNumber)}`, data); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '更新项目工单失败'); }, submit: async (serialNumber: string, completionNote: string) => { const response = await apiClient.post(`/project-orders/${encodeURIComponent(serialNumber)}/submit`, { completionNote, }); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '提交完成资料失败'); }, generateQrCode: async (serialNumber: string, baseUrl?: string) => { const response = await apiClient.post(`/project-orders/${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(`/project-orders/${encodeURIComponent(serialNumber)}/reassign`, { technicianId, }); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '重新分配失败'); }, forceClose: async (serialNumber: string) => { const response = await apiClient.post(`/project-orders/${encodeURIComponent(serialNumber)}/force-close`); if (response.data.order) { return response.data.order as ProjectOrder; } throw new Error(response.data.error || '确认完成失败'); }, delete: async (serialNumber: string) => { const response = await apiClient.delete(`/project-orders/${encodeURIComponent(serialNumber)}`); if (response.data.message) { return true; } throw new Error(response.data.error || '删除项目工单失败'); }, publicQuery: async (serialNumber: string) => { const response = await apiClient.get(`/project-orders/${encodeURIComponent(serialNumber)}/query`); if (response.data.order) { return response.data.order as ProjectOrderPublicView; } throw new Error(response.data.error || '查询项目工单失败'); }, complete: async (serialNumber: string, data: ProjectEngineerCompleteRequest) => { const response = await apiClient.post(`/project-orders/${encodeURIComponent(serialNumber)}/complete`, data); if (response.data.order) { return response.data.order as ProjectOrderPublicView; } throw new Error(response.data.error || '提交完成失败'); }, uploadSiteImages: async (serialNumber: string, files: File[]) => { const formData = new FormData(); files.forEach((file) => formData.append('files', file)); const response = await apiClient.post( `/project-orders/${encodeURIComponent(serialNumber)}/site-images`, formData, ); if (response.data.siteImages) { return response.data.siteImages as string[]; } throw new Error(response.data.error || '上传现场图片失败'); }, }; export const usersApi = { assignable: async () => { const response = await apiClient.get('/users/assignable'); return (response.data?.data || []) as User[]; }, }; export const employeesApi = { list: async (filter?: UserListFilter) => { 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?.role) params.append('role', filter.role); if (filter?.search) params.append('search', filter.search); const url = params.toString() ? `/employees?${params.toString()}` : '/employees'; const response = await apiClient.get(url); return response.data as UserListResponse; }, create: async (data: CreateUserRequest) => { const response = await apiClient.post('/employees', data); if (response.data.employee) { return response.data.employee as User; } throw new Error(response.data.error || '创建员工失败'); }, update: async (id: number, data: UpdateUserRequest) => { const response = await apiClient.patch(`/employees/${id}`, data); if (response.data.employee) { return response.data.employee as User; } throw new Error(response.data.error || '更新员工失败'); }, resetPassword: async (id: number, newPassword: string) => { const response = await apiClient.post(`/employees/${id}/reset-password`, { newPassword }); if (response.data.message) { return true; } throw new Error(response.data.error || '重置密码失败'); }, delete: async (id: number) => { const response = await apiClient.delete(`/employees/${id}`); if (response.data.message) { return true; } throw new Error(response.data.error || '删除员工失败'); }, };