fix(web): 积分商品图片预览 — 改用 React state 驱动替代 antd Form shouldUpdate
ant Form shouldUpdate + setFieldValue 在 DrawerForm 上下文中无法正确触发重渲染, 改用独立 imageUrl state 管理,Input/预览/MediaPicker/Upload 全部通过 state 同步
This commit is contained in:
@@ -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('已选择图片');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user