Files
VolunteerManage-for-byw/client/src/api/index.ts
T
Trae Assistant 25b2b8f381 feat: 初始化义工时长管理系统
- 管理台:活动设置、时长登记、用户管理、数据导出(Excel/PNG)
- 用户端:姓名查询个人义工时长与活动记录
- 技术栈:React + Vite + TailwindCSS / Node.js + Express + sql.js (SQLite)
- 支持 Docker 部署,附带 Gitea Actions 自动部署工作流
- 响应式界面,兼容移动端与 PC 端
- 默认管理员账号:admin / admin123
2026-10-04 18:06:24 +08:00

87 lines
3.3 KiB
TypeScript

import axios from 'axios';
import type {
WorkGroup, UserItem, UserDetail, ActivityItem, Participant,
HoursActivityResult, ExportResult,
} from '../types';
const api = axios.create({
baseURL: '/api',
timeout: 15000,
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('volunteer_admin_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem('volunteer_admin_token');
localStorage.removeItem('volunteer_admin_username');
if (!window.location.pathname.startsWith('/admin/login')) {
window.location.href = '/admin/login';
}
}
return Promise.reject(err);
}
);
export const errMsg = (e: any, def = '操作失败'): string => {
return e?.response?.data?.error || e?.message || def;
};
export default api;
export const authApi = {
login: (username: string, password: string) =>
api.post<{ token: string; username: string }>('/auth/login', { username, password }).then(r => r.data),
changePassword: (oldPassword: string, newPassword: string) =>
api.post('/auth/change-password', { oldPassword, newPassword }).then(r => r.data),
};
export const groupsApi = {
list: () => api.get<WorkGroup[]>('/groups').then(r => r.data),
create: (name: string) => api.post<WorkGroup>('/groups', { name }).then(r => r.data),
update: (id: number, name: string) => api.put(`/groups/${id}`, { name }).then(r => r.data),
remove: (id: number) => api.delete(`/groups/${id}`).then(r => r.data),
};
export const usersApi = {
list: () => api.get<UserItem[]>('/users').then(r => r.data),
search: (name: string) => api.get<UserItem[]>('/users/search', { params: { name } }).then(r => r.data),
detail: (id: number) => api.get<UserDetail>(`/users/${id}`).then(r => r.data),
create: (data: Partial<UserItem>) => api.post('/users', data).then(r => r.data),
update: (id: number, data: Partial<UserItem>) => api.put(`/users/${id}`, data).then(r => r.data),
remove: (id: number) => api.delete(`/users/${id}`).then(r => r.data),
};
export const activitiesApi = {
list: () => api.get<ActivityItem[]>('/activities').then(r => r.data),
detail: (id: number) => api.get<ActivityItem>(`/activities/${id}`).then(r => r.data),
create: (data: any) => api.post('/activities', data).then(r => r.data),
update: (id: number, data: any) => api.put(`/activities/${id}`, data).then(r => r.data),
remove: (id: number) => api.delete(`/activities/${id}`).then(r => r.data),
participants: (id: number) => api.get<{ activity: ActivityItem; list: Participant[] }>(`/activities/${id}/participants`).then(r => r.data),
};
export const hoursApi = {
batch: (activity_id: number, items: Array<{ user_id: number; hours: number; remark: string }>) =>
api.post('/hours/batch', { activity_id, items }).then(r => r.data),
byActivity: (id: number) => api.get<HoursActivityResult>(`/hours/activity/${id}`).then(r => r.data),
};
export const exportApi = {
data: (opts: {
include_users?: boolean;
include_activities?: boolean;
include_hours?: boolean;
include_user_totals?: boolean;
include_user_activities?: boolean;
}) => api.post<ExportResult>('/export', opts).then(r => r.data),
};