import React, { useEffect, useMemo, useState } from 'react'; import { Button, Card, Form, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography, } from 'antd'; import { CalculatorOutlined, PlusOutlined, EditOutlined, DeleteOutlined, FontSizeOutlined, } from '@ant-design/icons'; import { getCreditRatios, saveCreditRatio, deleteCreditRatio, getSystemConfigs, updateSystemConfig, getGenerationAiEngines, } from '../api'; import type { GenerationAiEngineOption } from '../types'; type CreditGenType = 'image' | 'video'; interface CreditRatio { id: string; modelConfigId: string; genType: CreditGenType | string; resolution: string; ratio: number; baseCredits: number; perSecondCredits: number; inputVideoRatio: number; inputVideoBaseCredits: number; inputVideoPerSecondCredits: number; inputImageRatio: number; inputImageBaseCredits: number; inputImagePerImageCredits: number; } interface CreditRatioFormValues { modelConfigId: string; genType: CreditGenType; resolution: string; ratio: number; baseCredits: number; perSecondCredits?: number; inputVideoRatio?: number; inputVideoBaseCredits?: number; inputVideoPerSecondCredits?: number; inputImageRatio?: number; inputImageBaseCredits?: number; inputImagePerImageCredits?: number; } const DEFAULT_IMAGE_SIZES = ['2K', '4K']; const DEFAULT_VIDEO_RESOLUTIONS = ['480p', '720p', '1080p']; const AdminCreditRatios: React.FC = () => { const [ratios, setRatios] = useState([]); const [engines, setEngines] = useState([]); const [loading, setLoading] = useState(false); const [modal, setModal] = useState<{ open: boolean; ratio: CreditRatio | null }>({ open: false, ratio: null }); const [form] = Form.useForm(); const [textRate, setTextRate] = useState(10); const [textRateConfig, setTextRateConfig] = useState<{ id: string } | null>(null); const [savingTextRate, setSavingTextRate] = useState(false); const genType = Form.useWatch('genType', form) || 'video'; const selectedEngineId = Form.useWatch('modelConfigId', form); const load = async () => { setLoading(true); try { const [ratioData, sysConfigs, enginesData] = await Promise.all([ getCreditRatios(), getSystemConfigs(), getGenerationAiEngines(), ]); const imageEngines: GenerationAiEngineOption[] = (enginesData?.engine?.image || []).map(engine => ({ ...engine, genType: 'image' as const, })); const videoEngines: GenerationAiEngineOption[] = (enginesData?.engine?.video || []).map(engine => ({ ...engine, genType: 'video' as const, })); setRatios(ratioData); setEngines([...videoEngines, ...imageEngines]); const textCfg = sysConfigs.find((c: any) => c.key === 'text_credits_per_1000_tokens'); if (textCfg) { setTextRate(Number(textCfg.value) || 10); setTextRateConfig({ id: textCfg.id }); } } catch { message.error('加载积分比例失败'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const currentEngineOptions = useMemo(() => ( engines .filter(engine => engine.genType === genType) .map(engine => ({ value: engine.id, label: `${engine.name}${engine.modelName ? `(${engine.modelName})` : ''}`, })) ), [engines, genType]); const selectedEngine = useMemo(() => ( engines.find(engine => engine.id === selectedEngineId && engine.genType === genType) ), [engines, genType, selectedEngineId]); const resolutionOptions = useMemo(() => { if (genType === 'image') { const sizes = selectedEngine?.supportedSizes ? Object.keys(selectedEngine.supportedSizes) : []; return (sizes.length ? sizes : DEFAULT_IMAGE_SIZES).map(size => ({ value: size, label: size })); } const resolutions = selectedEngine?.supportedResolutions || []; return (resolutions.length ? resolutions : DEFAULT_VIDEO_RESOLUTIONS).map(resolution => ({ value: resolution, label: resolution, })); }, [genType, selectedEngine]); const engineNameOf = (id: string, rowGenType?: string) => { const engine = engines.find(item => item.id === id && (!rowGenType || item.genType === rowGenType)); return engine?.name || id; }; const handleSave = async () => { try { const values = await form.validateFields(); const payload: any = { model_config_id: values.modelConfigId, gen_type: values.genType, resolution: values.resolution, ratio: values.ratio, base_credits: values.baseCredits, per_second_credits: values.genType === 'image' ? 0 : (values.perSecondCredits || 0), }; if (values.genType === 'video') { payload.input_video_ratio = values.inputVideoRatio ?? 1.0; payload.input_video_base_credits = values.inputVideoBaseCredits ?? 0; payload.input_video_per_second_credits = values.inputVideoPerSecondCredits ?? 0; } payload.input_image_ratio = values.inputImageRatio ?? 1.0; payload.input_image_base_credits = values.inputImageBaseCredits ?? 0; payload.input_image_per_image_credits = values.inputImagePerImageCredits ?? 0; if (modal.ratio) { await saveCreditRatio({ id: modal.ratio.id, ...payload }); message.success('已更新'); } else { await saveCreditRatio(payload); message.success('已添加'); } setModal({ open: false, ratio: null }); form.resetFields(); load(); } catch (e: any) { if (e?.errorFields) return; message.error(e?.message || '保存失败'); } }; const handleDelete = async (id: string) => { try { await deleteCreditRatio(id); message.success('已删除'); load(); } catch { message.error('删除失败'); } }; const handleSaveTextRate = async () => { if (!textRateConfig) return; setSavingTextRate(true); try { await updateSystemConfig(textRateConfig.id, String(textRate)); message.success('文字积分费率已更新'); } catch (e: any) { message.error(e?.message || '保存失败'); } finally { setSavingTextRate(false); } }; const openEdit = (ratio?: CreditRatio) => { setModal({ open: true, ratio: ratio || null }); if (ratio) { form.setFieldsValue({ modelConfigId: ratio.modelConfigId, genType: ratio.genType === 'image' ? 'image' : 'video', resolution: ratio.resolution, ratio: ratio.ratio, baseCredits: ratio.baseCredits, perSecondCredits: ratio.perSecondCredits, inputVideoRatio: ratio.inputVideoRatio, inputVideoBaseCredits: ratio.inputVideoBaseCredits, inputVideoPerSecondCredits: ratio.inputVideoPerSecondCredits, inputImageRatio: ratio.inputImageRatio, inputImageBaseCredits: ratio.inputImageBaseCredits, inputImagePerImageCredits: ratio.inputImagePerImageCredits, }); } else { form.resetFields(); form.setFieldsValue({ genType: 'video', ratio: 1.0, baseCredits: 60, perSecondCredits: 2, inputVideoRatio: 1.0, inputVideoBaseCredits: 0, inputVideoPerSecondCredits: 0.5, inputImageRatio: 1.0, inputImageBaseCredits: 0, inputImagePerImageCredits: 0.5, }); } }; const columns = [ { title: '类型', dataIndex: 'genType', width: 80, render: (v: string) => {v === 'image' ? '图片' : '视频'}, }, { title: '引擎', dataIndex: 'modelConfigId', width: 180, render: (v: string, r: CreditRatio) => {engineNameOf(v, r.genType)}, }, { title: '分辨率/尺寸', dataIndex: 'resolution', width: 110, render: (v: string) => { const colors: Record = { '480p': 'blue', '720p': 'blue', '1080p': 'blue', '4K': 'green', '2K': 'green' }; return {v}; }, }, { title: '倍率', dataIndex: 'ratio', width: 100, sorter: (a: CreditRatio, b: CreditRatio) => a.ratio - b.ratio, render: (v: number) => ( = 2 ? '#ef4444' : v >= 1.5 ? '#f59e0b' : '#10b981' }}> x{v} ), }, { title: '基础积分', dataIndex: 'baseCredits', width: 100, render: (v: number) => {v} 积分, }, { title: '每秒积分', dataIndex: 'perSecondCredits', width: 100, render: (v: number, r: CreditRatio) => ( {r.genType === 'image' ? '-' : `${v} 积分/秒`} ), }, { title: '传入视频', children: [ { title: '倍率', dataIndex: 'inputVideoRatio', width: 90, render: (v: number, r: CreditRatio) => ( {r.genType === 'image' ? '-' : ( = 2 ? '#ef4444' : v >= 1.5 ? '#f59e0b' : '#10b981' }}>x{v} )} ), }, { title: '基础积分', dataIndex: 'inputVideoBaseCredits', width: 100, render: (v: number, r: CreditRatio) => ( {r.genType === 'image' ? '-' : `${v} 积分`} ), }, { title: '每秒积分', dataIndex: 'inputVideoPerSecondCredits', width: 100, render: (v: number, r: CreditRatio) => ( {r.genType === 'image' ? '-' : `${v} 积分/秒`} ), }, ], }, { title: '传入图片', children: [ { title: '倍率', dataIndex: 'inputImageRatio', width: 90, render: (v: number) => ( = 2 ? '#ef4444' : v >= 1.5 ? '#f59e0b' : '#10b981' }}>x{v} ), }, { title: '基础积分', dataIndex: 'inputImageBaseCredits', width: 100, render: (v: number) => {v} 积分, }, { title: '每张积分', dataIndex: 'inputImagePerImageCredits', width: 100, render: (v: number) => {v} 积分/张, }, ], }, { title: '示例计算', key: 'example', width: 160, render: (_: any, r: CreditRatio) => { let total: number; if (r.genType === 'image') { total = Math.round(r.baseCredits * r.ratio); if (r.inputImageRatio && r.inputImagePerImageCredits) { total += Math.round( (r.inputImageBaseCredits + r.inputImagePerImageCredits * 3) * r.inputImageRatio ); } } else { total = Math.round((r.baseCredits + r.perSecondCredits * 15) * r.ratio); if (r.inputVideoRatio && r.inputVideoPerSecondCredits) { total += Math.round( (r.inputVideoBaseCredits + r.inputVideoPerSecondCredits * 15) * r.inputVideoRatio ); } if (r.inputImageRatio && r.inputImagePerImageCredits) { total += Math.round( (r.inputImageBaseCredits + r.inputImagePerImageCredits * 3) * r.inputImageRatio ); } } return {total} 积分; }, }, { title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: any, r: CreditRatio) => ( handleDelete(r.id)}> ), }, ]; return (
{/* Text Credit Rate */}
文字积分费率
文字积分计算公式:ceil(总token数 x 费率 / 1000),最低1积分
每1000 token消耗积分: setTextRate(v || 0)} size="large" style={{ width: '100%' }} /> 积分 示例:1000 token = {textRate} 积分,500 token = {(500 * textRate / 1000).toFixed(4)} 积分
{/* Generation Credit Ratios */}
积分比例配置 {ratios.length} 条规则
视频积分公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率;图片积分公式:基础积分 x 模型倍率 {modal.ratio ? '编辑比例' : '添加比例'}} open={modal.open} onOk={handleSave} onCancel={() => { setModal({ open: false, ratio: null }); form.resetFields(); }} okText="保存" cancelText="取消" width={520} >
form.setFieldsValue({ resolution: undefined as any })} />