From 73dfbbe8b5f19cf45cd96cf7d1af4599d0d346b9 Mon Sep 17 00:00:00 2001 From: liangxu Date: Mon, 20 Apr 2026 11:08:09 +0800 Subject: [PATCH] perf(desktop): lazy-load renderer routes and pause polling when hidden Drop the eager Antd global import in favour of unplugin-vue-components on-demand resolution, async route components, defineAsyncComponent for the shell/login split, and a dynamic Modal import inside showClientActionError. Pair that with a visibility-aware runtime poller that stops the 15s snapshot refresh when the window is hidden and kicks off an immediate refresh on re-show. Refresh DesktopShell nav with inline icons and a calmer active/hover treatment, and land ActionButton/DisclosurePanel/PaginationBar primitives for upcoming views. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop-client/electron.vite.config.ts | 14 +- apps/desktop-client/package.json | 1 + apps/desktop-client/src/renderer/App.vue | 8 +- .../src/renderer/components/ActionButton.vue | 169 ++++++++++++++++++ .../src/renderer/components/DesktopShell.vue | 59 ++++-- .../renderer/components/DisclosurePanel.vue | 88 +++++++++ .../src/renderer/components/PaginationBar.vue | 142 +++++++++++++++ .../renderer/composables/useDesktopRuntime.ts | 71 ++++++-- .../src/renderer/lib/client-errors.ts | 5 +- apps/desktop-client/src/renderer/main.ts | 3 +- apps/desktop-client/src/renderer/routes.ts | 46 +++-- pnpm-lock.yaml | 26 +++ 12 files changed, 579 insertions(+), 53 deletions(-) create mode 100644 apps/desktop-client/src/renderer/components/ActionButton.vue create mode 100644 apps/desktop-client/src/renderer/components/DisclosurePanel.vue create mode 100644 apps/desktop-client/src/renderer/components/PaginationBar.vue diff --git a/apps/desktop-client/electron.vite.config.ts b/apps/desktop-client/electron.vite.config.ts index 6dc722c..7e932f5 100644 --- a/apps/desktop-client/electron.vite.config.ts +++ b/apps/desktop-client/electron.vite.config.ts @@ -3,6 +3,8 @@ import { fileURLToPath } from "node:url"; import vue from "@vitejs/plugin-vue"; import { defineConfig, externalizeDepsPlugin } from "electron-vite"; +import Components from "unplugin-vue-components/vite"; +import { AntDesignVueResolver } from "unplugin-vue-components/resolvers"; const rootDir = fileURLToPath(new URL(".", import.meta.url)); @@ -48,7 +50,17 @@ export default defineConfig({ }, }, renderer: { - plugins: [vue()], + plugins: [ + vue(), + Components({ + dts: false, + resolvers: [ + AntDesignVueResolver({ + importStyle: false, + }), + ], + }), + ], resolve: { alias: { "@renderer": resolve(rootDir, "src/renderer"), diff --git a/apps/desktop-client/package.json b/apps/desktop-client/package.json index 111a206..afff018 100644 --- a/apps/desktop-client/package.json +++ b/apps/desktop-client/package.json @@ -33,6 +33,7 @@ "electron-builder": "^25.0.0", "electron-vite": "^2.0.0", "typescript": "^5.9.3", + "unplugin-vue-components": "^32.0.0", "vite": "^5.4.19", "vitest": "^2.0.0", "vue-tsc": "^3.2.6" diff --git a/apps/desktop-client/src/renderer/App.vue b/apps/desktop-client/src/renderer/App.vue index 79e5233..2c5665e 100644 --- a/apps/desktop-client/src/renderer/App.vue +++ b/apps/desktop-client/src/renderer/App.vue @@ -1,9 +1,9 @@ + + + + diff --git a/apps/desktop-client/src/renderer/components/DesktopShell.vue b/apps/desktop-client/src/renderer/components/DesktopShell.vue index 1e7c5eb..2fc2850 100644 --- a/apps/desktop-client/src/renderer/components/DesktopShell.vue +++ b/apps/desktop-client/src/renderer/components/DesktopShell.vue @@ -1,6 +1,13 @@ + + + + diff --git a/apps/desktop-client/src/renderer/components/PaginationBar.vue b/apps/desktop-client/src/renderer/components/PaginationBar.vue new file mode 100644 index 0000000..54561fc --- /dev/null +++ b/apps/desktop-client/src/renderer/components/PaginationBar.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/apps/desktop-client/src/renderer/composables/useDesktopRuntime.ts b/apps/desktop-client/src/renderer/composables/useDesktopRuntime.ts index 3ac6429..cd49337 100644 --- a/apps/desktop-client/src/renderer/composables/useDesktopRuntime.ts +++ b/apps/desktop-client/src/renderer/composables/useDesktopRuntime.ts @@ -2,12 +2,19 @@ import { computed, onMounted, onUnmounted, readonly, shallowRef } from "vue"; import type { DesktopRuntimeSnapshot } from "../types"; +const RUNTIME_POLL_INTERVAL_MS = 15_000; + const snapshot = shallowRef(null); const loading = shallowRef(false); const error = shallowRef(null); let intervalHandle: ReturnType | null = null; let subscribers = 0; +let visibilityListenerBound = false; + +function isPageVisible(): boolean { + return typeof document === "undefined" || document.visibilityState === "visible"; +} async function refreshRuntimeSnapshot() { loading.value = true; @@ -47,18 +54,8 @@ async function refreshAccounts() { } } -function startPolling() { - if (intervalHandle) { - return; - } - - intervalHandle = setInterval(() => { - void refreshRuntimeSnapshot(); - }, 15_000); -} - function stopPolling() { - if (subscribers > 0 || !intervalHandle) { + if (!intervalHandle) { return; } @@ -66,18 +63,66 @@ function stopPolling() { intervalHandle = null; } +function syncPollingState() { + if (subscribers === 0 || !isPageVisible()) { + stopPolling(); + return; + } + + if (intervalHandle) { + return; + } + + intervalHandle = setInterval(() => { + void refreshRuntimeSnapshot(); + }, RUNTIME_POLL_INTERVAL_MS); +} + +function handleVisibilityChange() { + if (isPageVisible()) { + void refreshRuntimeSnapshot(); + } + + syncPollingState(); +} + +function bindVisibilityListener() { + if (visibilityListenerBound || typeof document === "undefined") { + return; + } + + document.addEventListener("visibilitychange", handleVisibilityChange); + visibilityListenerBound = true; +} + +function unbindVisibilityListener() { + if (!visibilityListenerBound || typeof document === "undefined") { + return; + } + + document.removeEventListener("visibilitychange", handleVisibilityChange); + visibilityListenerBound = false; +} + export function useDesktopRuntime() { onMounted(() => { subscribers += 1; + bindVisibilityListener(); if (!snapshot.value && !loading.value) { void refreshRuntimeSnapshot(); } - startPolling(); + syncPollingState(); }); onUnmounted(() => { subscribers = Math.max(0, subscribers - 1); - stopPolling(); + if (subscribers === 0) { + stopPolling(); + unbindVisibilityListener(); + return; + } + + syncPollingState(); }); return { diff --git a/apps/desktop-client/src/renderer/lib/client-errors.ts b/apps/desktop-client/src/renderer/lib/client-errors.ts index 630bfc2..4441e67 100644 --- a/apps/desktop-client/src/renderer/lib/client-errors.ts +++ b/apps/desktop-client/src/renderer/lib/client-errors.ts @@ -1,5 +1,3 @@ -import { Modal } from "ant-design-vue"; - type ClientErrorTone = "error" | "warning"; type ClientActionKind = "bind-account" | "open-console" | "unbind-account"; @@ -119,8 +117,9 @@ function presentClientError(kind: ClientActionKind, error: unknown): ClientError }; } -export function showClientActionError(kind: ClientActionKind, error: unknown): void { +export async function showClientActionError(kind: ClientActionKind, error: unknown): Promise { const presentation = presentClientError(kind, error); + const { Modal } = await import("ant-design-vue"); const showModal = presentation.tone === "warning" ? Modal.warning : Modal.error; showModal({ diff --git a/apps/desktop-client/src/renderer/main.ts b/apps/desktop-client/src/renderer/main.ts index d47c431..10876eb 100644 --- a/apps/desktop-client/src/renderer/main.ts +++ b/apps/desktop-client/src/renderer/main.ts @@ -1,5 +1,4 @@ import { createApp } from "vue"; -import Antd from "ant-design-vue"; import App from "./App.vue"; import { router } from "./routes"; @@ -15,4 +14,4 @@ const themeQuery = window.matchMedia("(prefers-color-scheme: dark)"); applyTheme(themeQuery.matches); themeQuery.addEventListener("change", (event) => applyTheme(event.matches)); -createApp(App).use(Antd).use(router).mount("#app"); +createApp(App).use(router).mount("#app"); diff --git a/apps/desktop-client/src/renderer/routes.ts b/apps/desktop-client/src/renderer/routes.ts index 1dfd1ce..e82341d 100644 --- a/apps/desktop-client/src/renderer/routes.ts +++ b/apps/desktop-client/src/renderer/routes.ts @@ -1,20 +1,36 @@ -import { createRouter, createWebHashHistory } from "vue-router"; +import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router"; -import AccountsView from "./views/AccountsView.vue"; -import AiPlatformsView from "./views/AiPlatformsView.vue"; -import DiagnosticsView from "./views/DiagnosticsView.vue"; -import HomeView from "./views/HomeView.vue"; -import PublishManagementView from "./views/PublishManagementView.vue"; +const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "home", + component: () => import("./views/HomeView.vue"), + }, + { + path: "/publish-management", + name: "publish-management", + component: () => import("./views/PublishManagementView.vue"), + }, + { path: "/tasks", redirect: "/publish-management" }, + { + path: "/media-platforms", + name: "media-platforms", + component: () => import("./views/AccountsView.vue"), + }, + { + path: "/ai-platforms", + name: "ai-platforms", + component: () => import("./views/AiPlatformsView.vue"), + }, + { path: "/accounts", redirect: "/media-platforms" }, + { + path: "/diagnostics", + name: "diagnostics", + component: () => import("./views/DiagnosticsView.vue"), + }, +]; export const router = createRouter({ history: createWebHashHistory(), - routes: [ - { path: "/", name: "home", component: HomeView }, - { path: "/publish-management", name: "publish-management", component: PublishManagementView }, - { path: "/tasks", redirect: "/publish-management" }, - { path: "/media-platforms", name: "media-platforms", component: AccountsView }, - { path: "/ai-platforms", name: "ai-platforms", component: AiPlatformsView }, - { path: "/accounts", redirect: "/media-platforms" }, - { path: "/diagnostics", name: "diagnostics", component: DiagnosticsView }, - ], + routes, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db89061..7678638 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + unplugin-vue-components: + specifier: ^32.0.0 + version: 32.0.0(vue@3.5.31(typescript@5.9.3)) vite: specifier: ^5.4.19 version: 5.4.21(@types/node@24.12.0)(lightningcss@1.32.0) @@ -4182,6 +4185,16 @@ packages: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} + unplugin-vue-components@32.0.0: + resolution: {integrity: sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + unplugin@3.0.0: resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9235,6 +9248,19 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.4 + unplugin-vue-components@32.0.0(vue@3.5.31(typescript@5.9.3)): + dependencies: + chokidar: 5.0.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + obug: 2.1.1 + picomatch: 4.0.4 + tinyglobby: 0.2.15 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.31(typescript@5.9.3) + unplugin@3.0.0: dependencies: '@jridgewell/remapping': 2.3.5