@@ -96,19 +96,17 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
|
|||||||
|
|
||||||
ClinicalLayout
|
ClinicalLayout
|
||||||
├─ 工作台
|
├─ 工作台
|
||||||
│ ├─ /workbench/home 首页总览
|
│ ├─ /workbench/home 首页
|
||||||
│ ├─ /workbench/signing 签署工作台
|
│ └─ /workbench/signing 签署工作台
|
||||||
│ ├─ /management/documents 文档库管理
|
|
||||||
│ └─ /management/reports 报表分析
|
|
||||||
└─ 管理
|
└─ 管理
|
||||||
|
├─ /management/documents 文档库管理
|
||||||
|
├─ /management/reports 报表分析
|
||||||
├─ /management/users 用户与权限
|
├─ /management/users 用户与权限
|
||||||
├─ /management/document-permissions 文档权限
|
├─ /management/document-permissions 文档权限
|
||||||
└─ /management/settings 系统设置
|
└─ /management/settings 系统设置
|
||||||
```
|
```
|
||||||
|
|
||||||
菜单分组按原型呈现,与路由 path 前缀不完全对应:文档库管理和报表分析归入侧边栏「工作台」分栏,但路径仍保留在 `/management` 下。
|
布局拆分为 `Menu`、`Header` 和 `Container` 三个公共组件,页面内容由各自的 View 负责。
|
||||||
|
|
||||||
布局拆分为 `Menu`、`Header` 和 `Container` 三个公共组件,页面内容由各自的 View 负责;侧边栏与顶栏共用 `components/layout/AppIcon.vue` 提供图标。
|
|
||||||
|
|
||||||
页面目录按业务域组织:`auth` 保留登录页;工作台页面位于 `views/workbench`;管理页面位于 `views/management`。每个具体页面目录都有 `index.vue` 入口和 `components` 目录,用于继续拆分当前页面组件。
|
页面目录按业务域组织:`auth` 保留登录页;工作台页面位于 `views/workbench`;管理页面位于 `views/management`。每个具体页面目录都有 `index.vue` 入口和 `components` 目录,用于继续拆分当前页面组件。
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 布局层图标组件。
|
|
||||||
* 图标节点与新版原型保持一致,采用 lucide 图标数据(24 视图框、描边风格),
|
|
||||||
* 避免引入额外依赖,同时保证侧边栏与顶栏的图标细节可以逐像素对齐原型。
|
|
||||||
*/
|
|
||||||
type IconNode = readonly [string, Record<string, string | number>]
|
|
||||||
|
|
||||||
const icons: Record<string, readonly IconNode[]> = {
|
|
||||||
'layout-dashboard': [
|
|
||||||
['rect', { width: 7, height: 9, x: 3, y: 3, rx: 1 }],
|
|
||||||
['rect', { width: 7, height: 5, x: 14, y: 3, rx: 1 }],
|
|
||||||
['rect', { width: 7, height: 9, x: 14, y: 12, rx: 1 }],
|
|
||||||
['rect', { width: 7, height: 5, x: 3, y: 16, rx: 1 }],
|
|
||||||
],
|
|
||||||
'file-pen-line': [
|
|
||||||
[
|
|
||||||
'path',
|
|
||||||
{
|
|
||||||
d: 'M14.364 13.634a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506l4.013-4.009a1 1 0 0 0-3.004-3.004z',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
['path', { d: 'M14.487 7.858A1 1 0 0 1 14 7V2' }],
|
|
||||||
[
|
|
||||||
'path',
|
|
||||||
{
|
|
||||||
d: 'M20 19.645V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l2.516 2.516',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
['path', { d: 'M8 18h1' }],
|
|
||||||
],
|
|
||||||
'clipboard-check': [
|
|
||||||
['rect', { width: 8, height: 4, x: 8, y: 2, rx: 1, ry: 1 }],
|
|
||||||
['path', { d: 'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2' }],
|
|
||||||
['path', { d: 'm9 14 2 2 4-4' }],
|
|
||||||
],
|
|
||||||
activity: [
|
|
||||||
[
|
|
||||||
'path',
|
|
||||||
{
|
|
||||||
d: 'M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'users-round': [
|
|
||||||
['path', { d: 'M18 21a8 8 0 0 0-16 0' }],
|
|
||||||
['circle', { cx: 10, cy: 8, r: 5 }],
|
|
||||||
['path', { d: 'M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3' }],
|
|
||||||
],
|
|
||||||
'lock-keyhole': [
|
|
||||||
['circle', { cx: 12, cy: 16, r: 1 }],
|
|
||||||
['rect', { x: 3, y: 10, width: 18, height: 12, rx: 2 }],
|
|
||||||
['path', { d: 'M7 10V7a5 5 0 0 1 10 0v3' }],
|
|
||||||
],
|
|
||||||
settings: [
|
|
||||||
[
|
|
||||||
'path',
|
|
||||||
{
|
|
||||||
d: 'M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
['circle', { cx: 12, cy: 12, r: 3 }],
|
|
||||||
],
|
|
||||||
stethoscope: [
|
|
||||||
['path', { d: 'M11 2v2' }],
|
|
||||||
['path', { d: 'M5 2v2' }],
|
|
||||||
['path', { d: 'M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1' }],
|
|
||||||
['path', { d: 'M8 15a6 6 0 0 0 12 0v-3' }],
|
|
||||||
['circle', { cx: 20, cy: 10, r: 2 }],
|
|
||||||
],
|
|
||||||
ellipsis: [
|
|
||||||
['circle', { cx: 12, cy: 12, r: 1 }],
|
|
||||||
['circle', { cx: 19, cy: 12, r: 1 }],
|
|
||||||
['circle', { cx: 5, cy: 12, r: 1 }],
|
|
||||||
],
|
|
||||||
bell: [
|
|
||||||
['path', { d: 'M10.268 21a2 2 0 0 0 3.464 0' }],
|
|
||||||
[
|
|
||||||
'path',
|
|
||||||
{
|
|
||||||
d: 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'building-2': [
|
|
||||||
['path', { d: 'M10 12h4' }],
|
|
||||||
['path', { d: 'M10 8h4' }],
|
|
||||||
['path', { d: 'M14 21v-3a2 2 0 0 0-4 0v3' }],
|
|
||||||
['path', { d: 'M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2' }],
|
|
||||||
['path', { d: 'M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16' }],
|
|
||||||
],
|
|
||||||
'chevron-down': [['path', { d: 'm6 9 6 6 6-6' }]],
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{ name: string }>()
|
|
||||||
|
|
||||||
const nodes = computed(() => icons[props.name] ?? [])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
class="app-icon"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.85"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
aria-hidden="true"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<component :is="node[0]" v-for="(node, index) in nodes" :key="index" v-bind="node[1]" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app-icon {
|
|
||||||
display: block;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -8,13 +8,13 @@
|
|||||||
.clinical-container {
|
.clinical-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 18px 30px;
|
padding: 18px 20px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.clinical-container {
|
.clinical-container {
|
||||||
padding: 16px 20px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
import AppIcon from '@/components/layout/AppIcon.vue'
|
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -23,17 +21,6 @@ const campusPlaceholder = computed(() => {
|
|||||||
return appStore.campusLoadFailed ? '院区加载失败' : '暂无可用院区'
|
return appStore.campusLoadFailed ? '院区加载失败' : '暂无可用院区'
|
||||||
})
|
})
|
||||||
|
|
||||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
|
||||||
const todayText = computed(() => {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 ${weekdays[now.getDay()]}`
|
|
||||||
})
|
|
||||||
|
|
||||||
function handleMessages() {
|
|
||||||
ElMessage.info('暂无新消息')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void appStore.loadCampuses()
|
void appStore.loadCampuses()
|
||||||
})
|
})
|
||||||
@@ -42,34 +29,26 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<header class="clinical-header">
|
<header class="clinical-header">
|
||||||
<div class="header-page">
|
<div class="header-page">
|
||||||
<p class="header-subtitle">{{ pageGroup }} · 医签通平台</p>
|
<span class="header-title">{{ pageTitle }}</span>
|
||||||
<h1 class="header-title">{{ pageTitle }}</h1>
|
<span class="header-breadcrumb">{{ pageGroup }} / {{ pageTitle }}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header-actions">
|
|
||||||
<label class="campus-scope">
|
|
||||||
<AppIcon name="building-2" class="scope-icon" />
|
|
||||||
<select
|
|
||||||
v-model="selectedCampusId"
|
|
||||||
:disabled="appStore.campusLoading || !appStore.campusRecords.length"
|
|
||||||
aria-label="选择院区"
|
|
||||||
>
|
|
||||||
<option v-if="!appStore.campusRecords.length" value="">
|
|
||||||
{{ campusPlaceholder }}
|
|
||||||
</option>
|
|
||||||
<option v-for="campus in appStore.campusRecords" :key="campus.id" :value="campus.id">
|
|
||||||
{{ campus.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<AppIcon name="chevron-down" class="scope-arrow" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button class="icon-button" type="button" aria-label="查看消息" @click="handleMessages">
|
|
||||||
<AppIcon name="bell" class="icon-button-icon" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="header-date">{{ todayText }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="header-spacer" />
|
||||||
|
<label class="campus-selector">
|
||||||
|
<span class="campus-label">院区</span>
|
||||||
|
<select
|
||||||
|
v-model="selectedCampusId"
|
||||||
|
:disabled="appStore.campusLoading || !appStore.campusRecords.length"
|
||||||
|
aria-label="选择院区"
|
||||||
|
>
|
||||||
|
<option v-if="!appStore.campusRecords.length" value="">
|
||||||
|
{{ campusPlaceholder }}
|
||||||
|
</option>
|
||||||
|
<option v-for="campus in appStore.campusRecords" :key="campus.id" :value="campus.id">
|
||||||
|
{{ campus.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span class="environment-badge">{{ appStore.environment }}</span>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,109 +56,69 @@ onMounted(() => {
|
|||||||
.clinical-header {
|
.clinical-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
gap: 14px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
padding: 0 30px;
|
padding: 0 20px;
|
||||||
background: rgb(255 255 255 / 93%);
|
background: #fff;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-page {
|
.header-page {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: baseline;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-subtitle {
|
.header-title {
|
||||||
margin: 0;
|
flex-shrink: 0;
|
||||||
color: #82969f;
|
color: var(--ink);
|
||||||
font-size: 10px;
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-breadcrumb {
|
||||||
margin: 3px 0 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--ink);
|
color: var(--mut);
|
||||||
font-size: 17px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campus-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 14px;
|
|
||||||
align-items: center;
|
|
||||||
color: #6e828b;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.campus-scope {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 32px;
|
|
||||||
padding: 0 9px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-icon {
|
.campus-label {
|
||||||
width: 14px;
|
color: var(--mut);
|
||||||
height: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope-arrow {
|
.campus-selector select {
|
||||||
width: 11px;
|
|
||||||
height: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.campus-scope select {
|
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 150px;
|
padding: 4px 8px;
|
||||||
padding: 0;
|
color: var(--brand-d);
|
||||||
overflow: hidden;
|
font-size: 12.5px;
|
||||||
color: #4d6873;
|
font-weight: 700;
|
||||||
font-size: 11px;
|
background: var(--brand-l);
|
||||||
text-overflow: ellipsis;
|
border: 1px solid var(--brand);
|
||||||
background: transparent;
|
border-radius: 6px;
|
||||||
border: 0;
|
}
|
||||||
outline: 0;
|
|
||||||
appearance: none;
|
.environment-badge {
|
||||||
}
|
flex-shrink: 0;
|
||||||
|
padding: 3px 8px;
|
||||||
.campus-scope select:disabled {
|
color: var(--warn);
|
||||||
color: #9aabb3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
color: #506c77;
|
|
||||||
place-items: center;
|
|
||||||
background: #edf4f6;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 9px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button:hover {
|
|
||||||
color: #08788d;
|
|
||||||
background: #def0f3;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button-icon {
|
|
||||||
width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-date {
|
|
||||||
color: #6e828b;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
background: var(--warn-l);
|
||||||
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,56 +1,43 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { RouterLink, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import AppIcon from '@/components/layout/AppIcon.vue'
|
|
||||||
import { useAppStore } from '@/stores/app'
|
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
path: string
|
path: string
|
||||||
label: string
|
label: string
|
||||||
icon: string
|
|
||||||
permission?: string
|
permission?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuGroup {
|
interface MenuGroup {
|
||||||
label: string
|
label: string
|
||||||
ariaLabel: string
|
|
||||||
items: MenuItem[]
|
items: MenuItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginStore = useLoginStore()
|
const loginStore = useLoginStore()
|
||||||
const appStore = useAppStore()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const menuGroups: MenuGroup[] = [
|
const menuGroups: MenuGroup[] = [
|
||||||
{
|
{
|
||||||
label: '工作台',
|
label: '工作台',
|
||||||
ariaLabel: '主要导航',
|
|
||||||
items: [
|
items: [
|
||||||
{ path: '/workbench/home', label: '首页总览', icon: 'layout-dashboard' },
|
{ path: '/workbench/home', label: '首页' },
|
||||||
{ path: '/workbench/signing', label: '签署工作台', icon: 'file-pen-line' },
|
{ path: '/workbench/signing', label: '签署工作台' },
|
||||||
{ path: '/management/documents', label: '文档库管理', icon: 'clipboard-check' },
|
|
||||||
{ path: '/management/reports', label: '报表分析', icon: 'activity' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '管理',
|
label: '管理',
|
||||||
ariaLabel: '管理导航',
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{ path: '/management/documents', label: '文档库管理' },
|
||||||
path: '/management/users',
|
{ path: '/management/reports', label: '报表分析' },
|
||||||
label: '用户与权限',
|
{ path: '/management/users', label: '用户与权限', permission: 'system:user:query' },
|
||||||
icon: 'users-round',
|
|
||||||
permission: 'system:user:query',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/management/document-permissions',
|
path: '/management/document-permissions',
|
||||||
label: '文档权限',
|
label: '文档权限',
|
||||||
icon: 'lock-keyhole',
|
|
||||||
permission: 'sign:template:permission:query',
|
permission: 'sign:template:permission:query',
|
||||||
},
|
},
|
||||||
{ path: '/management/settings', label: '系统设置', icon: 'settings' },
|
{ path: '/management/settings', label: '系统设置' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -66,28 +53,7 @@ const visibleMenuGroups = computed(() =>
|
|||||||
.filter((group) => group.items.length),
|
.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() {
|
async function handleLogout() {
|
||||||
closeUserMenu()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginStore.logout()
|
await loginStore.logout()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -96,70 +62,57 @@ async function handleLogout() {
|
|||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('click', handleDocumentClick)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener('click', handleDocumentClick)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside class="clinical-menu">
|
<aside class="clinical-menu">
|
||||||
<div class="brand-lockup">
|
<div class="menu-brand">
|
||||||
<span class="brand-mark">
|
<div class="menu-brand-icon" aria-hidden="true">
|
||||||
<AppIcon name="stethoscope" class="brand-mark-icon" />
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
</span>
|
<path
|
||||||
<div class="brand-copy">
|
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">
|
||||||
<strong>医签通</strong>
|
<strong>医签通</strong>
|
||||||
<small>MEDISIGN V2.0</small>
|
<span>MEDISIGN</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-scroll">
|
<nav class="menu-nav" aria-label="主导航">
|
||||||
<div v-for="group in visibleMenuGroups" :key="group.label" class="menu-group">
|
<div v-for="group in visibleMenuGroups" :key="group.label" class="menu-group">
|
||||||
<p class="nav-caption">{{ group.label }}</p>
|
<div class="menu-group-title">{{ group.label }}</div>
|
||||||
<nav class="menu-nav" :aria-label="group.ariaLabel">
|
<RouterLink
|
||||||
<RouterLink
|
v-for="item in group.items"
|
||||||
v-for="item in group.items"
|
:key="item.path"
|
||||||
:key="item.path"
|
:to="item.path"
|
||||||
:to="item.path"
|
class="menu-item"
|
||||||
class="nav-item"
|
active-class="is-active"
|
||||||
active-class="is-active"
|
>
|
||||||
>
|
<span class="menu-dot" aria-hidden="true" />
|
||||||
<AppIcon :name="item.icon" class="nav-icon" />
|
<span>{{ item.label }}</span>
|
||||||
<span class="nav-label">{{ item.label }}</span>
|
</RouterLink>
|
||||||
</RouterLink>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<div class="connection">
|
<div class="menu-profile">
|
||||||
<i aria-hidden="true" />
|
<div class="menu-profile-avatar">{{ loginStore.userName.slice(0, 1) }}</div>
|
||||||
<span>{{ appStore.environment }}</span>
|
<div class="menu-profile-copy">
|
||||||
</div>
|
<strong>{{ loginStore.userName }}</strong>
|
||||||
|
<span>{{ loginStore.department }}</span>
|
||||||
<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>
|
</div>
|
||||||
|
<button class="menu-logout" type="button" @click="handleLogout">退出</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -170,237 +123,152 @@ onBeforeUnmount(() => {
|
|||||||
flex: 0 0 var(--sidebar-width);
|
flex: 0 0 var(--sidebar-width);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
padding: 21px 13px 14px;
|
color: #c8dde6;
|
||||||
color: #d8edf3;
|
background: var(--side);
|
||||||
background: linear-gradient(#0b455a 0%, #072e3e 100%);
|
|
||||||
box-shadow: 8px 0 32px rgb(5 37 49 / 7%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-lockup {
|
.menu-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 11px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px 20px;
|
padding: 18px 16px 16px;
|
||||||
border-bottom: 1px solid rgb(255 255 255 / 9%);
|
border-bottom: 1px solid rgb(255 255 255 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.menu-brand-icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 40px;
|
width: 34px;
|
||||||
height: 40px;
|
height: 34px;
|
||||||
color: #fff;
|
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: linear-gradient(145deg, #0f7590, #18a6a6);
|
border-radius: 9px;
|
||||||
border: 1px solid rgb(255 255 255 / 20%);
|
background: linear-gradient(135deg, #18a0c0, var(--brand));
|
||||||
border-radius: 11px;
|
|
||||||
box-shadow: 0 8px 20px rgb(0 21 31 / 33%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark-icon {
|
.menu-brand-copy strong,
|
||||||
width: 22px;
|
.menu-brand-copy span {
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-copy {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-copy strong {
|
|
||||||
display: block;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-brand-copy strong {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-copy small {
|
.menu-brand-copy span {
|
||||||
display: block;
|
color: #8fb3c2;
|
||||||
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;
|
font-size: 10px;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-nav {
|
.menu-nav {
|
||||||
display: grid;
|
flex: 1;
|
||||||
gap: 4px;
|
padding: 12px 10px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.menu-group-title {
|
||||||
position: relative;
|
padding: 10px 10px 6px;
|
||||||
|
color: #7ba2b3;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 11px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 42px;
|
margin-bottom: 2px;
|
||||||
padding: 0 12px;
|
padding: 10px 12px;
|
||||||
color: #bcd3db;
|
color: #c8dde6;
|
||||||
font-size: 14px;
|
font-size: 13.5px;
|
||||||
text-align: left;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
transition: all 0.22s;
|
transition:
|
||||||
|
color 0.15s ease,
|
||||||
|
background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.menu-item:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: rgb(255 255 255 / 5%);
|
background: rgb(255 255 255 / 7%);
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.is-active {
|
.menu-item.is-active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(90deg, #137f98, #0f6a82);
|
font-weight: 700;
|
||||||
box-shadow: 0 8px 20px rgb(0 20 30 / 25%);
|
background: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.is-active::before {
|
.menu-dot {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
gap: 7px;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 0;
|
|
||||||
margin: auto 0 12px 8px;
|
|
||||||
color: #79a8b5;
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection i {
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: #45d4a5;
|
background: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 0 4px rgb(69 212 165 / 11%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection span {
|
.menu-item.is-active .menu-dot {
|
||||||
overflow: hidden;
|
background: #5ee0a8;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-user {
|
.menu-profile {
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-card {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 9px;
|
flex-shrink: 0;
|
||||||
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
padding: 14px 16px;
|
||||||
padding: 10px 9px;
|
border-top: 1px solid rgb(255 255 255 / 8%);
|
||||||
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 {
|
.menu-profile-avatar {
|
||||||
background: rgb(255 255 255 / 6%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #18a5a6;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
background: #18a0c0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-copy {
|
.menu-profile-copy {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-copy strong {
|
.menu-profile-copy strong,
|
||||||
display: block;
|
.menu-profile-copy span {
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-copy small {
|
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-top: 2px;
|
|
||||||
color: #8fb7c4;
|
|
||||||
font-size: 9px;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-more {
|
.menu-profile-copy strong {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-profile-copy span {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #8fb3c2;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-logout {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #78a0ac;
|
padding: 0;
|
||||||
}
|
color: #8fb3c2;
|
||||||
|
font-size: 11px;
|
||||||
.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;
|
background: transparent;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-pop button:hover {
|
.menu-logout:hover {
|
||||||
background: #eef6f8;
|
color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const router = createRouter({
|
|||||||
path: 'home',
|
path: 'home',
|
||||||
name: 'workbench-home',
|
name: 'workbench-home',
|
||||||
component: DashboardView,
|
component: DashboardView,
|
||||||
meta: { title: '首页总览', group: '工作台' },
|
meta: { title: '首页', group: '工作台' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signing',
|
path: 'signing',
|
||||||
|
|||||||
@@ -26,6 +26,6 @@
|
|||||||
--font-sans:
|
--font-sans:
|
||||||
'Microsoft YaHei', 'PingFang SC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
'Microsoft YaHei', 'PingFang SC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
--sidebar-width: 220px;
|
--sidebar-width: 200px;
|
||||||
--header-height: 72px;
|
--header-height: 54px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user