Files
frontend/src/services/api.ts
T
2026-06-04 10:26:05 +08:00

641 lines
22 KiB
TypeScript

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 || '删除员工失败');
},
};