880 lines
33 KiB
Vue
880 lines
33 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>
|
||
<Button @click="settingsMenu">test</Button>
|
||
</li> -->
|
||
<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">
|
||
<Transition name="fade1" :duration="{ enter: 1000, leave: 0 }">
|
||
<div v-show="!isSearching" @click="isSearching = true" class="icon-box" v-wave>
|
||
<SvgIcon name="header-1" color="#333333" />
|
||
</div>
|
||
</Transition>
|
||
<Transition name="search" :duration="{ enter: 300, leave: 300 }">
|
||
<div v-show="isSearching" class="search-box">
|
||
<input ref="refSearchInput" v-model="searchAppText" type="text" placeholder="请输入应用名称搜索" />
|
||
<SvgIcon @click="isSearching = false" class="icon" name="header-1" size="20" color="#c4c4c4" />
|
||
</div>
|
||
</Transition>
|
||
</li>
|
||
<li>
|
||
<Poptip class="fl" popper-class="message-wrap reset-panel" :width="360" trigger="click"
|
||
placement="bottom-end" v-model="isMsgShow" :transfer="true">
|
||
<div class="icon-box" v-wave>
|
||
<Badge :count="unreadCount" :offset="[2, 0]" overflow-count="99">
|
||
<SvgIcon name="header-2" color="#333333" />
|
||
</Badge>
|
||
</div>
|
||
<template #content>
|
||
<h3 class="title">查看消息</h3>
|
||
<vuescroll>
|
||
<ul class="msg-ul">
|
||
<li class="no-msg-box" v-if="userMsgList.length == 0">暂无消息</li>
|
||
<template v-else v-for="(item, index) in userMsgList">
|
||
<li class="fix">
|
||
<h4 :class="{
|
||
'text-ellipsis': true,
|
||
read: item.isRead === 1,
|
||
}">
|
||
<span class="app-name-box">{{ item.appName || msgTypeLabel(item.msgType) }}</span>
|
||
<span class="content" :title="item.content"
|
||
@click="onClickMsg(item)">
|
||
{{ item.title }}:{{ item.content }}
|
||
</span>
|
||
</h4>
|
||
<span v-if="item.msgType === 2" class="approval-box">
|
||
<a href="javascript:;" class="approval-link approve"
|
||
@click="openApproval(item, 'approve')">同意</a>
|
||
<a href="javascript:;" class="approval-link reject"
|
||
@click="openApproval(item, 'reject')">拒绝</a>
|
||
</span>
|
||
<a v-if="item.showJump" href="javascript:;" @click="onClickMsg(item)"
|
||
title="查看详情">
|
||
<SvgIcon name="arrow-right" color="#bbbbbb" />
|
||
</a>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
</vuescroll>
|
||
<div class="footer fix">
|
||
<a href="javascript:;" class="fl link-all" @click="showMsgList">查看全部</a>
|
||
<a href="javascript:;" class="fr read-all" v-if="userMsgList.length > 0"
|
||
@click="readMsgAll(1)">
|
||
全部已读
|
||
</a>
|
||
|
||
<!-- <a class="tzql_r" @click="clearMsg" href="javascript:;" title="清空消息"
|
||
><img src="image/msg-clear.png"
|
||
/></a> -->
|
||
</div>
|
||
</template>
|
||
</Poptip>
|
||
</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>
|
||
<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>
|
||
</li>
|
||
</template>
|
||
</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>
|
||
<li @click="settingsMenu" 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>
|
||
</template>
|
||
</Poptip>
|
||
</li>
|
||
</ul>
|
||
</header>
|
||
|
||
<Modal v-model="isMsgListShow" width="900" :mask="true" title="消息" class-name="msg-modal" :transfer="false">
|
||
<div class="fix mb-10">
|
||
<Button type="primary" @click="resetMsgPage" size="small" class="fl ml-10 width-10" ghost>刷新</Button>
|
||
<Button type="primary" @click="readMsgAll(2)" size="small" class="fl ml-10 width-10" ghost>全部已读</Button>
|
||
</div>
|
||
<TablePage :table-height="360" ref="refTablePage" page-type="small" :dataApi="tableApi" :pageParams="pageParams"
|
||
:columns="columns" :auto-load="false" />
|
||
<template #footer>
|
||
<Button type="primary" ghost @click="isMsgListShow = false">关闭</Button>
|
||
</template>
|
||
</Modal>
|
||
<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):同意可选意见 / 拒绝意见必填;身份服务端取会话 -->
|
||
<Modal v-model="approval.visible" width="460" :mask-closable="false"
|
||
:title="approval.action === 'approve' ? '同意办理' : '拒绝办理'">
|
||
<div class="approval-body">
|
||
<p class="approval-title text-ellipsis">{{ approval.title || "确认办理该待办?" }}</p>
|
||
<Input v-model="approval.opinion" type="textarea" :rows="3" maxlength="200" show-word-limit
|
||
:placeholder="approval.action === 'approve' ? '审批意见(可选)' : '请输入拒绝意见(必填)'" />
|
||
</div>
|
||
<template #footer>
|
||
<Button type="primary" :loading="approval.submitting" @click="submitApproval">
|
||
{{ approval.action === "approve" ? "确认同意" : "确认拒绝" }}
|
||
</Button>
|
||
<Button @click="approval.visible = false">取消</Button>
|
||
</template>
|
||
</Modal>
|
||
|
||
<MenuBarModal ref="refMenuBarModal" />
|
||
<StatsBarModal ref="refStatsBarModal" />
|
||
<TeacherCopilotModal ref="refTeacherCopilotModal" />
|
||
</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 } 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 MenuBarModal from "@/components/MenuBarModal.vue";
|
||
import StatsBarModal from "@/components/StatsBarModal.vue";
|
||
import TeacherCopilotModal from "@/components/TeacherCopilotModal.vue";
|
||
import BaseApi from "@/api/BaseApi";
|
||
import TablePage from "@/components/TablePage.vue";
|
||
import { Notice } from "view-ui-plus";
|
||
import { IframePageParmas, RoleInfo } from "@/types";
|
||
import { isEqual, orderBy } from "lodash";
|
||
import { PageParams } from "@/types/Basics";
|
||
import UserSettingApi from "@/api/UserSettingApi";
|
||
import mittBus from "@/utils/mitt";
|
||
import MenuBarApi from "@/api/MenuBarApi";
|
||
import Utils from "@/utils";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const store = useDefaultStore();
|
||
const refTablePage: any = ref(null);
|
||
const { $conf } = useCurrentInstance();
|
||
const baseUrl = computed(() => $conf.fileHost);
|
||
|
||
const refSearchInput = ref<any>();
|
||
const refMenuBarModal = 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 isMsgShow = ref(false),
|
||
isMsgListShow = ref(false),
|
||
isUserInfoShow = ref(false),
|
||
isRoleInfoShow = ref(false),
|
||
isWallpaperShow = ref(false),
|
||
isLogoShow = ref(false),
|
||
isSearching = ref(false),
|
||
searchAppText = ref<string | null>(null),
|
||
wallpaperList = ref<any[]>([]),
|
||
logoList = ref<any[]>([]),
|
||
userMsgList = ref<any[]>([]),
|
||
/** 未读消息数(msgCenter/summary 拉取 + WS newMessage 实时累加) */
|
||
unreadCount = ref(0);
|
||
|
||
// ===== 消息中心 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();
|
||
unreadCount.value = (summary && summary.unreadCount) || 0;
|
||
} 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);
|
||
}
|
||
};
|
||
}
|
||
|
||
function onNewMessage(frame: any) {
|
||
unreadCount.value += 1;
|
||
reloadMsg();
|
||
if (isMsgListShow.value) resetMsgPage();
|
||
Notice.info({
|
||
title: frame.title || "新消息",
|
||
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");
|
||
|
||
const pageParams: PageParams = reactive({ page: 1, count: 10 });
|
||
const tableApi = (params: PageParams) => BaseApi.getMsgCenterList(params);
|
||
const columns = ref<any[]>([
|
||
{
|
||
type: "index",
|
||
title: "序号",
|
||
width: 70,
|
||
align: "center",
|
||
},
|
||
{
|
||
title: "应用",
|
||
width: 110,
|
||
align: "center",
|
||
render(h: any, params: any) {
|
||
return h("span", params.row.appName || msgTypeLabel(params.row.msgType));
|
||
},
|
||
},
|
||
{
|
||
title: "消息内容",
|
||
render(h: any, params: any) {
|
||
const row = params.row;
|
||
return h("span", { attrs: { title: row.content } }, `${row.title || ""}:${row.content || ""}`);
|
||
},
|
||
},
|
||
{
|
||
title: "状态",
|
||
width: 100,
|
||
render(h: any, params: any) {
|
||
return h("span", params.row.isRead === 1 ? "已读" : "未读");
|
||
},
|
||
},
|
||
{
|
||
title: "日期",
|
||
align: "center",
|
||
width: 200,
|
||
key: "sendTime",
|
||
},
|
||
{
|
||
title: "操作",
|
||
width: 210,
|
||
align: "center",
|
||
render(h: any, params: any) {
|
||
const row = params.row;
|
||
const nodes: any[] = [];
|
||
// 审批待办(msgType=2)直批入口:taskId 由服务端按 bizId 匹配定位;
|
||
// 无权限/已处理由后端返回文案,直批按钮不受跳转权限影响
|
||
if (row.msgType === 2 && row.msgId) {
|
||
nodes.push(
|
||
h(
|
||
"a",
|
||
{
|
||
attrs: { href: "javascript:;" },
|
||
on: {
|
||
click: () => openApproval(row, "approve"),
|
||
},
|
||
},
|
||
"同意"
|
||
),
|
||
h("span", " "),
|
||
h(
|
||
"a",
|
||
{
|
||
attrs: { href: "javascript:;" },
|
||
on: {
|
||
click: () => openApproval(row, "reject"),
|
||
},
|
||
},
|
||
"拒绝"
|
||
)
|
||
);
|
||
}
|
||
if (row.showJump) {
|
||
if (nodes.length) nodes.push(h("span", " "));
|
||
nodes.push(
|
||
h(
|
||
"a",
|
||
{
|
||
attrs: { href: "javascript:;" },
|
||
on: {
|
||
click: () => {
|
||
if (row.isRead !== 1 && row.msgId) {
|
||
BaseApi.readMsgCenter(row.msgId);
|
||
row.isRead = 1;
|
||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||
}
|
||
window.open(row.jumpUrl, "_blank");
|
||
},
|
||
},
|
||
},
|
||
"去处理"
|
||
)
|
||
);
|
||
}
|
||
if (!nodes.length) return h("span", "—");
|
||
return h("span", nodes);
|
||
},
|
||
},
|
||
]);
|
||
onMounted(async () => {
|
||
setTimeout(() => {
|
||
reloadMsg();
|
||
loadSummary();
|
||
connectMsgWs();
|
||
// isWallpaperShow.value = true;
|
||
}, 200);
|
||
loadLogoList();
|
||
mittBus.on("searchReset", () => {
|
||
isSearching.value = false;
|
||
searchAppText.value = null;
|
||
});
|
||
});
|
||
onUnmounted(() => {
|
||
mittBus.off("searchReset");
|
||
closeMsgWs();
|
||
});
|
||
watch(isSearching, val => {
|
||
if (val) {
|
||
setTimeout(() => refSearchInput.value?.focus(), 300);
|
||
} else {
|
||
// searchAppText.value = null;
|
||
}
|
||
});
|
||
|
||
watch(searchAppText, val => {
|
||
mittBus.emit("searchChange", val);
|
||
});
|
||
function getIsCheckRoles(item: RoleInfo) {
|
||
return isEqual(item, store.curRoleInfo);
|
||
}
|
||
|
||
async function changeRole(role: RoleInfo) {
|
||
if (!!getIsCheckRoles(role)) return;
|
||
setTimeout(() => {
|
||
isRoleInfoShow.value = false;
|
||
}, 150);
|
||
store.setCurRoleInfo(role);
|
||
await BaseApi.savUserRole(role);
|
||
mittBus.emit("switchRole", role);
|
||
await getUserWallpaperList();
|
||
await loadLogoList();
|
||
}
|
||
|
||
function onUserinfoClickRoles() {
|
||
isUserInfoShow.value = false;
|
||
setTimeout(() => {
|
||
isRoleInfoShow.value = true;
|
||
}, 0);
|
||
}
|
||
|
||
async function readMsgAll(type: 1 | 2) {
|
||
await BaseApi.readMsgCenterAll();
|
||
await reloadMsg();
|
||
unreadCount.value = 0;
|
||
if (type == 2) resetMsgPage();
|
||
}
|
||
|
||
async function reloadMsg() {
|
||
const result: any = await BaseApi.getMsgCenterList({ page: 1, count: 10 });
|
||
userMsgList.value = result.list || [];
|
||
}
|
||
|
||
/** 消息类型名(UserMsg msgType:0系统通知/1业务提醒/2审批待办) */
|
||
function msgTypeLabel(msgType: number) {
|
||
return { 0: "系统通知", 1: "业务提醒", 2: "审批待办" }[msgType as 0 | 1 | 2] || "系统通知";
|
||
}
|
||
|
||
/** 点击消息:未读先标读;配了 jumpUrl(如 flow.task 待办)新窗口打开落地页 */
|
||
async function onClickMsg(item: any) {
|
||
if (item.isRead !== 1 && item.msgId) {
|
||
await BaseApi.readMsgCenter(item.msgId);
|
||
item.isRead = 1;
|
||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||
}
|
||
if (item.jumpUrl) window.open(item.jumpUrl, "_blank");
|
||
}
|
||
|
||
// ===== 卡片直批(B3 §2):同意/拒绝待办;operatorId/schId/operatorName 服务端取会话 =====
|
||
const approval = reactive({
|
||
visible: false,
|
||
msgId: 0,
|
||
action: "approve" as "approve" | "reject",
|
||
title: "",
|
||
opinion: "",
|
||
submitting: false,
|
||
});
|
||
|
||
function openApproval(item: any, action: "approve" | "reject") {
|
||
approval.msgId = item.msgId;
|
||
approval.action = action;
|
||
approval.title = item.title ? String(item.title) : "";
|
||
approval.opinion = "";
|
||
approval.visible = true;
|
||
}
|
||
|
||
async function submitApproval() {
|
||
if (approval.submitting) return;
|
||
if (approval.action === "reject" && !approval.opinion.trim()) {
|
||
Notice.warning({ title: "拒绝必须填写意见", duration: 3 });
|
||
return;
|
||
}
|
||
approval.submitting = true;
|
||
try {
|
||
const res: any = await BaseApi.handleMsg(approval.msgId, approval.action, approval.opinion.trim());
|
||
if (res && res.ok) {
|
||
Notice.success({ title: "办理成功", duration: 3 });
|
||
approval.visible = false;
|
||
markRowRead(approval.msgId);
|
||
} else {
|
||
Notice.warning({ title: (res && res.message) || "办理失败", duration: 5 });
|
||
}
|
||
// 成功/失效/被他人处理等场景都同步刷新,让卡片状态跟上服务端
|
||
reloadMsg();
|
||
} catch (e) {
|
||
// flow-center 不可达等:降级为提示,不阻塞消息卡片其余功能
|
||
Notice.error({ title: "流程中心暂不可用,请稍后重试", duration: 5 });
|
||
} finally {
|
||
approval.submitting = false;
|
||
}
|
||
}
|
||
|
||
function markRowRead(msgId: number) {
|
||
const row = userMsgList.value.find((m: any) => m.msgId === msgId);
|
||
if (row && row.isRead !== 1) {
|
||
row.isRead = 1;
|
||
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
||
}
|
||
}
|
||
|
||
function resetMsgPage() {
|
||
refTablePage.value.reloadData();
|
||
}
|
||
function showMsgList() {
|
||
isMsgShow.value = false;
|
||
isMsgListShow.value = true;
|
||
refTablePage.value.reloadData();
|
||
}
|
||
|
||
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"]);
|
||
}
|
||
async function settingsWallpaper() {
|
||
await getUserWallpaperList();
|
||
isUserInfoShow.value = false;
|
||
isWallpaperShow.value = true;
|
||
}
|
||
|
||
async function settingsMenu() {
|
||
isUserInfoShow.value = false;
|
||
refMenuBarModal.value.show();
|
||
}
|
||
|
||
async function settingsStats() {
|
||
isUserInfoShow.value = false;
|
||
refStatsBarModal.value.show();
|
||
}
|
||
|
||
async function settingsTeacherCopilot() {
|
||
isUserInfoShow.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() {
|
||
isUserInfoShow.value = false;
|
||
router.push("/syssetting");
|
||
}
|
||
|
||
function linkBackendManage() {
|
||
isUserInfoShow.value = false;
|
||
const url = `${$conf.baseHost}/userbase/front/index.html`;
|
||
window.open(url, "_blank");
|
||
}
|
||
|
||
async function linkLogoSetting() {
|
||
await loadLogoList();
|
||
isUserInfoShow.value = false;
|
||
isLogoShow.value = true;
|
||
}
|
||
|
||
function showSeachBox() { }
|
||
|
||
function showUserInfo() {
|
||
isUserInfoShow.value = false;
|
||
mittBus.emit("showUserInfoModal");
|
||
}
|
||
|
||
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");
|
||
|
||
::v-deep .ivu-table td {
|
||
height: 32px;
|
||
}
|
||
|
||
/* 卡片直批(B3):待办消息行内「同意/拒绝」入口 */
|
||
.approval-box {
|
||
margin-right: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.approval-link {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
color: #577afb;
|
||
}
|
||
|
||
.approval-link.reject {
|
||
color: #ed4014;
|
||
}
|
||
|
||
.approval-body {
|
||
padding: 4px 2px;
|
||
}
|
||
|
||
.approval-title {
|
||
margin-bottom: 10px;
|
||
color: #515a6e;
|
||
font-weight: 600;
|
||
}
|
||
</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>
|