UI界面模拟shadcn admin效果

This commit is contained in:
Cheng Zhou
2025-12-25 15:47:47 +08:00
parent 148fa6ad9a
commit a1a4964cd2
25 changed files with 1061 additions and 636 deletions
+105 -52
View File
@@ -1,6 +1,6 @@
<template>
<el-container class="layout-container">
<el-aside width="240px" class="layout-aside" v-if="isAdmin || study.currentStudy">
<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>
@@ -8,6 +8,8 @@
<el-menu
router
:default-active="$route.path"
:collapse="isCollapsed"
:collapse-transition="true"
class="aside-menu"
>
<el-menu-item v-if="!isAdmin" index="/workbench">
@@ -34,32 +36,32 @@
<span>项目概览</span>
</el-menu-item>
<el-menu-item index="/study/milestones">
<el-icon><Timer /></el-icon>
<el-icon><Calendar /></el-icon>
<span>伦理与启动</span>
</el-menu-item>
<el-menu-item index="/study/subjects">
<el-icon><UserFilled /></el-icon>
<el-icon><User /></el-icon>
<span>受试者</span>
</el-menu-item>
<el-menu-item index="/study/aes">
<el-icon><Warning /></el-icon>
<el-icon><WarningFilled /></el-icon>
<span>不良事件</span>
</el-menu-item>
<el-menu-item index="/study/issues">
<el-icon><InfoFilled /></el-icon>
<el-icon><Flag /></el-icon>
<span>风险与问题</span>
</el-menu-item>
<el-menu-item index="/study/data-queries">
<el-icon><QuestionFilled /></el-icon>
<el-icon><ChatDotRound /></el-icon>
<span>数据问题</span>
</el-menu-item>
<el-menu-item index="/study/verifications">
<el-icon><Checked /></el-icon>
<el-icon><CircleCheck /></el-icon>
<span>数据核查</span>
</el-menu-item>
<el-sub-menu index="imp">
<template #title>
<el-icon><Management /></el-icon>
<el-icon><Box /></el-icon>
<span>药品管理</span>
</template>
<el-menu-item index="/study/imp/inventory">库存</el-menu-item>
@@ -68,15 +70,15 @@
<el-menu-item index="/study/imp/transactions">交易台账</el-menu-item>
</el-sub-menu>
<el-menu-item index="/study/finance">
<el-icon><Money /></el-icon>
<el-icon><Coin /></el-icon>
<span>费用治理</span>
</el-menu-item>
<el-menu-item index="/study/faq">
<el-icon><Collection /></el-icon>
<el-icon><Notebook /></el-icon>
<span>知识库</span>
</el-menu-item>
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">
<el-icon><Memo /></el-icon>
<el-icon><Document /></el-icon>
<span>审计日志</span>
</el-menu-item>
</template>
@@ -86,7 +88,14 @@
<el-container class="main-container">
<el-header class="layout-header">
<div class="header-left">
<!-- 移除冗余的 study-tag StudySelector 统一展示交互 -->
<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 />
@@ -133,15 +142,14 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import StudySelector from "./StudySelector.vue";
import {
Monitor, User, Suitcase, House, Timer, UserFilled,
Warning, InfoFilled, QuestionFilled, Checked, Management,
Money, Collection, Memo, ArrowDown, SwitchButton
Monitor, User, Suitcase, House, Calendar, WarningFilled, Flag, ChatDotRound,
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
} from "@element-plus/icons-vue";
const auth = useAuthStore();
@@ -149,6 +157,12 @@ const study = useStudyStore();
const router = useRouter();
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 toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value;
localStorage.setItem("ctms_sidebar_collapsed", isCollapsed.value ? "1" : "0");
};
const onLogout = () => {
auth.logout();
@@ -168,16 +182,37 @@ const onCommand = (cmd: string) => {
<style scoped>
.layout-container {
height: 100vh;
background-color: #f6f8fb;
background-color: transparent;
}
.layout-aside {
background-color: #001529; /* 经典深色,保持专业感 */
background-color: #ffffff;
display: flex;
flex-direction: column;
box-shadow: 4px 0 10px rgba(0, 0, 0, 0.05);
box-shadow: none;
z-index: 10;
border-right: 1px solid rgba(255, 255, 255, 0.05);
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 {
@@ -185,23 +220,25 @@ const onCommand = (cmd: string) => {
display: flex;
align-items: center;
padding: 0 24px;
background-color: #001529;
background-color: transparent;
transition: padding 0.18s ease;
}
.logo-icon {
width: 28px;
height: 28px;
background: linear-gradient(135deg, var(--ctms-primary) 0%, #6366f1 100%);
border-radius: 8px;
background: var(--ctms-primary);
border-radius: 10px;
margin-right: 12px;
box-shadow: 0 4px 8px rgba(47, 84, 235, 0.3);
box-shadow: 0 6px 14px rgba(63, 93, 117, 0.18);
}
.logo-text {
color: #fff;
font-size: 20px;
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 {
@@ -216,7 +253,7 @@ const onCommand = (cmd: string) => {
padding: 16px 24px 8px;
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.35);
color: var(--ctms-text-secondary);
text-transform: uppercase;
letter-spacing: 1.5px;
}
@@ -225,47 +262,47 @@ const onCommand = (cmd: string) => {
height: 44px;
line-height: 44px;
margin: 4px 12px;
border-radius: var(--ctms-radius);
color: rgba(255, 255, 255, 0.7) !important;
border-radius: 12px;
color: var(--ctms-text-regular) !important;
transition: var(--ctms-transition);
}
:deep(.el-menu-item:hover) {
color: #fff !important;
background-color: rgba(255, 255, 255, 0.05) !important;
color: var(--ctms-text-main) !important;
background-color: var(--ctms-bg-muted) !important;
}
:deep(.el-menu-item.is-active) {
color: #fff !important;
background-color: var(--ctms-primary) !important;
box-shadow: 0 4px 12px rgba(47, 84, 235, 0.2);
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: -12px;
left: 8px;
top: 12px;
bottom: 12px;
width: 4px;
background-color: #fff;
border-radius: 0 4px 4px 0;
width: 3px;
background-color: var(--ctms-primary);
border-radius: 999px;
}
:deep(.el-sub-menu__title) {
color: rgba(255, 255, 255, 0.7) !important;
color: var(--ctms-text-regular) !important;
margin: 4px 12px;
border-radius: var(--ctms-radius);
border-radius: 12px;
height: 44px;
line-height: 44px;
}
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
color: #fff !important;
color: var(--ctms-text-main) !important;
}
:deep(.el-menu--inline) {
background-color: rgba(0, 0, 0, 0.2) !important;
background-color: transparent !important;
margin: 0 !important;
}
@@ -275,14 +312,15 @@ const onCommand = (cmd: string) => {
}
.layout-header {
background-color: #fff;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
box-shadow: none;
height: 64px !important;
z-index: 9;
border-bottom: 1px solid var(--ctms-border-color);
}
.header-right {
@@ -291,6 +329,21 @@ const onCommand = (cmd: string) => {
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;
@@ -301,23 +354,23 @@ const onCommand = (cmd: string) => {
display: flex;
align-items: center;
padding: 4px 12px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #eef2f6;
color: #334155;
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: #f1f5f9;
border-color: #e2e8f0;
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: 8px !important;
border-radius: 10px !important;
font-weight: 700;
flex-shrink: 0;
font-size: 14px;
@@ -339,7 +392,7 @@ const onCommand = (cmd: string) => {
}
.content-wrapper {
padding: 24px;
padding: 28px;
min-height: calc(100vh - 64px);
}