refactor(web): 重写非 Workbench 工业控制台视觉层
This commit is contained in:
@@ -218,6 +218,46 @@ test("R6 MDTODO task rows expose opaque task references", () => {
|
||||
assert.match(taskTreeSource, /:data-task-ref="task\.taskRef"/u);
|
||||
});
|
||||
|
||||
test("R2 console rewrite owns the final visual layer and removes private letter glyphs", () => {
|
||||
const mainSource = read("src/main.ts");
|
||||
assert.match(mainSource, /console-shell\.css/u);
|
||||
assert.match(mainSource, /console-r2\.css/u);
|
||||
assert.doesNotMatch(mainSource, /console-legacy\.css/u);
|
||||
|
||||
const iconSource = read("src/components/console/ConsoleIcon.vue");
|
||||
for (const icon of ["menu", "info", "logout", "empty", "project", "key", "provider", "skills"])
|
||||
assert.match(iconSource, new RegExp(`\\b${icon}:`, "u"), `ConsoleIcon should define ${icon}`);
|
||||
|
||||
const migratedViews = [
|
||||
"src/components/common/EmptyState.vue",
|
||||
"src/views/SkillsView.vue",
|
||||
"src/views/projects/ProjectsView.vue",
|
||||
"src/views/admin/ProviderProfilesView.vue",
|
||||
"src/views/user/ApiKeysView.vue",
|
||||
];
|
||||
for (const relativePath of migratedViews) {
|
||||
const source = read(relativePath);
|
||||
assert.match(source, /ConsoleIcon/u, `${relativePath} should use the shared icon system`);
|
||||
assert.doesNotMatch(source, />\s*(?:SK|PJ|AI|KEY|HW)\s*</u, `${relativePath} should not render private letter glyphs`);
|
||||
}
|
||||
|
||||
const r2Styles = read("src/styles/console-r2.css");
|
||||
for (const selector of [".platform-sidebar", ".page-command-bar", ".console-status-strip", ".console-data-grid", ".pcb-card", ".mdtodo-workspace"])
|
||||
assert.match(r2Styles, new RegExp(selector.replaceAll(".", "\\."), "u"), `R2 visual layer should own ${selector}`);
|
||||
assert.match(r2Styles, /@media \(max-width: 1100px\)/u);
|
||||
assert.match(r2Styles, /@media \(max-width: 760px\)/u);
|
||||
assert.match(r2Styles, /@media \(max-width: 430px\)/u);
|
||||
assert.match(r2Styles, /prefers-reduced-motion/u);
|
||||
|
||||
const collectionSource = read("src/components/common/ResourceCollection.vue");
|
||||
assert.match(collectionSource, /VirtualList/u);
|
||||
assert.match(collectionSource, /virtualizeAfter/u);
|
||||
assert.match(collectionSource, /itemIcon/u);
|
||||
const cardSource = read("src/components/common/EntityCard.vue");
|
||||
assert.match(cardSource, /ConsoleIcon/u);
|
||||
assert.match(cardSource, /entity-card-meta/u);
|
||||
});
|
||||
|
||||
test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => {
|
||||
const issueBody = readIssueBodyFixture();
|
||||
assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import ApiErrorDiagnostic from "./ApiErrorDiagnostic.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import type { ApiError, ErrorDiagnostic } from "@/types";
|
||||
|
||||
defineProps<{ title: string; description?: string; actionLabel?: string; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null }>();
|
||||
withDefaults(defineProps<{ title: string; description?: string; actionLabel?: string; icon?: string; apiError?: ApiError | null; diagnostic?: ErrorDiagnostic | null }>(), {
|
||||
icon: "empty"
|
||||
});
|
||||
defineEmits<{ action: [] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="empty-state">
|
||||
<div class="empty-state-mark">HW</div>
|
||||
<div class="empty-state-mark" aria-hidden="true"><ConsoleIcon :name="icon" :size="20" /></div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p v-if="description">{{ description }}</p>
|
||||
<ApiErrorDiagnostic v-if="apiError || diagnostic" :error="description" :api-error="apiError" :diagnostic="diagnostic" compact :show-message="false" />
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import StatusBadge from "./StatusBadge.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
status?: string;
|
||||
statusLabel?: string;
|
||||
icon?: string;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
interactive?: boolean;
|
||||
@@ -17,6 +19,7 @@ const props = withDefaults(defineProps<{
|
||||
subtitle: "",
|
||||
status: "",
|
||||
statusLabel: "",
|
||||
icon: "",
|
||||
selected: false,
|
||||
disabled: false,
|
||||
interactive: true
|
||||
@@ -50,12 +53,16 @@ function onKeydown(event: KeyboardEvent): void {
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<header class="entity-card-header">
|
||||
<div class="entity-card-title-stack">
|
||||
<strong>{{ title }}</strong>
|
||||
<small v-if="subtitle">{{ subtitle }}</small>
|
||||
<div class="entity-card-heading">
|
||||
<span v-if="icon || $slots.icon" class="entity-card-icon" aria-hidden="true"><slot name="icon"><ConsoleIcon :name="icon" /></slot></span>
|
||||
<div class="entity-card-title-stack">
|
||||
<strong>{{ title }}</strong>
|
||||
<small v-if="subtitle">{{ subtitle }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge v-if="status || statusLabel" :status="status" :label="statusLabel" />
|
||||
</header>
|
||||
<div v-if="$slots.meta" class="entity-card-meta"><slot name="meta" /></div>
|
||||
<div v-if="$slots.default" class="entity-card-body"><slot /></div>
|
||||
<footer v-if="$slots.footer" class="entity-card-footer" @click.stop><slot name="footer" /></footer>
|
||||
</article>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<script setup lang="ts" generic="T extends object">
|
||||
import EntityCard from "./EntityCard.vue";
|
||||
import VirtualList from "./VirtualList.vue";
|
||||
|
||||
withDefaults(defineProps<{
|
||||
items: T[];
|
||||
@@ -11,22 +12,30 @@ withDefaults(defineProps<{
|
||||
itemTitle?: (item: T) => string;
|
||||
itemSubtitle?: (item: T) => string;
|
||||
itemStatus?: (item: T) => string;
|
||||
itemIcon?: (item: T) => string;
|
||||
view?: "cards" | "list";
|
||||
density?: "compact" | "comfortable";
|
||||
selectedId?: string | null;
|
||||
ariaLabel?: string;
|
||||
emptyText?: string;
|
||||
showControls?: boolean;
|
||||
virtualizeAfter?: number;
|
||||
virtualItemHeight?: number;
|
||||
virtualHeight?: number | string;
|
||||
}>(), {
|
||||
itemTitle: undefined,
|
||||
itemSubtitle: undefined,
|
||||
itemStatus: undefined,
|
||||
itemIcon: undefined,
|
||||
view: "cards",
|
||||
density: "comfortable",
|
||||
selectedId: null,
|
||||
ariaLabel: "资源集合",
|
||||
emptyText: "暂无资源。",
|
||||
showControls: true
|
||||
showControls: true,
|
||||
virtualizeAfter: 80,
|
||||
virtualItemHeight: 56,
|
||||
virtualHeight: "min(62vh, 680px)"
|
||||
});
|
||||
|
||||
const emitEvent = defineEmits<{
|
||||
@@ -55,7 +64,37 @@ function selectItem(item: T): void {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="items.length" class="resource-collection-items" :data-view="view" :data-density="density" role="list" :aria-label="ariaLabel">
|
||||
<VirtualList
|
||||
v-if="items.length >= virtualizeAfter && view === 'list'"
|
||||
class="resource-collection-virtual"
|
||||
:items="items"
|
||||
:item-key="itemKey"
|
||||
:item-height="virtualItemHeight"
|
||||
:height="virtualHeight"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="resource-collection-item resource-collection-item--virtual">
|
||||
<slot
|
||||
v-if="$slots.list"
|
||||
name="list"
|
||||
:item="item"
|
||||
:selected="itemKey(item) === selectedId"
|
||||
:select="() => selectItem(item)"
|
||||
/>
|
||||
<EntityCard
|
||||
v-else
|
||||
:title="itemTitle?.(item) || itemKey(item)"
|
||||
:subtitle="itemSubtitle?.(item)"
|
||||
:status="itemStatus?.(item)"
|
||||
:icon="itemIcon?.(item)"
|
||||
:selected="itemKey(item) === selectedId"
|
||||
@select="selectItem(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VirtualList>
|
||||
<div v-else-if="items.length" class="resource-collection-items" :data-view="view" :data-density="density" role="list" :aria-label="ariaLabel">
|
||||
<div v-for="item in items" :key="itemKey(item)" class="resource-collection-item" role="listitem">
|
||||
<slot
|
||||
v-if="view === 'list' && $slots.list"
|
||||
@@ -69,6 +108,7 @@ function selectItem(item: T): void {
|
||||
:title="itemTitle?.(item) || itemKey(item)"
|
||||
:subtitle="itemSubtitle?.(item)"
|
||||
:status="itemStatus?.(item)"
|
||||
:icon="itemIcon?.(item)"
|
||||
:selected="itemKey(item) === selectedId"
|
||||
@select="selectItem(item)"
|
||||
/>
|
||||
@@ -84,6 +124,7 @@ function selectItem(item: T): void {
|
||||
:title="itemTitle?.(item) || itemKey(item)"
|
||||
:subtitle="itemSubtitle?.(item)"
|
||||
:status="itemStatus?.(item)"
|
||||
:icon="itemIcon?.(item)"
|
||||
:selected="itemKey(item) === selectedId"
|
||||
@select="selectItem(item)"
|
||||
/>
|
||||
|
||||
@@ -86,6 +86,10 @@ const paths: Record<string, string[]> = {
|
||||
"M12 18h.01",
|
||||
"M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20",
|
||||
],
|
||||
menu: ["M4 7h16", "M4 12h16", "M4 17h16"],
|
||||
info: ["M12 11v6", "M12 7h.01", "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20"],
|
||||
logout: ["M10 5H5v14h5", "M14 8l4 4-4 4", "M18 12H9"],
|
||||
empty: ["M5 7h14v12H5z", "M8 4h8", "M9 12h6", "M12 9v6"],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRoute } from "vue-router";
|
||||
import BaseDialog from "@/components/common/BaseDialog.vue";
|
||||
import BaseDrawer from "@/components/common/BaseDrawer.vue";
|
||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import WorkbenchBuildSummary from "@/components/workbench/WorkbenchBuildSummary.vue";
|
||||
import WorkbenchProbePanel from "@/components/workbench/WorkbenchProbePanel.vue";
|
||||
import AppNavigation from "./AppNavigation.vue";
|
||||
@@ -42,8 +43,8 @@ watch(showWorkbenchDiagnostics, (visible) => {
|
||||
</aside>
|
||||
<section class="platform-main">
|
||||
<header class="platform-topbar">
|
||||
<button class="icon-button desktop-sidebar-toggle" type="button" :aria-label="app.sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'" @click="app.sidebarCollapsed = !app.sidebarCollapsed">☰</button>
|
||||
<button class="icon-button mobile-sidebar-toggle" type="button" aria-label="打开主导航" @click="app.mobileOpen = true">☰</button>
|
||||
<button class="icon-button desktop-sidebar-toggle" type="button" :aria-label="app.sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'" @click="app.sidebarCollapsed = !app.sidebarCollapsed"><ConsoleIcon name="menu" /></button>
|
||||
<button class="icon-button mobile-sidebar-toggle" type="button" aria-label="打开主导航" @click="app.mobileOpen = true"><ConsoleIcon name="menu" /></button>
|
||||
<div class="topbar-status">
|
||||
<span v-if="app.loading" class="topbar-progress" aria-live="polite"><span aria-hidden="true" /></span>
|
||||
<span>同源 API</span>
|
||||
@@ -51,10 +52,10 @@ watch(showWorkbenchDiagnostics, (visible) => {
|
||||
</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>
|
||||
<ConsoleIcon name="info" :size="16" />
|
||||
<StatusBadge :status="workbench.chatPending ? 'running' : 'pending'" :label="workbench.chatPending ? '处理中' : '诊断'" />
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click="auth.logout">退出</button>
|
||||
<button class="btn btn-secondary" type="button" @click="auth.logout"><ConsoleIcon name="logout" :size="16" />退出</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="platform-content">
|
||||
|
||||
@@ -12,10 +12,11 @@ import { workbenchRuntimePolicy } from "@/config/workbench-runtime-policy";
|
||||
import { installWebRum } from "@/utils/rum";
|
||||
import "./styles/tokens.css";
|
||||
import "./style.css";
|
||||
import "./styles/console-legacy.css";
|
||||
import "./styles/console-shell.css";
|
||||
import "./styles/workbench.css";
|
||||
import "./styles/console-pages.css";
|
||||
import "./styles/console-foundation.css";
|
||||
import "./styles/console-r2.css";
|
||||
|
||||
function initThemeClass(): void {
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
/* SPEC: PJ2026-010405 云端控制台,MDTODO hwlab-web-product-experience/R2。 */
|
||||
/* Responsibility: 统一非 Workbench 页面的工业运营台壳层、密度、状态和响应式视觉。 */
|
||||
|
||||
:root {
|
||||
--console-canvas: #dfe5e2;
|
||||
--console-surface: #f5f6f2;
|
||||
--console-surface-raised: #fbfcf8;
|
||||
--console-surface-muted: #edf0ec;
|
||||
--console-border: #bcc7c4;
|
||||
--console-border-strong: #8fa09c;
|
||||
--console-shadow-sm: 0 1px 0 rgb(255 255 255 / 65%), 0 1px 3px rgb(8 20 22 / 9%);
|
||||
--console-shadow-md: 0 18px 48px rgb(8 20 22 / 22%);
|
||||
--console-sidebar-width: 256px;
|
||||
--console-sidebar-collapsed-width: 68px;
|
||||
--console-topbar-height: 46px;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(rgb(17 25 28 / 3%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgb(17 25 28 / 3%) 1px, transparent 1px),
|
||||
var(--console-canvas);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.platform-shell {
|
||||
grid-template-columns: var(--console-sidebar-width) minmax(0, 1fr);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.platform-shell.is-sidebar-collapsed {
|
||||
grid-template-columns: var(--console-sidebar-collapsed-width) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.platform-sidebar {
|
||||
gap: 14px;
|
||||
border-right-color: #263538;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(255 255 255 / 4%), transparent 120px),
|
||||
#10191c;
|
||||
padding: 14px 10px;
|
||||
color: #dce7e4;
|
||||
box-shadow: 8px 0 28px rgb(8 20 22 / 12%);
|
||||
}
|
||||
|
||||
.brand-lockup {
|
||||
min-height: 42px;
|
||||
border-bottom: 1px solid #2b3b3e;
|
||||
padding: 0 6px 12px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #37565a;
|
||||
border-radius: 5px;
|
||||
background: #18272a;
|
||||
color: #59d0d0;
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 3%);
|
||||
}
|
||||
|
||||
.brand-lockup strong {
|
||||
color: #f3f8f6;
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.brand-lockup small {
|
||||
color: #819492;
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-groups {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-section h2 {
|
||||
margin: 7px 8px 4px;
|
||||
color: #718582;
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: .11em;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
min-height: 34px;
|
||||
gap: 9px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
color: #b7c6c3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-item::before {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
left: -10px;
|
||||
width: 2px;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
border-color: #2f4346;
|
||||
background: #172427;
|
||||
color: #f0f6f4;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: #809793;
|
||||
}
|
||||
|
||||
.nav-item[data-active="true"] {
|
||||
border-color: #31565a;
|
||||
background: #173135;
|
||||
color: #eaf8f6;
|
||||
box-shadow: inset 0 0 0 1px rgb(70 204 204 / 4%);
|
||||
}
|
||||
|
||||
.nav-item[data-active="true"]::before {
|
||||
background: #43c7c7;
|
||||
}
|
||||
|
||||
.nav-item[data-active="true"] .nav-icon {
|
||||
background: transparent;
|
||||
color: #54d2d0;
|
||||
}
|
||||
|
||||
.platform-main {
|
||||
grid-template-rows: var(--console-topbar-height) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.platform-topbar {
|
||||
gap: 8px;
|
||||
border-bottom-color: var(--console-border-strong);
|
||||
background: rgb(245 247 244 / 94%);
|
||||
padding: 0 14px;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.platform-topbar .icon-button,
|
||||
.platform-topbar .btn {
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.topbar-status {
|
||||
gap: 8px;
|
||||
font-family: var(--console-font-mono);
|
||||
}
|
||||
|
||||
.topbar-status::before {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--console-green-700);
|
||||
box-shadow: 0 0 0 3px var(--console-green-100);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.topbar-status span {
|
||||
color: var(--console-graphite-600);
|
||||
font-size: 9px;
|
||||
letter-spacing: .07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.topbar-status strong {
|
||||
overflow: hidden;
|
||||
color: var(--console-graphite-800);
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.platform-content {
|
||||
padding: 14px clamp(12px, 1.5vw, 24px) 24px;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.route-stack,
|
||||
.hwpod-console,
|
||||
.mdtodo-page {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-command-bar {
|
||||
position: relative;
|
||||
min-height: 76px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--console-border);
|
||||
border-radius: 6px;
|
||||
background:
|
||||
linear-gradient(90deg, rgb(0 131 138 / 6%), transparent 34%),
|
||||
var(--console-surface-raised);
|
||||
padding: 12px 14px 12px 18px;
|
||||
box-shadow: var(--console-shadow-sm);
|
||||
}
|
||||
|
||||
.page-command-bar::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
border-radius: 5px 0 0 5px;
|
||||
background: var(--console-cyan-600);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.page-command-bar[data-sticky="true"] {
|
||||
top: 8px;
|
||||
border-bottom: 1px solid var(--console-border);
|
||||
background: rgb(251 252 248 / 96%);
|
||||
}
|
||||
|
||||
.page-command-copy {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.page-command-eyebrow {
|
||||
color: var(--console-cyan-700);
|
||||
font-size: 9px;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
|
||||
.page-command-title {
|
||||
font-size: clamp(20px, 2vw, 26px);
|
||||
font-weight: 720;
|
||||
letter-spacing: -.018em;
|
||||
}
|
||||
|
||||
.page-command-description {
|
||||
color: var(--console-graphite-600);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.console-status-strip {
|
||||
gap: 0;
|
||||
border-radius: 5px;
|
||||
background: var(--console-surface-raised);
|
||||
box-shadow: var(--console-shadow-sm);
|
||||
}
|
||||
|
||||
.console-status-item {
|
||||
position: relative;
|
||||
min-width: 132px;
|
||||
min-height: 46px;
|
||||
border-right: 1px solid var(--console-border);
|
||||
background: transparent;
|
||||
padding: 7px 10px 7px 15px;
|
||||
}
|
||||
|
||||
.console-status-item:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.console-status-item::before {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
left: 7px;
|
||||
width: 2px;
|
||||
height: auto;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.console-status-item dt {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-status-item dd {
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-panel,
|
||||
.table-page-layout,
|
||||
.console-section-frame,
|
||||
.operations-workspace,
|
||||
.bounded-workspace,
|
||||
.entity-index,
|
||||
.operations-inspector-panel,
|
||||
.metric-card,
|
||||
.empty-state {
|
||||
border-color: var(--console-border);
|
||||
border-radius: 6px;
|
||||
background: var(--console-surface-raised);
|
||||
box-shadow: var(--console-shadow-sm);
|
||||
}
|
||||
|
||||
.data-panel,
|
||||
.table-page-layout,
|
||||
.admin-crud-panel {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.table-page-header,
|
||||
.operations-inspector-header,
|
||||
.entity-index-toolbar {
|
||||
border-bottom-color: var(--console-border);
|
||||
}
|
||||
|
||||
.panel-header h2,
|
||||
.table-page-header h2,
|
||||
.operations-inspector-header h2 {
|
||||
color: var(--console-graphite-900);
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.icon-button,
|
||||
.table-action,
|
||||
.segmented-control,
|
||||
.segmented-control button,
|
||||
.view-mode-switch,
|
||||
.view-mode-switch button,
|
||||
.console-tabs,
|
||||
.console-tabs button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.table-action,
|
||||
.segmented-control button,
|
||||
.view-mode-switch button,
|
||||
.console-tabs button {
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.table-action.primary {
|
||||
border-color: #006b70;
|
||||
background: #007b80;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.table-action.primary:hover:not(:disabled) {
|
||||
background: #00666b;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.icon-button,
|
||||
.table-action,
|
||||
.segmented-control button,
|
||||
.view-mode-switch button,
|
||||
.console-tabs button {
|
||||
border-color: var(--console-border-strong);
|
||||
background: var(--console-surface-raised);
|
||||
color: var(--console-graphite-700);
|
||||
}
|
||||
|
||||
.input,
|
||||
input:not([type="checkbox"]):not([type="radio"]),
|
||||
select,
|
||||
textarea {
|
||||
border-color: var(--console-border-strong);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
button:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--console-focus) 55%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.console-data-grid-wrap,
|
||||
.data-table-wrap {
|
||||
border-color: var(--console-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console-data-grid,
|
||||
.data-table {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.console-data-grid thead,
|
||||
.data-table thead {
|
||||
background: #e5eae7;
|
||||
}
|
||||
|
||||
.console-data-grid th,
|
||||
.data-table th {
|
||||
color: var(--console-graphite-700);
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 760;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.console-data-grid td,
|
||||
.console-data-grid th,
|
||||
.data-table td,
|
||||
.data-table th {
|
||||
border-color: #d5dcda;
|
||||
}
|
||||
|
||||
.console-data-grid tbody tr:nth-child(even),
|
||||
.data-table tbody tr:nth-child(even) {
|
||||
background: #f5f7f4;
|
||||
}
|
||||
|
||||
.console-data-grid tbody tr:hover,
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--console-cyan-100);
|
||||
}
|
||||
|
||||
.resource-collection-items[data-view="cards"] {
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
|
||||
}
|
||||
|
||||
.entity-card-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.entity-card-icon {
|
||||
display: inline-grid;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid #80aaa7;
|
||||
border-radius: 4px;
|
||||
background: #dcecea;
|
||||
color: #146866;
|
||||
}
|
||||
|
||||
.entity-card-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px 10px;
|
||||
color: var(--console-graphite-600);
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.resource-collection-virtual {
|
||||
border: 1px solid var(--console-border);
|
||||
border-radius: 4px;
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
.resource-collection-item--virtual {
|
||||
height: 100%;
|
||||
border-bottom: 1px solid #d8dfdd;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.resource-collection-item--virtual .entity-card {
|
||||
height: 100%;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 6px 9px;
|
||||
}
|
||||
|
||||
.entity-card,
|
||||
.project-card,
|
||||
.skill-card,
|
||||
.provider-card,
|
||||
.api-key-card,
|
||||
.pcb-card,
|
||||
.node-card {
|
||||
border-color: var(--console-border);
|
||||
border-radius: 5px;
|
||||
background:
|
||||
linear-gradient(135deg, rgb(0 131 138 / 5%), transparent 46%),
|
||||
var(--console-surface-raised);
|
||||
box-shadow: var(--console-shadow-sm);
|
||||
}
|
||||
|
||||
.entity-card[data-interactive="true"]:hover,
|
||||
.project-card:hover,
|
||||
.skill-card:hover,
|
||||
.provider-card:hover,
|
||||
.api-key-card:hover {
|
||||
border-color: var(--console-cyan-600);
|
||||
box-shadow: 0 8px 22px rgb(11 31 33 / 11%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 148px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state-mark,
|
||||
.skill-glyph,
|
||||
.project-glyph,
|
||||
.api-key-glyph,
|
||||
.provider-glyph {
|
||||
display: inline-grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border: 1px solid #6faaa7;
|
||||
border-radius: 5px;
|
||||
background: #d9eeeb;
|
||||
color: #126764;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.provider-glyph {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-pill,
|
||||
.status-badge,
|
||||
.pm-status,
|
||||
.task-state,
|
||||
.stage-status {
|
||||
border-radius: 3px;
|
||||
font-family: var(--console-font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.mini-log,
|
||||
.json-panel,
|
||||
.help-body,
|
||||
.content-viewer pre,
|
||||
.source-output,
|
||||
.report-markdown {
|
||||
border-color: #334649;
|
||||
border-radius: 4px;
|
||||
background: #111b1e;
|
||||
color: #d9e7e4;
|
||||
font-family: var(--console-font-mono);
|
||||
}
|
||||
|
||||
.hwpod-workspace,
|
||||
.mdtodo-workspace {
|
||||
min-height: min(72vh, 760px);
|
||||
}
|
||||
|
||||
.pcb-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pcb-card::after {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -34px;
|
||||
width: 130px;
|
||||
height: 100px;
|
||||
border: 1px solid rgb(0 131 138 / 18%);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 16px rgb(0 131 138 / 4%), 0 0 0 32px rgb(0 131 138 / 3%);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pcb-trace,
|
||||
.pcb-grid-line {
|
||||
color: var(--console-cyan-600);
|
||||
}
|
||||
|
||||
.mdtodo-workspace,
|
||||
.document-workspace,
|
||||
.report-viewer,
|
||||
.task-index {
|
||||
border-color: var(--console-border-strong);
|
||||
background: var(--console-surface-raised);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
:root {
|
||||
--console-sidebar-width: 224px;
|
||||
}
|
||||
|
||||
.platform-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.page-command-bar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.page-command-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.overview-grid,
|
||||
.settings-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.platform-shell,
|
||||
.platform-shell.is-sidebar-collapsed {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.platform-sidebar,
|
||||
.desktop-sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.platform-topbar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.platform-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.page-command-bar {
|
||||
min-height: 0;
|
||||
padding: 10px 10px 10px 15px;
|
||||
}
|
||||
|
||||
.page-command-title {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.page-command-description {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.console-status-item {
|
||||
min-width: 118px;
|
||||
}
|
||||
|
||||
.overview-grid,
|
||||
.settings-grid,
|
||||
.usage-panels,
|
||||
.field-grid,
|
||||
.form-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.resource-collection-items[data-view="cards"] {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.hwpod-workspace,
|
||||
.mdtodo-workspace {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.topbar-status span,
|
||||
.topbar-actions .btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.page-command-actions,
|
||||
.page-command-filters,
|
||||
.table-page-actions,
|
||||
.table-actions-inline {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.page-command-actions > *,
|
||||
.page-command-filters > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.console-status-strip {
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.console-status-item {
|
||||
flex-basis: 136px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.entity-card,
|
||||
.project-card,
|
||||
.skill-card,
|
||||
.provider-card,
|
||||
.api-key-card,
|
||||
.pcb-card,
|
||||
.node-card {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* SPEC: PJ2026-010405 云端控制台. */
|
||||
/* Implementation reference: draft-2026-07-13-p0-cloud-console. */
|
||||
/* Responsibility: 应用壳、认证入口和 Workbench 共享诊断表面。 */
|
||||
.platform-shell {
|
||||
display: grid;
|
||||
height: 100dvh;
|
||||
@@ -13,6 +13,7 @@ import type { DataGridColumn } from "@/components/common/DataGrid.vue";
|
||||
import EmptyState from "@/components/common/EmptyState.vue";
|
||||
import ResourceCollection from "@/components/common/ResourceCollection.vue";
|
||||
import VirtualList from "@/components/common/VirtualList.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
|
||||
import TablePageLayout from "@/components/layout/TablePageLayout.vue";
|
||||
import { useForm } from "@/composables/useForm";
|
||||
@@ -235,7 +236,7 @@ function routeFilePath(): string {
|
||||
</DataGrid>
|
||||
<ResourceCollection v-else-if="filteredRows.length" :items="filteredRows" :item-key="rowKey" view="cards" :selected-id="selectedSkill?.id" :show-controls="false" aria-label="Skill 卡片" @select="inspectSkill">
|
||||
<template #card="{ item: row }"><article class="skill-card" :data-selected="selectedSkill?.id === row.id">
|
||||
<header><span class="skill-glyph" aria-hidden="true">SK</span><div><strong>{{ row.name || row.id }}</strong><small>{{ row.description || '未提供描述' }}</small></div><span class="status-pill" :data-status="row.source || 'pending'">{{ row.source || '-' }}</span></header>
|
||||
<header><span class="skill-glyph" aria-hidden="true"><ConsoleIcon name="skills" /></span><div><strong>{{ row.name || row.id }}</strong><small>{{ row.description || '未提供描述' }}</small></div><span class="status-pill" :data-status="row.source || 'pending'">{{ row.source || '-' }}</span></header>
|
||||
<dl><div><dt>版本</dt><dd>{{ row.version || row.commitId || '-' }}</dd></div><div><dt>文件</dt><dd>{{ row.fileCount ?? 0 }} / {{ formatBytes(row.sizeBytes) }}</dd></div><div><dt>更新</dt><dd>{{ displayDate(row.updatedAt || row.createdAt) }}</dd></div></dl>
|
||||
<footer><code>{{ row.manifestPath || 'manifest unavailable' }}</code><button class="table-action" type="button" @click="inspectSkill(row)">查看目录 →</button></footer>
|
||||
</article></template>
|
||||
|
||||
@@ -13,6 +13,7 @@ import DataGrid from "@/components/common/DataGrid.vue";
|
||||
import type { DataGridColumn } from "@/components/common/DataGrid.vue";
|
||||
import EmptyState from "@/components/common/EmptyState.vue";
|
||||
import ResourceCollection from "@/components/common/ResourceCollection.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
|
||||
import TablePageLayout from "@/components/layout/TablePageLayout.vue";
|
||||
import { useForm } from "@/composables/useForm";
|
||||
@@ -316,7 +317,7 @@ function restoreSelectedProfile(): void {
|
||||
</DataGrid>
|
||||
<ResourceCollection v-else-if="filteredRows.length" :items="filteredRows" :item-key="rowKey" view="cards" :selected-id="selectedProfile?.profile" :show-controls="false" aria-label="Provider Profile 卡片" @select="selectProfile">
|
||||
<template #card="{ item: row }"><article class="provider-card" :data-selected="selectedProfile?.profile === row.profile">
|
||||
<header><button class="provider-glyph" type="button" @click="selectProfile(row)">AI</button><div><button class="provider-name" type="button" @click="selectProfile(row)">{{ row.profile }}</button><small>{{ row.secretRefText }}</small></div><span class="status-pill" :data-status="row.configured ? 'active' : 'disabled'">{{ row.configured ? 'configured' : 'missing' }}</span></header>
|
||||
<header><button class="provider-glyph" type="button" :aria-label="`选择 ${row.profile}`" @click="selectProfile(row)"><ConsoleIcon name="provider" /></button><div><button class="provider-name" type="button" @click="selectProfile(row)">{{ row.profile }}</button><small>{{ row.secretRefText }}</small></div><span class="status-pill" :data-status="row.configured ? 'active' : 'disabled'">{{ row.configured ? 'configured' : 'missing' }}</span></header>
|
||||
<dl><div><dt>Fingerprint</dt><dd>key {{ row.credentialHashSuffix || '-' }} / cfg {{ row.configHashSuffix || '-' }}</dd></div><div><dt>Bridge</dt><dd>{{ row.bridgeText || '-' }}</dd></div><div><dt>Validation</dt><dd>{{ row.validationStatus || 'not run' }}<small v-if="row.failureKind">{{ row.failureKind }}</small></dd></div></dl>
|
||||
<footer class="table-actions-inline"><button class="table-action" type="button" @click="openCredential(row)">Key</button><button class="table-action" type="button" @click="openConfig(row)">Config</button><button class="table-action" type="button" @click="openTest(row)">测试</button><button class="table-action" type="button" :disabled="validatingProfile !== null" @click="validateProfile(row)">{{ validatingProfile === row.profile ? '验证中' : 'Validate' }}</button><button class="table-action danger" type="button" @click="openRemove(row)">Remove</button></footer>
|
||||
</article></template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { projectManagementAPI, type MdtodoTaskRecord, type ProjectNavigationResp
|
||||
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
|
||||
import EmptyState from "@/components/common/EmptyState.vue";
|
||||
import ResourceCollection from "@/components/common/ResourceCollection.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue";
|
||||
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
|
||||
import StatusStrip from "@/components/layout/StatusStrip.vue";
|
||||
@@ -258,14 +259,14 @@ function projectApiError(err: unknown): { error: string; apiError: ApiError | nu
|
||||
<ResourceCollection v-else :items="filteredProjects" :item-key="projectKey" :item-title="projectTitle" :item-subtitle="projectSubtitle" :item-status="projectStatus" :view="viewMode" :density="density" :selected-id="selectedProject?.projectId" :show-controls="false" aria-label="项目集合" @select="openProject">
|
||||
<template #card="{ item: project }">
|
||||
<RouterLink class="project-card" :class="{ selected: selectedProject?.projectId === project.projectId }" :data-project-id="project.projectId" :to="{ name: 'ProjectDetail', params: { projectId: project.projectId }, query: route.query }">
|
||||
<div class="project-card-main"><span class="project-glyph" aria-hidden="true">PJ</span><span class="pm-status" :data-tone="statusTone(project.status)">{{ project.status || 'unknown' }}</span><div><h3>{{ projectTitle(project) }}</h3><p>{{ project.projectId }}</p></div></div>
|
||||
<div class="project-card-main"><span class="project-glyph" aria-hidden="true"><ConsoleIcon name="project" /></span><span class="pm-status" :data-tone="statusTone(project.status)">{{ project.status || 'unknown' }}</span><div><h3>{{ projectTitle(project) }}</h3><p>{{ project.projectId }}</p></div></div>
|
||||
<div class="project-source-chips" aria-label="项目 source"><span v-for="source in projectSources(project)" :key="source.sourceId" class="source-chip" :data-status="source.status || 'unknown'">{{ source.displayName || source.sourceId }} · {{ source.status || 'unknown' }}</span><span v-if="!projectSources(project).length" class="source-chip muted">无可见 source</span></div>
|
||||
<span class="project-open-hint">查看详情 →</span>
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template #list="{ item: project }">
|
||||
<RouterLink class="project-card" :class="{ selected: selectedProject?.projectId === project.projectId }" :data-project-id="project.projectId" :to="{ name: 'ProjectDetail', params: { projectId: project.projectId }, query: route.query }">
|
||||
<div class="project-card-main"><span class="project-glyph" aria-hidden="true">PJ</span><span class="pm-status" :data-tone="statusTone(project.status)">{{ project.status || 'unknown' }}</span><div><h3>{{ projectTitle(project) }}</h3><p>{{ project.projectId }}</p></div></div>
|
||||
<div class="project-card-main"><span class="project-glyph" aria-hidden="true"><ConsoleIcon name="project" /></span><span class="pm-status" :data-tone="statusTone(project.status)">{{ project.status || 'unknown' }}</span><div><h3>{{ projectTitle(project) }}</h3><p>{{ project.projectId }}</p></div></div>
|
||||
<div class="project-source-chips" aria-label="项目 source"><span v-for="source in projectSources(project)" :key="source.sourceId" class="source-chip" :data-status="source.status || 'unknown'">{{ source.displayName || source.sourceId }} · {{ source.status || 'unknown' }}</span><span v-if="!projectSources(project).length" class="source-chip muted">无可见 source</span></div>
|
||||
<span class="project-open-hint">查看详情 →</span>
|
||||
</RouterLink>
|
||||
|
||||
@@ -9,6 +9,7 @@ import BaseDialog from "@/components/common/BaseDialog.vue";
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
|
||||
import EmptyState from "@/components/common/EmptyState.vue";
|
||||
import ResourceCollection from "@/components/common/ResourceCollection.vue";
|
||||
import ConsoleIcon from "@/components/console/ConsoleIcon.vue";
|
||||
import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue";
|
||||
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
|
||||
import StatusStrip from "@/components/layout/StatusStrip.vue";
|
||||
@@ -197,7 +198,7 @@ function formatDate(value: unknown): string {
|
||||
<ResourceCollection v-else :items="filteredKeys" :item-key="keyId" :item-title="keyTitle" :item-subtitle="keySubtitle" :item-status="keyStatus" :view="viewMode" :density="density" :selected-id="selectedKey?.id" :show-controls="false" aria-label="API Key 集合" @select="selectKey">
|
||||
<template #card="{ item: key }">
|
||||
<RouterLink class="api-key-card" :class="{ selected: selectedKey?.id === key.id }" :to="{ name: 'ApiKeyDetail', params: { keyId: key.id }, query: route.query }">
|
||||
<span class="api-key-glyph" aria-hidden="true">KEY</span>
|
||||
<span class="api-key-glyph" aria-hidden="true"><ConsoleIcon name="key" /></span>
|
||||
<div class="api-key-name"><strong>{{ key.name || key.id }}</strong><code>{{ key.id }}</code></div>
|
||||
<code class="api-key-prefix">{{ key.prefix || '-' }}</code>
|
||||
<span class="status-pill" :data-status="key.revokedAt ? 'disabled' : 'active'">{{ key.revokedAt ? 'revoked' : 'active' }}</span>
|
||||
@@ -207,7 +208,7 @@ function formatDate(value: unknown): string {
|
||||
</template>
|
||||
<template #list="{ item: key }">
|
||||
<RouterLink class="api-key-card" :class="{ selected: selectedKey?.id === key.id }" :to="{ name: 'ApiKeyDetail', params: { keyId: key.id }, query: route.query }">
|
||||
<span class="api-key-glyph" aria-hidden="true">KEY</span>
|
||||
<span class="api-key-glyph" aria-hidden="true"><ConsoleIcon name="key" /></span>
|
||||
<div class="api-key-name"><strong>{{ key.name || key.id }}</strong><code>{{ key.id }}</code></div>
|
||||
<code class="api-key-prefix">{{ key.prefix || '-' }}</code>
|
||||
<span class="status-pill" :data-status="key.revokedAt ? 'disabled' : 'active'">{{ key.revokedAt ? 'revoked' : 'active' }}</span>
|
||||
|
||||
Reference in New Issue
Block a user