三级重构(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:
+1
-1
@@ -125,7 +125,7 @@ class Base extends Abstract {
|
||||
const url = path + "/msgCenter/summary";
|
||||
return this.getRequest<{
|
||||
unreadCount: number;
|
||||
todos: { appKey: string; count: number }[];
|
||||
todos: { count: number }[];
|
||||
sysUnread?: number;
|
||||
bizUnread?: number;
|
||||
approvalUnread?: number;
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import Abstract from "@/utils/axios/abstract";
|
||||
import { PageData, PageParams } from "@/types/Basics";
|
||||
|
||||
const path = "/api";
|
||||
|
||||
/** 系统设置-基础设置(机构树 / 壁纸 / Logo 管理视图) */
|
||||
/** 门户外观管理视图(壁纸 / Logo 池;S4.7 后由设置中心「门户外观」pane 调用,机构树随「系统设置-基础设置」一起移除) */
|
||||
class SysSettingApi extends Abstract {
|
||||
/** 机构树(仅超管),已裁剪到学校层 */
|
||||
getSchTree() {
|
||||
const url = path + "/getSchTree";
|
||||
return this.getRequest<any>({ url });
|
||||
}
|
||||
|
||||
/** 壁纸管理列表(系统 + 目标学校,不含个人壁纸) */
|
||||
getSchWallpaperManageList(schId?: number) {
|
||||
const url = path + "/getSchWallpaperManageList";
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import Abstract from "@/utils/axios/abstract";
|
||||
|
||||
const path = "/api";
|
||||
|
||||
/**
|
||||
* 桌面/常用应用三级默认(used_app:BASE 平台 / SCHOOL 本校 / USER 个人)。
|
||||
* 后端 UsedAppController + UsedAppServiceImpl.findById 已实现「用户 > 学校 > 系统」级联与覆盖式保存,
|
||||
* 本封装只服务设置中心「桌面应用」pane(S6),不改后端接口签名。
|
||||
*/
|
||||
class UsedAppApi extends Abstract {
|
||||
/**
|
||||
* 取某角色生效的常用应用(BoaApp 详情,按 appIds 顺序)。
|
||||
* @param roleType 角色(1 校管理员 / 2 教师 / 3 学生)
|
||||
* @param schId 学校ID;0 时后端按登录人主学校→归属组推断
|
||||
* @param isSch 学校级读取:true 且非超管时后端令 uid=schId,于是只回本校 SCHOOL 那一行;
|
||||
* 超管传 true 也会被后端强制回 false(走级联,实际取到 BASE 平台行)
|
||||
*/
|
||||
getUsedApp(roleType: number, schId: number, isSch: boolean) {
|
||||
const url = path + "/usedapp/getUsedApp";
|
||||
return this.getRequest<any[]>({ url, params: { roleType, schId, isSch } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖式保存某角色本级默认桌面(同 setId+type+roleType+schId 存在则更新)。
|
||||
* 数量上限 ≤8 由前端控制(后端注释口径);超管传任何 type 都会被后端强制为 BASE(0)。
|
||||
*/
|
||||
saveUsedApp(params: { appIds: string; type: number; roleType: number; schId?: number }) {
|
||||
const url = path + "/usedapp/saveUsedApp";
|
||||
return this.postRequest({ url, data: params });
|
||||
}
|
||||
}
|
||||
|
||||
export default new UsedAppApi();
|
||||
@@ -154,7 +154,9 @@
|
||||
.app-cmdk-layer.is-open & {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition-delay: calc(0.14s + var(--i) * 0.035s), calc(0.14s + var(--i) * 0.035s), 0s;
|
||||
/* --i 封顶 20:应用多时(100+)末尾条目延迟不再线性膨胀到数秒,滚动空白窗上限 ≈0.84s */
|
||||
transition-delay: calc(0.14s + min(var(--i), 20) * 0.035s),
|
||||
calc(0.14s + min(var(--i), 20) * 0.035s), 0s;
|
||||
}
|
||||
&.is-hl,
|
||||
&:hover {
|
||||
|
||||
@@ -152,6 +152,33 @@ div.dib {
|
||||
}
|
||||
}
|
||||
|
||||
// 分段 Tab(设置中心「桌面应用」角色分段 / 系统设置「应用管理」角色分段共用,替代 iView Tabs 观感)
|
||||
.seg-tabs {
|
||||
display: inline-flex;
|
||||
background: #f0f2f6;
|
||||
border-radius: 9px;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
.sg {
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
border-radius: 7px;
|
||||
color: #7a8088;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: #5979ea;
|
||||
}
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #5979ea;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .chart-box {
|
||||
// height: 50px;
|
||||
// width: 100%;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
+1024
-544
File diff suppressed because it is too large
Load Diff
+116
-553
@@ -1,185 +1,102 @@
|
||||
<template>
|
||||
<div class="syssetting-wrap fix" v-if="isAdmin">
|
||||
<!-- 左侧机构树(仅超管) -->
|
||||
<div v-if="isSuperAdmin" class="left-box fl">
|
||||
<h4 class="tree-title">机构</h4>
|
||||
<Tree :data="treeData" select-node @on-select-change="onTreeSelect" />
|
||||
</div>
|
||||
<div :class="['right-box', isSuperAdmin ? 'ml-315' : '']">
|
||||
<!-- 面包屑:仅超管展示当前选中的机构路径 -->
|
||||
<Breadcrumb v-if="isSuperAdmin" class="sch-crumb">
|
||||
<BreadcrumbItem v-for="(n, i) in currentSchPath" :key="n.id">
|
||||
<span :class="['crumb-item', i === currentSchPath.length - 1 ? 'is-last' : '']">{{ n.title }}</span>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
<Tabs v-model="tabIndex" type="card" class="syssetting-tabs">
|
||||
<TabPane label="基础设置" name="basic">
|
||||
<div class="basic-wrap">
|
||||
<!-- Logo -->
|
||||
<div class="setting-item">
|
||||
<h4 class="item-title">
|
||||
学校Logo
|
||||
<span class="tip">(单张,上传即覆盖;推荐尺寸 70x70)</span>
|
||||
</h4>
|
||||
<div class="logo-uploader">
|
||||
<ul class="logo-ul">
|
||||
<li v-for="(item, index) in logoList" :key="item.id">
|
||||
<label :class="['badge', item.type == 0 ? 'sys' : 'sch']">
|
||||
{{ item.type == 0 ? "系统" : "校" }}
|
||||
</label>
|
||||
<img :src="item.url" alt="" />
|
||||
<div class="operate" v-if="item.isEdit">
|
||||
<a href="javascript:;" class="del" @click="onDeleteLogo(item)">删除</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="add-cell">
|
||||
<Upload
|
||||
class="logo-upload"
|
||||
:on-success="logoSuccess"
|
||||
:show-upload-list="false"
|
||||
:action="logoUploadAction"
|
||||
accept="image/jpeg, image/jpg, image/png, image/gif, image/webp"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div class="add-box add-box--logo">
|
||||
<SvgIcon name="upload-add" color="#939393" style="width: 22px; height: 22px" />
|
||||
<p>上传Logo</p>
|
||||
</div>
|
||||
</Upload>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 壁纸 -->
|
||||
<div class="setting-item">
|
||||
<h4 class="item-title">
|
||||
壁纸
|
||||
<span class="tip">(多张,与 Header「设置壁纸」弹窗数据互通;推荐尺寸 1920x1200)</span>
|
||||
</h4>
|
||||
<ul class="wallpaper-ul">
|
||||
<li v-for="(item, index) in wallpaperList" :key="item.id">
|
||||
<label :class="['badge', item.type == 0 ? 'sys' : 'sch']">
|
||||
{{ item.type == 0 ? "系统" : "校" }}
|
||||
</label>
|
||||
<img :src="item.url" alt="" />
|
||||
<div class="operate" v-if="item.isEdit">
|
||||
<a href="javascript:;" class="del" @click="onDeleteWallpaper(item)">删除</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="uploader">
|
||||
<Upload
|
||||
class="add-wrap tc"
|
||||
:on-success="wallpaperSuccess"
|
||||
:show-upload-list="false"
|
||||
:action="wallpaperUploadAction"
|
||||
accept="image/jpeg, image/jpg, image/png"
|
||||
:with-credentials="true"
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<div class="add-box add-box--wallpaper">
|
||||
<SvgIcon name="upload-add" color="#939393" style="width: 28px; height: 28px" />
|
||||
<p>点击上传图片</p>
|
||||
</div>
|
||||
</Upload>
|
||||
<!-- 应用管理(S4.7:原「基础设置」Tab 的 Logo/壁纸已迁到设置中心「门户外观」,此处只剩应用管理,
|
||||
故去掉外层 Tabs 与只为基础设置服务的机构树/面包屑——应用管理接口不带 schId,跨校无需树) -->
|
||||
<div class="right-box">
|
||||
<div class="app-manage">
|
||||
<!-- 角色分段用全局 .seg-tabs(与设置中心「桌面应用」同一套分段样式,方案 §10「应用管理自定义 Tab」) -->
|
||||
<div class="role-tabs-row fix">
|
||||
<div class="seg-tabs fl">
|
||||
<div
|
||||
v-for="item in roleTypeList"
|
||||
:key="item.k"
|
||||
:class="['sg', { active: roleTypeTab === item.k }]"
|
||||
@click="onRoleTypeChange(item.k)"
|
||||
>{{ item.v }}</div>
|
||||
</div>
|
||||
<Button class="fr" size="small" type="primary" @click="showCategory()">新增应用</Button>
|
||||
</div>
|
||||
<div class="search-wrap fix">
|
||||
<Input
|
||||
placeholder="输入关键字搜索"
|
||||
class="fr"
|
||||
search
|
||||
suffix="ios-search"
|
||||
style="width: 200px"
|
||||
@on-search="onSearch"
|
||||
v-model="dataParmas.name"
|
||||
/>
|
||||
</div>
|
||||
<TablePage
|
||||
:table-height="tableHeight"
|
||||
:data-api="tableDataApi"
|
||||
:columns="columns"
|
||||
:page-params="pageParams"
|
||||
:auto-load="true"
|
||||
ref="refTablePage"
|
||||
/>
|
||||
<!-- 新增/编辑应用 -->
|
||||
<Modal
|
||||
v-model="isModal"
|
||||
:mask-closable="false"
|
||||
width="600"
|
||||
:title="currentId == null ? '新增应用' : '编辑应用'"
|
||||
>
|
||||
<Form ref="refFormData" class="mt-8 mr-24" :model="formData" :rules="ruleValidate" :label-width="120">
|
||||
<FormItem label="应用名称:" prop="name">
|
||||
<Input placeholder="请输入应用名称" v-model="formData.name" />
|
||||
</FormItem>
|
||||
<FormItem label="接口地址:" prop="apiUrl">
|
||||
<Input placeholder="请输入接口地址" v-model="formData.apiUrl" />
|
||||
</FormItem>
|
||||
<FormItem label="接口参数:" prop="paramsKey">
|
||||
<ul class="parmas-ul">
|
||||
<li class="fix mb-12" v-for="(item, index) in parmasList" :key="index">
|
||||
<span class="ivu-form-item-label">参数名称</span>
|
||||
<Input class="fl mr-12" v-model="item.k" style="width: 80px" />
|
||||
<template v-if="item.type == 1">
|
||||
<span class="ivu-form-item-label">参数类型</span>
|
||||
<span>userId</span>
|
||||
</template>
|
||||
<template v-else-if="item.type == 2">
|
||||
<span class="ivu-form-item-label">参数类型</span>
|
||||
<span>学校Id</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="ivu-form-item-label">参数定值</span>
|
||||
<Input class="fl mr-12" style="width: 80px" v-model="item.v" />
|
||||
</template>
|
||||
<Button
|
||||
v-if="item.type != 1"
|
||||
type="error"
|
||||
ghost
|
||||
icon="md-remove"
|
||||
class="ml-12"
|
||||
@click="parmasList.splice(index, 1)"
|
||||
></Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane label="应用管理" name="app">
|
||||
<div class="app-manage">
|
||||
<Tabs v-model="roleTypeTab" @on-click="onRoleTypeChange">
|
||||
<TabPane :label="item.v" :name="item.k + ''" v-for="item in roleTypeList" :key="item.k" />
|
||||
<template #extra>
|
||||
<Button class="fl" size="small" type="primary" @click="showCategory()">新增应用</Button>
|
||||
</template>
|
||||
</Tabs>
|
||||
<div class="search-wrap fix">
|
||||
<Input
|
||||
placeholder="输入关键字搜索"
|
||||
class="fr"
|
||||
search
|
||||
suffix="ios-search"
|
||||
style="width: 200px"
|
||||
@on-search="onSearch"
|
||||
v-model="dataParmas.name"
|
||||
/>
|
||||
</div>
|
||||
<TablePage
|
||||
:table-height="tableHeight"
|
||||
:data-api="tableDataApi"
|
||||
:columns="columns"
|
||||
:page-params="pageParams"
|
||||
:auto-load="true"
|
||||
ref="refTablePage"
|
||||
/>
|
||||
<!-- 新增/编辑应用 -->
|
||||
<Modal
|
||||
v-model="isModal"
|
||||
:mask-closable="false"
|
||||
width="600"
|
||||
:title="currentId == null ? '新增应用' : '编辑应用'"
|
||||
>
|
||||
<Form ref="refFormData" class="mt-8 mr-24" :model="formData" :rules="ruleValidate" :label-width="120">
|
||||
<FormItem label="应用名称:" prop="name">
|
||||
<Input placeholder="请输入应用名称" v-model="formData.name" />
|
||||
</FormItem>
|
||||
<FormItem label="接口地址:" prop="apiUrl">
|
||||
<Input placeholder="请输入接口地址" v-model="formData.apiUrl" />
|
||||
</FormItem>
|
||||
<FormItem label="接口参数:" prop="paramsKey">
|
||||
<ul class="parmas-ul">
|
||||
<li class="fix mb-12" v-for="(item, index) in parmasList" :key="index">
|
||||
<span class="ivu-form-item-label">参数名称</span>
|
||||
<Input class="fl mr-12" v-model="item.k" style="width: 80px" />
|
||||
<template v-if="item.type == 1">
|
||||
<span class="ivu-form-item-label">参数类型</span>
|
||||
<span>userId</span>
|
||||
</template>
|
||||
<template v-else-if="item.type == 2">
|
||||
<span class="ivu-form-item-label">参数类型</span>
|
||||
<span>学校Id</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="ivu-form-item-label">参数定值</span>
|
||||
<Input class="fl mr-12" style="width: 80px" v-model="item.v" />
|
||||
</template>
|
||||
<Button
|
||||
v-if="item.type != 1"
|
||||
type="error"
|
||||
ghost
|
||||
icon="md-remove"
|
||||
class="ml-12"
|
||||
@click="parmasList.splice(index, 1)"
|
||||
></Button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="fix">
|
||||
<Button class="fl" @click="parmasList.push({ k: '', v: '', type: 3 })">添加固定参数</Button>
|
||||
<Button
|
||||
v-if="!!currentId"
|
||||
class="ml-8"
|
||||
@click="parmasList.push({ k: 'schId', v: '', type: 2 })"
|
||||
>
|
||||
添加SchId参数
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem label="数据解析:" prop="dataKey">
|
||||
<Input placeholder="请输入数据字段" v-model="formData.dataKey" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="isSubmitLoading" @click="onEditSave">提交</Button>
|
||||
<Button @click="onReset">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<div class="fix">
|
||||
<Button class="fl" @click="parmasList.push({ k: '', v: '', type: 3 })">添加固定参数</Button>
|
||||
<Button
|
||||
v-if="!!currentId"
|
||||
class="ml-8"
|
||||
@click="parmasList.push({ k: 'schId', v: '', type: 2 })"
|
||||
>
|
||||
添加SchId参数
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem label="数据解析:" prop="dataKey">
|
||||
<Input placeholder="请输入数据字段" v-model="formData.dataKey" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="isSubmitLoading" @click="onEditSave">提交</Button>
|
||||
<Button @click="onReset">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-permission" v-else>无权限访问</div>
|
||||
@@ -189,126 +106,19 @@
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import TablePage from "@/components/TablePage.vue";
|
||||
import Utils from "@/utils";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { PageParams } from "@/types/Basics";
|
||||
import SysSettingApi from "@/api/SysSettingApi";
|
||||
import AppSettingApi, { AppCategoryItem } from "@/api/AppSettingApi";
|
||||
import mittBus from "@/utils/mitt";
|
||||
|
||||
const router = useRouter();
|
||||
const store = useDefaultStore();
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
|
||||
|
||||
const isSuperAdmin = computed(() => store.userInfo.type == 1);
|
||||
const isAdmin = computed(() => store.getIsAdmin);
|
||||
|
||||
const tabIndex = ref("basic");
|
||||
const currentSchId = ref<number>(0);
|
||||
|
||||
const treeData = ref<any[]>([
|
||||
{ id: 0, title: "平台默认设置", selected: true },
|
||||
]);
|
||||
|
||||
// 当前选中的机构路径(面包屑),默认平台默认设置
|
||||
const currentSchPath = ref<{ id: number; title: string }[]>([
|
||||
{ id: 0, title: "平台默认设置" },
|
||||
]);
|
||||
|
||||
function findPathInTree(list: any[], id: number): { id: number; title: string }[] | null {
|
||||
for (const n of list || []) {
|
||||
if (n.id === id) return [{ id: n.id, title: n.title }];
|
||||
if (n.children && n.children.length) {
|
||||
const sub = findPathInTree(n.children, id);
|
||||
if (sub) return [{ id: n.id, title: n.title }, ...sub];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function onTreeSelect(nodes: any[]) {
|
||||
if (!nodes || nodes.length == 0) return;
|
||||
const node = nodes[0];
|
||||
currentSchId.value = node.id;
|
||||
if (node.id === 0) {
|
||||
currentSchPath.value = [{ id: 0, title: "平台默认设置" }];
|
||||
} else {
|
||||
const path = findPathInTree(treeData.value, node.id);
|
||||
currentSchPath.value = path && path.length ? path : [{ id: node.id, title: node.title }];
|
||||
}
|
||||
loadBasicData();
|
||||
}
|
||||
|
||||
function mapTree(list: any[]): any[] {
|
||||
return (list || []).map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
children: item.children && item.children.length ? mapTree(item.children) : [],
|
||||
}));
|
||||
}
|
||||
|
||||
function getTreeData() {
|
||||
SysSettingApi.getSchTree()
|
||||
.then((resp: any) => {
|
||||
treeData.value = [{ id: 0, title: "平台默认设置", selected: true }, ...mapTree(resp.children)];
|
||||
})
|
||||
.catch(() => {
|
||||
treeData.value = [{ id: 0, title: "平台默认设置", selected: true }];
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 基础设置(Logo + 壁纸) ====================
|
||||
|
||||
const logoList = ref<any[]>([]);
|
||||
const wallpaperList = ref<any[]>([]);
|
||||
|
||||
const logoUploadAction = computed(() => {
|
||||
let url = $conf.serviceHost + "/api/logoUpload";
|
||||
if (isSuperAdmin.value && currentSchId.value > 0) url += "?schId=" + currentSchId.value;
|
||||
return url;
|
||||
});
|
||||
|
||||
const wallpaperUploadAction = computed(() => {
|
||||
let url = $conf.serviceHost + "/api/wallpaperUpload";
|
||||
if (isSuperAdmin.value && currentSchId.value > 0) url += "?schId=" + currentSchId.value;
|
||||
return url;
|
||||
});
|
||||
|
||||
async function loadBasicData() {
|
||||
const [walls, logos] = await Promise.all([
|
||||
SysSettingApi.getSchWallpaperManageList(isSuperAdmin.value ? currentSchId.value : undefined),
|
||||
SysSettingApi.getSchLogoManageList(isSuperAdmin.value ? currentSchId.value : undefined),
|
||||
]);
|
||||
wallpaperList.value = walls || [];
|
||||
logoList.value = logos || [];
|
||||
}
|
||||
|
||||
function wallpaperSuccess() {
|
||||
loadBasicData();
|
||||
}
|
||||
|
||||
function logoSuccess() {
|
||||
loadBasicData();
|
||||
}
|
||||
|
||||
async function onDeleteWallpaper(item: any) {
|
||||
await SysSettingApi.deleteWallById(item.id);
|
||||
Message.success("删除成功");
|
||||
loadBasicData();
|
||||
}
|
||||
|
||||
async function onDeleteLogo(item: any) {
|
||||
await SysSettingApi.deleteLogoById(item.id);
|
||||
Message.success("删除成功");
|
||||
loadBasicData();
|
||||
}
|
||||
|
||||
// ==================== 应用管理(AppCategory) ====================
|
||||
|
||||
const roleTypeList = [
|
||||
@@ -317,7 +127,7 @@ const roleTypeList = [
|
||||
{ k: 3, v: "学生" },
|
||||
];
|
||||
|
||||
const roleTypeTab = ref("1");
|
||||
const roleTypeTab = ref<number>(1);
|
||||
const dataParmas = reactive({ name: "" });
|
||||
const pageParams: PageParams = reactive({ page: 1, count: 10 });
|
||||
const refTablePage = ref<any>(null);
|
||||
@@ -326,7 +136,7 @@ const tableHeight = Utils.useTableHeight({ isSearch: true, isTabs: true, isPage:
|
||||
const tableDataApi = (params: PageParams) =>
|
||||
AppSettingApi.getAppCategoryList({
|
||||
...params,
|
||||
roleType: Number(roleTypeTab.value),
|
||||
roleType: roleTypeTab.value,
|
||||
name: dataParmas.name || undefined,
|
||||
});
|
||||
|
||||
@@ -379,7 +189,10 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function onRoleTypeChange() {
|
||||
/** 分段 Tab 点击(原 iView @on-click 改为自绘分段,值即 roleType) */
|
||||
function onRoleTypeChange(k: number) {
|
||||
if (roleTypeTab.value === k) return;
|
||||
roleTypeTab.value = k;
|
||||
refTablePage.value?.reloadData();
|
||||
}
|
||||
|
||||
@@ -416,7 +229,7 @@ async function onEditSave() {
|
||||
}
|
||||
isSubmitLoading.value = true;
|
||||
try {
|
||||
const item: AppCategoryItem = { ...formData, roleType: Number(roleTypeTab.value) };
|
||||
const item: AppCategoryItem = { ...formData, roleType: roleTypeTab.value };
|
||||
await AppSettingApi.saveAppCategory(item);
|
||||
Message.success(`${currentId.value == null ? "添加" : "操作"}成功!`);
|
||||
refTablePage.value.reloadData(currentId.value == null ? OperatorEnum.add : OperatorEnum.edit);
|
||||
@@ -448,22 +261,14 @@ async function onDeleteCategory(id: number) {
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
async function onSwitchRole() {
|
||||
/** 切换身份/角色后重取应用列表(首屏由 TablePage 的 auto-load 负责) */
|
||||
function onSwitchRole() {
|
||||
if (!isAdmin.value) return;
|
||||
if (isSuperAdmin.value) {
|
||||
currentSchId.value = 0;
|
||||
getTreeData();
|
||||
}
|
||||
await loadBasicData();
|
||||
refTablePage.value?.reloadData();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
if (!isAdmin.value) return;
|
||||
if (isSuperAdmin.value) {
|
||||
getTreeData();
|
||||
}
|
||||
await loadBasicData();
|
||||
mittBus.on("switchRole", onSwitchRole);
|
||||
});
|
||||
|
||||
@@ -477,21 +282,6 @@ onUnmounted(() => {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
|
||||
.left-box {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
|
||||
.tree-title {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
@@ -501,36 +291,6 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.ml-315 {
|
||||
margin-left: 315px;
|
||||
}
|
||||
}
|
||||
|
||||
.sch-crumb {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
|
||||
.crumb-item {
|
||||
&.is-last {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ivu-breadcrumb-item-separator) {
|
||||
margin: 0 8px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.syssetting-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,217 +302,20 @@ onUnmounted(() => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.basic-wrap {
|
||||
height: calc(100% - 44px);
|
||||
overflow-y: auto;
|
||||
padding: 18px 24px 24px;
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 28px;
|
||||
|
||||
& + .setting-item {
|
||||
padding-top: 20px;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
margin: 0 0 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 22px;
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-uploader {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
&.add-cell {
|
||||
padding: 0;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wallpaper-ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
height: 140px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&.uploader {
|
||||
padding: 0;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 强约束 view-ui-plus Upload 组件渲染层都撑满 li
|
||||
li.uploader :deep(.ivu-upload),
|
||||
li.uploader :deep(.ivu-upload-select) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
z-index: 2;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 18px;
|
||||
|
||||
&.sys {
|
||||
background: #5979ea;
|
||||
}
|
||||
|
||||
&.sch {
|
||||
background: #45b972;
|
||||
}
|
||||
}
|
||||
|
||||
.operate {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
z-index: 2;
|
||||
|
||||
a.del {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
background: #fafbfc;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, color 0.2s, background-color 0.2s;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #5979ea;
|
||||
color: #5979ea;
|
||||
background: #f3f6fd;
|
||||
}
|
||||
|
||||
&--logo {
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
&--wallpaper {
|
||||
width: 250px;
|
||||
height: 140px;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-upload {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 应用管理:.right-box 是纵向 flex,此处占满剩余高度(原外层 Tabs 的 44px 头部已随「基础设置」Tab 一起移除) */
|
||||
.app-manage {
|
||||
height: calc(100% - 44px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.role-tabs-row {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.fr {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
+23
-47
@@ -3,11 +3,12 @@
|
||||
只在本路由的 document 内加载通道(组件挂载时动态 append init 脚本),
|
||||
档位类与通道 CSS 只落在本 document 的 <html> 上——UserHome 本体(父页)永远不吃通道 CSS。
|
||||
纯展示:根容器 pointer-events:none,不渲染搜索区/分页操作/ThemeSwitcher。
|
||||
DOM 结构对齐 Notice layout(header-container / left-menu / path-nav / main-container),
|
||||
观感(配色/侧栏形态)由通道 base.layout.css 按 URL 覆盖参数(?yh-theme= 等)驱动的档位类提供。
|
||||
默认头像轮(2026-09-30):logo 换云海图形、用户胶囊补三态头像圆 + icon-logout、
|
||||
右侧补静态配色胶囊、左菜单换 iconfont + 可展开子菜单(预览 markup 落后于通道皮肤修复)。 -->
|
||||
<div class="theme-preview-root">
|
||||
DOM 结构逐层对齐 Notice(layout.vue + App.vue):.layout-wrap 作用域内放
|
||||
header-container / left-menu / path-nav,内容区用 .base-wrap-main > .main-wrapper > .main-container;
|
||||
通道皮肤(skin.src.css)的侧栏/顶栏/面包屑规则全部以 .layout-wrap 为祖先,故根必须挂该类,
|
||||
侧栏深色/浅色/跟随顶栏、渐变Header 这些档位令牌(html.side-deep/.grad-on…)才会被消费到。
|
||||
观感(配色、侧栏形态、内容区卡片)由通道按 URL 覆盖参数(?yh-theme= 等)驱动的档位提供,本页不写死。 -->
|
||||
<div class="theme-preview-root layout-wrap">
|
||||
<header class="header-container fix">
|
||||
<span class="logo-badge fl"><img src="@/assets/image/yunhai-logo.svg" alt="云海" /></span>
|
||||
<div class="delimiter fl align-middle"></div>
|
||||
@@ -20,8 +21,7 @@
|
||||
<a href="javascript:;" class="logout fl align-middle" title="退出登陆"><i class="iconfont icon-logout"></i></a>
|
||||
</div>
|
||||
<!-- 配色胶囊:静态展示(不接 ThemeSwitcher,避免预览里出现可点控件与真实切换器语义混淆)。
|
||||
置于 header-aside 之后:本页不吃 .layout-wrap 皮肤(flex order 失效、走 float),
|
||||
float:right 的 DOM 靠后者居左——才能复刻 Notice「配色在左、用户胶囊在右」 -->
|
||||
顶栏是 flex,皮肤给 .theme-cap{order:1} / .header-aside{order:2} → 胶囊恒落在用户胶囊左侧。 -->
|
||||
<div class="theme-cap fr"><span class="cap-btn">配色 ▾</span></div>
|
||||
</header>
|
||||
<div class="left-menu">
|
||||
@@ -44,10 +44,12 @@
|
||||
<div class="path-nav">
|
||||
<span>首页</span><span class="crumb-sep">/</span><span>通知管理</span><span class="crumb-sep">/</span><span>通知列表</span>
|
||||
</div>
|
||||
<div class="preview-main">
|
||||
<div class="main-container">
|
||||
<h3 class="panel-title">通知列表</h3>
|
||||
<Table :columns="columns" :data="rows" size="small" :border="true" />
|
||||
<div class="base-wrap-main">
|
||||
<div class="main-wrapper">
|
||||
<div class="main-container">
|
||||
<h3 class="panel-title">通知列表</h3>
|
||||
<Table :columns="columns" :data="rows" size="small" :border="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,25 +93,17 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 本页只补最小布局;配色/圆角/阴影等观感全部由通道 base.layout.css 的档位类驱动 */
|
||||
/* 本页只补"铺满 iframe + 纯展示"这层壳;配色、顶栏/侧栏/面包屑观感、内容区卡片全部交给通道
|
||||
(骨架 skeleton.src.css 出结构、皮肤 skin.src.css 出观感,皮肤规则以 .layout-wrap 为祖先——
|
||||
故根元素同时挂 .layout-wrap,档位令牌变化才会实时反映到预览上)。 */
|
||||
.theme-preview-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none; /* 纯展示:整页不可交互 */
|
||||
background: #f5f6f8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
h4 {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 配色胶囊(静态展示):观感对齐 ThemeSwitcher .theme-btn(通道皮肤只给 .theme-cap 排版,胶囊样式在切换器组件内) */
|
||||
.cap-btn {
|
||||
display: flex;
|
||||
@@ -124,35 +118,17 @@ onMounted(() => {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
/* 菜单图标(通道下发的 ysch-icons 掩码类)与文字间距(Notice 同款覆盖) */
|
||||
.left-menu a .iconfont {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 子菜单缩进:预览页不吃 .layout-wrap 前缀的皮肤层,骨架 a 无缩进差,补出层级(对齐皮肤 24px 缩进) */
|
||||
.left-menu .child-item-wrap a {
|
||||
padding-left: 59px;
|
||||
}
|
||||
|
||||
/* 面包屑分隔符:预览用纯 span 复刻(Notice 侧是 iView Breadcrumb,皮肤只作用于 .ivu-breadcrumb-*) */
|
||||
.path-nav .crumb-sep {
|
||||
margin: 0 8px;
|
||||
color: #c5c8cd;
|
||||
}
|
||||
|
||||
.preview-main {
|
||||
position: fixed;
|
||||
top: calc(var(--topbar-h, 52px) + var(--crumb-h, 44px));
|
||||
left: var(--side-w, 200px);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 14px 24px 24px;
|
||||
overflow: auto;
|
||||
|
||||
.panel-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2c3138;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
/* 内容卡标题:通道未定义,本页补最小排版(色值取通道文本色变量,未加载时回退) */
|
||||
.panel-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #2c3138);
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user