import { useMemo, useRef, useState } from 'react'; import { Button, Descriptions, Divider, Drawer, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography, message, } from 'antd'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; 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 DetailImageUrlList from '../components/DetailImageUrlList'; import ProductDetailTemplatePicker from '../components/ProductDetailTemplatePicker'; import type { FormInstance } from 'antd/es/form'; type ProductDetailContentDto = { storyTitle?: string; storyText?: string; features?: Array<{ icon: string; title: string; desc: string }>; }; type Row = { id: string; skuCode: string; barcode69: string; name: string; subtitle?: string; aromaType: string; spec: string; price: number; benefitAmount: number; status: string; sortOrder: number; allowOnSitePickup?: boolean; allowOnlinePurchase?: boolean; allowCrossCityDelivery?: boolean; visibilityWhitelistEnabled?: boolean; visibilityPhones?: string[]; mainImageUrl?: string | null; carouselUrls?: string[]; detailImageUrls?: string[]; detailContent?: ProductDetailContentDto | null; createdAt: string; }; type ProductFormValues = { skuCode?: string; barcode69?: string; name: string; subtitle?: string; aromaType?: string; spec: string; price: number; benefitAmount?: number; status?: string; sortOrder?: number; allowOnSitePickup?: boolean; allowOnlinePurchase?: boolean; allowCrossCityDelivery?: boolean; visibilityWhitelistEnabled?: boolean; visibilityPhones?: string[]; coverUrl?: string; carouselUrls?: string[]; detailImageUrls?: string[]; storyTitle?: string; storyText?: string; features?: Array<{ icon?: string; title?: string; desc?: string }>; }; type UserPickRow = { id: string; phone?: string | null; nickname?: string | null; userNo?: string; }; function mapDetailToForm(d: Record) { const detail = (d.detailContent ?? {}) as ProductDetailContentDto; const row = d as Row; return { ...d, coverUrl: (d as { mainImageUrl?: string }).mainImageUrl, 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, visibilityWhitelistEnabled: !!row.visibilityWhitelistEnabled, visibilityPhones: (row.visibilityPhones ?? []) as string[], storyTitle: detail.storyTitle ?? '', storyText: detail.storyText ?? '', features: detail.features?.length ? detail.features : [{ icon: 'water_drop', title: '', desc: '' }], }; } function buildProductPayload(v: ProductFormValues) { const carouselUrls = (v.carouselUrls ?? []).map((u) => u?.trim()).filter(Boolean); const detailImageUrls = (v.detailImageUrls ?? []).map((u) => u?.trim()).filter(Boolean); const features = (v.features ?? []) .filter((f) => f?.title?.trim() || f?.desc?.trim()) .map((f) => ({ icon: f.icon?.trim() || 'star', title: f.title?.trim() || '', desc: f.desc?.trim() || '', })); const detailContent: ProductDetailContentDto = { storyTitle: v.storyTitle?.trim() || undefined, storyText: v.storyText?.trim() || undefined, features: features.length ? features : undefined, }; const visibilityPhones = (v.visibilityPhones ?? []) .map((p) => String(p || '').replace(/\D/g, '').trim()) .filter(Boolean); return { barcode69: v.barcode69, name: v.name, subtitle: v.subtitle, aromaType: v.aromaType, spec: v.spec, price: v.price, benefitAmount: v.benefitAmount, status: v.status, sortOrder: v.sortOrder, allowOnSitePickup: !!v.allowOnSitePickup, allowOnlinePurchase: v.allowOnlinePurchase !== false, allowCrossCityDelivery: v.allowOnlinePurchase === false ? false : v.allowCrossCityDelivery !== false, visibilityWhitelistEnabled: !!v.visibilityWhitelistEnabled, visibilityPhones, coverUrl: v.coverUrl, carouselUrls, detailImageUrls, detailContent, }; } function ImageUrlList({ name, label, bizType }: { name: string; label: string; bizType: string }) { return ( {(fields, { add, remove }) => ( <> {fields.map((field) => ( {fields.length > 1 && ( remove(field.name)} style={{ marginTop: 8 }} /> )} ))} )} ); } function ProductDetailFields({ form, aromaType }: { form: FormInstance; aromaType?: string }) { return ( <> 详情页轮播(CAROUSEL,单张最大 10MB) 详情长图(DETAIL,可逐张修改,单张最大 10MB) 卖点特色 {(fields, { add, remove }) => ( <> {fields.map((field) => ( {fields.length > 1 && ( remove(field.name)} style={{ marginTop: 30 }} /> )} ))} )} ); } function VisibilityWhitelistFields({ form }: { form: FormInstance }) { const [userOptions, setUserOptions] = useState([]); const [userSearching, setUserSearching] = useState(false); const searchTimer = useRef | null>(null); async function searchUsers(keyword: string) { const q = keyword.trim(); if (searchTimer.current) clearTimeout(searchTimer.current); if (!q) { setUserOptions([]); return; } searchTimer.current = setTimeout(() => { void (async () => { setUserSearching(true); try { const qs = new URLSearchParams({ page: '1', pageSize: '20', phone: q }); const res = await request<{ items: UserPickRow[] }>(`/admin/users?${qs}`); setUserOptions((res.items ?? []).filter((u) => !!u.phone)); } catch { setUserOptions([]); } finally { setUserSearching(false); } })(); }, 300); } const enabled = Form.useWatch('visibilityWhitelistEnabled', form); return ( <> {enabled ? ( <> ({ value: u.phone!, label: `${u.phone}${u.nickname ? ` · ${u.nickname}` : ''}${u.userNo ? `(${u.userNo})` : ''}`, }))} onSearch={searchUsers} onSelect={(phone: string) => { const cur = (form.getFieldValue('visibilityPhones') as string[] | undefined) ?? []; if (!cur.includes(phone)) { form.setFieldsValue({ visibilityPhones: [...cur, phone] }); } }} notFoundContent={userSearching ? '搜索中…' : '输入手机号搜索'} /> ) : null} ); } function BaseInfoFields({ mode, form }: { mode: 'create' | 'edit'; form: FormInstance }) { return ( <> {mode === 'create' && ( <> ({ value, label }))} /> { setPage(p); setPageSize(ps); } }} /> setDrawerOpen(false)} extra={detail && ( )}> {detail && ( <> {String(detail.skuCode)} {String(detail.barcode69)} {AROMA_TYPE_LABELS[String(detail.aromaType)] || String(detail.aromaType)}
}, { key: 'detail', label: '详情页', children: ( ), }, ]} /> )}
setCreateOpen(false)} onOk={async () => { const v = await createForm.validateFields(); if (!v.allowOnlinePurchase && !v.allowOnSitePickup) { message.error('请至少勾选「允许线上购买」或「允许现场取货」之一'); return; } const payload = buildProductPayload(v); await request('/admin/products', { method: 'POST', body: JSON.stringify(payload) }); message.success('已创建'); setCreateOpen(false); createForm.resetFields(); void reload(); }} width={720}>
}, { key: 'detail', label: '详情页', children: ( prev.aromaType !== cur.aromaType}> {() => ( )} ), }, ]} />
); }