|
|
<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) => {
|
|
|
// 管理端 / 医院端都按 SVG(normal + 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> |