Files
hfb_sys/frontend/src/shared/components/business/AuthImage.vue
T
yml 12f589f0df 二维码池批量上传与认证图片预览修复
- 后端新增批量创建二维码接口 POST /admin/chats/qrcodes/batch
- 前端二维码管理页支持多图拖拽批量上传
- 注册 qrcode 图片压缩场景(maxSide=2560, quality=0.95)
- 后端 normalizeScene 注册 qrcode scene
- 增强 AuthImage 组件支持 el-image 大图预览(previewSrcList)
- 修复 WithdrawalDetailDialog 提现凭证图片 401 问题
- 删除重复的 components/AuthImage.vue,统一使用共享组件
- 提交3剩余: 会话列表群类型图标、发布成功跳群、管理端路由等
2026-06-17 17:57:05 +08:00

129 lines
3.0 KiB
Vue

<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
const props = withDefaults(
defineProps<{
source: string
alt?: string
admin?: boolean
imageClass?: string
fallbackClass?: string
loading?: 'eager' | 'lazy'
decoding?: 'async' | 'sync' | 'auto'
previewSrcList?: string[]
previewTeleported?: boolean
fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
imageStyle?: any
}>(),
{
alt: '',
admin: false,
imageClass: '',
fallbackClass: '',
loading: 'lazy',
decoding: 'async',
previewTeleported: true,
}
)
const imageURL = ref('')
const previewURLs = ref<string[]>([])
const failed = ref(false)
const createdURLs: string[] = []
const usePreview = computed(() => !!props.previewSrcList?.length)
const fallbackText = computed(() => (failed.value ? '图片加载失败' : '图片加载中'))
function extractObjectKey(value: string) {
try {
const parsed = new URL(value, window.location.origin)
return parsed.searchParams.get('key') || ''
} catch {
return ''
}
}
function shouldFetchWithAuth(value: string) {
return value.includes('/api/files/object') || value.includes('/api/admin/files/object')
}
async function resolveURL(url: string): Promise<string> {
if (!url || !shouldFetchWithAuth(url)) return url
const key = extractObjectKey(url)
const blob =
props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(url)
const blobURL = URL.createObjectURL(blob)
createdURLs.push(blobURL)
return blobURL
}
function cleanup() {
createdURLs.forEach((u) => URL.revokeObjectURL(u))
createdURLs.length = 0
}
async function loadImage() {
cleanup()
imageURL.value = ''
previewURLs.value = []
failed.value = false
if (!props.source) {
failed.value = true
return
}
try {
imageURL.value = await resolveURL(props.source)
if (props.previewSrcList?.length) {
previewURLs.value = await Promise.all(props.previewSrcList.map(resolveURL))
}
} catch {
failed.value = true
}
}
watch(() => [props.source, props.admin, props.previewSrcList] as const, loadImage, {
immediate: true,
deep: true,
})
onBeforeUnmount(cleanup)
</script>
<template>
<el-image
v-if="imageURL && usePreview"
:src="imageURL"
:fit="fit"
:style="imageStyle"
:preview-src-list="previewURLs"
:preview-teleported="previewTeleported"
/>
<img
v-else-if="imageURL"
:class="imageClass"
:src="imageURL"
:alt="alt"
:loading="loading"
:decoding="decoding"
/>
<span v-else :class="['auth-image-fallback', fallbackClass]">{{ fallbackText }}</span>
</template>
<style scoped>
.auth-image-fallback {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
border: 1px dashed #d8dee8;
border-radius: 12px;
background: #f8fafc;
color: #94a3b8;
font-size: 13px;
font-weight: 700;
}
</style>