feat(desktop): 收口桌面工作台视觉与活动反馈

This commit is contained in:
Cheng Zhou
2026-07-02 15:52:16 +08:00
parent 3a866accd9
commit 84d5daebab
19 changed files with 2027 additions and 222 deletions
+325 -58
View File
@@ -4,8 +4,7 @@
<header class="sidebar-head">
<div class="sidebar-title-row">
<div>
<p class="sidebar-kicker">Workspace</p>
<h1>{{ TEXT.common.appName }} Desktop</h1>
<h1>{{ TEXT.common.appName }}</h1>
</div>
</div>
@@ -64,7 +63,7 @@
</section>
<section v-if="adminNavigationItems.length" class="nav-section">
<div class="section-label">{{ TEXT.menu.admin }}</div>
<div class="section-label">{{ DESKTOP_ADMIN_SECTION_LABEL }}</div>
<template v-for="item in adminNavigationItems" :key="item.path">
<button
v-if="!item.children?.length"
@@ -149,20 +148,10 @@
<section class="desktop-main">
<header class="desktop-toolbar">
<div class="toolbar-left">
<div class="history-controls">
<button class="icon-button" type="button" title="后退" @click="router.back()">
<el-icon><ArrowLeft /></el-icon>
</button>
<button class="icon-button" type="button" title="前进" @click="router.forward()">
<el-icon><ArrowRight /></el-icon>
</button>
<button class="icon-button" type="button" title="刷新当前视图" @click="refreshCurrentDesktopView">
<el-icon><Refresh /></el-icon>
</button>
</div>
<div class="breadcrumb-strip">
<span v-for="crumb in desktopBreadcrumbs" :key="crumb" class="breadcrumb-chip">{{ crumb }}</span>
<div class="toolbar-left" data-tauri-drag-region>
<div class="toolbar-context" data-tauri-drag-region>
<div class="toolbar-title" data-tauri-drag-region>{{ desktopToolbarTitle }}</div>
<div class="toolbar-subtitle" data-tauri-drag-region>{{ desktopToolbarSubtitle }}</div>
</div>
</div>
@@ -172,6 +161,48 @@
<span>{{ desktopUpdateNoticeLabel }}</span>
</button>
<el-popover placement="bottom-end" width="340" trigger="click" popper-class="desktop-activity-popover">
<template #reference>
<button class="icon-button activity-button" type="button" title="活动">
<el-badge v-if="desktopActivityBadgeCount > 0" :value="desktopActivityBadgeValue" class="activity-badge">
<el-icon><Clock /></el-icon>
</el-badge>
<el-icon v-else><Clock /></el-icon>
</button>
</template>
<div class="activity-panel">
<div class="activity-head">
<div>
<div class="panel-title">活动</div>
<div class="panel-subtitle">上传下载导出与更新</div>
</div>
<button
v-if="hasFinishedDesktopActivities"
class="activity-clear"
type="button"
@click="clearFinishedDesktopActivities"
>
清除已结束
</button>
</div>
<div v-if="desktopActivities.length" class="activity-list">
<div v-for="item in desktopActivities" :key="item.id" class="activity-item" :class="`is-${item.status}`">
<span class="activity-mark" :class="`is-${item.status}`"></span>
<span class="activity-body">
<span class="activity-title">{{ item.title }}</span>
<span class="activity-detail">{{ item.detail || desktopActivityStatusLabel(item.status) }}</span>
</span>
<span class="activity-meta">
<span v-if="item.status === 'running' && item.progress !== null">{{ item.progress }}%</span>
<span v-else>{{ desktopActivityStatusLabel(item.status) }}</span>
<small>{{ formatDesktopActivityTime(item.updatedAt) }}</small>
</span>
</div>
</div>
<div v-else class="activity-empty">暂无活动</div>
</div>
</el-popover>
<button class="command-trigger" type="button" @click="openCommandPalette">
<el-icon><Search /></el-icon>
<span>命令</span>
@@ -367,8 +398,6 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
ArrowDown,
ArrowLeft,
ArrowRight,
Bell,
Box,
Calendar,
@@ -409,6 +438,13 @@ import {
listenDesktopUpdateStatus,
type DesktopUpdateStatusSnapshot,
} from "../session/desktopUpdateManager";
import {
clearFinishedDesktopActivities,
getDesktopActivities,
listenDesktopActivities,
type DesktopActivityItem,
type DesktopActivityStatus,
} from "../session/desktopActivityCenter";
import {
DESKTOP_SERVER_URL_CHANGED_EVENT,
getAppMetadata,
@@ -442,6 +478,7 @@ const router = useRouter();
const route = useRoute();
const desktopMetadata = getAppMetadata();
const DESKTOP_WORKSPACE_TAB_CACHE_MAX = 12;
const DESKTOP_ADMIN_SECTION_LABEL = "系统管理";
const isAdmin = computed(() => !!auth.user?.is_admin);
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
const canAccessAdminPermissions = computed(() => isAdmin.value || projectRole.value === "PM");
@@ -456,6 +493,7 @@ const profileDialogVisible = ref(false);
const profileDialogDirty = ref(false);
const desktopServerUrl = ref(getDesktopServerUrl());
const desktopUpdateStatus = ref<DesktopUpdateStatusSnapshot>(getDesktopUpdateStatus());
const desktopActivities = ref<DesktopActivityItem[]>(getDesktopActivities());
const studies = ref<any[]>([]);
const desktopRecentRoutes = ref<DesktopRoutePreference[]>(readDesktopRecentRoutes());
const desktopFavoriteRoutes = ref<DesktopRoutePreference[]>(readDesktopFavoriteRoutes());
@@ -478,6 +516,7 @@ const headerRemindersLoading = ref(false);
const headerReminderStats = ref({ overdueAes: 0, overdueMonitoringIssues: 0 });
let desktopMenuUnlisten: (() => void) | undefined;
let desktopUpdateStatusUnlisten: (() => void) | undefined;
let desktopActivityUnlisten: (() => void) | undefined;
const iconComponentMap = {
audit: Document,
@@ -555,6 +594,32 @@ const desktopUpdateNoticeLabel = computed(() => {
const pending = desktopUpdateStatus.value.pendingUpdate;
return pending ? `新版本 ${pending.version}` : "";
});
const desktopActivityBadgeCount = computed(() =>
desktopActivities.value.filter((item) => item.status === "running" || item.status === "failed").length
);
const desktopActivityBadgeValue = computed(() => (desktopActivityBadgeCount.value > 9 ? "9+" : desktopActivityBadgeCount.value));
const hasFinishedDesktopActivities = computed(() =>
desktopActivities.value.some((item) => item.status === "completed" || item.status === "cancelled" || item.status === "failed")
);
const desktopActivityStatusLabel = (status: DesktopActivityStatus) => {
switch (status) {
case "running":
return "进行中";
case "completed":
return "完成";
case "failed":
return "失败";
case "cancelled":
return "已取消";
default:
return "";
}
};
const formatDesktopActivityTime = (value: string) => {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
};
const currentDesktopRoutePreference = computed(() => {
const item = desktopNavigationItems.value.find((navItem) => navItem.path === activeMenu.value);
@@ -565,20 +630,26 @@ const currentRouteFavorited = computed(() => {
const current = currentDesktopRoutePreference.value;
return Boolean(current && desktopFavoriteRoutes.value.some((item) => item.path === current.path));
});
const desktopBreadcrumbs = computed(() => {
const desktopToolbarTitle = computed(() => {
const current = currentDesktopRoutePreference.value;
if (!current) return [String(route.meta?.title || "")].filter(Boolean);
return current?.title || String(route.meta?.title || TEXT.common.appName);
});
const desktopToolbarSubtitle = computed(() => {
const current = currentDesktopRoutePreference.value;
if (!current) return TEXT.common.appName;
const isAdminRoute = adminDesktopNavigationItems.value.some((item) => item.path === current.path);
if (isAdminRoute) {
const section = current.group && current.group !== TEXT.menu.admin ? current.group : "";
return [TEXT.menu.admin, section, current.title].filter(Boolean);
return [DESKTOP_ADMIN_SECTION_LABEL, section].filter(Boolean).join(" / ");
}
const isProjectRoute = projectDesktopNavigationItems.value.some((item) => item.path === current.path);
if (isProjectRoute) {
const section = current.group && current.group !== TEXT.menu.currentProject ? current.group : "";
return [TEXT.menu.currentProject, section, current.title].filter(Boolean);
const projectName = study.currentStudy?.name || TEXT.menu.currentProject;
return [projectName, section].filter(Boolean).join(" / ");
}
return [current.group, current.title].filter(Boolean);
return current.group || TEXT.common.appName;
});
const canReadRiskIssueAes = computed(() => !isAdminContext.value && canAccessProjectPath("/risk-issues/sae"));
@@ -1015,6 +1086,9 @@ onMounted(async () => {
desktopUpdateStatusUnlisten = listenDesktopUpdateStatus((status) => {
desktopUpdateStatus.value = status;
});
desktopActivityUnlisten = listenDesktopActivities((items) => {
desktopActivities.value = items;
});
if (auth.token && !auth.user) {
try {
await auth.fetchMe();
@@ -1031,6 +1105,7 @@ onBeforeUnmount(() => {
window.removeEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
desktopMenuUnlisten?.();
desktopUpdateStatusUnlisten?.();
desktopActivityUnlisten?.();
});
useDesktopShortcuts(
@@ -1098,7 +1173,7 @@ useDesktopShortcuts(
.sidebar-head {
flex: 0 0 auto;
padding: 18px 16px 14px;
padding: 54px 16px 14px;
border-bottom: 1px solid #dfe7f0;
}
@@ -1109,14 +1184,6 @@ useDesktopShortcuts(
gap: 12px;
}
.sidebar-kicker {
margin: 0 0 4px;
color: #66778d;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
}
.sidebar-head h1 {
margin: 0;
overflow: hidden;
@@ -1295,7 +1362,7 @@ useDesktopShortcuts(
.desktop-main {
display: grid;
grid-template-rows: 48px auto minmax(0, 1fr);
grid-template-rows: 52px auto minmax(0, 1fr);
min-width: 0;
min-height: 0;
}
@@ -1312,15 +1379,16 @@ useDesktopShortcuts(
}
.toolbar-left,
.toolbar-right,
.history-controls {
.toolbar-right {
display: flex;
align-items: center;
min-width: 0;
}
.toolbar-left {
gap: 10px;
align-self: stretch;
cursor: default;
user-select: none;
}
.toolbar-right {
@@ -1328,35 +1396,55 @@ useDesktopShortcuts(
gap: 8px;
}
.history-controls {
gap: 3px;
.toolbar-right,
.toolbar-right :deep(*),
.desktop-sidebar button,
.desktop-sidebar :deep(.el-dropdown),
.workspace-tabs,
.workspace-tabs *,
.workspace-tab-context-menu,
.workspace-tab-context-menu * {
-webkit-app-region: no-drag;
}
.breadcrumb-strip {
display: flex;
.toolbar-left[data-tauri-drag-region],
.toolbar-context[data-tauri-drag-region],
.toolbar-title[data-tauri-drag-region],
.toolbar-subtitle[data-tauri-drag-region] {
-webkit-app-region: drag;
}
.toolbar-context {
display: grid;
min-width: 0;
align-items: center;
gap: 6px;
align-content: center;
gap: 2px;
width: 100%;
height: 100%;
overflow: hidden;
}
.breadcrumb-chip {
display: inline-flex;
align-items: center;
max-width: 220px;
height: 26px;
padding: 0 9px;
border: 1px solid #d7e0ea;
border-radius: 999px;
background: #ffffff;
color: #53657b;
font-size: 12px;
font-weight: 650;
.toolbar-title,
.toolbar-subtitle {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar-title {
color: #142033;
font-size: 14px;
font-weight: 800;
line-height: 1.25;
}
.toolbar-subtitle {
color: #66778d;
font-size: 11px;
font-weight: 700;
line-height: 1.2;
}
.command-trigger,
.update-notice,
.connection-button,
@@ -1555,6 +1643,151 @@ useDesktopShortcuts(
text-align: center;
}
.activity-button {
position: relative;
}
.activity-badge {
display: inline-flex;
}
.activity-badge :deep(.el-badge__content) {
height: 16px;
min-width: 16px;
padding: 0 4px;
border: 2px solid #ffffff;
background: #3f5d75;
font-size: 10px;
line-height: 12px;
}
.activity-panel {
width: 320px;
padding: 10px;
}
.activity-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 4px 4px 10px;
}
.activity-clear {
appearance: none;
height: 24px;
padding: 0 7px;
border: 1px solid #d2dce8;
border-radius: 6px;
background: #ffffff;
color: #52667e;
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 700;
}
.activity-clear:hover {
background: #eef4fb;
color: #183756;
}
.activity-list {
display: flex;
max-height: 286px;
flex-direction: column;
gap: 3px;
overflow-y: auto;
}
.activity-item {
display: grid;
grid-template-columns: 7px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
min-height: 46px;
padding: 7px;
border-radius: 7px;
}
.activity-item:hover {
background: #f4f7fa;
}
.activity-mark {
width: 7px;
height: 28px;
border-radius: 999px;
background: #7b8da3;
}
.activity-mark.is-running {
background: #3f5d75;
}
.activity-mark.is-completed {
background: #3f8f6b;
}
.activity-mark.is-failed {
background: #c24b4b;
}
.activity-mark.is-cancelled {
background: #9aaabd;
}
.activity-body {
display: inline-flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.activity-title,
.activity-detail {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-title {
color: #142033;
font-size: 13px;
font-weight: 800;
}
.activity-detail {
color: #7b8da3;
font-size: 12px;
font-weight: 600;
}
.activity-meta {
display: inline-flex;
min-width: 44px;
flex-direction: column;
align-items: flex-end;
gap: 2px;
color: #52667e;
font-size: 12px;
font-weight: 800;
}
.activity-meta small {
color: #9aaabd;
font-size: 10px;
font-weight: 700;
}
.activity-empty {
padding: 24px 8px;
color: #8a9bb0;
font-size: 13px;
text-align: center;
}
.account-button {
gap: 7px;
max-width: 188px;
@@ -1780,14 +2013,15 @@ useDesktopShortcuts(
border-bottom-color: #26364a;
}
:global([data-ctms-theme="dark"] .sidebar-kicker),
:global([data-ctms-theme="dark"] .section-label),
:global([data-ctms-theme="dark"] .panel-subtitle),
:global([data-ctms-theme="dark"] .tab-group) {
:global([data-ctms-theme="dark"] .tab-group),
:global([data-ctms-theme="dark"] .toolbar-subtitle) {
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .sidebar-head h1),
:global([data-ctms-theme="dark"] .toolbar-title),
:global([data-ctms-theme="dark"] .panel-title),
:global([data-ctms-theme="dark"] .panel-row code),
:global([data-ctms-theme="dark"] .reminder-body span) {
@@ -1809,7 +2043,6 @@ useDesktopShortcuts(
:global([data-ctms-theme="dark"] .update-notice),
:global([data-ctms-theme="dark"] .connection-button),
:global([data-ctms-theme="dark"] .account-button),
:global([data-ctms-theme="dark"] .breadcrumb-chip),
:global([data-ctms-theme="dark"] .workspace-tab) {
border-color: #334155;
background: #172033;
@@ -1895,6 +2128,40 @@ useDesktopShortcuts(
color: #dbe5f1;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover) {
border-color: #334155;
background: #172033;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-item:hover) {
background: #1f2d3d;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-title) {
color: #f8fafc;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-detail),
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-meta small),
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-empty) {
color: #94a3b8;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-meta) {
color: #cbd5e1;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-clear) {
border-color: #334155;
background: #172033;
color: #cbd5e1;
}
:global([data-ctms-theme="dark"] .desktop-activity-popover .activity-clear:hover) {
background: #243247;
color: #bfdbfe;
}
:global([data-ctms-theme="dark"] .panel-row) {
color: #94a3b8;
}