test: fix raw upload assertions

This commit is contained in:
root
2026-10-17 09:57:43 +08:00
parent f24088f359
commit 10fa64b5c8
17 changed files with 150 additions and 56 deletions
+16 -1
View File
@@ -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
+20 -4
View File
@@ -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) {
+14
View File
@@ -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 });