初始化移动端提交

This commit is contained in:
chenbowen
2025-09-30 00:08:23 +08:00
parent 08784ca8f3
commit f2ffc65094
406 changed files with 55626 additions and 93 deletions

View File

@@ -0,0 +1,203 @@
<!--
默认头像组件
参考 zt-vue-element CropperAvatar 实现
支持默认头像逻辑头像URL优先否则显示用户昵称首字母
-->
<template>
<view class="avatar-container" @click="handleClick">
<!-- 有头像时显示图片 -->
<image
v-if="avatarUrl"
:src="avatarUrl"
:style="avatarStyle"
class="avatar-image"
mode="aspectFill"
@error="handleImageError"
/>
<!-- 无头像时显示首字母头像 -->
<view
v-else
class="avatar-initial"
:style="[avatarStyle, initialStyle]"
>
{{ initialText }}
</view>
<!-- 编辑按钮 -->
<view v-if="editable && !readonly" class="edit-mask">
<text class="edit-icon">+</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
// 组件属性
const props = defineProps({
// 头像URL
src: {
type: String,
default: ''
},
// 用户昵称,用于生成首字母
nickname: {
type: String,
default: ''
},
// 头像尺寸
size: {
type: [String, Number],
default: 80
},
// 是否可编辑
editable: {
type: Boolean,
default: false
},
// 是否只读
readonly: {
type: Boolean,
default: false
},
// 背景色(用于首字母头像)
bgColor: {
type: String,
default: '#0055A2'
},
// 文字色(用于首字母头像)
textColor: {
type: String,
default: '#ffffff'
},
// 自定义初始字符
initial: {
type: String,
default: ''
}
})
// 事件定义
const emit = defineEmits(['click', 'error'])
// 响应式数据
const avatarUrl = ref(props.src)
const imageError = ref(false)
// 计算属性
const avatarStyle = computed(() => {
const size = typeof props.size === 'number' ? `${props.size}rpx` : props.size
return {
width: size,
height: size,
borderRadius: '50%'
}
})
const initialStyle = computed(() => ({
backgroundColor: props.bgColor,
color: props.textColor,
fontSize: `${Math.floor(props.size * 0.4)}rpx`,
fontWeight: 'bold'
}))
// 生成初始字符逻辑
const initialText = computed(() => {
// 如果提供了自定义初始字符,使用它
if (props.initial) {
return props.initial.charAt(0).toUpperCase()
}
// 如果有昵称,取首字母
if (props.nickname) {
// 处理中文和英文
const firstChar = props.nickname.charAt(0)
// 如果是中文,直接使用
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
return firstChar
}
// 如果是英文,转大写
return firstChar.toUpperCase()
}
// 默认返回用户图标
return '用'
})
// 监听src变化
watch(() => props.src, (newSrc) => {
avatarUrl.value = newSrc
imageError.value = false
}, { immediate: true })
// 方法
const handleClick = () => {
if (!props.readonly) {
emit('click')
}
}
const handleImageError = () => {
imageError.value = true
avatarUrl.value = '' // 清空URL显示首字母头像
emit('error')
}
// 暴露方法给父组件
defineExpose({
refresh: () => {
avatarUrl.value = props.src
imageError.value = false
}
})
</script>
<style lang="scss" scoped>
.avatar-container {
position: relative;
display: inline-block;
overflow: hidden;
&:hover .edit-mask {
opacity: 1;
}
}
.avatar-image {
display: block;
object-fit: cover;
}
.avatar-initial {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--theme-primary, #0055A2);
color: #ffffff;
font-weight: bold;
text-align: center;
user-select: none;
}
.edit-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.edit-icon {
color: #ffffff;
font-size: 48rpx;
font-weight: bold;
}
</style>