2644 lines
74 KiB
Vue
2644 lines
74 KiB
Vue
<template>
|
||
<div class="desktop-workbench" @click="closeWorkspaceTabMenu">
|
||
<aside class="desktop-sidebar">
|
||
<header class="sidebar-head">
|
||
<div class="sidebar-title-row">
|
||
<div class="sidebar-app-brand">
|
||
<img class="sidebar-app-icon" src="/icons/ctms-icon-192.png" alt="" aria-hidden="true" />
|
||
<h1>{{ TEXT.common.appName }}</h1>
|
||
</div>
|
||
|
||
<button v-if="showSidebarProjectChooser" class="study-switcher-trigger empty" type="button" @click="openDesktopProjectEntry">
|
||
选择项目
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="sidebar-scroll">
|
||
<section v-if="desktopFavoriteRoutes.length" class="nav-section">
|
||
<div class="section-label">收藏</div>
|
||
<button
|
||
v-for="item in desktopFavoriteRoutes"
|
||
:key="`favorite:${item.path}`"
|
||
type="button"
|
||
class="sidebar-link compact"
|
||
:class="{ active: activeMenu === item.path }"
|
||
@click="router.push(item.path)"
|
||
>
|
||
<el-icon><StarFilled /></el-icon>
|
||
<span>{{ item.title }}</span>
|
||
</button>
|
||
</section>
|
||
|
||
<section v-if="adminNavigationItems.length" class="nav-section">
|
||
<div class="section-label">{{ DESKTOP_ADMIN_SECTION_LABEL }}</div>
|
||
<template v-for="item in adminNavigationItems" :key="item.path">
|
||
<button
|
||
v-if="!item.children?.length"
|
||
type="button"
|
||
class="sidebar-link"
|
||
:class="{ active: activeMenu === item.path }"
|
||
@click="router.push(item.path)"
|
||
>
|
||
<el-icon><component :is="iconFor(item.icon)" /></el-icon>
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
<div v-else class="sidebar-group" :class="{ expanded: isNavigationGroupExpanded(item) }">
|
||
<button
|
||
type="button"
|
||
class="sidebar-link group-title"
|
||
:class="{ active: isNavigationGroupActive(item) }"
|
||
:aria-expanded="isNavigationGroupExpanded(item)"
|
||
@click="toggleNavigationGroup(item)"
|
||
>
|
||
<el-icon><component :is="iconFor(item.icon)" /></el-icon>
|
||
<span>{{ item.label }}</span>
|
||
<el-icon class="group-expander"><ArrowDown /></el-icon>
|
||
</button>
|
||
<div v-show="isNavigationGroupExpanded(item)" class="sidebar-children">
|
||
<button
|
||
v-for="child in item.children"
|
||
:key="child.path"
|
||
type="button"
|
||
class="sidebar-link child"
|
||
:class="{ active: activeMenu === child.path }"
|
||
@click="router.push(child.path)"
|
||
>
|
||
<span>{{ child.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
|
||
<section v-if="projectNavigationItems.length" class="nav-section">
|
||
<div class="section-label">{{ desktopProjectSectionLabel }}</div>
|
||
<template v-for="item in projectNavigationItems" :key="item.path">
|
||
<button
|
||
v-if="!item.children?.length"
|
||
type="button"
|
||
class="sidebar-link"
|
||
:class="{ active: activeMenu === item.path }"
|
||
@click="router.push(item.path)"
|
||
>
|
||
<el-icon><component :is="iconFor(item.icon)" /></el-icon>
|
||
<span>{{ item.label }}</span>
|
||
</button>
|
||
<div v-else class="sidebar-group" :class="{ expanded: isNavigationGroupExpanded(item) }">
|
||
<button
|
||
type="button"
|
||
class="sidebar-link group-title"
|
||
:class="{ active: isNavigationGroupActive(item) }"
|
||
:aria-expanded="isNavigationGroupExpanded(item)"
|
||
@click="toggleNavigationGroup(item)"
|
||
>
|
||
<el-icon><component :is="iconFor(item.icon)" /></el-icon>
|
||
<span>{{ item.label }}</span>
|
||
<el-icon class="group-expander"><ArrowDown /></el-icon>
|
||
</button>
|
||
<div v-show="isNavigationGroupExpanded(item)" class="sidebar-children">
|
||
<button
|
||
v-for="child in item.children"
|
||
:key="child.path"
|
||
type="button"
|
||
class="sidebar-link child"
|
||
:class="{ active: activeMenu === child.path }"
|
||
@click="router.push(child.path)"
|
||
>
|
||
<span>{{ child.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="desktop-main">
|
||
<header class="desktop-toolbar">
|
||
<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>
|
||
|
||
<div class="toolbar-right">
|
||
<button v-if="desktopUpdateNoticeVisible" class="update-notice" type="button" title="打开系统偏好安装更新" @click="openDesktopPreferences">
|
||
<el-icon><Download /></el-icon>
|
||
<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>
|
||
<kbd>⌘K</kbd>
|
||
</button>
|
||
|
||
<button class="icon-button" type="button" :title="currentRouteFavorited ? '取消收藏当前模块' : '收藏当前模块'" @click="toggleCurrentFavorite">
|
||
<el-icon><StarFilled v-if="currentRouteFavorited" /><Star v-else /></el-icon>
|
||
</button>
|
||
|
||
<el-popover placement="bottom-end" width="320" trigger="click" popper-class="desktop-connection-popover">
|
||
<template #reference>
|
||
<button class="connection-button" type="button">
|
||
<span class="connection-dot" :class="desktopConnectionClass"></span>
|
||
<span>{{ desktopConnectionLabel }}</span>
|
||
</button>
|
||
</template>
|
||
<div class="connection-panel">
|
||
<div class="panel-title">在线桌面客户端</div>
|
||
<div class="panel-row">
|
||
<span>服务器</span>
|
||
<code>{{ desktopServerUrl || "未配置" }}</code>
|
||
</div>
|
||
<div class="panel-row">
|
||
<span>客户端</span>
|
||
<code>{{ desktopMetadata.version }} / {{ desktopMetadata.platform }}</code>
|
||
</div>
|
||
<el-button size="small" @click="openDesktopPreferences">系统偏好</el-button>
|
||
</div>
|
||
</el-popover>
|
||
|
||
<el-dropdown trigger="click" placement="bottom-end" @command="handleReminderCommand">
|
||
<button class="icon-button reminder-button" type="button" title="项目提醒">
|
||
<el-badge v-if="headerReminderTotal > 0" :value="headerReminderBadgeValue" class="reminder-badge">
|
||
<el-icon><Bell /></el-icon>
|
||
</el-badge>
|
||
<el-icon v-else><Bell /></el-icon>
|
||
</button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu class="reminder-menu">
|
||
<div class="reminder-panel">
|
||
<div class="reminder-head">
|
||
<div>
|
||
<div class="panel-title">{{ TEXT.common.labels.projectReminders }}</div>
|
||
<div class="panel-subtitle">{{ TEXT.common.labels.projectRemindersSubtitle }}</div>
|
||
</div>
|
||
<span v-if="headerReminderTotal > 0" class="reminder-total">{{ headerReminderTotal }}</span>
|
||
</div>
|
||
<template v-if="headerReminderItems.length">
|
||
<el-dropdown-item
|
||
v-for="item in headerReminderItems"
|
||
:key="item.key"
|
||
:command="item.command"
|
||
class="reminder-item"
|
||
>
|
||
<span class="reminder-mark" :class="`is-${item.tone}`"></span>
|
||
<span class="reminder-body">
|
||
<span>{{ item.title }}</span>
|
||
<small>{{ item.description }}</small>
|
||
</span>
|
||
<strong>{{ item.count }}</strong>
|
||
</el-dropdown-item>
|
||
</template>
|
||
<div v-else class="reminder-empty">
|
||
{{ headerRemindersLoading ? TEXT.common.loading : TEXT.common.labels.projectRemindersEmpty }}
|
||
</div>
|
||
</div>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
|
||
<el-dropdown trigger="click" placement="bottom-end" @command="onCommand">
|
||
<button class="account-button" type="button">
|
||
<span class="account-avatar">{{ userDisplayInitial }}</span>
|
||
<span class="account-name">{{ userDisplayName }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="profile">
|
||
<el-icon><User /></el-icon>
|
||
<span>{{ TEXT.menu.profile }}</span>
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="projectEntry">
|
||
<el-icon><Suitcase /></el-icon>
|
||
<span>{{ projectEntryMenuLabel }}</span>
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="desktopPreferences">
|
||
<el-icon><Monitor /></el-icon>
|
||
<span>系统偏好</span>
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="logout" :disabled="loggingOut" divided>
|
||
<el-icon><SwitchButton /></el-icon>
|
||
<span>{{ loggingOut ? "正在退出..." : TEXT.menu.logout }}</span>
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</header>
|
||
|
||
<div v-if="workspaceTabs.length" ref="workspaceTabsElement" class="workspace-tabs">
|
||
<div
|
||
class="workspace-tabs-list"
|
||
:style="{ gridTemplateColumns: `repeat(${visibleWorkspaceTabs.length}, minmax(0, 238px))` }"
|
||
>
|
||
<div
|
||
v-for="item in visibleWorkspaceTabs"
|
||
:key="item.path"
|
||
class="workspace-tab"
|
||
:class="{ active: activeMenu === item.path, dragging: draggingWorkspaceTabPath === item.path }"
|
||
draggable="true"
|
||
@dragstart="startWorkspaceTabDrag(item.path, $event)"
|
||
@dragover="allowWorkspaceTabDrop"
|
||
@drop.prevent="dropWorkspaceTab(item.path, $event)"
|
||
@dragend="draggingWorkspaceTabPath = ''"
|
||
@contextmenu.prevent.stop="openWorkspaceTabMenu(item, $event)"
|
||
>
|
||
<button
|
||
class="workspace-tab-action"
|
||
type="button"
|
||
:title="item.title"
|
||
@click="navigateWorkspaceTab(item.path)"
|
||
>
|
||
<span>{{ item.title }}</span>
|
||
</button>
|
||
<button class="tab-close" type="button" title="关闭标签" @click.stop="closeWorkspaceTab(item.path)">
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dropdown
|
||
v-if="overflowWorkspaceTabs.length"
|
||
class="workspace-tabs-overflow"
|
||
popper-class="workspace-tabs-overflow-popper"
|
||
trigger="click"
|
||
placement="bottom-end"
|
||
@command="navigateOverflowWorkspaceTab"
|
||
@visible-change="workspaceTabsOverflowOpen = $event"
|
||
>
|
||
<button
|
||
class="workspace-tabs-overflow-trigger"
|
||
:class="{ 'is-open': workspaceTabsOverflowOpen }"
|
||
type="button"
|
||
:aria-label="`查看其余 ${overflowWorkspaceTabs.length} 个标签`"
|
||
>
|
||
<span>更多</span>
|
||
<span class="workspace-tabs-overflow-count">{{ overflowWorkspaceTabs.length }}</span>
|
||
<el-icon><ArrowDown /></el-icon>
|
||
</button>
|
||
<template #dropdown>
|
||
<div class="workspace-tabs-overflow-menu">
|
||
<div class="workspace-tabs-overflow-header">
|
||
<span>其他标签</span>
|
||
<span>{{ overflowWorkspaceTabs.length }} 个</span>
|
||
</div>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item
|
||
v-for="item in overflowWorkspaceTabs"
|
||
:key="item.path"
|
||
:command="item.path"
|
||
:class="{ 'is-current': activeMenu === item.path }"
|
||
>
|
||
<span class="workspace-tabs-overflow-item-mark" aria-hidden="true"></span>
|
||
<span class="workspace-tabs-overflow-label">
|
||
<span :title="item.title">{{ item.title }}</span>
|
||
</span>
|
||
<button
|
||
class="workspace-tabs-overflow-close"
|
||
type="button"
|
||
:aria-label="`关闭标签:${item.title}`"
|
||
title="关闭标签"
|
||
@click.stop="closeWorkspaceTab(item.path)"
|
||
>
|
||
×
|
||
</button>
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</div>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
|
||
<main class="desktop-content">
|
||
<router-view v-slot="{ Component, route: currentRoute }">
|
||
<transition name="desktop-route" mode="out-in">
|
||
<KeepAlive :max="DESKTOP_WORKSPACE_TAB_CACHE_MAX">
|
||
<component
|
||
:is="Component"
|
||
v-if="Component"
|
||
:key="desktopRouteCacheKey(currentRoute)"
|
||
class="desktop-route-shell"
|
||
/>
|
||
</KeepAlive>
|
||
</transition>
|
||
</router-view>
|
||
</main>
|
||
</section>
|
||
</div>
|
||
|
||
<el-dialog
|
||
v-model="profileDialogVisible"
|
||
class="profile-settings-dialog"
|
||
:show-close="false"
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
:before-close="beforeProfileDialogClose"
|
||
width="980px"
|
||
align-center
|
||
destroy-on-close
|
||
@closed="profileDialogDirty = false"
|
||
>
|
||
<ProfileSettings
|
||
@close-request="requestProfileDialogClose"
|
||
@dirty-change="profileDialogDirty = $event"
|
||
@saved="profileDialogVisible = false"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<DesktopCommandPalette v-model="commandPaletteVisible" :commands="desktopCommands" />
|
||
|
||
<el-dialog
|
||
v-model="desktopPreferencesVisible"
|
||
class="desktop-preferences-dialog"
|
||
:show-close="false"
|
||
:close-on-click-modal="true"
|
||
width="940px"
|
||
align-center
|
||
destroy-on-close
|
||
>
|
||
<DesktopPreferences @close-request="desktopPreferencesVisible = false" />
|
||
</el-dialog>
|
||
|
||
<div
|
||
v-if="workspaceTabMenu.visible && workspaceTabMenu.tab"
|
||
class="workspace-tab-context-menu"
|
||
:style="{ left: `${workspaceTabMenu.x}px`, top: `${workspaceTabMenu.y}px` }"
|
||
@click.stop
|
||
>
|
||
<button type="button" @click="navigateWorkspaceTabFromMenu(workspaceTabMenu.tab.path)">切换到标签</button>
|
||
<button type="button" @click="toggleWorkspaceTabFavorite(workspaceTabMenu.tab)">
|
||
{{ isWorkspaceTabFavorited(workspaceTabMenu.tab) ? "取消收藏模块" : "收藏模块" }}
|
||
</button>
|
||
<button type="button" @click="closeWorkspaceTabFromMenu(workspaceTabMenu.tab.path)">关闭标签</button>
|
||
<button type="button" :disabled="workspaceTabs.length <= 1" @click="closeOtherWorkspaceTabs(workspaceTabMenu.tab.path)">
|
||
关闭其他标签
|
||
</button>
|
||
<button type="button" :disabled="!lastClosedWorkspaceTab" @click="restoreLastClosedWorkspaceTab">
|
||
重新打开最近关闭
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import {
|
||
ArrowDown,
|
||
Bell,
|
||
Box,
|
||
Calendar,
|
||
CircleCheck,
|
||
Clock,
|
||
Coin,
|
||
DataAnalysis,
|
||
Document,
|
||
Download,
|
||
Files,
|
||
Flag,
|
||
Folder,
|
||
House,
|
||
Key,
|
||
Monitor,
|
||
Notebook,
|
||
Refresh,
|
||
Search,
|
||
Setting,
|
||
Star,
|
||
StarFilled,
|
||
Suitcase,
|
||
SwitchButton,
|
||
User,
|
||
UserFilled,
|
||
} from "@element-plus/icons-vue";
|
||
import { ElMessageBox } from "element-plus";
|
||
import { useAuthStore } from "../store/auth";
|
||
import { useStudyStore } from "../store/study";
|
||
import { fetchOverdueAesCount } from "../api/dashboard";
|
||
import { listMonitoringVisitIssues } from "../api/monitoringVisitIssues";
|
||
import { TEXT } from "../locales";
|
||
import { forceLogout, LOGOUT_REASON_MANUAL } from "../session/sessionManager";
|
||
import { DESKTOP_SESSION_RESTORE_PATH, shouldPreserveDesktopSessionOnAuthCheckFailure } from "../session/authRecovery";
|
||
import {
|
||
checkDesktopUpdateAndPrompt,
|
||
getDesktopUpdateStatus,
|
||
listenDesktopUpdateStatus,
|
||
type DesktopUpdateStatusSnapshot,
|
||
} from "../session/desktopUpdateManager";
|
||
import {
|
||
clearFinishedDesktopActivities,
|
||
getDesktopActivities,
|
||
listenDesktopActivities,
|
||
type DesktopActivityItem,
|
||
type DesktopActivityStatus,
|
||
} from "../session/desktopActivityCenter";
|
||
import {
|
||
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
||
DESKTOP_SHORTCUTS_CHANGED_EVENT,
|
||
clearLegacyDesktopRouteHistoryPreference,
|
||
formatDesktopShortcut,
|
||
getAppMetadata,
|
||
getDesktopServerUrl,
|
||
listenDesktopMenuCommand,
|
||
readDesktopFavoriteRoutes,
|
||
readDesktopShortcutPreferences,
|
||
toggleDesktopFavoriteRoute,
|
||
type DesktopRoutePreference,
|
||
type DesktopShortcutPreferences,
|
||
} from "../runtime";
|
||
import { dispatchDesktopRefreshCurrentView } from "../composables/useDesktopRefresh";
|
||
import { useDesktopShortcuts } from "../composables/useDesktopShortcuts";
|
||
import { getProjectRoutePermission, hasProjectPermission, projectRouteLandingPaths } from "../utils/projectRoutePermissions";
|
||
import type { DesktopCommand } from "../types/desktopCommands";
|
||
import DesktopCommandPalette from "./DesktopCommandPalette.vue";
|
||
import DesktopPreferences from "../views/DesktopPreferences.vue";
|
||
import ProfileSettings from "../views/ProfileSettings.vue";
|
||
import {
|
||
buildAdminNavigationItems,
|
||
buildProjectNavigationItems,
|
||
flattenLayoutNavigationItems,
|
||
getActiveLayoutPath,
|
||
type LayoutNavigationIcon,
|
||
type LayoutNavigationItem,
|
||
} from "./layout/navigation";
|
||
import {
|
||
createWorkspaceTabHistory,
|
||
currentWorkspaceTabPath,
|
||
moveWorkspaceTabHistory,
|
||
recordWorkspaceTabPath,
|
||
replaceWorkspaceTabPath,
|
||
type WorkspaceTabHistory,
|
||
} from "./layout/workspaceTabHistory";
|
||
|
||
const auth = useAuthStore();
|
||
const study = useStudyStore();
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
const desktopMetadata = getAppMetadata();
|
||
const DESKTOP_WORKSPACE_TAB_CACHE_MAX = 12;
|
||
const DESKTOP_WORKSPACE_TAB_MIN_WIDTH = 130;
|
||
const DESKTOP_WORKSPACE_TAB_GAP = 6;
|
||
const DESKTOP_WORKSPACE_TABS_HORIZONTAL_PADDING = 20;
|
||
const DESKTOP_WORKSPACE_TABS_OVERFLOW_WIDTH = 106;
|
||
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");
|
||
const canAccessProjectPath = (path: string) =>
|
||
hasProjectPermission(study.currentPermissions, projectRole.value, getProjectRoutePermission(path), isAdmin.value);
|
||
const hasAnyProjectModuleAccess = computed(() => projectRouteLandingPaths.some((path) => canAccessProjectPath(path)));
|
||
const isAdminContext = computed(() => route.path.startsWith("/admin"));
|
||
const showSidebarProjectChooser = computed(() => !study.currentStudy && !isAdminContext.value);
|
||
const loggingOut = ref(false);
|
||
const commandPaletteVisible = ref(false);
|
||
const desktopPreferencesVisible = ref(false);
|
||
const profileDialogVisible = ref(false);
|
||
const profileDialogDirty = ref(false);
|
||
const desktopServerUrl = ref(getDesktopServerUrl());
|
||
const desktopUpdateStatus = ref<DesktopUpdateStatusSnapshot>(getDesktopUpdateStatus());
|
||
const desktopActivities = ref<DesktopActivityItem[]>(getDesktopActivities());
|
||
const desktopFavoriteRoutes = ref<DesktopRoutePreference[]>(readDesktopFavoriteRoutes());
|
||
const desktopShortcutPreferences = ref<DesktopShortcutPreferences>(readDesktopShortcutPreferences());
|
||
const workspaceTabs = ref<DesktopRoutePreference[]>([]);
|
||
const workspaceTabHistories = ref<Record<string, WorkspaceTabHistory>>({});
|
||
const workspaceTabsElement = ref<HTMLElement | null>(null);
|
||
const workspaceTabsAvailableWidth = ref(0);
|
||
const workspaceTabsOverflowOpen = ref(false);
|
||
const draggingWorkspaceTabPath = ref("");
|
||
const lastClosedWorkspaceTab = ref<DesktopRoutePreference | null>(null);
|
||
const lastClosedWorkspaceTabHistory = ref<WorkspaceTabHistory | null>(null);
|
||
const workspaceTabMenu = ref<{
|
||
visible: boolean;
|
||
x: number;
|
||
y: number;
|
||
tab: DesktopRoutePreference | null;
|
||
}>({
|
||
visible: false,
|
||
x: 0,
|
||
y: 0,
|
||
tab: null,
|
||
});
|
||
const expandedNavigationGroups = ref<Set<string>>(new Set());
|
||
const collapsedNavigationGroups = ref<Set<string>>(new Set());
|
||
const headerRemindersLoading = ref(false);
|
||
const headerReminderStats = ref({ overdueAes: 0, overdueMonitoringIssues: 0 });
|
||
let desktopMenuUnlisten: (() => void) | undefined;
|
||
let desktopUpdateStatusUnlisten: (() => void) | undefined;
|
||
let desktopActivityUnlisten: (() => void) | undefined;
|
||
let workspaceTabsResizeObserver: ResizeObserver | undefined;
|
||
|
||
const iconComponentMap = {
|
||
audit: Document,
|
||
calendar: Calendar,
|
||
check: CircleCheck,
|
||
coin: Coin,
|
||
dashboard: House,
|
||
document: Files,
|
||
flag: Flag,
|
||
folder: Box,
|
||
key: Key,
|
||
monitor: DataAnalysis,
|
||
notebook: Notebook,
|
||
project: Suitcase,
|
||
settings: Setting,
|
||
subject: User,
|
||
users: UserFilled,
|
||
};
|
||
|
||
const iconFor = (icon: LayoutNavigationIcon) => iconComponentMap[icon] || Folder;
|
||
|
||
const activeMenu = computed(() => getActiveLayoutPath(route.path));
|
||
const navigationGroupKey = (item: LayoutNavigationItem) => `${item.group}:${item.label}:${item.path}`;
|
||
const isNavigationGroupActive = (item: LayoutNavigationItem) =>
|
||
Boolean(item.children?.some((child) => activeMenu.value === child.path));
|
||
const isNavigationGroupExpanded = (item: LayoutNavigationItem) => {
|
||
const key = navigationGroupKey(item);
|
||
return expandedNavigationGroups.value.has(key) || (isNavigationGroupActive(item) && !collapsedNavigationGroups.value.has(key));
|
||
};
|
||
const toggleNavigationGroup = (item: LayoutNavigationItem) => {
|
||
const key = navigationGroupKey(item);
|
||
const nextExpanded = new Set(expandedNavigationGroups.value);
|
||
const nextCollapsed = new Set(collapsedNavigationGroups.value);
|
||
if (isNavigationGroupExpanded(item)) {
|
||
nextExpanded.delete(key);
|
||
nextCollapsed.add(key);
|
||
} else {
|
||
nextExpanded.add(key);
|
||
nextCollapsed.delete(key);
|
||
}
|
||
expandedNavigationGroups.value = nextExpanded;
|
||
collapsedNavigationGroups.value = nextCollapsed;
|
||
};
|
||
const adminNavigationItems = computed(() =>
|
||
study.currentStudy
|
||
? []
|
||
: buildAdminNavigationItems({
|
||
hasUser: Boolean(auth.user),
|
||
isAdmin: isAdmin.value,
|
||
canAccessAdminPermissions: canAccessAdminPermissions.value,
|
||
}),
|
||
);
|
||
const projectNavigationItems = computed(() =>
|
||
buildProjectNavigationItems({
|
||
hasCurrentStudy: Boolean(study.currentStudy),
|
||
hasAnyProjectModuleAccess: hasAnyProjectModuleAccess.value,
|
||
canAccessProjectPath,
|
||
}),
|
||
);
|
||
const adminDesktopNavigationItems = computed(() => flattenLayoutNavigationItems(adminNavigationItems.value));
|
||
const projectDesktopNavigationItems = computed(() => flattenLayoutNavigationItems(projectNavigationItems.value));
|
||
const desktopNavigationItems = computed(() =>
|
||
[...adminDesktopNavigationItems.value, ...projectDesktopNavigationItems.value],
|
||
);
|
||
const workspaceTabFallbackPath = computed(() => {
|
||
if (isAdminContext.value) return adminDesktopNavigationItems.value[0]?.path || "/admin/projects";
|
||
return study.currentStudy ? "/project/overview" : "/desktop/project-entry";
|
||
});
|
||
|
||
const userDisplayName = computed(
|
||
() => auth.user?.full_name || auth.user?.username || auth.user?.email || TEXT.common.labels.userFallback,
|
||
);
|
||
const userDisplayInitial = computed(() => {
|
||
const source = auth.user?.full_name || auth.user?.username || auth.user?.email || TEXT.common.labels.userInitialFallback;
|
||
return source.charAt(0).toUpperCase();
|
||
});
|
||
const desktopProjectSectionLabel = computed(() => study.currentStudy?.name || TEXT.menu.currentProject);
|
||
const projectEntryMenuLabel = computed(() => (study.currentStudy ? "返回项目选择" : "选择项目"));
|
||
const desktopConnectionLabel = computed(() => (desktopServerUrl.value ? "已连接" : "未配置"));
|
||
const desktopConnectionClass = computed(() => (desktopServerUrl.value ? "is-connected" : "is-warning"));
|
||
const desktopUpdateNoticeVisible = computed(
|
||
() =>
|
||
Boolean(desktopUpdateStatus.value.pendingUpdate) &&
|
||
!desktopUpdateStatus.value.checking &&
|
||
!desktopUpdateStatus.value.installing &&
|
||
!["postponed", "suppressed"].includes(desktopUpdateStatus.value.lastStatus),
|
||
);
|
||
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);
|
||
if (!item) return null;
|
||
return { path: item.path, title: item.label, group: item.group };
|
||
});
|
||
const visibleWorkspaceTabCapacity = computed(() => {
|
||
const tabCount = workspaceTabs.value.length;
|
||
const availableWidth = workspaceTabsAvailableWidth.value;
|
||
if (!tabCount || availableWidth <= 0) return tabCount;
|
||
const maxWithoutOverflow = Math.max(
|
||
1,
|
||
Math.floor((availableWidth + DESKTOP_WORKSPACE_TAB_GAP) / (DESKTOP_WORKSPACE_TAB_MIN_WIDTH + DESKTOP_WORKSPACE_TAB_GAP)),
|
||
);
|
||
if (tabCount <= maxWithoutOverflow) return tabCount;
|
||
return Math.max(
|
||
1,
|
||
Math.floor(
|
||
(availableWidth - DESKTOP_WORKSPACE_TABS_OVERFLOW_WIDTH) /
|
||
(DESKTOP_WORKSPACE_TAB_MIN_WIDTH + DESKTOP_WORKSPACE_TAB_GAP),
|
||
),
|
||
);
|
||
});
|
||
const visibleWorkspaceTabs = computed(() => {
|
||
const capacity = visibleWorkspaceTabCapacity.value;
|
||
if (workspaceTabs.value.length <= capacity) return workspaceTabs.value;
|
||
const firstTabs = workspaceTabs.value.slice(0, capacity);
|
||
const activeTab = workspaceTabs.value.find((item) => item.path === activeMenu.value);
|
||
if (!activeTab || firstTabs.some((item) => item.path === activeTab.path)) return firstTabs;
|
||
return [...firstTabs.slice(0, -1), activeTab];
|
||
});
|
||
const overflowWorkspaceTabs = computed(() => {
|
||
const visiblePaths = new Set(visibleWorkspaceTabs.value.map((item) => item.path));
|
||
return workspaceTabs.value.filter((item) => !visiblePaths.has(item.path));
|
||
});
|
||
const currentRouteFavorited = computed(() => {
|
||
const current = currentDesktopRoutePreference.value;
|
||
return Boolean(current && desktopFavoriteRoutes.value.some((item) => item.path === current.path));
|
||
});
|
||
const desktopToolbarTitle = computed(() => {
|
||
const current = currentDesktopRoutePreference.value;
|
||
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 [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 : "";
|
||
const projectName = study.currentStudy?.name || TEXT.menu.currentProject;
|
||
return [projectName, section].filter(Boolean).join(" / ");
|
||
}
|
||
return current.group || TEXT.common.appName;
|
||
});
|
||
|
||
const canReadRiskIssueAes = computed(() => !isAdminContext.value && canAccessProjectPath("/risk-issues/sae"));
|
||
const canReadMonitoringIssues = computed(() => !isAdminContext.value && canAccessProjectPath("/risk-issues/monitoring-visits"));
|
||
const hasAnyRiskReminderAccess = computed(() => canReadRiskIssueAes.value || canReadMonitoringIssues.value);
|
||
const toHeaderNumber = (value: unknown) => {
|
||
const num = typeof value === "number" ? value : Number(value);
|
||
return Number.isFinite(num) && num > 0 ? num : 0;
|
||
};
|
||
const headerReminderItems = computed(() => [
|
||
canReadRiskIssueAes.value && headerReminderStats.value.overdueAes > 0 ? {
|
||
key: "overdueAes",
|
||
command: "overdueAes",
|
||
title: TEXT.common.labels.overdueAes,
|
||
description: TEXT.common.labels.overdueAesDesc,
|
||
count: headerReminderStats.value.overdueAes,
|
||
tone: "danger",
|
||
} : null,
|
||
canReadMonitoringIssues.value && headerReminderStats.value.overdueMonitoringIssues > 0 ? {
|
||
key: "overdueMonitoringIssues",
|
||
command: "overdueMonitoringIssues",
|
||
title: TEXT.common.labels.overdueMonitoringIssues,
|
||
description: TEXT.common.labels.overdueMonitoringIssuesDesc,
|
||
count: headerReminderStats.value.overdueMonitoringIssues,
|
||
tone: "warning",
|
||
} : null,
|
||
].filter(Boolean) as Array<{
|
||
key: string;
|
||
command: string;
|
||
title: string;
|
||
description: string;
|
||
count: number;
|
||
tone: "danger" | "warning";
|
||
}>);
|
||
const headerReminderTotal = computed(() => headerReminderItems.value.reduce((sum, item) => sum + item.count, 0));
|
||
const headerReminderBadgeValue = computed(() => (headerReminderTotal.value > 99 ? "99+" : headerReminderTotal.value));
|
||
|
||
const refreshDesktopRoutePreferences = () => {
|
||
const allowedPaths = new Set(desktopNavigationItems.value.map((item) => item.path));
|
||
desktopFavoriteRoutes.value = readDesktopFavoriteRoutes().filter((item) => allowedPaths.has(item.path));
|
||
};
|
||
|
||
const addWorkspaceTab = (routePreference: Pick<DesktopRoutePreference, "path" | "title" | "group">) => {
|
||
const item = { ...routePreference, updatedAt: new Date().toISOString() };
|
||
const existingIndex = workspaceTabs.value.findIndex((tab) => tab.path === item.path);
|
||
if (existingIndex >= 0) {
|
||
const next = [...workspaceTabs.value];
|
||
next[existingIndex] = { ...next[existingIndex], ...item };
|
||
workspaceTabs.value = next;
|
||
return;
|
||
}
|
||
workspaceTabs.value = [...workspaceTabs.value, item];
|
||
};
|
||
|
||
const setWorkspaceTabHistory = (tabPath: string, history: WorkspaceTabHistory) => {
|
||
workspaceTabHistories.value = { ...workspaceTabHistories.value, [tabPath]: history };
|
||
};
|
||
|
||
const removeWorkspaceTabHistory = (tabPath: string) => {
|
||
if (!workspaceTabHistories.value[tabPath]) return;
|
||
const next = { ...workspaceTabHistories.value };
|
||
delete next[tabPath];
|
||
workspaceTabHistories.value = next;
|
||
};
|
||
|
||
const recordWorkspaceTabRoute = (tabPath: string, fullPath: string) => {
|
||
setWorkspaceTabHistory(tabPath, recordWorkspaceTabPath(workspaceTabHistories.value[tabPath], fullPath));
|
||
};
|
||
|
||
const replaceWorkspaceTabRoute = (tabPath: string, fullPath: string) => {
|
||
setWorkspaceTabHistory(tabPath, replaceWorkspaceTabPath(workspaceTabHistories.value[tabPath], fullPath));
|
||
};
|
||
|
||
const workspaceTabDestination = (tabPath: string) =>
|
||
currentWorkspaceTabPath(workspaceTabHistories.value[tabPath], tabPath);
|
||
|
||
const desktopRouteCacheKey = (currentRoute: { fullPath: string }) => currentRoute.fullPath;
|
||
|
||
const navigateWorkspaceTab = (path: string) => {
|
||
if (activeMenu.value === path) return;
|
||
void router.push(workspaceTabDestination(path));
|
||
};
|
||
|
||
const navigateCurrentWorkspaceTabHistory = (offset: -1 | 1) => {
|
||
const tabPath = activeMenu.value;
|
||
const movement = moveWorkspaceTabHistory(workspaceTabHistories.value[tabPath], offset);
|
||
if (!movement) return;
|
||
setWorkspaceTabHistory(tabPath, movement.history);
|
||
void router.push(movement.path);
|
||
};
|
||
|
||
const navigateOverflowWorkspaceTab = (path: string) => {
|
||
navigateWorkspaceTab(path);
|
||
};
|
||
|
||
const updateWorkspaceTabsAvailableWidth = () => {
|
||
const element = workspaceTabsElement.value;
|
||
workspaceTabsAvailableWidth.value = element
|
||
? Math.max(0, element.clientWidth - DESKTOP_WORKSPACE_TABS_HORIZONTAL_PADDING)
|
||
: 0;
|
||
};
|
||
|
||
const observeWorkspaceTabsElement = (element: HTMLElement | null) => {
|
||
workspaceTabsResizeObserver?.disconnect();
|
||
if (!element) {
|
||
workspaceTabsAvailableWidth.value = 0;
|
||
return;
|
||
}
|
||
updateWorkspaceTabsAvailableWidth();
|
||
if (typeof ResizeObserver === "undefined") return;
|
||
workspaceTabsResizeObserver ||= new ResizeObserver(updateWorkspaceTabsAvailableWidth);
|
||
workspaceTabsResizeObserver.observe(element);
|
||
};
|
||
|
||
const closeWorkspaceTabMenu = () => {
|
||
if (!workspaceTabMenu.value.visible) return;
|
||
workspaceTabMenu.value = { visible: false, x: 0, y: 0, tab: null };
|
||
};
|
||
|
||
const openWorkspaceTabMenu = (tab: DesktopRoutePreference, event: MouseEvent) => {
|
||
workspaceTabMenu.value = {
|
||
visible: true,
|
||
x: Math.min(event.clientX, window.innerWidth - 196),
|
||
y: Math.min(event.clientY, window.innerHeight - 184),
|
||
tab,
|
||
};
|
||
};
|
||
|
||
const navigateWorkspaceTabFromMenu = (path: string) => {
|
||
navigateWorkspaceTab(path);
|
||
closeWorkspaceTabMenu();
|
||
};
|
||
|
||
const isWorkspaceTabFavorited = (tab: DesktopRoutePreference) =>
|
||
desktopFavoriteRoutes.value.some((item) => item.path === tab.path);
|
||
|
||
const toggleWorkspaceTabFavorite = (tab: DesktopRoutePreference) => {
|
||
desktopFavoriteRoutes.value = toggleDesktopFavoriteRoute({ path: tab.path, title: tab.title, group: tab.group });
|
||
refreshDesktopRoutePreferences();
|
||
closeWorkspaceTabMenu();
|
||
};
|
||
|
||
const startWorkspaceTabDrag = (path: string, event: DragEvent) => {
|
||
draggingWorkspaceTabPath.value = path;
|
||
event.dataTransfer?.setData("text/plain", path);
|
||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
||
};
|
||
|
||
const allowWorkspaceTabDrop = (event: DragEvent) => {
|
||
event.preventDefault();
|
||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||
};
|
||
|
||
const dropWorkspaceTab = (targetPath: string, event: DragEvent) => {
|
||
const sourcePath = draggingWorkspaceTabPath.value || event.dataTransfer?.getData("text/plain") || "";
|
||
draggingWorkspaceTabPath.value = "";
|
||
if (!sourcePath || sourcePath === targetPath) return;
|
||
const sourceIndex = workspaceTabs.value.findIndex((tab) => tab.path === sourcePath);
|
||
const targetIndex = workspaceTabs.value.findIndex((tab) => tab.path === targetPath);
|
||
if (sourceIndex < 0 || targetIndex < 0) return;
|
||
const next = [...workspaceTabs.value];
|
||
const [moved] = next.splice(sourceIndex, 1);
|
||
next.splice(targetIndex, 0, moved);
|
||
workspaceTabs.value = next;
|
||
};
|
||
|
||
const closeWorkspaceTab = (path: string) => {
|
||
const index = workspaceTabs.value.findIndex((tab) => tab.path === path);
|
||
if (index >= 0) {
|
||
lastClosedWorkspaceTab.value = workspaceTabs.value[index];
|
||
lastClosedWorkspaceTabHistory.value = workspaceTabHistories.value[path] || createWorkspaceTabHistory(path);
|
||
}
|
||
workspaceTabs.value = workspaceTabs.value.filter((tab) => tab.path !== path);
|
||
removeWorkspaceTabHistory(path);
|
||
if (activeMenu.value !== path) return;
|
||
const next =
|
||
workspaceTabs.value[index] ||
|
||
workspaceTabs.value[index - 1] ||
|
||
desktopFavoriteRoutes.value.find((item) => item.path !== path);
|
||
if (next) {
|
||
router.push(workspaceTabDestination(next.path));
|
||
} else {
|
||
router.push(workspaceTabFallbackPath.value);
|
||
}
|
||
};
|
||
|
||
const closeWorkspaceTabFromMenu = (path: string) => {
|
||
closeWorkspaceTab(path);
|
||
closeWorkspaceTabMenu();
|
||
};
|
||
|
||
const closeOtherWorkspaceTabs = (path: string) => {
|
||
const target = workspaceTabs.value.find((tab) => tab.path === path);
|
||
if (!target) return;
|
||
workspaceTabs.value = [target];
|
||
const targetHistory = workspaceTabHistories.value[path];
|
||
workspaceTabHistories.value = targetHistory ? { [path]: targetHistory } : {};
|
||
if (activeMenu.value !== path) void router.push(workspaceTabDestination(path));
|
||
closeWorkspaceTabMenu();
|
||
};
|
||
|
||
const restoreLastClosedWorkspaceTab = () => {
|
||
const tab = lastClosedWorkspaceTab.value;
|
||
if (!tab) return;
|
||
addWorkspaceTab(tab);
|
||
if (lastClosedWorkspaceTabHistory.value) {
|
||
setWorkspaceTabHistory(tab.path, lastClosedWorkspaceTabHistory.value);
|
||
}
|
||
void router.push(workspaceTabDestination(tab.path));
|
||
lastClosedWorkspaceTab.value = null;
|
||
lastClosedWorkspaceTabHistory.value = null;
|
||
closeWorkspaceTabMenu();
|
||
};
|
||
|
||
const updateDesktopServerUrl = () => {
|
||
desktopServerUrl.value = getDesktopServerUrl();
|
||
};
|
||
|
||
const updateDesktopShortcutPreferences = (event?: Event) => {
|
||
desktopShortcutPreferences.value =
|
||
(event as CustomEvent<DesktopShortcutPreferences> | undefined)?.detail || readDesktopShortcutPreferences();
|
||
};
|
||
|
||
const openCommandPalette = () => {
|
||
commandPaletteVisible.value = true;
|
||
};
|
||
|
||
const openDesktopPreferences = () => {
|
||
desktopPreferencesVisible.value = true;
|
||
};
|
||
|
||
const openDesktopProjectEntry = async () => {
|
||
study.clearCurrentStudy();
|
||
workspaceTabs.value = [];
|
||
workspaceTabHistories.value = {};
|
||
lastClosedWorkspaceTab.value = null;
|
||
lastClosedWorkspaceTabHistory.value = null;
|
||
await router.push("/desktop/project-entry");
|
||
};
|
||
|
||
const toggleCurrentFavorite = () => {
|
||
const current = currentDesktopRoutePreference.value;
|
||
if (!current) return;
|
||
desktopFavoriteRoutes.value = toggleDesktopFavoriteRoute(current);
|
||
refreshDesktopRoutePreferences();
|
||
};
|
||
|
||
const refreshCurrentDesktopView = () => {
|
||
const handled = dispatchDesktopRefreshCurrentView();
|
||
if (!handled) router.go(0);
|
||
};
|
||
|
||
const handleDesktopMenuCommand = (command: string) => {
|
||
if (command === "ctms.desktop.commandPalette") {
|
||
openCommandPalette();
|
||
return;
|
||
}
|
||
if (command === "ctms.desktop.preferences") {
|
||
openDesktopPreferences();
|
||
return;
|
||
}
|
||
if (command === "ctms.desktop.serverSettings") {
|
||
openDesktopPreferences();
|
||
return;
|
||
}
|
||
if (command === "ctms.desktop.refresh") {
|
||
refreshCurrentDesktopView();
|
||
return;
|
||
}
|
||
if (command === "ctms.desktop.back") {
|
||
navigateCurrentWorkspaceTabHistory(-1);
|
||
return;
|
||
}
|
||
if (command === "ctms.desktop.forward") {
|
||
navigateCurrentWorkspaceTabHistory(1);
|
||
}
|
||
};
|
||
|
||
const desktopCommands = computed<DesktopCommand[]>(() => {
|
||
const navigationCommands: DesktopCommand[] = desktopNavigationItems.value.map((item) => ({
|
||
id: `route:${item.path}`,
|
||
title: item.label,
|
||
group: item.group,
|
||
keywords: item.keywords,
|
||
visible: true,
|
||
run: () => {
|
||
void router.push(item.path);
|
||
},
|
||
}));
|
||
const workspaceTabCommands: DesktopCommand[] = workspaceTabs.value.map((item) => ({
|
||
id: `workspace-tab:${item.path}`,
|
||
title: `切换标签:${item.title}`,
|
||
group: "标签页",
|
||
keywords: [item.title, item.group, item.path].filter((value): value is string => Boolean(value)),
|
||
visible: true,
|
||
run: () => navigateWorkspaceTab(item.path),
|
||
}));
|
||
return [
|
||
{
|
||
id: "desktop:tab-back",
|
||
title: "当前标签后退",
|
||
group: "标签页",
|
||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.back),
|
||
visible: true,
|
||
run: () => navigateCurrentWorkspaceTabHistory(-1),
|
||
},
|
||
{
|
||
id: "desktop:tab-forward",
|
||
title: "当前标签前进",
|
||
group: "标签页",
|
||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.forward),
|
||
visible: true,
|
||
run: () => navigateCurrentWorkspaceTabHistory(1),
|
||
},
|
||
{
|
||
id: "desktop:refresh",
|
||
title: "刷新当前视图",
|
||
group: "桌面",
|
||
shortcut: formatDesktopShortcut(desktopShortcutPreferences.value.refresh),
|
||
visible: true,
|
||
run: refreshCurrentDesktopView,
|
||
},
|
||
{
|
||
id: "desktop:preferences",
|
||
title: "打开系统偏好",
|
||
group: "桌面",
|
||
shortcut: "⌘,",
|
||
visible: true,
|
||
run: openDesktopPreferences,
|
||
},
|
||
{
|
||
id: "desktop:server-settings",
|
||
title: "连接设置",
|
||
group: "桌面",
|
||
visible: true,
|
||
run: openDesktopPreferences,
|
||
},
|
||
{
|
||
id: "desktop:update",
|
||
title: "检查桌面更新",
|
||
group: "桌面",
|
||
visible: true,
|
||
run: async () => {
|
||
await checkDesktopUpdateAndPrompt({ notifyWhenCurrent: true });
|
||
},
|
||
},
|
||
{
|
||
id: "desktop:project-entry",
|
||
title: projectEntryMenuLabel.value,
|
||
group: "桌面",
|
||
visible: true,
|
||
run: openDesktopProjectEntry,
|
||
},
|
||
{
|
||
id: "desktop:favorite-current",
|
||
title: currentRouteFavorited.value ? "取消收藏当前模块" : "收藏当前模块",
|
||
group: "桌面",
|
||
visible: Boolean(currentDesktopRoutePreference.value),
|
||
run: toggleCurrentFavorite,
|
||
},
|
||
{
|
||
id: "desktop:restore-closed-tab",
|
||
title: "重新打开最近关闭标签",
|
||
group: "标签页",
|
||
visible: Boolean(lastClosedWorkspaceTab.value),
|
||
run: restoreLastClosedWorkspaceTab,
|
||
},
|
||
...workspaceTabCommands,
|
||
...navigationCommands,
|
||
];
|
||
});
|
||
|
||
const loadHeaderReminders = async () => {
|
||
const studyId = study.currentStudy?.id;
|
||
if (!studyId || isAdminContext.value || !hasAnyRiskReminderAccess.value) {
|
||
headerReminderStats.value = { overdueAes: 0, overdueMonitoringIssues: 0 };
|
||
return;
|
||
}
|
||
headerRemindersLoading.value = true;
|
||
try {
|
||
const [aesRes, monitoringRes] = await Promise.allSettled([
|
||
canReadRiskIssueAes.value ? fetchOverdueAesCount(studyId) : Promise.resolve(null),
|
||
canReadMonitoringIssues.value ? listMonitoringVisitIssues(studyId, { overdue: true, limit: 500 }) : Promise.resolve(null),
|
||
]);
|
||
if (study.currentStudy?.id !== studyId || isAdminContext.value) return;
|
||
headerReminderStats.value = {
|
||
overdueAes: aesRes.status === "fulfilled" && aesRes.value ? toHeaderNumber((aesRes.value.data as any)?.total) : 0,
|
||
overdueMonitoringIssues: monitoringRes.status === "fulfilled" && monitoringRes.value && Array.isArray(monitoringRes.value.data)
|
||
? monitoringRes.value.data.length
|
||
: 0,
|
||
};
|
||
} finally {
|
||
if (study.currentStudy?.id === studyId) {
|
||
headerRemindersLoading.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleReminderCommand = (cmd: string) => {
|
||
if (cmd === "overdueAes" && canReadRiskIssueAes.value) {
|
||
router.push("/risk-issues/sae");
|
||
return;
|
||
}
|
||
if (cmd === "overdueMonitoringIssues" && canReadMonitoringIssues.value) {
|
||
router.push("/risk-issues/monitoring-visits");
|
||
}
|
||
};
|
||
|
||
const logoutImmediately = async () => {
|
||
await auth.logout();
|
||
study.clearCurrentStudy();
|
||
router.replace("/login");
|
||
};
|
||
|
||
const onLogout = async () => {
|
||
if (loggingOut.value) return;
|
||
const confirmed = await ElMessageBox.confirm(
|
||
"退出后需要重新登录才能继续访问项目数据。",
|
||
"确认退出登录",
|
||
{
|
||
type: "warning",
|
||
confirmButtonText: "退出登录",
|
||
cancelButtonText: TEXT.common.actions.cancel,
|
||
distinguishCancelAndClose: true,
|
||
},
|
||
).catch(() => null);
|
||
if (!confirmed) return;
|
||
loggingOut.value = true;
|
||
try {
|
||
await forceLogout(LOGOUT_REASON_MANUAL);
|
||
} finally {
|
||
window.setTimeout(() => {
|
||
loggingOut.value = false;
|
||
}, 300);
|
||
}
|
||
};
|
||
|
||
const onCommand = (cmd: string) => {
|
||
if (cmd === "logout") {
|
||
onLogout();
|
||
} else if (cmd === "profile") {
|
||
profileDialogVisible.value = true;
|
||
} else if (cmd === "projectEntry") {
|
||
openDesktopProjectEntry();
|
||
} else if (cmd === "desktopPreferences") {
|
||
openDesktopPreferences();
|
||
}
|
||
};
|
||
|
||
const confirmDiscardProfileChanges = async () => {
|
||
if (!profileDialogDirty.value) return true;
|
||
const confirmed = await ElMessageBox.confirm(
|
||
"当前修改尚未保存,关闭后将丢失这些改动。",
|
||
"确认关闭个人中心",
|
||
{
|
||
type: "warning",
|
||
confirmButtonText: "关闭",
|
||
cancelButtonText: "继续编辑",
|
||
},
|
||
).catch(() => false);
|
||
return Boolean(confirmed);
|
||
};
|
||
|
||
const requestProfileDialogClose = async () => {
|
||
if (await confirmDiscardProfileChanges()) {
|
||
profileDialogVisible.value = false;
|
||
}
|
||
};
|
||
|
||
const beforeProfileDialogClose = async (done: () => void) => {
|
||
if (await confirmDiscardProfileChanges()) {
|
||
done();
|
||
}
|
||
};
|
||
|
||
watch(
|
||
() => route.fullPath,
|
||
(fullPath, previousFullPath) => {
|
||
study.setViewContext(null);
|
||
const current = currentDesktopRoutePreference.value;
|
||
if (current) {
|
||
addWorkspaceTab(current);
|
||
const previousTabPath = previousFullPath ? getActiveLayoutPath(router.resolve(previousFullPath).path) : "";
|
||
const replacesCurrentTabEntry =
|
||
previousTabPath === current.path && Boolean((window.history.state as { replaced?: boolean } | null)?.replaced);
|
||
if (replacesCurrentTabEntry) {
|
||
replaceWorkspaceTabRoute(current.path, fullPath);
|
||
} else {
|
||
recordWorkspaceTabRoute(current.path, fullPath);
|
||
}
|
||
refreshDesktopRoutePreferences();
|
||
}
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => [study.currentStudy?.id, route.path] as const,
|
||
() => {
|
||
refreshDesktopRoutePreferences();
|
||
loadHeaderReminders();
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => desktopNavigationItems.value.map((item) => item.path).join("|"),
|
||
() => refreshDesktopRoutePreferences(),
|
||
);
|
||
|
||
watch(workspaceTabsElement, (element) => observeWorkspaceTabsElement(element), { flush: "post" });
|
||
|
||
onMounted(async () => {
|
||
window.addEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
|
||
window.addEventListener(DESKTOP_SHORTCUTS_CHANGED_EVENT, updateDesktopShortcutPreferences);
|
||
window.addEventListener("resize", updateWorkspaceTabsAvailableWidth);
|
||
clearLegacyDesktopRouteHistoryPreference();
|
||
const current = currentDesktopRoutePreference.value;
|
||
if (current) {
|
||
addWorkspaceTab(current);
|
||
recordWorkspaceTabRoute(current.path, route.fullPath);
|
||
}
|
||
refreshDesktopRoutePreferences();
|
||
desktopMenuUnlisten = await listenDesktopMenuCommand(handleDesktopMenuCommand).catch(() => undefined);
|
||
desktopUpdateStatusUnlisten = listenDesktopUpdateStatus((status) => {
|
||
desktopUpdateStatus.value = status;
|
||
});
|
||
desktopActivityUnlisten = listenDesktopActivities((items) => {
|
||
desktopActivities.value = items;
|
||
});
|
||
if (auth.token && !auth.user) {
|
||
try {
|
||
await auth.fetchMe({ disableNetworkRetry: true, suppressErrorMessage: true });
|
||
} catch (error) {
|
||
if (shouldPreserveDesktopSessionOnAuthCheckFailure(error)) {
|
||
await router.replace({ path: DESKTOP_SESSION_RESTORE_PATH, query: { redirect: route.fullPath } });
|
||
return;
|
||
}
|
||
await logoutImmediately();
|
||
return;
|
||
}
|
||
}
|
||
loadHeaderReminders();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
|
||
window.removeEventListener(DESKTOP_SHORTCUTS_CHANGED_EVENT, updateDesktopShortcutPreferences);
|
||
window.removeEventListener("resize", updateWorkspaceTabsAvailableWidth);
|
||
workspaceTabsResizeObserver?.disconnect();
|
||
desktopMenuUnlisten?.();
|
||
desktopUpdateStatusUnlisten?.();
|
||
desktopActivityUnlisten?.();
|
||
});
|
||
|
||
useDesktopShortcuts(
|
||
() => true,
|
||
{
|
||
openCommandPalette,
|
||
closeActiveLayer: () => {
|
||
if (workspaceTabMenu.value.visible) {
|
||
closeWorkspaceTabMenu();
|
||
return true;
|
||
}
|
||
if (commandPaletteVisible.value) {
|
||
commandPaletteVisible.value = false;
|
||
return true;
|
||
}
|
||
if (desktopPreferencesVisible.value) {
|
||
desktopPreferencesVisible.value = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
navigateBack: () => navigateCurrentWorkspaceTabHistory(-1),
|
||
navigateForward: () => navigateCurrentWorkspaceTabHistory(1),
|
||
refreshCurrentView: refreshCurrentDesktopView,
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.desktop-workbench {
|
||
display: grid;
|
||
grid-template-columns: 248px minmax(0, 1fr);
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
min-height: 0;
|
||
min-width: 1180px;
|
||
overflow: hidden;
|
||
background: #eef2f6;
|
||
color: #142033;
|
||
}
|
||
|
||
.icon-button {
|
||
appearance: none;
|
||
border: 0;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.icon-button:active {
|
||
transform: scale(0.96);
|
||
}
|
||
|
||
.desktop-sidebar {
|
||
display: flex;
|
||
height: 100%;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
flex-direction: column;
|
||
background: #f8fafc;
|
||
border-right: 1px solid #d7e0ea;
|
||
}
|
||
|
||
.sidebar-head {
|
||
flex: 0 0 auto;
|
||
padding: 44px 10px 10px;
|
||
border-bottom: 1px solid #dfe7f0;
|
||
}
|
||
|
||
.sidebar-title-row {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.sidebar-head h1 {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
color: #101827;
|
||
font-size: 16px;
|
||
line-height: 1.25;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sidebar-app-brand {
|
||
display: inline-grid;
|
||
min-width: 0;
|
||
grid-template-columns: 30px minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 9px;
|
||
}
|
||
|
||
.sidebar-app-icon {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.14);
|
||
object-fit: contain;
|
||
}
|
||
|
||
.icon-button {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 1px solid transparent;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #5d6f85;
|
||
transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.16s ease;
|
||
}
|
||
|
||
.icon-button:hover {
|
||
border-color: #c8d4e1;
|
||
background: #ffffff;
|
||
color: #172233;
|
||
}
|
||
|
||
.study-switcher-trigger {
|
||
appearance: none;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
min-height: 32px;
|
||
margin-top: 0;
|
||
padding: 0 8px;
|
||
border: 1px solid #d7e0ea;
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
color: #26384f;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
}
|
||
|
||
.study-switcher-trigger.empty {
|
||
display: inline-flex;
|
||
justify-content: center;
|
||
justify-self: start;
|
||
width: 104px;
|
||
color: #3f5d75;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.sidebar-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
max-height: 100%;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
padding: 10px 8px 24px;
|
||
scrollbar-gutter: stable;
|
||
}
|
||
|
||
.sidebar-scroll::-webkit-scrollbar {
|
||
width: 10px;
|
||
}
|
||
|
||
.sidebar-scroll::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.sidebar-scroll::-webkit-scrollbar-thumb {
|
||
border: 3px solid #f8fafc;
|
||
border-radius: 999px;
|
||
background: #c5d1df;
|
||
}
|
||
|
||
.sidebar-scroll::-webkit-scrollbar-thumb:hover {
|
||
background: #aebdcd;
|
||
}
|
||
|
||
.nav-section + .nav-section {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.section-label {
|
||
padding: 0 6px 6px;
|
||
color: #7b8da3;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.sidebar-link {
|
||
appearance: none;
|
||
display: grid;
|
||
grid-template-columns: 16px minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
min-height: 32px;
|
||
margin: 1px 0;
|
||
padding: 0 8px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: #3d5068;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 14px;
|
||
line-height: 1.3;
|
||
text-align: left;
|
||
}
|
||
|
||
.sidebar-link.compact {
|
||
grid-template-columns: 16px minmax(0, 1fr);
|
||
}
|
||
|
||
.sidebar-link.child {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
min-height: 28px;
|
||
padding-left: 32px;
|
||
color: #607289;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sidebar-link :deep(.el-icon) {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.sidebar-link span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sidebar-link:hover {
|
||
background: #edf3f8;
|
||
color: #142033;
|
||
}
|
||
|
||
.sidebar-link.active {
|
||
background: #e4edf6;
|
||
color: #183756;
|
||
font-weight: 760;
|
||
}
|
||
|
||
.sidebar-group {
|
||
margin: 1px 0 4px;
|
||
}
|
||
|
||
.group-title {
|
||
grid-template-columns: 16px minmax(0, 1fr) 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.group-expander {
|
||
justify-self: end;
|
||
color: #7a8ca2;
|
||
font-size: 12px;
|
||
transition: transform 0.16s ease;
|
||
}
|
||
|
||
.sidebar-group.expanded .group-expander {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.sidebar-children {
|
||
padding: 2px 0 4px;
|
||
}
|
||
|
||
.desktop-main {
|
||
display: grid;
|
||
grid-template-rows: 52px auto minmax(0, 1fr);
|
||
min-width: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
.desktop-toolbar {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 0 12px;
|
||
border-bottom: 1px solid #d7e0ea;
|
||
background: rgba(248, 250, 252, 0.92);
|
||
backdrop-filter: blur(14px);
|
||
}
|
||
|
||
.toolbar-left,
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.toolbar-left {
|
||
align-self: stretch;
|
||
cursor: default;
|
||
user-select: none;
|
||
}
|
||
|
||
.toolbar-right {
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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-content: center;
|
||
gap: 2px;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.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,
|
||
.account-button {
|
||
appearance: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 32px;
|
||
border: 1px solid #d2dce8;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
color: #33475f;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.update-notice {
|
||
gap: 6px;
|
||
max-width: 138px;
|
||
padding: 0 9px;
|
||
border-color: #d8bd6c;
|
||
background: #fff8e5;
|
||
color: #6f5317;
|
||
}
|
||
|
||
.update-notice span {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.command-trigger {
|
||
gap: 7px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.command-trigger kbd {
|
||
padding: 1px 5px;
|
||
border: 1px solid #c7d2df;
|
||
border-radius: 5px;
|
||
background: #f6f8fb;
|
||
color: #66778d;
|
||
font-size: 10px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||
}
|
||
|
||
.connection-button {
|
||
gap: 7px;
|
||
padding: 0 9px;
|
||
}
|
||
|
||
.connection-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 999px;
|
||
background: #c58b2a;
|
||
}
|
||
|
||
.connection-dot.is-connected {
|
||
background: #3f8f6b;
|
||
}
|
||
|
||
.connection-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.panel-title {
|
||
color: #101827;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.panel-subtitle {
|
||
color: #8a9bb0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel-row {
|
||
display: grid;
|
||
grid-template-columns: 56px minmax(0, 1fr);
|
||
gap: 10px;
|
||
color: #66778d;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel-row code {
|
||
overflow-wrap: anywhere;
|
||
color: #101827;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||
}
|
||
|
||
.reminder-button {
|
||
position: relative;
|
||
}
|
||
|
||
.reminder-badge {
|
||
display: inline-flex;
|
||
}
|
||
|
||
.reminder-badge :deep(.el-badge__content) {
|
||
height: 16px;
|
||
min-width: 16px;
|
||
padding: 0 4px;
|
||
border: 2px solid #ffffff;
|
||
background: #c24b4b;
|
||
font-size: 10px;
|
||
line-height: 12px;
|
||
}
|
||
|
||
.reminder-menu {
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.reminder-panel {
|
||
width: 314px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.reminder-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 4px 4px 10px;
|
||
}
|
||
|
||
.reminder-total {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 22px;
|
||
height: 22px;
|
||
padding: 0 7px;
|
||
border-radius: 999px;
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.reminder-item {
|
||
width: 100%;
|
||
height: auto;
|
||
padding: 8px 7px !important;
|
||
border-radius: 7px;
|
||
}
|
||
|
||
.reminder-mark {
|
||
width: 7px;
|
||
height: 28px;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.reminder-mark.is-danger {
|
||
background: #c24b4b;
|
||
}
|
||
|
||
.reminder-mark.is-warning {
|
||
background: #c58b2a;
|
||
}
|
||
|
||
.reminder-body {
|
||
display: inline-flex;
|
||
flex: 1;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
margin: 0 10px;
|
||
}
|
||
|
||
.reminder-body span,
|
||
.reminder-body small {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.reminder-body span {
|
||
color: #142033;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.reminder-body small {
|
||
color: #7b8da3;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.reminder-empty {
|
||
padding: 20px 8px;
|
||
color: #8a9bb0;
|
||
font-size: 13px;
|
||
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;
|
||
padding: 0 8px 0 3px;
|
||
}
|
||
|
||
.account-avatar {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 7px;
|
||
background: #3f5d75;
|
||
color: #ffffff;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.account-name {
|
||
max-width: 110px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.workspace-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
gap: 6px;
|
||
padding: 7px 10px;
|
||
overflow: hidden;
|
||
border-bottom: 1px solid #dfe7f0;
|
||
background: #f4f7fa;
|
||
}
|
||
|
||
.workspace-tabs-list {
|
||
display: grid;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
gap: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.workspace-tab {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
height: 30px;
|
||
box-sizing: border-box;
|
||
padding: 0 5px 0 10px;
|
||
border: 1px solid #d2dce8;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
color: #3d5068;
|
||
cursor: grab;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.workspace-tab:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.workspace-tab.active {
|
||
border-color: #aebfd1;
|
||
background: #eaf1f8;
|
||
color: #183756;
|
||
}
|
||
|
||
.workspace-tab.dragging {
|
||
opacity: 0.58;
|
||
}
|
||
|
||
.workspace-tab-action {
|
||
appearance: none;
|
||
display: block;
|
||
min-width: 0;
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-weight: inherit;
|
||
}
|
||
|
||
.workspace-tab-action span {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-close {
|
||
width: 18px;
|
||
height: 18px;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 5px;
|
||
background: transparent;
|
||
color: #7b8da3;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tab-close:hover {
|
||
background: rgba(15, 23, 42, 0.08);
|
||
color: #142033;
|
||
}
|
||
|
||
.workspace-tabs-overflow {
|
||
display: flex;
|
||
flex: 0 0 106px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.workspace-tabs-overflow-trigger {
|
||
appearance: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 106px;
|
||
height: 30px;
|
||
gap: 6px;
|
||
padding: 0 9px 0 11px;
|
||
border: 1px solid #c5d1df;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||
color: #3d5068;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.workspace-tabs-overflow-trigger:hover,
|
||
.workspace-tabs-overflow-trigger:focus-visible,
|
||
.workspace-tabs-overflow-trigger.is-open {
|
||
border-color: #aebfd1;
|
||
background: #eaf1f8;
|
||
color: #183756;
|
||
outline: none;
|
||
}
|
||
|
||
.workspace-tabs-overflow-count {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 20px;
|
||
height: 18px;
|
||
box-sizing: border-box;
|
||
padding: 0 6px;
|
||
border-radius: 9px;
|
||
background: #e3ebf4;
|
||
color: #31506f;
|
||
font-size: 10px;
|
||
line-height: 1;
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper.el-popper) {
|
||
overflow: hidden;
|
||
padding: 0;
|
||
border: 1px solid #d6e0eb;
|
||
border-radius: 10px;
|
||
background: #ffffff;
|
||
box-shadow: 0 14px 38px rgba(15, 23, 42, 0.16), 0 2px 8px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper .el-popper__arrow::before) {
|
||
border-color: #d6e0eb;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.workspace-tabs-overflow-menu {
|
||
width: 252px;
|
||
}
|
||
|
||
.workspace-tabs-overflow-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 38px;
|
||
padding: 0 12px;
|
||
border-bottom: 1px solid #e5ebf2;
|
||
color: #2d4058;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.workspace-tabs-overflow-header span:last-child {
|
||
color: #8292a6;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu) {
|
||
min-width: 0;
|
||
max-height: min(360px, calc(100vh - 120px));
|
||
padding: 6px;
|
||
border: 0;
|
||
background: transparent;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item) {
|
||
display: flex;
|
||
min-height: 38px;
|
||
gap: 9px;
|
||
padding: 0 6px 0 10px;
|
||
border-radius: 7px;
|
||
color: #33485f;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item:hover),
|
||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item:focus) {
|
||
background: #f0f5fa;
|
||
color: #183756;
|
||
}
|
||
|
||
:global(.workspace-tabs-overflow-popper .el-dropdown-menu__item.is-current) {
|
||
background: #eaf1f8;
|
||
color: #183756;
|
||
}
|
||
|
||
.workspace-tabs-overflow-label {
|
||
display: block;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.workspace-tabs-overflow-item-mark {
|
||
flex: 0 0 7px;
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: #9fb1c4;
|
||
}
|
||
|
||
.workspace-tabs-overflow-label > span {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.workspace-tabs-overflow-close {
|
||
appearance: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 24px;
|
||
width: 24px;
|
||
height: 24px;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: #7b8da3;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 16px;
|
||
opacity: 0.62;
|
||
}
|
||
|
||
.workspace-tabs-overflow-close:hover,
|
||
.workspace-tabs-overflow-close:focus-visible {
|
||
background: rgba(15, 23, 42, 0.08);
|
||
color: #142033;
|
||
opacity: 1;
|
||
outline: none;
|
||
}
|
||
|
||
.workspace-tab-context-menu {
|
||
position: fixed;
|
||
z-index: 2300;
|
||
display: flex;
|
||
width: 184px;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
padding: 6px;
|
||
border: 1px solid #cbd7e5;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
box-shadow: 0 14px 38px rgba(15, 23, 42, 0.16);
|
||
}
|
||
|
||
.workspace-tab-context-menu button {
|
||
appearance: none;
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-height: 30px;
|
||
padding: 0 9px;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: #223349;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
text-align: left;
|
||
}
|
||
|
||
.workspace-tab-context-menu button:hover:not(:disabled) {
|
||
background: #eef4fb;
|
||
color: #183756;
|
||
}
|
||
|
||
.workspace-tab-context-menu button:disabled {
|
||
cursor: not-allowed;
|
||
color: #9aaabd;
|
||
}
|
||
|
||
.desktop-content {
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
padding: 0;
|
||
background: #eef2f6;
|
||
}
|
||
|
||
.desktop-route-shell {
|
||
height: 100%;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.desktop-route-enter-active,
|
||
.desktop-route-leave-active {
|
||
transition: opacity 140ms ease, transform 160ms ease;
|
||
}
|
||
|
||
.desktop-route-enter-from {
|
||
opacity: 0;
|
||
transform: translateY(4px);
|
||
}
|
||
|
||
.desktop-route-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
/* profile-settings-dialog:移除外层卡片外壳,只保留容器行为 */
|
||
:global(.profile-settings-dialog) {
|
||
--el-dialog-bg-color: transparent !important;
|
||
--el-dialog-box-shadow: none !important;
|
||
--el-dialog-border-radius: 0 !important;
|
||
overflow: visible !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
border-radius: 0 !important;
|
||
border: none !important;
|
||
}
|
||
|
||
:global(.desktop-preferences-dialog) {
|
||
/* 移除外层卡片外壳,内容区自带圆角阴影 */
|
||
--el-dialog-bg-color: transparent !important;
|
||
--el-dialog-box-shadow: none !important;
|
||
--el-dialog-border-radius: 0 !important;
|
||
overflow: visible !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
border-radius: 0 !important;
|
||
border: none !important;
|
||
}
|
||
|
||
:global(.profile-settings-dialog .el-dialog__header),
|
||
:global(.desktop-preferences-dialog .el-dialog__header) {
|
||
display: none;
|
||
}
|
||
|
||
:global(.profile-settings-dialog .el-dialog__body) {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
:global(.desktop-preferences-dialog .el-dialog__body) {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .desktop-workbench) {
|
||
background: #0f172a;
|
||
color: #e5edf7;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .desktop-sidebar) {
|
||
border-right-color: #26364a;
|
||
background: #111827;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-head) {
|
||
border-bottom-color: #26364a;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .section-label),
|
||
:global([data-ctms-theme="dark"] .panel-subtitle),
|
||
: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) {
|
||
color: #f8fafc;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .icon-button) {
|
||
color: #9aaabd;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .icon-button:hover) {
|
||
border-color: #334155;
|
||
background: #1f2d3d;
|
||
color: #f8fafc;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .study-switcher-trigger),
|
||
:global([data-ctms-theme="dark"] .command-trigger),
|
||
: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"] .workspace-tab),
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-trigger) {
|
||
border-color: #334155;
|
||
background: #172033;
|
||
color: #dbe5f1;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .study-switcher-trigger.empty) {
|
||
color: #bfdbfe;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-link) {
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-link.child) {
|
||
color: #9aaabd;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-link:hover) {
|
||
background: #1f2d3d;
|
||
color: #f8fafc;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-link.active),
|
||
:global([data-ctms-theme="dark"] .workspace-tab.active) {
|
||
border-color: #3e5c77;
|
||
background: #243247;
|
||
color: #bfdbfe;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .desktop-toolbar) {
|
||
border-bottom-color: #26364a;
|
||
background: rgba(17, 24, 39, 0.92);
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs) {
|
||
border-bottom-color: #26364a;
|
||
background: #111827;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-count) {
|
||
background: #243247;
|
||
color: #bfdbfe;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu) {
|
||
background: transparent;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper.el-popper) {
|
||
border-color: #334155;
|
||
background: #172033;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-popper__arrow::before) {
|
||
border-color: #334155;
|
||
background: #172033;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-header) {
|
||
border-bottom-color: #334155;
|
||
color: #e5edf7;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-header span:last-child) {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item) {
|
||
color: #dbe5f1;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item:hover),
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item:focus),
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-popper .el-dropdown-menu__item.is-current) {
|
||
background: #243247;
|
||
color: #bfdbfe;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-item-mark) {
|
||
background: #6f87a1;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close) {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close:hover),
|
||
:global([data-ctms-theme="dark"] .workspace-tabs-overflow-close:focus-visible) {
|
||
background: rgba(226, 232, 240, 0.1);
|
||
color: #f8fafc;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .desktop-content) {
|
||
background: #0f172a;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .command-trigger kbd) {
|
||
border-color: #334155;
|
||
background: #0f172a;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .account-avatar) {
|
||
background: #3e5c77;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tab-context-menu) {
|
||
border-color: #334155;
|
||
background: #172033;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tab-context-menu button) {
|
||
color: #dbe5f1;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tab-context-menu button:hover:not(:disabled)) {
|
||
background: #243247;
|
||
color: #bfdbfe;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .workspace-tab-context-menu button:disabled) {
|
||
color: #64748b;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .tab-close) {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .tab-close:hover) {
|
||
background: rgba(226, 232, 240, 0.1);
|
||
color: #f8fafc;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .connection-panel) {
|
||
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;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .reminder-total) {
|
||
background: rgba(242, 139, 139, 0.14);
|
||
color: #fecaca;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .reminder-body small),
|
||
:global([data-ctms-theme="dark"] .reminder-empty) {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-scroll::-webkit-scrollbar-thumb) {
|
||
border-color: #111827;
|
||
background: #334155;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .sidebar-scroll::-webkit-scrollbar-thumb:hover) {
|
||
background: #475569;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .profile-settings-dialog),
|
||
:global([data-ctms-theme="dark"] .desktop-preferences-dialog) {
|
||
background: #111827;
|
||
}
|
||
|
||
:global([data-ctms-theme="dark"] .desktop-preferences-dialog .el-dialog__body) {
|
||
/* 与深色侧边栏 --pref-bg-sidebar 保持一致 */
|
||
background: #131b28;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.icon-button,
|
||
.desktop-route-enter-active,
|
||
.desktop-route-leave-active {
|
||
transition-duration: 1ms !important;
|
||
}
|
||
|
||
.desktop-route-enter-from,
|
||
.desktop-route-leave-to {
|
||
transform: none;
|
||
}
|
||
}
|
||
</style>
|