congming_huose-apk/common/components/AvatarUploader.vue

192 lines
4.8 KiB
Vue
Raw Normal View History

2025-11-08 11:30:06 +08:00
<template>
<view class="avatar-uploader">
<view class="avatar-wrapper" @click="onOpenSheet">
<image class="avatar-image" :src="modelValue || placeholder" mode="aspectFill"></image>
<slot name="badge">
<image class="avatar-badge" :src="badgeIcon" mode="aspectFit" v-if="badgeIcon"></image>
</slot>
</view>
<!-- 底部选择弹窗 -->
<view class="action-sheet" v-if="internalVisible" @click="hideActionSheet">
<view class="action-sheet-content" @click.stop>
<view class="action-sheet-item" @click="handleChoose('camera')">
<text class="action-sheet-text">{{ $i18n.t('takePhoto') }}</text>
</view>
<view class="action-sheet-item" @click="handleChoose('album')">
<text class="action-sheet-text">{{ $i18n.t('chooseFromAlbum') }}</text>
</view>
<view class="action-sheet-cancel" @click="hideActionSheet">
<text class="action-sheet-text">{{ $i18n.t('actionSheetCancel') }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'AvatarUploader',
props: {
modelValue: {
type: String,
default: ''
},
qiniuToken: {
type: String,
default: ''
},
qiniuUploadUrl: {
type: String,
default: 'https://up-z2.qiniup.com'
},
qiniuKeyPrefix: {
type: String,
default: 'smartmeter/img/'
},
placeholder: {
type: String,
default: ''
},
badgeIcon: {
type: String,
default: ''
}
},
data() {
return {
internalVisible: false
}
},
methods: {
// 点击显示上传方式
onOpenSheet() {
this.$emit('open');
this.internalVisible = true;
},
// 点击隐藏上传方式
hideActionSheet() {
this.internalVisible = false;
this.$emit('close');
},
// 上传图片
handleChoose(source) {
this.hideActionSheet();
const sourceType = source === 'camera' ? ['camera'] : ['album'];
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType,
success: (imgRes) => {
const tempPath = imgRes.tempFilePaths[0]
this.uploadToQiniu(tempPath)
},
fail: (err) => {
console.error('chooseImage failed:', err)
uni.showToast({
title: this.$i18n.t('imageOperationCancelled'),
icon: 'none',
duration: 3000
})
}
})
},
uploadToQiniu(filePath) {
if (!this.qiniuToken) {
uni.showToast({ title: this.$i18n.t('uploadFailed'), icon: 'none' })
return;
}
const key = this.qiniuKeyPrefix + this.generateGuid(20)
uni.uploadFile({
url: this.qiniuUploadUrl,
name: 'file',
filePath,
formData: { token: this.qiniuToken, key },
success: (res) => {
try {
const data = JSON.parse(res.data);
const finalUrl = 'https://api.ccttiot.com/' + data.key;
this.$emit('update:modelValue', finalUrl);
this.$emit('success', finalUrl, data);
uni.showToast({ title: this.$i18n.t('imageSelectedSuccess'), icon: 'success' })
} catch (e) {
console.error('parse upload response failed', e)
uni.showToast({ title: this.$i18n.t('uploadFailed'), icon: 'none' })
}
},
fail: (err) => {
console.error('upload failed:', err)
this.$emit('error', err)
uni.showToast({ title: this.$i18n.t('uploadFailed'), icon: 'none' })
}
})
},
// 随机数
generateGuid(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return 'static/' + result;
}
}
}
</script>
<style scoped lang="less">
.avatar-uploader {
position: relative;
}
.avatar-wrapper {
position: relative;
left: 50%;
transform: translateX(-50%);
width: 272rpx;
}
.avatar-image {
width: 272rpx;
height: 272rpx;
border-radius: 50%;
}
.avatar-badge {
width: 116rpx;
height: 72rpx;
position: absolute;
right: 0;
bottom: 0;
}
/* action sheet styles */
.action-sheet {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.action-sheet-content {
width: 100%;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding-bottom: 40rpx;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.action-sheet-item { padding: 30rpx 40rpx; border-bottom: 1rpx solid #f0f0f0; text-align: center; }
.action-sheet-item:active { background-color: #f5f5f5; }
.action-sheet-cancel { padding: 30rpx 40rpx; text-align: center; margin-top: 20rpx; background-color: #f8f8f8; }
.action-sheet-cancel:active { background-color: #e8e8e8; }
.action-sheet-text { font-size: 32rpx; color: #333; }
.action-sheet-cancel .action-sheet-text { color: #666; }
</style>