feat(desktop-client): 阶段4-6 Electron EXE 客户端(登录/课表/作业/通知/成绩/设置 + Socket.IO 实时 + IndexedDB 离线缓存与自动同步 + electron-builder 打包)
This commit is contained in:
1 parent
4566d7cd5e
commit
6e4c7c1050
42 files changed
+5767
-60
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { ApiErrorResponse, ApiResponse } from '@classhelper/shared';
|
||||
import { apiBaseOf } from '../config.js';
|
||||
|
||||
export const http = axios.create({ timeout: 15000 });
|
||||
|
||||
/** 服务器地址变化时更新 baseURL(登录页/设置页可改) */
|
||||
export function setApiBaseUrl(serverUrl: string): void {
|
||||
http.defaults.baseURL = apiBaseOf(serverUrl);
|
||||
}
|
||||
|
||||
let tokenProvider: () => string | null = () => null;
|
||||
export function setTokenProvider(provider: () => string | null): void {
|
||||
tokenProvider = provider;
|
||||
}
|
||||
|
||||
let unauthorizedHandler: () => void = () => undefined;
|
||||
export function setUnauthorizedHandler(handler: () => void): void {
|
||||
unauthorizedHandler = handler;
|
||||
}
|
||||
|
||||
let reachabilityReporter: (reachable: boolean) => void = () => undefined;
|
||||
export function setReachabilityReporter(reporter: (reachable: boolean) => void): void {
|
||||
reachabilityReporter = reporter;
|
||||
}
|
||||
|
||||
/** 判断是否为「网络不可达」(用于离线判定,不弹提示) */
|
||||
export function isNetworkError(error: unknown): boolean {
|
||||
const axiosError = error as AxiosError | undefined;
|
||||
return Boolean(axiosError && !axiosError.response);
|
||||
}
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = tokenProvider();
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
reachabilityReporter(true);
|
||||
const body = response.data as ApiResponse<unknown> | undefined;
|
||||
if (body && typeof body === 'object' && 'success' in body) {
|
||||
if (!body.success) {
|
||||
ElMessage.error(body.message || '请求失败');
|
||||
return Promise.reject(new Error(body.message || '请求失败'));
|
||||
}
|
||||
return body.data as never;
|
||||
}
|
||||
return response.data as never;
|
||||
},
|
||||
(error: AxiosError<ApiErrorResponse>) => {
|
||||
// 网络不可达:交给调用方走离线缓存,不打扰用户
|
||||
if (!error.response) {
|
||||
reachabilityReporter(false);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
reachabilityReporter(true);
|
||||
const status = error.response.status;
|
||||
const message = error.response.data?.message ?? `请求失败(${status})`;
|
||||
|
||||
if (status === 401) {
|
||||
ElMessage.warning('登录状态已失效,请重新登录');
|
||||
unauthorizedHandler();
|
||||
} else {
|
||||
ElMessage.error(message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
return (await http.request(config)) as unknown as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(url: string, params?: Record<string, unknown>): Promise<T> =>
|
||||
request<T>({ method: 'GET', url, params }),
|
||||
post: <T>(url: string, data?: unknown): Promise<T> => request<T>({ method: 'POST', url, data }),
|
||||
patch: <T>(url: string, data?: unknown): Promise<T> => request<T>({ method: 'PATCH', url, data }),
|
||||
delete: <T>(url: string): Promise<T> => request<T>({ method: 'DELETE', url }),
|
||||
};
|
||||
|
||||
/** 健康检查(用于可达性轮询,不弹任何提示) */
|
||||
export async function pingHealth(): Promise<{ ok: boolean; status?: string }> {
|
||||
try {
|
||||
const base = http.defaults.baseURL ?? '';
|
||||
const response = await axios.get(`${base}/health`, { timeout: 5000 });
|
||||
return { ok: true, status: response.data?.data?.status as string | undefined };
|
||||
} catch {
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
API_PATHS,
|
||||
type ClassDetailDto,
|
||||
type ClassDto,
|
||||
type DashboardSummary,
|
||||
type GradeDto,
|
||||
type HomeworkDto,
|
||||
type HomeworkStatusDto,
|
||||
type LoginRequest,
|
||||
type LoginResponse,
|
||||
type NotificationDto,
|
||||
type ScheduleDto,
|
||||
type ScheduleWeekView,
|
||||
type StudentDto,
|
||||
} from '@classhelper/shared';
|
||||
import { api } from './http.js';
|
||||
|
||||
/** 客户端只需用到学生视角的这批接口(与后端同一套 REST 契约) */
|
||||
export const authApi = {
|
||||
login: (payload: LoginRequest): Promise<LoginResponse> => api.post(API_PATHS.auth.login, payload),
|
||||
logout: (): Promise<{ loggedOut: boolean }> => api.post(API_PATHS.auth.logout, {}),
|
||||
me: (): Promise<StudentDto> => api.get(API_PATHS.auth.me),
|
||||
};
|
||||
|
||||
export const classApi = {
|
||||
list: (): Promise<ClassDto[]> => api.get(API_PATHS.classes),
|
||||
detail: (id: string): Promise<ClassDetailDto> => api.get(`${API_PATHS.classes}/${id}`),
|
||||
};
|
||||
|
||||
export const scheduleApi = {
|
||||
list: (params: { classId?: string; week?: number }): Promise<ScheduleDto[]> =>
|
||||
api.get(API_PATHS.schedules, params),
|
||||
grid: (params: { classId?: string; week?: number }): Promise<ScheduleWeekView> =>
|
||||
api.get(`${API_PATHS.schedules}/grid`, params),
|
||||
};
|
||||
|
||||
export const homeworkApi = {
|
||||
list: (params?: { classId?: string; pendingOnly?: boolean; courseId?: string }): Promise<HomeworkDto[]> =>
|
||||
api.get(API_PATHS.homeworks, params),
|
||||
detail: (id: string): Promise<HomeworkDto> => api.get(`${API_PATHS.homeworks}/${id}`),
|
||||
updateStatus: (id: string, completed: boolean): Promise<HomeworkStatusDto> =>
|
||||
api.patch(`${API_PATHS.homeworks}/${id}/status`, { completed }),
|
||||
};
|
||||
|
||||
export const notificationApi = {
|
||||
list: (params?: { classId?: string; unreadOnly?: boolean }): Promise<NotificationDto[]> =>
|
||||
api.get(API_PATHS.notifications, params),
|
||||
unreadCount: (): Promise<{ count: number }> => api.get(`${API_PATHS.notifications}/unread-count`),
|
||||
markRead: (id: string): Promise<{ notificationId: string; readAt: string }> =>
|
||||
api.post(`${API_PATHS.notifications}/${id}/read`, {}),
|
||||
markAllRead: (): Promise<{ marked: number }> => api.post(`${API_PATHS.notifications}/read-all`, {}),
|
||||
};
|
||||
|
||||
export const gradeApi = {
|
||||
my: (): Promise<GradeDto[]> => api.get(`${API_PATHS.grades}/my`),
|
||||
};
|
||||
|
||||
export const dashboardApi = {
|
||||
summary: (): Promise<DashboardSummary> => api.get(API_PATHS.dashboard),
|
||||
term: (): Promise<{ currentWeek: number; maxWeek: number }> => api.get('/dashboard/term'),
|
||||
};
|
||||
Reference in new issue
Block a user