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
@@ -1,75 +1,21 @@
import { Button, Image, Space, Typography } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { STORE_PACKAGE_IMAGE_MAX_COUNT } from '@dukang/shared-types';
import OssUpload from './OssUpload';
import MultiImageUpload from './MultiImageUpload';
type Props = {
value?: string[];
onChange?: (urls: string[]) => void;
};
/** 套餐多图上传最多 STORE_PACKAGE_IMAGE_MAX_COUNT 张,支持替换与删除 */
/** 套餐多图:批量上传最多 STORE_PACKAGE_IMAGE_MAX_COUNT 张 */
export default function PackageImagesUpload({ value, onChange }: Props) {
const urls = (value ?? []).map((u) => String(u || '').trim()).filter(Boolean);
function updateAt(index: number, url: string) {
const next = [...urls];
const trimmed = url.trim();
if (!trimmed) {
next.splice(index, 1);
} else {
next[index] = trimmed;
}
onChange?.(next);
}
function removeAt(index: number) {
onChange?.(urls.filter((_, i) => i !== index));
}
function addSlot() {
if (urls.length >= STORE_PACKAGE_IMAGE_MAX_COUNT) return;
onChange?.([...urls, '']);
}
const slots = urls.length ? urls : [''];
return (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Typography.Text type="secondary">
{STORE_PACKAGE_IMAGE_MAX_COUNT}
</Typography.Text>
{slots.map((url, index) => (
<div key={`${index}-${url || 'empty'}`} style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<OssUpload
bizType="STORE_PACKAGE"
mediaType="IMAGE"
value={url || undefined}
onChange={(next) => updateAt(index, next)}
/>
</div>
{(urls.length > 0 || url) && (
<Button type="link" danger onClick={() => removeAt(index)} style={{ marginTop: 8 }}>
</Button>
)}
</div>
))}
{urls.length < STORE_PACKAGE_IMAGE_MAX_COUNT ? (
<Button type="dashed" icon={<PlusOutlined />} onClick={addSlot} block>
{urls.filter(Boolean).length}/{STORE_PACKAGE_IMAGE_MAX_COUNT}
</Button>
) : null}
{urls.length > 1 ? (
<Image.PreviewGroup>
<Space wrap size={8}>
{urls.map((u) => (
<Image key={u} src={u} width={64} height={64} style={{ objectFit: 'cover', borderRadius: 4 }} />
))}
</Space>
</Image.PreviewGroup>
) : null}
</Space>
<MultiImageUpload
bizType="STORE_PACKAGE"
mediaType="IMAGE"
maxCount={STORE_PACKAGE_IMAGE_MAX_COUNT}
value={value}
onChange={onChange}
tip={`套餐图片最多 ${STORE_PACKAGE_IMAGE_MAX_COUNT} 张,支持批量选择上传,可逐张删除`}
/>
);
}