103 lines
4.1 KiB
Vue
103 lines
4.1 KiB
Vue
<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>
|