feat(avatar): 默认头像轮前端集成——设置页两级样式 + 登录写学校默认键 + 顶栏三态头像
build / build (push) Successful in 5m35s

- BaseApi:schAvatarStyle/saveSchAvatarStyle/clearSchAvatarStyle 三端点(后端 SchSettingsController 配套,本仓 service 仓随轮提交)
- App.vue:登录后 loadSchAvatarDefault()(与 loadSchThemeDefault 同构:失败/404/空→不写键不报错不阻塞),写 localStorage[yh-avatar-sch-default]
- Settings.vue:新增 avatar pane(我的头像样式人人可见 + 本校默认管理员可配,watch 进 pane 拉取)
- AppHeader/UserInfoModal:头像展示位换 UserAvatar 三态组件
- ThemePreview:预览页头像形态联动
运行时联调与部署验证待人工(后端端点须随 service 发版)
This commit is contained in:
chenxing
2026-10-06 20:20:44 +08:00
parent 4d9da5e3a6
commit e2c937f424
6 changed files with 390 additions and 37 deletions
+16
View File
@@ -62,6 +62,7 @@ mittBus.on("openBschIframePage", async (params: IframePageParmas) => {
const [r1, r2] = await Promise.all([p1, p2]); const [r1, r2] = await Promise.all([p1, p2]);
store.$state.userInfo = userinfo; store.$state.userInfo = userinfo;
loadSchThemeDefault(); loadSchThemeDefault();
loadSchAvatarDefault();
if (r1.length == 0) { if (r1.length == 0) {
store.$state.userRoleList = [ store.$state.userRoleList = [
{ {
@@ -103,6 +104,21 @@ function loadSchThemeDefault() {
.catch(() => undefined); .catch(() => undefined);
} }
/** 拉学校默认头像样式写键(默认头像轮 2026-09-30;与 loadSchThemeDefault 同构:失败/404/空 → 不写、不报错、不阻塞登录流程) */
function loadSchAvatarDefault() {
BaseApi.getSchAvatarStyle()
.then((res: any) => {
if (res && res.avatarStyle) {
try {
localStorage.setItem("yh-avatar-sch-default", res.avatarStyle);
} catch (e) {
/* 隐私模式写失败静默 */
}
}
})
.catch(() => undefined);
}
/**切换角色 */ /**切换角色 */
async function switchRole() { async function switchRole() {
await loadUserMenuBarList(); await loadUserMenuBarList();
+23
View File
@@ -213,6 +213,29 @@ class Base extends Abstract {
return this.postRequest({ url }); return this.postRequest({ url });
} }
// ===== 学校默认头像样式(默认头像轮 2026-09-30;后端契约见 SchSettingsController#schAvatarStyle)=====
/**
* 读本校默认头像样式(已登录可读):{avatarStyle: "text"|"silhouette"|null}——
* null 表示未配置(前端"不写键、不报错"),登录后调一次写 localStorage["yh-avatar-sch-default"]
*/
getSchAvatarStyle() {
const url = path + "/schAvatarStyle";
return this.getRequest<{ avatarStyle: string | null; schId: number }>({ url });
}
/** 保存本校默认头像样式(仅学校管理员):avatarStyle = "text" | "silhouette" */
saveSchAvatarStyle(avatarStyle: string) {
const url = path + "/saveSchAvatarStyle";
return this.postRequest({ url, params: { avatarStyle } });
}
/** 清空本校默认头像样式(仅学校管理员;置空后读侧返回 null) */
clearSchAvatarStyle() {
const url = path + "/clearSchAvatarStyle";
return this.postRequest({ url });
}
savUserRole(info: any) { savUserRole(info: any) {
const url = path + "/user/savUserRole"; const url = path + "/user/savUserRole";
return this.postRequest({ url, data: info as any }); return this.postRequest({ url, data: info as any });
+2 -1
View File
@@ -8,7 +8,7 @@
<h4 class="fl align-middle">{{ appName }}</h4> <h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div> <div class="school-bg"></div>
<div class="header-aside fr"> <div class="header-aside fr">
<img :src="userInfo.profilePicture" @click="showUserInfo" class="fl align-middle" /> <UserAvatar :src="userInfo.profilePicture" :name="userInfo.name" :size="28" placement="brand" class="fl align-middle" @click="showUserInfo" />
<a class="name fl align-middle text-ellipsis" @click="showUserInfo" href="javascript:void(0);"> <a class="name fl align-middle text-ellipsis" @click="showUserInfo" href="javascript:void(0);">
Hi Hi
<span class="ml-4">{{ userInfo.name }}</span> <span class="ml-4">{{ userInfo.name }}</span>
@@ -27,6 +27,7 @@
import { useDefaultStore } from "@/store"; import { useDefaultStore } from "@/store";
import { computed } from "vue"; import { computed } from "vue";
import SvgIcon from "@/components/SvgIcon.vue"; import SvgIcon from "@/components/SvgIcon.vue";
import UserAvatar from "@/components/UserAvatar.vue";
import mittBus from "@/utils/mitt"; import mittBus from "@/utils/mitt";
import { StateEnum } from "@/types"; import { StateEnum } from "@/types";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
+2 -2
View File
@@ -15,7 +15,7 @@
:headers="uploadHeaders" :headers="uploadHeaders"
> >
<div class="avatar-wrap" title="点击更换头像"> <div class="avatar-wrap" title="点击更换头像">
<img :src="requireImg('user-avatar-default.png')" v-real-img="store.userInfo.profilePicture" alt="" /> <UserAvatar :src="store.userInfo.profilePicture" :name="store.userInfo.name" :size="120" placement="light" />
<div class="avatar-mask">更换头像</div> <div class="avatar-mask">更换头像</div>
</div> </div>
</Upload> </Upload>
@@ -67,8 +67,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, reactive } from "vue"; import { ref, onMounted, onUnmounted, reactive } from "vue";
import { requireImg } from "@/utils/hook/useImg";
import { useDefaultStore } from "@/store"; import { useDefaultStore } from "@/store";
import UserAvatar from "@/components/UserAvatar.vue";
import mittBus from "@/utils/mitt"; import mittBus from "@/utils/mitt";
import BaseApi from "@/api/BaseApi"; import BaseApi from "@/api/BaseApi";
import Utils from "@/utils"; import Utils from "@/utils";
+303 -6
View File
@@ -49,8 +49,7 @@
<div class="form-row"> <div class="form-row">
<span class="lb">头像</span> <span class="lb">头像</span>
<div class="ctl"> <div class="ctl">
<img class="avatar-img avatar-img-sm" :src="requireImg('user-avatar-default.png')" <UserAvatar :src="store.userInfo.profilePicture" :name="store.userInfo.name" :size="40" placement="light" />
v-real-img="store.userInfo.profilePicture" alt="" />
<button class="btn ghost sm" @click="setPane('avatar')">更换头像</button> <button class="btn ghost sm" @click="setPane('avatar')">更换头像</button>
</div> </div>
</div> </div>
@@ -136,8 +135,7 @@
<h4>当前头像</h4> <h4>当前头像</h4>
<div class="avatar-pad"> <div class="avatar-pad">
<div class="avatar-box"> <div class="avatar-box">
<img class="avatar-img" :src="requireImg('user-avatar-default.png')" <UserAvatar :src="store.userInfo.profilePicture" :name="store.userInfo.name" :size="120" placement="light" />
v-real-img="store.userInfo.profilePicture" alt="" />
<div class="avatar-ops"> <div class="avatar-ops">
<div class="avatar-btns"> <div class="avatar-btns">
<!-- 直传逻辑移植 UserInfoModal:action/headers/校验/成功回调照抄 --> <!-- 直传逻辑移植 UserInfoModal:action/headers/校验/成功回调照抄 -->
@@ -170,6 +168,59 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 默认头像样式(默认头像轮 2026-09-30):未上传头像时的兜底形态,链路与主题同构(用户键 > 学校键 > 系统默认)。
示例即 <UserAvatar> 实时渲染:首字示例带 name(样式生效即显字),剪影示例无 name(恒剪影);
:key=myAvatarTick 使 localStorage 直写后重挂组件、示例即时切换(resolveAvatarStyle 非响应式) -->
<div class="sec-card">
<h4>默认头像样式 <span class="tag">未上传头像时生效</span></h4>
<div class="style-grid">
<div
:class="['style-opt', { sel: myAvatarEffective === 'text' }]"
@click="pickMyAvatarStyle('text')"
>
<UserAvatar :key="myAvatarTick" :name="store.userInfo.name" :size="48" placement="light" />
<span class="sn">首字</span>
<i v-if="myAvatarEffective === 'text'" class="tick">✓</i>
</div>
<div
:class="['style-opt', { sel: myAvatarEffective === 'silhouette' }]"
@click="pickMyAvatarStyle('silhouette')"
>
<UserAvatar :key="myAvatarTick" :size="48" placement="light" />
<span class="sn">剪影</span>
<i v-if="myAvatarEffective === 'silhouette'" class="tick">✓</i>
</div>
</div>
<div class="pane-foot style-foot">
<span class="origin">当前来源:<b>{{ myAvatarOriginLabel }}</b></span>
<button class="btn plain" :disabled="myAvatarOrigin !== 'mine'" @click="followSchoolAvatar">跟随学校默认</button>
</div>
</div>
<!-- 本校默认头像样式(仅管理员):写入 sch_settings.avatar_style,契约复刻 schTheme 三接口 -->
<div v-if="isAdmin" class="sec-card">
<h4>本校默认头像样式</h4>
<div class="style-grid">
<div
:class="['style-opt', { sel: schAvatarCandidate === 'text' }]"
@click="schAvatarCandidate = 'text'"
>
<span class="sn">首字</span>
</div>
<div
:class="['style-opt', { sel: schAvatarCandidate === 'silhouette' }]"
@click="schAvatarCandidate = 'silhouette'"
>
<span class="sn">剪影</span>
</div>
</div>
<div class="pane-foot style-foot">
<span class="origin">当前已配置:<b>{{ schAvatarConfigured ? AVATAR_STYLE_LABEL[schAvatarConfigured] : "未配置(系统默认:首字)" }}</b></span>
<span class="btns">
<button class="btn primary" :disabled="schAvatarSaving" @click="onSaveSchAvatar">保存为本校默认</button>
<button class="btn plain" :disabled="schAvatarSaving || !schAvatarConfigured" @click="onClearSchAvatar">跟随系统默认(清空)</button>
</span>
</div>
</div>
</div> </div>
<!-- 我的主题(所有人;主题设置 2026-09-30) --> <!-- 我的主题(所有人;主题设置 2026-09-30) -->
@@ -192,6 +243,23 @@
<i v-if="myEffective.theme === t.key" class="tick">✓</i> <i v-if="myEffective.theme === t.key" class="tick">✓</i>
</div> </div>
</div> </div>
<!-- 侧栏样式(默认头像轮 2026-09-30):状态字段 theme.ts 早已有(sideFollow/sideDeep/gradOn),此处只补 UI;
即时生效即入 localStorage(与 Notice 配色胶囊一致),无保存按钮;开关样式照搬 ThemeSwitcher .sw -->
<div class="sec-card side-card">
<h4>侧栏样式</h4>
<label class="tp-row">
<span>侧栏跟随顶栏色</span>
<span class="sw"><input type="checkbox" v-model="sideFollow" @change="pickSide({ sideFollow })" /><i></i></span>
</label>
<label class="tp-row">
<span>侧栏深色(关=浅色)</span>
<span class="sw"><input type="checkbox" v-model="sideDeep" @change="pickSide({ sideDeep })" /><i></i></span>
</label>
<label class="tp-row">
<span>顶部过渡色(渐变)</span>
<span class="sw"><input type="checkbox" v-model="gradOn" @change="pickSide({ gradOn })" /><i></i></span>
</label>
</div>
<div class="pane-foot theme-foot"> <div class="pane-foot theme-foot">
<!-- 删用户键后回落学校默认/系统默认(§3.3) --> <!-- 删用户键后回落学校默认/系统默认(§3.3) -->
<button class="btn plain" @click="followSchoolTheme" :disabled="myOrigin !== 'mine'">跟随学校默认</button> <button class="btn plain" @click="followSchoolTheme" :disabled="myOrigin !== 'mine'">跟随学校默认</button>
@@ -518,13 +586,13 @@ import { ref, reactive, computed, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useDefaultStore } from "@/store"; import { useDefaultStore } from "@/store";
import useCurrentInstance from "@/utils/hook/useCurrentInstance"; import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import { requireImg } from "@/utils/hook/useImg";
import SysSetting from "@/views/SysSetting.vue"; import SysSetting from "@/views/SysSetting.vue";
import BaseApi from "@/api/BaseApi"; import BaseApi from "@/api/BaseApi";
import SysSettingApi from "@/api/SysSettingApi"; import SysSettingApi from "@/api/SysSettingApi";
import UserSettingApi from "@/api/UserSettingApi"; import UserSettingApi from "@/api/UserSettingApi";
import MenuBarApi from "@/api/MenuBarApi"; import MenuBarApi from "@/api/MenuBarApi";
import SvgIcon from "@/components/SvgIcon.vue"; import SvgIcon from "@/components/SvgIcon.vue";
import UserAvatar from "@/components/UserAvatar.vue";
import MyConfirm from "@/components/MyConfirm"; import MyConfirm from "@/components/MyConfirm";
import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeDict } from "@/types"; import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeDict } from "@/types";
import Utils from "@/utils"; import Utils from "@/utils";
@@ -943,8 +1011,10 @@ const THEME_OPTIONS = [
{ key: "teal", name: "青碧", color: "#0d9488" }, { key: "teal", name: "青碧", color: "#0d9488" },
]; ];
/** 预览 iframe src:候选档经 URL 覆盖参数拼进 #/theme-preview(§3.2/§3.4,切候选即换 src 重载) */ /** 预览 iframe src:候选档经 URL 覆盖参数拼进 #/theme-preview(§3.2/§3.4,切候选即换 src 重载)。
侧栏三开关直写 localStorage 非响应式,靠 void myTick.value 让 src 对主题态变化保持响应(否则改开关预览不重载) */
function previewSrc(candidate: string) { function previewSrc(candidate: string) {
void myTick.value;
const cur = resolveEffectiveThemeState(); const cur = resolveEffectiveThemeState();
const q = new URLSearchParams(); const q = new URLSearchParams();
q.set("yh-theme", candidate); q.set("yh-theme", candidate);
@@ -1071,6 +1141,123 @@ watch(curPane, pane => {
if (pane === "mytheme" || pane === "schooltheme") loadSchTheme(); if (pane === "mytheme" || pane === "schooltheme") loadSchTheme();
}); });
// ===== 默认头像样式(默认头像轮 2026-09-30:用户自选 + 学校默认两级,链路与主题同构)=====
import {
AVATAR_STORAGE_KEY,
SCHOOL_DEFAULT_AVATAR_STORAGE_KEY,
resolveAvatarStyle,
saveAvatarStyle,
clearAvatarStyle,
} from "@/utils/avatar";
import type { AvatarStyle } from "@/utils/avatar";
const AVATAR_STYLE_LABEL: Record<AvatarStyle, string> = { text: "首字", silhouette: "剪影" };
// ---- 我的默认头像(所有人)----
const myAvatarCandidate = ref<AvatarStyle>(resolveAvatarStyle());
const myAvatarTick = ref(0);
const myAvatarEffective = computed(() => {
void myAvatarTick.value;
return resolveAvatarStyle();
});
const myAvatarOrigin = computed<"mine" | "school" | "system">(() => {
if (keyExists(AVATAR_STORAGE_KEY)) return "mine";
if (keyExists(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY)) return "school";
return "system";
});
const myAvatarOriginLabel = computed(
() => ({ mine: "我的选择", school: "学校默认", system: "系统默认" })[myAvatarOrigin.value]
);
/** 选样式即写键即生效(saveAvatarStyle 直写 localStorage 无响应式,靠 tick 刷示例与来源标签) */
function pickMyAvatarStyle(key: AvatarStyle) {
saveAvatarStyle(key);
myAvatarCandidate.value = key;
myAvatarTick.value += 1;
}
/** 跟随学校默认 = 删用户自选键(之后回落学校默认/系统默认,与 followSchoolTheme 同构) */
function followSchoolAvatar() {
clearAvatarStyle();
myAvatarTick.value += 1;
}
// ---- 本校默认头像(仅管理员)----
const schAvatarCandidate = ref<AvatarStyle>(resolveAvatarStyle());
const schAvatarConfigured = ref<AvatarStyle | null>(null); // 已配置的学校默认(text|silhouette)
const schAvatarSaving = ref(false);
async function loadSchAvatar() {
try {
const res = await BaseApi.getSchAvatarStyle();
if (res && res.avatarStyle) {
schAvatarConfigured.value = res.avatarStyle as AvatarStyle;
schAvatarCandidate.value = res.avatarStyle as AvatarStyle;
} else {
schAvatarConfigured.value = null;
}
} catch (e) {
/* 读取失败不打扰 */
}
}
/** 保存本校默认:成功后同步本地学校键(管理员自身立即生效,与 onSaveSchTheme 同构) */
async function onSaveSchAvatar() {
if (schAvatarSaving.value) return;
schAvatarSaving.value = true;
try {
await BaseApi.saveSchAvatarStyle(schAvatarCandidate.value);
try {
localStorage.setItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY, schAvatarCandidate.value);
} catch (e) {
/* 隐私模式静默 */
}
schAvatarConfigured.value = schAvatarCandidate.value;
myAvatarTick.value += 1;
Message.success("本校默认头像样式已保存");
} catch (error) {
Message.error("保存失败:" + error);
} finally {
schAvatarSaving.value = false;
}
}
/** 跟随系统默认(清空学校配置,与 onClearSchTheme 同构) */
async function onClearSchAvatar() {
if (schAvatarSaving.value) return;
schAvatarSaving.value = true;
try {
await BaseApi.clearSchAvatarStyle();
try {
localStorage.removeItem(SCHOOL_DEFAULT_AVATAR_STORAGE_KEY);
} catch (e) {
/* 隐私模式静默 */
}
schAvatarConfigured.value = null;
myAvatarTick.value += 1;
Message.success("已恢复系统默认");
} catch (error) {
Message.error("操作失败:" + error);
} finally {
schAvatarSaving.value = false;
}
}
// 打开头像 pane 时拉一次学校默认头像配置(管理员视角;失败静默)
watch(curPane, pane => {
if (pane === "avatar" && isAdmin.value) loadSchAvatar();
});
// ---- 侧栏样式三开关(mytheme pane;即时生效即入 localStorage,无保存按钮)----
const sideFollow = ref(resolveEffectiveThemeState().sideFollow);
const sideDeep = ref(resolveEffectiveThemeState().sideDeep);
const gradOn = ref(resolveEffectiveThemeState().gradOn);
function pickSide(patch: Partial<ThemeState>) {
saveThemeState({ ...resolveEffectiveThemeState(), ...patch } as ThemeState);
myTick.value += 1;
}
/* ===== sidebar:真实边栏管理(2026-09-30 二轮自 Header MenuBarModal 迁入:接口/字段/校验/直传/确认删除语义照抄) ===== */ /* ===== sidebar:真实边栏管理(2026-09-30 二轮自 Header MenuBarModal 迁入:接口/字段/校验/直传/确认删除语义照抄) ===== */
const sidebarList = ref<any[]>([]); const sidebarList = ref<any[]>([]);
const sidebarLoading = ref(false); const sidebarLoading = ref(false);
@@ -1612,6 +1799,116 @@ function openBackend() {
display: block; display: block;
} }
/* 侧栏样式三开关(默认头像轮 2026-09-30):tp-row/sw 照搬 YschTheme ThemeSwitcher.vue:234-273(38×21 轨道、17px 位移) */
.side-card {
.tp-row + .tp-row {
margin-top: 10px;
}
}
.tp-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12.5px;
color: var(--text-2, #5a6478);
cursor: pointer;
padding: 0 2px;
}
.sw {
position: relative;
width: 38px;
height: 21px;
flex: 0 0 auto;
}
.sw input {
position: absolute;
inset: 0;
opacity: 0;
margin: 0;
cursor: pointer;
z-index: 1;
}
.sw i {
position: absolute;
inset: 0;
border-radius: 999px;
background: #d7dce6;
transition: background 0.2s;
pointer-events: none;
}
.sw i::after {
content: "";
position: absolute;
top: 2.5px;
left: 2.5px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.28);
transition: transform 0.2s;
}
.sw input:checked + i {
background: var(--brand, #577afb);
}
.sw input:checked + i::after {
transform: translateX(17px);
}
/* 默认头像样式二选一(默认头像轮 2026-09-30):卡片规格对齐 theme-item */
.style-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-width: 320px;
padding: 12px 0 4px;
}
.style-opt {
position: relative;
height: 112px;
border: 2px solid #eef0f3;
border-radius: 10px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 9px;
cursor: pointer;
transition: all 0.18s;
&:hover {
border-color: #5979ea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&.sel {
border-color: #5979ea;
box-shadow: 0 0 0 3px rgba(89, 121, 234, 0.18);
}
.sn {
font-size: 13.5px;
color: #2c3138;
}
.tick {
display: flex;
}
}
.style-foot {
align-items: center;
justify-content: space-between;
.origin {
font-size: 12.5px;
color: #999;
b {
color: #2c3138;
}
}
.btns {
display: flex;
gap: 10px;
}
}
/* 壁纸 / LOGO 网格 */ /* 壁纸 / LOGO 网格 */
.wall-grid { .wall-grid {
display: grid; display: grid;
+44 -28
View File
@@ -4,25 +4,40 @@
档位类与通道 CSS 只落在本 document 的 <html> 上——UserHome 本体(父页)永远不吃通道 CSS。 档位类与通道 CSS 只落在本 document 的 <html> 上——UserHome 本体(父页)永远不吃通道 CSS。
纯展示:根容器 pointer-events:none,不渲染搜索区/分页操作/ThemeSwitcher。 纯展示:根容器 pointer-events:none,不渲染搜索区/分页操作/ThemeSwitcher。
DOM 结构对齐 Notice layout(header-container / left-menu / path-nav / main-container), DOM 结构对齐 Notice layout(header-container / left-menu / path-nav / main-container),
观感(配色/侧栏形态)由通道 base.layout.css 按 URL 覆盖参数(?yh-theme= 等)驱动的档位类提供。 --> 观感(配色/侧栏形态)由通道 base.layout.css 按 URL 覆盖参数(?yh-theme= 等)驱动的档位类提供。
默认头像轮(2026-09-30):logo 换云海图形、用户胶囊补三态头像圆 + icon-logout、
右侧补静态配色胶囊、左菜单换 iconfont + 可展开子菜单(预览 markup 落后于通道皮肤修复)。 -->
<div class="theme-preview-root"> <div class="theme-preview-root">
<header class="header-container fix"> <header class="header-container fix">
<span class="logo-badge fl"><span class="logo-mark">云海</span></span> <span class="logo-badge fl"><img src="@/assets/image/yunhai-logo.svg" alt="云海" /></span>
<div class="delimiter fl align-middle"></div> <div class="delimiter fl align-middle"></div>
<h4 class="fl align-middle">管理端预览</h4> <h4 class="fl align-middle">管理端预览</h4>
<div class="school-bg"></div> <div class="school-bg"></div>
<div class="header-aside fr"> <div class="header-aside fr">
<UserAvatar :size="28" placement="brand" />
<span class="name fl align-middle text-ellipsis">演示用户</span> <span class="name fl align-middle text-ellipsis">演示用户</span>
<div class="delimiter fl align-middle"></div> <div class="delimiter fl align-middle"></div>
<a href="javascript:;" class="logout fl align-middle" title="退出登陆"><i>⎋</i></a> <a href="javascript:;" class="logout fl align-middle" title="退出登陆"><i class="iconfont icon-logout"></i></a>
</div> </div>
<!-- 配色胶囊:静态展示(不接 ThemeSwitcher,避免预览里出现可点控件与真实切换器语义混淆)。
置于 header-aside 之后:本页不吃 .layout-wrap 皮肤(flex order 失效、走 float),
float:right 的 DOM 靠后者居左——才能复刻 Notice「配色在左、用户胶囊在右」 -->
<div class="theme-cap fr"><span class="cap-btn">配色 ▾</span></div>
</header> </header>
<div class="left-menu"> <div class="left-menu">
<div class="menu-top-box"></div> <div class="menu-top-box"></div>
<ul> <ul>
<li v-for="(m, i) in menus" :key="m" :class="{ active: i === 1 }"> <li><a href="javascript:void(0);"><i class="iconfont icon-shouye"></i>工作台</a></li>
<a href="javascript:void(0);"><span class="menu-dot"></span>{{ m }}</a> <li>
<a href="javascript:void(0);"><i class="iconfont icon-liebiao"></i>通知管理<i class="arrow-down"></i></a>
</li> </li>
<div class="child-item-wrap">
<li class="child-item active"><a href="javascript:void(0);">通知列表</a></li>
<li class="child-item"><a href="javascript:void(0);">通知设置</a></li>
</div>
<li><a href="javascript:void(0);"><i class="iconfont icon-tongji"></i>统计分析</a></li>
<li><a href="javascript:void(0);"><i class="iconfont icon-apps"></i>应用中心</a></li>
<li><a href="javascript:void(0);"><i class="iconfont icon-shezhi"></i>系统设置</a></li>
</ul> </ul>
<div class="clear-box"></div> <div class="clear-box"></div>
</div> </div>
@@ -40,17 +55,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { onMounted } from "vue";
import UserAvatar from "@/components/UserAvatar.vue";
import useCurrentInstance from "@/utils/hook/useCurrentInstance"; import useCurrentInstance from "@/utils/hook/useCurrentInstance";
/** /**
* 主题预览路由页:档位来源 = URL 覆盖参数(?yh-theme= / ?yh-side= / ?yh-grad=,由设置页 iframe src 拼入, * 主题预览路由页:档位来源 = URL 覆盖参数(?yh-theme= / ?yh-side= / ?yh-grad=,由设置页 iframe src 拼入,
* 切候选档即换 src 重载),init 启动段按合成优先级挂档——本组件不读写 localStorage、不提供任何交互。 * 切候选档即换 src 重载),init 启动段按合成优先级挂档——本组件不读写 localStorage、不提供任何交互。
* 直开(不经设置页、未登录)也可渲染:本页由 App.vue 顶层条件分支挂载,不依赖桌面框架与登录态。 * 直开(不经设置页、未登录)也可渲染:本页由 App.vue 顶层条件分支挂载,不依赖桌面框架与登录态。
* 头像圆走 UserAvatar(默认头像轮 2026-09-30):无 src 无 name → 恒剪影,brand 场景 = S1 玻璃底。
*/ */
const { $conf } = useCurrentInstance(); const { $conf } = useCurrentInstance();
const menus = ["工作台", "通知管理", "统计分析", "应用中心", "系统设置"];
const columns = [ const columns = [
{ type: "index", title: "序号", width: 70, align: "center" }, { type: "index", title: "序号", width: 70, align: "center" },
{ title: "标题", key: "title" }, { title: "标题", key: "title" },
@@ -87,18 +102,6 @@ onMounted(() => {
} }
.header-container { .header-container {
.logo-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.22);
color: #fff;
font-size: 12px;
font-weight: 600;
}
h4 { h4 {
color: #fff; color: #fff;
font-size: 16px; font-size: 16px;
@@ -107,15 +110,28 @@ onMounted(() => {
} }
} }
.menu-dot { /* 配色胶囊(静态展示):观感对齐 ThemeSwitcher .theme-btn(通道皮肤只给 .theme-cap 排版,胶囊样式在切换器组件内) */
display: inline-block; .cap-btn {
width: 7px; display: flex;
height: 7px; align-items: center;
border-radius: 50%; gap: 6px;
background: currentColor; height: 30px;
opacity: 0.45; padding: 0 11px;
margin-right: 9px; border: 1px solid rgba(255, 255, 255, 0.34);
vertical-align: middle; border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #fff;
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;
} }
.path-nav .crumb-sep { .path-nav .crumb-sep {