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
@@ -34,7 +34,7 @@ function mapToForm(d: Record<string, unknown>) {
const row = d as Row;
return {
...row,
detailImageUrls: row.detailImageUrls?.length ? row.detailImageUrls : [''],
detailImageUrls: row.detailImageUrls?.length ? row.detailImageUrls : [],
features: row.features?.length
? row.features
: [{ icon: 'star', title: '', desc: '' }],
@@ -68,12 +68,14 @@ function buildPayload(v: TemplateFormValues) {
function TemplateContentFields() {
return (
<>
<Typography.Text type="secondary"></Typography.Text>
<DetailImageUrlList
label="详情图"
bizType="DETAIL_TEMPLATE"
maxCount={TEMPLATE_MAX_DETAIL_IMAGES}
/>
<Typography.Text type="secondary"></Typography.Text>
<Form.Item name="detailImageUrls" style={{ marginBottom: 0 }}>
<DetailImageUrlList
label="详情图"
bizType="DETAIL_TEMPLATE"
maxCount={TEMPLATE_MAX_DETAIL_IMAGES}
/>
</Form.Item>
<Divider />
<Form.Item name="storyTitle" label="故事标题">
<Input placeholder="如:千年杜康 · 唯有此处" />
@@ -228,7 +230,7 @@ export default function ProductDetailTemplatesPage() {
}} width={640}>
<Form form={createForm} layout="vertical" initialValues={{
status: 'ACTIVE', sortOrder: 0,
detailImageUrls: [''],
detailImageUrls: [],
features: [{ icon: 'water_drop', title: '', desc: '' }],
}}>
<Form.Item name="code" label="编码" rules={[{ required: true }]} extra="唯一标识,如 dukang-classic">
+16 -25
View File
@@ -9,6 +9,7 @@ import { request } from '../lib/api';
import { AROMA_TYPE_LABELS, PRODUCT_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload';
import MultiImageUpload from '../components/MultiImageUpload';
import DetailImageUrlList from '../components/DetailImageUrlList';
import ProductDetailTemplatePicker from '../components/ProductDetailTemplatePicker';
import type { FormInstance } from 'antd/es/form';
@@ -73,8 +74,8 @@ function mapDetailToForm(d: Record<string, unknown>) {
return {
...d,
coverUrl: (d as { mainImageUrl?: string }).mainImageUrl,
carouselUrls: (row.carouselUrls?.length ? row.carouselUrls : ['']) as string[],
detailImageUrls: (row.detailImageUrls?.length ? row.detailImageUrls : ['']) as string[],
carouselUrls: (row.carouselUrls?.length ? row.carouselUrls : []) as string[],
detailImageUrls: (row.detailImageUrls?.length ? row.detailImageUrls : []) as string[],
allowOnlinePurchase: row.allowOnlinePurchase !== false,
allowCrossCityDelivery: row.allowOnlinePurchase === false ? false : row.allowCrossCityDelivery !== false,
allowOnSitePickup: !!row.allowOnSitePickup,
@@ -129,25 +130,13 @@ function buildProductPayload(v: ProductFormValues) {
function ImageUrlList({ name, label, bizType }: { name: string; label: string; bizType: string }) {
return (
<Form.List name={name}>
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<Space key={field.key} align="start" style={{ display: 'flex', marginBottom: 8 }}>
<Form.Item {...field} style={{ flex: 1, marginBottom: 0 }}>
<OssUpload bizType={bizType} mediaType="IMAGE" />
</Form.Item>
{fields.length > 1 && (
<MinusCircleOutlined onClick={() => remove(field.name)} style={{ marginTop: 8 }} />
)}
</Space>
))}
<Button type="dashed" onClick={() => add('')} block icon={<PlusOutlined />}>
{label}
</Button>
</>
)}
</Form.List>
<Form.Item name={name} style={{ marginBottom: 0 }}>
<MultiImageUpload
bizType={bizType}
mediaType="IMAGE"
tip={`${label}支持一次选择多张批量上传`}
/>
</Form.Item>
);
}
@@ -156,11 +145,13 @@ function ProductDetailFields({ form, aromaType }: { form: FormInstance; aromaTyp
<>
<ProductDetailTemplatePicker form={form} aromaType={aromaType} />
<Divider />
<Typography.Text type="secondary">CAROUSEL 10MB</Typography.Text>
<Typography.Text type="secondary">CAROUSEL 10MB</Typography.Text>
<ImageUrlList name="carouselUrls" label="轮播图" bizType="CAROUSEL" />
<Divider />
<Typography.Text type="secondary">DETAIL 10MB</Typography.Text>
<DetailImageUrlList label="详情图" bizType="DETAIL" />
<Typography.Text type="secondary">DETAIL 10MB</Typography.Text>
<Form.Item name="detailImageUrls" style={{ marginBottom: 0 }}>
<DetailImageUrlList label="详情图" bizType="DETAIL" />
</Form.Item>
<Divider />
<Form.Item name="storyTitle" label="故事标题">
<Input placeholder="如:千年杜康 · 唯有此处" />
@@ -468,7 +459,7 @@ export default function ProductsPage() {
aromaType: 'QINGXIANG', status: 'DRAFT', sortOrder: 0,
allowOnlinePurchase: true, allowCrossCityDelivery: true, allowOnSitePickup: false,
visibilityWhitelistEnabled: false, visibilityPhones: [],
carouselUrls: [''], detailImageUrls: [''],
carouselUrls: [], detailImageUrls: [],
features: [{ icon: 'water_drop', title: '', desc: '' }],
}}>
<Tabs items={[
+27 -65
View File
@@ -40,6 +40,7 @@ import { useAdminList } from '../lib/useAdminList';
import { resolveRegionBinding } from '../lib/china-region';
import ChinaRegionCascader from '../components/ChinaRegionCascader';
import OssUpload from '../components/OssUpload';
import MultiImageUpload from '../components/MultiImageUpload';
import TencentLocPickerModal from '../components/TencentLocPickerModal';
import AdminStorePackagesSection from '../components/AdminStorePackagesSection';
@@ -118,42 +119,18 @@ function StoreAuditMediaEditor() {
<Form.Item name="coverUrl" label="门头照">
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
</Form.Item>
<Typography.Text strong></Typography.Text>
<Typography.Paragraph type="secondary" style={{ marginTop: 4 }}>
3 20
</Typography.Paragraph>
<Form.List name="envPhotoUrls">
{(fields, { add, remove }) => (
<div style={{ marginTop: 8 }}>
{fields.map((field, index) => (
<div key={field.key} style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
<Form.Item
name={field.name}
label={`环境图 ${index + 1}`}
style={{ flex: 1, marginBottom: 12 }}
>
<OssUpload bizType="STORE_ENV" mediaType="IMAGE" />
</Form.Item>
<Button
type="link"
danger
style={{ marginTop: 30 }}
onClick={() => remove(field.name)}
>
</Button>
</div>
))}
{fields.length < 20 ? (
<Button type="dashed" onClick={() => add('')} block>
</Button>
) : (
<Typography.Text type="secondary"> 20 </Typography.Text>
)}
</div>
)}
</Form.List>
<Form.Item
name="envPhotoUrls"
label="环境照片"
extra="建议至少 3 张;支持批量上传,最多 20 张。可逐张删除后保存。"
>
<MultiImageUpload
bizType="STORE_ENV"
mediaType="IMAGE"
maxCount={20}
tip="环境照支持一次选择多张批量上传"
/>
</Form.Item>
<Form.Item name="contractUrl" label="签约合同" style={{ marginTop: 16 }}>
<OssUpload bizType="STORE_CONTRACT" mediaType="FILE" accept="image/*,.pdf" />
</Form.Item>
@@ -404,7 +381,7 @@ export default function StoresPage() {
envPhotoUrls: (() => {
const { envs } = collectMediaUrls(d);
const urls = envs.map((item) => item.url).filter(Boolean);
return urls.length ? urls : [''];
return urls;
})(),
contractUrl: (() => {
const { contracts } = collectMediaUrls(d);
@@ -559,7 +536,7 @@ export default function StoresPage() {
function openCreateModal() {
void loadOptions();
createForm.setFieldsValue({
envPhotoUrls: ['', '', ''],
envPhotoUrls: [],
settlementRate: 60,
openTime: '10:00',
closeTime: '22:00',
@@ -1261,33 +1238,18 @@ export default function StoresPage() {
<Form.Item name="coverUrl" label="门头照">
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
</Form.Item>
<Typography.Text strong></Typography.Text>
<Typography.Paragraph type="secondary" style={{ marginTop: 4 }}>
3 20
</Typography.Paragraph>
<Form.List name="envPhotoUrls">
{(fields, { add, remove }) => (
<div style={{ marginTop: 8 }}>
{fields.map((field, index) => (
<div key={field.key} style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
<Form.Item key={field.key} name={field.name} label={`环境图 ${index + 1}`} style={{ flex: 1 }}>
<OssUpload bizType="STORE_ENV" mediaType="IMAGE" />
</Form.Item>
<Button type="link" danger style={{ marginTop: 30 }} onClick={() => remove(field.name)}>
</Button>
</div>
))}
{fields.length < 20 ? (
<Button type="dashed" onClick={() => add('')} block>
</Button>
) : (
<Typography.Text type="secondary"> 20 </Typography.Text>
)}
</div>
)}
</Form.List>
<Form.Item
name="envPhotoUrls"
label="环境照片"
extra="选填;支持批量上传,最多 20 张"
>
<MultiImageUpload
bizType="STORE_ENV"
mediaType="IMAGE"
maxCount={20}
tip="环境照支持一次选择多张批量上传"
/>
</Form.Item>
<Form.Item name="contractUrl" label="签约合同">
<OssUpload bizType="STORE_CONTRACT" mediaType="FILE" accept="image/*,.pdf" />
</Form.Item>