feat(工作台): 优化入口布局与连接安全状态
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<el-container class="layout-container web-layout-container">
|
||||
<el-aside :width="isCollapsed ? '68px' : '200px'" class="layout-aside" :class="{ collapsed: isCollapsed }">
|
||||
<el-container
|
||||
class="layout-container web-layout-container"
|
||||
:class="{ 'mobile-nav-open': isMobileNavOpen, 'mobile-layout': isMobileViewport }"
|
||||
@keydown.esc="closeMobileNav"
|
||||
>
|
||||
<el-aside
|
||||
:width="isMobileViewport ? '280px' : (isCollapsed ? '68px' : '200px')"
|
||||
class="layout-aside"
|
||||
:class="{ collapsed: isCollapsed, 'mobile-open': isMobileNavOpen }"
|
||||
>
|
||||
<div class="aside-logo">
|
||||
<img class="logo-icon" src="/icons/ctms-icon-192.png" alt="" aria-hidden="true" />
|
||||
<span class="logo-text">{{ TEXT.common.appName }}</span>
|
||||
@@ -129,7 +137,12 @@
|
||||
<el-container class="main-container">
|
||||
<el-header class="layout-header">
|
||||
<div class="header-left">
|
||||
<el-button class="collapse-btn" @click="toggleCollapse" :aria-label="TEXT.common.labels.collapseSidebar">
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
:aria-expanded="isMobileViewport ? isMobileNavOpen : undefined"
|
||||
@click="toggleCollapse"
|
||||
:aria-label="isMobileViewport ? '打开导航菜单' : TEXT.common.labels.collapseSidebar"
|
||||
>
|
||||
<el-icon class="collapse-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" />
|
||||
@@ -358,6 +371,14 @@
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<button
|
||||
v-if="isMobileNavOpen"
|
||||
type="button"
|
||||
class="mobile-nav-backdrop"
|
||||
aria-label="关闭导航菜单"
|
||||
@click="closeMobileNav"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="profileDialogVisible"
|
||||
class="profile-settings-dialog"
|
||||
@@ -444,7 +465,12 @@ const route = useRoute();
|
||||
const isDesktop = isTauriRuntime();
|
||||
const desktopMetadata = getAppMetadata();
|
||||
const isAdmin = computed(() => !!auth.user?.is_admin);
|
||||
const isCollapsed = ref(localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
||||
const isCollapsed = ref(typeof localStorage !== "undefined" && localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
||||
const isMobileViewport = ref(
|
||||
typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches,
|
||||
);
|
||||
const isMobileNavOpen = ref(false);
|
||||
let mobileViewportMediaQuery: MediaQueryList | undefined;
|
||||
const projectRole = computed(() => study.currentStudyRole || (study.currentStudy as any)?.role_in_study || "");
|
||||
const canAccessAdminPermissions = computed(() => isAdmin.value || projectRole.value === "PM");
|
||||
const canAccessAuditLogs = computed(() =>
|
||||
@@ -478,13 +504,14 @@ const headerReminderStats = ref({
|
||||
const headerClockNow = ref(new Date());
|
||||
let headerClockTimer: number | undefined;
|
||||
let desktopMenuUnlisten: (() => void) | undefined;
|
||||
let mobileViewportChangeHandler: ((event: MediaQueryListEvent) => void) | undefined;
|
||||
const isAdminContext = computed(() => route.path.startsWith("/admin"));
|
||||
const hasProjectContext = computed(() => Boolean(study.currentStudy) && !isAdminContext.value);
|
||||
|
||||
/* 动态设置 body 上的侧边栏宽度 CSS 变量,用于全局弹窗定位偏移 */
|
||||
const hasSidebar = computed(() => isAdmin.value || !!study.currentStudy);
|
||||
watchEffect(() => {
|
||||
const width = !hasSidebar.value ? '0px' : (isCollapsed.value ? '68px' : '200px');
|
||||
const width = !hasSidebar.value || isMobileViewport.value ? '0px' : (isCollapsed.value ? '68px' : '200px');
|
||||
document.body.style.setProperty('--ctms-sidebar-width', width);
|
||||
// 在 body 上添加/移除标记类,以便全局 CSS :has() 降级使用
|
||||
if (hasSidebar.value) {
|
||||
@@ -1021,6 +1048,7 @@ const handleBreadcrumbCommand = async (cmd: any) => {
|
||||
};
|
||||
|
||||
const handleMenuSelect = (index: string) => {
|
||||
closeMobileNav();
|
||||
if (!index || index === route.path) {
|
||||
return;
|
||||
}
|
||||
@@ -1047,6 +1075,7 @@ const handleReminderCommand = (cmd: string) => {
|
||||
};
|
||||
|
||||
watch(() => route.path, () => {
|
||||
closeMobileNav();
|
||||
study.setViewContext(null);
|
||||
if (isDesktop) {
|
||||
const current = currentDesktopRoutePreference.value;
|
||||
@@ -1070,7 +1099,20 @@ watch(
|
||||
() => refreshDesktopRoutePreferences(),
|
||||
);
|
||||
|
||||
const closeMobileNav = () => {
|
||||
isMobileNavOpen.value = false;
|
||||
};
|
||||
|
||||
const syncMobileViewport = (matches: boolean) => {
|
||||
isMobileViewport.value = matches;
|
||||
if (!matches) closeMobileNav();
|
||||
};
|
||||
|
||||
const toggleCollapse = () => {
|
||||
if (isMobileViewport.value) {
|
||||
isMobileNavOpen.value = !isMobileNavOpen.value;
|
||||
return;
|
||||
}
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
localStorage.setItem("ctms_sidebar_collapsed", isCollapsed.value ? "1" : "0");
|
||||
};
|
||||
@@ -1110,6 +1152,12 @@ const onLogout = async () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (typeof window.matchMedia === "function") {
|
||||
mobileViewportMediaQuery = window.matchMedia("(max-width: 767px)");
|
||||
syncMobileViewport(mobileViewportMediaQuery.matches);
|
||||
mobileViewportChangeHandler = (event) => syncMobileViewport(event.matches);
|
||||
mobileViewportMediaQuery.addEventListener("change", mobileViewportChangeHandler);
|
||||
}
|
||||
headerClockNow.value = new Date();
|
||||
headerClockTimer = window.setInterval(() => {
|
||||
headerClockNow.value = new Date();
|
||||
@@ -1139,6 +1187,9 @@ onBeforeUnmount(() => {
|
||||
window.clearInterval(headerClockTimer);
|
||||
}
|
||||
window.removeEventListener(DESKTOP_SERVER_URL_CHANGED_EVENT, updateDesktopServerUrl);
|
||||
if (mobileViewportMediaQuery && mobileViewportChangeHandler) {
|
||||
mobileViewportMediaQuery.removeEventListener("change", mobileViewportChangeHandler);
|
||||
}
|
||||
desktopMenuUnlisten?.();
|
||||
});
|
||||
|
||||
@@ -2400,6 +2451,163 @@ useDesktopShortcuts(
|
||||
transform: translateY(-4px) scale(0.998);
|
||||
}
|
||||
|
||||
.mobile-nav-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.web-layout-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.web-layout-container .layout-aside {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 280px !important;
|
||||
max-width: calc(100vw - 42px);
|
||||
transform: translateX(-105%);
|
||||
box-shadow: 14px 0 36px rgba(15, 23, 42, 0.24);
|
||||
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.web-layout-container.mobile-nav-open .layout-aside {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.web-layout-container .main-container {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.web-layout-container .layout-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.web-layout-container .content-wrapper {
|
||||
min-height: calc(100dvh - 52px);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-breadcrumb {
|
||||
max-width: none;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.header-breadcrumb > :not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-breadcrumb .breadcrumb-item-wrapper {
|
||||
max-width: min(48vw, 210px);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.workbench-return-button {
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.workbench-return-button .el-icon {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-account-role,
|
||||
.user-profile .username,
|
||||
.user-profile .el-icon,
|
||||
.user-profile :deep(.account-connection-status) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-trigger.compact {
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.mobile-nav-backdrop {
|
||||
position: fixed;
|
||||
z-index: 90;
|
||||
inset: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: rgba(15, 23, 42, 0.38);
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.web-layout-container .layout-header {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.web-layout-container .content-wrapper {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.header-breadcrumb .breadcrumb-item-wrapper {
|
||||
max-width: 42vw;
|
||||
}
|
||||
|
||||
.header-reminder-button,
|
||||
.collapse-btn,
|
||||
.workbench-return-button,
|
||||
.dropdown-trigger.compact {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏网页端:增加呼吸感,但保留业务表格的全宽工作区。 */
|
||||
@media (min-width: 1440px) {
|
||||
.web-layout-container .layout-header {
|
||||
height: 56px !important;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.web-layout-container .aside-logo {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.web-layout-container .content-wrapper {
|
||||
min-height: calc(100dvh - 56px);
|
||||
padding: 18px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
.web-layout-container .layout-header {
|
||||
padding-right: 28px;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.web-layout-container .content-wrapper {
|
||||
padding: 22px 32px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.layout-aside,
|
||||
.aside-menu,
|
||||
|
||||
Reference in New Issue
Block a user