847a3f0dc3
Enable online/cross-city/on-site purchase switches with trade gates, HQ and proxy UI, and server-generated DK SKUs. Co-authored-by: Cursor <cursoragent@cursor.com>
567 lines
22 KiB
TypeScript
567 lines
22 KiB
TypeScript
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<string, unknown>) {
|
||
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 (
|
||
<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>
|
||
);
|
||
}
|
||
|
||
function ProductDetailFields({ form, aromaType }: { form: FormInstance; aromaType?: string }) {
|
||
return (
|
||
<>
|
||
<ProductDetailTemplatePicker form={form} aromaType={aromaType} />
|
||
<Divider />
|
||
<Typography.Text type="secondary">详情页轮播(CAROUSEL)</Typography.Text>
|
||
<ImageUrlList name="carouselUrls" label="轮播图" bizType="CAROUSEL" />
|
||
<Divider />
|
||
<Typography.Text type="secondary">详情长图(DETAIL,可逐张修改)</Typography.Text>
|
||
<DetailImageUrlList label="详情图" bizType="DETAIL" />
|
||
<Divider />
|
||
<Form.Item name="storyTitle" label="故事标题">
|
||
<Input placeholder="如:千年杜康 · 唯有此处" />
|
||
</Form.Item>
|
||
<Form.Item name="storyText" label="故事正文">
|
||
<Input.TextArea rows={4} placeholder="商品故事描述" />
|
||
</Form.Item>
|
||
<Typography.Text type="secondary">卖点特色</Typography.Text>
|
||
<Form.List name="features">
|
||
{(fields, { add, remove }) => (
|
||
<>
|
||
{fields.map((field) => (
|
||
<Space key={field.key} direction="vertical" style={{ display: 'flex', marginBottom: 12, width: '100%' }}>
|
||
<Space align="start">
|
||
<Form.Item {...field} name={[field.name, 'icon']} label="图标" style={{ marginBottom: 0 }}>
|
||
<Input placeholder="material icon 名" style={{ width: 140 }} />
|
||
</Form.Item>
|
||
<Form.Item {...field} name={[field.name, 'title']} label="标题" style={{ marginBottom: 0, flex: 1 }}>
|
||
<Input placeholder="标题" />
|
||
</Form.Item>
|
||
{fields.length > 1 && (
|
||
<MinusCircleOutlined onClick={() => remove(field.name)} style={{ marginTop: 30 }} />
|
||
)}
|
||
</Space>
|
||
<Form.Item {...field} name={[field.name, 'desc']} label="描述" style={{ marginBottom: 0 }}>
|
||
<Input placeholder="简短描述" />
|
||
</Form.Item>
|
||
</Space>
|
||
))}
|
||
<Button type="dashed" onClick={() => add({ icon: 'star', title: '', desc: '' })} block icon={<PlusOutlined />}>
|
||
添加卖点
|
||
</Button>
|
||
</>
|
||
)}
|
||
</Form.List>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function VisibilityWhitelistFields({ form }: { form: FormInstance }) {
|
||
const [userOptions, setUserOptions] = useState<UserPickRow[]>([]);
|
||
const [userSearching, setUserSearching] = useState(false);
|
||
const searchTimer = useRef<ReturnType<typeof setTimeout> | 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 (
|
||
<>
|
||
<Form.Item
|
||
name="visibilityWhitelistEnabled"
|
||
label="可见白名单"
|
||
valuePropName="checked"
|
||
extra="开启后仅名单内手机号在 C 端可见/可购,用于在线测试"
|
||
>
|
||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||
</Form.Item>
|
||
{enabled ? (
|
||
<>
|
||
<Form.Item
|
||
name="visibilityPhones"
|
||
label="白名单手机号"
|
||
rules={[{ required: true, message: '请至少添加一个手机号' }]}
|
||
extra="可直接输入多个手机号回车添加,或从下方用户库选择"
|
||
>
|
||
<Select
|
||
mode="tags"
|
||
tokenSeparators={[',', ' ', ',', ';', ';']}
|
||
placeholder="输入手机号后回车"
|
||
style={{ width: '100%' }}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="从用户库添加">
|
||
<Select
|
||
showSearch
|
||
filterOption={false}
|
||
placeholder="按手机号搜索用户"
|
||
loading={userSearching}
|
||
options={userOptions.map((u) => ({
|
||
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 ? '搜索中…' : '输入手机号搜索'}
|
||
/>
|
||
</Form.Item>
|
||
</>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function BaseInfoFields({ mode, form }: { mode: 'create' | 'edit'; form: FormInstance }) {
|
||
return (
|
||
<>
|
||
{mode === 'create' && (
|
||
<>
|
||
<Form.Item label="SKU">
|
||
<Input disabled placeholder="保存后自动生成" />
|
||
</Form.Item>
|
||
<Form.Item name="barcode69" label="69码" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="aromaType" label="香型" rules={[{ required: true }]}>
|
||
<Select options={Object.entries(AROMA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
</>
|
||
)}
|
||
<Form.Item name="name" label="商品名" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="subtitle" label="副标题">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="spec" label="规格" rules={[{ required: true }]}>
|
||
<Input placeholder="500ml | 52度" />
|
||
</Form.Item>
|
||
<Form.Item name="price" label="售价" rules={[{ required: true }]}>
|
||
<InputNumber min={0} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="benefitAmount" label="权益额">
|
||
<InputNumber min={0} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
<Form.Item name="sortOrder" label="排序">
|
||
<InputNumber min={0} style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="allowOnlinePurchase"
|
||
label="允许线上购买"
|
||
valuePropName="checked"
|
||
extra="配送到址(同城)"
|
||
>
|
||
<Switch
|
||
checkedChildren="开"
|
||
unCheckedChildren="关"
|
||
onChange={(checked) => {
|
||
if (!checked) form.setFieldsValue({ allowCrossCityDelivery: false });
|
||
}}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.allowOnlinePurchase !== cur.allowOnlinePurchase}>
|
||
{() => (
|
||
<Form.Item
|
||
name="allowCrossCityDelivery"
|
||
label="允许跨城配送"
|
||
valuePropName="checked"
|
||
extra="须先开启线上购买"
|
||
>
|
||
<Switch
|
||
checkedChildren="开"
|
||
unCheckedChildren="关"
|
||
disabled={!form.getFieldValue('allowOnlinePurchase')}
|
||
/>
|
||
</Form.Item>
|
||
)}
|
||
</Form.Item>
|
||
<Form.Item name="allowOnSitePickup" label="允许现场取货" valuePropName="checked">
|
||
<Switch checkedChildren="开" unCheckedChildren="关" />
|
||
</Form.Item>
|
||
<VisibilityWhitelistFields form={form} />
|
||
<Form.Item name="coverUrl" label="封面">
|
||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||
</Form.Item>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default function ProductsPage() {
|
||
const [form] = Form.useForm();
|
||
const [editForm] = Form.useForm();
|
||
const [createForm] = Form.useForm();
|
||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||
'/admin/products',
|
||
() => {
|
||
const qs = new URLSearchParams();
|
||
if (filters.name) qs.set('name', filters.name);
|
||
if (filters.status) qs.set('status', filters.status);
|
||
if (filters.aromaType) qs.set('aromaType', filters.aromaType);
|
||
return qs;
|
||
},
|
||
[filters],
|
||
);
|
||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
|
||
async function handleDelete(row: Row) {
|
||
try {
|
||
await request(`/admin/products/${row.id}`, { method: 'DELETE' });
|
||
message.success('已删除');
|
||
if (detail?.id === row.id) {
|
||
setDrawerOpen(false);
|
||
setDetail(null);
|
||
}
|
||
void reload();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败');
|
||
}
|
||
}
|
||
|
||
const columns: ColumnsType<Row> = useMemo(() => [
|
||
{ title: 'SKU', dataIndex: 'skuCode', width: 90 },
|
||
{ title: '商品名', dataIndex: 'name', width: 180, ellipsis: true },
|
||
{ title: '香型', dataIndex: 'aromaType', width: 80, render: (v) => AROMA_TYPE_LABELS[v] || v },
|
||
{ title: '规格', dataIndex: 'spec', width: 120, ellipsis: true },
|
||
{ title: '售价', dataIndex: 'price', width: 80, render: (v) => `¥${v}` },
|
||
{ title: '权益额', dataIndex: 'benefitAmount', width: 80, render: (v) => `¥${v}` },
|
||
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => (
|
||
<Tag color={s === 'ON_SALE' ? 'green' : undefined}>{PRODUCT_STATUS_LABELS[s] || s}</Tag>
|
||
) },
|
||
{
|
||
title: '白名单',
|
||
dataIndex: 'visibilityWhitelistEnabled',
|
||
width: 90,
|
||
render: (v: boolean, row) =>
|
||
v ? <Tag color="orange">限{row.visibilityPhones?.length ?? 0}人</Tag> : <Tag>公开</Tag>,
|
||
},
|
||
{
|
||
title: '履约',
|
||
key: 'fulfillment',
|
||
width: 160,
|
||
render: (_, row) => (
|
||
<Space size={[0, 4]} wrap>
|
||
{row.allowOnlinePurchase !== false ? <Tag color="blue">线上</Tag> : null}
|
||
{row.allowOnlinePurchase !== false && row.allowCrossCityDelivery !== false ? (
|
||
<Tag color="cyan">跨城</Tag>
|
||
) : null}
|
||
{row.allowOnSitePickup ? <Tag color="green">现场</Tag> : null}
|
||
{row.allowOnlinePurchase === false && !row.allowOnSitePickup ? <Tag>无</Tag> : null}
|
||
</Space>
|
||
),
|
||
},
|
||
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
|
||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||
{
|
||
title: '操作', width: 120,
|
||
render: (_, row) => (
|
||
<Space size={0}>
|
||
<Button type="link" size="small" onClick={async () => {
|
||
const d = await request<Record<string, unknown>>(`/admin/products/${row.id}`);
|
||
setDetail(d);
|
||
editForm.setFieldsValue(mapDetailToForm(d));
|
||
setDrawerOpen(true);
|
||
}}>编辑</Button>
|
||
<Popconfirm
|
||
title="确认删除该商品?"
|
||
description={`将永久删除「${row.name}」(${row.skuCode}),此操作不可恢复。`}
|
||
okText="确认删除"
|
||
cancelText="取消"
|
||
okButtonProps={{ danger: true }}
|
||
onConfirm={() => handleDelete(row)}
|
||
>
|
||
<Button type="link" size="small" danger>删除</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
], [detail, editForm]);
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>商品管理</Typography.Title>
|
||
<Button type="primary" onClick={() => setCreateOpen(true)}>新建商品</Button>
|
||
</Space>
|
||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
|
||
<Form.Item name="name" label="名称"><Input allowClear /></Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select allowClear style={{ width: 110 }} options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
<Form.Item name="aromaType" label="香型">
|
||
<Select allowClear style={{ width: 110 }} options={Object.entries(AROMA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
||
</Form>
|
||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1320 }}
|
||
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||
<Drawer title="编辑商品" width={720} open={drawerOpen} onClose={() => setDrawerOpen(false)}
|
||
extra={detail && (
|
||
<Button type="primary" onClick={async () => {
|
||
const v = await editForm.validateFields();
|
||
if (!v.allowOnlinePurchase && !v.allowOnSitePickup) {
|
||
message.error('请至少勾选「允许线上购买」或「允许现场取货」之一');
|
||
return;
|
||
}
|
||
const payload = buildProductPayload(v);
|
||
await request(`/admin/products/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
||
message.success('已保存');
|
||
setDrawerOpen(false);
|
||
void reload();
|
||
}}>保存</Button>
|
||
)}>
|
||
{detail && (
|
||
<>
|
||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="SKU">{String(detail.skuCode)}</Descriptions.Item>
|
||
<Descriptions.Item label="69码">{String(detail.barcode69)}</Descriptions.Item>
|
||
<Descriptions.Item label="香型">{AROMA_TYPE_LABELS[String(detail.aromaType)] || String(detail.aromaType)}</Descriptions.Item>
|
||
</Descriptions>
|
||
<Form form={editForm} layout="vertical">
|
||
<Tabs items={[
|
||
{ key: 'base', label: '基础信息', children: <BaseInfoFields mode="edit" form={editForm} /> },
|
||
{
|
||
key: 'detail',
|
||
label: '详情页',
|
||
children: (
|
||
<ProductDetailFields
|
||
form={editForm}
|
||
aromaType={String(detail.aromaType ?? '')}
|
||
/>
|
||
),
|
||
},
|
||
]} />
|
||
</Form>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
<Modal title="新建商品" open={createOpen} onCancel={() => 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}>
|
||
<Form form={createForm} layout="vertical" initialValues={{
|
||
aromaType: 'QINGXIANG', status: 'DRAFT', sortOrder: 0,
|
||
allowOnlinePurchase: true, allowCrossCityDelivery: true, allowOnSitePickup: false,
|
||
visibilityWhitelistEnabled: false, visibilityPhones: [],
|
||
carouselUrls: [''], detailImageUrls: [''],
|
||
features: [{ icon: 'water_drop', title: '', desc: '' }],
|
||
}}>
|
||
<Tabs items={[
|
||
{ key: 'base', label: '基础信息', children: <BaseInfoFields mode="create" form={createForm} /> },
|
||
{
|
||
key: 'detail',
|
||
label: '详情页',
|
||
children: (
|
||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.aromaType !== cur.aromaType}>
|
||
{() => (
|
||
<ProductDetailFields
|
||
form={createForm}
|
||
aromaType={createForm.getFieldValue('aromaType') as string | undefined}
|
||
/>
|
||
)}
|
||
</Form.Item>
|
||
),
|
||
},
|
||
]} />
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|