feat(v3.4.15): HQ store media edit and package images up to 20
Allow HQ to replace/delete store photos; support multi-image packages with a 20-image cap. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import type { PackageFormItem } from '../lib/storePackages';
|
||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||
import { STORE_PACKAGE_IMAGE_MAX_COUNT, STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||
import { emptyPackage } from '../lib/storePackages';
|
||||
import OssUploadField from './OssUploadField';
|
||||
|
||||
@@ -131,13 +131,70 @@ export default function StorePackagesForm({ items, onChange, disabled, embedded
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>套餐图片</label>
|
||||
<OssUploadField
|
||||
bizType="STORE_PACKAGE"
|
||||
value={item.imageUrl || ''}
|
||||
disabled={disabled}
|
||||
onChange={(url) => updateAt(index, { imageUrl: url })}
|
||||
/>
|
||||
<label>套餐图片(最多 {STORE_PACKAGE_IMAGE_MAX_COUNT} 张)</label>
|
||||
{(() => {
|
||||
const slots =
|
||||
Array.isArray(item.imageUrls) && item.imageUrls.length > 0
|
||||
? item.imageUrls.map((u) => String(u ?? ''))
|
||||
: 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>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { StorePackageItemDto } from '@dukang/shared-types';
|
||||
import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types';
|
||||
import {
|
||||
STORE_PACKAGE_IMAGE_MAX_COUNT,
|
||||
STORE_PACKAGE_MAX_COUNT,
|
||||
normalizeStorePackageImageUrls,
|
||||
} from '@dukang/shared-types';
|
||||
|
||||
export type PackageFormItem = StorePackageItemDto;
|
||||
|
||||
@@ -11,24 +15,34 @@ export function emptyPackage(index = 0): PackageFormItem {
|
||||
usableTime: '',
|
||||
otherNotes: '',
|
||||
imageUrl: '',
|
||||
imageUrls: [],
|
||||
sortOrder: index,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePackageFormItems(raw: PackageFormItem[]): PackageFormItem[] {
|
||||
return raw
|
||||
.map((item, index) => ({
|
||||
name: item.name.trim(),
|
||||
price: item.price.trim(),
|
||||
dishes: item.dishes.trim(),
|
||||
usableTime: item.usableTime?.trim() || '',
|
||||
otherNotes: item.otherNotes?.trim() || '',
|
||||
imageUrl: item.imageUrl?.trim() || '',
|
||||
sortOrder: index,
|
||||
}))
|
||||
.map((item, index) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls(item);
|
||||
return {
|
||||
name: item.name.trim(),
|
||||
price: item.price.trim(),
|
||||
dishes: item.dishes.trim(),
|
||||
usableTime: item.usableTime?.trim() || '',
|
||||
otherNotes: item.otherNotes?.trim() || '',
|
||||
imageUrl: imageUrls[0] ?? '',
|
||||
imageUrls,
|
||||
sortOrder: index,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(item) =>
|
||||
item.name || item.price || item.dishes || item.usableTime || item.otherNotes || item.imageUrl,
|
||||
item.name ||
|
||||
item.price ||
|
||||
item.dishes ||
|
||||
item.usableTime ||
|
||||
item.otherNotes ||
|
||||
item.imageUrls.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +57,9 @@ export function validatePackageFormItems(items: PackageFormItem[]): string | nul
|
||||
const price = Number(item.price);
|
||||
if (!Number.isFinite(price) || price < 0) return `第 ${i + 1} 条套餐价格须为非负数字`;
|
||||
if (!item.dishes) return `第 ${i + 1} 条套餐菜品不能为空`;
|
||||
if ((item.imageUrls?.length ?? 0) > STORE_PACKAGE_IMAGE_MAX_COUNT) {
|
||||
return `第 ${i + 1} 条套餐图片最多 ${STORE_PACKAGE_IMAGE_MAX_COUNT} 张`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import type { StorePackagesResponse } from '@dukang/shared-types';
|
||||
import { normalizeStorePackageImageUrls } from '@dukang/shared-types';
|
||||
import StorePackagesForm from '../components/StorePackagesForm';
|
||||
import { request } from '../lib/api';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
@@ -30,7 +31,18 @@ export default function StorePackagesPage() {
|
||||
: data.live?.length
|
||||
? data.live
|
||||
: [emptyPackage()];
|
||||
setItems(base.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })));
|
||||
setItems(
|
||||
base.map((p, i) => {
|
||||
const imageUrls = normalizeStorePackageImageUrls(p);
|
||||
return {
|
||||
...p,
|
||||
price: String(p.price),
|
||||
imageUrl: imageUrls[0] ?? '',
|
||||
imageUrls,
|
||||
sortOrder: i,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setPending(data.pendingRequest ?? null);
|
||||
})
|
||||
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'))
|
||||
|
||||
Reference in New Issue
Block a user