feat(web): 积分商品图片选择器 — 媒体库 + 上传替代手动 URL
- PointsProductList: 图片字段改为 Input + 媒体库按钮 + 上传按钮 + 图片预览 - DrawerForm: 新增可选 form prop,允许外部控制表单实例
This commit is contained in:
@@ -19,6 +19,7 @@ interface DrawerFormProps {
|
|||||||
sections?: FormSection[];
|
sections?: FormSection[];
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
columns?: 1 | 2;
|
columns?: 1 | 2;
|
||||||
|
form?: ReturnType<typeof Form.useForm>[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DrawerForm({
|
export function DrawerForm({
|
||||||
@@ -32,8 +33,10 @@ export function DrawerForm({
|
|||||||
sections,
|
sections,
|
||||||
children,
|
children,
|
||||||
columns = 2,
|
columns = 2,
|
||||||
|
form: externalForm,
|
||||||
}: DrawerFormProps) {
|
}: DrawerFormProps) {
|
||||||
const [form] = Form.useForm();
|
const [internalForm] = Form.useForm();
|
||||||
|
const form = externalForm ?? internalForm;
|
||||||
const isDark = useThemeMode();
|
const isDark = useThemeMode();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Badge,
|
Badge,
|
||||||
Switch,
|
Switch,
|
||||||
|
Upload,
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
PictureOutlined,
|
||||||
|
UploadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
pointsApi,
|
pointsApi,
|
||||||
@@ -26,8 +29,11 @@ import {
|
|||||||
import { AuthButton } from '../../components/AuthButton';
|
import { AuthButton } from '../../components/AuthButton';
|
||||||
import { DrawerForm } from '../../components/DrawerForm';
|
import { DrawerForm } from '../../components/DrawerForm';
|
||||||
import type { FormSection } from '../../components/DrawerForm';
|
import type { FormSection } from '../../components/DrawerForm';
|
||||||
|
import MediaPicker from '../../components/MediaPicker';
|
||||||
import { PageContainer } from '../../components/PageContainer';
|
import { PageContainer } from '../../components/PageContainer';
|
||||||
|
import { uploadFile } from '../../api/upload';
|
||||||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||||||
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
import { formatDateTime } from '../../utils/format';
|
import { formatDateTime } from '../../utils/format';
|
||||||
|
|
||||||
/** 商品类型映射 */
|
/** 商品类型映射 */
|
||||||
@@ -59,6 +65,9 @@ interface ProductFilters {
|
|||||||
export default function PointsProductList() {
|
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 [form] = Form.useForm();
|
||||||
|
const isDark = useThemeMode();
|
||||||
|
|
||||||
const fetchProducts = useCallback(
|
const fetchProducts = useCallback(
|
||||||
async (page: number, pageSize: number, filters: ProductFilters) => {
|
async (page: number, pageSize: number, filters: ProductFilters) => {
|
||||||
@@ -309,8 +318,53 @@ export default function PointsProductList() {
|
|||||||
title: '展示设置',
|
title: '展示设置',
|
||||||
fields: (
|
fields: (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="image_url" label="图片链接">
|
<Form.Item name="image_url" label="商品图片">
|
||||||
<Input placeholder="商品图片 URL" />
|
<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>
|
||||||
<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" />
|
||||||
@@ -402,6 +456,16 @@ export default function PointsProductList() {
|
|||||||
width={600}
|
width={600}
|
||||||
columns={2}
|
columns={2}
|
||||||
sections={formSections}
|
sections={formSections}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MediaPicker
|
||||||
|
open={mediaPickerOpen}
|
||||||
|
onClose={() => setMediaPickerOpen(false)}
|
||||||
|
onSelect={(url) => {
|
||||||
|
form.setFieldValue('image_url', url);
|
||||||
|
message.success('已选择图片');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user