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:
@@ -101,11 +101,11 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
|||||||
usableTime: item.usableTime?.trim() || null,
|
usableTime: item.usableTime?.trim() || null,
|
||||||
otherNotes: item.otherNotes?.trim() || null,
|
otherNotes: item.otherNotes?.trim() || null,
|
||||||
imageUrl: imageUrls[0] ?? null,
|
imageUrl: imageUrls[0] ?? null,
|
||||||
imageUrls: imageUrls.length ? imageUrls : null,
|
imageUrls,
|
||||||
sortOrder: index,
|
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++) {
|
for (let i = 0; i < filled.length; i++) {
|
||||||
const item = filled[i];
|
const item = filled[i];
|
||||||
@@ -130,13 +130,27 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
|||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await request(`/admin/stores/${storeId}/packages`, {
|
const data = await request<{ live: PackageRow[] }>(`/admin/stores/${storeId}/packages`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })),
|
packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
message.success('套餐已保存并生效');
|
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) {
|
} catch (e) {
|
||||||
message.error(e instanceof Error ? e.message : '保存失败');
|
message.error(e instanceof Error ? e.message : '保存失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -271,7 +285,7 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||||
修改后点击下方按钮保存,C 端将立即展示生效套餐。
|
上传图片后须点击下方「保存套餐」才会写入数据库;仅上传未保存,刷新后会丢失。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
<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 { Button, Image, Space, Typography, Upload, message } from 'antd';
|
||||||
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import type { UploadProps } from 'antd';
|
import type { UploadProps } from 'antd';
|
||||||
@@ -33,14 +33,33 @@ export default function MultiImageUpload({
|
|||||||
accept = 'image/*',
|
accept = 'image/*',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const urls = normalizeUrls(value);
|
const urls = normalizeUrls(value);
|
||||||
|
const urlsRef = useRef(urls);
|
||||||
|
const onChangeRef = useRef(onChange);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const batchBuf = useRef<File[]>([]);
|
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 remaining = maxCount != null ? Math.max(0, maxCount - urls.length) : Number.POSITIVE_INFINITY;
|
||||||
const canAdd = remaining > 0;
|
const canAdd = remaining > 0;
|
||||||
|
|
||||||
async function uploadBatch(files: File[]) {
|
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);
|
const picked = files.slice(0, room);
|
||||||
if (!picked.length) {
|
if (!picked.length) {
|
||||||
message.warning(maxCount != null ? `最多 ${maxCount} 张` : '无法上传');
|
message.warning(maxCount != null ? `最多 ${maxCount} 张` : '无法上传');
|
||||||
@@ -63,7 +82,10 @@ export default function MultiImageUpload({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appended.length) {
|
if (appended.length) {
|
||||||
onChange?.([...urls, ...appended]);
|
// 始终基于最新列表追加,避免并行上传互相覆盖
|
||||||
|
const next = [...urlsRef.current, ...appended];
|
||||||
|
urlsRef.current = next;
|
||||||
|
onChangeRef.current?.(next);
|
||||||
message.success(`成功上传 ${appended.length} 张${fail ? `,失败 ${fail} 张` : ''}`);
|
message.success(`成功上传 ${appended.length} 张${fail ? `,失败 ${fail} 张` : ''}`);
|
||||||
} else if (fail) {
|
} else if (fail) {
|
||||||
message.error('上传失败');
|
message.error('上传失败');
|
||||||
@@ -73,18 +95,37 @@ export default function MultiImageUpload({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = (file, fileList) => {
|
function enqueueUploadBatch(files: File[]) {
|
||||||
batchBuf.current.push(file as File);
|
uploadChain.current = uploadChain.current
|
||||||
if (batchBuf.current.length >= fileList.length) {
|
.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];
|
const files = [...batchBuf.current];
|
||||||
batchBuf.current = [];
|
batchBuf.current = [];
|
||||||
void uploadBatch(files);
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeAt(index: number) {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ export default function MultiOssUploadField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFiles(files: File[]) {
|
async function uploadFiles(files: File[]) {
|
||||||
const picked = files.slice(0, remaining);
|
const current = normalizeUrls(value);
|
||||||
|
const room = Math.max(0, maxCount - current.length);
|
||||||
|
const picked = files.slice(0, room);
|
||||||
if (!picked.length) {
|
if (!picked.length) {
|
||||||
showUploadError(`最多 ${maxCount} 张`);
|
showUploadError(`最多 ${maxCount} 张`);
|
||||||
return;
|
return;
|
||||||
@@ -68,7 +70,7 @@ export default function MultiOssUploadField({
|
|||||||
appended.push(result.url);
|
appended.push(result.url);
|
||||||
}
|
}
|
||||||
if (appended.length) {
|
if (appended.length) {
|
||||||
onChange?.([...urls, ...appended]);
|
onChange?.([...current, ...appended]);
|
||||||
toastSuccess(`已上传 ${appended.length} 张`);
|
toastSuccess(`已上传 ${appended.length} 张`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user