import { ref } from 'vue' import { supabase } from '@/lib/supabase' export interface UploadItem { id: string file: File preview: string cloudCategoryId: number | 'other' | null customCloudType: string locationName: string description: string isHidden: boolean latitude: number | null longitude: number | null capturedAt: string errors: Record } let nextId = 0 function readFileAsArrayBuffer(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result as ArrayBuffer) reader.onerror = reject reader.readAsArrayBuffer(file) }) } function extractExifDate(buffer: ArrayBuffer): string | null { const view = new DataView(buffer) if (view.getUint16(0, false) !== 0xffd8) return null let offset = 2 while (offset < view.byteLength - 2) { const marker = view.getUint16(offset, false) offset += 2 if ((marker & 0xff00) !== 0xff00) break const length = view.getUint16(offset, false) offset += 2 if (marker === 0xffe1) { const exifStart = offset const header = String.fromCharCode( view.getUint8(exifStart), view.getUint8(exifStart + 1), view.getUint8(exifStart + 2), view.getUint8(exifStart + 3), ) if (header !== 'Exif') break const tiffStart = exifStart + 6 const isLE = view.getUint16(tiffStart, false) === 0x4949 const ifd0Offset = view.getUint32(tiffStart + 4, isLE) const ifdStart = tiffStart + ifd0Offset const numEntries = view.getUint16(ifdStart, isLE) for (let i = 0; i < numEntries; i++) { const entryStart = ifdStart + 2 + i * 12 if (entryStart + 12 > buffer.byteLength) break const tag = view.getUint16(entryStart, isLE) if (tag === 0x9003) { const dataOffset = view.getUint32(entryStart + 8, isLE) const dateStr = Array.from({ length: 20 }, (_, j) => String.fromCharCode(view.getUint8(tiffStart + dataOffset + j)), ).join('') const iso = dateStr.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3') const d = new Date(iso) if (!isNaN(d.getTime())) return d.toISOString() } } } offset += length - 2 } return null } export function useUpload() { const items = ref([]) const uploading = ref(false) const overallProgress = ref(0) const currentItemIndex = ref(0) const totalItems = ref(0) async function addFiles(files: File[]) { const imageFiles = files.filter(f => f.type.startsWith('image/')) for (const file of imageFiles) { let capturedAt = new Date().toISOString() try { const buffer = await readFileAsArrayBuffer(file) const exifDate = extractExifDate(buffer) if (exifDate) capturedAt = exifDate } catch { // fallback to now } items.value.push({ id: String(++nextId), file, preview: URL.createObjectURL(file), cloudCategoryId: null, customCloudType: '', locationName: '', description: '', isHidden: false, latitude: null, longitude: null, capturedAt, errors: {}, }) } } function removeItem(id: string) { const idx = items.value.findIndex(i => i.id === id) if (idx !== -1) { URL.revokeObjectURL(items.value[idx].preview) items.value.splice(idx, 1) } } function validateItem(item: UploadItem): boolean { const errors: Record = {} if (!item.cloudCategoryId) { errors.cloudCategory = '请选择类别' } if (item.cloudCategoryId === 'other' && !item.customCloudType.trim()) { errors.cloudCategory = '请输入自定义云型名称' } if (!item.capturedAt) { errors.capturedAt = '请选择拍摄时间' } item.errors = errors return Object.keys(errors).length === 0 } function validateAll(): boolean { let allValid = true for (const item of items.value) { if (!validateItem(item)) { allValid = false } } return allValid } function blurCoordinate(value: number): number { return Math.round(value * 100) / 100 } async function uploadAll(): Promise { if (!validateAll()) return false uploading.value = true totalItems.value = items.value.length currentItemIndex.value = 0 overallProgress.value = 0 const userId = (await supabase.auth.getUser()).data.user?.id if (!userId) { uploading.value = false return false } try { for (let i = 0; i < items.value.length; i++) { const item = items.value[i] currentItemIndex.value = i + 1 overallProgress.value = Math.round(i / items.value.length * 100) const ext = item.file.name.split('.').pop() || 'jpg' const basePath = `${userId}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}` const imagePath = `${basePath}.${ext}` const { error: imgError } = await supabase.storage .from('clouds') .upload(imagePath, item.file, { upsert: false }) if (imgError) throw imgError overallProgress.value = Math.round((i + 0.5) / items.value.length * 100) const { data: { publicUrl: imageUrl } } = supabase.storage.from('clouds').getPublicUrl(imagePath) const latitude = item.latitude ? blurCoordinate(item.latitude) : null const longitude = item.longitude ? blurCoordinate(item.longitude) : null const { error: dbError } = await supabase .from('clouds') .insert({ user_id: userId, cloud_type_id: item.cloudCategoryId === 'other' ? null : item.cloudCategoryId, custom_cloud_type: item.cloudCategoryId === 'other' ? (item.customCloudType.trim() || null) : null, image_url: imageUrl, thumbnail_url: null, latitude, longitude, location_name: item.locationName || null, description: item.description.trim() || null, captured_at: item.capturedAt, is_hidden: item.isHidden, }) if (dbError) throw dbError overallProgress.value = Math.round((i + 1) / items.value.length * 100) } for (const item of items.value) { URL.revokeObjectURL(item.preview) } items.value = [] return true } catch { return false } finally { uploading.value = false } } return { items, uploading, overallProgress, currentItemIndex, totalItems, addFiles, removeItem, validateItem, validateAll, uploadAll } }