- 订单双视图:商品订单/处方/挂号列表新增卡片视图(CardList)、视图切换组件 view-mode-switch、stat-islands 统计岛、constants 字典 - 工作台:新增工作日历 Widget、待办日历、即将到访预约、公告滚动 NoticeTicker、日历面板 CalendarPanel - 医生排班:新增排班 API、ScheduleDrawer 抽屉、schedule-calendar 组件、门店设置弹窗 - 日志与通知:新增排班变更日志页、排班变更通知视图 - 挂号提醒:新增挂号语音播报资源与 register-notify 工具 - 桌面端:新增 apps/desktop 壳及 desktop 工具方法 - 其他:处方/订单导出、聊天设置与 WebSocket 等小幅优化
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { existsSync, statSync } from 'node:fs';
|
||
import { join, normalize } from 'node:path';
|
||
import { pathToFileURL } from 'node:url';
|
||
|
||
import { app, net, protocol } from 'electron';
|
||
|
||
import { API_UPSTREAM, APP_SCHEME, IM_UPSTREAM } from './config';
|
||
|
||
/**
|
||
* app:// 自定义协议:桌面端的「本地 nginx」
|
||
* 1. 静态文件服务:把 web-antd 生产 dist 按 URL 路径映射到本地文件,带 SPA fallback
|
||
* (等价 nginx 的 try_files $uri /index.html),因此无需把前端路由改成 hash 模式
|
||
* 2. 反向代理:/api/admin/* 与 /im-api/* 用 net.fetch 转发到线上上游 ——
|
||
* 渲染层看到的是同源请求,天然规避 CORS,后端与 nginx 都不需要任何改动
|
||
*/
|
||
|
||
/** Electron net.fetch 支持但标准 RequestInit 类型缺失的扩展字段 */
|
||
type ElectronFetchInit = RequestInit & {
|
||
/** 转发出去的请求不再进入自定义协议处理器,防止死循环 */
|
||
bypassCustomProtocolHandlers?: boolean;
|
||
/** 携带流式 body 时 fetch 规范要求显式声明半双工 */
|
||
duplex?: 'half';
|
||
};
|
||
|
||
/**
|
||
* 注册特权协议,必须在 app ready 之前调用
|
||
* standard/secure 让 app:// 成为「安全上下文」,navigator.clipboard、getUserMedia、
|
||
* IndexedDB、crypto.subtle 等浏览器能力才可用;stream 支持流式响应(音频等大文件)
|
||
*/
|
||
export function registerSchemePrivileges(): void {
|
||
protocol.registerSchemesAsPrivileged([
|
||
{
|
||
scheme: APP_SCHEME,
|
||
privileges: {
|
||
standard: true,
|
||
secure: true,
|
||
supportFetchAPI: true,
|
||
stream: true,
|
||
corsEnabled: true,
|
||
},
|
||
},
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 解析 web dist 根目录
|
||
* 打包后位于 resources/web(electron-builder extraResources 拷入,不进 asar);
|
||
* 未打包(本地验证生产包行为)时兜底读仓库内 web-antd/dist
|
||
*/
|
||
export function getWebRoot(): string {
|
||
return app.isPackaged
|
||
? join(process.resourcesPath, 'web')
|
||
: join(__dirname, '../../../web-antd/dist');
|
||
}
|
||
|
||
/**
|
||
* 把渲染层的同源请求转发到远端上游(等价 nginx proxy_pass)
|
||
* 保留原请求的方法 / 头 / 流式请求体,Bearer token 请求头原样透传
|
||
*/
|
||
function proxyRequest(target: string, request: Request): Promise<Response> {
|
||
const init: ElectronFetchInit = {
|
||
method: request.method,
|
||
headers: request.headers,
|
||
body: request.body,
|
||
bypassCustomProtocolHandlers: true,
|
||
};
|
||
if (request.body) init.duplex = 'half';
|
||
return net.fetch(target, init);
|
||
}
|
||
|
||
/**
|
||
* 静态文件解析:带路径穿越防护 + SPA fallback
|
||
* 命中不了真实文件的路径(前端路由地址、不存在的资源)统一回 index.html 交给前端路由
|
||
*/
|
||
function resolveStaticFile(webRoot: string, pathname: string): string {
|
||
const indexHtml = join(webRoot, 'index.html');
|
||
try {
|
||
const filePath = normalize(join(webRoot, decodeURIComponent(pathname)));
|
||
// 防路径穿越:解析结果必须仍在 webRoot 目录内
|
||
if (!filePath.startsWith(normalize(webRoot))) return indexHtml;
|
||
if (existsSync(filePath) && statSync(filePath).isFile()) return filePath;
|
||
} catch {
|
||
// decode 失败等异常场景直接回 index.html,不让协议处理器抛错
|
||
}
|
||
return indexHtml;
|
||
}
|
||
|
||
/**
|
||
* 注册 app:// 协议处理器,在 app ready 之后调用
|
||
*/
|
||
export function registerAppProtocol(): void {
|
||
const webRoot = getWebRoot();
|
||
protocol.handle(APP_SCHEME, (request) => {
|
||
const url = new URL(request.url);
|
||
// 反代 /api/admin/*:生产构建的 VITE_GLOB_API_URL=/api/admin,路径直接透传
|
||
if (url.pathname.startsWith('/api/')) {
|
||
return proxyRequest(`${API_UPSTREAM}${url.pathname}${url.search}`, request);
|
||
}
|
||
// 反代 /im-api/*:对齐开发代理规则,剥掉 /im-api 前缀后拼到上游 /api/*
|
||
if (url.pathname.startsWith('/im-api/')) {
|
||
const stripped = url.pathname.slice('/im-api'.length);
|
||
return proxyRequest(`${IM_UPSTREAM}/api${stripped}${url.search}`, request);
|
||
}
|
||
// 其余全部按静态资源处理(net.fetch file:// 会按扩展名自动带上正确的 Content-Type)
|
||
const fileUrl = pathToFileURL(resolveStaticFile(webRoot, url.pathname)).toString();
|
||
return net.fetch(fileUrl, { bypassCustomProtocolHandlers: true } as ElectronFetchInit);
|
||
});
|
||
}
|