diff --git a/apps/admin-web/src/components/ProductSpecsEditor.tsx b/apps/admin-web/src/components/ProductSpecsEditor.tsx index 60f7821..b89901b 100644 --- a/apps/admin-web/src/components/ProductSpecsEditor.tsx +++ b/apps/admin-web/src/components/ProductSpecsEditor.tsx @@ -1,10 +1,11 @@ import { useMemo, useState } from 'react'; import { - Button, Input, InputNumber, Radio, Select, Space, Switch, Table, Typography, message, + Button, Image, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Typography, message, } from 'antd'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { EditOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { request } from '../lib/api'; import { PRODUCT_STATUS_LABELS } from '../lib/constants'; +import OssUpload from './OssUpload'; type SpecValue = { id?: string; name: string; sortOrder?: number }; type SpecAttr = { id?: string; name: string; sortOrder?: number; values: SpecValue[] }; @@ -24,6 +25,7 @@ type SkuRow = { isDefault: boolean; sortOrder?: number; specText?: string; + imageUrl?: string | null; }; function cartesian(attrs: SpecAttr[]): string[][] { @@ -73,6 +75,7 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku saleUnit: s.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE', bottlesPerUnit: s.bottlesPerUnit || (s.saleUnit === 'BOX' ? 6 : 1), isDefault: !!s.isDefault, + imageUrl: s.imageUrl ?? '', })) : [ { @@ -86,10 +89,12 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku saleUnit: 'BOTTLE', bottlesPerUnit: 1, isDefault: true, + imageUrl: '', }, ], ); const [saving, setSaving] = useState(false); + const [editIndex, setEditIndex] = useState(null); const combos = useMemo(() => cartesian(attrs.filter((a) => a.name.trim() && a.values.some((v) => v.name.trim()))), [attrs]); @@ -111,12 +116,21 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku bottlesPerUnit: 1, isDefault: i === 0, sortOrder: i, + imageUrl: '', }; }); if (next.length && !next.some((s) => s.isDefault)) next[0].isDefault = true; setSkus(next.length ? next : skus); } + function patchSku(index: number, patch: Partial) { + setSkus((prev) => { + const next = [...prev]; + next[index] = { ...next[index], ...patch }; + return next; + }); + } + async function handleSave() { setSaving(true); try { @@ -168,6 +182,7 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku bottlesPerUnit: s.saleUnit === 'BOX' ? s.bottlesPerUnit || 6 : 1, isDefault: !!s.isDefault, sortOrder: i, + imageUrl: s.imageUrl?.trim() || null, })); const barcodes = payloadSkus.map((row) => row.barcode69); @@ -189,11 +204,13 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku } } + const editing = editIndex != null ? skus[editIndex] : null; + return (
- SKU 码由系统自动生成(DK 开头),无需填写。每个规格必须填写互不相同的 69 码。 - 先配置销售规格轴(如「包装」),再生成 SKU 矩阵;未配置规格时仅保留默认一行。 + SKU 码由系统自动生成(DK 开头),无需填写。每个规格必须填写互不相同的 69 码,并可单独上传主图。 + 点「填写」在弹窗中编辑。先配置销售规格轴(如「包装」),再生成 SKU 矩阵。 规格轴 @@ -446,12 +463,154 @@ export default function ProductSpecsEditor({ productId, initialAttrs, initialSku /> ), }, + { + title: '主图', + width: 72, + render: (_, row) => + row.imageUrl ? ( + + ) : ( + + ), + }, + { + title: '编辑', + width: 80, + fixed: 'right', + render: (_, row, index) => ( + + ), + }, ]} /> + + setEditIndex(null)} + onOk={() => setEditIndex(null)} + okText="完成" + width={560} + destroyOnClose + > + {editing && editIndex != null ? ( + +
+ SKU +
{editing.skuCode || '保存后自动生成'}
+
+
+ 69 码 * + patchSku(editIndex, { barcode69: e.target.value })} + /> +
+
+ 主图 + patchSku(editIndex, { imageUrl: url })} + /> +
+ +
+ 售价 + patchSku(editIndex, { price: Number(v) || 0 })} + /> +
+
+ 权益 + patchSku(editIndex, { benefitAmount: v == null ? undefined : Number(v) })} + /> +
+
+
+ 状态 + + patchSku(editIndex, { + saleUnit, + bottlesPerUnit: saleUnit === 'BOX' ? editing.bottlesPerUnit || 6 : 1, + }) + } + /> + {editing.saleUnit === 'BOX' ? ( + patchSku(editIndex, { bottlesPerUnit: Number(v) || 6 })} + /> + ) : null} + + + + patchSku(editIndex, { + allowOnlinePurchase: checked, + allowCrossCityDelivery: checked ? editing.allowCrossCityDelivery : false, + }) + } + /> + patchSku(editIndex, { allowCrossCityDelivery: checked })} + /> + patchSku(editIndex, { allowOnSitePickup: checked })} + /> + { + if (!checked) return; + setSkus((prev) => prev.map((s, i) => ({ ...s, isDefault: i === editIndex }))); + }} + /> + + + ) : null} +
); } diff --git a/apps/admin-web/src/pages/ProductsPage.tsx b/apps/admin-web/src/pages/ProductsPage.tsx index 51412b1..e02bcff 100644 --- a/apps/admin-web/src/pages/ProductsPage.tsx +++ b/apps/admin-web/src/pages/ProductsPage.tsx @@ -408,7 +408,7 @@ export default function ProductsPage() { { setPage(p); setPageSize(ps); } }} /> - setDrawerOpen(false)} + setDrawerOpen(false)} extra={detail && (