fix(web): 积分商品图片预览 — 改用 React state 驱动替代 antd Form shouldUpdate

ant Form shouldUpdate + setFieldValue 在 DrawerForm 上下文中无法正确触发重渲染,
改用独立 imageUrl state 管理,Input/预览/MediaPicker/Upload 全部通过 state 同步
This commit is contained in:
iven
2026-05-26 10:30:22 +08:00
parent 42299a6722
commit 9d6a92e1d7

View File

@@ -66,7 +66,7 @@ export default function PointsProductList() {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<PointsProduct | null>(null); const [editing, setEditing] = useState<PointsProduct | null>(null);
const [mediaPickerOpen, setMediaPickerOpen] = useState(false); const [mediaPickerOpen, setMediaPickerOpen] = useState(false);
const [form] = Form.useForm(); const [imageUrl, setImageUrl] = useState<string>('');
const isDark = useThemeMode(); const isDark = useThemeMode();
const fetchProducts = useCallback( const fetchProducts = useCallback(
@@ -103,11 +103,13 @@ export default function PointsProductList() {
// ---- 新建 / 编辑 ---- // ---- 新建 / 编辑 ----
const openCreate = () => { const openCreate = () => {
setEditing(null); setEditing(null);
setImageUrl('');
setModalOpen(true); setModalOpen(true);
}; };
const openEdit = (record: PointsProduct) => { const openEdit = (record: PointsProduct) => {
setEditing(record); setEditing(record);
setImageUrl(record.image_url || '');
setModalOpen(true); setModalOpen(true);
}; };
@@ -124,12 +126,12 @@ export default function PointsProductList() {
points_cost: number; points_cost: number;
stock: number; stock: number;
description?: string; description?: string;
image_url?: string;
sort_order?: number; sort_order?: number;
}; };
if (editing) { if (editing) {
await pointsApi.updateProduct(editing.id, { await pointsApi.updateProduct(editing.id, {
...typed, ...typed,
image_url: imageUrl || undefined,
version: editing.version, version: editing.version,
}); });
} else { } else {
@@ -139,7 +141,7 @@ export default function PointsProductList() {
points_cost: typed.points_cost, points_cost: typed.points_cost,
stock: typed.stock, stock: typed.stock,
description: typed.description, description: typed.description,
image_url: typed.image_url, image_url: imageUrl || undefined,
sort_order: typed.sort_order, sort_order: typed.sort_order,
}; };
await pointsApi.createProduct(req); await pointsApi.createProduct(req);
@@ -318,59 +320,51 @@ export default function PointsProductList() {
title: '展示设置', title: '展示设置',
fields: ( fields: (
<> <>
<Form.Item name="image_url" hidden><Input /></Form.Item> <Form.Item label="商品图片">
<Form.Item label="商品图片" shouldUpdate={(prev, cur) => prev.image_url !== cur.image_url}> <Space.Compact style={{ width: '100%' }}>
{({ getFieldValue, setFieldValue }) => { <Input
const imageUrl: string | undefined = getFieldValue('image_url'); value={imageUrl}
return ( onChange={(e) => setImageUrl(e.target.value)}
<> placeholder="输入 URL 或从媒体库选择"
<Space.Compact style={{ width: '100%' }}> style={{ flex: 1 }}
<Input />
value={imageUrl || ''} <Button icon={<PictureOutlined />} onClick={() => setMediaPickerOpen(true)}>
onChange={(e) => setFieldValue('image_url', e.target.value || undefined)}
placeholder="输入 URL 或从媒体库选择" </Button>
style={{ flex: 1 }} <Upload
/> accept="image/*"
<Button icon={<PictureOutlined />} onClick={() => setMediaPickerOpen(true)}> showUploadList={false}
beforeUpload={async (file) => {
</Button> try {
<Upload const result = await uploadFile(file);
accept="image/*" setImageUrl(result.url);
showUploadList={false} message.success('图片上传成功');
beforeUpload={async (file) => { } catch {
try { message.error('图片上传失败');
const result = await uploadFile(file); }
setFieldValue('image_url', result.url); return false;
message.success('图片上传成功'); }}
} catch { >
message.error('图片上传失败'); <Button icon={<UploadOutlined />}></Button>
} </Upload>
return false; </Space.Compact>
}} {imageUrl && (
> <div
<Button icon={<UploadOutlined />}></Button> style={{
</Upload> marginTop: 8,
</Space.Compact> borderRadius: 8,
{imageUrl && ( overflow: 'hidden',
<div border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`,
style={{ }}
marginTop: 8, >
borderRadius: 8, <img
overflow: 'hidden', src={imageUrl}
border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`, alt="商品图片预览"
}} style={{ width: '100%', height: 120, objectFit: 'cover' }}
> onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
<img />
src={imageUrl} </div>
alt="商品图片预览" )}
style={{ width: '100%', height: 120, objectFit: 'cover' }}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
</div>
)}
</>
);
}}
</Form.Item> </Form.Item>
<Form.Item name="sort_order" label="排序"> <Form.Item name="sort_order" label="排序">
<InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" /> <InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" />
@@ -454,7 +448,6 @@ export default function PointsProductList() {
points_cost: editing.points_cost, points_cost: editing.points_cost,
stock: editing.stock, stock: editing.stock,
description: editing.description, description: editing.description,
image_url: editing.image_url,
sort_order: editing.sort_order, sort_order: editing.sort_order,
} }
: { stock: -1, sort_order: 0 }} : { stock: -1, sort_order: 0 }}
@@ -462,14 +455,13 @@ export default function PointsProductList() {
width={600} width={600}
columns={2} columns={2}
sections={formSections} sections={formSections}
form={form}
/> />
<MediaPicker <MediaPicker
open={mediaPickerOpen} open={mediaPickerOpen}
onClose={() => setMediaPickerOpen(false)} onClose={() => setMediaPickerOpen(false)}
onSelect={(url) => { onSelect={(url) => {
form.setFieldValue('image_url', url); setImageUrl(url);
message.success('已选择图片'); message.success('已选择图片');
}} }}
/> />