fix(web): 积分商品图片选择器 + 更新 422 修复

- 图片字段改用 shouldUpdate 自定义渲染,值正确绑定到 Input 和预览
- updateProduct API 改为嵌套 { data, version } 格式,匹配后端 DTO
- 隐藏 Form.Item 保存 image_url 值到 form store
This commit is contained in:
iven
2026-05-26 10:12:04 +08:00
parent a2864713d6
commit 42299a6722
2 changed files with 52 additions and 45 deletions

View File

@@ -322,10 +322,11 @@ export const pointsApi = {
}, },
updateProduct: async (id: string, req: Partial<CreatePointsProductReq> & { is_active?: boolean; version: number }) => { updateProduct: async (id: string, req: Partial<CreatePointsProductReq> & { is_active?: boolean; version: number }) => {
const { version, ...fields } = req;
const { data } = await client.put<{ const { data } = await client.put<{
success: boolean; success: boolean;
data: PointsProduct; data: PointsProduct;
}>(`/health/admin/points/products/${id}`, req); }>(`/health/admin/points/products/${id}`, { data: fields, version });
return data.data; return data.data;
}, },

View File

@@ -318,51 +318,57 @@ export default function PointsProductList() {
title: '展示设置', title: '展示设置',
fields: ( fields: (
<> <>
<Form.Item name="image_url" label="商品图片"> <Form.Item name="image_url" hidden><Input /></Form.Item>
<Space.Compact style={{ width: '100%' }}> <Form.Item label="商品图片" shouldUpdate={(prev, cur) => prev.image_url !== cur.image_url}>
<Input placeholder="输入 URL 或从媒体库选择" style={{ flex: 1 }} /> {({ getFieldValue, setFieldValue }) => {
<Button icon={<PictureOutlined />} onClick={() => setMediaPickerOpen(true)}> const imageUrl: string | undefined = getFieldValue('image_url');
</Button>
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={async (file) => {
try {
const result = await uploadFile(file);
form.setFieldValue('image_url', result.url);
message.success('图片上传成功');
} catch {
message.error('图片上传失败');
}
return false;
}}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
</Space.Compact>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.image_url !== cur.image_url}>
{({ getFieldValue }) => {
const url: string | undefined = getFieldValue('image_url');
if (!url) return null;
return ( return (
<div <>
style={{ <Space.Compact style={{ width: '100%' }}>
marginTop: -20, <Input
marginBottom: 16, value={imageUrl || ''}
borderRadius: 8, onChange={(e) => setFieldValue('image_url', e.target.value || undefined)}
overflow: 'hidden', placeholder="输入 URL 或从媒体库选择"
border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`, style={{ flex: 1 }}
}} />
> <Button icon={<PictureOutlined />} onClick={() => setMediaPickerOpen(true)}>
<img
src={url} </Button>
alt="商品图片预览" <Upload
style={{ width: '100%', height: 120, objectFit: 'cover' }} accept="image/*"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} showUploadList={false}
/> beforeUpload={async (file) => {
</div> try {
const result = await uploadFile(file);
setFieldValue('image_url', result.url);
message.success('图片上传成功');
} catch {
message.error('图片上传失败');
}
return false;
}}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
</Space.Compact>
{imageUrl && (
<div
style={{
marginTop: 8,
borderRadius: 8,
overflow: 'hidden',
border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`,
}}
>
<img
src={imageUrl}
alt="商品图片预览"
style={{ width: '100%', height: 120, objectFit: 'cover' }}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
</div>
)}
</>
); );
}} }}
</Form.Item> </Form.Item>