test: fix raw upload assertions
This commit is contained in:
+16
-1
@@ -1084,13 +1084,28 @@ function listFootprints(d, query) {
|
||||
if (!row) throw new Error('附件不存在');
|
||||
withTx(d, () => {
|
||||
const h = hlcNext(d);
|
||||
d.prepare('UPDATE attachment SET remote_url=?, remote_thumb=?, hlc=? WHERE id=?')
|
||||
d.prepare('UPDATE attachment SET remote_url=COALESCE(?, remote_url), remote_thumb=COALESCE(?, remote_thumb), hlc=? WHERE id=?')
|
||||
.run(payload.remote_url ?? null, payload.remote_thumb ?? null, h, row.id);
|
||||
touchOplog(d, 'attachment', row.uuid, 'upsert', h);
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'attachment_set_thumb': {
|
||||
// 客户端上传原始二进制后回填缩略图(本地元数据,不进 oplog)
|
||||
const row = d.prepare('SELECT * FROM attachment WHERE id=?').get(payload.id);
|
||||
if (!row) throw new Error('附件不存在');
|
||||
const tb = Buffer.from(payload.thumb_b64 || '', 'base64');
|
||||
if (!tb.length) throw new Error('缩略图为空');
|
||||
const rel = path.join('attachments', String(row.owner_id), crypto.randomUUID() + '_thumb.webp');
|
||||
const abs = path.join(DATA_DIR, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, tb);
|
||||
d.prepare('UPDATE attachment SET thumb_rel_path=? WHERE id=?')
|
||||
.run(rel.split(path.sep).join('/'), row.id);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case 'view_list':
|
||||
return { items: d.prepare('SELECT * FROM saved_view WHERE deleted_at IS NULL ORDER BY id').all().map(v => ({
|
||||
...v, filter: JSON.parse(v.filter || '{}'), sort: JSON.parse(v.sort || '{}'), is_default: !!v.is_default
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fmtSize } from '@/lib/format';
|
||||
import { makeThumb } from '@/lib/img';
|
||||
import type { Attachment } from '@/lib/types';
|
||||
|
||||
export interface PendingAtt { file_name: string; mime?: string; size: number; data_b64: string; thumb_b64?: string; preview?: string; }
|
||||
export interface PendingAtt { file: File; file_name: string; mime?: string; size: number; data_b64: string; thumb_b64?: string; preview?: string; }
|
||||
|
||||
export function AttachmentPicker({ items, pending, onPick, onDeleteSaved, onDeletePending, onOpenSaved, uploading }: {
|
||||
items: Attachment[];
|
||||
@@ -83,11 +83,26 @@ export function AttachmentPicker({ items, pending, onPick, onDeleteSaved, onDele
|
||||
}
|
||||
|
||||
export async function fileToPending(file: File): Promise<PendingAtt> {
|
||||
// 大图压缩:>3MB 的照片重编码为 2560px JPEG q0.85(佐证材料足够清晰,上传体积降 60-80%)
|
||||
let uploadFile: File = file;
|
||||
if (file.type === 'image/jpeg' && file.size > 3 * 1024 * 1024) {
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const scale = Math.min(1, 2560 / Math.max(bitmap.width, bitmap.height));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.round(bitmap.width * scale);
|
||||
canvas.height = Math.round(bitmap.height * scale);
|
||||
canvas.getContext('2d')?.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
const blob = await new Promise<Blob | null>(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
||||
if (blob && blob.size < file.size) uploadFile = new File([blob], file.name.replace(/\.jpe?g$/i, '') + '.jpg', { type: 'image/jpeg' });
|
||||
bitmap.close?.();
|
||||
} catch { /* 压缩失败用原图 */ }
|
||||
}
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ''));
|
||||
reader.onerror = () => reject(new Error('读取文件失败'));
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(uploadFile);
|
||||
});
|
||||
const comma = result.indexOf(',');
|
||||
const data_b64 = comma >= 0 ? result.slice(comma + 1) : result;
|
||||
@@ -100,9 +115,10 @@ export async function fileToPending(file: File): Promise<PendingAtt> {
|
||||
} catch { thumb_b64 = undefined; }
|
||||
}
|
||||
return {
|
||||
file: uploadFile,
|
||||
file_name: file.name,
|
||||
mime: file.type || undefined,
|
||||
size: file.size,
|
||||
mime: uploadFile.type || undefined,
|
||||
size: uploadFile.size,
|
||||
data_b64,
|
||||
thumb_b64,
|
||||
preview: file.type?.startsWith('image/') ? URL.createObjectURL(file) : undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
attachmentAdd, attachmentDelete, attachmentList,
|
||||
attachmentDelete, attachmentList, attachmentUploadRaw,
|
||||
footprintGet, footprintList, footprintUpsert, listDimensions, listTaxonomy, listTypes
|
||||
} from '@/lib/api';
|
||||
import { AttachmentPreview } from '@/components/common/AttachmentPreview';
|
||||
@@ -157,7 +157,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
||||
};
|
||||
const saved = await footprintUpsert(input);
|
||||
for (const p of pendingAtts) {
|
||||
await attachmentAdd({ owner_id: saved.id, file_name: p.file_name, mime: p.mime, size: p.size, data_b64: p.data_b64, thumb_b64: p.thumb_b64 });
|
||||
await attachmentUploadRaw(saved.id, p.file, p.thumb_b64);
|
||||
}
|
||||
return saved.id;
|
||||
},
|
||||
@@ -348,7 +348,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
||||
continue;
|
||||
}
|
||||
const p = await fileToPending(f);
|
||||
await attachmentAdd({ owner_id: footprintId, file_name: p.file_name, mime: p.mime, size: p.size, data_b64: p.data_b64, thumb_b64: p.thumb_b64 });
|
||||
await attachmentUploadRaw(footprintId, p.file, p.thumb_b64);
|
||||
done++;
|
||||
}
|
||||
if (done > 0) {
|
||||
|
||||
@@ -135,6 +135,20 @@ export const attachmentList = (owner_id: number) => call<{ items: Attachment[] }
|
||||
export const attachmentDelete = (id: number) => call<{ ok: true }>('attachment_delete', { id });
|
||||
export const attachmentRead = (id: number) =>
|
||||
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_read', { id });
|
||||
export async function attachmentUploadRaw(ownerId: number, file: File, thumbB64?: string): Promise<Attachment> {
|
||||
const q = new URLSearchParams({ owner: String(ownerId), name: file.name, mime: file.type || 'application/octet-stream' });
|
||||
const r = await fetch(getServerUrl() + '/sync/attachment/raw?' + q, {
|
||||
method: 'PUT',
|
||||
headers: { authorization: 'Bearer ' + getToken(), 'content-type': file.type || 'application/octet-stream' },
|
||||
body: file,
|
||||
});
|
||||
const j = await r.json().catch(() => ({ ok: false, error: 'HTTP ' + r.status }));
|
||||
if (!j.ok) throw new Error(j.error || '上传失败');
|
||||
const item = j.data.item as Attachment;
|
||||
if (thumbB64) { try { await call('attachment_set_thumb', { id: item.id, thumb_b64: thumbB64 }); } catch {} }
|
||||
return item;
|
||||
}
|
||||
|
||||
export const attachmentThumb = (id: number) =>
|
||||
call<{ data_b64: string; mime?: string; file_name: string }>('attachment_thumb', { id });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user