refactor(theme): A 类主题通道接入+本地样式回收、操作列统一 actionColumn、顶栏三态头像
build / build (push) Successful in 2m9s

- P1 通道:view-ui-plus 锁 1.3.24(A 类 loader + vite /static dev 代理核验,五档/断源降级走查)
- P2 回收:删本地 layout.css/icons.css(+bak) 并清 main.ts import;骨架六类对齐 + 品牌色 var(--brand) 化;index CSS 779→393kB
- P3 操作列:新增 utils/tableActions.ts(同构 UserBase,7 函数+2 表,无 AUTO_MODE);7 页迁 actionColumn text 形态;旧 operatingHandle/operatingTextHandle 薄封装保留、签名不变
- P4 外观:接入三态头像 avatar.ts+UserAvatar.vue(同步自 YschTheme),顶栏裸 img 替换;外观三键红线零写
- 失效注释订正(layout.vue/def-editor)
This commit is contained in:
chenxing
2026-10-10 21:24:39 +08:00
parent ed5f818c1a
commit 43753f4f59
27 changed files with 508 additions and 1165 deletions
+102
View File
@@ -0,0 +1,102 @@
<script lang="ts" setup>
/* 默认头像三态组件(YschTheme 公共源,scripts/sync-apps.mjs 下发三端 src/components/UserAvatar.vue):
头像 URL 非空 → <img>;否则 style==="text" 且能取到字 → 首字;否则 → 剪影(纯 CSS/SVG,零资源文件)。
取值链/取字契约见《设置页修复与默认头像落地方案》§10.1/§10.2;样式全在本文件 <style scoped>(唯一 CSS 源,
不进通道 icons.src.css——通道 CSS 对 UserHome 不可达,内联分发才能消双份漂移,见同方案 §10.4)。
根元素即 .yh-avatar 圆:尺寸内联必须挂圆本身(挂外层 span 会被 flex 拉成椭圆,§12 踩坑)。 */
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { pickAvatarText, resolveAvatarStyle } from "@/utils/avatar";
const props = withDefaults(
defineProps<{
/** 头像 URL:非空直接 <img>(三态第一优先) */
src?: string | null;
/** 取字来源(首字态用):经 pickAvatarText 判定,取不到字回落剪影 */
name?: string | null;
/** 直径 px;font-size = round(size * 0.44) */
size?: number;
/** 配色场景:light=页面浅底(品牌实底+白内容);brand=header 品牌色底(剪影 S1 玻璃底 / 首字 S2 白底反色,§10.3 分档) */
placement?: "light" | "brand";
}>(),
{ src: null, name: null, size: 28, placement: "light" },
);
const text = computed(() => (props.src ? "" : pickAvatarText(props.name)));
/* 外观偏好异步补齐(yh-theme-init 在挂载后拉服务端真源收敛 localStorage 键)会在
document 上派发 yh-appearance-updated;tick 自增让 style 依赖失效重读键,三态即时切换(无需刷新)。 */
const appearanceTick = ref(0);
const onAppearanceUpdated = () => { appearanceTick.value += 1; };
onMounted(() => document.addEventListener("yh-appearance-updated", onAppearanceUpdated));
onBeforeUnmount(() => document.removeEventListener("yh-appearance-updated", onAppearanceUpdated));
const style = computed(() => {
appearanceTick.value;
return resolveAvatarStyle();
});
/** 首字态 = style==="text" 且能取到字(契约 §10.2:二者缺一即剪影,取到字但档位是剪影时绝不能出字) */
const showText = computed(() => style.value === "text" && !!text.value);
const rootClass = computed(() => [
"yh-avatar",
props.placement === "brand" ? "yh-avatar--brand" : "yh-avatar--light",
{ "is-text": showText.value },
]);
const rootStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
fontSize: `${Math.round(props.size * 0.44)}px`,
}));
</script>
<template>
<span :class="rootClass" :style="rootStyle">
<img v-if="src" class="yh-avatar__img" :src="src" alt="" />
<template v-else-if="showText">{{ text }}</template>
<svg v-else class="yh-avatar__sil" viewBox="0 0 1024 1024" aria-hidden="true">
<circle cx="512" cy="399" r="174" />
<ellipse cx="512" cy="946" rx="420" ry="240" />
</svg>
</span>
</template>
<style scoped>
.yh-avatar {
position: relative;
border-radius: 50%;
overflow: hidden;
display: inline-grid;
place-items: center;
flex: 0 0 auto;
font-weight: 600;
line-height: 1;
user-select: none;
}
.yh-avatar__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* 剪影 SVG:容器圆 overflow:hidden 裁切肩部溢出(禁用 clipPath);overflow:visible 放行 viewBox 外形状 */
.yh-avatar__sil {
display: block;
width: 100%;
height: 100%;
overflow: visible;
fill: currentColor;
}
/* 页面内(浅底)——品牌实底 + 白内容 */
.yh-avatar--light {
background: var(--brand, #577afb);
color: #fff;
}
/* header(品牌色底)——与页面内反向:剪影档 = S1 半透明白,首字档 = S2 纯白底 + 品牌色字 */
.yh-avatar--brand {
background: rgba(255, 255, 255, 0.22);
color: #fff;
}
.yh-avatar--brand.is-text {
background: #fff;
color: var(--brand, #577afb);
}
</style>
+2 -1
View File
@@ -46,7 +46,7 @@
>
<path
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
fill="#2d8cf0"
fill="currentColor"
></path>
</svg>
</div>
@@ -488,6 +488,7 @@ export default {
position: relative;
top: 50%;
margin-top: -17px;
color: var(--brand);
}
.auto-box {
width: 200px;
+6 -5
View File
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import { resolveMenuPage } from "@/utils/menuRoute";
import defaultLogo from "@/assets/image/yunhai-logo.svg";
import UserAvatar from "@/components/UserAvatar.vue";
const props = defineProps<{ layoutData: Record<string, any> }>();
const route = useRoute();
@@ -154,7 +155,7 @@ const childChange = (index: number) => {
};
/** 根据菜单项映射图标:优先按解析后的 page 判断(平台模块 id 不可依赖),
* 兜底保留旧版 modId 对照。图标为本地 bsch-icons(iconfont 掩码类,见 styles/icons.css) */
* 兜底保留旧版 modId 对照。图标为通道 iconfont 掩码类(A 类接入后由 YschTheme 发版 icons.css 提供,本地副本已回收) */
const getMenuIcon = (item?: any): string => {
const page: string = item?.page || "";
if (page === "/list/new") return "icon-xiugaijl";
@@ -199,7 +200,7 @@ function linkHomePage() {
<h4 class="fl align-middle">{{ appName }}</h4>
<div class="school-bg"></div>
<div class="header-aside fr">
<img :src="userInfo.avatar" class="fl align-middle" @click="linkUserInfo" />
<UserAvatar class="fl align-middle" :src="userInfo.avatar" :name="userInfo.name" :size="28" placement="brand" @click="linkUserInfo" />
<a class="name fl align-middle text-ellipsis" href="javascript:void(0);" @click="linkUserInfo">
{{ userInfo.name }}
</a>
@@ -291,14 +292,14 @@ function linkHomePage() {
</template>
<style scoped>
/* 仅覆盖/补充必要的动态样式,其余样式由全局 layout.css 提供 */
/* 仅覆盖/补充必要的动态样式,其余样式由主题通道 base.layout.css 提供 */
/* 菜单图标(本地 bsch-icons 掩码类)与文字间距 */
.left-menu a .iconfont {
margin-right: 8px;
}
/* 面包屑「首页」垂直居中:layout.css 遗留的 .path-nav .icon-category{position:relative;top:1px}
/* 面包屑「首页」垂直居中:通道 base.layout.css 的 .path-nav .icon-category{position:relative;top:1px}
是给老 iconfont 字体字形做的基线补偿,换成 ysch mask 的 1em 方块后反而把图标顶偏;
这里用 inline-flex 真居中并关掉该补偿(不动压缩产物 layout.css,覆盖收在本组件内) */
这里用 inline-flex 真居中并关掉该补偿(不改通道产物,覆盖收在本组件内) */
.crumb-home {
display: inline-flex;
align-items: center;