429 lines
12 KiB
Vue
429 lines
12 KiB
Vue
<template>
|
|
<el-container class="layout-container">
|
|
<el-aside :width="isCollapsed ? '72px' : '240px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
|
|
<div class="aside-logo">
|
|
<div class="logo-icon"></div>
|
|
<span class="logo-text">CTMS</span>
|
|
</div>
|
|
<el-menu
|
|
router
|
|
:default-active="activeMenu"
|
|
:collapse="isCollapsed"
|
|
:collapse-transition="true"
|
|
class="aside-menu"
|
|
>
|
|
<el-menu-item v-if="!isAdmin" index="/workbench">
|
|
<el-icon><Monitor /></el-icon>
|
|
<span>我的工作台</span>
|
|
</el-menu-item>
|
|
|
|
<template v-if="isAdmin">
|
|
<div class="menu-divider">管理后台</div>
|
|
<el-menu-item index="/admin/users">
|
|
<el-icon><User /></el-icon>
|
|
<span>账号管理</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/admin/projects">
|
|
<el-icon><Suitcase /></el-icon>
|
|
<span>项目管理</span>
|
|
</el-menu-item>
|
|
<el-menu-item v-if="isAdmin" index="/admin/audit-logs">
|
|
<el-icon><Document /></el-icon>
|
|
<span>审计日志</span>
|
|
</el-menu-item>
|
|
</template>
|
|
|
|
<template v-if="study.currentStudy">
|
|
<div class="menu-divider">当前项目</div>
|
|
<el-menu-item index="/study/home">
|
|
<el-icon><House /></el-icon>
|
|
<span>项目概览</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/milestones">
|
|
<el-icon><Calendar /></el-icon>
|
|
<span>伦理与启动</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/subjects">
|
|
<el-icon><User /></el-icon>
|
|
<span>受试者</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/aes">
|
|
<el-icon><WarningFilled /></el-icon>
|
|
<span>不良事件</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/issues">
|
|
<el-icon><Flag /></el-icon>
|
|
<span>风险与问题</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/data-queries">
|
|
<el-icon><ChatDotRound /></el-icon>
|
|
<span>数据问题</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/verifications">
|
|
<el-icon><CircleCheck /></el-icon>
|
|
<span>数据核查</span>
|
|
</el-menu-item>
|
|
<el-sub-menu index="imp">
|
|
<template #title>
|
|
<el-icon><Box /></el-icon>
|
|
<span>药品管理</span>
|
|
</template>
|
|
<el-menu-item index="/study/imp/inventory">库存</el-menu-item>
|
|
<el-menu-item index="/study/imp/products">产品</el-menu-item>
|
|
<el-menu-item index="/study/imp/batches">批次</el-menu-item>
|
|
<el-menu-item index="/study/imp/transactions">交易台账</el-menu-item>
|
|
</el-sub-menu>
|
|
<el-menu-item index="/study/finance">
|
|
<el-icon><Coin /></el-icon>
|
|
<span>费用管理</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="/study/faq">
|
|
<el-icon><Notebook /></el-icon>
|
|
<span>知识库</span>
|
|
</el-menu-item>
|
|
</template>
|
|
</el-menu>
|
|
</el-aside>
|
|
|
|
<el-container class="main-container">
|
|
<el-header class="layout-header">
|
|
<div class="header-left">
|
|
<el-button class="collapse-btn" @click="toggleCollapse" aria-label="收缩侧边栏">
|
|
<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" />
|
|
<line x1="9.5" y1="5" x2="9.5" y2="19" stroke="currentColor" stroke-width="1.8" />
|
|
</svg>
|
|
</el-icon>
|
|
</el-button>
|
|
</div>
|
|
<div class="header-right">
|
|
<StudySelector />
|
|
<el-dropdown trigger="click" @command="onCommand" class="user-profile">
|
|
<span class="dropdown-trigger compact">
|
|
<el-avatar
|
|
:size="28"
|
|
:src="auth.user?.avatar_url"
|
|
shape="square"
|
|
class="user-avatar-comp"
|
|
>
|
|
{{ (auth.user?.username || auth.user?.email || "U").charAt(0).toUpperCase() }}
|
|
</el-avatar>
|
|
<span class="username">{{ auth.user?.username || auth.user?.email || "用户" }}</span>
|
|
<el-icon><ArrowDown /></el-icon>
|
|
</span>
|
|
<template #dropdown>
|
|
<el-dropdown-menu class="user-dropdown-menu">
|
|
<el-dropdown-item command="profile">
|
|
<el-icon><User /></el-icon>
|
|
<span>个人中心</span>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="logout">
|
|
<el-icon><SwitchButton /></el-icon>
|
|
<span>退出登录</span>
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</el-header>
|
|
|
|
<el-main class="layout-main">
|
|
<div class="content-wrapper">
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="fade-transform" mode="out-in">
|
|
<component :is="Component" />
|
|
</transition>
|
|
</router-view>
|
|
</div>
|
|
</el-main>
|
|
</el-container>
|
|
</el-container>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { useAuthStore } from "../store/auth";
|
|
import { useStudyStore } from "../store/study";
|
|
import StudySelector from "./StudySelector.vue";
|
|
import {
|
|
Monitor, User, Suitcase, House, Calendar, WarningFilled, Flag, ChatDotRound,
|
|
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
|
|
} from "@element-plus/icons-vue";
|
|
|
|
const auth = useAuthStore();
|
|
const study = useStudyStore();
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
|
const isPm = computed(() => auth.user?.role === "PM" || study.currentStudyRole === "PM");
|
|
const isCollapsed = ref(localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
|
const activeMenu = computed(() => {
|
|
const path = route.path;
|
|
if (path.startsWith("/study/milestones/")) return "/study/milestones";
|
|
if (path.startsWith("/study/subjects/")) return "/study/subjects";
|
|
if (path.startsWith("/study/aes/")) return "/study/aes";
|
|
if (path.startsWith("/study/issues/")) return "/study/issues";
|
|
if (path.startsWith("/study/data-queries/")) return "/study/data-queries";
|
|
if (path.startsWith("/study/finance/")) return "/study/finance";
|
|
if (path.startsWith("/study/faq/")) return "/study/faq";
|
|
if (path.startsWith("/projects/")) return "/admin/projects";
|
|
if (path.startsWith("/admin/projects/")) return "/admin/projects";
|
|
return path;
|
|
});
|
|
|
|
const toggleCollapse = () => {
|
|
isCollapsed.value = !isCollapsed.value;
|
|
localStorage.setItem("ctms_sidebar_collapsed", isCollapsed.value ? "1" : "0");
|
|
};
|
|
|
|
const onLogout = () => {
|
|
auth.logout();
|
|
study.clearCurrentStudy();
|
|
router.replace("/login");
|
|
};
|
|
|
|
const onCommand = (cmd: string) => {
|
|
if (cmd === "logout") {
|
|
onLogout();
|
|
} else if (cmd === "profile") {
|
|
router.push("/profile");
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.layout-container {
|
|
height: 100vh;
|
|
background-color: transparent;
|
|
}
|
|
|
|
.layout-aside {
|
|
background-color: #ffffff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: none;
|
|
z-index: 10;
|
|
border-right: 1px solid var(--ctms-border-color);
|
|
transition: width 0.18s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.aside-menu {
|
|
transition: padding 0.18s ease;
|
|
}
|
|
|
|
.layout-aside.collapsed .logo-text {
|
|
opacity: 0;
|
|
transform: translateX(-4px);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.layout-aside.collapsed .aside-logo {
|
|
justify-content: center;
|
|
padding: 0;
|
|
}
|
|
|
|
.layout-aside.collapsed .menu-divider {
|
|
display: none;
|
|
}
|
|
|
|
.aside-logo {
|
|
height: 64px;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 24px;
|
|
background-color: transparent;
|
|
transition: padding 0.18s ease;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: var(--ctms-primary);
|
|
border-radius: 10px;
|
|
margin-right: 12px;
|
|
box-shadow: 0 6px 14px rgba(63, 93, 117, 0.18);
|
|
}
|
|
|
|
.logo-text {
|
|
color: var(--ctms-text-main);
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
letter-spacing: 1px;
|
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
}
|
|
|
|
.aside-menu {
|
|
border-right: none;
|
|
background-color: transparent;
|
|
flex: 1;
|
|
padding: 12px 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.menu-divider {
|
|
padding: 16px 24px 8px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--ctms-text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
|
|
:deep(.el-menu-item) {
|
|
height: 44px;
|
|
line-height: 44px;
|
|
margin: 4px 12px;
|
|
border-radius: 12px;
|
|
color: var(--ctms-text-regular) !important;
|
|
transition: var(--ctms-transition);
|
|
}
|
|
|
|
:deep(.el-menu-item:hover) {
|
|
color: var(--ctms-text-main) !important;
|
|
background-color: var(--ctms-bg-muted) !important;
|
|
}
|
|
|
|
:deep(.el-menu-item.is-active) {
|
|
color: var(--ctms-primary) !important;
|
|
background-color: var(--ctms-primary-light) !important;
|
|
box-shadow: none;
|
|
}
|
|
|
|
:deep(.el-menu-item.is-active::before) {
|
|
content: "";
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 12px;
|
|
bottom: 12px;
|
|
width: 3px;
|
|
background-color: var(--ctms-primary);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
:deep(.el-sub-menu__title) {
|
|
color: var(--ctms-text-regular) !important;
|
|
margin: 4px 12px;
|
|
border-radius: 12px;
|
|
height: 44px;
|
|
line-height: 44px;
|
|
}
|
|
|
|
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
|
color: var(--ctms-text-main) !important;
|
|
}
|
|
|
|
:deep(.el-menu--inline) {
|
|
background-color: transparent !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
:deep(.el-menu--inline .el-menu-item) {
|
|
padding-left: 48px !important;
|
|
margin: 2px 12px;
|
|
}
|
|
|
|
.layout-header {
|
|
background-color: #ffffff;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 24px;
|
|
box-shadow: none;
|
|
height: 64px !important;
|
|
z-index: 9;
|
|
border-bottom: 1px solid var(--ctms-border-color);
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.collapse-btn {
|
|
border: 1px solid var(--ctms-border-color);
|
|
color: var(--ctms-text-regular);
|
|
}
|
|
|
|
.collapse-icon {
|
|
font-size: 18px;
|
|
}
|
|
|
|
|
|
.user-profile {
|
|
display: flex;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dropdown-trigger.compact {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 12px;
|
|
border-radius: 999px;
|
|
background: #ffffff;
|
|
border: 1px solid var(--ctms-border-color);
|
|
color: var(--ctms-text-regular);
|
|
transition: var(--ctms-transition);
|
|
height: 40px;
|
|
}
|
|
|
|
.dropdown-trigger.compact:hover {
|
|
background: var(--ctms-bg-muted);
|
|
border-color: var(--ctms-border-color-hover);
|
|
}
|
|
|
|
.user-avatar-comp {
|
|
color: var(--ctms-primary) !important;
|
|
background-color: transparent !important;
|
|
border-radius: 10px !important;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.username {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin: 0 10px 0 8px;
|
|
max-width: 150px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.layout-main {
|
|
padding: 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.content-wrapper {
|
|
padding: 28px;
|
|
min-height: calc(100vh - 64px);
|
|
}
|
|
|
|
/* 过渡动画 */
|
|
.fade-transform-enter-active,
|
|
.fade-transform-leave-active {
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.fade-transform-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(-15px);
|
|
}
|
|
|
|
.fade-transform-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(15px);
|
|
}
|
|
</style>
|