feat(settings): 设置中心三级重构(一层一角色)+ 审计缺陷修复
build / build (push) Successful in 44s

三级重构(S1-S6,方案见 docs/设置中心三级重构-执行方案-20261008.md):
- Settings.vue 三层化主体:curLayer=my/school/platform 单 pane 分层,主题/头像/壁纸/桌面/LOGO 按身份只渲染本级
- 桌面应用三级默认 pane(used_app SCHOOL/BASE 读写 + 角色分段 + ≤8 上限)+ 统计设置真身迁入(StatsBarModal 退役)
- SysSetting.vue 去基础设置/机构树;ThemePreview 预览壳套 .layout-wrap; UsedAppApi 新增
审计缺陷修复(设置中心三级重构与消息搜索-审计报告-20261008):
- 【P0】DESKTOP_ROLE_TYPES 角色 1/2 标签对齐 SysRoleEnum(1教师/2校领导/3学生),桌面默认不再张冠李戴
- 命令面板:Enter/↑↓ 加 isComposing 守卫防中文输入法误触;过滤后高亮索引归零;stagger 延迟 --i 封顶 20
- WS 通知标题移除 frame.appKey 死消费;BaseApi summary todos 类型同步去 appKey
This commit is contained in:
chenxing
2026-10-08 23:08:16 +08:00
parent 2bd7ef3b80
commit 49f15b8a05
10 changed files with 1233 additions and 1469 deletions
+5 -14
View File
@@ -170,7 +170,6 @@
<!-- 审批直批弹窗(B3 §2)已抽共享组件(消息对接 2026-09-30),供 /msg 页复用;Header 内已无直批入口 -->
<MsgApprovalModal ref="refMsgApproval" />
<StatsBarModal ref="refStatsBarModal" />
<TeacherCopilotModal ref="refTeacherCopilotModal" />
<!-- 搜索命令面板(2026-10-08 定稿 B 方案):必须 Teleport 到 body ——
@@ -231,7 +230,6 @@ 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";
@@ -257,7 +255,6 @@ const baseUrl = computed(() => $conf.fileHost);
* 但摘要拉取与 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) };
@@ -336,13 +333,8 @@ async function connectMsgWs() {
};
}
/** WS 帧按 appKey 匹配应用名(store 应用列表);查无回退类型标签(缺失降级,不报错) */
/** WS 通知标题用类型标签(appKey 属后端签名标识,帧消费已随 0e57022 口径移除) */
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);
}
@@ -414,6 +406,7 @@ onUnmounted(() => {
// 不再 emit("searchChange") —— 背景应用网格保持原样(2026-10-08 用户修订 1)
watch(searchAppText, () => {
cmdkStagger.value = false; // 输入过滤时不重播逐条入场
cmdkIndex.value = 0; // 过滤使结果集变化,高亮回归首项(否则旧索引落空/错位,Enter 哑炮)
});
// ===== 搜索命令面板(2026-10-08 定稿 B 方案)=====
const refCmdkList = ref<any>();
@@ -474,6 +467,9 @@ function pickApp(item: any) {
function onCmdkKeydown(ev: KeyboardEvent) {
if (!isSearching.value) return; // 面板未开:绝不拦截全局按键
// IME 组合态(拼音候选未上屏)不响应导航/确认:keydown 的 Enter/↑↓ 属输入法内部按键,
// 不拦截防误开应用、不 preventDefault 防干扰候选词选择(keyCode 229 为旧式组合态标记)
if (ev.isComposing || ev.keyCode === 229) return;
if (ev.key === "Escape") {
closeSearch();
return;
@@ -542,11 +538,6 @@ function linkSidebarSetting() {
router.push("/settings?pane=sidebar");
}
async function settingsStats() {
isUserCardShow.value = false;
refStatsBarModal.value.show();
}
async function settingsTeacherCopilot() {
isUserCardShow.value = false;
refTeacherCopilotModal.value.show();
-301
View File
@@ -1,301 +0,0 @@
<template>
<Modal
v-model="isTableModalShow"
class-name="menuwapper-modal"
width="1160"
:mask="true"
title="统计模块设置"
:transfer="false"
>
<div class="fix mb-10">
<Button type="primary" size="small" class="fl ml-10 width-10" ghost @click="onEditFormShow()">
新建统计模块
</Button>
</div>
<Table
:columns="menuBarColumns"
:height="360"
:loading="tableLoading"
:data="tableList"
class="link-table"
no-data-text="暂无数据"
>
<template #icon="{ row }">
<img class="icon-box" v-if="!!row.icon" :src="$conf.serviceHost + row.icon" alt="" />
</template>
<template #isShow="{ row }">
<Switch :model-value="row.isShow" true-color="#5979ea" @on-change="onMenuBarShowChange(row.id)" />
</template>
<template #roleIds="{ row }">
<span>{{ row.roleIds.map((item: number) => RoleTypeDict.find(x => x.k == item)?.v).join(",") }}</span>
</template>
<!-- <template #orderNum="{ row }">
<InputNumber :model-value="row.orderNum" :min="1" />
</template> -->
</Table>
<template #footer>
<Button type="primary" ghost @click="isTableModalShow = false">关闭</Button>
</template>
</Modal>
<Modal
v-model="isEditModalShow"
scrollable
width="550"
:title="(!formData.id ? '添加' : '编辑') + '统计模块'"
:transfer="true"
>
<Form ref="refEditForm" class="mr-30" :model="formData" :rules="ruleValidate" :label-width="110">
<Form-Item label="权限角色:" class="fix" prop="roleIds">
<Checkbox
class="fl"
:indeterminate="indeterminate"
:model-value="compCheckAll"
@click.prevent="handleCheckAll"
>
全部
</Checkbox>
<CheckboxGroup v-model="formData.roleIds">
<Checkbox v-for="item in RoleTypeDict" :label="item.k">{{ item.v }}</Checkbox>
</CheckboxGroup>
</Form-Item>
<Form-Item label="名称:" prop="name">
<Input max="4" v-model="formData.name" maxlength="30" placeholder="请输入名称" />
</Form-Item>
<Form-Item label="网址:" prop="url">
<Input v-model="formData.url" placeholder="请输入链接" />
</Form-Item>
<Form-Item label="图标:" prop="icon">
<Upload
:on-success="uploadIconSuccess"
:show-upload-list="false"
:action="$conf.serviceHost + '/api/iconUpload'"
:format="['jpg', 'jpeg', 'png', 'gif']"
accept="image/gif,image/jpeg,image/jpg,image/png"
:with-credentials="true"
:headers="uploadHeaders"
>
<div v-if="!formData.icon" class="icon-upload-wrap">
<SvgIcon name="upload-add" color="#646566" style="width: 20px; height: 20px" />
</div>
<div class="icon-preview-wrap" v-else>
<SvgIcon
@click.stop="formData.icon = null"
class="cloase-btn"
name="widget-close"
size="18"
color="#fff"
/>
<img :src="$conf.serviceHost + formData.icon" alt="" />
</div>
</Upload>
</Form-Item>
<Form-Item label="排序号:" prop="orderNum">
<InputNumber style="width: 120px" v-model="formData.orderNum" :min="1" placeholder="请输入排序号" />
</Form-Item>
<Form-Item label="打开方式:" prop="linkOpenType">
<RadioGroup v-model="formData.linkOpenType" type="button" button-style="solid">
<Radio :label="item.k" v-for="item in linkOpenTypeDict">{{ item.v }}</Radio>
</RadioGroup>
</Form-Item>
</Form>
<template #footer>
<Button type="primary" :loading="editLoading" @click="onEditSave">保存</Button>
<Button @click="isEditModalShow = false">取消</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import StatsBarApi from "@/api/StatsBarApi";
import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeEnum, RoleTypeDict } from "@/types";
import Utils from "@/utils";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { Message } from "view-ui-plus";
import { ref, reactive, computed } from "vue";
import SvgIcon from "@/components/SvgIcon.vue";
import MyConfirm from "@/components/MyConfirm";
import { BaseDict } from "@/types/Basics";
import { find } from "lodash";
interface FormData {
id: string | null;
roleIds: number[];
name: string | null;
url: string | null;
linkOpenType: number;
icon: string | null;
orderNum: number | null;
isShow: boolean;
isEdit: boolean;
}
// 初始化 formData 的默认值
const defaultFormData: FormData = {
id: null,
roleIds: [], // 添加 roleIds 属性
name: "",
url: "",
linkOpenType: 0,
icon: "",
orderNum: null,
isShow: true,
isEdit: true,
};
const { $conf } = useCurrentInstance();
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
const refEditForm = ref<any>(null);
const isTableModalShow = ref(false),
isEditModalShow = ref(false),
tableLoading = ref(false),
editLoading = ref(false),
tableList = ref<any[]>([]),
formData = reactive(defaultFormData);
const ruleValidate = ref({
roleIds: { required: true, type: "array", message: "请选择权限角色", trigger: "change,submit" },
name: { required: true, message: "请输入名称", trigger: "blur" },
url: { required: true, message: "请输入网址", trigger: "blur" },
icon: { required: true, message: "请上传图标", trigger: "change,submit" },
});
const linkOpenTypeDict = computed(() => LinkOpenTypeDict.filter(x => x.k != LinkOpenTypeEnum.Modal));
const compCheckAll = computed(() => formData.roleIds.length == RoleTypeDict.length && RoleTypeDict.length > 0);
const indeterminate = computed(() => formData.roleIds.length > 0 && formData.roleIds.length < RoleTypeDict.length);
const menuBarColumns = ref([
{
title: "名称",
key: "name",
},
{
title: "图标",
slot: "icon",
},
{
title: "角色",
width: 150,
slot: "roleIds",
},
{
title: "地址",
key: "url",
width: 150,
tooltip: true,
},
{
title: "打开方式",
key: "linkType",
},
{
title: "是否显示",
width: 100,
align: "center",
slot: "isShow",
},
{
title: "排序号",
key: "orderNum",
align: "center",
},
{
title: "操作",
width: 120,
align: "center",
render: (h: any, params: any) => {
const disabled = !params.row.isEdit,
id = params.row.id;
return [
Utils.operatingTextHandle(h, "编辑", "mr-2", () => onEditFormShow(id), disabled),
Utils.operatingTextHandle(h, "删除", "text-error", () => onDeleteMenu(id), disabled),
];
},
},
]);
async function show() {
isTableModalShow.value = true;
await getMenuBarList();
console.log("show");
}
async function getMenuBarList() {
tableLoading.value = true;
const list = await StatsBarApi.getStatsBarSchList();
list.forEach(element => {
element.linkType = find(linkOpenTypeDict.value, { k: element.linkOpenType })?.v;
});
tableList.value = list;
tableLoading.value = false;
}
function handleCheckAll() {
formData.roleIds = !compCheckAll.value ? RoleTypeDict.map(item => item.k) : [];
}
function uploadIconSuccess(response: any, file: any, fileListt: any) {
if (response.code == 0) formData.icon = response.data.url;
else Message.error("上传错误!");
// console.log(response, file, fileListt);
}
async function onEditFormShow(id?: number) {
await refEditForm.value?.resetFields();
isEditModalShow.value = false;
if (!id) {
formData.id = null;
formData.linkOpenType = LinkOpenTypeEnum.Open;
} else {
let find = tableList.value.find(x => x.id == id);
Object.assign(formData, find);
}
isEditModalShow.value = true;
}
async function onDeleteMenu(id: number) {
await MyConfirm({
title: "删除",
msg: "确定删除该统计模块吗?",
info: "删除后无法恢复!",
async onOk() {
await StatsBarApi.deleteStatsBar(id);
},
});
await getMenuBarList();
}
async function onEditSave() {
let validate: boolean = await refEditForm.value?.validate();
if (!validate) return false;
editLoading.value = true;
try {
await StatsBarApi.saveStatsBar(formData);
Message.success("操作成功!");
await getMenuBarList();
isEditModalShow.value = false;
} catch (error) {
Message.success("操作失败!");
} finally {
editLoading.value = false;
}
}
function onMenuBarShowChange(id: number) {
StatsBarApi.toggleStatsBarShow(id);
}
defineExpose({
show,
});
</script>
<style scoped>
.icon-box {
width: 20px;
height: 20px;
}
</style>