- 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:
+16
@@ -62,6 +62,7 @@ mittBus.on("openBschIframePage", async (params: IframePageParmas) => {
|
||||
const [r1, r2] = await Promise.all([p1, p2]);
|
||||
store.$state.userInfo = userinfo;
|
||||
loadSchThemeDefault();
|
||||
loadSchAvatarDefault();
|
||||
if (r1.length == 0) {
|
||||
store.$state.userRoleList = [
|
||||
{
|
||||
@@ -103,6 +104,21 @@ function loadSchThemeDefault() {
|
||||
.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() {
|
||||
await loadUserMenuBarList();
|
||||
|
||||
@@ -213,6 +213,29 @@ class Base extends Abstract {
|
||||
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) {
|
||||
const url = path + "/user/savUserRole";
|
||||
return this.postRequest({ url, data: info as any });
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h4 class="fl align-middle">{{ appName }}</h4>
|
||||
<div class="school-bg"></div>
|
||||
<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);">
|
||||
Hi
|
||||
<span class="ml-4">{{ userInfo.name }}</span>
|
||||
@@ -27,6 +27,7 @@
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { computed } from "vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import { StateEnum } from "@/types";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
:headers="uploadHeaders"
|
||||
>
|
||||
<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>
|
||||
</Upload>
|
||||
@@ -67,8 +67,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, reactive } from "vue";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import mittBus from "@/utils/mitt";
|
||||
import BaseApi from "@/api/BaseApi";
|
||||
import Utils from "@/utils";
|
||||
|
||||
+303
-6
@@ -49,8 +49,7 @@
|
||||
<div class="form-row">
|
||||
<span class="lb">头像</span>
|
||||
<div class="ctl">
|
||||
<img class="avatar-img avatar-img-sm" :src="requireImg('user-avatar-default.png')"
|
||||
v-real-img="store.userInfo.profilePicture" alt="" />
|
||||
<UserAvatar :src="store.userInfo.profilePicture" :name="store.userInfo.name" :size="40" placement="light" />
|
||||
<button class="btn ghost sm" @click="setPane('avatar')">更换头像</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,8 +135,7 @@
|
||||
<h4>当前头像</h4>
|
||||
<div class="avatar-pad">
|
||||
<div class="avatar-box">
|
||||
<img class="avatar-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-ops">
|
||||
<div class="avatar-btns">
|
||||
<!-- 直传逻辑移植 UserInfoModal:action/headers/校验/成功回调照抄 -->
|
||||
@@ -170,6 +168,59 @@
|
||||
</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>
|
||||
|
||||
<!-- 我的主题(所有人;主题设置 2026-09-30) -->
|
||||
@@ -192,6 +243,23 @@
|
||||
<i v-if="myEffective.theme === t.key" class="tick">✓</i>
|
||||
</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">
|
||||
<!-- 删用户键后回落学校默认/系统默认(§3.3) -->
|
||||
<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 { 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 MenuBarApi from "@/api/MenuBarApi";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { LinkOpenTypeDict, LinkOpenTypeEnum, RoleTypeDict } from "@/types";
|
||||
import Utils from "@/utils";
|
||||
@@ -943,8 +1011,10 @@ const THEME_OPTIONS = [
|
||||
{ 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) {
|
||||
void myTick.value;
|
||||
const cur = resolveEffectiveThemeState();
|
||||
const q = new URLSearchParams();
|
||||
q.set("yh-theme", candidate);
|
||||
@@ -1071,6 +1141,123 @@ watch(curPane, pane => {
|
||||
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 迁入:接口/字段/校验/直传/确认删除语义照抄) ===== */
|
||||
const sidebarList = ref<any[]>([]);
|
||||
const sidebarLoading = ref(false);
|
||||
@@ -1612,6 +1799,116 @@ function openBackend() {
|
||||
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 网格 */
|
||||
.wall-grid {
|
||||
display: grid;
|
||||
|
||||
+44
-28
@@ -4,25 +4,40 @@
|
||||
档位类与通道 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= 等)驱动的档位类提供。 -->
|
||||
观感(配色/侧栏形态)由通道 base.layout.css 按 URL 覆盖参数(?yh-theme= 等)驱动的档位类提供。
|
||||
默认头像轮(2026-09-30):logo 换云海图形、用户胶囊补三态头像圆 + icon-logout、
|
||||
右侧补静态配色胶囊、左菜单换 iconfont + 可展开子菜单(预览 markup 落后于通道皮肤修复)。 -->
|
||||
<div class="theme-preview-root">
|
||||
<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>
|
||||
<h4 class="fl align-middle">管理端预览</h4>
|
||||
<div class="school-bg"></div>
|
||||
<div class="header-aside fr">
|
||||
<UserAvatar :size="28" placement="brand" />
|
||||
<span class="name fl align-middle text-ellipsis">演示用户</span>
|
||||
<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>
|
||||
<!-- 配色胶囊:静态展示(不接 ThemeSwitcher,避免预览里出现可点控件与真实切换器语义混淆)。
|
||||
置于 header-aside 之后:本页不吃 .layout-wrap 皮肤(flex order 失效、走 float),
|
||||
float:right 的 DOM 靠后者居左——才能复刻 Notice「配色在左、用户胶囊在右」 -->
|
||||
<div class="theme-cap fr"><span class="cap-btn">配色 ▾</span></div>
|
||||
</header>
|
||||
<div class="left-menu">
|
||||
<div class="menu-top-box"></div>
|
||||
<ul>
|
||||
<li v-for="(m, i) in menus" :key="m" :class="{ active: i === 1 }">
|
||||
<a href="javascript:void(0);"><span class="menu-dot"></span>{{ m }}</a>
|
||||
<li><a href="javascript:void(0);"><i class="iconfont icon-shouye"></i>工作台</a></li>
|
||||
<li>
|
||||
<a href="javascript:void(0);"><i class="iconfont icon-liebiao"></i>通知管理<i class="arrow-down"></i></a>
|
||||
</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>
|
||||
<div class="clear-box"></div>
|
||||
</div>
|
||||
@@ -40,17 +55,17 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import UserAvatar from "@/components/UserAvatar.vue";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
|
||||
/**
|
||||
* 主题预览路由页:档位来源 = URL 覆盖参数(?yh-theme= / ?yh-side= / ?yh-grad=,由设置页 iframe src 拼入,
|
||||
* 切候选档即换 src 重载),init 启动段按合成优先级挂档——本组件不读写 localStorage、不提供任何交互。
|
||||
* 直开(不经设置页、未登录)也可渲染:本页由 App.vue 顶层条件分支挂载,不依赖桌面框架与登录态。
|
||||
* 头像圆走 UserAvatar(默认头像轮 2026-09-30):无 src 无 name → 恒剪影,brand 场景 = S1 玻璃底。
|
||||
*/
|
||||
const { $conf } = useCurrentInstance();
|
||||
|
||||
const menus = ["工作台", "通知管理", "统计分析", "应用中心", "系统设置"];
|
||||
|
||||
const columns = [
|
||||
{ type: "index", title: "序号", width: 70, align: "center" },
|
||||
{ title: "标题", key: "title" },
|
||||
@@ -87,18 +102,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
@@ -107,15 +110,28 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.menu-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.45;
|
||||
margin-right: 9px;
|
||||
vertical-align: middle;
|
||||
/* 配色胶囊(静态展示):观感对齐 ThemeSwitcher .theme-btn(通道皮肤只给 .theme-cap 排版,胶囊样式在切换器组件内) */
|
||||
.cap-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 30px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.34);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user