Files
pda-uniapp/utils/evidence-upload-queue.js
T
2026-08-10 11:29:03 +08:00

43 lines
2.7 KiB
JavaScript

/**
* Small, observable upload queue for PDA evidence photos.
* The queue deliberately keeps local paths only for the active session; callers
* decide when it is safe to persist/clear them (see PDA session policy).
*/
export function createEvidenceUploadQueue({ upload, onChange } = {}) {
const entries = []
const notify = () => onChange?.(entries.map((item) => ({ ...item })))
const snapshot = () => entries.map((item) => ({ ...item }))
const add = (payload) => {
if (!payload?.evidenceType || !payload?.localPath) throw new Error('缺少证据类型或本地照片')
// Evidence is append-only by type. Incorrect or duplicate photos are
// explicitly voided, preserving the local audit trail instead of replacing.
const entry = { id: payload.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, ...payload, status: 'PENDING', attempts: 0, progress: 0, error: '' }
entries.push(entry); notify(); return entry
}
const cancel = (id) => { const item = entries.find((entry) => entry.id === id); if (!item || item.status === 'UPLOADING') return false; item.status = 'CANCELLED'; notify(); return true }
const voidEntry = (id, reason, actor) => {
const item = entries.find((entry) => entry.id === id)
if (!item || item.status === 'UPLOADING' || item.status === 'VOIDED') return false
if (!String(reason || '').trim()) throw new Error('作废原因不能为空')
item.status = 'VOIDED'; item.voidReason = String(reason).trim(); item.voidedBy = actor || null; item.voidedAt = new Date().toISOString(); notify(); return true
}
const retry = (id) => { const item = entries.find((entry) => entry.id === id); if (!item || !['FAILED', 'CANCELLED'].includes(item.status)) return false; item.status = 'PENDING'; item.error = ''; notify(); return true }
async function flush() {
if (typeof upload !== 'function') throw new Error('未配置文件上传器')
for (const item of entries) {
if (item.status !== 'PENDING') continue
item.status = 'UPLOADING'; item.attempts += 1; item.error = ''; notify()
try {
const result = await upload(item, (progress) => { item.progress = progress; notify() })
item.status = 'SUCCEEDED'; item.progress = 100; item.result = result; notify()
} catch (error) {
item.status = 'FAILED'; item.error = error?.message || '上传失败'; item.network = Boolean(error?.network); notify()
}
}
return snapshot()
}
const latest = (evidenceType) => [...entries].reverse().find((item) => item.evidenceType === evidenceType && item.status !== 'VOIDED') || null
const active = (evidenceType) => entries.filter((item) => item.evidenceType === evidenceType && item.status === 'SUCCEEDED')
return { add, cancel, voidEntry, retry, flush, latest, active, snapshot }
}