From beba5e8febe77cf698e5a689373198d591dd6d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 14 Aug 2026 17:46:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E5=8D=95=E5=8F=8C=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E3=80=81=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E3=80=81=E5=8C=BB=E7=94=9F=E6=8E=92=E7=8F=AD?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=B8=8E=E6=8C=82=E5=8F=B7=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 订单双视图:商品订单/处方/挂号列表新增卡片视图(CardList)、视图切换组件 view-mode-switch、stat-islands 统计岛、constants 字典 - 工作台:新增工作日历 Widget、待办日历、即将到访预约、公告滚动 NoticeTicker、日历面板 CalendarPanel - 医生排班:新增排班 API、ScheduleDrawer 抽屉、schedule-calendar 组件、门店设置弹窗 - 日志与通知:新增排班变更日志页、排班变更通知视图 - 挂号提醒:新增挂号语音播报资源与 register-notify 工具 - 桌面端:新增 apps/desktop 壳及 desktop 工具方法 - 其他:处方/订单导出、聊天设置与 WebSocket 等小幅优化 --- .cursor/rules/Code-Standards.mdc | 1 + .npmrc | 3 + apps/desktop/.gitignore | 2 + apps/desktop/README.md | 46 + apps/desktop/electron-builder.config.cjs | 78 + apps/desktop/electron.vite.config.ts | 26 + apps/desktop/package.json | 27 + apps/desktop/resources/entitlements.mac.plist | 18 + apps/desktop/resources/icon.ico | Bin 0 -> 285478 bytes apps/desktop/resources/icon.png | Bin 0 -> 95883 bytes apps/desktop/src/main/auto-launch.ts | 30 + apps/desktop/src/main/badge.ts | 24 + apps/desktop/src/main/config.ts | 54 + apps/desktop/src/main/index.ts | 81 + apps/desktop/src/main/ipc.ts | 117 ++ apps/desktop/src/main/printer.ts | 75 + apps/desktop/src/main/protocol.ts | 108 + apps/desktop/src/main/settings.ts | 67 + apps/desktop/src/main/shortcut.ts | 21 + apps/desktop/src/main/tray.ts | 78 + apps/desktop/src/main/updater.ts | 139 ++ apps/desktop/src/main/window.ts | 212 ++ apps/desktop/src/preload/index.ts | 102 + apps/desktop/tsconfig.json | 18 + apps/web-antd/package.json | 1 + .../web-antd/public/audio/register-notify.mp3 | Bin 0 -> 14210 bytes .../form/components/store-multi-search.vue | 101 +- .../loading/PrescriptionLoading.vue | 4 +- .../src/components/stat-islands/index.ts | 5 + .../components/stat-islands/stat-islands.vue | 166 ++ .../src/components/stat-islands/typing.ts | 19 + .../components/table-action/card-actions.vue | 88 + .../src/components/table-action/index.ts | 1 + .../src/components/view-mode-switch/index.ts | 3 + .../view-mode-switch/use-view-mode.ts | 49 + .../view-mode-switch/view-mode-switch.vue | 112 ++ apps/web-antd/src/layouts/basic.vue | 34 + apps/web-antd/src/types/desktop.d.ts | 37 + apps/web-antd/src/util/desktop.ts | 122 ++ apps/web-antd/src/utils/register-notify.ts | 144 ++ apps/web-antd/src/utils/tagColor.ts | 41 + .../chat/components/SettingsModal.vue | 11 + .../business/chat/composables/useWebSocket.ts | 10 + .../src/views/business/chat/stores/chat.ts | 42 + .../components/PrescriptionOrderCardList.vue | 477 +++++ .../order/prescription/config/constants.ts | 54 + .../order/prescription/config/search.ts | 78 +- .../order/prescription/config/table.ts | 14 +- .../business/order/prescription/index.vue | 168 +- .../business/order/product-order/api/index.ts | 4 + .../components/ProductOrderCardList.vue | 954 +++++++++ .../components/ProductOrderExportModal.vue | 100 +- .../components/cells/OrderUserInfoCell.vue | 2 +- .../order/product-order/config/constants.ts | 58 + .../order/product-order/config/search.ts | 75 +- .../order/product-order/config/table.ts | 16 +- .../business/order/product-order/index.vue | 496 +++-- .../utils/exportProductOrderExcel.ts | 173 ++ .../business/order/register/api/index.ts | 15 +- .../components/RegisterDetailModal.vue | 397 ++++ .../components/RegisterOrderCardList.vue | 568 ++++++ .../order/register/config/constants.ts | 65 + .../business/order/register/config/search.ts | 111 +- .../views/business/order/register/index.vue | 342 +++- .../views/business/store/settings/index.vue | 65 +- .../calendar/components/CalendarPanel.vue | 1661 ++++++++++++++++ .../calendar/components/day-todos-modal.vue | 179 -- .../src/views/dashboard/calendar/index.vue | 592 +----- .../views/dashboard/calendar/utils/almanac.ts | 140 +- .../views/dashboard/workspace/api/index.ts | 62 + .../workspace/components/NoticeTicker.vue | 455 +++++ .../workspace/components/WidgetAdminHub.vue | 3 + .../components/WidgetAppointmentUpcoming.vue | 399 ++++ .../components/WidgetCalendarTodo.vue | 386 ++++ .../workspace/components/WidgetNotice.vue | 3 + .../components/WidgetWorkCalendar.vue | 28 + .../dashboard/workspace/config/widgets.ts | 14 +- .../src/views/dashboard/workspace/index.vue | 178 +- .../components/PrescriptionDetail.vue | 116 +- .../src/views/doctor/doctor/api/schedule.ts | 182 ++ .../doctor/components/ScheduleDrawer.vue | 410 ++++ .../doctor/components/StoreSettingModal.vue | 152 ++ .../AddDoctorScheduleModal.vue | 520 +++++ .../ScheduleCalendarModal.vue | 93 + .../schedule-calendar/ScheduleContextMenu.vue | 284 +++ .../schedule-calendar/ScheduleMatrixView.vue | 531 +++++ .../schedule-calendar/SchedulePanel.vue | 1760 +++++++++++++++++ .../schedule-calendar/ShiftTemplateModal.vue | 345 ++++ .../schedule-calendar/SlotRowsEditor.vue | 124 ++ .../src/views/doctor/doctor/index.vue | 75 +- .../components/PatientExperienceCard.vue | 2 +- .../log/doctor-schedule-log/api/index.ts | 20 + .../components/detail-modal.vue | 241 +++ .../doctor-schedule-log/config/constants.ts | 22 + .../log/doctor-schedule-log/config/search.ts | 70 + .../log/doctor-schedule-log/config/table.ts | 65 + .../views/log/doctor-schedule-log/index.vue | 87 + .../notice/views/ScheduleChangeNoticeView.vue | 295 +++ .../src/views/notice/views/registry.ts | 2 + .../web-antd/src/views/system/store/index.vue | 24 + package.json | 2 + .../ui-kit/form-ui/src/vben-use-form.vue | 41 +- pnpm-lock.yaml | 1390 ++++++++++++- pnpm-workspace.yaml | 9 + turbo.json | 3 +- 105 files changed, 16261 insertions(+), 1253 deletions(-) create mode 100644 apps/desktop/.gitignore create mode 100644 apps/desktop/README.md create mode 100644 apps/desktop/electron-builder.config.cjs create mode 100644 apps/desktop/electron.vite.config.ts create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/resources/entitlements.mac.plist create mode 100644 apps/desktop/resources/icon.ico create mode 100644 apps/desktop/resources/icon.png create mode 100644 apps/desktop/src/main/auto-launch.ts create mode 100644 apps/desktop/src/main/badge.ts create mode 100644 apps/desktop/src/main/config.ts create mode 100644 apps/desktop/src/main/index.ts create mode 100644 apps/desktop/src/main/ipc.ts create mode 100644 apps/desktop/src/main/printer.ts create mode 100644 apps/desktop/src/main/protocol.ts create mode 100644 apps/desktop/src/main/settings.ts create mode 100644 apps/desktop/src/main/shortcut.ts create mode 100644 apps/desktop/src/main/tray.ts create mode 100644 apps/desktop/src/main/updater.ts create mode 100644 apps/desktop/src/main/window.ts create mode 100644 apps/desktop/src/preload/index.ts create mode 100644 apps/desktop/tsconfig.json create mode 100644 apps/web-antd/public/audio/register-notify.mp3 create mode 100644 apps/web-antd/src/components/stat-islands/index.ts create mode 100644 apps/web-antd/src/components/stat-islands/stat-islands.vue create mode 100644 apps/web-antd/src/components/stat-islands/typing.ts create mode 100644 apps/web-antd/src/components/table-action/card-actions.vue create mode 100644 apps/web-antd/src/components/view-mode-switch/index.ts create mode 100644 apps/web-antd/src/components/view-mode-switch/use-view-mode.ts create mode 100644 apps/web-antd/src/components/view-mode-switch/view-mode-switch.vue create mode 100644 apps/web-antd/src/types/desktop.d.ts create mode 100644 apps/web-antd/src/util/desktop.ts create mode 100644 apps/web-antd/src/utils/register-notify.ts create mode 100644 apps/web-antd/src/utils/tagColor.ts create mode 100644 apps/web-antd/src/views/business/order/prescription/components/PrescriptionOrderCardList.vue create mode 100644 apps/web-antd/src/views/business/order/prescription/config/constants.ts create mode 100644 apps/web-antd/src/views/business/order/product-order/components/ProductOrderCardList.vue create mode 100644 apps/web-antd/src/views/business/order/product-order/config/constants.ts create mode 100644 apps/web-antd/src/views/business/order/register/components/RegisterDetailModal.vue create mode 100644 apps/web-antd/src/views/business/order/register/components/RegisterOrderCardList.vue create mode 100644 apps/web-antd/src/views/business/order/register/config/constants.ts create mode 100644 apps/web-antd/src/views/dashboard/calendar/components/CalendarPanel.vue delete mode 100644 apps/web-antd/src/views/dashboard/calendar/components/day-todos-modal.vue create mode 100644 apps/web-antd/src/views/dashboard/workspace/components/NoticeTicker.vue create mode 100644 apps/web-antd/src/views/dashboard/workspace/components/WidgetAppointmentUpcoming.vue create mode 100644 apps/web-antd/src/views/dashboard/workspace/components/WidgetCalendarTodo.vue create mode 100644 apps/web-antd/src/views/dashboard/workspace/components/WidgetWorkCalendar.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/api/schedule.ts create mode 100644 apps/web-antd/src/views/doctor/doctor/components/ScheduleDrawer.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/StoreSettingModal.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/AddDoctorScheduleModal.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleCalendarModal.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleContextMenu.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleMatrixView.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SchedulePanel.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ShiftTemplateModal.vue create mode 100644 apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SlotRowsEditor.vue create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/api/index.ts create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/components/detail-modal.vue create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/config/constants.ts create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/config/search.ts create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/config/table.ts create mode 100644 apps/web-antd/src/views/log/doctor-schedule-log/index.vue create mode 100644 apps/web-antd/src/views/notice/views/ScheduleChangeNoticeView.vue diff --git a/.cursor/rules/Code-Standards.mdc b/.cursor/rules/Code-Standards.mdc index cdd0c377..5400dfa9 100644 --- a/.cursor/rules/Code-Standards.mdc +++ b/.cursor/rules/Code-Standards.mdc @@ -10,6 +10,7 @@ alwaysApply: true 3. 有封装好的方法、组件需要复用,不要重复造轮子 4. 小程序端的抽屉全部需要用 page-container 来防止用户意外退出页面(记得使用 v-if 而不是 v-show) 5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化 +6. 卡片强调样式:**禁止**用左侧竖色条(`border-left: Npx solid 色值` / 左侧色块)做卡片强调或分类标识;统一用「1px 同色系边框 + 微光」——`border: 1px solid hsl(var(--主题色) / 30%)` 配 `box-shadow: 0 0 6px hsl(var(--主题色) / 18%)`;历史页面暂不强制回改,新增/改动的卡片必须遵守 # 全局架构规范 diff --git a/.npmrc b/.npmrc index 7549542d..ea2d746e 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ registry=https://registry.npmmirror.com +# Electron 二进制与 electron-builder 打包工具链的国内镜像(不配置在国内网络下大概率下载超时失败) +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore new file mode 100644 index 00000000..dcfe6825 --- /dev/null +++ b/apps/desktop/.gitignore @@ -0,0 +1,2 @@ +out/ +release/ diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 00000000..294d9789 --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,46 @@ +# @vben/desktop — 萧康云医 PC 桌面端 + +Electron 壳工程:打包加载 `apps/web-antd` 的生产构建产物,提供桌面通知、开机自启、系统托盘、未读角标、静默打印、全局快捷键与自动更新能力。 + +## 常用命令(在仓库根目录执行) + +```bash +# 开发:并行启动 web-antd dev server(:5666)与 Electron 壳(壳会自动等待/重试连接 dev server) +pnpm dev:desktop + +# 打包:turbo 先构建 web-antd dist,再构建主进程并出安装包(产物在 apps/desktop/release/) +pnpm build:desktop +``` + +- Windows 机器出 NSIS 安装包;mac 包必须在 macOS 机器上执行同一命令(electron-builder 不支持交叉编译 mac 包)。 +- 不需要打桌面包的同事 / CI,可设置环境变量 `ELECTRON_SKIP_BINARY_DOWNLOAD=1` 跳过 Electron 二进制下载,不影响 web 开发。 + +## 环境切换(dev 走本地,打包走生产) + +| 模式 | 页面来源 | API 走向 | +|---|---|---| +| `pnpm dev:desktop` | `http://localhost:5666`(web-antd vite dev server) | vite 代理转发到**本地后端**:`/api/*` → `localhost:18001`、`/im-api/*` → `localhost:12080`(见 `apps/web-antd/vite.config.ts`) | +| 打包后的安装包 | `app://local/`(内置 web-antd 生产 dist) | 主进程协议处理器转发到**生产上游**:`/api/admin/*` → `api.xiaokang88.com`、`/im-api/*` → `api.ws.g.xiaokang88.com/api`(见 `src/main/config.ts`,可用环境变量覆盖) | + +即:开发调试自动对接本地后端(与网页开发完全一致),只有 `pnpm build:desktop` 出的安装包才请求生产环境,无需手动切换。 + +## 环境变量 + +| 变量 | 用途 | +|---|---| +| `XK_DESKTOP_API_UPSTREAM` | 覆盖 `/api/admin` 转发上游(联调测试环境用) | +| `XK_DESKTOP_IM_UPSTREAM` | 覆盖 `/im-api` 转发上游 | +| `XK_DESKTOP_DEV_URL` | 覆盖开发模式加载的 dev server 地址(默认 `http://localhost:5666`) | +| `CSC_LINK` / `CSC_KEY_PASSWORD` | 代码签名证书;**未配置时自动跳过签名,出未签名包,构建不报错** | +| `APPLE_ID` / `APPLE_APP_SPECIFIC_PASSWORD` / `APPLE_TEAM_ID` | mac 公证三件套;三者齐全才启用公证 | + +未签名 mac 包可内部分发:首次打开需右键「打开」,且 mac 自动更新不可用(应用内会降级为提示前往下载页手动安装)。 + +## 发布自动更新 + +`release/` 里的以下产物上传到静态托管地址(`electron-builder.config.cjs` 的 `publish.url`): + +- Windows:`latest.yml` + `xk-desktop-*.exe` + `*.blockmap` +- mac:`latest-mac.yml` + `*.dmg` + `*.zip` + `*.blockmap`(zip 是 mac 自动更新必需格式) + +前端每次发版后跟着跑一次 `pnpm build:desktop` 并上传产物,桌面端即可自动更新。 diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs new file mode 100644 index 00000000..879364d4 --- /dev/null +++ b/apps/desktop/electron-builder.config.cjs @@ -0,0 +1,78 @@ +/** + * electron-builder 打包配置 + * 用 JS 配置而不是 yml:mac 公证需要按「环境变量是否配齐」动态开关。 + * + * 签名策略(按需求做成条件化,未配置证书时出未签名包且构建不报错): + * - Windows / mac 代码签名:配置了 CSC_LINK + CSC_KEY_PASSWORD(或 mac 钥匙串里有 + * Developer ID 证书)时 electron-builder 自动签名;否则自动跳过,只输出告警 + * - mac 公证:APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID 三者齐全才启用 + * - 未签名 mac 包仍可内部分发:首次打开需右键「打开」,且自动更新会降级为提示手动下载 + */ +const canNotarize = Boolean( + process.env.APPLE_ID && + process.env.APPLE_APP_SPECIFIC_PASSWORD && + process.env.APPLE_TEAM_ID, +); + +/** @type {import('electron-builder').Configuration} */ +const config = { + // appId 必须与主进程 setAppUserModelId 一致,否则 Windows 系统通知不显示 + appId: 'com.xiaokang.xkadmin', + productName: '萧康云医', + // 安装包文件名用 ASCII,避免中文名在静态托管/下载链接上出编码问题 + artifactName: 'xk-desktop-${version}-${os}-${arch}.${ext}', + directories: { + output: 'release', + buildResources: 'resources', + }, + files: ['out/**'], + extraResources: [ + // web-antd 生产构建产物:放 resources/web(不进 asar),为后续渲染层热更新留路 + { from: '../web-antd/dist', to: 'web' }, + // 应用图标供运行时使用(托盘、Windows 窗口图标) + { from: 'resources/icon.png', to: 'icon.png' }, + ], + asar: true, + compression: 'normal', + win: { + // TODO(待确认):正式图标待提供,当前用 web-antd logo 预生成的 ico 占位 + // 直接给 .ico 让 builder 跳过 png 转换(本机 builder 的 wasm 图标工具会报内存错误) + icon: 'resources/icon.ico', + target: [{ target: 'nsis', arch: ['x64'] }], + }, + nsis: { + // 非一键安装:允许用户选择安装目录,符合企业内部软件分发习惯 + oneClick: false, + perMachine: false, + allowToChangeInstallationDirectory: true, + createDesktopShortcut: true, + createStartMenuShortcut: true, + shortcutName: '萧康云医', + }, + mac: { + icon: 'resources/icon.png', + category: 'public.app-category.medical', + // dmg 给用户安装,zip 是 electron-updater 在 mac 自动更新的必需格式,缺一不可 + target: [ + { target: 'dmg', arch: ['x64', 'arm64'] }, + { target: 'zip', arch: ['x64', 'arm64'] }, + ], + hardenedRuntime: true, + gatekeeperAssess: false, + entitlements: 'resources/entitlements.mac.plist', + entitlementsInherit: 'resources/entitlements.mac.plist', + // IM 音视频通话必需的隐私用途描述,缺失时 mac 上调用摄像头/麦克风直接崩溃 + extendInfo: { + NSCameraUsageDescription: '视频问诊需要使用摄像头', + NSMicrophoneUsageDescription: '语音/视频通话需要使用麦克风', + }, + notarize: canNotarize, + }, + publish: { + provider: 'generic', + // TODO(待确认):更新包静态托管地址,需与主进程 config.ts 的 DOWNLOAD_PAGE_URL 保持一致 + url: 'https://static.xiaokang88.com/xk-desktop/', + }, +}; + +module.exports = config; diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts new file mode 100644 index 00000000..162224df --- /dev/null +++ b/apps/desktop/electron.vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; + +/** + * electron-vite 构建配置 + * 只构建主进程与预加载脚本,不配置 renderer: + * - 开发模式渲染层直接加载 web-antd 的 vite dev server(http://localhost:5666) + * - 打包模式渲染层加载 app://local/(web-antd 的生产 dist,由主进程自定义协议提供服务) + * externalizeDepsPlugin:dependencies(electron-updater / electron-log)保持外部化, + * 由 electron-builder 按生产依赖收集进安装包,避免打包器转译这类含运行时文件读取的库出问题 + */ +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'out/main', + lib: { entry: 'src/main/index.ts' }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'out/preload', + lib: { entry: 'src/preload/index.ts' }, + }, + }, +}); diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 00000000..6bf74c20 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vben/desktop", + "version": "1.0.0", + "private": true, + "description": "萧康云医 PC 桌面端(Electron 壳,打包加载 web-antd 生产构建产物)", + "author": "萧康云医", + "main": "out/main/index.js", + "scripts": { + "dev": "electron-vite dev", + "build": "electron-vite build", + "pack": "electron-builder --config electron-builder.config.cjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "electron-log": "catalog:", + "electron-updater": "^6.8.9" + }, + "devDependencies": { + "@types/node": "catalog:", + "@vben/web-antd": "workspace:*", + "electron": "^43.4.0", + "electron-builder": "catalog:", + "electron-vite": "catalog:", + "typescript": "catalog:", + "vite": "^7.3.6" + } +} diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist new file mode 100644 index 00000000..f3950fd2 --- /dev/null +++ b/apps/desktop/resources/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + com.apple.security.device.camera + + com.apple.security.device.audio-input + + + diff --git a/apps/desktop/resources/icon.ico b/apps/desktop/resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..dddf37849cdc61e814cf4075056c9d1f9219ae1a GIT binary patch literal 285478 zcmeEP2Y6J)7T)wi2}MCf1Uo3AqEAIgve}YN0i-KMA0P*8X zlt4my-Sl2}ciw+)vbcnVY(mNccfW7GySul|oSFZ$IdeuJ&=VL6UVmMHV;{lV7X$)Z zfk4owPtEU60zoeR>x3hnpJ^x%c)pDb@S)#*`xpvT?+FBb@f(j2Os@GK2mbR2Q^0lK zw>_o=^`AW{xbLC=hQ7RidicQqtv$KW;z1zE%EO<&HO#L|j>t!k*=m%r!99{To_7Zt zboaCV=)w-8g1VbCisdmk&0e|eA;wErly-^pEZD>}4x-7^Z~41Nt9ZQ{LSmT||9?X`OS)x5(` z3Elp!zrBxHna8&OJ)_ml&2-Vh!@G^X3wQW!R6zF>33!Io%dl#Kf8QgEx6OR3p;s*5 zKXZ`8v!?zSHy;J``@5lgn)7_n8XvvS!v}{-JoWG7BdCjmPrHIYc8#mifa`wiXH-DXC3RofGXKusHEA@-e{~&dA4dc{p5(rDR-JlG@TqyDWZotfx9`Do#sxj? zURO=yPt55T8}|GUvFir8-`eo{H#d(j>{-`!js5SF@Ynvn{r(!hr}>-OX76Q@@lDw9 znO{4N?K*s-P5XsmU-kbq*m8-?$GB4STKYcHw_D8VYX@PuRDaH02p6R69r?fXKqEy) zkRHnn(r1~0`YbJ^eOcO(=@Zr0HT5{bzxU!tMxa4e!l4T8>F{BLOAqxaGQ)b-N(2%T-T zU(eX3EEcRDGC$Y%L0?7iQp9*6wwgjC1cV@CZu1LuD4Y9^v2R;+1`RpRayrp-4mK(O46q?%!{c zY)sCK;`%IU%is{=GwS+k9%)?NSZ}i`AtB~AudS~hP5IyTz^5Z+J|-2Kd{P9Oj}Ce& zCM;s!Q;adaxmzaLk8yc%z0IHPc8zuIx8gsErL&1y&G~b9)mtn zeHY;Hn)>;%XFWS*Y_)O8Sl=rR<3g2%aZNH-JznIYU9&UK{_wo|`j$K{{CoNb(B)Kh zVN>(J&w{NcQ#~@Hd@Yj0pS_}4Uv<6dm=4o`>b9w*IF(b>hXB^_@I|p zeX~wDdFM5sZgJ(8JFMLO`HM5xyyYzQG*Xb=ty$OF-w6|+he$rAI>oPl!?|Ahiu$Md z8>$Yh{;K!=*5B*c&%+&j&5Nim+}J=J=qLuyfWBXS#P8Z;zINTY-=0QF*Bw7RyJPE0 zS?6gq3@o9E?-h-U8v5DEFT6xbBcIory(gO8W zwd<_;TOMSn+~6_#$$F}$)!);@-&sL&U7LE+*l_gjP%%2-$ugOjk)p2tfPS`%UVP~9 zr535drj?rYt^J*N&!x2vKWbIcdM+C6^~AA;>Q20IY(US;zifP8e4<8Y@ICNH=Wo_p z{sf!$j=uYEXuqq<3dX%HVbwE-YuCB<@3hdaxmnkJU#_Qm4gURscdrxosz1?uLhwr$ zcbpCET-U?g4t-;9>uFqdyDvNqm0pKu^`mQ}_l$cxHORC?9<0a7sM>X>-y{dp!=Fma z-2cIdn5eyt=Kr6s8RA}_zC!JidK)U;cKklDuDYxLn;h__zotx>vTDG|*rY3*j&vk! zv@CVilk)U?w1L_ohfGKc>z)%8_F~j^zbB)jg8H1jwdR#Msh9lfq?`KPbGA(GCwpk8K3FBC0Umm{$%XMgG*^ztlrP;4PzV}ecF}MSh-yXe4o1J^|P+~ zf542}y+4zGv+rf2;iuxBf%{=MD85>6xjgLHoV)Z1dSd*(|4z;bGOem>T&c$v*~Kwy zU;XE^FF&sH8S0vO?wa$$%wX$flKI`&jdprI{QK1-W~>PK%~XBG!Z% zuaB?q8AC!7X?D4GhAcMQw zK-fsyJjg|4YhB;%_x=Bz)su(7&!Vhr+|YlM!v~!I;g8SmIaiS!@uPJ@*pq404`dhD z*a@n%jc@p*ocyyz>`2Xq|FF*Xd$E_kN>h#zrbh*I%XHiSlUDk{_0!(%5_Sxoa@750 zkK_#leoZ|3^}DbmHDA+W-`8vi5-iy@ZTQ5X-g}38cao3s?-ujTCd+_b7X$8Fzi+Wh z^ACPxDF9q>(9k#@A*kT@+&R@pfpz1K>igz$&naN@xZ{yI^SR?(>zZS4g9Li+T!G$^ zIn~Fx*4$AbP#=5O{HEg(L^IL<`3gPm$Pn&Czf}T#!5kbFf_wwU9gkShv0N~>H;(y& z>SF?Wn0_C@ZFd~?^ybp_M@){)rTdRqBnSn)aqOMYM}X*MVf(1Lr~ zQ_Weo(E(4NQ}@Bt)vs*X{*O<+^B0YI@0wF9jXw_QYd74l%jiEgjH_2a9_RPWVR}FO zY$;kHuI>~GI4Obt$|9$UnM?;s{KAl;|+-F9RQPs7h zi?p^izTF`FV1#e?GY%e|ZcGe)$!+1LPdH!I-^YWDX0CnjC)jj%&NblulR{otu|0HJ z@4DWfy6TCG+P1&;b*FEwGx%clkns~jo>?+2=OBz(Ypb^HhUP{#%LigDWU zWw+>c?~SKESKGLBObs$COFj5OqsE}R4#%&D>yHiWu~F>$NX*j)fyYA zqf^&kKMol1sn5aL9dGR(|8jarmo$2R21;^Rk29(3`!7h{H|{0%^BQ~ndYy1=?f$0U z{|y*0ot_nU(a$7h!yDfwg+8$z{;qOrFWHy!t^ZlXl$W*U!^;EyFdg&z>pgroEj3fu z@z1|M9x%+eQ(9f){R7_@yb_X5>!+@Mc2{kBCkJ)3$VJ4TNg7SK79;uanK zI_6CX>%DVqaL)twz9yBL7`R;z^ugqy*MGjUMkuPwKb9EOeY3g^4INh={=C1;8$N2# zw9bNeadQxKtht6_TBi988IZiG{}*-nRg%NHpKR#8>hlvro}FD+8>x@S1ok*HHS~>- zzS(Rw3Uk>mG9N>(o;2=%3m-c$YJ+r0UG0|#8mQuS4ApA9PYQi@j=Ei%#|gpD9XoPi z8?^C%Kc9xbT^;j?_fKE-&Qiv*^%7P;6GUUZHXY<49g9;p5Bc?Qpu}qDl2?XY-1FJv z|32y8Z@+FD^@_~L=#F?MDRhcCTPEHWf94T3UmyBr0@c&b$5{E(#^GPyh}mnLyy1D4FA5K`}X;ox5p0kYoARqCXRmX z^S@dzfyV8-fjG8o~Q2t+n1UQ_3fB-`~5ZJ@uSd}f;F#w>BT;Rl+}074-q|5 z!k>21ytXmlzuj!}hE!uNL0ykwo?TLEKPPG9fH}2eDCqqdFAHj4tL^Vuo5uE%cp2O& zcR+*TK3(sMg-Jd2-3zI~=B1kBQXXtpny|O&yqM~VIKi!&{FvmzwD32-sH+W0yN3@; z3GEu45ooALU-d+E`VJ{AJG!sWl06gKO$vR^|9xMJ%uxYdvA05CXrT}YsFfyvSGrwo$wpEh9XQr_EymG_Rvi?|Q@DQE($56b=c zDc}Pzm>S;yUD*E}@u)_Bpu2PW{xJUzv9O)%bhwhf?%8Q|J>z}<&Jo=Gz6MH1Kl5BQ z52WK%pBJZQLeD0iFAp{;fxdkgE}>U<8|L-cXP>PeB%BsDa4>Y*ImAy8H&yDn+EPZk z8{MD2>ak;Wy`TR3VWVv~J743>Y8~OpWmEk6zp+33pt~=6GNVV zZKz+n#P|IzuFhEVZcp|9$?INpNDF)X7x4Rg&hM+QZ|KNC1xEf)D)^p^k^t>G0i%nK z`6zSA^MGQA|CT=^5Gbg;f$LfIx>Yb30ld2R#b+9-Cx4!&08as)0z3tH3h)%*DZo>J zrvOg@o&r1tcna_o;3>dUfTsXY0iFUp1$YYZ6yPbqQ-G%cPXV3+JOy|P@D$)Fz*B&y z08as)0z3tH3h)%*DZo>JrvOg@o&r1tcna_o;3>dU;K8GSLRHuykHz(iE;u5JNZ2Th zh+Zc=g%X*zOPE#>F{Feg_f)V-tYG0kJOy|PJm?fCQRW*&%g=Q=5x3!e*F#^g_$FAo zZ@hEAgfXtY6(iibC`NjiD@J&r51zk$Z1~1Y%f7iTKlM&*L9`(j z$8zffA9Pyq_2((@s8ArGfInDc-y~~XEA_`-Q2zQViHlzDShpuGxE=g*c1mH?09~g8Uq7A# zj~WFSQ|QI#oqq49wbC`Vi%lzRT@6(v$7Mm-;}1ATZV+Zi`}uHt`j{}0w<(kO8Z#Nr zQ31~JTaO8mOU)Aaw*>yh;1jkevH&9{@iRc)OZ|$m?oS?d+C6h@>2}M`*|1XPjeW3yUlC+I6&qlhawN*|UFL;?@7x{`96tcQYxn}? zl9S-P1lLKhM~W0YgK{C#K46rK&lYxrt+zlWb~ViTY}24ksikKIGp6Ew1G+dU_&Plj z6ev){S^*PgY0RwL+cBXh+4n6c z|2chu>;v!{v5!DuySV$|eK$4^VXU$hTF$?Mr@*64fs(4k_P=fz{+;!5qjU-U_Jn^M z;9rCy!JJ-g?2{ePoccfEpZ1BghQDwy<`%5n4HZL|JbB=9+Fsg&kkLZL$Rd-nJghYpoQf>;h*;25t0tztIx#ldWuOt1NI~p zCcM>x&F3G@Q{a)N!1=h{A4#1plHbGJyb#zYysP)ZBDqX^5YYZxn!dl5kpFl43=vo0 zEKtnfKEpSsBD4J?-AR1oc?z@u1@g<|pP%Ob)@3nduMo0d4Bel@Kk%-J$F{&f@gNy& z0on5QdHW-`ehfQ+UqjFWV}O6;qe%hAiVVUd=6+$l)Sz4zOggtAWPLUMYwuUsR`|C^ zOtC|No@&hU*UzSxrZjnv%}29``Bw83sGkB^c_$wqx3br1iqW-4OrNc{3F3JSnGA8g zn)*NIn`;C8pXy+T`3H%YA+vPVukt>z_**5vZm9ka<YA}B> zPXV3+wl8)+wYX`gQ!2uzXuzfV2A#qQU{Kjs0%h)t5Z>ak@zrtd>6K&RUc;=k|| z;3=Rr1@^`VEwXe5?!DS07l-cJ{~TX||4)iF$hKIAASFA%U7t})(N&h@TIHvKh>+2{3ra|xtf-5y81gW|8Msw|AnW3_7wQvT8AJJt@Cs1{J`O# zvjbd=%0tf1Xyr57_^Rt9y{-KluS=E+*w+|9r>e|8|e^ zUw8^=Pl4Yzei%gS_ia5oRO4TV<9}-F|0MtIT+Pb2MlR&>uYH&ESMn6NO@T!_X9ik( zV6DGfdnV=L|G^7%)dx_^{wMkG;L;KPzcAkace_XVFFXabr$F%Ozd|jY^_j@^!Nb1< zu>hleo~TGDJUdAH7V}r~6yPbKrojG$;BN;z=_^Do9S~Cs*{{p`zdHVh_95^vU}ESk zQ`h#5D`RPW)a~JqJOy|PXitIIv@3%ho!S+PLXlU)!yK_S8guN3v8&$CO9yNMjxS-| zucr~SMEtJ^If^V@1?;cQpYOsRL1x;wn7@*z08arm1=33+&8Pamd_asn`m7*R)v~HtMXI*8rNd}YMuf-1vDveB6{=4!M~d*WRUZ^ zjDIQekx7u3koG082Os#>d&H&+R?O!C*6d9FJ5PZ|QXsc9uP z=FGv>oY^7Q9=DIT4%q)(4pOXBpuNULe#WX{E>9ngDLnpIt)}olc?$3pXaWT?i?2U3 z)~nAcOLxlm4LJ+hFYz$0UT?1h_@{YEv4q7$_>2s{rQXKa)Htw@xXS_0a_dX^gcW6L23v7V~sV*G~X|AGh~yTUOSSM6a7?E z+s5C=Q-G&H!xYFUi5)P^>B$>1=-XoC*lTV5pW`=>`IPTl2s=cAJ-;00^oYMHKVbvC zzu^Y(SMU_zDbQpJRIw}rj{{#j4_RbT)fV%AgntS24iRGfq+YN={(3y=T-=@+RaI4d zoPU#hmA{dvfc6x)mVUag{nDq?+G0;I^7T<1qRgos8@cewlv6R`U#gTUbM0%-U&&K| zr@;MCK%r6?eZO_|wm}{OCTnT`zxw%vdy@Uo`>mbz*oVt{q+L(h^^HnV*6x1V!r#kN zfTuv?DX>2}^h@hySZfPRih)Tn{BdHe36gN@f|_SefScEYjc*NqpV$jI$h=IL?Q(;g zOLtCm$u5i0avGFzM@8BuGQ{a9n5S#hW3l2+rW(k3R5$(N)VuSVfv<9E^(c%C5 zofjnh(>@+j;NBYkMxndW^^KS2k146TWq7~!=kMn!z*C^<6ew1tn$KKqz0VTsg2)db z!d#pP`9Zk2pk~6pI zU%W5&+avsw4WPEy?}G;r?!}&X7K$x$c?@^$8hs}5zyuX5HExc&^UvTZz*FGYn;ZMI7D z58^4nQ=nNWa6|sjEXh1GCd1y~*3kEDCEW1S=xcR^P#y{l0gySDZjADyNJNJxDExhn_vsRdY z7*7G70?kH&xV#Iq>=$)pV#xga#Xs2$HtyK74|9FeB?g5%udMhQHb0-gui1Kwe-KZB z=A}S%_L*6-zuF@=aP#0_0=r-8X2QPq8@#8ys^H1ytu6mFo&r1tnv(*T(~ry&&d2&Y zLNvqpA9*^pf46KF^T_zB z_auhSdiO8N1Bg68&;g{hKM;Ly2>WzieJ#ZcQ^9>A@PLVauUxp5f3rhV`-{Jir$BR1 z;HvzD-nK~3X95oXvGl7D;pGoJUdS4{yjzyzijGkvFg>o7w;JV!_gaSC5eSsdo@R0boCj2r+@$H-9@n1vFDyG=Ig8X2cz%W z$baw(HTH3&XCSW#^FI0gTwQH1U$2Lo0xA_VzEW_k$EHi(lV*C^tRA|wQ`~Sb{VIw@ zvBVrV&2QUKoC|Wyi-HjE7K~#s;?j`|#~Sx=4Yj zl0%)ho%8x`f=9n-o25n-_E^V4v8S?dEIyA((L01(c_pKE3)DkaKCU!I9 z_Q;laVE+l6laL)ue8Nwl61wYE*}HU3UViAK^+^TC-ej!op@?5kFTBugir;`}5w|ZO z@K5?Y=>WtBnjrrrxPQFIYiEjz3;5iF zZ|P>vMDS@3b=PC#m-V=O=$iA_g)GV9LC;kcn;9{|amiyU31OY|e=6kD(+2x=4e(C; z36bxBj#jSvZ2s=aUZ9KqLps#>X6YCO%2|1jwGlpx?H50mi?OQ`b2@yrE-U5a7|Ak< zMX%01!5xV|(0|-fTM3E{_NaK*MZe_dogW6K6vn-zW8`R5bpp;U z@ca+#bx!7Mi+S~R9G{@If@X*t#-2jhuUop%pla{spc##-1b;D4fhJO*q%5tw+m7#- zNoRK}df&4Hvx99f^}(JN*x%+(3^ziismHhGQNz#8@d3oRh}`rlr}@4e7L$Jc*(O%M zO>fI6PBope+UBGXe6Ue6Uz0ANx5OLyL;OH*_*3kcw9k!9Km2B!HjIDyBSC@u()7ny zY?`)E{HH;IqYwG7s^yr(i}tjE{MQ_(_xl|-0L^U?_SMA>d(_%_2+GGgy%cykbI&sm z8qtr%@8~;tar;#Ai!^Di5787cz7nh(7SVp4*iUHWvR=2gMmaxJKGFxRUmu0~mMD|E zF4#J8fpCFQzKr&?hpk?1hpPS9_bKmd^9#bi#yl7K$|Wvl&^5qkLIh>we4g>Um3#4l ziF2=H#d_bb8|6O4%|IzeyiSwxPkH;v#{wTK#Sc$d`9|!vD1SbW=c53^Hfm!PEAHrV z;LEvEXTw|}Y)&i8+u2|rOB)vpCUI?7E%z}No2=8;#y!jgtx(gJ8$$w9Ce|5I+-ZbQN2HHCWy1xzWBMM$P$?vt;lQB^| z{vQP(wsIR4s&f69%uD@0Sn}!tJB(M#RU@Uml8u?KYQi{g2pcqqVDC@y1OhgAk!k+1 zgw3Nc|E1T;;cR@V4iTS z`?E(9i?6?1qN>s<9>7%0AhzIy-58fAFUmaIfq%kw$6Sdm*6THyK7@bxK}E13Xx??4 z|I@J>E(Y=VZ_L2vuhohIClfY*YQ3yOsT6r?hK2xh9>_7{VZz1qae99X$o9tIg7|;~ zbYO|IKsj@ja9?Ug;(tn&m0Go}r7tRGc?P-V36hV!-#KgLtgmtifbSB1dl?n*&!*~7 zGMZ;Wn{8a%Lk}=yBYZpE@;LH`PUiYys$#v$teD3OV)ngW8n^C^!dqM3C`~!=1}nSz z8e>f0KiYITI58$q&#({k1XpZs=O# zLH3Y?4v>d7o_fkz;a}`2E-xL})^t{=n0|h_+~(IcvVTObdaA*|s|<7YwjLA{3I9nG z=|?n|QG7M6_jR^lvLHk3=WCR4@Y=TL+Pck*73rs^T`|v0IXXBka{dozH;$PXxoVKh zt>9PA-1O;L8s*!qG}60A=>@+&rI*466kXZ;-to8-V^_+fT|drEJ^FHKPPT!r+Hh@6 z+qQWB>C`nsv5RT37-P5E=SO}w$gMV(`I0Vu-x#)<_yChja&#lO}T zG^2My=hrJJPqX@E%g7Uh-2^Ie5a@?`a{RJ6cqYw@kPi`kLG!TIE{5f6t}GbYR_(1| zITi&OhepL6oVh9{@RjIj&-Qr4ADI8TMo>u8DEi zuZZ$}Jmcopffv)R|FXQa_~z?MrqW4$rmbk)+V54uXXUhSroRxe<+MMIE@561|1=LE z0{(@V52KvdjSgJ*wk}u})&8KAXLD%tpqlT6GSDv0T&a)W01rY#aXfkrlnNqFmas48$g80^1o_ z+bO?C$q3SCY0xQCLIo@XI!pp$nQ!`aU}s#+R2O{u#iSqkWr?CRw$1gp)*e5)^1d6n zrz|G8y?l;js|0=8tn>QKg@c;s(B3&hV1nktsh;+*kFAy%DkcY7Z9bK@@730#DP!e& zkr{j6o9yxW;bHCuN=Nuh#PIcrY45$}Y;z6%Ih_pcmB1bt?fk;cjG}9;lJ^Td&a5Q= zsP*NYRUhVvlXv%-L94$}?bAG{$Q*QeonIru(d-zLb z@R!gW1;s8ktNx^x{}gXUd!i8oPZu`G_tEGnsWWIT7)VlAE`}9r9fc{<*PP)7y zWWE7Q0}qG^F=Da7_|571_45Fd|6~hfqMy=%e|exjE5v+mrk^Q`4=`nApj`>d6*pt$ z^}nS@OA0QE8O!0F=mdCMPe6&$|-*D?%Ef^sw!d2>Tqq?}2~F{~TOj0L-V5{vTw- zGW?BMc7PH10c-=@p9DS<2mW#`OrVSi>~%adVZ9K#N3-@{E5PZ2dqp9O?>jy8Kj~sm zj9rrbD9tLrNM32aH_QFE%pz%7B&_aIE-WgLfhrg zd&vIJ1b(ybQRqDJi)_;SkpndYg>;Ba@)d%Aq~m-F{^R&bh(3!8F;HIf>2jnvX1Nt( z**p(uiB7J0W?a>&QKQ{^C@9WPhH)#aku%LVewz~u_kI@fhUx-bQtX|Z0UI*kv}*LS zm*Zxyoe&goZ0;MYPAtEtueq?e#8h4q|N71BlQTD5Sn;2-!n*m zCk%5qhs-DJg9l{6ey9JMQZga?Yx@q^7>u1^NtUq)lPM%FyANALk!HzW?mx;e;a5qO)#T~BGf+#pltVs>u4|1QQ@(<}C(?3;Lc{b_6+=m{|%dM7}t1OorF)KIRk8v-CZExpI zdCN%NgTHV(NE^@!ZId9bz##y!fbP8xr4-!k(p*EMsv^rY^Vo+|qPzw1S0V4!_*eV( zIXO=F2R-B@|4}l*2bzL?x`*(uel{Jsc>?kaz%Iy#ewFBFq>OOuy{|Mm{B78&&C2JR zi)Ih{bGBY|8ZBF9swCS|23wQzdOfhX*Wg0Ic?DQ>2*<*N-?yuT^uv(v<<8vjkjd+qz~ z!awO7WLu!*!dIA!oRX=5hKi_wXLpz6Uv6=KrzSlfmG2uaEF5j`RD*wvcNvX;4IDj) z-#Hr`7?!yMCtOYu_~e0eG36ljYLC(m_%_3u8ssDOXotL{=Fkt!Alos24}L*;4rI`W zDCUOz3$$l1w-$=?!Pn*itxPZQGFvX57`g~zi>1zHiC5BgPFE>elh*{4W8dJCoKq8S zI(LrAf`2X(dyD2FzKVQvYW$O)o(Wvm#&DB=<5{)!f9@SLKY)3{6e{pLW}-eReg=xj z^)~(`nNjT@6yJ-^D>Mt%2!PB zL$>6*Lz^iFG401og^Me|(GGeLM++Ru=H}!+uBUrA8QfMplEXjQA6O4eIhnw-Kk*x! zkX@DB+liKGu6+fcM*TdQu_p< z3#6f+GNI?>z#kA7Vp^0Jxp1n2j}2&6*0BEk;?YA_nnC}s!3)VaZj6zfd9X`0AJAw6 zwM(+4$a^c-Q(_q-WIV8pIFCCdgT%ue#*Z`$BncWZz~5z$5;!wWpVi zc3fdzjep_+4-x(!bpBOu{kfO`*eBFiq{ATgVWjhO=Th^cx;J(>SCn3}i17Q*p&N+B zONFe>hTScP{7;9>Z8Q99>I%SqI_i{$c;Pq?LuK;mPkw{1qS1V=v2=MZv|CtqM@C}ISBIj9`puqINx#6rH~qW8<% z_J8Je=U!*s+Q(O{GDSQee14dB&j$7>zK{F>ZYf9){~+OBv4E^wHlo7*i7Ds+GY9MEUn`=i=sM40r&KMcttRr~i}e zr+y;7ZGarfdd$jMPZ4_Te>1fim#m8ErJVeIPGYb@RSwoJP^?ciK6M25YTg50L~9MR z5qFvBXU>wh2-pFyF69+Dr^VW|lfR~63T(aVHB+?Gpo-?cBsJsdHvZN60FQs&!9S;$ zRqOt0{a=K6K#{v4`zcVmy;PZS&lqPK5tWLv7cZ_6UC4w^o$n9)x0SD7jq`gP^T-x~ z4v>ia3Mp#@?4+BSBIUx$ISr3&{t9g<5S@4Gg%Qhp$fbzA6xaBcc>L?Iy;B?i;L&0f z8P?Q);Pgz|$>eoC8|wG6!mA&j^6ae0!a9ju`1*Ak|Fp&c>xB}q4vF#y#Cn>u=sjas zHPlA_yml0*QWcubT045D*wa8IMr_#~*yrWH?#O+$4ps~Q)pG%eTZ9kC!Kq_**xC8L z8|wL@jBRe05f4@91Dl%j`RjnKUzZL*_CI_Q6ekqF3hR(DPnnH4%`0pA2RGD4{=9Y+ zfDb@#b;Lrk)iTpO!oL_czZkwHF&7IMzP z8)_$iUV93xKDTtKXgPA~VO$8Yu9I@xh>&Z$I)46Ne-p<8XbjdKm;CS6p~+qNN8b?s zJvtKpng8kEH#OAj@rS0nyEC_CE*ViA}&No>*(PFpfz!K_yu_Uw+`0p;;HrF-?JmLaWi7>M}FAY zP_IYto#H|1-%nxJ|@ftzE*3jF{AkM zJFw6D1ltb##1HDiKdnoI{I_v4WS+-=+R{+3NACmvA7uIOYg}3I&u={b8|vsro>wus zso$YlGet{{l10$}tq~6-Lf$Ki4V2dS11a8w^9gAB0@dH^l2=XqPfBZBa4+oxPWnf6 zZZBGsUz7Kb#yosjbRDhLqntbx4=<^SiRW};(u=uuGV~1G2i`^d8mia!anHuwIj!~Q zp5Mf}wyG^u8~@ZFT>NGS>=S07@QaxBLqpwuWA9X#Z1is|V(N4z)~_CWg6#iv%>QwD z$nc$x$%Owizw`KSsH2;3KB?fi*;jspzm%=ePqFvLK9=s73qU>{u@~~&)7${?L;G=a zG3cZVP-$+TAbgP4kWcTy4zcnxWJAawiTpf6fZ@U51@F1(vv-yW*jtMXmBPiHO6`|E zSvJc3xw4V&&zFtzc%f{#Ywt39=f}&$OWT)PE-o1yd(`ncE-|q0 zvXRd>8M!@L!;WvtJGk{fi2qOVGeeGG;2w3(z}lo+ejO@PZZ6{O|AsociRWW-51CJL zdF`8_o+jxeM+p<$S~HS0qz`a5LCg9Ch~L{91Ll539w(I2l*EaOMDnH&gq3B}Yf;_=^jvd-0!Q6%fduoZn3y2>`$k&0KIuhsuI*WbcNp;~Lxr`*(S4ZYzp1o-M z7f&_R?KyD)E;k_mv%xdcbQu4{Gvt{6OXYHfV;vx3fsqr8#RT>!&XsR$a?X>6=)<2E z2+qrQwExKM`R^S(3^GVo%1F|+X_5PD;h)w3h&_-G418BOav^F^dKFc_)Rk|7e`o;3nxE?%CTb2UR*?T*F;yLI? z>-ilr9ib;XV9YoIA2QA#&@%F$@Gl0R846xtx5!ZbeXw-p#pDBn3abi@nxnOpCnu%o z;!Eq!&H3enC4CcYe($8Pb2W#b0Q(9-{}=f{$GJ~F)*`r9*Qpl%$u}c*N1c%uc>U zu^E+dPc%fQ-1HqT7vLw!_cg=XA05U&@J_KmX|MzHp|9jn9B@!O7PDo|yY?4M$(tlTVJb`N0E-7jXQbmi#9hnzN(vKgKNQW2=cJ zBstGv1jlM09L{5WSNrhv*bsL?;UxbTw`@G~*PEH8@h!5}6Z(TudeM#NR_yv=xo}>G zTd0RZh%rXFKGkcUsZQ$BqL@}=TbpBT{6qfBU@Hlo4A`Or6E{|{q6TvS$K&x`|P0$o|M}` z&WO2%C*7SW|56gFUO@ce6syT}yZ5+eMtJjP~%w zIHtKp3G9i%kiVAjMM;9J^6AW!Sg&w4#I!U$_52`h+R0zjd=yZzoOV}p_YM6qeAGItC8mXz zZt$6LSb$Fr^Mn##@}Fa#fb*%rCWWph#(qwcRgmk*;UvL#Db|~cVMmBvIsp5)p62Vw z?uLy&WSO9Bg4?Tmj^5lnxttYQG+)iNd(y4Ke_oy*WW9ONGC`FPGRKMv<_hef>o}mu z&@MZaHrE3*@XzIHfKQg@Nk_Z&iMf?~Z zNcZtMWoeH$zDCV=M^0H%|G7Ip*!J!+fr5CKC9olaU)>h-gmxHvx`cniz8(CP63B6} zv!2T3z^AimEtfXb$d6j#k_z4(i?#o{EdRNE1ECjYAooYYY5}_zq*oMwX6aYh??9_| zYC{YDnx;{pf|VFVW*m5G)jxmC{x;n5*chKK=dG6LWk{T{KPKkbXdS^&$aY7_COhP( zvxB@5JHbwHG02s!G`lo4;I%Vu2fkRmr5ZpUuSRn_*bt}KYg`7cEh`F`{tS-OW0~urlWg1mXznd@kP+!JrWOr!X9&<_V8Jd z{I9nEX+MxQ^Z}3$mGilxUeHY}-Sn&G92os4<~6jMA1PEQO|NYlwY3VmO=5rvD}bF- zjWb=C1El<8l#7h!Bq;9(_5{IRK!tv$EW^iG8Fyx~kLI}Jzw;Dm2?}6e486>v=>L4Y z;_b6SH~22$*ARM{!*^-M#X#7>CrEL{t?dh>_u0eWZwK9<_BOTiX~%{w?U58;6!EII zbf_xa+beNZpSS|-uSIL?bFi5&JI(#&p5{Cmkdm_C9iwb2fi^s4?ObEZ7PEr&9~_ zk4@F4ef%{KHw6&$*)_XRKIlT?fvKS<{`l{bZDT+GZnfoxj{{#%`N(fTCY9-7Zzg}e zL9$`~o@q0~&Mcl1m44hYzcjhe{ZQs=?%|$87x(ra=Gvjm0kJ@Km=~b+5X~+BX%FVw z{G{5)P`y7Z?fE74Ky37)ZZ|F`9v+MNKCOIy)b&p?q0TP7XYC;6Yj&9Swv@1Jl_I!GbnEOSX00q%3*kIbfA-tl%4l&ca zBa{1MZ4dJNo^obK^%d_>&F8A?{%>8=XgwAUAzeYy#pLBGl0uetpsdDA$4v^`y1PV%nDlzyIsgKQ19V zY6^7zMq$5PS*53_jC=k{_?u@l!>ZTPsj*MTw!lBx8>#4vo5<^sig^fHHiBrrdEQ@}K3Sr`H>m*% zFjl62HhHsbtm}*C-dkay5<(`3U>k}McSHKM7<#q@`mt8>mGlF;R$T~}n7gNZUpCOG zg~<11LwUfUgIYTYR8#y1oW7ZRRE#-)t=5#Qs+7n-QrY23>XAv`gh&orE;TH*MExj6 z;9vgJuzq4oK19-EIQ#?q#IH!^I~z06G9&g$;J~xz5_Wu~WEJKOkgv|u5bTfccxdWvC0UGq6(%}AJo}k{?7-J0%h!q{?03|634$DJ7e!4|bwM|pvZBa{0USt=m@!!SU;b1BXc z^9%*B?eodMkMBvof~pAbr{qb;zFc3Gd*WTjavH7MG=(nwJ-S5!#tQYfTwZ7+o?{;2 z=w_&*`E@b;-V$KtE?M6M?5p_z zeh1!Bm)gGx_Zb|v3Ey=W!h158iw}Eq)84#@+eyRkY^>ESTC2|rRzvRNofOBH0bMHv zwoD}W-6^+cDo%$9uVh~MYg%dU>6aKQZMIkqb^H0_gFylKvGq^f3b%Av)ZzFrHxrcu zWF+bAoURYNG|85(M>im!`fcpjd>0~?P6)l7^bE=~=-_Fh8osp0ksF!E-(^Z=qj`Xu z>cxt*&QY2B-rRCw#n`1=CXf0tNH%JQ+kZw)S^4s)DX!0tn(F$>sPFuwqn2)-Icn>< zfKk`w7l);l#XhU;8Xom~>N=`aD!r<*u`m z+P?|^_2FLqKjA+GIz+MRsZB`>n)QK_kX-S zGt&9T!i)%UMTNY<9Hn)&jsNfAp#bb>z37}ngYA}dJ!0#rui|{^WUq5RFp9Hpq%B zZXEnQAm`TF;qvQ#PgE8B(-m?1k3r8frFB(pK^Oj=x<`TBl30sRgZga|c^j#$34_3= z1bDoy_j7Z1jkopHc&~jV`ER4a1Jt$v{YNqN)-}Znxiqbp=_%)Jw_jILmDNG_C|7@t zvg5p7KOfq6$4zfNMKj8J!|G;W(Tx;{2>8%|{ZlS{_tR;w?q?GN3@URmcap`)dqb8SWWwQ}eDYkb zPx$vVW&K^>$v+Q_6V7RlJ{4Hcqe9-*y+X!r z@OFG#41fplomVpc9_YBd!wp*x6O|0Or}gyBY3pml6Y9H$i^IkI0QiAwwC9u8GDo}^qm&1lBNvx`YT>C5JWI=? zo+;5X-=J=eM7~ySL4hk7hX+aLb;^*Dj!n7skW*X6`Tg1+|HMluhOc^FpxXa9V&PM% z8O2uywgs)!?^G&pm=~w5elg>pIS%o=XExp@Jj_oNpDZW z_ax->r1%|*x2cxp4cPm*wl@BWk5GP)0@(an$T66R{JWR^I;Q2`3jUf^Fdq9&?chiJ zzVw{V)%t%1-1GaZPfNWVqV)7Wu1{-Tr*}z)bjSFg@|=1pxF7EdG>VRnp>L-{a#z#Hs5vNLBK!F z?dSRn6jyf5Sgc}s4^$k4w*A0g*E$r)WjUq`_k8XuaTioM!cRr(ahgwGZ~145vDev; zYesBp4r(ctndVtnm;G?TqkBdc^m--q_hR_#Gbx5P6uN#551@D(vg4Z%*IGUcd?*Qa ze|msGnHKi^vGS5zJ)5K5{4*X+3RE$r>7k^ppIRH0M9e!H}R{HL|~#CQH# zX_1!~w|R`VYohOqF#OFAEd_36Uw>oR(tfunhrR@P=VfjkbSBnE8$Lw2ee6(o>qVX8 zj@{VZt0^5-sjS4BoQ!u)g?+d>*3+oCV2uF#SqfM*&8-ERVBHM%xd868iOp?peSqGn zE?h1EitU9D;>y+!ymHI)`F%E<(&79)90jtglAib>Z0siSJacaUKbl*+ckaHH*X6ar zeC=x_lzYfkP<|!jfNf*Dwn|x{S5X%KPUHrwL-7H6%5?bUDNcvx?$oh)YWy`fwi|vP z{0#8%r$H~vgf4Oo>z^`kyyRz=nw+*@*x0u6*YgxmQ=qJ}^oe?Y|XH@8b*m@a! zT(;ca)y=!EuQpa&;^$?EoLdsHcJJ$qr(@crlAIw*4B)6c?uryTyZ z+P?dSv8tv0-Uy!o`T9xb(>mT%tQSgzo^!+7P?fM(y0%J{&Cl~|+sXWOcTk|9D*f?! zTR&Q6<780mfH6k<=Fqx+nFsRsQeIy8)LR0lE%i*wB~19=cRl>$I~v-+k8(w^>5YTa zS6p^AROEyjv10i8@+f|X@??N6ZGrha-U;jz4@jik0LTp@htL1KXP45v*brd@P2*4V z6u655l$$m>Kce3cn{8JQ@emY9Fy2KN?-CU9`O{jTw%9lCe)t0MDk;_ih@Ay&f7I&# z-qq-P{E;2w?{LZ8ESc8N{mP%ZlLFXJLr=j<%&w*$n);D*zl%02^trtFgRxJR6?}d|4^u9O zpB?c4_y8Ux`Oo1W`3c0%dXWFCw8npF{^eeme4agCK>0DCU(2!1A&b`N0Aq6a{@QBK zo!Y!3ANY?AGGkW*I&k_);=V~PclIv-OP456s*v|iEQqu_8oy-?U>f+dhdey!t_DRrbq_6|`7RhRmW{udX~f&Evfl zW+f6gU>!MjDPOlO*3r?P7~H-X^qunKLkGAYdEDyv()=G}x7Ct%EF$KxR{B3vl^En4 z{B%yDx1cmV6u3tmFUeYNKl?WF^Vi0``gb1qQHn=brmTR*@~{ELy$L8>V~;=7b?IS9i3@z9sgsG9NH&i2=>R4c$+Cc z33?;N{m^%RoJ`vER#{b12k7bdorj4rrM|r6X19%(mwx`S-*b`T75bH8l`1TeS6e}e1H#<{g1YYX|2MduFBZV8y{*j9!v924?gbnggh5{jOFMHHP&?P zNIWSAKEE^%3&i@Gu;_r!l@-@E+G*29{+gDjKwQaH(-Xl8TN?6 zeiK%(r>!AZgpey_Ukl+|6JgF)1V5Y5qcgK|?#AsIHw1dOC34u9LtM_ucY=@n_Sc8Y`kt`<%TOUj9c=M#d+b$H zjcxMvQ^Ei6o!!Ph_x-`bKj*iA%ywM*Ya)?7i(dF*Xr-SM*GX{{ zdw@^43JOQN_P9BF{qUU|&n^FX%Q>GnkKNokFe>-_z_^kt1Jf&SaV4hs^1w^-QvhCV&OZUN5A}idf-fD48q3==@+DDp;vn3mx_RazJDc_|KSha*N+XlHDY#^6J;2A?O zuVu5+Tw&+&xMGBRZ^eh+FDj;lysG$b@T=T+2j{00BBw5j_m&wd-lhF^kz?vz`1J;% zK34D@iD*3!+4P*wua^0Eui6K|;h%VbX3=3DfcjRiPpqyl@w6c;1k7pw*Phyp!tBdV zGvwI2i*n~gBNu)Kcy?3P;kFi^pgDiqqoV{i&iSn$ZqcTV{58!_fl5}c7h800(9BSq z)mE4{mqMqfv8UeelH@E`fNjoy4m{WPqjSD$lCM-aOcE~XzMB8&-kRsoK6BjlB!9s( zsHop3_M)7aHSeZ+(0SOg>g!35;O?hq(6i|onuU8G-d)!@&FggRJE!YYtRHA(gEmXt zO|kwT@e59Rs#7;NO>C&wnc{}v@-~~04D4%}hrR9OzU{CdhVJ5DJ^z;wXbK*HH9rR? zZEdKH{Q2gm0QSAqyPCQGor!L*?v**UQvgUQeeNti}8u_S%v|ucjRNttHEw@}4|y{@)b)b|A+$kN<|c zxY^H_K<;0bA9{1d${xFHy#$J(@at9E(6wYLkNG=wj25DY9oZ{ncTsLq=sq@!44M0Z z&o^TKiU#-H&W_tM>a277@%I~|IdZ|Pvi05?D)Rd0~yQL zyIOGUb%%w|Z+G-DRoG#kS%N}(_=Bc9KNLCu>=irEUg{42h#S@_LUvovv8XtodTQjo z`cZu@I^vhvQBKCJ6nVMQaBmW9YvKbs5$g-A*QFnD_($v?$^W$d<9YpG-I*b$p4rv@ALR?sH2!u zn=UU?#&?d|{-1N{n5)Uax_-*#l@1*sQ-|mNNFSj1pA`5hQ`YFQsF1feYtvT#n){;w zQz?x0M25~C{HIBU1b+611wRi%J1GCZ$Q{^+E@r*LKy@VE!&;l(F0Q!J_v*%%%Tlqn zHzk!U<*Etrp93tPLF>g=;cO;-8=7 z-z!Rnm>G!&FYk4UZ;0Xlvw`oAVk3sRbxn-S*xE;%F_~X_{-uj+Un-@2c#=u>1M|9$ ze~L?@T*7JKS?S0}7Uk7GE%CrF4aPWXLl6F%x+#D?koEnJ&Ho*7cuIqy57&RvEBdUsIN|MxRCe-G7I&q`0$|&E_v_ z1O-?bGu(b-!EB+wpv>B>6LR9U=k~sP82x=*EFHzpAwHP%sgs`rwvIjGgdJgHJ3`NP zr0?K=j)=E$M63;W1pbCX_qL}vMVz51?52+47Jb;Tg2EL`3-;mP0CglLbdLb$1*QKuJ zOzMJsz*yHHh0K*=4WkTmZWISWdxCOtP{aek3)E{2T3TO_f?vs8k+Z~IpDo`#BUt<1 z6$m2td^0B*_07lJUmWlsk2Q7Kh_7jFpPu&b!#+ctzXEFjQ0GkWfNNMUl70G%0LCg? zEMG)pX!C;*)f2XsH(H+ux!BS&tu3BUJWYZkgFY;UFU=lUu)}y0(fk^U&=Waz z+@S|yZyjq_thaFkA3z=xDfYOOV9g`t+@m;L3G{HT{=g;0>ro=2ev?M{I&IOs+Sx*IUU$1np_wV#}fHiZR9|lozL^Wg2kUT0Edy=O;T_z#L#p z+qoEF4{cLsyWok!5li~V{uny)#?mdH-V8i8>t@)AKW~N|UwAV(;`f^#M?bweZ~L&D zU#%K&W1LHuTlUT-1(r@&^XzHH&5=r=SCc((hpyBDbW`j9HsCwIubZ$2xsvqn-LwCm zRTN)-?V?Ygocu8I0l>B<{Ohv+5BSd{+(Y-LJqKyeF`5S`#C*U-_nwg@IhS8Stjc}o ziu#vAkM6guq8ok21UwTZBJ9%|GsrUu=i_?70DTtCfPXkxQRA`<<8VvW;il=SHTUlAg;u)~Sq@m;h6z^_9HxB$FeT)><68wZ>}x z?oXUPbbI-Xc$rC=#8Y2o3%{T(mtPF)kMVvRvMIo4n#+Gm&ynDHV&pCm!&muDh)tA& z<@eBb)Uv8m1{cEz?<|HLoq;?F$%q3a`~Uy#eF=P2)%Ab&6$q$^sBP3LcC~74Er?7q z*#lC4YPHs1t9JWqt?f@+wN+~ZA_xgdAo~UZ0RdSQWK)D7n}nT^m4r<8Op;+H`((EJ z|DOA1!X%mO8S;WJpU=yC^Uk~X-Fx0W=bn4+SwyS^yzog#>158PxvgU~%+qeJ;B!D0}(uv3jlOz2|!Z zKp*~~e~S4RiuvL(@B#SAn*u+WT(|P8w>KB4F3i+2qvvRT-MYX5^L%! z(5F#c4Ke%w^yC3#6Icg1pen@!egQtFuQP5r-mKXp?=TL29c00UbiRs#xINE@2dr>3 z(%2-M|Iw0JM>5ujTUL%3Wd7-p#X*DfQ?N!~=+X3g`eSPgl&8WCjM>e89P)s(s-ybu z)ra4GGi<5P<$0-2`f!p#MoE?;-c0sCepttle%3F_i592Q| zW;vNOnExBnMUv}>WcxuLm9PV-#QfiiHC_2e2h*XC?>o}jqL^cpceqKDenSegA^jOPfzl8ZuIbS-`|v9S@eR zee~HC_kJ0;!T~bX=s#-&N>S{i?=_Sb}7S2U$Y** zHK2dWv8yC~0M^%(_qQH%G3~$gby(otXmgPB_ejc6d&qZz)-RN&=nCusS~J|4BE`XY z{^QyEbq$3x3HL+6hxDw2L)6aj@ZfmI@wNUZnt_fz7;!KIVVe^KT~+`c(D}`cp4f1# zaNV1XX(x=;_oZ>z)~cEPW@hN&An;Z{#62hdT`+Wm+_r-DR_Orm8no>&JqsCtYz@NF z+}Z3Xcf#+1)5A7BxNZ-InP(bd6gq_7rL;TBroHU^qKn`YxaaQ*5*1CoP67#P2BrJcu4&C z)Bn+aIL(PFwy7G>28;e2iv!b8GD}*%#bb-q~*<0J5PVF?d4A~yGEN9`17ZY7I8pH!8n_!yz zYcmlWEYlTq-#`5mUmzJ^Bs4sFWnm|rGLb1#r?3+E2_MaxP+xg+(YvNj<&9(g{m0Au zKRxwfSkMY*gP^Oz_!}md~ccmheZF7c}VWT_X&gzU~rNb3yHqDC|`B-(V_NNudlqR__O)% zmn6Gsny{X30`IJVO+a}jVgrvS`p0*#K>JJ9JF!mK2DQLv=uD!!=G?}RxQ0_3e_}8- zO;j1AH9>#l<5cCg@UR3QgGHw{F6f{1;baek`9B0UOQFkMP49j3d}6z)^@jfXIu(9#QI?DPjt&W$YuIL&j08$x6vTGZ19aB!~&7UI+@#3nV-Klck7XY)Vkzlzo{A3Ly?6i?QM{8RPRkF^e_qI3_<<=wyfqP22& zn6^VX75RfC-GB`OXw(+o`{Kv^4A?K6bJ=xyXS;+*p*8ilN8$FqR z@O6>>0>ufaLEfku%>VT`G>942n(TwajWuJOD3UzX#ar&bP=5ARyS=O${s zPm8Afp68d&J|+k4lD;AY_5uFbgXjKMB-am_*TR+=vcMeJI6lAX!J^jArU$G(>!2?i z)M?WWZTMbHd7@)WC)ou+7Er8nVV6I0VO7{0s3rXx=%4rj9YXxj?)pJnJmK2$1M*`Q z;)T&W1-oI`3YmEr*JHvKtP1jCKI}42W!+nSX4eyY@^T_REXhxdZd9y|R@Z(Kt*PA< zt*TrX-B6MgU2*EI=<{FvG5YkLXQI#Sdoud`-e;l<_WvWQ^yJd0x{^=+r$M>LS6%<9 z*Y&dJxGgt6Nxsb0L^^ao>@5W&hactY8!h{lp^xnW=mGS?_xs|U*vqLwA2)PfwwrCw zpb2RZ5AH8qH-FXw*LJc^48&eV0Bn_L?{YZ!A)6qI1tBavXM33)?XX?f-ysztEOnf zGpAS2IC^=Fn~`XvK7A6awvzuXe4yL>3VIXO{NI!QG5=RXjw$;TF~vWDU-3F2_5sNP zC7Tgj#BywMWEC5bdjNV671li(9JHrUkNWaqZ&Iql0|`((f}kEQ_F%?G5yx1IcFhqW%X1K&V9 zg#5rl-LyyXi{(y+O?gRAi4mXuwJERp^Ls)Pr`67e9UsM7pt*Zk>n#3luy%->3?0Z6 z!3F~U{qrO5Y%i-j6mKxL-_l<@`hw}<%h1_ccURewl~@nlc~P;-wY?_O!9;T?(S8kV z2KjmbG(~bX`RCK`I^3t6BEmuZz?s_#k&iLy2d~0}XDs%Gw07s~cJp&1)*GN6;zc}W zfQ1j>9i%5zr%Ylh%&z0~cR>J0?!F)9Gvn%u+u85K5$|yz>*+tcGy9k`KD1Qog z0dkTzA{I;o><_D9FHr;9uLEzX#++H7=EaIuc&YPtytq!=bn)jDQ@bxbi|u}6d)+O+ ziGAct`8o%aALJg=2M%jo4s$N+YtARxcwT}ln;Ywc+&3OfhPAUCa`SH&{J5m7=Fm-I z!{b2hfzPV<+Z&%b;zw%$^n>y%_0{*m-52x+v?ikO6#}_!Zp^f{gQZ(zJB|8b@c`-r z*)Uc&7EMuH%$!%gJ!s#_gxgdF$=;?)S_^{~%0VO5Ydnxk6m*{D#%+ij)=+K{7gm91 zN$QDS}k~2C3JR`pfS=pDq)W} z68}W&JqdvaQM>~|SIBt|U7LS%ogsL!5=TAwGTm2knAKiHkLSDUBEH2B_*8-)w5C9B zn&N0Wm3j98Rpm}OW9o6k1GZgE`s3_q=MF#YFOx6*;KrXF=6fq1LSs4%vOeX%4#3zB z#CMR#y0)%A8u=d+s~<4;?LhQ67Vtd!t{>kqd^I!0SDJ`3h z#YJoNt|*c+lBv0^FZwcKnP7lAh?a@=yXb#_=WVs8i^ssGvH@#^{MC+z zeX%!JD)XaWHJDU`j@2^KZ#9zgwT9eDuVe+T503ZJ1%qdcVgBc95Uwi&t$_YR;Lrbu zl>7HKbY2+t+%^&%jYiqanxhZTjh?EZHI_g0&!e@@qVE-o9KXSsd&9v)0~fnBy}R+r z9VP11KhrVYhreL94Beds)g%Mg$Qs@KAo3Oqms{-uPRb8+RACS~r*#RgOT z)X^9__N)UwGfrQZ?0htA@$~W#ZU;0<^ODtkK4h5!W13?0`axC>MC@I^NN>f7($7Qf zi3fYv-E1oKcq#SgdnxAH9Eu}~J`Nc#HgL-K1QEa753$Q>UvVC2$se&ygO^OtP0fD) zZ-ts8Q`Gt@dv|Y8^}|ocU9bN9JN5Z7FPz)_$kr2S-pvZ=(=_1qO^Daj0Qqwy^1rP) z+P?Q-a{yhT0(*?L&=IJxejz{QW?Y|-y;)tKzhaTzR6j2E2{TW=`MO`cvx-4wZ_R*3tK*#|4FXqamvi=Z2Qvi(C2+id#Ifh zuaIJtlw<#Z>kc8qsNs{|0z0qcaks0QYPLo)Qmf0t5w8k%|cOHX?e0w18(|E=lITQXJvyxo&Z=^r5rA>cj zz?}De^-E0mmu-Lf_j1hrav`=Dv)Dlm?dY}yLy|nu7Iz!vVaFtH{K1p@$g*U&PEdS6y&Z#L7JS+%y z0kj9lb6r@*Dvet@EWbzLEQjvVAMf!anIAeCilsYig{$GI#QQ(1X*+$Nm}qz;ZCQ6> z;UD}Un+H*x&{4)=x9?%8o8&jrH6o8rNWug*Ybp57vYRVDIk|L6r>^B5==(>~olz^x zAltRo?El%BPagaHY>I=1?2%hA{_4}bSq=8?1iH1*KAnd@XODJSvJF8d+3uyi-#M^h)Hn!gGUjnH4tdv_7D@ z#WKc5f&OK{kqq|7MEjKQRu=E1eN%Wl~!c(Gm9VtJ6J1 zPyg0-QFMI;YnWE(0Z2AbLk>6*@7dW_llf0E;kv)JT+!y;x^T;{V}qAXZVpPo*nwXa z@qtjRGs%vf+pmrm{iB{B*kw|jJMwXpf8b&KYgWMSvX-+y>#vRD5w@#19`ir#|6jv> zR{BnaV>-j8g6Ak&%E7>JYw#U z-MNMSX^x>Wg|lVt*-Zwux^IEgS;#p+@`E5l(72?1hj7gMLCak9QM-S?Q?0H0h4DJf z^+NN%%uN4wpZ`f-BOIG?PBL34@J#EuM<4^tfxS*>id*N?i*H+ds$i|C{HTqr`Hs>f z?`GsK_v&aS|3uJKJ?Q{QFNkX~?GcdtZ%zN?J58|)FTvmF>>77g1KL+&9i0Yz*{3hQvSQZp$70=#6k~ff_T%NGW5zm`^5F$R*DS~0b1-7?Q|^sW z>~DwCo;_rRaOm*o;`cnzHs#Tt2fa=x#mL8cl>FFv4o>K;16Mffej9hsx&4K!|EpzH z)0s&>E_{0i!e3AL$43|b;nC&57x`P7d1p_aIcmTAsGod10>Bfvtv9|)V7$8)I)PP( zt5fgj+1|1I&2*Z(E**F!>)0xnPBrXtwb&b|L+sBA_~>xI12Zoe?e+Xn>+41T;0L4& zq}T+NnE%O6T8Xw+VsG%$9>0$mQ;a2Ahx~5nh@4wGlt20Cc;rXFPPtR@%NQqpV1|P! z1Ttg@(JrmqQ#`>FCP8oSgIrY;A>U3w+*&Weo)CN>7<5nm#lg^f1tXuP9JEh+)v)O^ zhR01+#~pe#P0@Vvy3zCXh2+Ftuf7-%5C30`2fNSzH17{|fJgX2_kx(J9nZzxomNzP zYFNI+ef4*w_jR2|?>V;pXX`3bJ+;k{JL_P7T)rB10-!HzTi>D5>}Wo3i~hUG1H=!A z2cXTzS4^(aRDO2nNbxcj<&7pb;nbC{zPI`O@>hPJ@j!Oqhd!Mb~^bb0?ob26Nd-$~><#4eFPDfg2v%c}W zCo2wy#((jz^{=dZxbQb=x3q*Lcs4_pY=OS8B{1H#B_PqIC3K~GOIWOTOK9|@=3lM2 zt@(wl`;IKy_d?8pvJJm$)Ytr&{7pvMmhn0@ygX?BPfEopY)ML|JM8eJ!OCTP5OWe zJX;5SKwa|GJbiQVxUdDdp6G9^`?57Z=|$N}m(C!JUCIN*_d@!i&5_XcP|A{>Ah*9l zY~c{JEdcfj?|t^<_6~zuH1^g;R_jt(nEu;y@m{Shh`}N32hth<1#wKq+!YcPLgZ80+yUz5m)`;J-VcTjNK5f6)Dhz+ZeQ zGraEHO~0ExPHMq`OZ9VGlGIdv2ofv$i3B(3&6s_8#wi^7s<9 zUOD9W;`YKbV-?P2$KHx9M%**Jn&E|9)G~VC4iT(p&BM=-lx#nE;S3eXJHk%s{KXPRG4ef1+6;coTTAG)~ z%lzMf`Coy(ONw(;x;6BunD8pK#eNvj8QLcNbCv(Od9arVTr(g6%rPPf`Gy6J(bE zNoQEOEl8q&`;nZeYQCg8C!4qA#@4ye0|#OL4@`jlBdr5OO8+$f6YufE8sHxv$<8#H zYKNR>%$^^h{@A-O)TTKYDStV|3#!DvdM$YCaAWw`g9r4af4&C*`mcii?+RiC6mAWW z=--}$L_~FOyPW#(nTy?6SO#LNVx00k!0^Qz%lW;(cN6`GLH?hQxHh35_+Gi(R5(LK z=yXt@$+)LD!>NVnzYMXqO5iJ8MZUt&3EG2}hf`-y`ltDya;!H}9AU%>Jo#CeME_zM zRCa0$@=v-g`QoqZ{gAhtXpZuibNVO$KatM=)J}>gOun9T7EI9PR2+KVPUsm{was}^ zw_eP=u}O*88kE1Z9JEjLPkZbZ|DfT}>}ZzhN&n;%MD$Pb!|UJ|bZXaA68#VB>yNKO zolJepU$P#}ldm+#`yjm^_WpScr6KC&`kS*y6DG+2*rUYWx__(-TYo$@OH5dAY54rs zldJD&BAL1tJb+^DRFPevnCYL|3VdoZC$P(~i!VC%4@v(ora@(=wv@M?zW>>jA84<} z`=|L|-1KjeOM=L61Nrfm?E34+c0$jvs%_U^xb^(D2h|iWn0$g8F!xs?hHxd;*Vig` z_&~BkulXPJPq|VW5znX)`+v1X%MJ~z^HQO4g@NPMTke}5H@%(5P@{RDWPZyb()pja z4>k$`L&< ziE<^K%9>Wt)|x9S+e-V4eyP^Ig$X|mi<{V;{)q>WEFe<)r}>}L|4M=W|M9W7=$}Sa zQ{9(OpG%mmYXMEwVxOW6xx^^m29Lc1S)1se4jv~2a=<|H`e?&@(?8b#h@Dl2_&5c- zgSIf1CmP=hiAwE%UL}1Pd-Y{>_Wl~t(S5lAGhC#lWGXRuqpQU`C+~=2QpK^at-d-`w|JeHvK)wLVQxduF2~p|)En}or z`RRl_2DquTYwq?xNJ-fl4VsY4UJ*t{f9*w>LAfH>{{i7$mDyTVq{i9L$_ zzr-VNZxG{WYByYRKlJ5eiV9!D$M;&So5n8|GyBq&&xgw+hbrQ<02R6$i9kH+8P6Ej>SAT^F!B8iiaMIyg$Lo z9*6-TUfL)655WFJAmX}*EuEsuRUY`Mb$`Tk&5TO7+(S?7IG5&Zs!R3YHh`j_f5;-O zu%GHAza-cu9Z#5EsBhfqCnnrUZMilWI8wdmhjSMB)X#%F7>M}i@>Ea6+7wUxFI)SN zfyn0&2wlaWQy$pasLgf1HkcavTZ{&+Q+r#sRe7O zyd+m`Sx)3a6RVT<{D%IC$9;*zP1{_bOMM`h^nU@AD*^N`N4#{g%Kwz}-5{dBhX`k(6>Vdq@qucSCbG)d z+qhkjMJ5<+ogiQ9W3DH=<67h?t>5X%3V!x=86K=+J?#nDb4@@#NRTV=4$4WShTdNdeLrFm z8P2DCZ+B}+*29qdzco27#(E5h_5B*A@O*7!U^eCc_Cx-ofCRDu7KaWn1o}Sm^{3q6 zME|tzmo0HMZaW+IhFI}wt6h3Mb2REJ9xUGb#EybAC;er}0Hw$uOmaWz1uVLO(cTlF zI0RIu3VJ?@@mrPVYRF5!VV|aY?M%~kn8Vf=&onB>&XV}SGA+^_@Uxq#?AM)Cl35#;Yfd>{n6iHDcEXt$Iucn`UqT*ZO8 zp|wJ*>u}I@svoY+S-8D0ZHlfk)xo4(E5r&U9>8q`ai)BnbZz-f@dJgt;;_{vc^*fX zUl7?5;^6Ux%-6#6koU32-=5*ds?%I_d7B>D)zNg$&-g9MYd5s7^X-1Rr1s?9^P_G* zEyo&hCTxJ_Kn{>4xv>EF2U8xNYs~{l1_>qGMA#(B(%jkXOoz^w*Mx3v)3w|&p1+i- zgE^W^#v40S-~6p`gI`YRO3&67u9tU1{0$Gp3vgu>h{sqDJ%Jj(HEAA*&F9XKdYtPa zqbUJvfLx@c{6gRpO0wfyZ$F>##H=Wnia?65LwjwQ_vJWK-koc`CrIxLgx#DR`_yub^YCTg%|EX^ zbY1zT8#*t3x9V){-!Eo8aI|Elv!NCGdL3w44gAwSVF_Xfl*4zR74+PKwF1#K9Zitu zuOJ4B%FHK}kn<^iM|1K7+;>4fKR5mP3{Q3Go{()VW!a(dNt$rIj#;T+qlbYOy~^vI zPoMa!e5rF)80aqm^RFNDZ*uGjj0OGsC3-M9{O9E>JXCwnYdMcipbWG=D?8yDj zm2(STu3Y!f{!1&Tl@}yc7Y^`z5p zgl-@F!c?%@*?21b_J*R*Ux{rk+4&R3>OCOuOKW}cd~VUVB&T+**K4Ukf1exWT@!?y zfPS#I^QX7~;0M?8;5taG`DKeeucRIL_X4BQaFghfH^K($w0T|)`P&{YIE1MfkwGJ&_BKRTr+UJ9%!Cyd4vP|3(zn4;a=D(qm~+2 z(=Eepyi`!afP{e&W1!ujy780KaesX>=DVfg2~M5i&^3i%pV=R_g25C!82JLpHlODW z1P%A3cU!;Z&?n4691j`x^#j0HNY5Ao83R83hCgLK^!3HMFP>#at@nr)3zQHLC~ABf+f z@vg@C(NoJZj{Y;PUYmC>W9`yD^@zWkRDpzn(Zzt)*y(w$?&uTmY<_lENZiCKzeE>Z zaGINm+X#UNa5=ziGe|l-i~S(U{1&|)-IphlEx)@dD9*+BRO}sfiF@B%QP)}syHwq{ zwCfvPEK9XX7!WrGTA14PfI9UD>4*RC)qkYT+3~yOzN%-J-_SHS)>#*n>}U$bnji#g zf)K0^!a@7+#PPZ~Mc7 zEoY+wKG^lUX=gMa-OAYI35+#K{--^WD*cu)AYnklfP?`F0}=)#3`iJ|Fd$(-!hnPU z2?G)aBn(IxkT4)&K*E580SN;V1|$qf7?3a^VL-xwgaHWy5(Xp;NEnbXAYnklfP?`F z0}=)#3`iJ|Fd$(-!hnPU2?G)aBn(IxkT4)&K*E580SN;V1|$qf7?3a^VL-xwgaHWy z5(Xp;*Z~8GOD_G13Ikfl{+ko&;M?B=CI<%uobUTj%0AmLBS*XtUxH=H$0oyFYB}XNa2wK)Gxq& z`+UCyCj{CXKe7Sv-#0v)n%5733h;mLZNK<)j}$3XNmD0n~gIS_qw{@~tc_t-%2 zf%p%H`}SQw&+;J~0NxLn%J| z&POf(Y`C&fo>K+yU_WqP4o})Bub}pE1OO3z!8zXXWWVriGhESakl`-=2tkno9L?m| zGXKjaFh>whR6m*_vw9$sz?S4A&#QlCI3EwfJFdd$%17wNKyWJr+AQBE+?@|+YXo;MKOmf=-@1Hcw+fuE z4cuw~WZQbb6iv8?TEGsy;R3=e5A=o$A50i_pcC`sSH~wGFh0Vt zw+W9#-7-#AxG<4DZGPsrm(G1Vjkk@a3<(`cH!{WK;hm z6!f2W=qi-TIx-Fs{&!=Lc#hh}Yc@Y+o{zd}puX{zIh#L27eu(AMsdu!3-E5}!z1)+ zj(IZ|>W-*5myEV~iN^owamjJ$mR#q0$Jt+T>19+t67Zg_vxWoQ6M2@Q<%{C{^%1z}_Q&t?ZKmSIe!uXZZs^t0$F{(Q_jH42cMZud*3%7+nj-|P0WMrv!_6%> zzrQ!!+;RbKgY_r`NT2ZRZg8PC>%kv60Nfhv;M=Rb`NNo>@>TE)tNOdcWxe3ty6<3y zTj6IQxPUP8>ur?p4v!GtZ#6!;q1VDX`-YnzzdAm9lpiu&Xi!)C&E;iXFBmL5f{%Qe zxx2o-$_o^3fnQ}`=K6tgdbI_9^#hvWC_rDr*ZeuIejqa(FQhMRfnPLRh@W?LF(u#kY;Q$j2nnh15{wu)w`*3ODA6KP!0nXct4mw*1xr;$t!GSwxCj555 zpD(>rV7I6jf9SiT`8UA@90?!H0(W5M4?qu48T0cT--MtDtMMFh74A!y1eaOS3%@DU z*9G@AGf1kRS1Q!U;e=qHo(&Y>GDd%ZJ5w*re;g^o&j<^gBQjEWK=_FKx(m*w2J;{l zI>_O?c-O>ec?B;;m6?YFT@t1&edO?L+%yj-g7Ztdz#oSza3P?Y*UziU#y$MWP#5p7 z6`VIp!NCHY5NICmk#w6XHFrRUOOB5Sx)3IIUda3refeFK;b)FN2M4-o?jT(fU74HD zDUiR*+(F^BT3&>nq}TU`6GZsqaOz99@)oGMJnt)i($X!<141d@n1SGt`~fpsEain> zS&e|c;n@R}w+R=pYV`qo)Gs2q!dCyR+9$w$EtnI|Lx2kdP}XO8n{cb|BfP3d`Cjmd z?)482&c~1NX!qjgQP1IgsF|s{>nW@8$A`LYI3K?pZXuhl3OLdQcs8zut1hVZ_-FJ1 zdJG>k{R(#p?rT+)UuNT``7U)Z!X{kC?^0iSg854Yrb3fZujUDCB|<2su{>y13;$|X zzk6O%qk1CE`Wb3eFQi#N5NQKx*6?gg_pNVQU1QiqSpDpEY3R3ky&jitT=y9G{|oUl Ay8r+H literal 0 HcmV?d00001 diff --git a/apps/desktop/resources/icon.png b/apps/desktop/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8d7f47ae8ee728ae4b5258c55f71e6538323a5e5 GIT binary patch literal 95883 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 z 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

