feat(admin): photo upload in album photos tab (same staging pipeline as app)
- 照片页签工具栏加「上传照片」(多选 jpeg/png/heic)→ /v1/uploads
init → PUT staging → complete(clientTaskId 幂等;categoryId 随当前分类筛选)
- 逐张结果:部分成功语义(成功 N 张 toast + 失败明细前 3 条);完成后 reload
- 类型 UploadInitPayload/UploadSession/UploadCompleteResponse + api/uploads.ts
- 问题1 修复说明:详情页 404 系并行部署把 2618ad3 server 覆盖回 8e674a9,
已重滚双栈至 ui06-2618ad3(assets 端点恢复 200,详情页零失败请求)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,25 @@
|
||||
:label="categoryLabel(category)"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
:loading="uploadingCount > 0"
|
||||
data-test="photo-upload-btn"
|
||||
@click="photoInput?.click()"
|
||||
>
|
||||
{{ t('albumsManage.photos.upload') }}
|
||||
<span v-if="uploadingCount > 0">({{ uploadingCount }})</span>
|
||||
</ElButton>
|
||||
<input
|
||||
ref="photoInput"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/heic"
|
||||
multiple
|
||||
class="hidden"
|
||||
data-test="photo-upload-input"
|
||||
@change="onPhotosChange"
|
||||
/>
|
||||
<div class="seg push">
|
||||
<ElTooltip :content="t('albumsManage.list.viewGrid')" placement="top">
|
||||
<button
|
||||
@@ -252,6 +271,7 @@
|
||||
summarizeBatchHideResults,
|
||||
type BatchHideSummary
|
||||
} from '@/utils/albums/batchHide'
|
||||
import { fetchCompleteUpload, fetchInitUpload } from '@/api/uploads'
|
||||
import PhotoGrid from '@/components/business/albums/PhotoGrid.vue'
|
||||
import PhotoPreviewDialog from '@/components/business/albums/PhotoPreviewDialog.vue'
|
||||
|
||||
@@ -289,6 +309,67 @@
|
||||
gridSelected.value = next
|
||||
}
|
||||
|
||||
// ── 上传照片(与 App 同链路:init → PUT staging → complete;逐张结果) ──
|
||||
const photoInput = ref<HTMLInputElement | null>(null)
|
||||
const uploadingCount = ref(0)
|
||||
|
||||
async function onPhotosChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files ?? [])
|
||||
input.value = '' // 允许连续重选
|
||||
if (files.length === 0) return
|
||||
const CONTENT_TYPES = new Set(['image/jpeg', 'image/png', 'image/heic'])
|
||||
let ok = 0
|
||||
const failures: string[] = []
|
||||
for (const file of files) {
|
||||
if (!CONTENT_TYPES.has(file.type)) {
|
||||
failures.push(`${file.name}(${t('albumsManage.photos.uploadBadType')})`)
|
||||
continue
|
||||
}
|
||||
uploadingCount.value++
|
||||
try {
|
||||
// ① init:clientTaskId 幂等(每次会话重生成;重试时复用同值)
|
||||
const clientTaskId =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
const init = await fetchInitUpload({
|
||||
albumId: props.album.id,
|
||||
clientTaskId,
|
||||
filename: file.name,
|
||||
bytes: file.size,
|
||||
contentType: file.type as Api.Album.UploadInitPayload['contentType'],
|
||||
categoryId: categoryFilter.value === '' ? undefined : categoryFilter.value
|
||||
})
|
||||
// ② PUT staging(裸 fetch:预签名自带凭证;Content-Type 与 init 一致)
|
||||
const put = await fetch(init.credential.url, {
|
||||
method: init.credential.method,
|
||||
headers: { ...init.credential.headers, 'Content-Type': file.type },
|
||||
body: file
|
||||
})
|
||||
if (!put.ok) throw new Error(`PUT ${put.status}`)
|
||||
// ③ complete:核验受理(处理异步,状态经列表观察)
|
||||
await fetchCompleteUpload(init.uploadId)
|
||||
ok++
|
||||
} catch (e) {
|
||||
failures.push(`${file.name}(${(e as Error).message})`)
|
||||
} finally {
|
||||
uploadingCount.value--
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
ElMessage.warning(
|
||||
t('albumsManage.photos.uploadResult', { ok, fail: failures.length }) +
|
||||
':' +
|
||||
failures.slice(0, 3).join('、') +
|
||||
(failures.length > 3 ? ' …' : '')
|
||||
)
|
||||
} else {
|
||||
ElMessage.success(t('albumsManage.photos.uploadResultAllOk', { ok }))
|
||||
}
|
||||
if (ok > 0) reload()
|
||||
}
|
||||
|
||||
// ── 管理大图(示例 16) ──
|
||||
const viewerVisible = ref(false)
|
||||
const viewerPhoto = ref<Api.Album.AdminPhoto | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user