feat(record): 留痕详情照片预览

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-21 11:41:41 +08:00
parent 2ae7aec861
commit b21d7fb163
2 changed files with 53 additions and 1 deletions

View File

@ -122,6 +122,11 @@ export function getRecord(id: string): Promise<ServiceRecordDetail> {
return http.get<ServiceRecordDetail>(`/record/${id}`) return http.get<ServiceRecordDetail>(`/record/${id}`)
} }
/** 照片 GET 预签名 URL(临时预览/下载,默认 1 小时有效)。 */
export function getPhotoUrl(photoId: string): Promise<{ url: string }> {
return http.get<{ url: string }>(`/record/photo/${photoId}/url`)
}
/** 垫付台账:某老人 is_prepaid=1 的留痕列表。 */ /** 垫付台账:某老人 is_prepaid=1 的留痕列表。 */
export function listPrepaidRecords(elderId: string): Promise<ServiceRecord[]> { export function listPrepaidRecords(elderId: string): Promise<ServiceRecord[]> {
return http.get<ServiceRecord[]>('/record/prepaid-list', { params: { elderId } }) return http.get<ServiceRecord[]>('/record/prepaid-list', { params: { elderId } })

View File

@ -70,7 +70,21 @@
<el-divider content-position="left">照片</el-divider> <el-divider content-position="left">照片</el-divider>
<el-table v-if="detail.photos.length > 0" :data="detail.photos" border size="small"> <el-table v-if="detail.photos.length > 0" :data="detail.photos" border size="small">
<el-table-column prop="filePath" label="文件 Key(MinIO)" min-width="280" /> <el-table-column label="预览" min-width="140" align="center">
<template #default="{ row }">
<el-tooltip v-if="photoUrlOf(row.id)" :content="row.filePath" placement="top">
<el-image
:src="photoUrlOf(row.id)"
:preview-src-list="[photoUrlOf(row.id)]"
preview-teleported
fit="cover"
class="record-page__photo"
/>
</el-tooltip>
<span v-else-if="photoFailed(row.id)" class="text-xs text-text-placeholder">获取失败</span>
<span v-else class="text-xs text-text-placeholder">加载中…</span>
</template>
</el-table-column>
<el-table-column prop="fileSize" label="大小(字节)" width="120" align="right" /> <el-table-column prop="fileSize" label="大小(字节)" width="120" align="right" />
<el-table-column label="上传时间" width="160"> <el-table-column label="上传时间" width="160">
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
@ -115,6 +129,7 @@ import {
EXEC_RESULT_TAG_MAP, EXEC_RESULT_TAG_MAP,
LOCATE_STATUS_TAG_MAP, LOCATE_STATUS_TAG_MAP,
RECORD_STATUS_TAG_MAP, RECORD_STATUS_TAG_MAP,
getPhotoUrl,
getRecord, getRecord,
listRecords, listRecords,
type ServiceRecord, type ServiceRecord,
@ -177,6 +192,8 @@ const list = ref<ServiceRecord[]>([])
const detailVisible = ref(false) const detailVisible = ref(false)
const detailLoading = ref(false) const detailLoading = ref(false)
const detail = ref<ServiceRecordDetail | null>(null) const detail = ref<ServiceRecordDetail | null>(null)
/** photoId → presigned url;null 表示获取失败,缺失表示加载中。 */
const photoUrls = ref<Record<string, string | null>>({})
const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined) const str = (v: unknown): string | undefined => (typeof v === 'string' && v !== '' ? v : undefined)
@ -246,9 +263,11 @@ async function loadMaps(): Promise<void> {
async function openDetail(row: Record<string, any>): Promise<void> { async function openDetail(row: Record<string, any>): Promise<void> {
detailVisible.value = true detailVisible.value = true
detail.value = null detail.value = null
photoUrls.value = {}
detailLoading.value = true detailLoading.value = true
try { try {
detail.value = await getRecord(String(row.id)) detail.value = await getRecord(String(row.id))
if (detail.value) void loadPhotoUrls(detail.value.photos)
} catch (e) { } catch (e) {
if (e instanceof ApiError) ElMessage.error(e.message) if (e instanceof ApiError) ElMessage.error(e.message)
} finally { } finally {
@ -256,6 +275,30 @@ async function openDetail(row: Record<string, any>): Promise<void> {
} }
} }
/** 为每张照片并发取 presigned url;失败置 null → 模板显示「获取失败」。 */
async function loadPhotoUrls(photos: ServiceRecordDetail['photos']): Promise<void> {
const next: Record<string, string | null> = {}
await Promise.all(
photos.map(async (p) => {
try {
const { url } = await getPhotoUrl(p.id)
next[p.id] = url
} catch {
next[p.id] = null
}
}),
)
photoUrls.value = next
}
function photoUrlOf(id: string): string {
return photoUrls.value[id] ?? ''
}
function photoFailed(id: string): boolean {
return photoUrls.value[id] === null
}
onMounted(() => { onMounted(() => {
void loadMaps() void loadMaps()
fetchList() fetchList()
@ -278,4 +321,8 @@ onMounted(() => {
.record-page__detail { .record-page__detail {
@apply max-h-[60vh] overflow-auto; @apply max-h-[60vh] overflow-auto;
} }
.record-page__photo {
@apply block h-16 w-16 cursor-zoom-in rounded object-cover;
}
</style> </style>