From c68c5985f67d5f92a629798dce7a50ce7838d69c Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Wed, 24 Sep 2025 01:23:12 +0800 Subject: [PATCH] refine BaseUserAvatar styling --- frontend_nuxt/components/BaseUserAvatar.vue | 197 +++++++++++++++++++- 1 file changed, 191 insertions(+), 6 deletions(-) diff --git a/frontend_nuxt/components/BaseUserAvatar.vue b/frontend_nuxt/components/BaseUserAvatar.vue index bfaae5071..ea4fcb12f 100644 --- a/frontend_nuxt/components/BaseUserAvatar.vue +++ b/frontend_nuxt/components/BaseUserAvatar.vue @@ -6,7 +6,8 @@ :style="wrapperStyle" v-bind="wrapperAttrs" > - + + {{ userInitial }} @@ -69,19 +70,121 @@ const resolvedLink = computed(() => { const altText = computed(() => props.alt || '用户头像') +const identifier = computed(() => { + if (props.userId !== null && props.userId !== undefined && props.userId !== '') { + return String(props.userId) + } + if (props.alt && props.alt.trim()) { + return props.alt.trim() + } + return altText.value +}) + +const initialSource = computed(() => { + if (props.alt && props.alt.trim() && props.alt.trim() !== '用户头像') { + return props.alt.trim() + } + if (attrs.title && typeof attrs.title === 'string' && attrs.title.trim()) { + return attrs.title.trim() + } + if (props.userId !== null && props.userId !== undefined && props.userId !== '') { + return String(props.userId) + } + return '' +}) + +const isDefaultAvatar = computed(() => currentSrc.value === DEFAULT_AVATAR) + +function parseCssSize(value) { + if (typeof value === 'number' && Number.isFinite(value)) { + return { numeric: value, unit: 'px' } + } + if (typeof value !== 'string') return null + const trimmed = value.trim() + if (!trimmed) return null + const directNumber = Number(trimmed) + if (!Number.isNaN(directNumber)) { + return { numeric: directNumber, unit: 'px' } + } + const match = trimmed.match(/^(-?\d*\.?\d+)([a-z%]+)$/i) + if (!match) return null + return { numeric: Number(match[1]), unit: match[2] } +} + const sizeStyle = computed(() => { if (!props.width && props.width !== 0) return null const value = typeof props.width === 'number' ? `${props.width}px` : props.width if (!value) return null - return { width: value, height: value } + const parsed = parseCssSize(value) + const style = { width: value, height: value, '--avatar-size': value } + if (parsed && Number.isFinite(parsed.numeric)) { + const computedFont = (parsed.numeric * 0.42).toFixed(2) + const normalized = computedFont.replace(/\.00$/, '') + style['--avatar-font-size'] = `${normalized}${parsed.unit}` + } + return style +}) + +function stringToColorSeed(value) { + if (!value) return 0 + let hash = 0 + for (let i = 0; i < value.length; i += 1) { + hash = (hash << 5) - hash + value.charCodeAt(i) + hash |= 0 + } + return Math.abs(hash) +} + +const accentStyle = computed(() => { + if (!isDefaultAvatar.value) return null + const seed = stringToColorSeed(identifier.value) + const hue = seed % 360 + const altHue = (hue + 37) % 360 + const saturation = 72 + const lightness = 78 + const start = `hsl(${hue}, ${saturation}%, ${Math.min(lightness + 8, 95)}%)` + const end = `hsl(${altHue}, ${Math.max(saturation - 12, 45)}%, ${Math.max(lightness - 12, 48)}%)` + return { + '--avatar-background': `linear-gradient(135deg, ${start}, ${end})`, + '--avatar-border-color': `hsla(${hue}, ${Math.max(saturation - 24, 32)}%, ${Math.max( + lightness - 35, + 28, + )}%, 0.55)`, + '--avatar-text-color': '#ffffff', + } }) const wrapperStyle = computed(() => { const attrStyle = attrs.style - return [sizeStyle.value, attrStyle] + return [ + { '--avatar-font-size': 'clamp(0.75rem, 0.6rem + 0.4vw, 1.75rem)' }, + sizeStyle.value, + accentStyle.value, + attrStyle, + ] }) -const wrapperClass = computed(() => [attrs.class, { 'is-rounded': props.rounded }]) +const wrapperClass = computed(() => [ + attrs.class, + { + 'is-rounded': props.rounded, + 'has-default': isDefaultAvatar.value, + 'is-interactive': !props.disableLink && Boolean(resolvedLink.value), + }, +]) + +const imageClass = computed(() => ['base-user-avatar-img', { 'is-default': isDefaultAvatar.value }]) + +const userInitial = computed(() => { + const source = initialSource.value || '' + const trimmed = source.trim() + if (!trimmed) return '' + const match = trimmed.match(/[\p{L}\p{N}]/u) + if (match && match[0]) return match[0].toUpperCase() + return trimmed.charAt(0).toUpperCase() +}) + +const showInitial = computed(() => isDefaultAvatar.value && Boolean(userInitial.value)) const wrapperAttrs = computed(() => { const { class: _class, style: _style, ...rest } = attrs @@ -97,11 +200,28 @@ function onError() {