feat(admin): batch upload for package, store env, and product images

Allow multi-select uploads in HQ, partner, and shop for the three image galleries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 19:40:59 +08:00
parent 76375eff83
commit fe1c6c8158
11 changed files with 518 additions and 415 deletions
@@ -2,7 +2,7 @@ import { useState } from 'react';
import type { PackageFormItem } from '../lib/storePackages';
import { STORE_PACKAGE_IMAGE_MAX_COUNT, STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
import { emptyPackage } from '../lib/storePackages';
import OssUploadField from './OssUploadField';
import MultiOssUploadField from './MultiOssUploadField';
type Props = {
items: PackageFormItem[];
@@ -131,70 +131,25 @@ export default function StorePackagesForm({ items, onChange, disabled, embedded
</div>
<div className="partner-field">
<label> {STORE_PACKAGE_IMAGE_MAX_COUNT} </label>
{(() => {
const slots =
<label> {STORE_PACKAGE_IMAGE_MAX_COUNT} </label>
<MultiOssUploadField
bizType="STORE_PACKAGE"
maxCount={STORE_PACKAGE_IMAGE_MAX_COUNT}
disabled={disabled}
value={
Array.isArray(item.imageUrls) && item.imageUrls.length > 0
? item.imageUrls.map((u) => String(u ?? ''))
? item.imageUrls.map((u) => String(u ?? '')).filter((u) => u.trim())
: item.imageUrl
? [String(item.imageUrl)]
: [''];
const filled = slots.map((u) => u.trim()).filter(Boolean);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{slots.map((url, imgIndex) => (
<div key={`${imgIndex}-${url || 'empty'}`}>
<OssUploadField
bizType="STORE_PACKAGE"
value={url || ''}
onChange={(nextUrl) => {
const next = [...slots];
next[imgIndex] = nextUrl?.trim() || '';
const cleaned = next.map((u) => u.trim()).filter(Boolean);
updateAt(index, {
imageUrls: next,
imageUrl: cleaned[0] ?? '',
});
}}
/>
{!disabled && (filled.length > 0 || url) ? (
<button
type="button"
className="partner-packages-remove"
style={{ marginTop: 6 }}
onClick={() => {
const next = slots.filter((_, i) => i !== imgIndex);
const cleaned = next.map((u) => u.trim()).filter(Boolean);
updateAt(index, {
imageUrls: next.length ? next : [],
imageUrl: cleaned[0] ?? '',
});
}}
>
</button>
) : null}
</div>
))}
{!disabled && filled.length < STORE_PACKAGE_IMAGE_MAX_COUNT && slots.length < STORE_PACKAGE_IMAGE_MAX_COUNT ? (
<button
type="button"
className="partner-packages-add"
style={{ marginTop: 0 }}
onClick={() =>
updateAt(index, {
imageUrls: [...slots, ''],
imageUrl: filled[0] ?? '',
})
}
>
<span className="material-symbols-outlined">add_a_photo</span>
{filled.length}/{STORE_PACKAGE_IMAGE_MAX_COUNT}
</button>
) : null}
</div>
);
})()}
: []
}
onChange={(imageUrls) =>
updateAt(index, {
imageUrls,
imageUrl: imageUrls[0] ?? '',
})
}
/>
</div>
<div className="partner-field">