feat(布局): 重构侧边栏、顶栏与页面布局样式
This commit is contained in:
@@ -1,43 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
import AppIcon from '@/components/layout/AppIcon.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useLoginStore } from '@/stores/login'
|
||||
|
||||
interface MenuItem {
|
||||
path: string
|
||||
label: string
|
||||
icon: string
|
||||
permission?: string
|
||||
}
|
||||
|
||||
interface MenuGroup {
|
||||
label: string
|
||||
ariaLabel: string
|
||||
items: MenuItem[]
|
||||
}
|
||||
|
||||
const loginStore = useLoginStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
label: '工作台',
|
||||
ariaLabel: '主要导航',
|
||||
items: [
|
||||
{ path: '/workbench/home', label: '首页' },
|
||||
{ path: '/workbench/signing', label: '签署工作台' },
|
||||
{ path: '/workbench/home', label: '首页总览', icon: 'layout-dashboard' },
|
||||
{ path: '/workbench/signing', label: '签署工作台', icon: 'file-pen-line' },
|
||||
{ path: '/management/documents', label: '文档库管理', icon: 'clipboard-check' },
|
||||
{ path: '/management/reports', label: '报表分析', icon: 'activity' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '管理',
|
||||
ariaLabel: '管理导航',
|
||||
items: [
|
||||
{ path: '/management/documents', label: '文档库管理' },
|
||||
{ path: '/management/reports', label: '报表分析' },
|
||||
{ path: '/management/users', label: '用户与权限', permission: 'system:user:query' },
|
||||
{
|
||||
path: '/management/users',
|
||||
label: '用户与权限',
|
||||
icon: 'users-round',
|
||||
permission: 'system:user:query',
|
||||
},
|
||||
{
|
||||
path: '/management/document-permissions',
|
||||
label: '文档权限',
|
||||
icon: 'lock-keyhole',
|
||||
permission: 'sign:template:permission:query',
|
||||
},
|
||||
{ path: '/management/settings', label: '系统设置' },
|
||||
{ path: '/management/settings', label: '系统设置', icon: 'settings' },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -53,7 +66,28 @@ const visibleMenuGroups = computed(() =>
|
||||
.filter((group) => group.items.length),
|
||||
)
|
||||
|
||||
const avatarText = computed(() => loginStore.userName.slice(0, 1))
|
||||
const userMenuOpen = ref(false)
|
||||
|
||||
function toggleUserMenu() {
|
||||
userMenuOpen.value = !userMenuOpen.value
|
||||
}
|
||||
|
||||
function closeUserMenu() {
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement | null
|
||||
|
||||
if (target && !target.closest('.menu-user')) {
|
||||
closeUserMenu()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
closeUserMenu()
|
||||
|
||||
try {
|
||||
await loginStore.logout()
|
||||
} catch {
|
||||
@@ -62,57 +96,70 @@ async function handleLogout() {
|
||||
await router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="clinical-menu">
|
||||
<div class="menu-brand">
|
||||
<div class="menu-brand-icon" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M14 3v5h5M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z"
|
||||
stroke="#fff"
|
||||
stroke-width="1.7"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="m9 13.5 2.5 2.5 4-4.5"
|
||||
stroke="#fff"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="menu-brand-copy">
|
||||
<div class="brand-lockup">
|
||||
<span class="brand-mark">
|
||||
<AppIcon name="stethoscope" class="brand-mark-icon" />
|
||||
</span>
|
||||
<div class="brand-copy">
|
||||
<strong>医签通</strong>
|
||||
<span>MEDISIGN</span>
|
||||
<small>MEDISIGN V2.0</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="menu-nav" aria-label="主导航">
|
||||
<div class="menu-scroll">
|
||||
<div v-for="group in visibleMenuGroups" :key="group.label" class="menu-group">
|
||||
<div class="menu-group-title">{{ group.label }}</div>
|
||||
<RouterLink
|
||||
v-for="item in group.items"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="menu-item"
|
||||
active-class="is-active"
|
||||
>
|
||||
<span class="menu-dot" aria-hidden="true" />
|
||||
<span>{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
<p class="nav-caption">{{ group.label }}</p>
|
||||
<nav class="menu-nav" :aria-label="group.ariaLabel">
|
||||
<RouterLink
|
||||
v-for="item in group.items"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="nav-item"
|
||||
active-class="is-active"
|
||||
>
|
||||
<AppIcon :name="item.icon" class="nav-icon" />
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="menu-profile">
|
||||
<div class="menu-profile-avatar">{{ loginStore.userName.slice(0, 1) }}</div>
|
||||
<div class="menu-profile-copy">
|
||||
<strong>{{ loginStore.userName }}</strong>
|
||||
<span>{{ loginStore.department }}</span>
|
||||
<div class="connection">
|
||||
<i aria-hidden="true" />
|
||||
<span>{{ appStore.environment }}</span>
|
||||
</div>
|
||||
|
||||
<div class="menu-user">
|
||||
<button
|
||||
class="user-card"
|
||||
type="button"
|
||||
:aria-expanded="userMenuOpen"
|
||||
aria-haspopup="menu"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
<span class="user-avatar">{{ avatarText }}</span>
|
||||
<div class="user-copy">
|
||||
<strong>{{ loginStore.userName }}</strong>
|
||||
<small>{{ loginStore.department }}</small>
|
||||
</div>
|
||||
<AppIcon name="ellipsis" class="user-more" />
|
||||
</button>
|
||||
|
||||
<div v-if="userMenuOpen" class="user-pop" role="menu">
|
||||
<button type="button" role="menuitem" @click="handleLogout">退出登录</button>
|
||||
</div>
|
||||
<button class="menu-logout" type="button" @click="handleLogout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -123,152 +170,237 @@ async function handleLogout() {
|
||||
flex: 0 0 var(--sidebar-width);
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width);
|
||||
color: #c8dde6;
|
||||
background: var(--side);
|
||||
padding: 21px 13px 14px;
|
||||
color: #d8edf3;
|
||||
background: linear-gradient(#0b455a 0%, #072e3e 100%);
|
||||
box-shadow: 8px 0 32px rgb(5 37 49 / 7%);
|
||||
}
|
||||
|
||||
.menu-brand {
|
||||
.brand-lockup {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
gap: 11px;
|
||||
align-items: center;
|
||||
padding: 18px 16px 16px;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 8%);
|
||||
padding: 0 8px 20px;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 9%);
|
||||
}
|
||||
|
||||
.menu-brand-icon {
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #fff;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg, #18a0c0, var(--brand));
|
||||
background: linear-gradient(145deg, #0f7590, #18a6a6);
|
||||
border: 1px solid rgb(255 255 255 / 20%);
|
||||
border-radius: 11px;
|
||||
box-shadow: 0 8px 20px rgb(0 21 31 / 33%);
|
||||
}
|
||||
|
||||
.menu-brand-copy strong,
|
||||
.menu-brand-copy span {
|
||||
display: block;
|
||||
.brand-mark-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.menu-brand-copy strong {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.menu-brand-copy span {
|
||||
color: #8fb3c2;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.menu-nav {
|
||||
flex: 1;
|
||||
padding: 12px 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-group-title {
|
||||
padding: 10px 10px 6px;
|
||||
color: #7ba2b3;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
padding: 10px 12px;
|
||||
color: #c8dde6;
|
||||
font-size: 13.5px;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: #fff;
|
||||
background: rgb(255 255 255 / 7%);
|
||||
}
|
||||
|
||||
.menu-item.is-active {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.menu-dot {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.menu-item.is-active .menu-dot {
|
||||
background: #5ee0a8;
|
||||
}
|
||||
|
||||
.menu-profile {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
.menu-profile-avatar {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #18a0c0;
|
||||
}
|
||||
|
||||
.menu-profile-copy {
|
||||
.brand-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.menu-profile-copy strong,
|
||||
.menu-profile-copy span {
|
||||
.brand-copy strong {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.brand-copy small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: #8fb7c4;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.menu-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-caption {
|
||||
margin: 0;
|
||||
padding: 17px 12px 6px;
|
||||
color: #759ba8;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.menu-nav {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
align-items: center;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
color: #bcd3db;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all 0.22s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #fff;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.nav-item.is-active {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #137f98, #0f6a82);
|
||||
box-shadow: 0 8px 20px rgb(0 20 30 / 25%);
|
||||
}
|
||||
|
||||
.nav-item.is-active::before {
|
||||
position: absolute;
|
||||
left: -13px;
|
||||
width: 3px;
|
||||
height: 22px;
|
||||
background: #65d9d0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-profile-copy strong {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-profile-copy span {
|
||||
margin-top: 2px;
|
||||
color: #8fb3c2;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.menu-logout {
|
||||
.connection {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
padding: 0;
|
||||
color: #8fb3c2;
|
||||
font-size: 11px;
|
||||
background: transparent;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin: auto 0 12px 8px;
|
||||
color: #79a8b5;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.menu-logout:hover {
|
||||
.connection i {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #45d4a5;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px rgb(69 212 165 / 11%);
|
||||
}
|
||||
|
||||
.connection span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-user {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px 9px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
background: rgb(255 255 255 / 3%);
|
||||
border: 1px solid rgb(255 255 255 / 7%);
|
||||
border-radius: 11px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
background: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
place-items: center;
|
||||
background: #18a5a6;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-copy strong {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.user-copy small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin-top: 2px;
|
||||
color: #8fb7c4;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.1em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-more {
|
||||
flex-shrink: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: auto;
|
||||
color: #78a0ac;
|
||||
}
|
||||
|
||||
.user-pop {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 0;
|
||||
padding: 6px;
|
||||
background: #fff;
|
||||
border-radius: 11px;
|
||||
box-shadow: 0 18px 45px rgb(3 25 34 / 45%);
|
||||
}
|
||||
|
||||
.user-pop button {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
color: #25505f;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.user-pop button:hover {
|
||||
background: #eef6f8;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user