@@ -5,42 +5,37 @@ import type { UploadFile, UploadInstance } from 'element-plus'
55
66defineOptions ({ name: ' MkEditAvatar' })
77
8- const props = withDefaults (defineProps <{ editable ? : boolean ; size? : number }>(), {
9- editable: true ,
8+ const props = withDefaults (defineProps <{ disabled ? : boolean ; size? : number }>(), {
9+ disabled: false ,
1010 size: 32 ,
1111})
1212const avatar = defineModel <string >({ default: ' ' })
13- const emit = defineEmits <{ change: [icon : string , file : File | null ] }>()
13+ const emit = defineEmits <{ change: [file : File | null ] }>()
1414defineSlots <{ default(props : { icon: string | undefined }): unknown }>()
1515
1616// 头像编辑草稿:确认前不修改外部值。
1717const visible = ref (false )
1818const logoMode = ref <' default' | ' custom' >(' default' )
19- const draftIcon = ref (' ' )
19+ const previewIcon = ref (' ' )
2020const draftFile = ref <File | null >(null )
21- const reading = ref (false )
2221const referenceRef = useTemplateRef <HTMLElement >(' referenceRef' )
2322const uploadRef = useTemplateRef <UploadInstance >(' uploadRef' )
2423
25- let fileReader: FileReader | undefined
26-
2724function open() {
28- if (! props . editable || visible .value ) return
25+ if (visible .value ) return
2926 logoMode .value = avatar .value ? ' custom' : ' default'
30- draftIcon .value = logoMode .value === ' custom' ? avatar .value : ' '
27+ previewIcon .value = logoMode .value === ' custom' ? avatar .value : ' '
3128 draftFile .value = null
3229 visible .value = true
3330}
3431
3532function close() {
3633 visible .value = false
37- fileReader ?.abort ()
38- reading .value = false
3934}
4035
4136function clearDraft() {
4237 if (visible .value ) return
43- draftIcon .value = ' '
38+ previewIcon .value = ' '
4439 draftFile .value = null
4540 uploadRef .value ?.clearFiles ()
4641}
@@ -51,11 +46,14 @@ function closeOutside(event: MouseEvent) {
5146}
5247
5348function confirm() {
54- if (reading .value || (logoMode .value === ' custom' && ! draftIcon .value )) return
55- const icon = logoMode .value === ' default' ? ' ' : draftIcon .value
56- const file = logoMode .value === ' custom' ? draftFile .value : null
57- avatar .value = icon
58- emit (' change' , icon , file )
49+ if (logoMode .value === ' custom' && ! previewIcon .value ) return
50+ if (logoMode .value === ' default' ) {
51+ avatar .value = ' '
52+ emit (' change' , null )
53+ } else if (draftFile .value ) {
54+ avatar .value = previewIcon .value
55+ emit (' change' , draftFile .value )
56+ }
5957 close ()
6058}
6159
@@ -72,39 +70,40 @@ function selectImage(uploadFile: UploadFile) {
7270 ElMessage .warning (' 文件大小不能超过 10MB' )
7371 return
7472 }
75- fileReader ?.abort ()
76- const reader = new FileReader ()
77- fileReader = reader
78- reading .value = true
79- reader .onload = () => {
80- draftIcon .value = String (reader .result )
81- draftFile .value = file
82- reading .value = false
83- }
84- reader .onerror = () => {
85- reading .value = false
86- ElMessage .error (' 图片读取失败,请重新选择' )
73+ previewIcon .value = URL .createObjectURL (file )
74+ draftFile .value = file
75+ }
76+
77+ // 临时地址由组件统一管理,仍用于头像或弹层预览时不释放。
78+ function releaseUnusedPreview(icon : string ) {
79+ if (icon .startsWith (' blob:' ) && icon !== avatar .value && icon !== previewIcon .value ) {
80+ URL .revokeObjectURL (icon )
8781 }
88- reader .readAsDataURL (file )
8982}
9083
9184watch (
92- () => props .editable ,
93- (editable ) => {
94- if (! editable ) close ()
85+ [avatar , previewIcon ],
86+ (_current , [previousAvatar , previousPreview ]) => {
87+ releaseUnusedPreview (previousAvatar )
88+ if (previousPreview !== previousAvatar ) releaseUnusedPreview (previousPreview )
9589 },
90+ { flush: ' sync' },
9691)
97- onBeforeUnmount (() => fileReader ?.abort ())
92+ onBeforeUnmount (() => {
93+ if (previewIcon .value .startsWith (' blob:' ) && previewIcon .value !== avatar .value ) URL .revokeObjectURL (previewIcon .value )
94+ if (avatar .value .startsWith (' blob:' )) URL .revokeObjectURL (avatar .value )
95+ })
9896 </script >
9997
10098<template >
101- <el-popover :visible =" visible" :width =" 414" placement =" bottom-start" @after-leave =" clearDraft" >
99+ <!-- 单向控制显隐,避免选择文件时鼠标移出触发自动关闭。 -->
100+ <el-popover :visible =" visible" :disabled =" props.disabled" :width =" 414" placement =" bottom-start" @after-leave =" clearDraft" >
102101 <template #reference >
103102 <!-- 悬停修改头像 -->
104103 <span
105104 ref =" referenceRef"
106105 class =" inline-flex shrink-0 [& >*]:h-full! [& >*]:w-full!"
107- :class =" { 'cursor-pointer': props.editable }"
106+ :class =" { 'cursor-pointer': ! props.disabled }"
108107 :style =" { width: `${props.size}px`, height: `${props.size}px` }"
109108 @mouseenter =" open"
110109 >
@@ -114,7 +113,7 @@ onBeforeUnmount(() => fileReader?.abort())
114113
115114 <div v-click-outside =" closeOutside" class =" p-4" @keydown.esc.stop =" close" @click.stop >
116115 <div class =" mb-1" >Logo 设置</div >
117- <el-radio-group v-model =" logoMode" :disabled = " reading " class =" mb-2" >
116+ <el-radio-group v-model =" logoMode" class =" mb-2" >
118117 <el-radio value =" default" >默认 Logo</el-radio >
119118 <el-radio value =" custom" >自定义上传</el-radio >
120119 </el-radio-group >
@@ -130,11 +129,10 @@ onBeforeUnmount(() => fileReader?.abort())
130129 accept =" image/jpeg,image/png,image/gif"
131130 :auto-upload =" false"
132131 :show-file-list =" false"
133- :disabled =" reading"
134132 :on-change =" selectImage"
135133 >
136- <el-avatar v-if =" draftIcon " :size =" 80" shape =" square" >
137- <img :src =" draftIcon " class =" object-cover" />
134+ <el-avatar v-if =" previewIcon " :size =" 80" shape =" square" >
135+ <img :src =" previewIcon " class =" object-cover" />
138136 </el-avatar >
139137
140138 <div v-else class =" flex-center h-20 w-20 rounded-md border border-dashed bg-N200 hover:border-primary" >
@@ -148,7 +146,7 @@ onBeforeUnmount(() => fileReader?.abort())
148146 <!-- 取消头像修改 -->
149147 <el-button plain @click =" close" >取消</el-button >
150148 <!-- 确认头像修改 -->
151- <el-button type =" primary" :loading = " reading " : disabled =" logoMode === 'custom' && !draftIcon " @click =" confirm" >确定</el-button >
149+ <el-button type =" primary" :disabled =" logoMode === 'custom' && !previewIcon " @click =" confirm" >确定</el-button >
152150 </div >
153151 </div >
154152 </el-popover >
0 commit comments