修复chat生成参数提交错误BUG|模型引擎积分设置关联开发优化成功|视频/图片生成引擎API开发完成
This commit is contained in:
@@ -6,6 +6,7 @@ import { api, setToken, clearToken } from './client';
|
||||
import type {
|
||||
User, CreditRecord, Project, GenerationRecord, GenerationParams,
|
||||
Industry, AdminUser, AdminStats, ModelConfig, SystemConfig, AdminNotification,
|
||||
GenerationAiEnginesResponse,
|
||||
} from '../types';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────
|
||||
@@ -302,3 +303,9 @@ export async function adminGenerateVideo(
|
||||
): Promise<void> {
|
||||
await api.post(`/admin/generation-records/${recordId}/generate`, { aspect_ratio: aspectRatio, resolution, image_size });
|
||||
}
|
||||
|
||||
// ── Generation AI Engines (Admin) ─────────────────────────────
|
||||
|
||||
export async function getGenerationAiEngines(): Promise<GenerationAiEnginesResponse> {
|
||||
return api.get<GenerationAiEnginesResponse>(`/generation-ai/engines`);
|
||||
}
|
||||
|
||||
@@ -1,44 +1,74 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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, getModelConfigs, getSystemConfigs, updateSystemConfig } from '../api';
|
||||
import type { ModelConfig } from '../types';
|
||||
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: string;
|
||||
genType: CreditGenType | string;
|
||||
resolution: string;
|
||||
ratio: number;
|
||||
baseCredits: number;
|
||||
perSecondCredits: number;
|
||||
}
|
||||
|
||||
interface CreditRatioFormValues {
|
||||
modelConfigId: string;
|
||||
genType: CreditGenType;
|
||||
resolution: string;
|
||||
ratio: number;
|
||||
baseCredits: number;
|
||||
perSecondCredits?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_IMAGE_SIZES = ['2K', '4K'];
|
||||
const DEFAULT_VIDEO_RESOLUTIONS = ['480p', '720p', '1080p'];
|
||||
|
||||
const AdminCreditRatios: React.FC = () => {
|
||||
const [ratios, setRatios] = useState<CreditRatio[]>([]);
|
||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||
const [engines, setEngines] = useState<GenerationAiEngineOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modal, setModal] = useState<{ open: boolean; ratio: CreditRatio | null }>({ open: false, ratio: null });
|
||||
const [form] = Form.useForm();
|
||||
const [form] = Form.useForm<CreditRatioFormValues>();
|
||||
const [textRate, setTextRate] = useState<number>(10);
|
||||
const [textRateConfig, setTextRateConfig] = useState<{ id: string } | null>(null);
|
||||
const [savingTextRate, setSavingTextRate] = useState(false);
|
||||
const genType = Form.useWatch('genType', form);
|
||||
const genType = Form.useWatch('genType', form) || 'video';
|
||||
const selectedEngineId = Form.useWatch('modelConfigId', form);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [ratioData, modelData, sysConfigs] = await Promise.all([
|
||||
const [ratioData, sysConfigs, enginesData] = await Promise.all([
|
||||
getCreditRatios(),
|
||||
getModelConfigs(),
|
||||
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);
|
||||
setModels(modelData);
|
||||
setEngines([...videoEngines, ...imageEngines]);
|
||||
|
||||
const textCfg = sysConfigs.find((c: any) => c.key === 'text_credits_per_1000_tokens');
|
||||
if (textCfg) {
|
||||
setTextRate(Number(textCfg.value) || 10);
|
||||
@@ -53,7 +83,36 @@ const AdminCreditRatios: React.FC = () => {
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const modelNameOf = (id: string) => models.find(m => m.id === id)?.name || id;
|
||||
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 {
|
||||
@@ -64,7 +123,7 @@ const AdminCreditRatios: React.FC = () => {
|
||||
resolution: values.resolution,
|
||||
ratio: values.ratio,
|
||||
base_credits: values.baseCredits,
|
||||
per_second_credits: values.genType === 'image' ? 0 : values.perSecondCredits,
|
||||
per_second_credits: values.genType === 'image' ? 0 : (values.perSecondCredits || 0),
|
||||
};
|
||||
if (modal.ratio) {
|
||||
await saveCreditRatio({ id: modal.ratio.id, ...payload });
|
||||
@@ -108,7 +167,14 @@ const AdminCreditRatios: React.FC = () => {
|
||||
const openEdit = (ratio?: CreditRatio) => {
|
||||
setModal({ open: true, ratio: ratio || null });
|
||||
if (ratio) {
|
||||
form.setFieldsValue(ratio);
|
||||
form.setFieldsValue({
|
||||
modelConfigId: ratio.modelConfigId,
|
||||
genType: ratio.genType === 'image' ? 'image' : 'video',
|
||||
resolution: ratio.resolution,
|
||||
ratio: ratio.ratio,
|
||||
baseCredits: ratio.baseCredits,
|
||||
perSecondCredits: ratio.perSecondCredits,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ genType: 'video', ratio: 1.0, baseCredits: 60, perSecondCredits: 2 });
|
||||
@@ -121,8 +187,8 @@ const AdminCreditRatios: React.FC = () => {
|
||||
render: (v: string) => <Tag color={v === 'image' ? 'cyan' : 'orange'}>{v === 'image' ? '图片' : '视频'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '模型', dataIndex: 'modelConfigId', width: 150,
|
||||
render: (v: string) => <Tag color="purple">{modelNameOf(v)}</Tag>,
|
||||
title: '引擎', dataIndex: 'modelConfigId', width: 180,
|
||||
render: (v: string, r: CreditRatio) => <Tag color={r.genType === 'image' ? 'cyan' : 'purple'}>{engineNameOf(v, r.genType)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '分辨率/尺寸', dataIndex: 'resolution', width: 110,
|
||||
@@ -145,7 +211,9 @@ const AdminCreditRatios: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: '每秒积分', dataIndex: 'perSecondCredits', width: 100,
|
||||
render: (v: number) => <Typography.Text>{v} 积分/秒</Typography.Text>,
|
||||
render: (v: number, r: CreditRatio) => (
|
||||
<Typography.Text>{r.genType === 'image' ? '-' : `${v} 积分/秒`}</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '示例计算', key: 'example', width: 120,
|
||||
@@ -206,7 +274,7 @@ const AdminCreditRatios: React.FC = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Video Credit Ratios */}
|
||||
{/* Generation Credit Ratios */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Space>
|
||||
@@ -220,7 +288,7 @@ const AdminCreditRatios: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16, fontSize: 13 }}>
|
||||
积分计算公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率
|
||||
视频积分公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率;图片积分公式:基础积分 x 模型倍率
|
||||
</Typography.Text>
|
||||
|
||||
<Table
|
||||
@@ -229,7 +297,7 @@ const AdminCreditRatios: React.FC = () => {
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 800 }}
|
||||
scroll={{ x: 860 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -238,37 +306,43 @@ const AdminCreditRatios: React.FC = () => {
|
||||
open={modal.open}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModal({ open: false, ratio: null }); form.resetFields(); }}
|
||||
okText="保存" cancelText="取消" width={480}
|
||||
okText="保存" cancelText="取消" width={520}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="modelConfigId" label="模型" rules={[{ required: true, message: '请选择模型' }]}>
|
||||
<Select size="large" options={models.map(m => ({ value: m.id, label: m.name }))} />
|
||||
<Form.Item name="genType" label="生成类型" rules={[{ required: true, message: '请选择生成类型' }]}>
|
||||
<Select
|
||||
size="large"
|
||||
options={[
|
||||
{ value: 'video', label: '视频' },
|
||||
{ value: 'image', label: '图片' },
|
||||
]}
|
||||
onChange={() => {
|
||||
form.setFieldsValue({ modelConfigId: undefined as any, resolution: undefined as any });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="genType" label="生成类型" rules={[{ required: true }]}>
|
||||
<Select size="large" options={[
|
||||
{ value: 'video', label: '视频' },
|
||||
{ value: 'image', label: '图片' },
|
||||
]} />
|
||||
<Form.Item name="modelConfigId" label="引擎" rules={[{ required: true, message: '请选择引擎' }]}>
|
||||
<Select
|
||||
size="large"
|
||||
placeholder="请选择引擎"
|
||||
options={currentEngineOptions}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
onChange={() => form.setFieldsValue({ resolution: undefined as any })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="resolution" label={genType === 'image' ? '图片尺寸' : '分辨率'} rules={[{ required: true }]}>
|
||||
<Select size="large" options={genType === 'image' ? [
|
||||
{ value: '2K', label: '2K' },
|
||||
{ value: '4K', label: '4K' },
|
||||
] : [
|
||||
{ value: '480p', label: '480p' },
|
||||
{ value: '720p', label: '720p' },
|
||||
{ value: '1080p', label: '1080p' },
|
||||
]} />
|
||||
<Form.Item name="resolution" label={genType === 'image' ? '图片尺寸' : '分辨率'} rules={[{ required: true, message: genType === 'image' ? '请选择图片尺寸' : '请选择分辨率' }]}>
|
||||
<Select size="large" placeholder={genType === 'image' ? '请选择图片尺寸' : '请选择分辨率'} options={resolutionOptions} />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="ratio" label="倍率" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<Form.Item name="ratio" label="倍率" style={{ flex: 1 }} rules={[{ required: true, message: '请输入倍率' }]}>
|
||||
<InputNumber min={0.1} max={10} step={0.1} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="baseCredits" label="基础积分" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<Form.Item name="baseCredits" label="基础积分" style={{ flex: 1 }} rules={[{ required: true, message: '请输入基础积分' }]}>
|
||||
<InputNumber min={0} max={1000} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
{genType !== 'image' && (
|
||||
<Form.Item name="perSecondCredits" label="每秒积分" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<Form.Item name="perSecondCredits" label="每秒积分" style={{ flex: 1 }} rules={[{ required: true, message: '请输入每秒积分' }]}>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@@ -147,6 +147,56 @@ export interface AdminNotification {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
export type GenerationAiGenType = 'image' | 'video';
|
||||
|
||||
export interface GenerationAiImageEngine {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
modelName: string;
|
||||
supportedModels: string[];
|
||||
supportedSizes: Record<string, Record<string, string>>;
|
||||
defaultSize: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface GenerationAiVideoEngine {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
modelName: string;
|
||||
supportedModels: string[];
|
||||
supportedRatios: string[];
|
||||
supportedResolutions: string[];
|
||||
supportedDurations: number[];
|
||||
maxDuration: number;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface GenerationAiEnginesResponse {
|
||||
engine: {
|
||||
image: GenerationAiImageEngine[];
|
||||
video: GenerationAiVideoEngine[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GenerationAiEngineOption {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
modelName: string;
|
||||
supportedModels?: string[];
|
||||
supportedRatios?: string[];
|
||||
supportedResolutions?: string[];
|
||||
supportedDurations?: number[];
|
||||
supportedSizes?: Record<string, Record<string, string>>;
|
||||
defaultSize?: string;
|
||||
maxDuration?: number;
|
||||
priority: number;
|
||||
genType: GenerationAiGenType;
|
||||
}
|
||||
|
||||
export interface AdminGenerationRecord {
|
||||
id: string;
|
||||
userId: string;
|
||||
|
||||
Reference in New Issue
Block a user