refactor(header): 用户入口整合为单卡片——人像图标承载切换身份+个人信息+后台管理+退出登录
build / build (push) Successful in 45s

铃铛与齿轮两枚图标下线,人像图标成为唯一入口,点击展开一张卡片:
切换身份列表(9条实测溢出滚动)+ 个人信息 + 后台管理(仅管理员)+ 退出登录。

删除无功能损失的冗余入口:原齿轮菜单的 sys/logo/sidebar/statset/backend 五项
已于 2026-09-30 迁入 /settings(Settings.vue:620 ALL_PANES),齿轮成为纯冗余。
消息中心仍走左侧菜单 /msg,LeftMenu 未读角标不受影响。

改动要点:
- isUserInfoShow + isRoleInfoShow 合并为 isUserCardShow,9 处函数体同步改名;
  删除 onUserinfoClickRoles、storeToRefs import 与 unreadCount 解构(模板已无 Badge)
- showUserInfo 改跳 /settings?pane=profile,不再 emit showUserInfoModal
  (该事件仍有 AppHeader/BschIframe/harness-main 三处消费方,不可删)
- :key 用 roleId-schId-childId 复合键,避免同校多身份时 roleId 重复
- header.scss 删 .roleinfo-wrap 与随之失效的 @use "sass:color",新增 .user-card
  (身份列表 max-height:310px + overflow-y:scroll + 6px 自绘滚动条)
- settingsWallpaper 等 6 个入口函数保留:对应 Modal 仍挂模板尾部且 logoList
  是 header 徽标数据源,只改内部变量名并加注释

验证:未声明标识符全量扫 47 个 .vue 零问题;vite build 通过;mock 取证实测
scrollHeight 683 > clientHeight 310 出滚动条、1 条时 75=75 不滚动、
menu-rows 管理员 2/普通用户 1、bell-count 与 gear-count 均为 0。

