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) <noreply@anthropic.com>
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
|
||||
import DesktopShell from "./components/DesktopShell.vue";
|
||||
import { defineAsyncComponent, onMounted } from "vue";
|
||||
import { useDesktopSession } from "./composables/useDesktopSession";
|
||||
import LoginView from "./views/LoginView.vue";
|
||||
|
||||
const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue"));
|
||||
const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue"));
|
||||
|
||||
const { isAuthenticated, syncRuntimeSession } = useDesktopSession();
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
tone?: "primary" | "secondary" | "ghost" | "danger" | "text";
|
||||
size?: "sm" | "md";
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
title?: string;
|
||||
}>(), {
|
||||
tone: "secondary",
|
||||
size: "md",
|
||||
loading: false,
|
||||
disabled: false,
|
||||
type: "button",
|
||||
title: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "click", payload: MouseEvent): void;
|
||||
}>();
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (props.loading || props.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
emit("click", event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="props.type"
|
||||
class="button"
|
||||
:class="[props.tone, props.size, { loading: props.loading }]"
|
||||
:disabled="props.loading || props.disabled"
|
||||
:title="props.title"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="button__content">
|
||||
<span v-if="props.loading || $slots.icon" class="button__icon" aria-hidden="true">
|
||||
<span v-if="props.loading" class="button__spinner"></span>
|
||||
<slot v-else name="icon" />
|
||||
</span>
|
||||
<span class="button__label">
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button {
|
||||
appearance: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: #1f2937;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
}
|
||||
|
||||
.button__spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
animation: spin 0.65s linear infinite;
|
||||
}
|
||||
|
||||
.button__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sm {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.md {
|
||||
min-height: 38px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.primary:hover:not(:disabled) {
|
||||
background: #4096ff;
|
||||
border-color: #4096ff;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: #f8fafc;
|
||||
border-color: #dbe5f0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.secondary:hover:not(:disabled) {
|
||||
background: #eef4fb;
|
||||
border-color: #bfd0e4;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ghost:hover:not(:disabled) {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: #fff1f2;
|
||||
border-color: #fecdd3;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.danger:hover:not(:disabled) {
|
||||
background: #ffe4e6;
|
||||
border-color: #fda4af;
|
||||
}
|
||||
|
||||
.text {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.text:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { RouterLink, RouterView, useRoute } from "vue-router";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
SendOutlined,
|
||||
LinkOutlined,
|
||||
RobotOutlined,
|
||||
ToolOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||
import { useDesktopSession } from "../composables/useDesktopSession";
|
||||
@@ -30,30 +37,35 @@ const navItems = computed(() => {
|
||||
title: "控制台",
|
||||
description: "运行总览、活动流和总体健康度",
|
||||
count: data?.summary.issuesOpen ?? 0,
|
||||
icon: AppstoreOutlined,
|
||||
},
|
||||
{
|
||||
to: "/publish-management",
|
||||
title: "发布管理",
|
||||
description: "查看待发布队列、历史发送结果,并对文章再次发送",
|
||||
count: data?.summary.queuedTasks ?? 0,
|
||||
icon: SendOutlined,
|
||||
},
|
||||
{
|
||||
to: "/media-platforms",
|
||||
title: "媒体平台",
|
||||
description: "发布媒体授权、多个账号绑定与表格化管理",
|
||||
count: accounts.filter((item) => publishIDs.has(item.platform)).length,
|
||||
icon: LinkOutlined,
|
||||
},
|
||||
{
|
||||
to: "/ai-platforms",
|
||||
title: "AI 平台",
|
||||
description: "AI 平台一平台一绑定,按卡片查看状态与归属",
|
||||
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
|
||||
icon: RobotOutlined,
|
||||
},
|
||||
{
|
||||
to: "/diagnostics",
|
||||
title: "诊断",
|
||||
description: "vault、scheduler、transport 与原始快照",
|
||||
count: data?.clients.length ?? 0,
|
||||
icon: ToolOutlined,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -90,6 +102,7 @@ const clientLabel = computed(
|
||||
@click="handleNavClick(item.to)"
|
||||
>
|
||||
<div class="nav-copy">
|
||||
<component :is="item.icon" class="nav-icon" />
|
||||
<strong>{{ item.title }}</strong>
|
||||
</div>
|
||||
<span class="nav-count">{{ item.count }}</span>
|
||||
@@ -207,40 +220,56 @@ h1 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6edf5;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
border-color: #91caff;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: #f0f7ff;
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.1);
|
||||
background: #e6f4ff;
|
||||
border-color: #e6f4ff;
|
||||
}
|
||||
|
||||
.nav-copy {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 16px;
|
||||
color: #8c8c8c;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover .nav-icon {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.nav-link.active .nav-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.nav-copy strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.nav-link:hover .nav-copy strong {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.nav-link.active .nav-copy strong {
|
||||
color: #1677ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-count {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
title: string;
|
||||
open?: boolean;
|
||||
}>(), {
|
||||
open: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details class="panel" :open="open">
|
||||
<summary class="panel__summary">
|
||||
<span class="panel__title">{{ title }}</span>
|
||||
<span class="panel__indicator" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="panel__body">
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 10px;
|
||||
background: #fafafb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel + .panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.panel__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.panel__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel__indicator {
|
||||
position: relative;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel__indicator::before,
|
||||
.panel__indicator::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: #64748b;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.panel__indicator::after {
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
|
||||
.panel[open] .panel__indicator::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.panel__body {
|
||||
border-top: 1px dashed #e6edf5;
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel__title {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
maxVisiblePages?: number;
|
||||
}>(), {
|
||||
maxVisiblePages: 5,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "change", page: number): void;
|
||||
}>();
|
||||
|
||||
type PageToken = number | "ellipsis";
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize)));
|
||||
|
||||
const tokens = computed<PageToken[]>(() => {
|
||||
if (totalPages.value <= props.maxVisiblePages) {
|
||||
return Array.from({ length: totalPages.value }, (_value, index) => index + 1);
|
||||
}
|
||||
|
||||
const pages: PageToken[] = [1];
|
||||
let start = Math.max(2, props.current - 1);
|
||||
let end = Math.min(totalPages.value - 1, props.current + 1);
|
||||
|
||||
while (end - start < 2 && start > 2) {
|
||||
start -= 1;
|
||||
}
|
||||
while (end - start < 2 && end < totalPages.value - 1) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
if (start > 2) {
|
||||
pages.push("ellipsis");
|
||||
}
|
||||
|
||||
for (let page = start; page <= end; page += 1) {
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
if (end < totalPages.value - 1) {
|
||||
pages.push("ellipsis");
|
||||
}
|
||||
|
||||
pages.push(totalPages.value);
|
||||
return pages;
|
||||
});
|
||||
|
||||
function goTo(page: number) {
|
||||
if (page === props.current || page < 1 || page > totalPages.value) {
|
||||
return;
|
||||
}
|
||||
emit("change", page);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="totalPages > 1" class="pagination" aria-label="Pagination">
|
||||
<button class="pagination__nav" :disabled="props.current <= 1" @click="goTo(props.current - 1)">
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<div class="pagination__pages">
|
||||
<template v-for="token in tokens" :key="`${token}-${typeof token === 'number' ? token : Math.random()}`">
|
||||
<span v-if="token === 'ellipsis'" class="pagination__ellipsis">…</span>
|
||||
<button
|
||||
v-else
|
||||
class="pagination__page"
|
||||
:class="{ active: token === props.current }"
|
||||
@click="goTo(token)"
|
||||
>
|
||||
{{ token }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button class="pagination__nav" :disabled="props.current >= totalPages" @click="goTo(props.current + 1)">
|
||||
下一页
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination__pages {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination__nav,
|
||||
.pagination__page {
|
||||
appearance: none;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #dbe5f0;
|
||||
background: #ffffff;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination__nav:hover:not(:disabled),
|
||||
.pagination__page:hover:not(:disabled) {
|
||||
background: #f8fafc;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.pagination__nav:disabled,
|
||||
.pagination__page:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination__page.active {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pagination__ellipsis {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -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<DesktopRuntimeSnapshot | null>(null);
|
||||
const loading = shallowRef(false);
|
||||
const error = shallowRef<string | null>(null);
|
||||
|
||||
let intervalHandle: ReturnType<typeof setInterval> | 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 {
|
||||
|
||||
@@ -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<void> {
|
||||
const presentation = presentClientError(kind, error);
|
||||
const { Modal } = await import("ant-design-vue");
|
||||
const showModal = presentation.tone === "warning" ? Modal.warning : Modal.error;
|
||||
|
||||
showModal({
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Generated
+26
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user