Files
ClassHelper/packages/desktop-client/src/preload/index.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

111 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 type {
ReleaseProgressDto as ContractReleaseProgressDto,
CoursewareTransfer,
} from '@classhelper/shared';
import { contextBridge, ipcRenderer } from 'electron';
import { IPC_CHANNELS, UPDATE_AVAILABLE_CHANNEL } from '../main/channels.js';
import type { ClientAgentStatus } from '../main/client-agent.js';
import type {
DesktopAppInfo,
DesktopBridge,
DesktopStoredConfig,
IslandClassStatePayload,
IslandPushContext,
} from '../types/desktop.js';
import type { IslandNotification, UpdateInfo } from '@classhelper/shared';
/**
* 预加载脚本:在开启了 contextIsolation + sandbox 的前提下,
* 只向渲染进程暴露一组最小的、显式声明的能力(不暴露 ipcRenderer 本体)。
*
* 注意:contextBridge 在参数跨越"主世界 → 隔离世界"时就做结构化克隆,
* 因此**桥接层无法替调用方兜底**:传 Vue 响应式对象(Proxy)会在进入这里的函数体
* 之前就抛 `An object could not be cloned.`。调用方必须传纯数据,见 desktop.d.ts。
*/
const bridge: DesktopBridge = {
getIslandWeather: (coordinates) => ipcRenderer.invoke('island:weather', coordinates),
downloadCourseware: (id) => ipcRenderer.invoke('courseware:download', id),
revealCourseware: (id) => ipcRenderer.invoke('courseware:reveal', id),
listLocalCourseware: () => ipcRenderer.invoke('courseware:local-list'),
syncCourseware: () => ipcRenderer.invoke('courseware:sync'),
getCoursewareTransfer: () => ipcRenderer.invoke('courseware:get-progress'),
onCoursewareTransfer: (handler) => {
const listener = (_event: unknown, value: CoursewareTransfer | null) => handler(value);
ipcRenderer.on('courseware:progress', listener);
return () => ipcRenderer.removeListener('courseware:progress', listener);
},
islandEditLayout: () => ipcRenderer.invoke('island:edit-layout'),
onIslandEditorOpen: (handler) => {
const listener = () => handler();
ipcRenderer.on('island:open-editor', listener);
return () => ipcRenderer.removeListener('island:open-editor', listener);
},
smokeTest: process.env.ELECTRON_SMOKE_TEST === '1',
// 冒烟凭据:只在冒烟模式下透传,供渲染进程的冒烟钩子构造测试数据用。
// **不走代码字面量**——本文件与渲染产物都会进 app.asar,写死账号等于随安装包把教师口令发出去。
smokeCredentials:
process.env.ELECTRON_SMOKE_TEST === '1'
? {
username: process.env.ELECTRON_SMOKE_USER ?? '',
password: process.env.ELECTRON_SMOKE_PASSWORD ?? '',
classCode: process.env.ELECTRON_SMOKE_CLASS_CODE ?? '',
classPassword: process.env.ELECTRON_SMOKE_CLASS_PASSWORD ?? '',
}
: undefined,
getUpdateProgress: () => ipcRenderer.invoke('client:get-update-progress'),
onUpdateProgress: (handler: (progress: ContractReleaseProgressDto) => void) => {
const listener = (_event: unknown, value: ContractReleaseProgressDto) => handler(value);
ipcRenderer.on('client:update-progress', listener);
return () => ipcRenderer.removeListener('client:update-progress', listener);
},
getConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.getConfig),
saveConfig: (patch: Partial<DesktopStoredConfig>): Promise<DesktopStoredConfig> =>
ipcRenderer.invoke(IPC_CHANNELS.saveConfig, patch),
clearConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.clearConfig),
getAppInfo: (): Promise<DesktopAppInfo> => ipcRenderer.invoke(IPC_CHANNELS.appInfo),
openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openExternal, url),
/* 更新检查(主进程直连 GitHub,见 main/update.ts) */
checkForUpdates: (force = false): Promise<UpdateInfo> =>
ipcRenderer.invoke(IPC_CHANNELS.updateCheck, force),
ignoreUpdateVersion: (version: string): Promise<void> =>
ipcRenderer.invoke(IPC_CHANNELS.updateIgnore, version),
onUpdateAvailable: (handler: (info: UpdateInfo) => void): void => {
ipcRenderer.on(UPDATE_AVAILABLE_CHANNEL, (_event, info: UpdateInfo) => handler(info));
},
islandPush: (payload: { notification: IslandNotification; context?: IslandPushContext }): void => {
ipcRenderer.send('island:push', payload);
},
islandRemoveNotification: (id: string): void => {
ipcRenderer.send('island:remove-notification', id);
},
islandSetClassState: (payload: IslandClassStatePayload): void => {
ipcRenderer.send('island:class-state', payload);
},
islandGetState: () => ipcRenderer.invoke('island:get-state'),
islandSetAppearance: (appearance): void => {
ipcRenderer.send('island:set-appearance', appearance);
},
islandGetAppearance: () => ipcRenderer.invoke('island:get-appearance'),
/** 灵动岛点了"标为已读":主进程转交渲染进程同步通知中心 */
onIslandMarkRead: (handler: (id: string) => void): void => {
ipcRenderer.on('island:mark-read', (_event, id: string) => handler(id));
},
/** 灵动岛点了"标为已读"(多条通知的整批):一次同步这一批 */
onIslandMarkAllRead: (handler: (ids: string[]) => void): void => {
ipcRenderer.on('island:mark-all-read', (_event, ids: string[]) => handler(ids ?? []));
},
/* 开机自启 / 日志 / 服务端联动 */
getAutoLaunch: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.getAutoLaunch),
setAutoLaunch: (enabled: boolean): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.setAutoLaunch, enabled === true),
openLogDir: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openLogDir),
openDevTools: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openDevTools),
getClientAgentStatus: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientStatus),
beatClientNow: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientBeatNow),
};
contextBridge.exposeInMainWorld('desktop', bridge);