feat(desktop): 收口桌面工作台视觉与活动反馈
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user