fix(admin): keep all batch-uploaded images when saving packages

Serialize multi-file uploads and reload package list after save so imageUrls persist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 21:13:31 +08:00
parent 8f83624ffe
commit df546ff39f
3 changed files with 73 additions and 16 deletions
@@ -101,11 +101,11 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
usableTime: item.usableTime?.trim() || null,
otherNotes: item.otherNotes?.trim() || null,
imageUrl: imageUrls[0] ?? null,
imageUrls: imageUrls.length ? imageUrls : null,
imageUrls,
sortOrder: index,
};
})
.filter((item) => item.name || item.dishes || item.price || (item.imageUrls?.length ?? 0) > 0);
.filter((item) => item.name || item.dishes || item.price || item.imageUrls.length > 0);
for (let i = 0; i < filled.length; i++) {
const item = filled[i];
@@ -130,13 +130,27 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
setSaving(true);
try {
await request(`/admin/stores/${storeId}/packages`, {
const data = await request<{ live: PackageRow[] }>(`/admin/stores/${storeId}/packages`, {
method: 'PUT',
body: JSON.stringify({
packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })),
}),
});
message.success('套餐已保存并生效');
setItems(
data.live?.length
? data.live.map((p, i) => {
const imageUrls = normalizeStorePackageImageUrls(p);
return {
...p,
price: String(p.price),
imageUrl: imageUrls[0] ?? '',
imageUrls,
sortOrder: i,
};
})
: [emptyRow()],
);
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
} finally {
@@ -271,7 +285,7 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
) : null}
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
C
</Typography.Paragraph>
<Button type="primary" loading={saving} onClick={() => void save()}>
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Button, Image, Space, Typography, Upload, message } from 'antd';
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
@@ -33,14 +33,33 @@ export default function MultiImageUpload({
accept = 'image/*',
}: Props) {
const urls = normalizeUrls(value);
const urlsRef = useRef(urls);
const onChangeRef = useRef(onChange);
const [uploading, setUploading] = useState(false);
const batchBuf = useRef<File[]>([]);
const batchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const uploadChain = useRef(Promise.resolve());
useEffect(() => {
urlsRef.current = urls;
}, [urls]);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
return () => {
if (batchTimer.current) clearTimeout(batchTimer.current);
};
}, []);
const remaining = maxCount != null ? Math.max(0, maxCount - urls.length) : Number.POSITIVE_INFINITY;
const canAdd = remaining > 0;
async function uploadBatch(files: File[]) {
const room = maxCount != null ? Math.max(0, maxCount - urls.length) : files.length;
const current = urlsRef.current;
const room = maxCount != null ? Math.max(0, maxCount - current.length) : files.length;
const picked = files.slice(0, room);
if (!picked.length) {
message.warning(maxCount != null ? `最多 ${maxCount}` : '无法上传');
@@ -63,7 +82,10 @@ export default function MultiImageUpload({
}
}
if (appended.length) {
onChange?.([...urls, ...appended]);
// 始终基于最新列表追加,避免并行上传互相覆盖
const next = [...urlsRef.current, ...appended];
urlsRef.current = next;
onChangeRef.current?.(next);
message.success(`成功上传 ${appended.length}${fail ? `,失败 ${fail}` : ''}`);
} else if (fail) {
message.error('上传失败');
@@ -73,18 +95,37 @@ export default function MultiImageUpload({
}
}
const beforeUpload: UploadProps['beforeUpload'] = (file, fileList) => {
batchBuf.current.push(file as File);
if (batchBuf.current.length >= fileList.length) {
const files = [...batchBuf.current];
batchBuf.current = [];
void uploadBatch(files);
function enqueueUploadBatch(files: File[]) {
uploadChain.current = uploadChain.current
.then(() => uploadBatch(files))
.catch(() => undefined);
}
function flushBatch() {
if (batchTimer.current) {
clearTimeout(batchTimer.current);
batchTimer.current = null;
}
if (!batchBuf.current.length) return;
const files = [...batchBuf.current];
batchBuf.current = [];
enqueueUploadBatch(files);
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
batchBuf.current.push(file as File);
// 多选时 beforeUpload 可能逐文件触发;短防抖合并成一次批量
if (batchTimer.current) clearTimeout(batchTimer.current);
batchTimer.current = setTimeout(() => {
flushBatch();
}, 80);
return false;
};
function removeAt(index: number) {
onChange?.(urls.filter((_, i) => i !== index));
const next = urlsRef.current.filter((_, i) => i !== index);
urlsRef.current = next;
onChangeRef.current?.(next);
}
return (