- 重写课表档案工作区并合并时间表、课表与完整档案导入导出 - 修复课前/上下课提醒遮罩与通知收回冲突 - 增加课件文件夹分类、教师归档路径和客户端本机归档 - 打通客户端下载/安装/完成状态上报并在管理端展示更新过程 - 修复侧栏折叠裁切、课时多选与上传控件样式 - 统一版本到 1.4.1
110 lines
5.7 KiB
TypeScript
110 lines
5.7 KiB
TypeScript
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 = {
|
||
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);
|