N9Qh=9bE(9GN-fXfs%@) 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 K_(k3_^8WyFqlq{G literal 0 HcmV?d00001 diff --git a/apps/desktop/src/main/auto-launch.ts b/apps/desktop/src/main/auto-launch.ts new file mode 100644 index 00000000..ddde864c --- /dev/null +++ b/apps/desktop/src/main/auto-launch.ts @@ -0,0 +1,30 @@ +import { app } from 'electron'; +import log from 'electron-log/main'; + +/** + * 开机自启:基于系统登录项实现(Windows 写 HKCU\...\Run 注册表、mac 写「系统设置 → 登录项」), + * 无需管理员权限;系统登录项本身就是持久化存储,无需额外落盘记录状态。 + */ + +/** 读取当前开机自启状态(直接以系统登录项为准,避免自维护状态与系统不同步) */ +export function getAutoLaunchEnabled(): boolean { + // 开发模式下登录项指向 electron 开发二进制,无意义,统一视为关闭 + if (!app.isPackaged) return false; + return app.getLoginItemSettings().openAtLogin; +} + +/** + * 设置开机自启 + * --hidden 参数让开机自启时静默进托盘(主进程入口据此不弹主窗口),避免开机打扰 + */ +export function setAutoLaunchEnabled(enable: boolean): boolean { + if (!app.isPackaged) { + log.warn('开发模式不支持设置开机自启(登录项会错误指向开发二进制)'); + return false; + } + app.setLoginItemSettings({ + openAtLogin: enable, + args: enable ? ['--hidden'] : [], + }); + return getAutoLaunchEnabled(); +} diff --git a/apps/desktop/src/main/badge.ts b/apps/desktop/src/main/badge.ts new file mode 100644 index 00000000..86de4d3b --- /dev/null +++ b/apps/desktop/src/main/badge.ts @@ -0,0 +1,24 @@ +import { app, nativeImage } from 'electron'; + +import { getMainWindow } from './window'; + +/** + * 未读角标:按平台分支实现 + * - mac:Dock 图标原生支持文字角标,直接 setBadge + * - Windows:任务栏无原生数字角标,用「覆盖图标」实现(图由 preload 侧 canvas 绘制成 dataURL 传来, + * 因为主进程没有 DOM/canvas,在渲染侧画图是社区标准做法) + */ +export function applyBadge(count: number, image?: string): void { + if (process.platform === 'darwin') { + app.dock?.setBadge(count > 0 ? String(Math.min(count, 99)) : ''); + return; + } + const win = getMainWindow(); + if (!win) return; + if (count > 0 && image) { + win.setOverlayIcon(nativeImage.createFromDataURL(image), `${count} 条未读消息`); + } else { + // 数量归零或没有角标图时清除覆盖图标 + win.setOverlayIcon(null, ''); + } +} diff --git a/apps/desktop/src/main/config.ts b/apps/desktop/src/main/config.ts new file mode 100644 index 00000000..d2eef74a --- /dev/null +++ b/apps/desktop/src/main/config.ts @@ -0,0 +1,54 @@ +import { app } from 'electron'; +import { join } from 'node:path'; + +/** + * 桌面端全局配置 + * 打包模式下渲染层加载的是本地 dist,所有「按环境调整」的地址都收敛在这里, + * 并支持环境变量覆盖,便于联调测试环境时不改代码切换上游。 + */ + +/** 应用标识:必须与 electron-builder 的 appId 一致,否则 Windows 系统通知不显示 */ +export const APP_ID = 'com.xiaokang.xkadmin'; + +/** 自定义协议,注册为特权协议后 app://local/ 等价一个「本地 https 站点」(安全上下文) */ +export const APP_SCHEME = 'app'; +export const APP_HOST = 'local'; +export const APP_ORIGIN = `${APP_SCHEME}://${APP_HOST}`; +export const APP_INDEX_URL = `${APP_ORIGIN}/`; + +/** 开发模式加载 web-antd 的 vite dev server(端口对齐 apps/web-antd/.env.development 的 VITE_PORT=5666) */ +export const DEV_SERVER_URL = process.env.XK_DESKTOP_DEV_URL ?? 'http://localhost:5666'; + +/** + * /api/admin/* 的转发上游(仅打包后的 app:// 协议使用;dev 模式加载 vite dev server, + * 由 vite 代理转发到本地后端 localhost:18001,不经过这里) + * 地址来源:apps/web-antd/vite.config.ts 代理注释中的生产目标 https://api.xiaokang88.com/api/admin/ + */ +export const API_UPSTREAM = process.env.XK_DESKTOP_API_UPSTREAM ?? 'https://api.xiaokang88.com'; + +/** + * /im-api/* 的转发上游(转发规则对齐开发代理:/im-api/* → 上游 /api/*;同样仅打包后生效, + * dev 模式由 vite 代理转发到本地 IM localhost:12080) + * 地址来源:vite.config.ts 代理注释的生产目标 https://api.ws.g.xiaokang88.com/api/,与生产 WS 同主机 + */ +export const IM_UPSTREAM = process.env.XK_DESKTOP_IM_UPSTREAM ?? 'https://api.ws.g.xiaokang88.com'; + +/** + * 更新包下载页地址(与 electron-builder.config.cjs 的 publish.url 保持一致) + * 兼作 mac 未签名时「自动更新不可用 → 手动下载」的跳转页 + * TODO(待确认):更新包静态托管地址 + */ +export const DOWNLOAD_PAGE_URL = 'https://static.xiaokang88.com/xk-desktop/'; + +/** 全局快捷键:唤起/隐藏主窗口(CommandOrControl 在 Windows 映射 Ctrl、mac 映射 Cmd) */ +export const MAIN_WINDOW_SHORTCUT = 'CommandOrControl+Alt+X'; + +/** + * 解析运行时资源路径(图标等) + * 打包后 extraResources 位于 process.resourcesPath,开发时直接读工程 resources 目录 + */ +export function getResourcePath(name: string): string { + return app.isPackaged + ? join(process.resourcesPath, name) + : join(app.getAppPath(), 'resources', name); +} diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts new file mode 100644 index 00000000..c5cd6a46 --- /dev/null +++ b/apps/desktop/src/main/index.ts @@ -0,0 +1,81 @@ +import { app, Menu } from 'electron'; +import log from 'electron-log/main'; + +import { APP_ID } from './config'; +import { registerIpcHandlers } from './ipc'; +import { registerAppProtocol, registerSchemePrivileges } from './protocol'; +import { registerGlobalShortcut, unregisterGlobalShortcut } from './shortcut'; +import { createTray } from './tray'; +import { initUpdater } from './updater'; +import { createMainWindow, getMainWindow, markQuitting, showMainWindow } from './window'; + +/** + * 主进程入口:负责应用生命周期编排 + * 启动顺序:单实例锁 → 特权协议注册(必须在 ready 前)→ ready 后依次初始化 + * 协议服务 / IPC / 主窗口 / 托盘 / 全局快捷键 / 自动更新 + */ + +// 显式设置应用名:userData / 日志目录会随之落到 %APPDATA%\萧康云医, +// 不设置的话 Electron 会取 package.json 的 name(@vben/desktop),生成嵌套目录不利于运维排查 +app.setName('萧康云医'); + +// Windows 通知归属与任务栏分组标识,必须与 electron-builder 的 appId 一致, +// 否则系统通知不显示或显示为「electron.app.Electron」 +app.setAppUserModelId(APP_ID); + +// 单实例锁:避免多开导致托盘、自动更新、本地设置互相干扰 +if (app.requestSingleInstanceLock()) { + // 特权协议必须在 app ready 之前注册,否则 app:// 无法成为安全上下文 + registerSchemePrivileges(); + + // 重复启动(用户再次双击图标)时唤起已有窗口 + app.on('second-instance', () => showMainWindow()); + + app.whenReady().then(() => { + // electron-log 初始化:主进程日志落 userData/logs,便于排查线上问题 + log.initialize(); + + // mac 必须保留应用菜单,否则 Cmd+C/V/Q 等系统快捷键失效;Windows 隐藏菜单栏 + if (process.platform === 'darwin') { + Menu.setApplicationMenu( + Menu.buildFromTemplate([ + { role: 'appMenu' }, + { role: 'editMenu' }, + { role: 'viewMenu' }, + { role: 'windowMenu' }, + ]), + ); + } else { + Menu.setApplicationMenu(null); + } + + registerAppProtocol(); + registerIpcHandlers(); + + // 开机自启带 --hidden 参数:静默启动进托盘,不弹主窗口打扰用户 + const startHidden = process.argv.includes('--hidden'); + createMainWindow({ startHidden }); + + createTray(); + registerGlobalShortcut(); + initUpdater(); + }); + + // mac 点击 Dock 图标恢复窗口(关窗只是隐藏,符合平台惯例) + app.on('activate', () => { + if (getMainWindow()) showMainWindow(); + }); + + // 常驻托盘模式:窗口全部关闭也不退出(close 已被拦截为隐藏,这里是兜底),退出走托盘菜单/Cmd+Q + app.on('window-all-closed', () => { + // 故意留空:不调用 app.quit() + }); + + // 任何退出路径(托盘退出 / Cmd+Q / 自动更新安装)都先放行窗口 close 拦截 + app.on('before-quit', () => markQuitting()); + + // 退出前注销全局快捷键,避免残留占用 + app.on('will-quit', () => unregisterGlobalShortcut()); +} else { + app.quit(); +} diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts new file mode 100644 index 00000000..11cbd4af --- /dev/null +++ b/apps/desktop/src/main/ipc.ts @@ -0,0 +1,117 @@ +import { app, ipcMain, Notification } from 'electron'; + +import { getAutoLaunchEnabled, setAutoLaunchEnabled } from './auto-launch'; +import { applyBadge } from './badge'; +import { printHtml, type PrintHtmlOptions } from './printer'; +import { getSettings, updateSettings, type DesktopSettings } from './settings'; +import { refreshTrayMenu } from './tray'; +import { checkForUpdates } from './updater'; +import { getMainWindow, isTrustedPageUrl, showMainWindow } from './window'; + +/** + * IPC 汇总:preload 暴露的 desktopAPI 全部经由这里进入主进程 + * 所有 handler 先校验调用方来源(app:// 或开发服务器), + * 防止页面内嵌第三方 iframe 等场景越权调用桌面能力 + */ + +/** 校验 IPC 调用来源,不可信来源直接抛错拒绝 */ +function assertTrustedSender(event: Electron.IpcMainInvokeEvent): void { + const url = event.senderFrame?.url ?? ''; + if (!isTrustedPageUrl(url)) { + throw new Error(`拒绝来自不可信来源的 IPC 调用: ${url}`); + } +} + +/** 系统通知参数(route 用于点击通知后让渲染端跳转对应页面) */ +interface NotifyPayload { + title: string; + body: string; + route?: string; +} + +export function registerIpcHandlers(): void { + // 应用版本号(渲染端「关于/设置」展示用) + ipcMain.handle('desktop:get-version', (event) => { + assertTrustedSender(event); + return app.getVersion(); + }); + + // 系统通知:点击后唤起主窗口,并把目标路由回传渲染端执行 router.push + ipcMain.handle('desktop:notify', (event, payload: NotifyPayload) => { + assertTrustedSender(event); + if (!Notification.isSupported()) return false; + const notification = new Notification({ + title: payload.title, + body: payload.body, + }); + notification.on('click', () => { + showMainWindow(); + if (payload.route) { + getMainWindow()?.webContents.send('desktop:navigate', payload.route); + } + }); + notification.show(); + return true; + }); + + // 未读角标(mac Dock 数字 / Windows 任务栏覆盖图标,图由 preload canvas 绘制) + ipcMain.handle('desktop:set-badge', (event, payload: { count: number; image?: string }) => { + assertTrustedSender(event); + applyBadge(payload.count, payload.image); + }); + + // 任务栏闪烁提醒:仅窗口未聚焦时触发(聚焦状态下闪烁是干扰),聚焦后由窗口 focus 事件自动清除 + ipcMain.handle('desktop:flash-frame', (event) => { + assertTrustedSender(event); + const win = getMainWindow(); + if (win && !win.isFocused()) win.flashFrame(true); + }); + + // 开机自启读取/设置(设置后同步刷新托盘菜单勾选态) + ipcMain.handle('desktop:get-auto-launch', (event) => { + assertTrustedSender(event); + return getAutoLaunchEnabled(); + }); + ipcMain.handle('desktop:set-auto-launch', (event, enable: boolean) => { + assertTrustedSender(event); + const result = setAutoLaunchEnabled(enable); + refreshTrayMenu(); + return result; + }); + + // 打印机列表(渲染端打印设置下拉用) + ipcMain.handle('desktop:get-printers', async (event) => { + assertTrustedSender(event); + const win = getMainWindow(); + if (!win) return []; + return await win.webContents.getPrintersAsync(); + }); + + // 静默打印:渲染端拼好完整 HTML 传入,主进程隐藏窗口出纸 + ipcMain.handle('desktop:print-html', (event, options: PrintHtmlOptions) => { + assertTrustedSender(event); + return printHtml(options); + }); + + // 桌面端设置(打印机选择、静默开关等)读取/增量更新 + ipcMain.handle('desktop:get-settings', (event) => { + assertTrustedSender(event); + return getSettings(); + }); + ipcMain.handle('desktop:set-settings', (event, patch: Partial) => { + assertTrustedSender(event); + return updateSettings(patch); + }); + + // 手动检查更新(渲染端设置页/关于入口) + ipcMain.handle('desktop:check-update', (event) => { + assertTrustedSender(event); + checkForUpdates(true); + }); + + // 渲染端主动唤起主窗口(如页内提醒的「查看」动作) + ipcMain.handle('desktop:focus-window', (event) => { + assertTrustedSender(event); + showMainWindow(); + }); +} diff --git a/apps/desktop/src/main/printer.ts b/apps/desktop/src/main/printer.ts new file mode 100644 index 00000000..8dfcd29d --- /dev/null +++ b/apps/desktop/src/main/printer.ts @@ -0,0 +1,75 @@ +import { unlink, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { app, BrowserWindow } from 'electron'; +import log from 'electron-log/main'; + +/** + * 静默打印:隐藏窗口加载 HTML → webContents.print 直接出纸 + * 为什么用隐藏窗口:渲染层的处方打印本来就是拼好的完整 HTML(含内联样式), + * 加载到独立隐藏窗口打印可以完全复用现有模板,且不阻塞/不干扰主窗口。 + * 为什么落临时文件而不是 data: URL:处方 HTML 可能较大,data URL 有长度上限风险, + * 临时文件 loadFile 对任意大小都稳定,打完即删。 + */ + +export interface PrintHtmlOptions { + /** 完整的打印 HTML(含样式),由渲染层拼好传入 */ + html: string; + /** 目标打印机名称;不传时使用系统默认打印机 */ + deviceName?: string; + /** 是否静默打印(true 不弹系统打印对话框) */ + silent?: boolean; +} + +export interface PrintResult { + ok: boolean; + message?: string; +} + +/** + * 执行打印,返回是否成功与失败原因(渲染层据此提示用户) + */ +export async function printHtml(options: PrintHtmlOptions): Promise { + const { html, deviceName, silent = true } = options; + // 临时文件带时间戳防并发打印互相覆盖 + const tempFile = join(app.getPath('temp'), `xk-print-${Date.now()}-${Math.random().toString(36).slice(2)}.html`); + await writeFile(tempFile, html, 'utf8'); + + return new Promise((resolve) => { + let settled = false; + const win = new BrowserWindow({ + show: false, + webPreferences: { sandbox: true }, + }); + + /** 统一收尾:销毁隐藏窗口、清理临时文件、只 resolve 一次 */ + const finish = (ok: boolean, message?: string) => { + if (settled) return; + settled = true; + if (!win.isDestroyed()) win.destroy(); + unlink(tempFile).catch(() => {}); + if (!ok) log.warn(`静默打印失败: ${message}`); + resolve({ ok, message }); + }; + + win.webContents.once('did-finish-load', () => { + win.webContents.print( + { + silent, + deviceName: deviceName || undefined, + printBackground: true, + }, + (success, failureReason) => finish(success, success ? undefined : failureReason), + ); + }); + + win.webContents.once('did-fail-load', (_event, _code, desc) => finish(false, desc)); + + // 兜底超时:防止打印机脱机等场景下隐藏窗口泄漏 + setTimeout(() => finish(false, '打印超时(请检查打印机状态)'), 60_000); + + win.loadFile(tempFile).catch((error: unknown) => { + finish(false, error instanceof Error ? error.message : String(error)); + }); + }); +} diff --git a/apps/desktop/src/main/protocol.ts b/apps/desktop/src/main/protocol.ts new file mode 100644 index 00000000..c06cb8eb --- /dev/null +++ b/apps/desktop/src/main/protocol.ts @@ -0,0 +1,108 @@ +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 { + 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); + }); +} diff --git a/apps/desktop/src/main/settings.ts b/apps/desktop/src/main/settings.ts new file mode 100644 index 00000000..d93a6609 --- /dev/null +++ b/apps/desktop/src/main/settings.ts @@ -0,0 +1,67 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { app } from 'electron'; +import log from 'electron-log/main'; + +/** + * 桌面端本地设置持久化(userData/desktop-settings.json) + * 为什么不用 electron-store:目前只需存打印机等少量键值,直接读写 JSON 文件即可, + * 避免引入额外依赖(electron-store 新版为纯 ESM,与 CJS 主进程产物不兼容,还会拖入 ajv 等重依赖)。 + */ + +/** 桌面端设置项定义 */ +export interface DesktopSettings { + /** 静默打印使用的打印机名称,空串表示未选择 */ + printerName: string; + /** 是否启用静默打印(true 时打印处方不弹系统打印对话框) */ + silentPrint: boolean; +} + +const DEFAULT_SETTINGS: DesktopSettings = { + printerName: '', + silentPrint: false, +}; + +/** 内存缓存,避免每次 IPC 调用都读磁盘 */ +let cache: DesktopSettings | null = null; + +/** 设置文件落在 userData 目录,随应用卸载数据目录一起清理 */ +function getSettingsFilePath(): string { + return join(app.getPath('userData'), 'desktop-settings.json'); +} + +/** + * 读取设置:文件不存在或解析失败时回退默认值,保证任何情况下不影响应用启动 + */ +export function getSettings(): DesktopSettings { + if (cache) return cache; + try { + const filePath = getSettingsFilePath(); + if (existsSync(filePath)) { + // JSON.parse 返回 any,显式收窄成 Partial 再与默认值合并,保证字段完整 + const parsed = JSON.parse(readFileSync(filePath, 'utf8')) as Partial; + const merged: DesktopSettings = { ...DEFAULT_SETTINGS, ...parsed }; + cache = merged; + return merged; + } + } catch (error) { + log.warn('读取桌面端设置失败,回退默认值', error); + } + cache = { ...DEFAULT_SETTINGS }; + return cache; +} + +/** + * 增量更新设置并写盘:写盘失败只告警(内存态仍生效),下次成功写入时自动恢复 + */ +export function updateSettings(patch: Partial): DesktopSettings { + const next = { ...getSettings(), ...patch }; + cache = next; + try { + writeFileSync(getSettingsFilePath(), JSON.stringify(next, null, 2), 'utf8'); + } catch (error) { + log.warn('写入桌面端设置失败', error); + } + return next; +} diff --git a/apps/desktop/src/main/shortcut.ts b/apps/desktop/src/main/shortcut.ts new file mode 100644 index 00000000..f4d37bfe --- /dev/null +++ b/apps/desktop/src/main/shortcut.ts @@ -0,0 +1,21 @@ +import { globalShortcut } from 'electron'; +import log from 'electron-log/main'; + +import { MAIN_WINDOW_SHORTCUT } from './config'; +import { toggleMainWindow } from './window'; + +/** + * 全局快捷键:任何界面下按 CommandOrControl+Alt+X 快速唤起/隐藏主窗口 + * 注册失败(快捷键被其他应用占用)只记录警告,不影响应用启动 + */ +export function registerGlobalShortcut(): void { + const ok = globalShortcut.register(MAIN_WINDOW_SHORTCUT, () => toggleMainWindow()); + if (!ok) { + log.warn(`全局快捷键 ${MAIN_WINDOW_SHORTCUT} 注册失败(可能被其他应用占用)`); + } +} + +/** 应用退出前注销全部快捷键,避免残留占用 */ +export function unregisterGlobalShortcut(): void { + globalShortcut.unregisterAll(); +} diff --git a/apps/desktop/src/main/tray.ts b/apps/desktop/src/main/tray.ts new file mode 100644 index 00000000..7fc1b0a5 --- /dev/null +++ b/apps/desktop/src/main/tray.ts @@ -0,0 +1,78 @@ +import { app, Menu, nativeImage, Tray } from 'electron'; +import log from 'electron-log/main'; + +import { getAutoLaunchEnabled, setAutoLaunchEnabled } from './auto-launch'; +import { getResourcePath } from './config'; +import { checkForUpdates } from './updater'; +import { markQuitting, showMainWindow } from './window'; + +/** + * 系统托盘:应用常驻入口 + * - Windows 单击托盘图标恢复主窗口;mac 单击弹出菜单(跟随平台惯例) + * - 菜单:显示主窗口 / 开机自启开关 / 检查更新 / 退出 + * 注意:托盘实例必须保持模块级引用,否则会被 GC 导致图标消失(Electron 经典坑) + */ + +let tray: Tray | null = null; + +/** 构建托盘菜单:开机自启是动态勾选项,每次状态变化后需重建菜单 */ +function buildTrayMenu(): Menu { + return Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => showMainWindow(), + }, + { type: 'separator' }, + { + label: '开机自启', + type: 'checkbox', + checked: getAutoLaunchEnabled(), + // 开发模式登录项无意义,置灰避免误操作 + enabled: app.isPackaged, + click: (menuItem) => { + setAutoLaunchEnabled(menuItem.checked); + refreshTrayMenu(); + }, + }, + { + label: '检查更新', + click: () => checkForUpdates(true), + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + // 先标记退出,放行主窗口 close 拦截,再走正常退出流程 + markQuitting(); + app.quit(); + }, + }, + ]); +} + +/** 刷新托盘菜单(开机自启状态从渲染端 IPC 修改后也要同步勾选态) */ +export function refreshTrayMenu(): void { + tray?.setContextMenu(buildTrayMenu()); +} + +/** + * 创建系统托盘 + * 图标用打包的应用图标缩放(mac 菜单栏建议 18px、Windows 托盘 16px 的倍数由系统处理) + * TODO(待确认):正式图标到位后,mac 建议换单色 Template 图以适配深浅菜单栏 + */ +export function createTray(): void { + try { + const icon = nativeImage.createFromPath(getResourcePath('icon.png')); + const trayIcon = process.platform === 'darwin' ? icon.resize({ width: 18, height: 18 }) : icon.resize({ width: 16, height: 16 }); + tray = new Tray(trayIcon); + tray.setToolTip('萧康云医'); + tray.setContextMenu(buildTrayMenu()); + // Windows 单击直接唤起主窗口;mac 上设置了 contextMenu 后单击默认弹菜单,不重复绑定 + if (process.platform === 'win32') { + tray.on('click', () => showMainWindow()); + } + } catch (error) { + // 托盘创建失败(图标缺失等)不阻塞应用启动,仅损失常驻入口 + log.error('创建系统托盘失败', error); + } +} diff --git a/apps/desktop/src/main/updater.ts b/apps/desktop/src/main/updater.ts new file mode 100644 index 00000000..dc5a3ad3 --- /dev/null +++ b/apps/desktop/src/main/updater.ts @@ -0,0 +1,139 @@ +import { app, dialog, shell } from 'electron'; +import log from 'electron-log/main'; +import { autoUpdater } from 'electron-updater'; + +import { DOWNLOAD_PAGE_URL } from './config'; +import { getMainWindow, markQuitting, showMainWindow } from './window'; + +/** + * 自动更新(electron-updater + generic 静态托管) + * - 启动 10 秒后自动检查一次 + 每 4 小时轮询 + 托盘/渲染端手动触发 + * - autoDownload:发现新版本自动后台下载,进度事件转发渲染端展示 + * - 下载完成弹窗询问「立即重启安装 / 退出时自动安装」 + * - mac 未签名包 electron-updater 签名校验会报错:捕获后降级为「打开下载页手动安装」 + */ + +/** 手动检查标记:手动触发时才提示「已是最新版本」,自动轮询保持安静 */ +let isManualCheck = false; +/** 检查中防重入 */ +let isChecking = false; + +/** 把更新事件转发给渲染端(web 侧在 basic.vue 里做轻量提示) */ +function sendToRenderer(type: string, data?: unknown): void { + getMainWindow()?.webContents.send('desktop:update-event', { type, data }); +} + +/** 判断是否 mac 未签名导致的更新失败(这类错误重试无意义,直接引导手动下载) */ +function isMacSignatureError(message: string): boolean { + return ( + process.platform === 'darwin' && + /code signature|not signed|codesign|could not get code signature/i.test(message) + ); +} + +/** + * 初始化自动更新:仅打包后生效(开发模式没有 app-update.yml,检查必然失败) + */ +export function initUpdater(): void { + if (!app.isPackaged) return; + + autoUpdater.logger = log; + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + + autoUpdater.on('update-available', (info) => { + log.info(`发现新版本: ${info.version}`); + sendToRenderer('available', { version: info.version }); + }); + + autoUpdater.on('update-not-available', () => { + sendToRenderer('not-available'); + if (isManualCheck) { + dialog.showMessageBox({ + type: 'info', + title: '检查更新', + message: `当前已是最新版本(v${app.getVersion()})`, + }); + } + isManualCheck = false; + }); + + autoUpdater.on('download-progress', (progress) => { + sendToRenderer('progress', { percent: Math.round(progress.percent) }); + }); + + autoUpdater.on('update-downloaded', async (info) => { + sendToRenderer('downloaded', { version: info.version }); + // 唤起窗口再弹对话框,避免用户看不到安装确认 + showMainWindow(); + const { response } = await dialog.showMessageBox({ + type: 'question', + title: '发现新版本', + message: `新版本 v${info.version} 已下载完成`, + detail: '立即重启安装,或在退出应用时自动安装。', + buttons: ['立即重启安装', '退出时自动安装'], + defaultId: 0, + cancelId: 1, + }); + if (response === 0) { + // 放行窗口 close 拦截,否则 quitAndInstall 会被「关闭进托盘」逻辑挡住 + markQuitting(); + autoUpdater.quitAndInstall(); + } + }); + + autoUpdater.on('error', async (error) => { + const message = error?.message ?? String(error); + log.error('自动更新失败', message); + sendToRenderer('error', { message }); + // mac 未签名:自动更新走不通,引导用户手动下载安装 + if (isMacSignatureError(message)) { + const { response } = await dialog.showMessageBox({ + type: 'info', + title: '检查更新', + message: '当前安装包不支持自动更新', + detail: '请前往下载页获取最新版本手动安装。', + buttons: ['打开下载页', '取消'], + defaultId: 0, + cancelId: 1, + }); + if (response === 0) shell.openExternal(DOWNLOAD_PAGE_URL); + } else if (isManualCheck) { + dialog.showMessageBox({ + type: 'warning', + title: '检查更新', + message: '检查更新失败,请稍后重试', + detail: message, + }); + } + isManualCheck = false; + isChecking = false; + }); + + // 启动延时自动检查一次 + 每 4 小时轮询(内部工具常开着,轮询保证及时收到发版) + setTimeout(() => checkForUpdates(false), 10_000); + setInterval(() => checkForUpdates(false), 4 * 60 * 60 * 1000); +} + +/** + * 触发一次更新检查 + * @param manual 是否手动触发(手动时「已是最新/失败」都会弹窗反馈,自动轮询保持安静) + */ +export async function checkForUpdates(manual: boolean): Promise { + if (!app.isPackaged) { + if (manual) { + dialog.showMessageBox({ type: 'info', title: '检查更新', message: '开发模式不支持检查更新' }); + } + return; + } + if (isChecking) return; + isChecking = true; + isManualCheck = manual; + try { + await autoUpdater.checkForUpdates(); + } catch { + // 错误已在 error 事件里统一处理 + } finally { + isChecking = false; + } +} diff --git a/apps/desktop/src/main/window.ts b/apps/desktop/src/main/window.ts new file mode 100644 index 00000000..6691c2c8 --- /dev/null +++ b/apps/desktop/src/main/window.ts @@ -0,0 +1,212 @@ +import { join } from 'node:path'; + +import { app, BrowserWindow, Notification, session, shell, systemPreferences } from 'electron'; +import log from 'electron-log/main'; + +import { APP_INDEX_URL, APP_ORIGIN, DEV_SERVER_URL, getResourcePath } from './config'; + +/** + * 主窗口管理 + * - 加载目标:开发模式加载 web-antd 的 vite dev server(未起服务时自动重试);打包模式加载 app://local/ + * - 安全基线:contextIsolation + sandbox 开启,webSecurity 不关闭;window.open 一律交给系统浏览器 + * - 关闭行为:双平台都是「关窗隐藏、应用常驻」(Windows 走托盘、mac 走 Dock),真正退出走托盘菜单/Cmd+Q + * - 权限控制:只放行业务需要的媒体(IM 音视频)/ 通知 / 剪贴板 / 全屏,其余默认拒绝 + */ + +let mainWindow: BrowserWindow | null = null; +/** 标记「真正退出」:常驻模式下 close 只是隐藏,只有 quit 流程才放行关闭 */ +let isQuitting = false; +/** Windows 首次关窗提示「已最小化到托盘」,只提示一次避免打扰 */ +let hasShownHideTip = false; +/** 加载失败重试定时器,避免叠加多个重试 */ +let retryTimer: NodeJS.Timeout | null = null; + +/** 主窗口要加载的页面地址:打包后走自定义协议,开发走 dev server */ +const PAGE_URL = app.isPackaged ? APP_INDEX_URL : DEV_SERVER_URL; + +export function markQuitting(): void { + isQuitting = true; +} + +export function getMainWindow(): BrowserWindow | null { + return mainWindow; +} + +/** 判断 URL 是否属于信任的页面来源(本地协议或开发服务器),IPC 与权限校验共用 */ +export function isTrustedPageUrl(url: string): boolean { + return url.startsWith(APP_ORIGIN) || url.startsWith(DEV_SERVER_URL); +} + +/** + * 会话级权限控制:白名单内且来源可信才放行 + * media 供 IM 音视频通话;notifications 供 HTML5 Notification(业务主要走主进程通知,这里兜底) + */ +function setupPermissionHandlers(): void { + const allowedPermissions = new Set([ + 'media', + 'notifications', + 'clipboard-read', + 'clipboard-sanitized-write', + 'fullscreen', + ]); + + session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { + const trusted = isTrustedPageUrl(webContents.getURL()); + if (!trusted || !allowedPermissions.has(permission)) { + callback(false); + return; + } + // mac 首次使用摄像头/麦克风必须先过系统 TCC 授权弹窗,否则页面侧拿不到设备 + if (permission === 'media' && process.platform === 'darwin') { + Promise.all([ + systemPreferences.askForMediaAccess('microphone'), + systemPreferences.askForMediaAccess('camera'), + ]) + .then(() => callback(true)) + // TCC 弹窗异常时仍放行,让页面侧走自己的错误提示 + .catch(() => callback(true)); + return; + } + callback(true); + }); + + session.defaultSession.setPermissionCheckHandler((_webContents, permission, requestingOrigin) => { + return allowedPermissions.has(permission) && isTrustedPageUrl(requestingOrigin); + }); +} + +/** 加载失败时的内置错误页(数据 URL 形式,不依赖任何本地文件) */ +function buildErrorPageUrl(reason: string): string { + const html = ` + + + + 加载失败 + + + +

+
页面加载失败,正在自动重试…
+
请检查网络连接(${reason})
+
+ +`; + return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; +} + +/** + * 创建主窗口 + * @param options.startHidden 开机自启(--hidden)场景下静默启动,不弹窗打扰用户 + */ +export function createMainWindow(options: { startHidden: boolean }): BrowserWindow { + setupPermissionHandlers(); + + mainWindow = new BrowserWindow({ + width: 1440, + height: 900, + minWidth: 1100, + minHeight: 700, + show: false, + autoHideMenuBar: true, + title: '萧康云医', + // Windows/Linux 窗口与任务栏图标;mac 使用打包进 app 的 icns,无需在此指定 + icon: process.platform === 'darwin' ? undefined : getResourcePath('icon.png'), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + spellcheck: false, + }, + }); + + // 首次渲染完成再显示,避免白屏闪烁;静默启动场景保持隐藏(用户从托盘/快捷键唤起) + mainWindow.once('ready-to-show', () => { + if (!options.startHidden) mainWindow?.show(); + }); + + // window.open / target=_blank:外链一律交给系统默认浏览器,不在应用内开新窗(安全 + 体验) + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith('http://') || url.startsWith('https://')) { + shell.openExternal(url); + } + return { action: 'deny' }; + }); + + // 页面内导航限制在信任来源内,跳外部地址时转交系统浏览器 + mainWindow.webContents.on('will-navigate', (event, url) => { + if (isTrustedPageUrl(url)) return; + event.preventDefault(); + if (url.startsWith('http://') || url.startsWith('https://')) { + shell.openExternal(url); + } + }); + + // 加载失败(dev server 未启动 / 断网等)时展示内置错误页并定时重试 + mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDesc, _url, isMainFrame) => { + if (!isMainFrame) return; + // ERR_ABORTED(-3)是导航被主动取消(如重复 loadURL),不算失败 + if (errorCode === -3) return; + log.warn(`主窗口加载失败: ${errorCode} ${errorDesc}`); + mainWindow?.loadURL(buildErrorPageUrl(errorDesc || String(errorCode))); + if (retryTimer) clearTimeout(retryTimer); + // 开发模式快速重试等 vite 起服务;打包模式放慢节奏等网络恢复 + retryTimer = setTimeout(() => mainWindow?.loadURL(PAGE_URL), app.isPackaged ? 5000 : 1500); + }); + + // 窗口聚焦即清除任务栏闪烁提醒(配合新消息 flashFrame) + mainWindow.on('focus', () => mainWindow?.flashFrame(false)); + + // F12 切换 DevTools:内部管理系统保留排障入口 + mainWindow.webContents.on('before-input-event', (_event, input) => { + if (input.type === 'keyDown' && input.key === 'F12') { + mainWindow?.webContents.toggleDevTools(); + } + }); + + // 关闭行为:双平台都拦截为隐藏(Windows 常驻托盘 / mac 常驻 Dock),退出流程放行 + mainWindow.on('close', (event) => { + if (isQuitting) return; + event.preventDefault(); + mainWindow?.hide(); + // Windows 首次隐藏时提示托盘位置,避免用户以为应用被关闭 + if (process.platform === 'win32' && !hasShownHideTip && Notification.isSupported()) { + hasShownHideTip = true; + new Notification({ + title: '萧康云医仍在运行', + body: '应用已最小化到系统托盘,点击托盘图标可重新打开窗口', + }).show(); + } + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); + + mainWindow.loadURL(PAGE_URL); + return mainWindow; +} + +/** 唤起主窗口:还原最小化、显示并聚焦(通知点击 / 托盘 / 第二实例 / 快捷键共用) */ +export function showMainWindow(): void { + if (!mainWindow) return; + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.show(); + mainWindow.focus(); +} + +/** 切换主窗口显隐(全局快捷键使用) */ +export function toggleMainWindow(): void { + if (!mainWindow) return; + if (mainWindow.isVisible() && mainWindow.isFocused()) { + mainWindow.hide(); + } else { + showMainWindow(); + } +} diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts new file mode 100644 index 00000000..dcfb3446 --- /dev/null +++ b/apps/desktop/src/preload/index.ts @@ -0,0 +1,102 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +/** + * 预加载脚本:通过 contextBridge 向渲染层暴露受控的 desktopAPI + * - 渲染层(web-antd)通过 window.desktopAPI 使用桌面能力;浏览器环境没有该对象, + * 前端封装层(apps/web-antd/src/util/desktop.ts)会自动 no-op 降级 + * - sandbox 模式下本文件必须是 CJS 产物(electron-vite 已按包配置输出 CJS), + * 且只能使用 Electron 提供的受限 API(contextBridge / ipcRenderer 等) + */ + +/** + * Windows 任务栏角标图:用 canvas 画「红底白字」数字返回 dataURL + * 为什么在 preload 画:主进程没有 DOM/canvas;preload 运行在渲染进程内有 DOM, + * 由主进程 setOverlayIcon 消费,这是 Windows 数字角标的社区标准做法 + */ +function drawBadgeImage(count: number): string | undefined { + try { + const size = 32; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + // 红色圆底 + ctx.beginPath(); + ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); + ctx.fillStyle = '#f5222d'; + ctx.fill(); + // 白色数字,超过 99 显示 99+ + const text = count > 99 ? '99+' : String(count); + ctx.fillStyle = '#ffffff'; + ctx.font = `bold ${text.length > 2 ? 13 : 18}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(text, size / 2, size / 2 + 1); + return canvas.toDataURL('image/png'); + } catch { + // canvas 不可用时退化为无图(主进程会清除角标),不影响业务 + return undefined; + } +} + +/** 暴露给渲染层的桌面能力集合,字段语义与 web 侧 desktop.ts 封装一一对应 */ +const desktopAPI = { + /** 渲染层判断是否运行在桌面端的唯一标记 */ + isElectron: true as const, + /** 当前平台:win32 / darwin / linux */ + platform: process.platform, + /** 获取桌面端版本号(关于/设置展示用) */ + getVersion: (): Promise => ipcRenderer.invoke('desktop:get-version'), + /** 发送系统通知;route 为点击通知后要跳转的前端路由 */ + notify: (options: { title: string; body: string; route?: string }): Promise => + ipcRenderer.invoke('desktop:notify', options), + /** 设置未读角标数量(0 表示清除);Windows 角标图在此侧 canvas 生成 */ + setBadge: (count: number): Promise => + ipcRenderer.invoke('desktop:set-badge', { + count, + image: count > 0 && process.platform === 'win32' ? drawBadgeImage(count) : undefined, + }), + /** 任务栏闪烁提醒(窗口未聚焦时生效,聚焦后自动清除) */ + flashFrame: (): Promise => ipcRenderer.invoke('desktop:flash-frame'), + /** 读取开机自启状态 */ + getAutoLaunch: (): Promise => ipcRenderer.invoke('desktop:get-auto-launch'), + /** 设置开机自启,返回设置后的实际状态 */ + setAutoLaunch: (enable: boolean): Promise => + ipcRenderer.invoke('desktop:set-auto-launch', enable), + /** 获取系统打印机列表(打印设置下拉用) */ + getPrinters: (): Promise> => + ipcRenderer.invoke('desktop:get-printers'), + /** 静默打印:传入拼好的完整 HTML,主进程隐藏窗口出纸 */ + printHTML: (options: { + html: string; + deviceName?: string; + silent?: boolean; + }): Promise<{ ok: boolean; message?: string }> => + ipcRenderer.invoke('desktop:print-html', options), + /** 读取桌面端设置(打印机名称、静默打印开关等) */ + getSettings: (): Promise<{ printerName: string; silentPrint: boolean }> => + ipcRenderer.invoke('desktop:get-settings'), + /** 增量更新桌面端设置,返回更新后的完整设置 */ + setSettings: ( + patch: Partial<{ printerName: string; silentPrint: boolean }>, + ): Promise<{ printerName: string; silentPrint: boolean }> => + ipcRenderer.invoke('desktop:set-settings', patch), + /** 手动检查更新(结果由主进程弹窗反馈) */ + checkUpdate: (): Promise => ipcRenderer.invoke('desktop:check-update'), + /** 唤起并聚焦主窗口 */ + focusWindow: (): Promise => ipcRenderer.invoke('desktop:focus-window'), + /** 监听「通知点击跳转」事件:主进程通知被点击后回传目标路由,渲染层执行 router.push */ + onNavigate: (callback: (route: string) => void): void => { + ipcRenderer.on('desktop:navigate', (_event, route: string) => callback(route)); + }, + /** 监听更新事件(available / progress / downloaded / not-available / error),渲染层做轻量提示 */ + onUpdateEvent: (callback: (payload: { type: string; data?: unknown }) => void): void => { + ipcRenderer.on('desktop:update-event', (_event, payload) => callback(payload)); + }, +}; + +contextBridge.exposeInMainWorld('desktopAPI', desktopAPI); + +/** 供 web 侧类型声明引用(apps/web-antd/src/types/desktop.d.ts 与此保持同步) */ +export type DesktopAPI = typeof desktopAPI; diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 00000000..b49951dc --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "useUnknownInCatchVariables": true + }, + "include": ["src", "electron.vite.config.ts"] +} diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index acb384ca..3a5965db 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -52,6 +52,7 @@ "js-base64": "^3.7.7", "lodash-es": "^4.17.21", "lucide-vue-next": "^0.487.0", + "lunar-typescript": "^1.8.6", "markdown-it": "^14.1.0", "md-editor-v3": "^5.4.5", "pinia": "catalog:", diff --git a/apps/web-antd/public/audio/register-notify.mp3 b/apps/web-antd/public/audio/register-notify.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0493d6f1242649ddfbb858bb3f8ddbd4d17490b2 GIT binary patch literal 14210 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 literal 0 HcmV?d00001 diff --git a/apps/web-antd/src/components/form/components/store-multi-search.vue b/apps/web-antd/src/components/form/components/store-multi-search.vue index 5dd49b80..3f0b1cd1 100644 --- a/apps/web-antd/src/components/form/components/store-multi-search.vue +++ b/apps/web-antd/src/components/form/components/store-multi-search.vue @@ -14,6 +14,8 @@ import { watch, } from 'vue'; +import { useUserStore } from '@vben/stores'; + import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue'; import { useDebounceFn, useVModel } from '@vueuse/core'; import { Empty, Input, Spin, Tag } from 'ant-design-vue'; @@ -50,10 +52,49 @@ const props = withDefaults( }, ); +/** + * 模块级搜索结果缓存(所有实例共享):key = `${userId}|${storeType}|${keyword}` + * 门店数据变更低频,TTL 内相同关键词直接命中缓存零等待,避免每敲一个字都等接口转圈。 + * key 必须拼登录用户 id:不同账号数据权限不同(店管只能搜到本店可见门店), + * 切换账号后不允许在 TTL 内命中上一账号的结果,拼 id 后 key 天然隔离 + */ +const searchResultCache = new Map< + string, + { at: number; items: StoreSearchItem[] } +>(); +const SEARCH_CACHE_TTL = 5 * 60 * 1000; +const SEARCH_CACHE_MAX = 200; + +function readSearchCache(key: string): null | StoreSearchItem[] { + const hit = searchResultCache.get(key); + if (!hit) return null; + if (Date.now() - hit.at > SEARCH_CACHE_TTL) { + searchResultCache.delete(key); + return null; + } + return hit.items; +} + +function writeSearchCache(key: string, items: StoreSearchItem[]) { + // 简单容量控制:超限整体清空(低频触发,无需 LRU) + if (searchResultCache.size >= SEARCH_CACHE_MAX) { + searchResultCache.clear(); + } + searchResultCache.set(key, { at: Date.now(), items }); +} + const emits = defineEmits<{ 'update:value': [value: number[]]; }>(); +const userStore = useUserStore(); + +/** 当前登录用户 id(缓存隔离维度;后端返回的 admin 行主键是 id,兜底 vben 的 userId) */ +function currentUserId(): number | string { + const info = userStore.userInfo as null | Record; + return info?.id ?? info?.userId ?? 0; +} + const mValue = useVModel(props, 'value', emits, { passive: true, defaultValue: [], @@ -143,14 +184,36 @@ async function openPanel() { updatePanelPlacement(); } +/** 请求序号:只采纳最后一次请求的响应,避免快速输入时慢响应覆盖新结果 */ +let fetchSeq = 0; + async function fetchOptions() { const keyword = searchKeyword.value.trim(); if (!keyword) { + // 递增序号让在途请求作废,避免清空后慢响应又把旧结果写回来 + fetchSeq += 1; + loading.value = false; options.value = []; showDropdown.value = false; highlightIndex.value = -1; return; } + + const cacheKey = `${currentUserId()}|${props.storeType ?? 'all'}|${keyword}`; + const cachedItems = readSearchCache(cacheKey); + if (cachedItems) { + // 命中缓存:直接渲染,不发请求不转 loading(体感「秒出」) + fetchSeq += 1; + loading.value = false; + options.value = cachedItems; + await openPanel(); + highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1; + await nextTick(); + updatePanelPlacement(); + return; + } + + const seq = (fetchSeq += 1); loading.value = true; await openPanel(); try { @@ -161,15 +224,21 @@ async function fetchOptions() { : {}), limit: 20, }); - options.value = res?.items ?? []; + if (seq !== fetchSeq) return; + const items = res?.items ?? []; + writeSearchCache(cacheKey, items); + options.value = items; highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1; await nextTick(); updatePanelPlacement(); } catch { + if (seq !== fetchSeq) return; options.value = []; highlightIndex.value = -1; } finally { - loading.value = false; + if (seq === fetchSeq) { + loading.value = false; + } } } @@ -399,7 +468,13 @@ watch(visibleOptions, (list) => { color="blue" @close="() => removeSelected(item.id)" > - {{ item.name }}【{{ item.id }}】 + + + {{ item.name }}【{{ item.id }}】 + @@ -416,6 +491,26 @@ watch(visibleOptions, (list) => { flex-wrap: wrap; gap: 6px; margin-top: 8px; + + /* 单个超长门店名不能撑破容器:限宽 + 省略号,完整名走 title 悬停提示 */ + :deep(.ant-tag) { + display: inline-flex; + align-items: center; + max-width: 100%; + min-width: 0; + /* 间距交给 gap 控制,去掉 antd 自带右外边距避免换行时右侧不齐 */ + margin-inline-end: 0; + } + + :deep(.ant-tag-close-icon) { + flex-shrink: 0; + } +} + +.store-multi-search__tag-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/apps/web-antd/src/components/loading/PrescriptionLoading.vue b/apps/web-antd/src/components/loading/PrescriptionLoading.vue index 4996776f..232e3b06 100644 --- a/apps/web-antd/src/components/loading/PrescriptionLoading.vue +++ b/apps/web-antd/src/components/loading/PrescriptionLoading.vue @@ -666,12 +666,12 @@ onUnmounted(() => { width: 100%; height: 100%; min-height: 220px; - //background: #ffffff; + /* background: #ffffff; */ border-radius: 12px; overflow: hidden; } .loading-wrapper--dark { - //background: #1e2227; + /* background: #1e2227; */ } .loading-canvas { display: block; diff --git a/apps/web-antd/src/components/stat-islands/index.ts b/apps/web-antd/src/components/stat-islands/index.ts new file mode 100644 index 00000000..611ceb84 --- /dev/null +++ b/apps/web-antd/src/components/stat-islands/index.ts @@ -0,0 +1,5 @@ +/** + * 通用统计岛组件导出:紧凑玻璃质感统计小卡(设计对齐提现管理账户卡统计岛) + */ +export { default as StatIslands } from './stat-islands.vue'; +export type { StatIslandItem } from './typing'; diff --git a/apps/web-antd/src/components/stat-islands/stat-islands.vue b/apps/web-antd/src/components/stat-islands/stat-islands.vue new file mode 100644 index 00000000..6e765707 --- /dev/null +++ b/apps/web-antd/src/components/stat-islands/stat-islands.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/apps/web-antd/src/components/stat-islands/typing.ts b/apps/web-antd/src/components/stat-islands/typing.ts new file mode 100644 index 00000000..f32cc0b7 --- /dev/null +++ b/apps/web-antd/src/components/stat-islands/typing.ts @@ -0,0 +1,19 @@ +/** + * 统计岛组件类型定义(独立文件避免 index ↔ 组件循环引用) + */ + +/** 统计岛单项配置 */ +export interface StatIslandItem { + /** 唯一 key */ + key: string; + /** 指标名(小灰字) */ + label: string; + /** 指标值(数字,内部格式化两位小数) */ + value: number | string; + /** iconify 图标名(右上角,规范:SVG 图标不用 emoji) */ + icon: string; + /** 是否强调(主色字 + 更强荧光) */ + emphasize?: boolean; + /** 值前缀,默认 ¥;传空串则不显示前缀 */ + prefix?: string; +} diff --git a/apps/web-antd/src/components/table-action/card-actions.vue b/apps/web-antd/src/components/table-action/card-actions.vue new file mode 100644 index 00000000..f7193758 --- /dev/null +++ b/apps/web-antd/src/components/table-action/card-actions.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/apps/web-antd/src/components/table-action/index.ts b/apps/web-antd/src/components/table-action/index.ts index 00b7e6b6..39d9991a 100644 --- a/apps/web-antd/src/components/table-action/index.ts +++ b/apps/web-antd/src/components/table-action/index.ts @@ -1,2 +1,3 @@ +export { default as CardActions } from './card-actions.vue'; export { default as TableAction } from './table-action.vue'; export type * from './types.d.ts'; diff --git a/apps/web-antd/src/components/view-mode-switch/index.ts b/apps/web-antd/src/components/view-mode-switch/index.ts new file mode 100644 index 00000000..fe39309b --- /dev/null +++ b/apps/web-antd/src/components/view-mode-switch/index.ts @@ -0,0 +1,3 @@ +export { useViewMode } from './use-view-mode'; +export type { ViewMode } from './use-view-mode'; +export { default as ViewModeSwitch } from './view-mode-switch.vue'; diff --git a/apps/web-antd/src/components/view-mode-switch/use-view-mode.ts b/apps/web-antd/src/components/view-mode-switch/use-view-mode.ts new file mode 100644 index 00000000..0ce529ef --- /dev/null +++ b/apps/web-antd/src/components/view-mode-switch/use-view-mode.ts @@ -0,0 +1,49 @@ +import type { Ref } from 'vue'; + +import { ref, watch } from 'vue'; + +/** + * 通用「列表/卡片」视图模式类型 + * list=表格列表视图,card=卡片看板视图 + */ +export type ViewMode = 'card' | 'list'; + +/** + * 视图模式记忆 Hook + * + * 为什么封装:商品订单/处方订单/挂号订单等多个列表页都需要「列表/卡片」切换, + * 且要求刷新后记住用户上次选择。统一在这里读写 localStorage, + * 各页面只需传入唯一 storageKey,避免每页复制一份存取逻辑。 + * + * @param storageKey localStorage 键名(建议 `<模块>-view-mode`,如 `product-order-view-mode`) + * @param defaultMode 无记忆时的默认视图,默认列表 + * @returns 响应式视图模式 ref,直接 v-model 给 ViewModeSwitch 组件即可 + */ +export function useViewMode( + storageKey: string, + defaultMode: ViewMode = 'list', +): Ref { + // 初始化时同步读取本地记忆,避免先渲染默认视图再闪切 + const readStored = (): ViewMode => { + try { + const raw = localStorage.getItem(storageKey); + if (raw === 'list' || raw === 'card') return raw; + } catch { + // localStorage 不可用(隐私模式等)时静默回退默认值 + } + return defaultMode; + }; + + const viewMode = ref(readStored()); + + // 切换后立即写回,下次进入页面直接还原 + watch(viewMode, (mode) => { + try { + localStorage.setItem(storageKey, mode); + } catch { + // 写入失败不影响功能,忽略 + } + }); + + return viewMode; +} diff --git a/apps/web-antd/src/components/view-mode-switch/view-mode-switch.vue b/apps/web-antd/src/components/view-mode-switch/view-mode-switch.vue new file mode 100644 index 00000000..4d32e1fb --- /dev/null +++ b/apps/web-antd/src/components/view-mode-switch/view-mode-switch.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 64448027..835d7065 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -26,6 +26,7 @@ import DoctorTransferFloat from '#/components/doctor-transfer-float/DoctorTransf import HeaderVipBadge from '#/components/vip/HeaderVipBadge.vue'; import SwitchAccountModal from '#/layouts/components/SwitchAccountModal.vue'; import { useAuthStore } from '#/store'; +import { desktop, isElectron } from '#/util/desktop'; import { formatTimeToRelative } from '#/util/tool'; import LoginForm from '#/views/_core/authentication/login.vue'; import NoticeActionHost from '#/views/notice/action/NoticeActionHost.vue'; @@ -62,6 +63,15 @@ const getNoticeList = async () => { description: `您有 ${items.length} 条未读消息请注意查收`, duration: 5, }); + // 桌面端且窗口未聚焦时补发系统通知(页内 Toast 此时用户看不到),点击跳消息中心 + if (isElectron() && !document.hasFocus()) { + desktop.notify({ + title: '您有新消息', + body: `您有 ${items.length} 条未读消息请注意查收`, + route: '/notice/list', + }); + desktop.flashFrame(); + } } notifications.value = items.map((item: any) => { const color = resolveNoticeColor(item.type_color); @@ -122,11 +132,35 @@ localStorage.setItem = (key, value) => { window.addEventListener('storage', handleStorageChange); const stopRefreshListen = onNoticeRefresh(() => restartInterval()); +// 桌面端:未读数变化同步任务栏/Dock 角标(浏览器环境 no-op) +watch(unreadCount, (count) => desktop.setBadge(count)); + +// 桌面端:系统通知点击后主进程回传目标路由,这里执行实际跳转 +desktop.onNavigate((route) => { + router.push(route); +}); + +// 桌面端:自动更新事件轻量提示(下载完成后的安装确认由主进程弹窗负责) +desktop.onUpdateEvent((payload) => { + if (payload.type === 'available') { + const version = (payload.data as { version?: string })?.version; + notification.info({ + message: '发现新版本', + description: `新版本 ${version ? `v${version} ` : ''}正在后台下载,完成后将提示安装`, + duration: 5, + }); + } +}); + onUnmounted(() => { clearInterval(intervalId); window.removeEventListener('storage', handleStorageChange); localStorage.setItem = originalSetItem; stopRefreshListen(); + // 布局卸载(登出)时清空桌面角标,避免残留未读数 + desktop.setBadge(0); + // 解绑系统通知跳转回调,避免登出后点击残留通知仍对已登出会话执行 router.push + desktop.offNavigate(); }); const goNoticeList = () => { diff --git a/apps/web-antd/src/types/desktop.d.ts b/apps/web-antd/src/types/desktop.d.ts new file mode 100644 index 00000000..351e422f --- /dev/null +++ b/apps/web-antd/src/types/desktop.d.ts @@ -0,0 +1,37 @@ +/** + * 桌面端(Electron)preload 注入的全局 API 类型声明 + * 形状与 apps/desktop/src/preload/index.ts 暴露的 desktopAPI 保持同步; + * 浏览器环境该对象不存在(可选属性),业务代码统一走 #/util/desktop 封装访问。 + */ +interface Window { + desktopAPI?: { + /** 桌面端标记,恒为 true */ + isElectron: true; + /** 当前平台:win32 / darwin / linux */ + platform: string; + getVersion: () => Promise; + notify: (options: { body: string; route?: string; title: string }) => Promise; + setBadge: (count: number) => Promise; + flashFrame: () => Promise; + getAutoLaunch: () => Promise; + setAutoLaunch: (enable: boolean) => Promise; + getPrinters: () => Promise< + Array<{ displayName: string; isDefault: boolean; name: string }> + >; + printHTML: (options: { + deviceName?: string; + html: string; + silent?: boolean; + }) => Promise<{ message?: string; ok: boolean }>; + getSettings: () => Promise<{ printerName: string; silentPrint: boolean }>; + setSettings: ( + patch: Partial<{ printerName: string; silentPrint: boolean }>, + ) => Promise<{ printerName: string; silentPrint: boolean }>; + checkUpdate: () => Promise; + focusWindow: () => Promise; + onNavigate: (callback: (route: string) => void) => void; + onUpdateEvent: ( + callback: (payload: { data?: unknown; type: string }) => void, + ) => void; + }; +} diff --git a/apps/web-antd/src/util/desktop.ts b/apps/web-antd/src/util/desktop.ts new file mode 100644 index 00000000..3d1de68f --- /dev/null +++ b/apps/web-antd/src/util/desktop.ts @@ -0,0 +1,122 @@ +/** + * 桌面端(Electron)能力封装 + * 桌面端由 apps/desktop 的 preload 脚本注入 window.desktopAPI; + * 浏览器环境没有该对象,本封装的所有方法自动 no-op 降级,保证网页端行为完全不变。 + * 业务代码禁止直接访问 window.desktopAPI,统一走本文件,便于类型收敛与降级兜底。 + */ + +/** 系统打印机信息(Electron getPrintersAsync 的子集) */ +export interface DesktopPrinter { + name: string; + displayName: string; + isDefault: boolean; +} + +/** 桌面端本地设置(与 apps/desktop 主进程 settings.ts 保持同步) */ +export interface DesktopSettings { + /** 静默打印使用的打印机名称,空串表示系统默认打印机 */ + printerName: string; + /** 是否启用静默打印 */ + silentPrint: boolean; +} + +/** 自动更新事件(主进程 updater 转发,渲染层做轻量提示) */ +export interface DesktopUpdateEvent { + type: 'available' | 'downloaded' | 'error' | 'not-available' | 'progress'; + data?: unknown; +} + +/** 判断当前是否运行在 Electron 桌面端 */ +export function isElectron(): boolean { + return typeof window !== 'undefined' && !!window.desktopAPI?.isElectron; +} + +/** 取桥接 API:浏览器环境返回 undefined,调用方全部可选链降级 */ +function api() { + return typeof window === 'undefined' ? undefined : window.desktopAPI; +} + +// onNavigate / onUpdateEvent 的桥接层每调用一次就会新增一个 ipc 监听, +// 而布局组件会随登录/登出反复挂载。这里用「模块级单次桥接 + 可替换回调」避免监听器堆积 +let navigateCallback: ((route: string) => void) | null = null; +let navigateBridged = false; +let updateCallback: ((payload: DesktopUpdateEvent) => void) | null = null; +let updateBridged = false; + +export const desktop = { + /** 发送系统通知;route 为点击通知后要跳转的前端路由(不传则仅唤起窗口) */ + notify(options: { body: string; route?: string; title: string }): void { + api()?.notify(options); + }, + /** 设置未读角标数量,0 表示清除(mac Dock 数字 / Windows 任务栏覆盖图标) */ + setBadge(count: number): void { + api()?.setBadge(count); + }, + /** 任务栏闪烁提醒(窗口未聚焦时生效,聚焦后自动清除) */ + flashFrame(): void { + api()?.flashFrame(); + }, + /** 读取开机自启状态(浏览器环境恒为 false) */ + async getAutoLaunch(): Promise { + return (await api()?.getAutoLaunch()) ?? false; + }, + /** 设置开机自启,返回设置后的实际状态 */ + async setAutoLaunch(enable: boolean): Promise { + return (await api()?.setAutoLaunch(enable)) ?? false; + }, + /** 获取系统打印机列表(浏览器环境返回空数组) */ + async getPrinters(): Promise { + return (await api()?.getPrinters()) ?? []; + }, + /** 静默打印:传入拼好的完整 HTML,由主进程隐藏窗口出纸 */ + async printHTML(options: { + deviceName?: string; + html: string; + silent?: boolean; + }): Promise<{ message?: string; ok: boolean }> { + return ( + (await api()?.printHTML(options)) ?? { + ok: false, + message: '当前环境不支持桌面打印', + } + ); + }, + /** 读取桌面端设置 */ + async getSettings(): Promise { + return (await api()?.getSettings()) ?? { printerName: '', silentPrint: false }; + }, + /** 增量更新桌面端设置,返回更新后的完整设置 */ + async setSettings(patch: Partial): Promise { + return ( + (await api()?.setSettings(patch)) ?? { printerName: '', silentPrint: false } + ); + }, + /** 手动检查更新(结果由主进程弹窗反馈) */ + checkUpdate(): void { + api()?.checkUpdate(); + }, + /** 唤起并聚焦主窗口 */ + focusWindow(): void { + api()?.focusWindow(); + }, + /** 监听系统通知点击后的路由跳转请求(重复调用只替换回调,不会堆积监听器) */ + onNavigate(callback: (route: string) => void): void { + navigateCallback = callback; + if (!navigateBridged && api()) { + navigateBridged = true; + api()!.onNavigate((route) => navigateCallback?.(route)); + } + }, + /** 解绑路由跳转回调:布局卸载(登出)时调用,避免残留系统通知被点击后对已登出会话执行跳转 */ + offNavigate(): void { + navigateCallback = null; + }, + /** 监听自动更新事件(重复调用只替换回调,不会堆积监听器) */ + onUpdateEvent(callback: (payload: DesktopUpdateEvent) => void): void { + updateCallback = callback; + if (!updateBridged && api()) { + updateBridged = true; + api()!.onUpdateEvent((payload) => updateCallback?.(payload as DesktopUpdateEvent)); + } + }, +}; diff --git a/apps/web-antd/src/utils/register-notify.ts b/apps/web-antd/src/utils/register-notify.ts new file mode 100644 index 00000000..de65a1fe --- /dev/null +++ b/apps/web-antd/src/utils/register-notify.ts @@ -0,0 +1,144 @@ +/** + * 挂号实时提醒工具(医生端) + * + * xk-api 在挂号支付成功后通过 Go WebSocket 推 message_type=15 的系统消息, + * useWebSocket 收到后调用 handleRegisterNotifyMessage 统一处理: + * - 桌面端(Electron)且窗口未聚焦:弹系统级消息通知 + 任务栏闪烁 + 播放提示音, + * 点击通知唤起窗口并跳转挂号订单页(走 basic.vue 已接线的 desktop.onNavigate) + * - 页面端(浏览器 / 桌面端窗口聚焦中):弹站内 antd notification + 播放提示音 + */ +import { notification } from 'ant-design-vue'; + +import { router } from '#/router'; +import { desktop, isElectron } from '#/util/desktop'; + +// 与 util/tool.ts 的 sys.mp3 同方式引入:经 Vite 处理,生产构建自动带部署 base 路径 +import RegisterNotifyMp3 from '/public/audio/register-notify.mp3'; + +/** 与后端 App\Enum\chat\MessageTypeEnum::REGISTER_NOTIFY 对齐 */ +export const REGISTER_NOTIFY_MESSAGE_TYPE = 15; + +/** 挂号订单列表菜单路由(xk_menu path),通知点击后的统一落点 */ +const REGISTER_ORDER_ROUTE = '/business/register/list'; + +/** 去重窗口:同一挂号单 10 秒内只提醒一次(防多标签页/重复投递叠加提示音) */ +const DEDUPE_WINDOW_MS = 10_000; +const DEDUPE_KEY_PREFIX = 'xk-register-notify:'; + +/** 后端 RegisterDoctorWsNotifyJob 拼装的通知载荷(message_content JSON) */ +interface RegisterNotifyPayload { + body: string; + order_no?: string; + register_id: number; + scene?: string; + title: string; +} + +/** 提示音单例:复用同一个 Audio 对象,避免每次通知都新建请求 */ +let audioInstance: HTMLAudioElement | null = null; + +/** + * 播放通知提示音 + * play() 返回的 Promise 在浏览器自动播放策略拒绝时会 reject(用户从未与页面交互过), + * 这里静默吞掉:医生登录后必然点击过页面,正常场景不会被拦 + */ +function playNotifySound() { + try { + if (!audioInstance) { + audioInstance = new Audio(RegisterNotifyMp3); + } + audioInstance.currentTime = 0; + audioInstance.play().catch(() => {}); + } catch { + // Audio 不可用(极老浏览器)时跳过声音,不影响通知展示 + } +} + +/** + * 解析 WS 消息里的通知载荷 + * message_content 是后端 json_encode 的字符串;解析失败返回 null 直接忽略该条 + */ +function parsePayload(raw: unknown): null | RegisterNotifyPayload { + try { + const data = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (!data || typeof data !== 'object') return null; + const record = data as Record; + const registerId = Number(record.register_id); + if (!Number.isFinite(registerId) || registerId <= 0) return null; + return { + register_id: registerId, + title: String(record.title || '新的挂号订单'), + body: String(record.body || ''), + order_no: String(record.order_no || ''), + scene: String(record.scene || ''), + }; + } catch { + return null; + } +} + +/** + * 同一挂号单去重:localStorage 记录上次提醒时间戳 + * 医生开多个标签/窗口时每个连接都会收到推送(Go 服务发给用户所有在线设备), + * 不去重会同时响多次提示音;localStorage 跨标签共享,天然适合做这个锁 + */ +function shouldNotify(registerId: number): boolean { + const key = DEDUPE_KEY_PREFIX + registerId; + try { + const last = Number(localStorage.getItem(key) || 0); + const now = Date.now(); + if (now - last < DEDUPE_WINDOW_MS) return false; + localStorage.setItem(key, String(now)); + return true; + } catch { + // localStorage 不可用(隐私模式)时放行,宁可重复提醒不可漏提醒 + return true; + } +} + +/** + * 弹站内通知卡片 + * key 用挂号单 ID 保证同单只显示一张卡片;点击卡片跳转挂号订单页并收起。 + * 页面在后台标签时不自动关闭(duration=0),医生切回来仍能看到提醒 + */ +function showInPageNotification(payload: RegisterNotifyPayload) { + const key = `register-notify-${payload.register_id}`; + notification.open({ + key, + message: payload.title, + description: payload.body || payload.order_no, + duration: document.hidden ? 0 : 8, + onClick: () => { + router.push(REGISTER_ORDER_ROUTE).catch(() => {}); + notification.close(key); + }, + }); +} + +/** + * 挂号提醒统一入口(useWebSocket 收到 message_type=15 时调用) + * 流程:解析载荷 → 跨标签去重 → 播放提示音 → 按运行环境选择通知形态 + */ +export function handleRegisterNotifyMessage(data: { + message_content?: unknown; +}) { + const payload = parsePayload(data?.message_content); + if (!payload || !shouldNotify(payload.register_id)) return; + + playNotifySound(); + + // 桌面端窗口未聚焦:系统通知 + 任务栏闪烁(页内卡片此时用户看不到), + // 点击通知由主进程唤起窗口并回传路由(basic.vue 的 desktop.onNavigate 执行跳转) + if (isElectron() && !document.hasFocus()) { + desktop.notify({ + title: payload.title, + body: payload.body || payload.order_no || '', + route: REGISTER_ORDER_ROUTE, + }); + desktop.flashFrame(); + return; + } + + // 浏览器端 / 桌面端窗口聚焦中:站内提示 + showInPageNotification(payload); +} diff --git a/apps/web-antd/src/utils/tagColor.ts b/apps/web-antd/src/utils/tagColor.ts new file mode 100644 index 00000000..7418d954 --- /dev/null +++ b/apps/web-antd/src/utils/tagColor.ts @@ -0,0 +1,41 @@ +/** + * antd Tag 预设色名 → css 色值工具 + * + * 为什么需要:Apple 风格卡片用「状态彩点 + tinted 胶囊」替代 antd Tag 色块, + * 但字典常量里沿用 antd 色名(red/green/processing…),这里统一转换成真实色值, + * 供订单三页卡片视图与详情弹窗共用,避免各组件复制色板。 + */ +export const TAG_COLOR_HEX: Record = { + red: '#f5222d', + orange: '#fa8c16', + gold: '#faad14', + yellow: '#d4b106', + green: '#52c41a', + cyan: '#13c2c2', + blue: '#1677ff', + geekblue: '#2f54eb', + purple: '#722ed1', + pink: '#eb2f96', + magenta: '#eb2f96', + processing: '#1677ff', + success: '#52c41a', + warning: '#fa8c16', + error: '#f5222d', + gray: '#8c8c8c', + default: '#8c8c8c', +}; + +/** 未知色名兜底灰(单独常量,规避 Record 索引返回 undefined 的类型收窄问题) */ +const DEFAULT_TAG_HEX = '#8c8c8c'; + +/** Tag 色名转 css 色值;自定义 hex 原样返回,未知色名回退灰 */ +export function tagColorHex(color?: string): string { + if (!color) return DEFAULT_TAG_HEX; + return color.startsWith('#') ? color : (TAG_COLOR_HEX[color] ?? DEFAULT_TAG_HEX); +} + +/** Apple tinted 胶囊内联样式:同色 10% 透明度底 + 同色文字(8 位 hex alpha) */ +export function pillStyle(color?: string) { + const hex = tagColorHex(color); + return { color: hex, background: `${hex}1a` }; +} diff --git a/apps/web-antd/src/views/business/chat/components/SettingsModal.vue b/apps/web-antd/src/views/business/chat/components/SettingsModal.vue index 4deb755a..3366ac88 100644 --- a/apps/web-antd/src/views/business/chat/components/SettingsModal.vue +++ b/apps/web-antd/src/views/business/chat/components/SettingsModal.vue @@ -235,6 +235,7 @@ + + + + diff --git a/apps/web-antd/src/views/business/order/prescription/config/constants.ts b/apps/web-antd/src/views/business/order/prescription/config/constants.ts new file mode 100644 index 00000000..b79764e7 --- /dev/null +++ b/apps/web-antd/src/views/business/order/prescription/config/constants.ts @@ -0,0 +1,54 @@ +/** + * 处方订单模块字典常量 + * 列表 Tag 槽与卡片视图共用同一份映射,避免两处渲染语义漂移 + */ +export interface TagMeta { + color: string; + label: string; +} + +/** 处方类型映射(与列表 #status 槽第一行渲染语义一致) */ +export const PRESCRIPTION_ORDER_TYPE_MAP: Record = { + 1: { label: '中药', color: 'orange' }, + 2: { label: '商品【西药】', color: 'blue' }, + 3: { label: '商品【保健食品】', color: 'purple' }, + 4: { label: '已退款', color: 'green' }, + 5: { label: '产品服务包', color: 'pink' }, + 6: { label: '非药品', color: 'cyan' }, + 7: { label: '医疗器械', color: 'geekblue' }, +}; + +/** 审核状态映射(status:0待审核 1已通过 2已拒绝 3/4无需审核) */ +export const PRESCRIPTION_STATUS_MAP: Record = { + 0: { label: '待审核', color: 'red' }, + 1: { label: '已通过', color: 'green' }, + 2: { label: '已拒绝', color: 'red' }, + 3: { label: '无需审核', color: 'purple' }, + 4: { label: '无需审核', color: 'green' }, +}; + +/** + * 处方来源映射 + * is_online 由挂号 type 经 RegisterTypeEnum::getIsOnlineByType 映射: + * 1=在线问诊 2=诊所在线复诊 3=药店在线复诊 0=线下 + * (老页面漏了 3,药店复诊被误显示为线下,这里补全) + */ +export const PRESCRIPTION_IS_ONLINE_MAP: Record = { + 1: { label: '在线问诊', color: 'blue' }, + 2: { label: '在线复诊(诊所)', color: 'green' }, + 3: { label: '在线复诊(药店)', color: 'green' }, +}; + +/** 来源兜底:线下就诊 */ +export const PRESCRIPTION_IS_ONLINE_FALLBACK: TagMeta = { + label: '线下就诊', + color: 'default', +}; + +/** 搜索下拉:处方来源选项 */ +export const PRESCRIPTION_IS_ONLINE_OPTIONS = [ + { label: '在线问诊', value: 1 }, + { label: '在线复诊(诊所)', value: 2 }, + { label: '在线复诊(药店)', value: 3 }, + { label: '线下就诊', value: 0 }, +]; diff --git a/apps/web-antd/src/views/business/order/prescription/config/search.ts b/apps/web-antd/src/views/business/order/prescription/config/search.ts index 67af6e30..fb571765 100644 --- a/apps/web-antd/src/views/business/order/prescription/config/search.ts +++ b/apps/web-antd/src/views/business/order/prescription/config/search.ts @@ -3,13 +3,42 @@ import type { VbenFormProps } from '#/adapter/form'; import dayjs from 'dayjs'; import { getOrderPrescriptionTypeOption } from '#/views/business/order/product-order/api'; +import { PRESCRIPTION_IS_ONLINE_OPTIONS } from './constants'; + export const formOptions: VbenFormProps = { - // 默认展开 - collapsed: false, + // 默认收起只显示第一行,点「展开」看全部筛选(占位最小化) + collapsed: true, + commonConfig: { + // label 默认 100px,本页筛选名最长 4 字,收窄到 70px 让控件占满格子(否则控件被挤得只剩一半宽) + labelWidth: 70, + // antd Select/RangePicker 默认按内容收缩,统一撑满格子,保证一行里各控件等宽整齐 + componentProps: { + class: 'w-full', + }, + }, + // 响应式多列:抽出 grid 后 vben form 默认单列(一行一个字段),必须显式给栅格 + wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5', schema: [ { - component: 'VbenInput', + // 放第一位:时间范围有默认值(本月至今),默认收起时必须始终可见,否则用户不知道当前查的是哪段数据 + component: 'RangePicker', componentProps: { + format: 'YYYY-MM-DD', + // 提交纯日期,后端 getWhereBetween 会把结束日扩到当天 23:59:59 + valueFormat: 'YYYY-MM-DD', + }, + defaultValue: [ + dayjs().startOf('month').format('YYYY-MM-DD'), + dayjs().format('YYYY-MM-DD'), + ], + fieldName: 'search_time', + label: '时间范围', + }, + { + // 统一 antd 系控件:与 ApiSelect 同一套视觉(此前 VbenInput 是 shadcn 风格,高度圆角不一致) + component: 'Input', + componentProps: { + allowClear: true, placeholder: '输入订单号', }, defaultValue: '', @@ -36,18 +65,35 @@ export const formOptions: VbenFormProps = { label: '订单类型', }, { - component: 'RangePicker', + // 就诊人姓名/手机号模糊(后端 PersonKeywordScopeService,whereHas 关联表) + component: 'Input', componentProps: { - format: 'YYYY-MM-DD', - // 提交纯日期,后端 getWhereBetween 会把结束日扩到当天 23:59:59 - valueFormat: 'YYYY-MM-DD', + allowClear: true, + placeholder: '姓名/手机号', }, - defaultValue: [ - dayjs().startOf('month').format('YYYY-MM-DD'), - dayjs().format('YYYY-MM-DD'), - ], - fieldName: 'search_time', - label: '时间范围', + fieldName: 'patient_keyword', + label: '就诊人', + }, + { + // 医生姓名/手机号模糊(后端 PersonKeywordScopeService) + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '姓名/手机号', + }, + fieldName: 'doctor_keyword', + label: '医生', + }, + { + // 处方来源(后端 queryField 等值匹配;选项与 constants 映射同语义) + component: 'Select', + componentProps: { + allowClear: true, + options: PRESCRIPTION_IS_ONLINE_OPTIONS, + placeholder: '请选择', + }, + fieldName: 'is_online', + label: '处方来源', }, ], // 控制表单是否显示折叠按钮 @@ -55,9 +101,7 @@ export const formOptions: VbenFormProps = { submitButtonOptions: { content: '查询', }, - // 是否在字段值改变时提交表单 - // submitOnChange: true, - // submitOnChange: true, // 按下回车时是否提交表单 - submitOnEnter: false, + // 任意输入框回车即搜索;下拉面板打开时的回车只选中选项(core 层已处理),第二次回车才搜索 + submitOnEnter: true, }; diff --git a/apps/web-antd/src/views/business/order/prescription/config/table.ts b/apps/web-antd/src/views/business/order/prescription/config/table.ts index ee9c4743..a9a2df25 100644 --- a/apps/web-antd/src/views/business/order/prescription/config/table.ts +++ b/apps/web-antd/src/views/business/order/prescription/config/table.ts @@ -2,12 +2,18 @@ import type { VxeGridProps } from '#/adapter/vxe-table'; import { getPrescriptionListApi } from '../api'; +/** + * 处方列表行类型:后端行字段众多(含关联 store/doctor_info/user_patient), + * 用索引签名放宽,避免模板槽访问动态字段时 vue-tsc 报属性缺失 + */ interface RowType { - id: string; - name: string; - logo: string; - introduce: string; + id: number; + prescription_no: string; + prescription_type: number; + status: number; + is_online: number; created_at: string; + [key: string]: any; } export const gridOptions: VxeGridProps = { diff --git a/apps/web-antd/src/views/business/order/prescription/index.vue b/apps/web-antd/src/views/business/order/prescription/index.vue index 23ac1084..b4ba5491 100644 --- a/apps/web-antd/src/views/business/order/prescription/index.vue +++ b/apps/web-antd/src/views/business/order/prescription/index.vue @@ -1,19 +1,36 @@ + + + + + + +
+ +
+ + + +
diff --git a/apps/web-antd/src/views/dashboard/calendar/components/CalendarPanel.vue b/apps/web-antd/src/views/dashboard/calendar/components/CalendarPanel.vue new file mode 100644 index 00000000..2d61e1d5 --- /dev/null +++ b/apps/web-antd/src/views/dashboard/calendar/components/CalendarPanel.vue @@ -0,0 +1,1661 @@ + + + diff --git a/apps/web-antd/src/views/dashboard/calendar/components/day-todos-modal.vue b/apps/web-antd/src/views/dashboard/calendar/components/day-todos-modal.vue deleted file mode 100644 index c90a5483..00000000 --- a/apps/web-antd/src/views/dashboard/calendar/components/day-todos-modal.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - diff --git a/apps/web-antd/src/views/dashboard/calendar/index.vue b/apps/web-antd/src/views/dashboard/calendar/index.vue index 42aeea03..065c8062 100644 --- a/apps/web-antd/src/views/dashboard/calendar/index.vue +++ b/apps/web-antd/src/views/dashboard/calendar/index.vue @@ -1,597 +1,17 @@ - diff --git a/apps/web-antd/src/views/dashboard/calendar/utils/almanac.ts b/apps/web-antd/src/views/dashboard/calendar/utils/almanac.ts index 8bed9b1c..9267016b 100644 --- a/apps/web-antd/src/views/dashboard/calendar/utils/almanac.ts +++ b/apps/web-antd/src/views/dashboard/calendar/utils/almanac.ts @@ -3,31 +3,82 @@ import { HolidayUtil, Solar } from 'lunar-typescript'; /** * 黄历信息计算工具(基于 lunar-typescript) * 设计说明: - * - 中国传统节日(春节/端午/中秋等)由农历推算,国际节日(元旦/圣诞/母亲节等)按公历, - * 正好满足「中国节日算农历、其他节日按公历」的需求 + * - 中国传统节日(春节/端午/中秋等)由农历推算,公历节日(元旦/国庆等)按公历 + * - 农历文案与节日、节气三者并列返回互不覆盖:格子里农历始终显示,节日/节气展示在副行 + * - 公历节日走白名单只保留中国也在过的,纯国外节日暂时注释掉(见 SOLAR_FESTIVAL_WHITELIST) * - 法定节假日调休(休/班)数据由 HolidayUtil 内置,覆盖国务院历年放假安排 * - 按「y-m-d」缓存计算结果:日历一屏 42 格、频繁切月,避免重复实例化 Solar/Lunar */ -/** 单日黄历信息 */ +/** 节日条目:kind 用于差异化配色(传统农历节日红、公历现代节日橙),产品要求节日颜色不能都一样 */ +export interface FestivalItem { + kind: 'modern' | 'traditional'; + name: string; +} + +/** 单日黄历信息(日历格子用) */ export interface DayAlmanac { - /** 格子副标题:优先级 节日 > 节气 > 初一显示月名 > 农历日 */ - displayText: string; - /** 副标题类型,用于差异化配色:festival节日 / jieqi节气 / lunar农历 */ - displayType: 'festival' | 'jieqi' | 'lunar'; + /** 农历文案:初一显示月名(如「七月」),其余显示农历日(如「三十」),始终展示、不被节日覆盖 */ + lunarText: string; + /** 当天节气(无则空串),与节日并列展示互不覆盖 */ + jieQi: string; + /** 过滤后的节日列表:农历传统节日在前、公历现代节日在后,格子展示第一个、悬浮提示展示全部 */ + festivalItems: FestivalItem[]; /** 完整农历日期,如「正月初一」,悬浮提示用 */ lunarDate: string; - /** 当天节气(无则空串) */ - jieQi: string; - /** 当天全部节日(农历节日 + 公历/国际节日),悬浮提示用 */ - festivals: string[]; /** 法定节假日安排:null=正常工作日/周末,否则含名称与是否补班 */ holiday: null | { isWork: boolean; name: string }; } +/** 单日黄历详情(侧栏「选中日」卡片用,信息比格子更全) */ +export interface DayAlmanacDetail { + /** 星期文本,如「星期三」 */ + week: string; + /** 完整农历日期,如「六月廿九」 */ + lunarDate: string; + /** 干支纪年月日,如「丙午年 丙申月 庚辰日」 */ + ganZhi: string; + /** 生肖,如「马」 */ + shengXiao: string; + /** 黄历宜(民俗参考,侧栏氛围信息) */ + yi: string[]; + /** 黄历忌 */ + ji: string[]; +} + +/** + * 公历节日白名单:只保留中国也在过的节日(法定/官方节日 + 已本土化的舶来节日) + * 产品要求暂时屏蔽国内基本不过的国外节日,注释保留在这里,后续要恢复解开对应注释即可 + */ +const SOLAR_FESTIVAL_WHITELIST = new Set([ + '元旦节', + '情人节', + '妇女节', + '植树节', + '消费者权益日', + '劳动节', + '青年节', + '儿童节', + '建党节', + '建军节', + '教师节', + '国庆节', + '母亲节', + '父亲节', + '平安夜', + '圣诞节', + // '愚人节', + // '万圣节前夜', + // '万圣节', + // '感恩节', +]); + /** 计算结果缓存:跨月切换时同一天不重复计算 */ const cache = new Map(); +/** 详情缓存与格子缓存分开:详情仅选中日才算,字段更多没必要提前算 42 格 */ +const detailCache = new Map(); + /** * 获取某个公历日期的黄历信息(农历/节气/节日/法定调休) * @param year 公历年 @@ -42,38 +93,57 @@ export function getDayAlmanac(year: number, month: number, day: number): DayAlma const solar = Solar.fromYmd(year, month, day); const lunar = solar.getLunar(); - // 节日合并:农历节日在前(春节等中国节日优先展示),其后公历节日与国际纪念日 - const festivals = [ - ...lunar.getFestivals(), - ...solar.getFestivals(), - ...lunar.getOtherFestivals(), - ...solar.getOtherFestivals(), + // 节日组装:农历传统节日(含除夕/下元节等民俗,都是中国节日全保留)在前, + // 公历节日过白名单在后;solar.getOtherFestivals() 是「世界XX日」类国际纪念日,国内基本不过,整体不展示 + const festivalItems: FestivalItem[] = [ + ...[...lunar.getFestivals(), ...lunar.getOtherFestivals()].map((name) => ({ + kind: 'traditional' as const, + name, + })), + ...solar + .getFestivals() + .filter((name) => SOLAR_FESTIVAL_WHITELIST.has(name)) + .map((name) => ({ kind: 'modern' as const, name })), ]; - const jieQi = lunar.getJieQi(); - // 初一显示月名(如「二月」),其余显示农历日(如「十五」) - const lunarDay = - lunar.getDay() === 1 ? `${lunar.getMonthInChinese()}月` : lunar.getDayInChinese(); - // 副标题择优:节日 > 节气 > 农历日,格子空间有限只显示一条 - let displayText = lunarDay; - let displayType: DayAlmanac['displayType'] = 'lunar'; - if (festivals.length > 0) { - displayText = festivals[0] as string; - displayType = 'festival'; - } else if (jieQi) { - displayText = jieQi; - displayType = 'jieqi'; - } + // 初一显示月名(如「二月」),其余显示农历日(如「十五」);农历始终展示,不再被节日/节气覆盖 + const lunarText = + lunar.getDay() === 1 ? `${lunar.getMonthInChinese()}月` : lunar.getDayInChinese(); const h = HolidayUtil.getHoliday(year, month, day); const result: DayAlmanac = { - displayText, - displayType, + lunarText, + jieQi: lunar.getJieQi(), + festivalItems, lunarDate: `${lunar.getMonthInChinese()}月${lunar.getDayInChinese()}`, - jieQi, - festivals, holiday: h ? { isWork: h.isWork(), name: h.getName() } : null, }; cache.set(key, result); return result; } + +/** + * 获取某个公历日期的黄历详情(星期/完整农历/干支/生肖/宜忌) + * 供侧栏「选中日」卡片展示,走独立缓存避免与格子计算互相拖累 + * @param year 公历年 + * @param month 公历月(1-12) + * @param day 公历日 + */ +export function getDayAlmanacDetail(year: number, month: number, day: number): DayAlmanacDetail { + const key = `${year}-${month}-${day}`; + const cached = detailCache.get(key); + if (cached) return cached; + + const solar = Solar.fromYmd(year, month, day); + const lunar = solar.getLunar(); + const result: DayAlmanacDetail = { + week: `星期${solar.getWeekInChinese()}`, + lunarDate: `${lunar.getMonthInChinese()}月${lunar.getDayInChinese()}`, + ganZhi: `${lunar.getYearInGanZhi()}年 ${lunar.getMonthInGanZhi()}月 ${lunar.getDayInGanZhi()}日`, + shengXiao: lunar.getYearShengXiao(), + yi: lunar.getDayYi(), + ji: lunar.getDayJi(), + }; + detailCache.set(key, result); + return result; +} diff --git a/apps/web-antd/src/views/dashboard/workspace/api/index.ts b/apps/web-antd/src/views/dashboard/workspace/api/index.ts index bb1191ef..a43c303c 100644 --- a/apps/web-antd/src/views/dashboard/workspace/api/index.ts +++ b/apps/web-antd/src/views/dashboard/workspace/api/index.ts @@ -61,6 +61,45 @@ export type TodoCenterResult = { items: TodoCenterItem[]; }; +/** 未来7天预约:单日分布 */ +export type AppointmentUpcomingDay = { + date: string; + label: string; + week: string; + count: number; +}; + +/** 未来7天预约统计(卡片) */ +export type AppointmentUpcomingResult = { + total: number; + days: AppointmentUpcomingDay[]; +}; + +/** 未来7天预约明细行 */ +export type AppointmentUpcomingItem = { + register_id: number; + order_no: string; + visit_date: string; + week: string; + slot_name: string; + start_time: string; + end_time: string; + slot_label: string; + patient_name: string; + doctor_name: string; + store_name: string; + is_pay: number; + price: string; + status: number; + status_text: string; +}; + +/** 未来7天预约明细分页结果 */ +export type AppointmentUpcomingListResult = { + items: AppointmentUpcomingItem[]; + total: number; +}; + /** * 获取当前登录角色的工作台布局 */ @@ -115,3 +154,26 @@ export async function getPlatformOverviewApi() { export async function getTodoCenterApi() { return requestClient.get(`${prefix}todo-center`); } + +/** + * 未来7天预约号统计(卡片:总数 + 按天分布,scope 后端按登录身份判定) + */ +export async function getAppointmentUpcomingApi() { + return requestClient.get( + `${prefix}appointment-upcoming`, + ); +} + +/** + * 未来7天预约明细(卡片点击弹窗:分页 + 日期筛选) + */ +export async function getAppointmentUpcomingListApi(data: { + page: number; + page_size: number; + visit_date?: string; +}) { + return requestClient.get( + `${prefix}appointment-upcoming-list`, + { params: data }, + ); +} diff --git a/apps/web-antd/src/views/dashboard/workspace/components/NoticeTicker.vue b/apps/web-antd/src/views/dashboard/workspace/components/NoticeTicker.vue new file mode 100644 index 00000000..fa62a432 --- /dev/null +++ b/apps/web-antd/src/views/dashboard/workspace/components/NoticeTicker.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/apps/web-antd/src/views/dashboard/workspace/components/WidgetAdminHub.vue b/apps/web-antd/src/views/dashboard/workspace/components/WidgetAdminHub.vue index 097c4967..7e458c42 100644 --- a/apps/web-antd/src/views/dashboard/workspace/components/WidgetAdminHub.vue +++ b/apps/web-antd/src/views/dashboard/workspace/components/WidgetAdminHub.vue @@ -181,6 +181,9 @@ onMounted(fetchAll); /> + + + diff --git a/apps/web-antd/src/views/dashboard/workspace/components/WidgetAppointmentUpcoming.vue b/apps/web-antd/src/views/dashboard/workspace/components/WidgetAppointmentUpcoming.vue new file mode 100644 index 00000000..bf2f5c97 --- /dev/null +++ b/apps/web-antd/src/views/dashboard/workspace/components/WidgetAppointmentUpcoming.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/apps/web-antd/src/views/dashboard/workspace/components/WidgetCalendarTodo.vue b/apps/web-antd/src/views/dashboard/workspace/components/WidgetCalendarTodo.vue new file mode 100644 index 00000000..a0b6a0f0 --- /dev/null +++ b/apps/web-antd/src/views/dashboard/workspace/components/WidgetCalendarTodo.vue @@ -0,0 +1,386 @@ + + + diff --git a/apps/web-antd/src/views/dashboard/workspace/components/WidgetNotice.vue b/apps/web-antd/src/views/dashboard/workspace/components/WidgetNotice.vue index 7181e74c..037f58cb 100644 --- a/apps/web-antd/src/views/dashboard/workspace/components/WidgetNotice.vue +++ b/apps/web-antd/src/views/dashboard/workspace/components/WidgetNotice.vue @@ -1,5 +1,8 @@ + + diff --git a/apps/web-antd/src/views/dashboard/workspace/config/widgets.ts b/apps/web-antd/src/views/dashboard/workspace/config/widgets.ts index aaad31b0..534a28a1 100644 --- a/apps/web-antd/src/views/dashboard/workspace/config/widgets.ts +++ b/apps/web-antd/src/views/dashboard/workspace/config/widgets.ts @@ -1,22 +1,27 @@ import type { Component } from 'vue'; +import NoticeTicker from '../components/NoticeTicker.vue'; +import WidgetAppointmentUpcoming from '../components/WidgetAppointmentUpcoming.vue'; +import WidgetCalendarTodo from '../components/WidgetCalendarTodo.vue'; import WidgetClinicToday from '../components/WidgetClinicToday.vue'; import WidgetDoctorToday from '../components/WidgetDoctorToday.vue'; -import WidgetNotice from '../components/WidgetNotice.vue'; import WidgetOrderPending from '../components/WidgetOrderPending.vue'; import WidgetPharmacistPending from '../components/WidgetPharmacistPending.vue'; import WidgetPlatformOverview from '../components/WidgetPlatformOverview.vue'; import WidgetQuickEntry from '../components/WidgetQuickEntry.vue'; import WidgetSalespersonSummary from '../components/WidgetSalespersonSummary.vue'; import WidgetTodoCenter from '../components/WidgetTodoCenter.vue'; +import WidgetWorkCalendar from '../components/WidgetWorkCalendar.vue'; /** * widget_code → 组件映射(与后端 WorkbenchWidgetEnum 对齐) * 后端 layout 下发未知 code 时前端直接跳过,保证两端可独立灰度上线 + * notice 已由列表卡(WidgetNotice,弃用保留)换成单行轮播条 NoticeTicker, + * 实际渲染位置由 index.vue 固定插槽控制(驾驶舱卡底内嵌 / 无驾驶舱独立一行) */ export const WIDGET_COMPONENT_MAP: Record = { quick_nav: WidgetQuickEntry, - notice: WidgetNotice, + notice: NoticeTicker, doctor_today: WidgetDoctorToday, pharmacist_pending: WidgetPharmacistPending, salesperson_summary: WidgetSalespersonSummary, @@ -24,6 +29,10 @@ export const WIDGET_COMPONENT_MAP: Record = { order_pending: WidgetOrderPending, platform_overview: WidgetPlatformOverview, todo_center: WidgetTodoCenter, + appointment_upcoming: WidgetAppointmentUpcoming, + // 日程待办(快捷入口行右侧)与日程日历(快捷入口行下方全宽),布局位置固定不进 KPI 区 + calendar_todo: WidgetCalendarTodo, + work_calendar: WidgetWorkCalendar, }; /** KPI 类模块(渲染在中部 KPI 区,其余按固定位置渲染) */ @@ -35,4 +44,5 @@ export const KPI_WIDGET_CODES = new Set([ 'order_pending', 'platform_overview', 'todo_center', + 'appointment_upcoming', ]); diff --git a/apps/web-antd/src/views/dashboard/workspace/index.vue b/apps/web-antd/src/views/dashboard/workspace/index.vue index 6cfe2d7f..8c5321cc 100644 --- a/apps/web-antd/src/views/dashboard/workspace/index.vue +++ b/apps/web-antd/src/views/dashboard/workspace/index.vue @@ -1,9 +1,11 @@ + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/StoreSettingModal.vue b/apps/web-antd/src/views/doctor/doctor/components/StoreSettingModal.vue new file mode 100644 index 00000000..c5d7ca8f --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/StoreSettingModal.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/AddDoctorScheduleModal.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/AddDoctorScheduleModal.vue new file mode 100644 index 00000000..9346ad4b --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/AddDoctorScheduleModal.vue @@ -0,0 +1,520 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleCalendarModal.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleCalendarModal.vue new file mode 100644 index 00000000..849aa0eb --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleCalendarModal.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleContextMenu.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleContextMenu.vue new file mode 100644 index 00000000..6f34bc55 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleContextMenu.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleMatrixView.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleMatrixView.vue new file mode 100644 index 00000000..e75c9e09 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ScheduleMatrixView.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SchedulePanel.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SchedulePanel.vue new file mode 100644 index 00000000..0f7b3bc5 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SchedulePanel.vue @@ -0,0 +1,1760 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ShiftTemplateModal.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ShiftTemplateModal.vue new file mode 100644 index 00000000..af0f994d --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/ShiftTemplateModal.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SlotRowsEditor.vue b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SlotRowsEditor.vue new file mode 100644 index 00000000..d9d2e3f9 --- /dev/null +++ b/apps/web-antd/src/views/doctor/doctor/components/schedule-calendar/SlotRowsEditor.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/apps/web-antd/src/views/doctor/doctor/index.vue b/apps/web-antd/src/views/doctor/doctor/index.vue index 030c2bfc..b3db9fef 100644 --- a/apps/web-antd/src/views/doctor/doctor/index.vue +++ b/apps/web-antd/src/views/doctor/doctor/index.vue @@ -3,7 +3,7 @@ import type { VxeGridListeners } from '#/adapter/vxe-table'; import { ref } from 'vue'; -import { Page, useVbenModal } from '@vben/common-ui'; +import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui'; import { Button, message, Tag } from 'ant-design-vue'; @@ -19,6 +19,9 @@ import DoctorBasicInfoCell from './components/cells/DoctorBasicInfoCell.vue'; import DoctorProfessionalInfoCell from './components/cells/DoctorProfessionalInfoCell.vue'; import FormModalDemo from './components/modal.vue'; import DoctorCardModal from './components/DoctorCardModal.vue'; +import ScheduleCalendarModal from './components/schedule-calendar/ScheduleCalendarModal.vue'; +import ScheduleDrawerComp from './components/ScheduleDrawer.vue'; +import StoreSettingModalComp from './components/StoreSettingModal.vue'; import { formOptions } from './config/search'; import { gridOptions } from './config/table'; @@ -67,6 +70,16 @@ const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({ connectedComponent: DoctorCardModal, }); +/** 排班设置抽屉(真排班:每天/按星期 × 全天/上下午/自定义时段) */ +const [ScheduleDrawer, ScheduleDrawerApi] = useVbenDrawer({ + connectedComponent: ScheduleDrawerComp, +}); + +/** 门店统一可预约天数设置弹窗 */ +const [StoreSettingModal, StoreSettingModalApi] = useVbenModal({ + connectedComponent: StoreSettingModalComp, +}); + const showModal = (data = {}, isUpdate = false) => { formModalApi.setData({ values: data, @@ -121,6 +134,43 @@ const showDoctorCard = (row: Record) => { DoctorCardModalApi.open(); }; +/** 打开排班设置抽屉 */ +const showScheduleDrawer = (row: Record) => { + ScheduleDrawerApi.setData({ record: row }); + ScheduleDrawerApi.open(); +}; + +/** 排班日历弹窗(按日期排班,锁定该医生) */ +const scheduleCalendarModalRef = ref>(); + +/** + * 打开排班日历(锁定医生模式) + * 店上下文取医生关联门店(列表行已带 stores):单店直接进,多店弹窗顶部可切换; + * 未关联门店时排班无模板归属也无患者入口,直接提示先绑定诊所 + */ +const showScheduleCalendar = (row: Record) => { + const stores = (Array.isArray(row.stores) ? row.stores : []) + .filter((s: any) => s?.store?.id) + .map((s: any) => ({ id: Number(s.store.id), name: String(s.store.name || '') })); + if (stores.length === 0) { + message.warning('该医生未关联任何诊所,请先绑定诊所后再排班'); + return; + } + scheduleCalendarModalRef.value?.ModalApi?.setData({ + storeId: stores[0]!.id, + storeName: stores[0]!.name, + storeOptions: stores, + lockedDoctorId: Number(row.su_id || 0), + lockedDoctorName: String(row.name || ''), + }); + scheduleCalendarModalRef.value?.ModalApi?.open(); +}; + +/** 打开门店统一设置弹窗 */ +const showStoreSettingModal = () => { + StoreSettingModalApi.open(); +}; + const openPcWindows = (id: number) => { openPcWindowsApi(id).then(() => { message.success('开通成功!'); @@ -151,6 +201,9 @@ const refuse = (id: number) => { + + +