Files
ClassHelper/packages/desktop-client/src/preload/island.ts
T
LoveChengke 682818dd4a feat: 重做灵动岛编辑器与课表档案时间表编辑
灵动岛
- 重构自定义方案编辑器:组件库常驻、拖拽添加与排序、方案切换/新建/复制/删除、编辑模式预览与缩放,容器在编辑时展开全部子组件。
- 新增天气简报组件:主进程固定请求 Open-Meteo,只接受数值经纬度,失败退避并保留本次运行缓存;冒烟模式不访问外网。
- 倒计时支持目标名称与日期;课程表组件新增「今日时间点」,按时间点类型折叠/展开,课中不再常驻倒计时。
- preload 暴露 getIslandWeather 与时间点订阅,契约同步 desktop.d.ts。

课表档案
- 时间点默认课程:布局返回 defaultCourse,日课表没有条目时回退;新增 apply-default 覆盖所有引用课表;全校时间表不携带默认课程。
- 新增 ClassIsland 式时间表编辑器(列表/时间轴视图、拖动把柄、按类型添加、默认时长与缩放)与「课表信息」表单。
- 课表页默认进入单表编辑,底部保留列表/周/月视图;时间表支持新路由删除。
- 「课间」统一改称「课间休息」。

验证
- pnpm typecheck、pnpm lint 通过。
- verify:e2e 隔离验收 248/248 项通过(含默认课程回退、覆盖写入与时间表删除)。
- ui-smoke 覆盖单表编辑、课表信息与时间轴视图,并更新回归截图。
2026-10-25 16:41:29 +08:00

124 lines
5.3 KiB
TypeScript

import type {
IslandLayoutSettings as ContractIslandLayoutSettings,
IslandTimePointDto as ContractIslandTimePointDto,
IslandWeather,
ScheduleDto as ContractScheduleDto,
} from '@classhelper/shared';
import { contextBridge, ipcRenderer } from 'electron';
import type { IslandAppearance, IslandState } from '@classhelper/shared';
/**
* 灵动岛窗口的预加载脚本。
* 只暴露「订阅状态/外观」与「发送用户操作」两件事,渲染进程不接触任何 Node 能力。
*/
export interface IslandBridge {
getIslandWeather(coordinates: { latitude: number; longitude: number }): Promise<IslandWeather>;
getLayout(): Promise<ContractIslandLayoutSettings>;
saveLayout(value: ContractIslandLayoutSettings): Promise<ContractIslandLayoutSettings>;
onLayout(handler: (value: ContractIslandLayoutSettings) => void): void;
getEditing(): Promise<boolean>;
getVisible(): Promise<boolean>;
onVisibility(handler: (visible: boolean) => void): void;
setEditing(value: boolean): Promise<void>;
onEditing(handler: (value: boolean) => void): void;
reportSize(value: { width: number; height: number }): void;
reportResidentControl(value: { x: number; y: number; width: number; height: number } | null): void;
noteResidentTouch(): void;
onViewport(handler: (value: { width: number; height: number }) => void): () => void;
getSchedules(): Promise<ContractScheduleDto[]>;
onSchedules(handler: (value: ContractScheduleDto[]) => void): void;
getTimePoints(): Promise<ContractIslandTimePointDto[]>;
onTimePoints(handler: (value: ContractIslandTimePointDto[]) => void): void;
onState(handler: (state: IslandState) => void): void;
sendAction(
action:
| 'expand'
| 'expand-list'
| 'collapse'
| 'dismiss'
| 'dismiss-all'
| 'mark-read'
| 'mark-all-read'
| 'open-app',
id?: string,
): void;
getState(): Promise<IslandState>;
/** 订阅个性化外观(设置页改动后实时生效) */
onAppearance(handler: (appearance: IslandAppearance) => void): void;
getAppearance(): Promise<IslandAppearance>;
/** 命中提示:指针在岛上 → 让窗口接收鼠标(只上报 true,关闭由主进程轮询决定) */
setInteractive(interactive: boolean): void;
/** 上报岛体矩形(窗口内 CSS px):主进程据此按光标位置兜底校正命中 */
setHitRect(rect: { x: number; y: number; width: number; height: number } | null): void;
/**
* 上报"本机是触摸屏":主进程据此把窗口改为贴合岛体并始终接收输入。
* 触摸屏上按光标判定命中的那套永远打不开(触摸不产生 mousemove、也不动系统光标),
* 不上报的话手指点不到岛(希沃白板等触摸设备)。
*/
setTouchMode(enabled: boolean): void;
/** 心跳:主进程据此发现"渲染进程卡死的幽灵窗口"(岛还在屏幕上但点不动)并重建窗口 */
alive(): void;
}
const bridge: IslandBridge = {
getIslandWeather: (coordinates) => ipcRenderer.invoke('island:weather', coordinates),
getLayout: () => ipcRenderer.invoke('island:get-layout'),
saveLayout: (value) => ipcRenderer.invoke('island:save-layout', value),
onLayout: (handler) => {
ipcRenderer.on('island:layout', (_event, value) => handler(value));
},
getEditing: () => ipcRenderer.invoke('island:get-editing'),
getVisible: () => ipcRenderer.invoke('island:get-visible'),
onVisibility: (handler) => {
ipcRenderer.on('island:visibility', (_event, value) => handler(value));
},
setEditing: (value) => ipcRenderer.invoke('island:set-editing', value),
onEditing: (handler) => {
ipcRenderer.on('island:editing', (_event, value) => handler(value));
},
reportSize: (value) => ipcRenderer.send('island:content-size', value),
reportResidentControl: (value) => ipcRenderer.send('island:resident-control', value),
noteResidentTouch: () => ipcRenderer.send('island:resident-touch'),
onViewport: (handler) => {
const listener = (_event: Electron.IpcRendererEvent, value: { width: number; height: number }) =>
handler(value);
ipcRenderer.on('island:viewport', listener);
return () => ipcRenderer.removeListener('island:viewport', listener);
},
getSchedules: () => ipcRenderer.invoke('island:get-schedules'),
onSchedules: (handler) => {
ipcRenderer.on('island:schedules', (_event, value) => handler(value));
},
getTimePoints: () => ipcRenderer.invoke('island:get-time-points'),
onTimePoints: (handler) => {
ipcRenderer.on('island:time-points', (_event, value) => handler(value));
},
onState: (handler) => {
ipcRenderer.on('island:state', (_event, state: IslandState) => handler(state));
},
sendAction: (action, id) => {
ipcRenderer.send('island:action', { action, id });
},
getState: () => ipcRenderer.invoke('island:get-state'),
onAppearance: (handler) => {
ipcRenderer.on('island:appearance', (_event, appearance: IslandAppearance) => handler(appearance));
},
getAppearance: () => ipcRenderer.invoke('island:get-appearance'),
setInteractive: (interactive) => {
ipcRenderer.send('island:set-interactive', interactive);
},
setHitRect: (rect) => {
ipcRenderer.send('island:set-hit-rect', rect);
},
setTouchMode: (enabled) => {
ipcRenderer.send('island:set-touch-mode', enabled);
},
alive: () => {
ipcRenderer.send('island:alive');
},
};
contextBridge.exposeInMainWorld('island', bridge);