import React, { useEffect, useState } from 'react'; import { Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, } from 'antd'; import { PictureOutlined, PlusOutlined, EditOutlined, DeleteOutlined, } from '@ant-design/icons'; import { getImageEngines, saveImageEngine, deleteImageEngine } from '../api'; interface ImageEngine { id: string; name: string; provider: string; apiBase: string; apiKey: string; modelName: string; supportedModels: string[]; supportedSizes: Record>; defaultSize: string; maxImageCount: number; generateUrl: string; isActive: boolean; priority: number; multiGenerationEnabled: boolean; maxGenerationCount: number; multiImageMaxImages: number; maxReferenceImageCount: number; outputFormat: '' | 'png' | 'jpeg'; } function parseJsonArray(val: unknown): any[] { if (Array.isArray(val)) return val; if (typeof val === 'string') { try { return JSON.parse(val); } catch { return []; } } return []; } function parseSizes(val: unknown): Record> { if (val && typeof val === 'object' && !Array.isArray(val)) return val as Record>; if (typeof val === 'string') { try { const p = JSON.parse(val); return (p && typeof p === 'object' && !Array.isArray(p)) ? p : {}; } catch { return {}; } } return {}; } // Default size options with pixel mappings const SIZE_OPTIONS: Record> = { "1K": { "1:1": "1024x1024", "4:3": "1152x864", "3:4": "864x1152", "16:9": "1312x736", "9:16": "736x1312", "3:2": "1248x832", "2:3": "832x1248", "21:9": "1568x672", }, "2K": { "1:1": "2048x2048", "4:3": "2304x1728", "3:4": "1728x2304", "16:9": "2848x1600", "9:16": "1600x2848", "3:2": "2496x1664", "2:3": "1664x2496", "21:9": "3136x1344", }, "4K": { "1:1": "4096x4096", "4:3": "4704x3520", "3:4": "3520x4704", "16:9": "5504x3040", "9:16": "3040x5504", "3:2": "4992x3328", "2:3": "3328x4992", "21:9": "6197x2656", }, }; const ALL_RATIOS = ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"]; const AdminImageEngines: React.FC = () => { const [engines, setEngines] = useState([]); const [loading, setLoading] = useState(false); const [modal, setModal] = useState<{ open: boolean; engine: ImageEngine | null }>({ open: false, engine: null }); const [form] = Form.useForm(); const multiGenerationEnabled = Form.useWatch('multiGenerationEnabled', form) ?? false; const load = async () => { setLoading(true); try { const data = await getImageEngines(); setEngines(data.map((e: any) => ({ ...e, supportedModels: parseJsonArray(e.supportedModels), supportedSizes: parseSizes(e.supportedSizes), }))); } catch { message.error('加载图片引擎失败'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const handleSave = async () => { try { const values = await form.validateFields(); // Build supportedSizes from form values const sizes: Record> = {}; for (const tier of ["1K", "2K", "4K"]) { const selected: string[] = values[`size_${tier}`] || []; if (selected.length > 0) { sizes[tier] = {}; for (const ratio of selected) { sizes[tier][ratio] = SIZE_OPTIONS[tier]?.[ratio] || ratio; } } } const payload = { name: values.name, provider: values.provider, api_base: values.apiBase, api_key: values.apiKey, model_name: values.modelName, supported_models: JSON.stringify(values.supportedModels || []), supported_sizes: JSON.stringify(sizes), default_size: values.defaultSize || '2K', max_image_count: values.maxImageCount ?? 0, generate_url: values.generateUrl || '', is_active: values.isActive ?? true, priority: values.priority ?? 0, multi_generation_enabled: values.multiGenerationEnabled ?? false, max_generation_count: values.maxGenerationCount ?? 1, multi_image_max_images: values.multiImageMaxImages ?? 15, max_reference_image_count: values.maxReferenceImageCount ?? 14, output_format: values.outputFormat ?? '', }; if (modal.engine) { await saveImageEngine({ id: modal.engine.id, ...payload }); message.success('已更新'); } else { await saveImageEngine(payload); message.success('已添加'); } setModal({ open: false, engine: null }); form.resetFields(); load(); } catch (e: any) { if (e?.errorFields) return; message.error(e?.message || '保存失败'); } }; const handleDelete = async (id: string) => { try { await deleteImageEngine(id); message.success('已删除'); load(); } catch { message.error('删除失败'); } }; const openEdit = (engine?: ImageEngine) => { setModal({ open: true, engine: engine || null }); if (engine) { const sizeFields: Record = {}; for (const tier of ["1K", "2K", "4K"]) { sizeFields[`size_${tier}`] = Object.keys(engine.supportedSizes?.[tier] || {}); } form.setFieldsValue({ ...engine, ...sizeFields, }); } else { form.resetFields(); form.setFieldsValue({ isActive: true, priority: 0, multiGenerationEnabled: false, maxGenerationCount: 1, multiImageMaxImages: 15, maxReferenceImageCount: 14, outputFormat: '', supportedModels: ['doubao-seedream-5-0-260128'], defaultSize: '2K', maxImageCount: 0, size_1K: ALL_RATIOS, size_2K: ALL_RATIOS, size_4K: ALL_RATIOS, }); } }; const columns = [ { title: '引擎名称', key: 'name', width: 180, render: (_: any, r: ImageEngine) => (
{r.name}
{r.modelName}
), }, { title: '1K 支持比例', key: 'sizes_1k', width: 260, render: (_: any, r: ImageEngine) => { const ratios = Object.keys(r.supportedSizes?.["1K"] || {}); if (ratios.length === 0) return -; return {ratios.map(ratio => ( {ratio} {r.supportedSizes["1K"][ratio]} ))}; }, }, { title: '2K 支持比例', key: 'sizes_2k', width: 260, render: (_: any, r: ImageEngine) => { const ratios = Object.keys(r.supportedSizes?.["2K"] || {}); if (ratios.length === 0) return -; return {ratios.map(ratio => ( {ratio} {r.supportedSizes["2K"][ratio]} ))}; }, }, { title: '4K 支持比例', key: 'sizes_4k', width: 260, render: (_: any, r: ImageEngine) => { const ratios = Object.keys(r.supportedSizes?.["4K"] || {}); if (ratios.length === 0) return -; return {ratios.map(ratio => ( {ratio} {r.supportedSizes["4K"][ratio]} ))}; }, }, { title: '最大图片', dataIndex: 'maxImageCount', width: 100, render: (v: number) => {v} 张, }, { title: '多份生成', dataIndex: 'multiGenerationEnabled', width: 100, render: (v: boolean) => {v ? '开启' : '关闭'}, }, { title: '数量上限', dataIndex: 'maxGenerationCount', width: 100, render: (v: number, r: ImageEngine) => ( 1 ? 'magenta' : 'default'}> 最多 {r.multiGenerationEnabled ? (v || 1) : 1} 份 ), }, { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => {v ? '启用' : '停用'}, }, { title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: any, r: ImageEngine) => ( handleDelete(r.id)}> ), }, ]; return (
图片引擎配置 {engines.length} 个引擎
{modal.engine ? '编辑引擎' : '添加引擎'}} open={modal.open} onOk={handleSave} onCancel={() => { setModal({ open: false, engine: null }); form.resetFields(); }} okText="保存" cancelText="取消" width={680} >
多份生成能力 管理后台只控制是否允许客户端选择多份及最大数量。客户端本次选择 2-5 份时,后端只调用一次火山同步组图 API;失败绝不降级成多次单图请求。
); }; export default AdminImageEngines;