build / build (push) Successful in 1m21s
用户三点修订:
1. 面板已是模态框,去掉背景应用列表的同步过滤(解开 mittBus.searchChange 整条链)
2. 结果右侧灰色小字显示应用类型(来源 BoaAppType.name,与 /apps 分组标题同源)
3. 类型呈现暂按方案 A(右侧灰字);方案 B(类型分组)对照稿见 design/07
- Header.vue: watch(searchAppText) 不再 emit("searchChange");cmdkResult 由
getFlattenAppList(纯 flatten,会丢类型名)改为遍历 curUserAppList 分组展开并挂
appTypeName;模板加 .app-cmdk-type
- AppList.vue: 删 searchAppText / searchAppList / isSearch 与模板搜索态分支、
.no-search-box 样式、孤儿导入 flatten(lodash) 与 requireImg;删 mittBus 监听
- utils/mitt.ts: 删 searchChange 事件类型
- assets/styles/header.scss: 加 .app-cmdk-type 样式
- harness-main.ts: mock curUserAppList 补齐 BoaAppType 分组结构(id + name),
原 mock 丢了 name 导致类型名取不到
注:本仓工作区另有并行会话 WIP(StatsBarModal 迁入 Settings / 主题预览),
本提交只含本案 5 个文件与 hunks,未触碰对方改动。
672 lines
26 KiB
Vue
672 lines
26 KiB
Vue
<template>
|
||
<header :class="headerClass">
|
||
<img v-if="!!logoUrl" :src="baseUrl + '/image/default-logo.png'" v-real-img="logoUrl" class="logo-box" alt="" />
|
||
<ul class="operate-container fix">
|
||
<li v-if="isWidgetIconShow" :class="{ active: isWidgetShow }">
|
||
<div class="icon-box" v-wave @click="toggleWidgetShow">
|
||
<SvgIcon name="header-4" color="#333333" />
|
||
</div>
|
||
</li>
|
||
<li v-if="isSearchIconShow">
|
||
<div class="icon-box" v-wave @click="openSearch">
|
||
<SvgIcon name="header-1" color="#333333" />
|
||
</div>
|
||
</li>
|
||
<!-- 人像图标 =唯一入口(2026-10-08整合):铃铛与齿轮两枚图标下线,
|
||
消息中心仍走左侧菜单 /msg(LeftMenu 保留未读角标);
|
||
原齿轮菜单的 个人信息/后台管理/退出登陆 合并进本卡片,
|
||
其余 sys/logo/sidebar/statset 五项 2026-09-30 已迁/settings,不再重复入口 -->
|
||
<li>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<div class="user-card-logout">
|
||
<button class="logout-btn" @click="logout">退出登录</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</Poptip>
|
||
</li>
|
||
</ul>
|
||
</header>
|
||
|
||
<Modal v-model="isWallpaperShow" class-name="wallpaper-modal" width="960" :mask="false" title="设置壁纸"
|
||
:transfer="false">
|
||
<vuescroll>
|
||
<div class="wallpaper-wrap">
|
||
<ul class="wallpaper-ul">
|
||
<template v-for="(item, index) in wallpaperList">
|
||
<li :class="(index + 1) % 6 == 0 ? '' : 'mr-16'" @click="onClickWallpaper(item.url)">
|
||
<div class="check-box">
|
||
<SvgIcon size="16" v-if="getIsWallpaper(item.url)" color="#45B972"
|
||
name="role-checked" />
|
||
<SvgIcon size="16" v-else name="role-check" color="#dcdee0" />
|
||
</div>
|
||
|
||
<label v-if="item.type == 0">系统</label>
|
||
<label v-if="item.type == 1">校</label>
|
||
<ContextMenu v-if="item.isEdit" @select="onWallpaperSelect" :menu="[
|
||
{
|
||
label: '删除',
|
||
id: item.id,
|
||
},
|
||
]">
|
||
<img :src="item.url" alt="" />
|
||
</ContextMenu>
|
||
<template v-else>
|
||
<img :src="item.url" alt="" />
|
||
</template>
|
||
|
||
<div class="mask-box">{{ index + 1 }}</div>
|
||
</li>
|
||
</template>
|
||
<li>
|
||
<Upload class="add-wrap tc" :on-success="wallpaperSuccess" :show-upload-list="false"
|
||
:action="$conf.serviceHost + '/api/wallpaperUpload'"
|
||
accept="image/jpeg, image/jpg, image/png" :with-credentials="true"
|
||
:headers="uploadHeaders">
|
||
<div class="add-box">
|
||
<SvgIcon name="upload-add" color="#939393" style="width: 24px; height: 24px" />
|
||
<p>点击上传图片</p>
|
||
</div>
|
||
</Upload>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</vuescroll>
|
||
|
||
<template #footer>
|
||
<Button type="primary" ghost @click="isWallpaperShow = false">关闭</Button>
|
||
</template>
|
||
</Modal>
|
||
|
||
<Modal v-model="isLogoShow" class-name="logo-modal" width="720" :mask="false" title="设置LOGO" :transfer="false">
|
||
<vuescroll>
|
||
<div class="wallpaper-wrap">
|
||
<ul class="wallpaper-ul">
|
||
<template v-for="(item, index) in logoList">
|
||
<li :class="(index + 1) % 4 == 0 ? '' : 'mr-16'">
|
||
<label v-if="item.type == 0">系统</label>
|
||
<label v-else>校</label>
|
||
<ContextMenu v-if="item.isEdit" @select="onLogoSelect" :menu="[
|
||
{
|
||
label: '删除',
|
||
id: item.id,
|
||
},
|
||
]">
|
||
<img :src="item.url" alt="" />
|
||
</ContextMenu>
|
||
<template v-else>
|
||
<img :src="item.url" alt="" />
|
||
</template>
|
||
</li>
|
||
</template>
|
||
<li>
|
||
<Upload class="add-wrap tc" :on-success="logoSuccess" :show-upload-list="false"
|
||
:action="$conf.serviceHost + '/api/logoUpload'"
|
||
accept="image/jpeg, image/jpg, image/png, image/gif, image/webp"
|
||
:with-credentials="true" :headers="uploadHeaders">
|
||
<div class="add-box">
|
||
<SvgIcon name="upload-add" color="#939393" style="width: 24px; height: 24px" />
|
||
<p>点击上传图片</p>
|
||
</div>
|
||
</Upload>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</vuescroll>
|
||
|
||
<template #footer>
|
||
<Button type="primary" ghost @click="isLogoShow = false">关闭</Button>
|
||
</template>
|
||
</Modal>
|
||
|
||
<!-- 审批直批弹窗(B3 §2)已抽共享组件(消息对接 2026-09-30),供 /msg 页复用;Header 内已无直批入口 -->
|
||
<MsgApprovalModal ref="refMsgApproval" />
|
||
|
||
<StatsBarModal ref="refStatsBarModal" />
|
||
<TeacherCopilotModal ref="refTeacherCopilotModal" />
|
||
|
||
<!-- 搜索命令面板(2026-10-08 定稿 B 方案):必须 Teleport 到 body ——
|
||
.default-header 有 position:relative + z-index:10,会创建层叠上下文,
|
||
面板写在 Header 内部时无论 z-index 多高,对外只表现为 10,会被左侧菜单与内容区盖住 -->
|
||
<Teleport to="body">
|
||
<div class="app-cmdk-layer" :class="{ 'is-open': isSearching }">
|
||
<div class="app-cmdk-mask" @click="closeSearch"></div>
|
||
<div class="app-cmdk-panel">
|
||
<div class="app-cmdk-hd">
|
||
<SvgIcon name="header-1" size="19" color="#98a2b0" />
|
||
<input
|
||
ref="refSearchInput"
|
||
v-model="searchAppText"
|
||
type="text"
|
||
placeholder="输入应用名以搜索"
|
||
autocomplete="off"
|
||
/>
|
||
<span class="app-cmdk-kbd">Esc</span>
|
||
</div>
|
||
<div class="app-cmdk-list" :class="{ 'is-instant': !cmdkStagger }" ref="refCmdkList">
|
||
<div
|
||
v-for="(item, i) in cmdkResult"
|
||
:key="item.id ?? i"
|
||
class="app-cmdk-item"
|
||
:class="{ 'is-hl': i === cmdkIndex }"
|
||
:style="{ '--i': i }"
|
||
@click="pickApp(item)"
|
||
>
|
||
<AppIcon
|
||
v-if="vectorIconOf(item)"
|
||
:name="vectorIconOf(item)"
|
||
scene="tile"
|
||
:tint="item.iconColor"
|
||
/>
|
||
<img v-else :src="$utils.appIconPath(item.icon)" class="app-cmdk-img" alt="" />
|
||
<span class="app-cmdk-name">{{ item.name }}</span>
|
||
<span class="app-cmdk-type" v-if="item.appTypeName">{{ item.appTypeName }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="app-cmdk-empty" :class="{ 'is-show': !cmdkResult.length }">
|
||
{{ store.appListLoading ? "正在加载应用…" : "未找到匹配的应用" }}
|
||
</div>
|
||
<div class="app-cmdk-ft">
|
||
<span>↑↓ 选择</span><span>↵ 打开</span><span>Esc 关闭</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||
import SvgIcon from "@/components/SvgIcon.vue";
|
||
import vuescroll from "vuescroll";
|
||
import { ref, reactive, computed, onMounted, onUnmounted, inject, watch, nextTick } from "vue";
|
||
import { useDefaultStore } from "@/store";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { requireImg } from "@/utils/hook/useImg";
|
||
import ContextMenu from "@/components/ContextMenu.vue";
|
||
import StatsBarModal from "@/components/StatsBarModal.vue";
|
||
import TeacherCopilotModal from "@/components/TeacherCopilotModal.vue";
|
||
import BaseApi from "@/api/BaseApi";
|
||
import MsgApprovalModal from "@/components/MsgApprovalModal.vue";
|
||
import { Notice } from "view-ui-plus";
|
||
import { IframePageParmas, RoleInfo } from "@/types";
|
||
import { isEqual, orderBy } from "lodash";
|
||
import UserSettingApi from "@/api/UserSettingApi";
|
||
import mittBus from "@/utils/mitt";
|
||
import MenuBarApi from "@/api/MenuBarApi";
|
||
import Utils from "@/utils";
|
||
import AppIcon from "@/components/AppIcon.vue";
|
||
import { appiconKeyOf } from "@/constants/appIconRules";
|
||
import { openAppPage } from "@/utils/bschLinkPage";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const store = useDefaultStore();
|
||
const refMsgApproval = ref<any>();
|
||
const { $conf } = useCurrentInstance();
|
||
const baseUrl = computed(() => $conf.fileHost);
|
||
|
||
/** 未读消息数上提 pinia store(消息对接 2026-09-30):Header 铃铛角标已于 2026-10-08 下线,
|
||
* 但摘要拉取与 WS 推送仍需保留 —— /msg 页左栏与 LeftMenu 角标与本store 同源 */
|
||
|
||
const refSearchInput = ref<any>();
|
||
const refStatsBarModal = ref<any>();
|
||
const refTeacherCopilotModal = ref<any>();
|
||
|
||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||
|
||
const logoUrl = computed(() => {
|
||
const schLogo = logoList.value.find(x => x.type == 1);
|
||
if (schLogo) return schLogo.url;
|
||
const sysLogo = logoList.value.find(x => x.type == 0);
|
||
return sysLogo ? sysLogo.url : "";
|
||
});
|
||
const isUserCardShow = ref(false),
|
||
isWallpaperShow = ref(false),
|
||
isLogoShow = ref(false),
|
||
isSearching = ref(false),
|
||
searchAppText = ref<string | null>(null),
|
||
wallpaperList = ref<any[]>([]),
|
||
logoList = ref<any[]>([]);
|
||
|
||
// ===== 消息中心 WS 实时推送(UserMsg /ws/msg,一次性票据握手;断线 5s 重连换新票) =====
|
||
let msgWs: WebSocket | null = null;
|
||
let msgWsPingTimer: any = null;
|
||
let msgWsReconnectTimer: any = null;
|
||
let msgWsClosed = false;
|
||
|
||
async function loadSummary() {
|
||
try {
|
||
const summary = await BaseApi.getMsgCenterSummary();
|
||
store.setUnreadCount(summary && summary.unreadCount);
|
||
} catch (e) {
|
||
/* 摘要失败不打扰页面 */
|
||
}
|
||
}
|
||
|
||
async function connectMsgWs() {
|
||
if (msgWsClosed) return;
|
||
let wsUrl: string | null = null;
|
||
try {
|
||
const res: any = await BaseApi.getMsgCenterWsTicket();
|
||
wsUrl = res && res.url;
|
||
} catch (e) {
|
||
/* 未登录/不可达:走兜底重连 */
|
||
}
|
||
// 票据不可得(UserMsg 重启中/未配置)也要保持重连节奏:服务恢复后自动续上
|
||
if (!wsUrl) {
|
||
if (!msgWsClosed) {
|
||
clearTimeout(msgWsReconnectTimer);
|
||
msgWsReconnectTimer = setTimeout(connectMsgWs, 5000);
|
||
}
|
||
return;
|
||
}
|
||
const ws = new WebSocket(wsUrl);
|
||
msgWs = ws;
|
||
ws.onopen = () => {
|
||
// 25s 心跳保活(服务端 90s 空闲回收)
|
||
msgWsPingTimer = setInterval(() => {
|
||
if (ws.readyState === WebSocket.OPEN) ws.send('{"type":"ping"}');
|
||
}, 25000);
|
||
};
|
||
ws.onmessage = ev => {
|
||
try {
|
||
const frame = JSON.parse(ev.data);
|
||
if (frame.type === "newMessage") onNewMessage(frame);
|
||
} catch (e) {
|
||
/* 非 JSON 帧忽略 */
|
||
}
|
||
};
|
||
ws.onclose = () => {
|
||
clearInterval(msgWsPingTimer);
|
||
msgWsPingTimer = null;
|
||
if (msgWs === ws) msgWs = null;
|
||
// 票据一次性:重连必换新票;退避上限 30s 防服务端长时间不可用时空转
|
||
if (!msgWsClosed) {
|
||
clearTimeout(msgWsReconnectTimer);
|
||
msgWsReconnectTimer = setTimeout(connectMsgWs, 5000);
|
||
}
|
||
};
|
||
}
|
||
|
||
/** WS 帧按 appKey 匹配应用名(store 应用列表);查无回退类型标签(缺失降级,不报错) */
|
||
function frameAppName(frame: any) {
|
||
const appKey = frame && frame.appKey;
|
||
if (appKey) {
|
||
const hit = store.getFlattenAppList.find((x: any) => x.appKey == appKey || x.appCode == appKey);
|
||
if (hit && hit.name) return hit.name;
|
||
}
|
||
return msgTypeLabel(frame ? frame.msgType : null);
|
||
}
|
||
|
||
function onNewMessage(frame: any) {
|
||
store.incUnread();
|
||
// /msg 页开着时自刷新(页内监听该事件重拉第一页与摘要)
|
||
mittBus.emit("msgCenter:newMessage", frame);
|
||
Notice.info({
|
||
title: `【${frameAppName(frame)}】新消息`,
|
||
desc: frame.content || "",
|
||
duration: 5,
|
||
});
|
||
}
|
||
|
||
function closeMsgWs() {
|
||
msgWsClosed = true;
|
||
clearTimeout(msgWsReconnectTimer);
|
||
msgWsReconnectTimer = null;
|
||
clearInterval(msgWsPingTimer);
|
||
msgWsPingTimer = null;
|
||
if (msgWs) {
|
||
try {
|
||
msgWs.close();
|
||
} catch (e) {
|
||
/* 已断开 */
|
||
}
|
||
msgWs = null;
|
||
}
|
||
}
|
||
|
||
const headerClass = computed(() => {
|
||
return { "default-header": true, "base-mask": route.name !== "index" };
|
||
});
|
||
|
||
const isWidgetIconShow = computed(() => {
|
||
return route.name === "index";
|
||
});
|
||
|
||
const isSearchIconShow = computed(() => {
|
||
return route.name === "apps";
|
||
});
|
||
const isWidgetShow = computed(() => {
|
||
return store.getWidgetDragObj.isWidgetShow;
|
||
});
|
||
|
||
const userRole = computed(() => store.userRoleList);
|
||
|
||
const toggleSearch: any = inject("toggleSearch");
|
||
|
||
onMounted(async () => {
|
||
setTimeout(() => {
|
||
loadSummary();
|
||
connectMsgWs();
|
||
// isWallpaperShow.value = true;
|
||
}, 200);
|
||
loadLogoList();
|
||
mittBus.on("searchReset", () => {
|
||
isSearching.value = false;
|
||
searchAppText.value = null;
|
||
});
|
||
window.addEventListener("keydown", onCmdkKeydown);
|
||
});
|
||
onUnmounted(() => {
|
||
mittBus.off("searchReset");
|
||
window.removeEventListener("keydown", onCmdkKeydown);
|
||
closeMsgWs();
|
||
});
|
||
// 面板已是模态框,输入只驱动面板自身过滤;
|
||
// 不再 emit("searchChange") —— 背景应用网格保持原样(2026-10-08 用户修订 1)
|
||
watch(searchAppText, () => {
|
||
cmdkStagger.value = false; // 输入过滤时不重播逐条入场
|
||
});
|
||
// ===== 搜索命令面板(2026-10-08 定稿 B 方案)=====
|
||
const refCmdkList = ref<any>();
|
||
const cmdkIndex = ref(0);
|
||
const cmdkStagger = ref(true); // 仅首次打开做逐条入场,输入过滤时不重播
|
||
|
||
/** 与 AppList.vue / index.vue / SearchPanel.vue 同口径的矢量渲染回退链 */
|
||
function vectorIconOf(item: any): string {
|
||
if (item?.iconKey) return item.iconKey;
|
||
if (item?.icon) return "";
|
||
return appiconKeyOf(item?.name || "");
|
||
}
|
||
|
||
/** 结果取自 store.curUserAppList(写入方 = AppList.reloadAppData)。
|
||
* 它是 BoaAppType 分组结构 [{ id, name, apps }],name 就是应用类型名;
|
||
* 展开时把类型名挂到每条上供右侧灰字展示。
|
||
* 注意别用 store.getFlattenAppList —— 它是纯 flatten,会把类型名丢掉 */
|
||
const cmdkResult = computed(() => {
|
||
const q = (searchAppText.value || "").trim();
|
||
const out: any[] = [];
|
||
(store.curUserAppList || []).forEach((group: any) => {
|
||
(group.apps || []).forEach((app: any) => {
|
||
if (!q || (app.name || "").indexOf(q) !== -1) {
|
||
out.push({ ...app, appTypeName: group.name });
|
||
}
|
||
});
|
||
});
|
||
return out;
|
||
});
|
||
|
||
function openSearch() {
|
||
cmdkStagger.value = true;
|
||
cmdkIndex.value = 0;
|
||
searchAppText.value = null;
|
||
isSearching.value = true;
|
||
nextTick(() => refSearchInput.value?.focus());
|
||
}
|
||
|
||
function closeSearch() {
|
||
isSearching.value = false;
|
||
searchAppText.value = null;
|
||
}
|
||
|
||
function moveHighlight(step: number) {
|
||
const len = cmdkResult.value.length;
|
||
if (!len) return;
|
||
cmdkIndex.value = (cmdkIndex.value + step + len) % len;
|
||
nextTick(() => {
|
||
const el = refCmdkList.value?.children?.[cmdkIndex.value];
|
||
el?.scrollIntoView({ block: "nearest" });
|
||
});
|
||
}
|
||
|
||
function pickApp(item: any) {
|
||
closeSearch();
|
||
openAppPage(item, $conf.ifameAppIdArr); // 与点击应用图标同一条路径
|
||
}
|
||
|
||
function onCmdkKeydown(ev: KeyboardEvent) {
|
||
if (!isSearching.value) return; // 面板未开:绝不拦截全局按键
|
||
if (ev.key === "Escape") {
|
||
closeSearch();
|
||
return;
|
||
}
|
||
if (ev.key === "ArrowDown") {
|
||
ev.preventDefault();
|
||
moveHighlight(1);
|
||
return;
|
||
}
|
||
if (ev.key === "ArrowUp") {
|
||
ev.preventDefault();
|
||
moveHighlight(-1);
|
||
return;
|
||
}
|
||
if (ev.key === "Enter") {
|
||
ev.preventDefault();
|
||
const it = cmdkResult.value[cmdkIndex.value];
|
||
if (it) pickApp(it);
|
||
}
|
||
}
|
||
|
||
function getIsCheckRoles(item: RoleInfo) {
|
||
return isEqual(item, store.curRoleInfo);
|
||
}
|
||
|
||
async function changeRole(role: RoleInfo) {
|
||
if (!!getIsCheckRoles(role)) return;
|
||
setTimeout(() => {
|
||
isUserCardShow.value = false;
|
||
}, 150);
|
||
store.setCurRoleInfo(role);
|
||
await BaseApi.savUserRole(role);
|
||
mittBus.emit("switchRole", role);
|
||
await getUserWallpaperList();
|
||
await loadLogoList();
|
||
}
|
||
|
||
/** 消息类型名(UserMsg msgType:0系统通知/1业务提醒/2审批待办);WS 帧缺 appName 时的降级标签 */
|
||
function msgTypeLabel(msgType: number | null) {
|
||
return { 0: "系统通知", 1: "业务提醒", 2: "审批待办" }[msgType as 0 | 1 | 2] || "系统通知";
|
||
}
|
||
|
||
async function wallpaperSuccess(response: any, file: any, fileListt: any) {
|
||
await getUserWallpaperList();
|
||
}
|
||
|
||
async function getUserWallpaperList() {
|
||
const list = await UserSettingApi.getUserWallpaperList(store.getCurSchId);
|
||
// list.forEach(element => {
|
||
// if (element.type == 1) element.url = $conf.serviceHost + element.url;
|
||
// });
|
||
wallpaperList.value = orderBy(list, ["type", "id"], ["asc", "asc"]);
|
||
}
|
||
/** 以下入口函数自2026-10-08 齿轮菜单下线后已无 UI 调用方,但对应 Modal 组件仍挂在模板尾部、
|
||
* 且 logoList 是header 徽标的数据源,故保留函数体不删,仅把已并轨的 isUserInfoShow 换成 isUserCardShow。
|
||
* 恢复入口时直接绑到卡片菜单即可。 */
|
||
async function settingsWallpaper() {
|
||
await getUserWallpaperList();
|
||
isUserCardShow.value = false;
|
||
isWallpaperShow.value = true;
|
||
}
|
||
|
||
/** 边栏设置入口改跳设置中心 pane(原 settingsMenu 开旧 MenuBarModal 的能力已于 2026-09-30 迁入 Settings.vue sidebar pane) */
|
||
function linkSidebarSetting() {
|
||
isUserCardShow.value = false;
|
||
router.push("/settings?pane=sidebar");
|
||
}
|
||
|
||
async function settingsStats() {
|
||
isUserCardShow.value = false;
|
||
refStatsBarModal.value.show();
|
||
}
|
||
|
||
async function settingsTeacherCopilot() {
|
||
isUserCardShow.value = false;
|
||
refTeacherCopilotModal.value.show();
|
||
}
|
||
|
||
async function onClickWallpaper(url: string) {
|
||
await UserSettingApi.checkWallpaper(store.getCurSchId, url);
|
||
const userSetting = store.curUserSettingInfo;
|
||
if (!!userSetting) {
|
||
userSetting.wallpaperUrl = url;
|
||
store.setCurUserSettingsInfo(userSetting);
|
||
}
|
||
isWallpaperShow.value = false;
|
||
}
|
||
|
||
async function onWallpaperSelect(item: any) {
|
||
await UserSettingApi.deleteWallById(item.id);
|
||
await getUserWallpaperList();
|
||
}
|
||
|
||
async function loadLogoList() {
|
||
const list = await UserSettingApi.getLogoList();
|
||
logoList.value = list || [];
|
||
}
|
||
|
||
async function logoSuccess(response: any, file: any, fileListt: any) {
|
||
await loadLogoList();
|
||
}
|
||
|
||
async function onLogoSelect(item: any) {
|
||
await UserSettingApi.deleteLogoById(item.id);
|
||
await loadLogoList();
|
||
}
|
||
function getIsWallpaper(url: string) {
|
||
return url === store.getCurWallPaperUrl;
|
||
}
|
||
async function toggleWidgetShow() {
|
||
const id = store.curUserSettingInfo?.id;
|
||
if (!!store.curUserSettingInfo) store.curUserSettingInfo.isWidgetShow = !store.getWidgetDragObj.isWidgetShow;
|
||
await UserSettingApi.toggleWidgetShow(id);
|
||
}
|
||
|
||
function linkSysSetting() {
|
||
isUserCardShow.value = false;
|
||
router.push("/syssetting");
|
||
}
|
||
|
||
function linkBackendManage() {
|
||
isUserCardShow.value = false;
|
||
const url = `${$conf.baseHost}/userbase/front/index.html`;
|
||
window.open(url, "_blank");
|
||
}
|
||
|
||
async function linkLogoSetting() {
|
||
await loadLogoList();
|
||
isUserCardShow.value = false;
|
||
isLogoShow.value = true;
|
||
}
|
||
|
||
/** 卡片菜单:个人信息 → 设置中心 profile pane(与通知端/用户中心同一地址)。
|
||
* 注意:mittBus 的 showUserInfoModal 事件仍由 AppHeader/BschIframe 使用,此处不再触发 */
|
||
function showUserInfo() {
|
||
isUserCardShow.value = false;
|
||
router.push("/settings?pane=profile");
|
||
}
|
||
|
||
function logout() {
|
||
let url = window.location.href;
|
||
url = "/logout?service=" + url;
|
||
url = $conf.ssoHost + url;
|
||
if (!!window.top) window.top.location.href = url;
|
||
else window.location.href = url;
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import url("@/assets/styles/header.scss");
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
.reset-panel .ivu-poptip-body {
|
||
padding: 0;
|
||
}
|
||
|
||
.reset-panel .ivu-poptip-inner {
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.wallpaper-modal {
|
||
z-index: 11 !important;
|
||
}
|
||
|
||
.wallpaper-modal .ivu-modal-body {
|
||
padding: 0;
|
||
}
|
||
|
||
.logo-modal {
|
||
z-index: 11 !important;
|
||
}
|
||
|
||
.logo-modal .ivu-modal-body {
|
||
padding: 0;
|
||
}
|
||
|
||
.ivu-table-cell .operating-link {
|
||
display: inline-block;
|
||
color: #2d8cf0;
|
||
cursor: pointer;
|
||
padding-right: 10px;
|
||
|
||
&.disabled {
|
||
color: #aaa !important;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.ivu-table:before {
|
||
content: none !important;
|
||
}
|
||
</style>
|