133 lines
7.5 KiB
Vue
133 lines
7.5 KiB
Vue
<!-- SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-project-management-mdtodo. -->
|
|
<!-- Responsibility: App shell navigation entry for project-management pages without Workbench embedding. -->
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import StatusBadge from "@/components/common/StatusBadge.vue";
|
|
import WorkbenchBuildSummary from "@/components/workbench/WorkbenchBuildSummary.vue";
|
|
import WorkbenchProbePanel from "@/components/workbench/WorkbenchProbePanel.vue";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useAppStore } from "@/stores/app";
|
|
import { useWorkbenchStore } from "@/stores/workbench";
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
const app = useAppStore();
|
|
const workbench = useWorkbenchStore();
|
|
const diagnosticsOpen = ref(false);
|
|
|
|
interface NavItem {
|
|
name: string;
|
|
label: string;
|
|
path: string;
|
|
icon: string;
|
|
navId: string;
|
|
}
|
|
|
|
interface NavSection {
|
|
title: string;
|
|
items: NavItem[];
|
|
}
|
|
|
|
const navSections = [
|
|
{ title: "工作台", items: [{ name: "CodeWorkbench", label: "Code", path: "/workbench", icon: "C", navId: "workbench.code" }, { name: "OpenCode", label: "OpenCode", path: "/opencode", icon: "O", navId: "opencode.root" }, { name: "Dashboard", label: "概览", path: "/dashboard", icon: "D", navId: "user.dashboard" }] },
|
|
{ title: "项目", items: [{ name: "Projects", label: "项目", path: "/projects", icon: "P", navId: "project.overview" }, { name: "ProjectMdtodo", label: "MDTODO", path: "/projects/mdtodo", icon: "M", navId: "project.mdtodo" }] },
|
|
{ title: "用户", items: [{ name: "ApiKeys", label: "API Keys", path: "/api-keys", icon: "K", navId: "user.apiKeys" }, { name: "Usage", label: "用量", path: "/usage", icon: "U", navId: "user.usage" }, { name: "Billing", label: "充值", path: "/billing", icon: "$", navId: "user.billing" }] },
|
|
{ title: "管理", items: [{ name: "Access", label: "授权", path: "/admin/access", icon: "A", navId: "admin.access" }, { name: "AdminSecrets", label: "密钥", path: "/admin/secrets", icon: "K", navId: "admin.secrets" }, { name: "Users", label: "用户", path: "/admin/users", icon: "P", navId: "admin.users" }, { name: "AdminBilling", label: "账务", path: "/admin/billing", icon: "B", navId: "admin.billing" }, { name: "HwpodGroups", label: "HWPOD", path: "/admin/hwpod-groups", icon: "H", navId: "admin.hwpodGroups" }, { name: "ProviderProfiles", label: "Profiles", path: "/admin/provider-profiles", icon: "R", navId: "admin.providerProfiles" }] },
|
|
{ title: "系统", items: [{ name: "Gate", label: "Gate", path: "/gate", icon: "G", navId: "system.gate" }, { name: "Performance", label: "性能", path: "/performance", icon: "M", navId: "system.performance" }, { name: "Skills", label: "Skills", path: "/skills", icon: "S", navId: "system.skills" }, { name: "Settings", label: "设置", path: "/settings", icon: "T", navId: "system.settings" }, { name: "Help", label: "帮助", path: "/help", icon: "?", navId: "system.help" }] }
|
|
] satisfies NavSection[];
|
|
|
|
const shellless = computed(() => route.name === "Login" || route.name === "Register" || route.name === "NotFound");
|
|
const showWorkbenchDiagnostics = computed(() => route.meta.section === "workbench");
|
|
const visibleNavSections = computed(() => navSections
|
|
.map((section) => ({ ...section, items: section.items.filter((item) => auth.canAccessNav(item.navId)) }))
|
|
.filter((section) => section.items.length > 0));
|
|
|
|
watch(showWorkbenchDiagnostics, (visible) => {
|
|
if (!visible) diagnosticsOpen.value = false;
|
|
});
|
|
|
|
async function go(path: string): Promise<void> {
|
|
if (route.path === path) return;
|
|
app.setLoading(true);
|
|
try {
|
|
await router.push(path);
|
|
} finally {
|
|
window.setTimeout(() => app.setLoading(false), 120);
|
|
}
|
|
}
|
|
|
|
function isNavItemActive(item: NavItem): boolean {
|
|
return route.name === item.name || (item.name === "CodeWorkbench" && route.meta.section === "workbench");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<main v-if="shellless" class="auth-main">
|
|
<slot />
|
|
</main>
|
|
<main v-else class="platform-shell" :class="{ 'is-sidebar-collapsed': app.sidebarCollapsed }">
|
|
<aside class="platform-sidebar" :data-collapsed="app.sidebarCollapsed">
|
|
<div class="brand-lockup">
|
|
<span class="brand-mark">HW</span>
|
|
<div>
|
|
<strong>HWLAB</strong>
|
|
<small>v0.3 Cloud Web</small>
|
|
</div>
|
|
</div>
|
|
<nav class="nav-groups" aria-label="主导航">
|
|
<section v-for="section in visibleNavSections" :key="section.title" class="nav-section">
|
|
<h2>{{ section.title }}</h2>
|
|
<button v-for="item in section.items" :key="item.name" class="nav-item" :data-active="isNavItemActive(item)" type="button" :title="item.label" :aria-label="item.label" @click="go(item.path)">
|
|
<span class="nav-icon" aria-hidden="true">{{ item.icon }}</span>
|
|
<span class="nav-label">{{ item.label }}</span>
|
|
</button>
|
|
</section>
|
|
</nav>
|
|
</aside>
|
|
<section class="platform-main">
|
|
<header class="platform-topbar">
|
|
<button class="icon-button" type="button" aria-label="折叠侧边栏" @click="app.sidebarCollapsed = !app.sidebarCollapsed">☰</button>
|
|
<div class="topbar-status">
|
|
<span v-if="app.loading" class="topbar-progress" aria-live="polite"><span aria-hidden="true" /></span>
|
|
<span>同源 API</span>
|
|
<strong>{{ auth.user?.displayName || auth.user?.username || auth.user?.name || "HWLAB 用户" }}</strong>
|
|
</div>
|
|
<div class="topbar-actions">
|
|
<button v-if="showWorkbenchDiagnostics" id="workbench-diagnostics-toggle" class="workbench-diagnostics-toggle" type="button" aria-haspopup="dialog" @click="diagnosticsOpen = true">
|
|
<span aria-hidden="true">i</span>
|
|
<StatusBadge :status="workbench.chatPending ? 'running' : 'pending'" :label="workbench.chatPending ? '处理中' : '诊断'" />
|
|
</button>
|
|
<button class="btn btn-secondary" type="button" @click="auth.logout">退出</button>
|
|
</div>
|
|
</header>
|
|
<div class="platform-content">
|
|
<slot />
|
|
</div>
|
|
<div v-if="showWorkbenchDiagnostics && diagnosticsOpen" class="workbench-dialog-backdrop" role="presentation" @click.self="diagnosticsOpen = false">
|
|
<section id="workbench-diagnostics-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="workbench-diagnostics-title">
|
|
<header>
|
|
<div class="dialog-title-stack">
|
|
<h2 id="workbench-diagnostics-title">Workbench 诊断</h2>
|
|
<p>Code Agent session、trace、HWPOD 诊断仍走 Cloud Web 同源 API。</p>
|
|
</div>
|
|
<button type="button" class="dialog-close" aria-label="关闭" @click="diagnosticsOpen = false">x</button>
|
|
</header>
|
|
<div class="workbench-diagnostics-grid">
|
|
<dl class="workbench-diagnostics-summary">
|
|
<div><dt>Provider</dt><dd>{{ workbench.providerProfile }}</dd></div>
|
|
<div><dt>Composer</dt><dd>{{ workbench.composer.submitMode }}{{ workbench.composer.targetTraceId ? ` ${workbench.composer.targetTraceId}` : '' }}</dd></div>
|
|
<div><dt>Timeout</dt><dd>inactivity {{ Math.round(workbench.codeAgentTimeoutMs / 1000) }}s</dd></div>
|
|
<div><dt>Session</dt><dd>{{ workbench.selectedSessionId || '未选择' }}</dd></div>
|
|
</dl>
|
|
<WorkbenchProbePanel :live="workbench.live" />
|
|
<WorkbenchBuildSummary :live="workbench.live" />
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|