feat(web): 积分商品图片选择器 — 媒体库 + 上传替代手动 URL

- PointsProductList: 图片字段改为 Input + 媒体库按钮 + 上传按钮 + 图片预览
- DrawerForm: 新增可选 form prop,允许外部控制表单实例
This commit is contained in:
iven
2026-05-26 10:04:28 +08:00
parent ba93e6585c
commit a2864713d6
2 changed files with 70 additions and 3 deletions

View File

@@ -19,6 +19,7 @@ interface DrawerFormProps {
sections?: FormSection[];
children?: React.ReactNode;
columns?: 1 | 2;
form?: ReturnType<typeof Form.useForm>[0];
}
export function DrawerForm({
@@ -32,8 +33,10 @@ export function DrawerForm({
sections,
children,
columns = 2,
form: externalForm,
}: DrawerFormProps) {
const [form] = Form.useForm();
const [internalForm] = Form.useForm();
const form = externalForm ?? internalForm;
const isDark = useThemeMode();
React.useEffect(() => {

View File

@@ -11,12 +11,15 @@ import {
Tag,
Badge,
Switch,
Upload,
message,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
PictureOutlined,
UploadOutlined,
} from '@ant-design/icons';
import {
pointsApi,
@@ -26,8 +29,11 @@ import {
import { AuthButton } from '../../components/AuthButton';
import { DrawerForm } from '../../components/DrawerForm';
import type { FormSection } from '../../components/DrawerForm';
import MediaPicker from '../../components/MediaPicker';
import { PageContainer } from '../../components/PageContainer';
import { uploadFile } from '../../api/upload';
import { usePaginatedData } from '../../hooks/usePaginatedData';
import { useThemeMode } from '../../hooks/useThemeMode';
import { formatDateTime } from '../../utils/format';
/** 商品类型映射 */
@@ -59,6 +65,9 @@ interface ProductFilters {
export default function PointsProductList() {
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<PointsProduct | null>(null);
const [mediaPickerOpen, setMediaPickerOpen] = useState(false);
const [form] = Form.useForm();
const isDark = useThemeMode();
const fetchProducts = useCallback(
async (page: number, pageSize: number, filters: ProductFilters) => {
@@ -309,8 +318,53 @@ export default function PointsProductList() {
title: '展示设置',
fields: (
<>
<Form.Item name="image_url" label="图片链接">
<Input placeholder="商品图片 URL" />
<Form.Item name="image_url" label="商品图片">
<Space.Compact style={{ width: '100%' }}>
<Input placeholder="输入 URL 或从媒体库选择" style={{ flex: 1 }} />
<Button icon={<PictureOutlined />} onClick={() => setMediaPickerOpen(true)}>
</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 (
<div
style={{
marginTop: -20,
marginBottom: 16,
borderRadius: 8,
overflow: 'hidden',
border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`,
}}
>
<img
src={url}
alt="商品图片预览"
style={{ width: '100%', height: 120, objectFit: 'cover' }}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
</div>
);
}}
</Form.Item>
<Form.Item name="sort_order" label="排序">
<InputNumber min={0} max={9999} style={{ width: '100%' }} placeholder="0" />
@@ -402,6 +456,16 @@ export default function PointsProductList() {
width={600}
columns={2}
sections={formSections}
form={form}
/>
<MediaPicker
open={mediaPickerOpen}
onClose={() => setMediaPickerOpen(false)}
onSelect={(url) => {
form.setFieldValue('image_url', url);
message.success('已选择图片');
}}
/>
</PageContainer>
);