You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

560 lines
16 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="admin-layout">
<header class="admin-header">
<!-- 管理端显示 Logo / 医院端显示医院名 -->
<div class="logo-bar">
<span v-if="!isHospitalMode" class="logo-text admin"> 康策CSM系统管理平台<span class="version">v1.0</span> </span>
<span v-else class="logo-text hospital">{{ userStore.userInfo?.hospitalName || "医院端" }}</span>
</div>
<div class="header-middle">
<!-- 顶部水平菜单:管理端 / 医院端共用,按当前顶层路由动态生成 -->
<el-menu class="header-menu" mode="horizontal" :default-active="$route.path" :ellipsis="false" router>
<el-menu-item v-for="item in sideRoutes" :key="item.path" :index="item.path"
@mouseenter="hoveredPath = item.path" @mouseleave="hoveredPath = ''">
<!-- 管理端 / 医院端:统一使用 SVG 图标(正常/激活两种状态) -->
<img v-if="item.iconUrl" :src="isMenuHoveredOrActive(item.path)
? item.iconUrlActive || item.iconUrl
: item.iconUrl
" :alt="item.title" class="nav-icon" />
<template #title>
<span>{{ item.title }}</span>
</template>
</el-menu-item>
</el-menu>
<!-- 医院端:顶部菜单旁的快速提交按钮 -->
<div v-if="isHospitalMode" class="header-quick-submit">
<el-button type="primary" class="quick-submit-btn" @click="orderDialogStore.open()">
<img src="@/assets/image/hospital/quick.svg" class="quick-icon" alt="" />
<span>快速提交</span>
</el-button>
</div>
</div>
<div class="header-right">
<el-dropdown trigger="hover" @command="handleCommand">
<div class="user-trigger">
<!-- <SvgIcon name="bell-filled" :size="20" color="#fff" /> -->
<span class="username">{{
userStore.userInfo?.name ||
userStore.userInfo?.userName ||
"管理员"
}}</span>
<el-avatar :size="40" :src="avatarSrc"></el-avatar>
<!-- <el-icon>
<ArrowDown />
</el-icon> -->
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="theme">更换主题</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
<main class="admin-content">
<slot />
</main>
<!-- 更换主题弹窗 -->
<el-dialog v-model="themeDialogVisible" class="theme-dialog" :close-on-click-modal="false" align-center
destroy-on-close>
<div class="theme-body">
<div class="theme-header">
<span class="theme-title">更换主题</span>
<img src="@/assets/image/close-btn.svg" alt="" class="close-btn" @click="themeDialogVisible = false">
</div>
<div class="theme-grid">
<div v-for="t in presetThemes" :key="t.value" class="theme-card"
:class="{ active: themeStore.themeValue === t.value }" :style="{
'--border-color': t.borderColor
}" @click="handlePresetTheme(t.value)">
<div class="theme-preview" :style="{ background: t.preview, boxShadow: t.boxShadow }"></div>
<div class="theme-name">{{ t.name }}</div>
<div class="theme-desc">{{ t.desc }}</div>
</div>
</div>
</div>
</el-dialog>
<!-- 全局工单表单弹窗(医院端快速提交 / 列表编辑统一使用) -->
<OrderFormDialog v-model="orderDialogStore.visible" :order-id="orderDialogStore.orderId"
@success="onOrderDialogSuccess" />
<!-- 修改密码弹窗(管理端 / 医院端共用,根据 role 走对应接口) -->
<PasswordDialog v-model="passwordDialogVisible" :role="passwordRole" />
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from "element-plus";
import { ArrowDown } from "@element-plus/icons-vue";
import SvgIcon from "@/components/SvgIcon/index.vue";
import { useUserStore } from "@/stores/api/user";
import { useThemeStore } from "@/stores/theme";
import { useOrderDialogStore } from "@/stores/orderDialog";
import OrderFormDialog from "@/views/hospital/orders/OrderFormDialog.vue";
import PasswordDialog from "@/components/PasswordDialog.vue";
// 顶部菜单图标(直接以静态资源方式 import绕开 SvgIcon 的 fill 干扰)
// 管理端
import fuwuGd from "@/assets/image/router/fuwu-gd.svg";
import fuwuGdActive from "@/assets/image/router/fuwu-gd-active.svg";
import fuwuBg from "@/assets/image/router/fuwu-bg.svg";
import fuwuBgActive from "@/assets/image/router/fuwu-bg-active.svg";
import yiyuanXx from "@/assets/image/router/yiyuan-xx.svg";
import yiyuanXxActive from "@/assets/image/router/yiyuan-xx-active.svg";
import kehuFw from "@/assets/image/router/kehu-fw.svg";
import kehuFwActive from "@/assets/image/router/kehu-fw-active.svg";
import kehuJljf from "@/assets/image/router/kehu-jljf.svg";
import kehuJljfActive from "@/assets/image/router/kehu-jljf-active.svg";
import zhanghaoGl from "@/assets/image/router/zhanghao-gl.svg";
import zhanghaoGlActive from "@/assets/image/router/zhanghao-gl-active.svg";
// 医院端
import yyGzt from "@/assets/image/router/yy-gzt.svg";
import yyGztActive from "@/assets/image/router/yy-gzt-active.svg";
import yyGd from "@/assets/image/router/yy-gd.svg";
import yyGdActive from "@/assets/image/router/yy-gd-active.svg";
import yyJf from "@/assets/image/router/yy-jf.svg";
import yyJfActive from "@/assets/image/router/yy-jf-active.svg";
// 头像(按性别)
import avatarMale from "@/assets/image/avatar-male.svg";
import avatarFemale from "@/assets/image/avatar-female.svg";
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const themeStore = useThemeStore();
const orderDialogStore = useOrderDialogStore();
// 当前是否为医院端(用于切换头部布局样式)
const isHospitalMode = computed(() => route.path.startsWith("/hospital"));
// 头部快速提交 / 列表编辑成功后通知当前路由对应的列表页刷新
const onOrderDialogSuccess = () => {
// 触发全局事件,由 list.vue / detail.vue 监听
window.dispatchEvent(new CustomEvent("hospital-order-saved"));
};
// 主题弹窗显隐
const themeDialogVisible = ref(false);
// 修改密码弹窗显隐
const passwordDialogVisible = ref(false);
// 修改密码:根据当前所在端选择对应接口
const passwordRole = computed(() => (isHospitalMode.value ? "hospital" : "admin"));
// 预设主题配置(与现有 scss 主题保持一致)
const presetThemes = [
{
borderColor: "#3289FB",
boxShadow: "0 4px 12px 0 rgba(50, 137, 251, 0.5)",
value: "blue",
name: "穹宇蓝",
desc: "深邃星空\n科技感十足",
preview: "linear-gradient(to bottom, #3289FB 0%, #77B2FF 100%)",
},
{
borderColor: "#6155F5",
boxShadow: "0 4px 12px 0 rgba(97, 85, 245, 0.5)",
value: "purple",
name: "星轨紫",
desc: "神秘优雅\n梦幻氛围",
preview: "linear-gradient(to bottom, #6155F5 0%, #9B93FF 100%)",
},
{
borderColor: "#DE6262",
boxShadow: "0 4px 12px 0 rgba(222, 98, 98, 0.5)",
value: "red",
name: "流金粉",
desc: "晨雾染金\n自带光芒",
preview: "linear-gradient(to bottom, #DE6262 0%, #FF988B 100%)",
},
];
// 选择预设主题
const handlePresetTheme = (value) => {
themeStore.setTheme(value);
};
// 图标查找表route meta.icon -> { normal, active }
// 管理端 SVG
const ICON_MAP = {
"admin-router-fuwu-gd": { normal: fuwuGd, active: fuwuGdActive },
"admin-router-fuwu-bg": { normal: fuwuBg, active: fuwuBgActive },
"admin-router-yiyuan-xx": { normal: yiyuanXx, active: yiyuanXxActive },
"admin-router-kehu-fw": { normal: kehuFw, active: kehuFwActive },
"admin-router-kehu-jljf": { normal: kehuJljf, active: kehuJljfActive },
"admin-router-zhanghao-gl": { normal: zhanghaoGl, active: zhanghaoGlActive },
};
// 医院端 SVG
const HOSPITAL_SVG_ICON_MAP = {
"hospital-router-yy-gzt": { normal: yyGzt, active: yyGztActive },
"hospital-router-yy-gd": { normal: yyGd, active: yyGdActive },
"hospital-router-yy-jf": { normal: yyJf, active: yyJfActive },
};
// 当前 hover 的菜单项 path
const hoveredPath = ref("");
// 判断菜单项是否处于激活态
const isActiveMenu = (path) => {
if (!path || !route.path) return false;
return route.path === path;
};
// hover 或激活时都用 active 图标
const isMenuHoveredOrActive = (path) => {
return isActiveMenu(path) || hoveredPath.value === path;
};
// 顶部水平菜单:根据当前顶层路由(/admin 或 /hospital动态生成
const sideRoutes = computed(() => {
const groupPath = isHospitalMode.value ? "/hospital" : "/admin";
const groupRoute = route.matched.find((r) => r.path === groupPath);
if (!groupRoute || !groupRoute.children) return [];
return groupRoute.children
.filter((c) => c.meta?.title && !c.meta?.hidden && !c.path.includes(":"))
.map((c) => {
// 管理端 / 医院端都按 SVGnormal + active查表
const iconMap = isHospitalMode.value ? HOSPITAL_SVG_ICON_MAP : ICON_MAP;
const icon = iconMap[c.meta?.icon];
return {
path: `${groupPath}/${c.path}`,
title: c.meta.title,
iconUrl: icon?.normal || "",
iconUrlActive: icon?.active || "",
};
});
});
const currentTitle = computed(() => {
const last = [...route.matched].reverse().find((r) => r.meta?.title);
return last?.meta?.title || "管理端";
});
// 头像sex === 1 用男,否则用女
const avatarSrc = computed(() => {
return userStore.userInfo?.gender === '女' ? avatarFemale : avatarMale;
});
const handleCommand = async (cmd) => {
if (cmd === "password") {
passwordDialogVisible.value = true;
return;
}
if (cmd === "theme") {
themeDialogVisible.value = true;
return;
}
if (cmd === "logout") {
try {
await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
confirmButtonText: "退出",
cancelButtonText: "取消",
type: "warning",
});
userStore.clear();
router.push("/login");
} catch (_) {
/* canceled */
}
}
};
</script>
<style scoped lang="scss">
.admin-layout {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
background-color: #f9faff;
padding: 32px;
gap: 23px;
background-image: url("@/assets/image/layout-bg.svg");
background-repeat: no-repeat;
background-size: 100% auto;
background-position: right top;
.admin-header {
flex-shrink: 0;
height: 96px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
height: 96px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: linear-gradient(to right, #f9faff 0, #f9faff 75%, #a6cffe 100%);
// .header-left {
// display: flex;
// align-items: center;
// height: 100%;
// flex: 1;
// min-width: 0;
.logo-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 20px;
font-size: 16px;
font-weight: 600;
white-space: nowrap;
height: 100%;
// 管理端:渐变文字
.logo-text {
background: linear-gradient(to right, #3fc5fd, #3b90fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 22px;
.version {
font-size: 14px;
}
}
}
.header-middle {
display: flex;
align-items: center;
height: 100%;
min-width: 0;
}
// 医院端:菜单旁的快速提交按钮容器
.header-quick-submit {
margin-left: 14px;
}
.quick-submit-btn {
display: flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
border-radius: 18px;
font-weight: 500;
background: linear-gradient(90deg, #3289FB 0%, #00B2FF 100%);
border-radius: 28px 28px 28px 28px;
border: 1px solid rgba(255, 255, 255, 0.15);
width: 155px;
height: 43px;
font-weight: 400;
font-size: 14px;
color: #FFFFFF;
.quick-icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
// }
.header-right {
display: flex;
align-items: center;
height: 100%;
padding-right: 30px;
.user-trigger {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
color: #000000;
}
}
.header-menu {
height: 100%;
flex-shrink: 0;
flex-grow: 0;
min-width: 0;
background: transparent !important;
border-bottom: none !important;
height: 100%;
display: flex;
align-items: center !important;
// 顶部一级菜单
:deep(.el-menu-item) {
font-weight: 400;
font-size: 16px;
color: #1d2129 !important;
height: 60px;
line-height: 60px;
border-bottom: none !important;
padding: 0 14px !important;
display: flex;
align-items: center;
&:hover {
// background-color: rgba(50, 137, 251, 0.08) !important;
color: #3289fb !important;
}
}
// 一级激活
:deep(.el-menu-item.is-active) {
font-weight: 400;
font-size: 16px;
color: #3289fb !important;
background-color: transparent !important;
border-bottom: none !important;
&::after {
display: none !important;
}
}
}
}
.admin-content {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
}
.nav-icon {
width: 18px;
height: 18px;
margin-right: 6px;
vertical-align: middle;
object-fit: contain;
flex-shrink: 0;
}
// 医院端 Element Plus 图标尺寸
.el-icon.nav-icon {
width: 18px;
height: 18px;
font-size: 18px;
line-height: 18px;
color: inherit;
:deep(svg) {
width: 18px;
height: 18px;
}
}
</style>
<style lang="scss">
.admin-layout {
.el-menu-item:not(.is-disabled):focus,
.el-menu--horizontal .el-menu-item:not(.is-disabled):hover {
background-color: transparent !important;
}
}
.theme-dialog {
width: max-content;
border-radius: 24px 24px 24px 24px;
.theme-body {
padding: 24px 24px 33px;
border-radius: 24px 24px 24px 24px;
background-color: #fff;
width: max-content;
overflow: hidden;
.theme-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
font-size: 20px;
color: #1D2129;
margin-bottom: 16px;
.theme-title {
font-weight: 600;
font-size: 20px;
color: #1D2129;
}
.close-btn {
width: 30px;
height: 30px;
cursor: pointer;
}
}
.theme-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
.theme-card {
width: 126px;
height: 162px;
border-radius: 12px 12px 12px 12px;
border: 1px solid #E0E0E0;
padding: 15px;
cursor: pointer;
transition: all 0.25s ease;
text-align: center;
background: #fff;
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: center;
&:hover {
border-color: var(--border-color);
}
&.active {
border-color: var(--border-color);
}
}
.theme-preview {
width: 64px;
height: 64px;
border: 4px solid #FFFFFF;
border-radius: 50%;
margin-bottom: 12px;
}
.theme-name {
font-weight: 600;
font-size: 14px;
color: #1D2129;
margin-bottom: 4px;
}
.theme-desc {
font-weight: 400;
font-size: 12px;
color: #666666;
white-space: pre-line;
}
}
}
}
</style>