未知(继上次中断)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside :width="isCollapsed ? '72px' : '240px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
|
||||
<el-aside :width="isCollapsed ? '68px' : '200px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
|
||||
<div class="aside-logo">
|
||||
<div class="logo-icon"></div>
|
||||
<span class="logo-text">{{ TEXT.common.appName }}</span>
|
||||
@@ -100,9 +100,50 @@
|
||||
</svg>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
|
||||
<!-- 移入 Header 的面包屑 -->
|
||||
<div v-if="breadcrumbs.length" class="header-breadcrumb">
|
||||
<template v-for="(item, index) in breadcrumbs" :key="index">
|
||||
<!-- 带下拉的选择器类型 -->
|
||||
<el-dropdown v-if="item.hasDropdown" trigger="click" @command="handleBreadcrumbCommand">
|
||||
<div class="breadcrumb-item-wrapper is-link">
|
||||
<span class="breadcrumb-item-text is-link-text">{{ item.label }}</span>
|
||||
<el-icon class="breadcrumb-dropdown-icon"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="breadcrumb-dropdown-menu">
|
||||
<template v-if="item.type === 'study'">
|
||||
<el-dropdown-item v-for="s in studies" :key="s.id" :command="{ type: 'study', value: s }" :class="{ active: study.currentStudy?.id === s.id }">
|
||||
{{ s.name }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'site'">
|
||||
<el-dropdown-item :command="{ type: 'site', value: null }" :class="{ active: !study.currentSite }">
|
||||
{{ TEXT.common.labels.allSites || '所有中心' }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-for="s in sites" :key="s.id" :command="{ type: 'site', value: s }" :class="{ active: study.currentSite?.id === s.id }">
|
||||
{{ s.name }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 普通链接或静态文本 -->
|
||||
<div
|
||||
v-else
|
||||
class="breadcrumb-item-wrapper"
|
||||
:class="{ 'is-link': item.path && index < breadcrumbs.length - 1 }"
|
||||
@click="item.path && index < breadcrumbs.length - 1 && router.push(item.path)"
|
||||
>
|
||||
<span class="breadcrumb-item-text" :class="{ 'is-link-text': index > 0 && index < breadcrumbs.length - 1 }">{{ item.label }}</span>
|
||||
</div>
|
||||
|
||||
<span v-if="index < breadcrumbs.length - 1" class="breadcrumb-separator">/</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<StudySelector />
|
||||
<el-dropdown trigger="click" @command="onCommand" class="user-profile">
|
||||
<span class="dropdown-trigger compact">
|
||||
<el-avatar
|
||||
@@ -131,6 +172,7 @@
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
|
||||
<el-main class="layout-main">
|
||||
<div class="content-wrapper">
|
||||
@@ -146,16 +188,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import StudySelector from "./StudySelector.vue";
|
||||
import { fetchStudies } from "../api/studies";
|
||||
import { fetchSites } from "../api/sites";
|
||||
import { TEXT } from "../locales";
|
||||
import {
|
||||
Monitor, User, Suitcase, House, Calendar, Flag, ChatDotRound,
|
||||
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
@@ -189,6 +233,121 @@ const activeMenu = computed(() => {
|
||||
return path;
|
||||
});
|
||||
|
||||
const studies = ref<any[]>([]);
|
||||
const sites = ref<any[]>([]);
|
||||
|
||||
const loadStudies = async () => {
|
||||
try {
|
||||
const { data } = await fetchStudies();
|
||||
studies.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const loadSites = async () => {
|
||||
if (!study.currentStudy) {
|
||||
sites.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await fetchSites(study.currentStudy.id, { limit: 500 });
|
||||
sites.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} catch {
|
||||
sites.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const items: any[] = [];
|
||||
|
||||
if (route.path.startsWith("/admin")) {
|
||||
items.push({
|
||||
label: TEXT.menu.admin,
|
||||
path: "/admin/users"
|
||||
});
|
||||
} else if (study.currentStudy) {
|
||||
// 1. 项目名
|
||||
items.push({
|
||||
label: (study.currentStudy.name || "").replace(/^示例项目[::]/, ''),
|
||||
path: "/project/overview",
|
||||
hasDropdown: true,
|
||||
type: 'study'
|
||||
});
|
||||
|
||||
// 2. 中心名 (逻辑增强:详情页优先显示具体中心,列表页显示筛选中心)
|
||||
items.push({
|
||||
label: study.viewContext?.siteName || study.currentSite?.name || "所有中心",
|
||||
hasDropdown: true,
|
||||
type: 'site'
|
||||
});
|
||||
|
||||
// 3. 模块名 (逻辑增强:当在详情页时显示所属模块)
|
||||
const moduleMap: Record<string, { label: string, path: string }> = {
|
||||
"/fees/contracts": { label: TEXT.menu.feeContracts, path: "/fees/contracts" },
|
||||
"/fees/special": { label: TEXT.menu.feeSpecials, path: "/fees/special" },
|
||||
"/drug/shipments": { label: TEXT.menu.drugShipments, path: "/drug/shipments" },
|
||||
"/subjects": { label: TEXT.menu.subjects, path: "/subjects" },
|
||||
"/startup/feasibility": { label: TEXT.menu.startupFeasibilityEthics, path: "/startup/feasibility-ethics" },
|
||||
"/startup/ethics": { label: TEXT.menu.startupFeasibilityEthics, path: "/startup/feasibility-ethics" },
|
||||
"/startup/kickoff": { label: TEXT.menu.startupMeetingAuth, path: "/startup/meeting-auth" },
|
||||
"/startup/training": { label: TEXT.menu.startupMeetingAuth, path: "/startup/meeting-auth" },
|
||||
"/knowledge/medical-consult": { label: TEXT.menu.knowledgeMedicalConsult, path: "/knowledge/medical-consult" },
|
||||
"/knowledge/notes": { label: TEXT.menu.knowledgeNotes, path: "/knowledge/notes" },
|
||||
};
|
||||
|
||||
let matchedModule = null;
|
||||
for (const prefix in moduleMap) {
|
||||
if (route.path.startsWith(prefix)) {
|
||||
matchedModule = moduleMap[prefix];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedModule) {
|
||||
// 如果当前路径不是模块主路径,说明在子页面/详情页,需要把模块名展示出来
|
||||
if (route.path !== matchedModule.path) {
|
||||
items.push({
|
||||
label: matchedModule.label,
|
||||
path: matchedModule.path
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 当前页面标题
|
||||
const title = route.meta?.title as string | undefined;
|
||||
if (title && title !== study.currentStudy?.name && title !== TEXT.menu.projectOverview && title !== (study.viewContext?.siteName || study.currentSite?.name)) {
|
||||
// 检查 title 是否已经作为模块名添加过了
|
||||
const isModuleTitle = items.some(item => item.label === title);
|
||||
if (!isModuleTitle) {
|
||||
items.push({
|
||||
label: title,
|
||||
path: route.path
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const handleBreadcrumbCommand = (cmd: any) => {
|
||||
if (cmd.type === 'study') {
|
||||
study.setCurrentStudy(cmd.value);
|
||||
router.push("/project/overview");
|
||||
} else if (cmd.type === 'site') {
|
||||
study.setCurrentSite(cmd.value);
|
||||
// 刷新当前页面或根据业务逻辑处理
|
||||
ElMessage.success(`已切换至: ${cmd.value?.name || '所有中心'}`);
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => study.currentStudy, () => {
|
||||
loadSites();
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => route.path, () => {
|
||||
study.setViewContext(null);
|
||||
});
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
localStorage.setItem("ctms_sidebar_collapsed", isCollapsed.value ? "1" : "0");
|
||||
@@ -200,6 +359,10 @@ const onLogout = () => {
|
||||
router.replace("/login");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadStudies();
|
||||
});
|
||||
|
||||
const onCommand = (cmd: string) => {
|
||||
if (cmd === "logout") {
|
||||
onLogout();
|
||||
@@ -216,12 +379,12 @@ const onCommand = (cmd: string) => {
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
background-color: #ffffff;
|
||||
background-color: #2c3e50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: none;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
border-right: 1px solid var(--ctms-border-color);
|
||||
border-right: none;
|
||||
transition: width 0.18s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -249,7 +412,7 @@ const onCommand = (cmd: string) => {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
padding: 0 16px;
|
||||
background-color: transparent;
|
||||
transition: padding 0.18s ease;
|
||||
}
|
||||
@@ -257,17 +420,17 @@ const onCommand = (cmd: string) => {
|
||||
.logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--ctms-primary);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 6px 14px rgba(63, 93, 117, 0.18);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--ctms-text-main);
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
@@ -280,63 +443,68 @@ const onCommand = (cmd: string) => {
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
padding: 16px 24px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-secondary);
|
||||
padding: 16px 16px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
.layout-aside :deep(.el-menu-item) {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
margin: 4px 12px;
|
||||
border-radius: 12px;
|
||||
color: var(--ctms-text-regular) !important;
|
||||
margin: 4px 10px;
|
||||
border-radius: 8px;
|
||||
color: #f1f5f9 !important;
|
||||
transition: var(--ctms-transition);
|
||||
}
|
||||
|
||||
:deep(.el-menu-item:hover) {
|
||||
color: var(--ctms-text-main) !important;
|
||||
background-color: var(--ctms-bg-muted) !important;
|
||||
.layout-aside :deep(.el-menu-item:hover) {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
color: var(--ctms-primary) !important;
|
||||
background-color: var(--ctms-primary-light) !important;
|
||||
.layout-aside :deep(.el-menu-item.is-active) {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active::before) {
|
||||
.layout-aside :deep(.el-menu-item.is-active::before) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
width: 3px;
|
||||
background-color: var(--ctms-primary);
|
||||
border-radius: 999px;
|
||||
background-color: #3b82f6;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title) {
|
||||
color: var(--ctms-text-regular) !important;
|
||||
margin: 4px 12px;
|
||||
border-radius: 12px;
|
||||
.layout-aside :deep(.el-sub-menu__title) {
|
||||
color: #f1f5f9 !important;
|
||||
margin: 4px 10px;
|
||||
border-radius: 8px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
||||
color: var(--ctms-text-main) !important;
|
||||
.layout-aside :deep(.el-sub-menu__title:hover) {
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu--inline) {
|
||||
.layout-aside :deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.layout-aside :deep(.el-menu--inline) {
|
||||
background-color: transparent !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu--inline .el-menu-item) {
|
||||
.layout-aside :deep(.el-menu--inline .el-menu-item) {
|
||||
padding-left: 48px !important;
|
||||
margin: 2px 12px;
|
||||
}
|
||||
@@ -350,7 +518,53 @@ const onCommand = (cmd: string) => {
|
||||
box-shadow: none;
|
||||
height: 64px !important;
|
||||
z-index: 9;
|
||||
border-bottom: 1px solid var(--ctms-border-color);
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.header-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--ctms-text-regular);
|
||||
font-size: 14px;
|
||||
margin-left: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-item-wrapper.is-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breadcrumb-item-wrapper.is-link:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.is-link-text {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-item-text {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-icon {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 4px;
|
||||
color: #cbd5e1;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@@ -422,7 +636,7 @@ const onCommand = (cmd: string) => {
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 28px;
|
||||
padding: 16px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
@@ -441,4 +655,13 @@ const onCommand = (cmd: string) => {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
.breadcrumb-dropdown-menu {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.breadcrumb-dropdown-menu :deep(.el-dropdown-menu__item.active) {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user