Files
UserhomeFront/src/components/LeftMenu.vue
T
chenxing 0f7101983e feat(msgcenter): /msg 换真实数据——服务端筛选+滚动分页、铃铛直跳、unreadCount 上提 store、WS 通知带应用名
- views/msg/index.vue:数据层整体替换(msgType 0/1/2 映射、isRead 1=已读、fmtTime 重写绝对时间);列表走 msgCenter/list 服务端筛选(msgType/appKey/isRead/keyword 防抖 300ms),滚动到底追加下一页,切视图/搜索重置第 1 页;左栏分类计数走 summary(旧版 UserMsg 缺字段降级显示 —),应用清单取 store 应用列表、计数同源 todos;点击卡片标已读、全部已读、去处理(showJump/jumpUrl)、审批直批(共享弹窗);失败态有重试;WS 新消息经 mittBus 自刷新
- components/Header.vue:铃铛直跳 /msg(移除 Poptip 浮层与 900 宽 Modal 及 columns/tableApi 死代码);unreadCount 上提 pinia store(角标与 /msg、LeftMenu 同源);WS Notice.info 标题带应用名(appKey 匹配 store 应用列表,缺失回退类型标签);壁纸/LOGO Modal 不动
- components/MsgApprovalModal.vue(新增):审批直批弹窗抽共享组件,逻辑照抄原 Header 内联实现(同意可选意见/拒绝必填校验/三种失败反馈)
- components/LeftMenu.vue:「消息」入口(url=/msg)未读角标(读 store,红底白字胶囊)
- store/index.ts:unreadCount + setUnreadCount/incUnread/decUnread
- api/BaseApi.ts:getMsgCenterList 参数类型放宽(服务端筛选项);getMsgCenterSummary 返回类型补分类计数字段(可缺省)
- assets/styles/header.scss:删除消息浮层死样式 .message-wrap 段
2026-09-30 12:37:30 +08:00

