feat(settings): 五个 pane 对接真实数据(零新接口)——profile 读 store、改密登出流程、头像直传、logo/wallpaper 管理列表

- profile:姓名/工号(暂以登录账号展示,接口无独立工号字段)/部门/手机/邮箱改读 store.userInfo(无值显示 -),保存按钮占位 disabled(接口待 UserBase 支持本人自助修改,拍板搁置);头像行 v-real-img 真实图
- password:非空/一致性/不同旧密码/长度校验 + BaseApi.updatePassword,成功提示后 1500ms 登出跳 CAS(确认登出流程照抄 UserInfoModal L162-183,不自创)
- avatar:Upload 直传移植 UserInfoModal(action=/api/avatarUpload、accept/format/max-size 20480/with-credentials/网关头/成功写 store.profilePicture 照抄);预设色块保留、「使用默认头像」disabled 占位
- logo:getSchLogoManageList 管理列表(不带 schId,按登录人归属校返回;超管按机构切换走 sys pane)+ 直传 /api/logoUpload + deleteLogoById + 「保存设置」走 saveSchLogo(schId 后端按登录人推断)
- wallpaper:getSchWallpaperManageList + 直传 + deleteWallById + 「应用壁纸」采用 checkWallpaper 个人壁纸语义(与 Header 壁纸弹窗现状一致,执行报告写明;校级默认壁纸保存接口不存在,不发明新语义)
- BaseApi 补 saveSchLogo 封装(复用已有后端接口);sidebar/statset/sys/backend/noperm 五个不动 pane 模板 diff 为空;navGroups/ADMIN_PANES 权限结构未动
This commit is contained in:
chenxing
2026-09-30 12:43:27 +08:00
parent 0f7101983e
commit 0ded3cce9f
2 changed files with 281 additions and 68 deletions
+9
View File
@@ -172,6 +172,15 @@ class Base extends Abstract {
return this.postRequest({ url, data: { oldPassword, password } });
}
/**
* 保存校级 LOGO(schId 后端按登录人推断:主学校→归属组→0 平台默认)
* @memberof Basic
*/
saveSchLogo(logoUrl: string) {
const url = path + "/saveSchLogo";
return this.postRequest({ url, params: { logoUrl } });
}
savUserRole(info: any) {
const url = path + "/user/savUserRole";
return this.postRequest({ url, data: info as any });
+272 -68
View File
@@ -49,28 +49,29 @@
<div class="form-row">
<span class="lb">头像</span>
<div class="ctl">
<div class="avatar-now avatar-sm">张</div>
<img class="avatar-img avatar-img-sm" :src="requireImg('user-avatar-default.png')"
v-real-img="store.userInfo.profilePicture" alt="" />
<button class="btn ghost sm" @click="setPane('avatar')">更换头像</button>
</div>
</div>
<div class="form-row">
<span class="lb">姓名</span>
<div class="ctl">
<input class="inp" value="张明" disabled />
<input class="inp" :value="profileView.name" disabled />
<span class="hint">由学校统一维护</span>
</div>
</div>
<div class="form-row">
<span class="lb">工号</span>
<div class="ctl">
<input class="inp" value="T20240315" disabled />
<input class="inp" :value="profileView.account" disabled />
<span class="hint">由学校统一维护</span>
</div>
</div>
<div class="form-row">
<span class="lb">所属部门</span>
<div class="ctl">
<input class="inp" value="教务处 / 教研组" disabled />
<input class="inp" :value="profileView.deptName" disabled />
</div>
</div>
<div class="form-row">
@@ -87,7 +88,8 @@
</div>
</div>
<div class="pane-foot">
<button class="btn primary">保存修改</button>
<!-- 保存占位:接口待 UserBase 支持本人自助修改,本轮不做(拍板结论) -->
<button class="btn primary" disabled title="接口待 UserBase 支持本人自助修改,本轮暂不可用">保存修改</button>
<button class="btn plain" @click="resetProfileForm">重置</button>
</div>
</div>
@@ -120,7 +122,7 @@
</div>
</div>
<div class="pane-foot">
<button class="btn primary">确认修改</button>
<button class="btn primary" :disabled="passwordSubmitting" @click="onUpdatePassword">确认修改</button>
</div>
</div>
@@ -134,13 +136,22 @@
<h4>当前头像</h4>
<div class="avatar-pad">
<div class="avatar-box">
<div class="avatar-now">张</div>
<img class="avatar-img" :src="requireImg('user-avatar-default.png')"
v-real-img="store.userInfo.profilePicture" alt="" />
<div class="avatar-ops">
<div class="avatar-btns">
<button class="btn primary sm">上传新头像</button>
<button class="btn plain sm">使用默认头像</button>
<!-- 直传逻辑移植 UserInfoModal:action/headers/校验/成功回调照抄 -->
<Upload :on-success="uploadAvatarSuccess" :on-exceeded-size="onAvatarSizeExceeded"
:on-format-error="onAvatarFormatError" :show-upload-list="false"
:action="$conf.serviceHost + '/api/avatarUpload'"
accept=".jpg,.jpeg,.png,.gif,.webp" :format="['jpg', 'jpeg', 'png', 'gif', 'webp']"
:max-size="20480" :with-credentials="true" :headers="uploadHeaders">
<button class="btn primary sm">上传新头像</button>
</Upload>
<!-- 存储方案本轮不做(拍板),保持占位 -->
<button class="btn plain sm" disabled title="存储方案本轮不做">使用默认头像</button>
</div>
<span class="tip">上传后立即生效,全站同步展示。</span>
<span class="tip">上传后立即生效,全站同步展示。支持 JPG / PNG / GIF / WebP,≤ 20MB。</span>
</div>
</div>
</div>
@@ -170,25 +181,31 @@
<div class="sec-card">
<h4>当前 LOGO</h4>
<div class="logo-grid">
<div v-if="logoManageList.length === 0" class="logo-empty">暂无 LOGO,请先上传</div>
<div
v-for="(l, i) in logoList"
:key="i"
:class="['logo-item', { sel: logoSel === i }]"
@click="logoSel = i"
v-for="l in logoManageList"
:key="l.id"
:class="['logo-item', { sel: logoSelUrl === l.url }]"
@click="logoSelUrl = l.url"
>
<span class="mk">{{ l.marks }}</span>{{ l.name }}
<img :src="l.url" alt="" />
<i v-if="l.isEdit" class="del" title="删除" @click.stop="onDeleteLogo(l)">×</i>
</div>
</div>
<div class="form-row form-row--last form-row--top">
<span class="lb">上传 LOGO</span>
<div class="ctl">
<button class="btn ghost sm">选择文件</button>
<Upload :on-success="onLogoUploaded" :show-upload-list="false" :action="logoUploadAction"
accept="image/jpeg, image/jpg, image/png, image/gif, image/webp"
:with-credentials="true" :headers="uploadHeaders">
<button class="btn ghost sm">选择文件</button>
</Upload>
<span class="hint">建议 PNG 透明底,高度 ≥ 64px,≤ 1MB</span>
</div>
</div>
</div>
<div class="pane-foot">
<button class="btn primary">保存设置</button>
<button class="btn primary" :disabled="logoSaving" @click="onSaveLogo">保存设置</button>
</div>
</div>
@@ -201,24 +218,33 @@
<div class="sec-card">
<h4>系统壁纸</h4>
<div class="wall-grid">
<div v-if="wallpaperManageList.length === 0" class="logo-empty">暂无壁纸,请先上传</div>
<div
v-for="(w, i) in wallpaperList"
:key="i"
:class="['wall-item', { sel: wallSel === i }]"
:style="{ background: w }"
@click="wallSel = i"
v-for="w in wallpaperManageList"
:key="w.id"
:class="['wall-item', { sel: wallSelUrl === w.url }]"
:style="{ background: 'center / cover no-repeat url(' + w.url + ')' }"
@click="wallSelUrl = w.url"
>
<i v-if="w.isEdit" class="del" title="删除" @click.stop="onDeleteWallpaper(w)">×</i>
<i class="tick">✓</i>
</div>
</div>
<div class="form-row form-row--last form-row--top">
<span class="lb">上传壁纸</span>
<div class="ctl">
<button class="btn ghost sm">选择文件</button>
<Upload :on-success="onWallpaperUploaded" :show-upload-list="false" :action="wallpaperUploadAction"
accept="image/jpeg, image/jpg, image/png" :with-credentials="true" :headers="uploadHeaders">
<button class="btn ghost sm">选择文件</button>
</Upload>
<span class="hint">建议 1920×1080 以上,≤ 5MB</span>
</div>
</div>
</div>
<div class="pane-foot">
<!-- 保存语义=设为我的壁纸(checkWallpaper,与 Header 壁纸弹窗一致) -->
<button class="btn primary" :disabled="wallApplying" @click="onApplyWallpaper">应用壁纸</button>
</div>
</div>
<!-- 边栏设置(管理员) -->
@@ -291,11 +317,22 @@ import { ref, reactive, computed, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useDefaultStore } from "@/store";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { requireImg } from "@/utils/hook/useImg";
import SysSetting from "@/views/SysSetting.vue";
import BaseApi from "@/api/BaseApi";
import SysSettingApi from "@/api/SysSettingApi";
import UserSettingApi from "@/api/UserSettingApi";
import Utils from "@/utils";
import { Message } from "view-ui-plus";
/**
* 设置中心(/settings):左 216px 分组导航卡 + 右内容卡(B 方案左右双卡)。
* 本轮只做导航壳 + pane 切换 + 背景层,各 pane 内表单均为占位(接口对接属下一轮)。
* 五个 pane 已对接真实数据(设置对接 2026-09-30,零新接口):
* profile 读 store.userInfo(保存占位——接口待 UserBase 支持本人自助修改,本轮不做);
* password 走 updatePassword + 确认登出流程(照抄 UserInfoModal);
* avatar 移植 UserInfoModal 直传上传(预设色块/「使用默认头像」保持占位);
* logo/wallpaper 管理列表 + 直传 + 删除照抄 SysSetting.vue,LOGO 保存走 saveSchLogo(schId 后端按登录人推断),
* 「应用壁纸」采用 checkWallpaper 个人壁纸语义(与 Header 壁纸弹窗一致,执行报告已写明)。
*/
const route = useRoute();
@@ -366,13 +403,91 @@ const navGroups = computed(() => {
return isAdmin.value ? groups : groups.filter(g => !g.adminOnly);
});
/* ===== 以下均为 pane 占位数据(真实表单/接口对接属下一轮) ===== */
const profileForm = reactive({ mobile: "138****8899", email: "zhangming@school.edu.cn" });
/* ===== profile:读 store.userInfo(真实展示;保存占位) ===== */
const profileForm = reactive({ mobile: "", email: "" });
/** userInfo 派生展示(手机/邮箱编辑值初始同源;工号暂以登录账号展示——接口未提供独立工号字段) */
const profileView = computed(() => {
const u: any = store.userInfo || {};
return {
name: u.name || "-",
account: u.account || "-",
deptName: u.deptName || u.dept || "-",
phone: u.phone || "",
email: u.email || "",
};
});
watch(
() => store.userInfo,
() => {
profileForm.mobile = profileView.value.phone;
profileForm.email = profileView.value.email;
},
{ immediate: true },
);
function resetProfileForm() {
profileForm.mobile = "138****8899";
profileForm.email = "zhangming@school.edu.cn";
profileForm.mobile = profileView.value.phone;
profileForm.email = profileView.value.email;
}
/** 保存占位:接口待 UserBase 支持本人自助修改,本轮不做(拍板结论) */
/* ===== password:updatePassword + 确认登出(流程照抄 UserInfoModal) ===== */
const passwordForm = reactive({ old: "", next: "", confirm: "" });
const passwordSubmitting = ref(false);
async function onUpdatePassword() {
if (!passwordForm.old || !passwordForm.next) {
Message.warning("请填写原密码与新密码");
return;
}
if (passwordForm.next !== passwordForm.confirm) {
Message.warning("两次输入的新密码不一致");
return;
}
if (passwordForm.next === passwordForm.old) {
Message.warning("新密码不能与旧密码一致");
return;
}
if (passwordForm.next.length < 8) {
Message.warning("密码长度不能小于8位");
return;
}
if (passwordSubmitting.value) return;
passwordSubmitting.value = true;
try {
await BaseApi.updatePassword(passwordForm.old, passwordForm.next);
Message.success("操作成功!");
// 改密后旧会话失效,引导重新登录(照抄 UserInfoModal 确认登出流程)
setTimeout(logout, 1500);
} catch (error) {
Message.error("操作失败:" + error);
} finally {
passwordSubmitting.value = false;
}
}
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;
}
/* ===== avatar:移植 UserInfoModal 直传上传(预设色块/「使用默认头像」保持占位) ===== */
// Upload 组件直传不经 axios 封装,需手动补网关校验头(照抄 UserInfoModal)
const uploadHeaders = { yschapirequest: 1, yschreferer: Utils.strToHexCharCode(window.location.href) };
function uploadAvatarSuccess(response: any) {
if (response.code == 0 && response.data && response.data.url) {
store.userInfo.profilePicture = response.data.url;
Message.success("头像更新成功!");
} else {
Message.error("头像更新失败:" + (response.msg || "未知错误"));
}
}
function onAvatarSizeExceeded() {
Message.error("图片大小不能超过20MB!");
}
function onAvatarFormatError() {
Message.error("仅支持 jpg/jpeg/png/gif/webp 格式图片!");
}
const avatarPresets = [
"linear-gradient(135deg,#5979ea,#7d9bf5)",
@@ -381,24 +496,97 @@ const avatarPresets = [
"linear-gradient(135deg,#e8684a,#f1957e)",
"linear-gradient(135deg,#8a7ce0,#b3a8f0)",
];
const avatarLetters = ["张", "A", "B", "C", "D"];
const avatarLetters = computed(() => {
const name = String(store.userInfo?.name || "A").trim();
return [name.charAt(0) || "A", "A", "B", "C", "D"];
});
const avatarSel = ref(0);
const logoList = [
{ name: "智慧校园平台", marks: "🎓" },
{ name: "云海教育", marks: "📘" },
{ name: "第一中学", marks: "🏫" },
];
const logoSel = ref(0);
/* ===== logo / wallpaper:管理列表 + 直传 + 删除(照抄 SysSetting.vue 管理语义) ===== */
const logoManageList = ref<any[]>([]);
const wallpaperManageList = ref<any[]>([]);
const logoSelUrl = ref<string>("");
const wallSelUrl = ref<string>("");
/** 直传端点(本 pane 无机构树,超管按机构切换请用「系统设置」pane;不带 schId 即落到登录人归属校) */
const logoUploadAction = $conf.serviceHost + "/api/logoUpload";
const wallpaperUploadAction = $conf.serviceHost + "/api/wallpaperUpload";
const wallpaperList = [
"linear-gradient(135deg,#cfe0ea,#d9e0d2)",
"linear-gradient(135deg,#e6e2d6,#c9d4c0)",
"linear-gradient(135deg,#bfd4e8,#a8bdd6)",
"linear-gradient(135deg,#f0e2d0,#e0cdb8)",
"linear-gradient(135deg,#d6e0f0,#c0cde6)",
];
const wallSel = ref(0);
async function loadManageLists() {
// 本 pane 无机构树:不带 schId,列表按登录人归属校返回(超管按机构切换请用「系统设置」pane)
const [walls, logos] = await Promise.all([
SysSettingApi.getSchWallpaperManageList(),
SysSettingApi.getSchLogoManageList(),
]);
wallpaperManageList.value = walls || [];
logoManageList.value = logos || [];
}
function onLogoUploaded() {
loadManageLists();
}
function onWallpaperUploaded() {
loadManageLists();
}
async function onDeleteLogo(item: any) {
await SysSettingApi.deleteLogoById(item.id);
Message.success("删除成功");
loadManageLists();
}
async function onDeleteWallpaper(item: any) {
await SysSettingApi.deleteWallById(item.id);
Message.success("删除成功");
loadManageLists();
}
/** LOGO 保存:选中项写入学校级配置(schId 后端按登录人推断:主学校→归属组→0 平台默认) */
const logoSaving = ref(false);
async function onSaveLogo() {
if (!logoSelUrl.value) {
Message.warning("请先选择一个 LOGO");
return;
}
if (logoSaving.value) return;
logoSaving.value = true;
try {
await BaseApi.saveSchLogo(logoSelUrl.value);
Message.success("保存成功,全站 LOGO 生效");
} catch (error) {
Message.error("操作失败:" + error);
} finally {
logoSaving.value = false;
}
}
/** 应用壁纸:采用 checkWallpaper 个人壁纸语义(与 Header 壁纸弹窗现状一致,见执行报告) */
const wallApplying = ref(false);
async function onApplyWallpaper() {
if (!wallSelUrl.value) {
Message.warning("请先选择一张壁纸");
return;
}
if (wallApplying.value) return;
wallApplying.value = true;
try {
await UserSettingApi.checkWallpaper(store.getCurSchId, wallSelUrl.value);
const userSetting = store.curUserSettingInfo;
if (!!userSetting) {
userSetting.wallpaperUrl = wallSelUrl.value;
store.setCurUserSettingsInfo(userSetting);
}
Message.success("壁纸已应用");
} catch (error) {
Message.error("操作失败:" + error);
} finally {
wallApplying.value = false;
}
}
// 首次切到 logo / wallpaper pane 时懒加载管理列表
watch(curPane, pane => {
if ((pane === "logo" || pane === "wallpaper") && isAdmin.value && logoManageList.value.length === 0 && wallpaperManageList.value.length === 0) {
loadManageLists();
}
});
const sidebarItems = ref([
{ name: "通知公告", on: true },
@@ -672,23 +860,16 @@ function openBackend() {
align-items: center;
gap: 16px;
}
.avatar-now {
.avatar-img {
width: 72px;
height: 72px;
border-radius: 50%;
background: linear-gradient(135deg, #5979ea, #7d9bf5);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
font-weight: 600;
object-fit: cover;
flex-shrink: 0;
box-shadow: 0 4px 14px rgba(89, 121, 234, 0.3);
&.avatar-sm {
&.avatar-img-sm {
width: 44px;
height: 44px;
font-size: 16px;
}
}
.avatar-ops {
@@ -698,6 +879,7 @@ function openBackend() {
.avatar-btns {
display: flex;
gap: 8px;
align-items: center;
}
.tip {
font-size: 12px;
@@ -786,6 +968,12 @@ function openBackend() {
gap: 12px;
padding: 14px 0 4px;
}
.logo-empty {
grid-column: 1 / -1;
padding: 18px 0 8px;
color: #b3b3b3;
font-size: 13px;
}
.logo-item {
height: 72px;
border-radius: 6px;
@@ -796,10 +984,13 @@ function openBackend() {
justify-content: center;
cursor: pointer;
transition: all 0.18s;
font-size: 15px;
font-weight: 600;
color: #3f5fd4;
gap: 8px;
position: relative;
overflow: hidden;
img {
max-width: 86%;
max-height: 82%;
object-fit: contain;
}
&:hover {
border-color: #5979ea;
background: #eef2ff;
@@ -808,17 +999,30 @@ function openBackend() {
border-color: #5979ea;
box-shadow: 0 0 0 3px rgba(89, 121, 234, 0.18);
}
.mk {
width: 26px;
height: 26px;
border-radius: 7px;
background: linear-gradient(135deg, #5979ea, #7d9bf5);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
}
.wall-item {
position: relative;
}
.del {
position: absolute;
top: 4px;
right: 4px;
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
color: #fff;
font-size: 12px;
font-style: normal;
line-height: 18px;
text-align: center;
cursor: pointer;
display: none;
z-index: 2;
}
.logo-item:hover .del,
.wall-item:hover .del {
display: block;
}
/* 边栏设置:拖拽示意列表 */