另:.gitignore 忽略卡片取证三件套(本地临时用,与 harness-* 同类不进 git)。
This commit is contained in:
chenxing
2026-10-08 15:28:44 +08:00
parent 297934f862
commit e46df62c0b
3 changed files with 222 additions and 160 deletions
+70 -120
View File
@@ -20,110 +20,64 @@
</div>
</Transition>
</li>
<!-- 铃铛直跳 /msg(消息对接 2026-09-30:移除 Poptip 浮层与 900 宽列表 Modal,
未读数上提 store 与 /msg 页左栏同源;审批直批弹窗抽至 MsgApprovalModal) -->
<!-- 人像图标 =唯一入口(2026-10-08整合):铃铛与齿轮两枚图标下线,
消息中心仍走左侧菜单 /msg(LeftMenu 保留未读角标);
原齿轮菜单的 个人信息/后台管理/退出登陆 合并进本卡片,
其余 sys/logo/sidebar/statset 五项 2026-09-30 已迁/settings,不再重复入口 -->
<li>
<div class="icon-box" v-wave title="消息中心" @click="router.push('/msg')">
<Badge :count="unreadCount" :offset="[2, 0]" overflow-count="99">
<SvgIcon name="header-2" color="#333333" />
</Badge>
</div>
</li>
<li>
<Poptip class="fl" popper-class="roleinfo-wrap reset-panel" :width="240" trigger="click"
placement="bottom-end" v-model="isRoleInfoShow" :transfer="true">
<div class="icon-box" v-wave>
<Poptip class="fl" popper-class="user-card-wrap reset-panel" :width="260" trigger="click"
placement="bottom-end" v-model="isUserCardShow" :transfer="true">
<div class="icon-box" v-wave title="切换身份">
<SvgIcon name="header-3" color="#333333" />
</div>
<template #content>
<h3 class="title">
<SvgIcon name="switch-role" color="#272829" />
<span class="ml-8">切换身份</span>
</h3>
<vuescroll>
<ul class="roles-ul">
<template v-for="(item, index) in userRole">
<li :class="{ acitve: getIsCheckRoles(item) }" @click="changeRole(item)">
<SvgIcon v-if="getIsCheckRoles(item)" color="#45B972" name="role-checked" />
<SvgIcon v-else name="role-check" color="#dcdee0" />
<div class="user-wrap">
<div class="user-box fix">
<span v-if="item.roleId != 3" class="fl f16 mr-10">
{{ store.userInfo.name }}
</span>
<span v-else class="fl f16 mr-10">
{{ item.childName }}
</span>
<span v-if="!!item.roleName" class="role-info">
{{ item.roleName }}
</span>
</div>
<div class="schname-box">
{{ item.schName }}
</div>
<div class="user-card">
<div class="user-card-hd">
<SvgIcon name="switch-role" color="#272829" />
<span class="ttl">切换身份</span>
<span class="close" @click="isUserCardShow = false">✕</span>
</div>
<!-- 身份列表有最大高度,超出显示滚动条。
key 用 roleId+schId+childId 复合:同校多身份时 roleId 会重复 -->
<div class="user-card-roles">
<div v-for="item in userRole"
:key="`${item.roleId}-${item.schId}-${item.childId}`"
:class="['u-role', { active: getIsCheckRoles(item) }]" @click="changeRole(item)">
<SvgIcon v-if="getIsCheckRoles(item)" color="#45B972" name="role-checked" />
<SvgIcon v-else name="role-check" color="#dcdee0" />
<div class="who">
<div class="nm">
<span class="t">{{ item.roleId != 3 ? store.userInfo.name : item.childName }}</span>
<span v-if="!!item.roleName" class="role-info">{{ item.roleName }}</span>
</div>
</li>
</template>
<div class="sch">{{ item.schName }}</div>
</div>
</div>
</div>
<ul class="userinfo-ul user-card-menu">
<li @click="showUserInfo">
<img :src="requireImg('userinfo/icon1.png')" alt="" />
<span>个人信息</span>
<svg class="arw" viewBox="0 0 16 16" fill="none">
<path
d="M6 3.5L10.5 8L6 12.5" stroke="#b6bbc3" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</li>
<li v-if="!!store.getIsAdmin" @click="linkBackendManage">
<SvgIcon name="admin-backend" size="20" color="#323233" class="backend-manage-icon" />
<span>后台管理</span>
<svg class="arw" viewBox="0 0 16 16" fill="none">
<path
d="M6 3.5L10.5 8L6 12.5" stroke="#b6bbc3" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
</li>
</ul>
</vuescroll>
</template>
</Poptip>
</li>
<li>
<Poptip class="fl" popper-class="reset-panel" :width="220" trigger="click" placement="bottom-end"
v-model="isUserInfoShow" :transfer="true">
<div class="icon-box" v-wave>
<SvgIcon name="header-5" color="#333333" />
</div>
<template #content>
<ul class="userinfo-ul">
<!-- <li class="header border" @click="onUserinfoClickRoles">
<span class="name">{{ store.userInfo?.name }}</span>
<span v-if="!!store.curRoleInfo?.roleName" class="role-info mr-10">
{{ store.curRoleInfo?.roleName }}
</span>
<Icon :size="18" color="#333" type="ios-arrow-forward" />
</li> -->
<li class="border" @click="showUserInfo">
<img :src="requireImg('userinfo/icon1.png')" alt="" />
<span>个人信息</span>
</li>
<li v-if="!!store.getIsAdmin" @click="linkSysSetting">
<img :src="requireImg('userinfo/icon5.png')" alt="" />
<span>系统设置</span>
</li>
<li v-if="!!store.getIsAdmin" @click="linkBackendManage">
<SvgIcon name="admin-backend" size="20" color="#323233" class="backend-manage-icon" />
<span>后台管理</span>
</li>
<li v-if="!!store.getIsAdmin" @click="linkLogoSetting">
<img :src="requireImg('userinfo/icon3.png')" alt="" />
<span>设置LOGO</span>
</li>
<!-- 边栏设置已迁设置中心(/settings?pane=sidebar,2026-09-30 二轮迁入真实管理),
此处仅跳转不再开旧 MenuBarModal(组件保留未挂载) -->
<li @click="linkSidebarSetting" v-if="!!store.getIsAdmin">
<img :src="requireImg('userinfo/icon6.png')" alt="" />
<span>边栏设置</span>
</li>
<li @click="settingsStats" v-if="!!store.getIsAdmin">
<img :src="requireImg('userinfo/icon7.png')" alt="" />
<span>统计设置</span>
</li>
<!--<li @click="settingsTeacherCopilot" v-if="!!store.getIsAdmin">
<img :src="requireImg('userinfo/icon8.png')" alt="" />
<span>教师助手设置</span>
</li>-->
<li @click="settingsWallpaper" class="border">
<img :src="requireImg('userinfo/icon2.png')" alt="" />
<span>设置壁纸</span>
</li>
<li @click="logout">
<img :src="requireImg('userinfo/icon4.png')" alt="" />
<span>退出登陆</span>
</li>
</ul>
<div class="user-card-logout">
<button class="logout-btn" @click="logout">退出登录</button>
</div>
</div>
</template>
</Poptip>
</li>
@@ -234,7 +188,6 @@ import SvgIcon from "@/components/SvgIcon.vue";
import vuescroll from "vuescroll";
import { ref, reactive, computed, onMounted, onUnmounted, inject, watch } from "vue";
import { useDefaultStore } from "@/store";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { requireImg } from "@/utils/hook/useImg";
import ContextMenu from "@/components/ContextMenu.vue";
@@ -257,8 +210,8 @@ const refMsgApproval = ref<any>();
const { $conf } = useCurrentInstance();
const baseUrl = computed(() => $conf.fileHost);
/** 未读消息数上提 pinia store(消息对接 2026-09-30):Header 角标与 /msg 页左栏、LeftMenu 同源 */
const { unreadCount } = storeToRefs(store);
/** 未读消息数上提 pinia store(消息对接 2026-09-30):Header 铃铛角标已于 2026-10-08 下线,
* 但摘要拉取与 WS 推送仍需保留 —— /msg 页左栏与 LeftMenu 角标与本store 同源 */
const refSearchInput = ref<any>();
const refStatsBarModal = ref<any>();
@@ -272,8 +225,7 @@ const logoUrl = computed(() => {
const sysLogo = logoList.value.find(x => x.type == 0);
return sysLogo ? sysLogo.url : "";
});
const isUserInfoShow = ref(false),
isRoleInfoShow = ref(false),
const isUserCardShow = ref(false),
isWallpaperShow = ref(false),
isLogoShow = ref(false),
isSearching = ref(false),
@@ -431,7 +383,7 @@ function getIsCheckRoles(item: RoleInfo) {
async function changeRole(role: RoleInfo) {
if (!!getIsCheckRoles(role)) return;
setTimeout(() => {
isRoleInfoShow.value = false;
isUserCardShow.value = false;
}, 150);
store.setCurRoleInfo(role);
await BaseApi.savUserRole(role);
@@ -440,13 +392,6 @@ async function changeRole(role: RoleInfo) {
await loadLogoList();
}
function onUserinfoClickRoles() {
isUserInfoShow.value = false;
setTimeout(() => {
isRoleInfoShow.value = true;
}, 0);
}
/** 消息类型名(UserMsg msgType:0系统通知/1业务提醒/2审批待办);WS 帧缺 appName 时的降级标签 */
function msgTypeLabel(msgType: number | null) {
return { 0: "系统通知", 1: "业务提醒", 2: "审批待办" }[msgType as 0 | 1 | 2] || "系统通知";
@@ -463,25 +408,28 @@ async function getUserWallpaperList() {
// });
wallpaperList.value = orderBy(list, ["type", "id"], ["asc", "asc"]);
}
/** 以下入口函数自2026-10-08 齿轮菜单下线后已无 UI 调用方,但对应 Modal 组件仍挂在模板尾部、
* 且 logoList 是header 徽标的数据源,故保留函数体不删,仅把已并轨的 isUserInfoShow 换成 isUserCardShow。
* 恢复入口时直接绑到卡片菜单即可。 */
async function settingsWallpaper() {
await getUserWallpaperList();
isUserInfoShow.value = false;
isUserCardShow.value = false;
isWallpaperShow.value = true;
}
/** 边栏设置入口改跳设置中心 pane(原 settingsMenu 开旧 MenuBarModal 的能力已于 2026-09-30 迁入 Settings.vue sidebar pane) */
function linkSidebarSetting() {
isUserInfoShow.value = false;
isUserCardShow.value = false;
router.push("/settings?pane=sidebar");
}
async function settingsStats() {
isUserInfoShow.value = false;
isUserCardShow.value = false;
refStatsBarModal.value.show();
}
async function settingsTeacherCopilot() {
isUserInfoShow.value = false;
isUserCardShow.value = false;
refTeacherCopilotModal.value.show();
}
@@ -523,27 +471,29 @@ async function toggleWidgetShow() {
}
function linkSysSetting() {
isUserInfoShow.value = false;
isUserCardShow.value = false;
router.push("/syssetting");
}
function linkBackendManage() {
isUserInfoShow.value = false;
isUserCardShow.value = false;
const url = `${$conf.baseHost}/userbase/front/index.html`;
window.open(url, "_blank");
}
async function linkLogoSetting() {
await loadLogoList();
isUserInfoShow.value = false;
isUserCardShow.value = false;
isLogoShow.value = true;
}
function showSeachBox() { }
/** 卡片菜单:个人信息 → 设置中心 profile pane(与通知端/用户中心同一地址)。
* 注意:mittBus 的 showUserInfoModal 事件仍由 AppHeader/BschIframe 使用,此处不再触发 */
function showUserInfo() {
isUserInfoShow.value = false;
mittBus.emit("showUserInfoModal");
isUserCardShow.value = false;
router.push("/settings?pane=profile");
}
function logout() {