diff --git a/apps/admin-web/src/components/AdminStorePackagesSection.tsx b/apps/admin-web/src/components/AdminStorePackagesSection.tsx index 0fb8465..ae92c5d 100644 --- a/apps/admin-web/src/components/AdminStorePackagesSection.tsx +++ b/apps/admin-web/src/components/AdminStorePackagesSection.tsx @@ -2,13 +2,22 @@ import { useEffect, useState } from 'react'; import { Alert, Button, Form, Input, InputNumber, Modal, Space, Typography, message } from 'antd'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; 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'; import { request } from '../lib/api'; -import OssUpload from './OssUpload'; +import PackageImagesUpload from './PackageImagesUpload'; type PackageRow = StorePackageItemDto; function emptyRow(index = 0): PackageRow { - return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', imageUrl: '', sortOrder: index }; + return { + name: '', + price: '0', + dishes: '', + usableTime: '', + otherNotes: '', + imageUrl: '', + imageUrls: [], + sortOrder: index, + }; } export default function AdminStorePackagesSection({ storeId }: { storeId: string }) { @@ -23,7 +32,16 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string .then((data) => { setItems( data.live?.length - ? data.live.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })) + ? data.live.map((p, i) => { + const imageUrls = normalizeStorePackageImageUrls(p); + return { + ...p, + price: String(p.price), + imageUrl: imageUrls[0] ?? '', + imageUrls, + sortOrder: i, + }; + }) : [emptyRow()], ); }) @@ -74,16 +92,20 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string async function save() { const filled = items - .map((item, index) => ({ - name: item.name.trim(), - price: item.price.trim(), - dishes: item.dishes.trim(), - usableTime: item.usableTime?.trim() || null, - otherNotes: item.otherNotes?.trim() || null, - imageUrl: item.imageUrl?.trim() || null, - sortOrder: index, - })) - .filter((item) => item.name || item.dishes || item.price); + .map((item, index) => { + const imageUrls = normalizeStorePackageImageUrls(item); + return { + name: item.name.trim(), + price: item.price.trim(), + dishes: item.dishes.trim(), + usableTime: item.usableTime?.trim() || null, + otherNotes: item.otherNotes?.trim() || null, + imageUrl: imageUrls[0] ?? null, + imageUrls: imageUrls.length ? imageUrls : null, + sortOrder: index, + }; + }) + .filter((item) => item.name || item.dishes || item.price || (item.imageUrls?.length ?? 0) > 0); for (let i = 0; i < filled.length; i++) { const item = filled[i]; @@ -100,6 +122,10 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string message.warning(`第 ${i + 1} 条套餐价格须为非负数字`); return; } + if ((item.imageUrls?.length ?? 0) > STORE_PACKAGE_IMAGE_MAX_COUNT) { + message.warning(`第 ${i + 1} 条套餐图片最多 ${STORE_PACKAGE_IMAGE_MAX_COUNT} 张`); + return; + } } setSaving(true); @@ -215,11 +241,14 @@ export default function AdminStorePackagesSection({ storeId }: { storeId: string - updateAt(index, { imageUrl: url })} + + updateAt(index, { + imageUrls, + imageUrl: imageUrls[0] ?? '', + }) + } /> diff --git a/apps/admin-web/src/components/PackageImagesUpload.tsx b/apps/admin-web/src/components/PackageImagesUpload.tsx new file mode 100644 index 0000000..656f55a --- /dev/null +++ b/apps/admin-web/src/components/PackageImagesUpload.tsx @@ -0,0 +1,75 @@ +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'; + +type Props = { + value?: string[]; + onChange?: (urls: string[]) => void; +}; + +/** 套餐多图上传:最多 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 ( + + + 最多 {STORE_PACKAGE_IMAGE_MAX_COUNT} 张,可替换或删除 + + {slots.map((url, index) => ( +
+
+ updateAt(index, next)} + /> +
+ {(urls.length > 0 || url) && ( + + )} +
+ ))} + {urls.length < STORE_PACKAGE_IMAGE_MAX_COUNT ? ( + + ) : null} + {urls.length > 1 ? ( + + + {urls.map((u) => ( + + ))} + + + ) : null} +
+ ); +} diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index d2815cc..29f8eda 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -23,11 +23,10 @@ import { } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { FormInstance } from 'antd/es/form'; -import { FilePdfOutlined, LinkOutlined, EnvironmentOutlined } from '@ant-design/icons'; +import { EnvironmentOutlined } from '@ant-design/icons'; import { request, type Paginated } from '../lib/api'; import { ADMIN_OPTIONS_PAGE_SIZE, - RESOURCE_BIZ_TYPE_LABELS, STORE_AUDIT_STATUS_LABELS, STORE_STATUS_LABELS, fmtTime, @@ -82,18 +81,6 @@ type StoreMediaItem = { url?: string | null; }; -function isImageMedia(url: string, mediaType?: string) { - if (mediaType === 'IMAGE') return true; - if (mediaType === 'VIDEO' || mediaType === 'FILE') { - return /\.(png|jpe?g|gif|webp|bmp|heic)(\?|#|$)/i.test(url); - } - return /\.(png|jpe?g|gif|webp|bmp|heic)(\?|#|$)/i.test(url); -} - -function isPdfUrl(url: string) { - return /\.pdf(\?|#|$)/i.test(url); -} - function collectMediaUrls(detail: Record) { const media = Array.isArray(detail.media) ? (detail.media as StoreMediaItem[]) : []; const byType = (bizType: string) => @@ -119,161 +106,57 @@ function collectMediaUrls(detail: Record) { }; } -function StoreAuditMediaSection({ detail }: { detail: Record }) { - const { covers, envs, contracts } = collectMediaUrls(detail); - const [pdfUrl, setPdfUrl] = useState(null); - const gallery = [...covers, ...envs].filter((item) => isImageMedia(item.url, item.mediaType)); - - if (covers.length === 0 && envs.length === 0 && contracts.length === 0) { - return ( - - ); - } - +function StoreAuditMediaEditor() { return (
- - 审核材料 - - - {(covers.length > 0 || envs.length > 0) && ( -
- - 门头照 / 环境照(点击可放大浏览) - - - - {gallery.map((item) => ( -
- -
- {covers.some((c) => c.id === item.id) ? '门头照' : '环境照'} -
-
- ))} -
-
-
- )} - - {contracts.length > 0 && ( -
- - 签约合同 - - - {contracts.map((item, index) => { - const imageLike = isImageMedia(item.url, item.mediaType); - const pdf = isPdfUrl(item.url); - return ( -
+ + + + 环境照片 + + 建议至少 3 张;可替换、删除,最多 20 张 + + + {(fields, { add, remove }) => ( +
+ {fields.map((field, index) => ( +
+ - {imageLike ? ( - - - - ) : ( -
- -
- )} - - - {RESOURCE_BIZ_TYPE_LABELS.CONTRACT || '合同'} - {contracts.length > 1 ? ` ${index + 1}` : ''} - - - {item.url} - - - {imageLike ? ( - 点击缩略图放大查看 - ) : null} - {pdf ? ( - - ) : null} - - - -
- ); - })} - -
- )} - - setPdfUrl(null)} - width={900} - footer={[ - , - , - ]} - destroyOnClose - > - {pdfUrl ? ( -