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 @@
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
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