From 0ded3cce9fec9c556b897eceb054e49d7c8498eb Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 30 Sep 2026 12:43:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E4=BA=94=E4=B8=AA=20pane=20?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE=EF=BC=88?= =?UTF-8?q?=E9=9B=B6=E6=96=B0=E6=8E=A5=E5=8F=A3=EF=BC=89=E2=80=94=E2=80=94?= =?UTF-8?q?profile=20=E8=AF=BB=20store=E3=80=81=E6=94=B9=E5=AF=86=E7=99=BB?= =?UTF-8?q?=E5=87=BA=E6=B5=81=E7=A8=8B=E3=80=81=E5=A4=B4=E5=83=8F=E7=9B=B4?= =?UTF-8?q?=E4=BC=A0=E3=80=81logo/wallpaper=20=E7=AE=A1=E7=90=86=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 权限结构未动 --- src/api/BaseApi.ts | 9 ++ src/views/Settings.vue | 340 ++++++++++++++++++++++++++++++++--------- 2 files changed, 281 insertions(+), 68 deletions(-) diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 6992e20..5c63700 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -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 }); diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 4831424..8a490ed 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -49,28 +49,29 @@
头像
-
张
+
姓名
- + 由学校统一维护
工号
- + 由学校统一维护
所属部门
- +
@@ -87,7 +88,8 @@
- + +
@@ -120,7 +122,7 @@
- +
@@ -134,13 +136,22 @@

当前头像

-
张
+
- - + + + + + +
- 上传后立即生效,全站同步展示。 + 上传后立即生效,全站同步展示。支持 JPG / PNG / GIF / WebP,≤ 20MB。
@@ -170,25 +181,31 @@

当前 LOGO

+
暂无 LOGO,请先上传
- {{ l.marks }}{{ l.name }} + + ×
上传 LOGO
- + + + 建议 PNG 透明底,高度 ≥ 64px,≤ 1MB
- +
@@ -201,24 +218,33 @@

系统壁纸

+
暂无壁纸,请先上传
+ × ✓
上传壁纸
- + + + 建议 1920×1080 以上,≤ 5MB
+
+ + +
@@ -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([]); +const wallpaperManageList = ref([]); +const logoSelUrl = ref(""); +const wallSelUrl = ref(""); +/** 直传端点(本 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; } /* 边栏设置:拖拽示意列表 */