259 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div :class="menuClass">
<ul class="menu-ul" v-if="menuVisible">
<li class="operate-item" @click="menuVisible = false" title="收起菜单">
<SvgIcon name="menu-operate" color="#ececec" />
</li>
<template v-for="(item, index) in menuList" :key="index">
<li
v-wave="myWave"
@click="linkPage(item, index)"
:class="{ active: index == activeIndex, 'first-bottom': index == menuFirstBottomIndex }"
>
<a :title="item.name">
<!-- 图标走后台上传的 svg(菜单 icon 字段),保留本地 sprite 兜底能力 -->
<SvgIcon v-if="item.localIcon" :name="item.localIcon" size="24"
:color="index == activeIndex ? '#fff' : '#ececec'" />
<FetchSvg v-else size="24" :src="$conf.serviceHost + item.icon"
:color="index == activeIndex ? '#fff' : '#ececec'" />
<span>{{ item.name }}</span>
<!-- 「消息」入口未读角标(读 store,与 Header 铃铛同源) -->
<span v-if="item.url === '/msg' && store.unreadCount > 0" class="menu-badge">
{{ store.unreadCount > 99 ? "99+" : store.unreadCount }}
</span>
</a>
</li>
</template>
<!-- <li class="fengche-item" v-wave="myWave">
<a href="javascript:void(0)">
<SvgIcon name="fengche" color="#ffffff" />
<span>随机壁纸</span>
</a>
</li> -->
<!-- <li class="fengche-item" v-wave="myWave">
<RouterLink :to="{ name: 'AppDetails', params: { type: 1, id: 500 } }">
<SvgIcon name="fengche" color="#ffffff" />
<span>随机壁纸</span>
</RouterLink>
</li> -->
</ul>
<div class="menu-arrow_box" title="展开菜单" v-else @click="menuVisible = true">
<SvgIcon name="menu-hide-arrow" color="white" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watchEffect, inject, nextTick } from "vue";
// import { BaseDict } from "@/types/Basics.ts";
import SvgIcon from "@/components/SvgIcon.vue";
import { useRoute, useRouter } from "vue-router";
import { useDefaultStore } from "@/store";
import { storeToRefs } from "pinia";
import { LinkOpenTypeEnum, StateEnum } from "@/types";
import FetchSvg from "@/components/FetchSvg.vue";
import mittBus from "@/utils/mitt";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { bschLinkPage, isTickeRoterByName } from "@/utils/bschLinkPage";
const { $conf } = useCurrentInstance();
const myWave = ref({
color: "#fff",
initialOpacity: 0.5,
duration: 0.45,
easing: "ease-in",
});
const route = useRoute(),
router = useRouter();
const store = useDefaultStore();
const { menuVisible } = storeToRefs(store);
/** 一级入口(含「消息」「设置」)统一由后台「边栏设置」配置,图标走 icon 字段上传的 svg */
const menuList = computed<any[]>(() => {
const topArr = store.curUserMenuList.filter(x => x.isBottom !== true).sort((a, b) => a.orderNum - b.orderNum);
const bottomArr = store.curUserMenuList.filter(x => x.isBottom === true).sort((a, b) => a.orderNum - b.orderNum);
return [...topArr, ...bottomArr];
});
//第一个悬浮底部的index
const menuFirstBottomIndex = computed<number>(() => menuList.value.findIndex(x => x.isBottom === true));
const activeIndex = ref(-1);
watchEffect(() => {
let findIndex = menuList.value.findIndex(x => x.url === route.path);
if (findIndex > -1) {
activeIndex.value = findIndex;
return;
}
if (isTickeRoterByName(route.name as string)) {
let menuId = route.params.menuId as string;
activeIndex.value = menuList.value.findIndex(x => x.id == menuId);
}
});
onMounted(() => {
setTimeout(() => {
$(".app-desktop").addClass("app-desktop__animation");
}, 0);
});
const menuClass = computed(() => {
return ["menu-wrap", menuVisible.value ? "menu-wrap__show" : "menu-wrap__hide"];
});
const isAdmin = computed(() => store.getIsAdmin);
async function linkPage(item: any, index: number) {
// if (!item.isNative && !item.isHeader) {
// bschLinkPage({ url: item.url, type: item.openType });
// return;
// }
if (!item.isNative && item.openType === LinkOpenTypeEnum.Open) {
window.open(item.url);
return;
}
mittBus.emit("updateState", StateEnum.Desktop);
mittBus.emit("reload");
if (!!item.isNative) {
router.push(item.url);
} else {
router.push({ name: "AppDetails", params: { menuId: item.id } });
}
}
</script>
<style lang="scss">
.menu-wrap {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 70px;
z-index: 10;
// background: rgba(0, 0, 0, 0.4);
background-color: rgba(0, 0, 0, 0.3);
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(15px);
padding: 5px;
transition: left 0.2s ease-in-out, opacity 0.2s ease-in-out;
&__show {
left: 0;
}
&__hide {
left: -70px;
}
}
.menu-ul {
--textColor: #ececec;
display: flex;
flex-direction: column;
height: 100%;
li {
position: relative;
display: inline-block;
vertical-align: middle;
-webkit-transform: translateZ(0);
background-color: none;
transform: translateZ(0);
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
backface-visibility: hidden;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
-webkit-transition-duration: 0.5s;
transition-duration: 0.5s;
-webkit-transition-property: color, background-color;
transition-property: color, background-color;
width: 60px;
height: 60px;
margin: 8px 0;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
a {
color: var(--textColor);
text-align: center;
font-size: 13px;
display: block;
line-height: normal;
}
svg {
display: inline-block;
width: 24px;
height: 24px;
}
span {
display: block;
}
}
li.operate-item {
cursor: pointer;
svg {
width: 42px;
height: 42px;
}
&:hover {
background: none;
}
}
li.fengche-item {
position: absolute;
bottom: 8px;
left: 5px;
}
li:hover {
background-color: rgba(255, 255, 255, 0.15);
}
li.active {
background-color: rgba(255, 255, 255, 0.3);
--textColor: #fff;
// color: #fff;
}
li.first-bottom {
margin-top: auto;
}
}
/* 「消息」入口未读角标:图标右上角红底白字胶囊(与 Header Badge 同源数据) */
.menu-badge {
position: absolute;
top: 6px;
right: 4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: #f24822;
color: #fff;
font-size: 11px;
line-height: 16px;
text-align: center;
pointer-events: none;
}
.menu-arrow_box {
position: relative;
left: 62px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 42px;
border-radius: 0px 8px 8px 0px;
background: rgba(0, 0, 0, 0.3);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(15px);
cursor: pointer;
svg {
width: 22px;
height: 22px;
display: inline-block;
}
}
</style>