Files
X-Financial/src/components/layout/SidebarRail.vue

322 lines
6.5 KiB
Vue

<template>
<aside class="rail" aria-label="主导航">
<div class="rail-brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 36 36">
<path d="M19.8 4.5c5.7 1.1 9.9 5.7 10.5 11.6-2.8-.9-5.5-.7-7.9.6-2.8 1.5-4.5 4.3-5.2 8.2-4.4-2.8-6.5-6.5-6.3-11.1.2-4.2 3.5-7.8 8.9-9.3Z" />
<path d="M9 7.6c-3 3.5-4 7.3-2.9 11.2 1.2 4.2 4.6 7 10.1 8.5-2 1.8-4.6 2.6-7.6 2.3C5.1 26.7 3.5 23.1 3.7 19 4 14.4 5.7 10.6 9 7.6Z" />
</svg>
</div>
<strong class="brand-name">星海科技</strong>
<button class="brand-toggle" type="button" aria-label="打开 AI 助手" @click="emit('openChat')">
<i class="mdi mdi-chevron-double-left"></i>
</button>
</div>
<nav class="rail-nav" aria-label="功能导航">
<button
v-for="item in decoratedNavItems"
:key="item.id"
class="nav-btn"
:class="{ active: activeView === item.id }"
type="button"
@click="emit('navigate', item.id)"
>
<span class="nav-icon" v-html="item.icon"></span>
<span class="nav-label">{{ item.displayLabel }}</span>
<span v-if="item.badge" class="nav-badge">{{ item.badge }}</span>
</button>
</nav>
<button class="rail-user" type="button" aria-label="打开用户菜单">
<span class="user-avatar"></span>
<span class="user-copy">
<strong>张晓明</strong>
<span>财务管理员</span>
</span>
<i class="mdi mdi-chevron-down"></i>
</button>
</aside>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
navItems: { type: Array, required: true },
activeView: { type: String, required: true }
})
const emit = defineEmits(['navigate', 'openChat'])
const sidebarMeta = {
overview: { label: '总览' },
requests: { label: '差旅申请/报销' },
approval: { label: '审批中心', badge: '12' },
chat: { label: 'AI助手' },
policies: { label: '知识管理' },
audit: { label: '审计追踪' }
}
const decoratedNavItems = computed(() =>
props.navItems.map((item) => ({
...item,
displayLabel: sidebarMeta[item.id]?.label ?? item.label,
badge: sidebarMeta[item.id]?.badge
}))
)
</script>
<style scoped>
.rail {
position: sticky;
top: 0;
height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
background:
linear-gradient(180deg, rgba(255,255,255,.98), rgba(248,251,250,.96)),
#fff;
border-right: 1px solid #dbe4ee;
box-shadow: 1px 0 0 rgba(15,23,42,.02);
z-index: 20;
}
.rail-brand {
min-height: 86px;
display: grid;
grid-template-columns: 32px minmax(0, 1fr) 28px;
align-items: center;
gap: 10px;
padding: 22px 20px 18px;
}
.brand-mark {
width: 30px;
height: 30px;
display: grid;
place-items: center;
color: #07936f;
}
.brand-mark svg {
width: 30px;
height: 30px;
fill: currentColor;
}
.brand-name {
min-width: 0;
color: #0f172a;
font-size: 16px;
font-weight: 800;
letter-spacing: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.brand-toggle {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 0;
border-radius: 7px;
background: transparent;
color: #718096;
transition: background 180ms var(--ease), color 180ms var(--ease);
}
.brand-toggle:hover {
background: #eef7f4;
color: #07936f;
}
.rail-nav {
display: grid;
align-content: start;
gap: 14px;
padding: 16px 20px;
overflow-y: auto;
}
.nav-btn {
width: 100%;
min-height: 48px;
display: grid;
grid-template-columns: 28px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
padding: 0 12px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: #64748b;
text-align: left;
transition:
background 180ms var(--ease),
border-color 180ms var(--ease),
color 180ms var(--ease),
box-shadow 180ms var(--ease);
}
.nav-btn:hover {
background: rgba(16,185,129,.07);
color: #0f9f78;
}
.nav-btn.active {
background: linear-gradient(90deg, rgba(16,185,129,.16), rgba(16,185,129,.08));
border-color: rgba(16,185,129,.10);
color: #059669;
box-shadow: inset 3px 0 0 #10b981;
}
.nav-icon {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 7px;
color: currentColor;
}
.nav-btn :deep(svg) {
width: 19px;
height: 19px;
stroke: currentColor;
stroke-width: 2;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.nav-label {
min-width: 0;
color: currentColor;
font-size: 14px;
font-weight: 750;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-badge {
min-width: 34px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 8px;
border-radius: 999px;
background: #ff5b67;
color: #fff;
font-size: 12px;
font-weight: 800;
line-height: 1;
}
.rail-user {
min-width: 0;
min-height: 74px;
display: grid;
grid-template-columns: 38px minmax(0, 1fr) 22px;
align-items: center;
gap: 10px;
margin: 0;
padding: 16px 20px 18px;
border: 0;
border-top: 1px solid transparent;
background: transparent;
color: #64748b;
text-align: left;
transition: background 180ms var(--ease), border-color 180ms var(--ease);
}
.rail-user:hover {
border-top-color: #e2e8f0;
background: rgba(255,255,255,.72);
}
.user-avatar {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border: 2px solid #fff;
border-radius: 999px;
background: linear-gradient(135deg, #0f9f78, #65d6b4);
box-shadow: 0 6px 14px rgba(15,159,120,.18);
color: #fff;
font-size: 14px;
font-weight: 800;
}
.user-copy {
min-width: 0;
display: grid;
gap: 2px;
}
.user-copy strong {
color: #334155;
font-size: 14px;
font-weight: 750;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-copy span {
color: #64748b;
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rail-user .mdi {
justify-self: end;
color: #718096;
font-size: 13px;
}
@media (max-width: 980px) {
.rail {
position: relative;
height: auto;
}
}
@media (max-width: 760px) {
.rail {
border-right: 0;
border-bottom: 1px solid #dbe4ee;
}
.rail-brand {
min-height: 68px;
padding: 16px;
}
.rail-nav {
display: flex;
gap: 10px;
padding: 8px 16px 16px;
overflow-x: auto;
}
.nav-btn {
min-width: 148px;
}
.rail-user {
display: none;
}
}
</style>