Files
ClassHelper/packages/desktop-client/src/renderer/api/index.ts
T
LoveChengke 3880eee08e feat(desktop,island,shared): 灵动岛支持多条通知竖排列表 + 触摸模式 + 收回动画,产物加凭据门禁
灵动岛列表:多条通知时展开为竖排列表,默认显示 3 条,点「展开更多」铺到屏幕容纳
上限,放不下的收进「更多请前往应用内操作」。卡片高度由主进程与渲染进程各算一次
同一个数 —— 度量表 ISLAND_LIST_METRICS 与排序权重 PRIORITY_RANK 放在 shared,
窗口包围盒随之长高(不够高会把底部按钮裁掉)。

触摸模式:触摸屏上手指既不产生 mousemove、也不移动系统光标,原先两条命中链路都会
让窗口永远停在穿透态、手指点不开岛。现按 navigator.maxTouchPoints 上报后,主进程把
窗口贴合岛体并始终接收输入;非触摸屏机器完全不走这条路。

收回动画:main 先发 island:closing,渲染进程形变收敛后回 closing-done 才淡出,
不再靠猜时长(rAF 被节流时会把卡片缩到一半窗口就没了)。

安全门禁:新增 scripts/check-bundle-secrets.mjs 并接进 build:desktop ——
dist 产物会被打进 app.asar 发到每台学生机、且可解包,冒烟凭据一律改由环境变量注入,
产物里出现种子口令即构建失败。
2026-10-02 00:25:10 +08:00

138 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
API_PATHS,
type ClassDetailDto,
type ClassDto,
type ClassIslandNotificationChannel,
type CourseDto,
type HomeworkDaysDto,
type DashboardSummary,
type GradeDto,
type HomeworkDto,
type HomeworkStatusDto,
type HomeworkSubmissionsDto,
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),
/** 班级账号登录(学生端主入口):班级码 + 班级密码 */
classLogin: (payload: { code: string; password: string }): Promise<LoginResponse> =>
api.post('/auth/class-login', payload),
logout: (): Promise<{ loggedOut: boolean }> => api.post(API_PATHS.auth.logout, {}),
me: (): Promise<StudentDto> => api.get(API_PATHS.auth.me),
/**
* 修改密码(设置页「账号信息」里的入口)。
* 服务端同一个接口按会话类型分流:班级账号改的是**班级密码**,教师/管理员改的是本人登录密码 ——
* 因此这台机器无论以哪种身份登录,改密入口都走这一条。
*/
changePassword: (payload: {
currentPassword: string;
newPassword: string;
}): Promise<{ changed: boolean }> => api.patch(API_PATHS.auth.changePassword, payload),
};
export const classApi = {
list: (): Promise<ClassDto[]> => api.get(API_PATHS.classes),
detail: (id: string): Promise<ClassDetailDto> => api.get(`${API_PATHS.classes}/${id}`),
};
/**
* 通知显示位置(both / client / classisland)。
*
* 由**这台教室机器自己**决定"提醒弹在 ClassHelper 还是 ClassIsland":
* 本地存一份用于本机的弹窗/上岛判断,同时写回班级记录,服务端据此决定要不要推 ClassIsland。
*/
export const classChannelApi = {
get: (classId: string): Promise<{ notificationChannel: ClassIslandNotificationChannel }> =>
api.get(`${API_PATHS.classes}/${classId}/notification-channel`),
set: (
classId: string,
notificationChannel: ClassIslandNotificationChannel,
): Promise<{ notificationChannel: ClassIslandNotificationChannel }> =>
api.patch(`${API_PATHS.classes}/${classId}/notification-channel`, { notificationChannel }),
};
/** 本班 ClassIsland 联动状态(教室客户端「设置 → ClassIsland 联动」展示) */
export const classIslandStatusApi = {
get: (
classId: string,
): Promise<{
connected: boolean;
deviceName: string | null;
deviceCount: number;
lastSeenAt: string | null;
pluginVersion: string | null;
classIslandVersion: string | null;
}> => api.get(`${API_PATHS.classes}/${classId}/classisland-status`),
};
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 courseApi = {
list: (classId?: string): Promise<CourseDto[]> =>
api.get(API_PATHS.courses, classId ? { classId } : undefined),
};
export const homeworkApi = {
list: (params?: {
classId?: string;
pendingOnly?: boolean;
courseId?: string;
/** 只看某一天(YYYY-MM-DD,按作业所属日期) */
date?: string;
}): Promise<HomeworkDto[]> => api.get(API_PATHS.homeworks, params),
/** 哪些天有作业(日期选择器高亮) */
days: (params: { classId?: string; from?: string; to?: string; days?: number }): Promise<HomeworkDaysDto> =>
api.get(`${API_PATHS.homeworks}/days`, params),
/** 在教室机器上直接录入作业(班级账号可调用;也可选所属日期) */
create: (payload: {
classId: string;
courseId?: string | null;
title: string;
content: string;
assignDate?: string | null;
}): Promise<HomeworkDto> => api.post(API_PATHS.homeworks, payload),
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 }),
/** 提交名单 / 未交名单(教师与班级设备可用) */
submissions: (id: string): Promise<HomeworkSubmissionsDto> =>
api.get(`${API_PATHS.homeworks}/${id}/submissions`),
saveSubmissions: (id: string, notSubmittedUserIds: string[]): Promise<HomeworkSubmissionsDto> =>
api.patch(`${API_PATHS.homeworks}/${id}/submissions`, { notSubmittedUserIds }),
};
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`),
// id 来自服务端或灵动岛(作业卡是本地合成的 `homework-…`),一律 encodeURIComponent:
// 否则含 `/` 或 `..` 的 id 会改变实际请求的路径
markRead: (id: string): Promise<{ notificationId: string; readAt: string }> =>
api.post(`${API_PATHS.notifications}/${encodeURIComponent(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: (classId?: string): Promise<{ currentWeek: number; maxWeek: number }> =>
api.get('/dashboard/term', classId ? { classId } : undefined),
};