298 lines
11 KiB
TypeScript
298 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, Form, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography,
|
|
} from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import {
|
|
PlusOutlined, EditOutlined, DeleteOutlined, DollarOutlined,
|
|
} from '@ant-design/icons';
|
|
import {
|
|
getApiModelPricings, saveApiModelPricing, deleteApiModelPricing, getGenerationAiEngines,
|
|
} from '../api';
|
|
import type { GenerationAiEngineOption } from '../types';
|
|
|
|
type PricingGenType = 'image' | 'video';
|
|
|
|
interface ApiModelPricing {
|
|
id: string;
|
|
modelConfigId: string;
|
|
genType: PricingGenType | string;
|
|
resolution: string;
|
|
priceRatio: number;
|
|
basePrice: number;
|
|
perSecondPrice: number;
|
|
inputVideoRatio: number;
|
|
inputVideoBasePrice: number;
|
|
inputVideoPerSecondPrice: number;
|
|
inputImageRatio: number;
|
|
inputImageBasePrice: number;
|
|
inputImagePerImagePrice: number;
|
|
}
|
|
|
|
const DEFAULT_IMAGE_SIZES = ['2K', '4K'];
|
|
const DEFAULT_VIDEO_RESOLUTIONS = ['480p', '720p', '1080p'];
|
|
|
|
const AdminApiModelPricings: React.FC = () => {
|
|
const [pricings, setPricings] = useState<ApiModelPricing[]>([]);
|
|
const [engines, setEngines] = useState<GenerationAiEngineOption[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [modal, setModal] = useState<{ open: boolean; pricing: ApiModelPricing | null }>({ open: false, pricing: null });
|
|
const [form] = Form.useForm();
|
|
const genType = Form.useWatch('genType', form) || 'video';
|
|
const selectedEngineId = Form.useWatch('modelConfigId', form);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [pricingData, enginesData] = await Promise.all([
|
|
getApiModelPricings(),
|
|
getGenerationAiEngines(),
|
|
]);
|
|
setPricings(pricingData);
|
|
|
|
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,
|
|
}));
|
|
setEngines([...imageEngines, ...videoEngines]);
|
|
} catch {
|
|
message.error('加载失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const filteredEngines = engines.filter(e => e.genType === genType);
|
|
const selectedEngine = engines.find(e => e.id === selectedEngineId);
|
|
const resolutions: string[] = genType === 'video'
|
|
? (selectedEngine?.supportedResolutions?.length ? selectedEngine.supportedResolutions : DEFAULT_VIDEO_RESOLUTIONS)
|
|
: (selectedEngine?.supportedSizes?.length ? Object.keys(selectedEngine.supportedSizes) : DEFAULT_IMAGE_SIZES);
|
|
|
|
const openEdit = (pricing: ApiModelPricing | null = null) => {
|
|
if (pricing) {
|
|
form.setFieldsValue({
|
|
modelConfigId: pricing.modelConfigId,
|
|
genType: pricing.genType,
|
|
resolution: pricing.resolution,
|
|
priceRatio: pricing.priceRatio,
|
|
basePrice: pricing.basePrice,
|
|
perSecondPrice: pricing.perSecondPrice,
|
|
inputVideoRatio: pricing.inputVideoRatio,
|
|
inputVideoBasePrice: pricing.inputVideoBasePrice,
|
|
inputVideoPerSecondPrice: pricing.inputVideoPerSecondPrice,
|
|
inputImageRatio: pricing.inputImageRatio,
|
|
inputImageBasePrice: pricing.inputImageBasePrice,
|
|
inputImagePerImagePrice: pricing.inputImagePerImagePrice,
|
|
});
|
|
} else {
|
|
form.resetFields();
|
|
form.setFieldsValue({
|
|
genType: 'video',
|
|
priceRatio: 1.0,
|
|
basePrice: 0.0,
|
|
perSecondPrice: 0.00,
|
|
inputVideoRatio: 1.0,
|
|
inputVideoBasePrice: 0,
|
|
inputVideoPerSecondPrice: 0,
|
|
inputImageRatio: 1.0,
|
|
inputImageBasePrice: 0,
|
|
inputImagePerImagePrice: 0,
|
|
});
|
|
}
|
|
setModal({ open: true, pricing });
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
const payload = {
|
|
...(modal.pricing?.id ? { id: modal.pricing.id } : {}),
|
|
modelConfigId: values.modelConfigId,
|
|
genType: values.genType,
|
|
resolution: values.resolution,
|
|
priceRatio: values.priceRatio,
|
|
basePrice: values.basePrice,
|
|
perSecondPrice: values.perSecondPrice || 0,
|
|
inputVideoRatio: values.inputVideoRatio || 1.0,
|
|
inputVideoBasePrice: values.inputVideoBasePrice || 0,
|
|
inputVideoPerSecondPrice: values.inputVideoPerSecondPrice || 0,
|
|
inputImageRatio: values.inputImageRatio || 1.0,
|
|
inputImageBasePrice: values.inputImageBasePrice || 0,
|
|
inputImagePerImagePrice: values.inputImagePerImagePrice || 0,
|
|
};
|
|
await saveApiModelPricing(payload);
|
|
message.success('保存成功');
|
|
setModal({ open: false, pricing: null });
|
|
form.resetFields();
|
|
load();
|
|
} catch (e: any) {
|
|
if (e?.errorFields) return;
|
|
message.error('保存失败');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await deleteApiModelPricing(id);
|
|
message.success('已删除');
|
|
load();
|
|
} catch {
|
|
message.error('删除失败');
|
|
}
|
|
};
|
|
|
|
const columns: ColumnsType<ApiModelPricing> = [
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'genType',
|
|
width: 80,
|
|
render: (v: string) => <Tag color={v === 'video' ? 'blue' : 'green'}>{v === 'video' ? '视频' : '图片'}</Tag>,
|
|
},
|
|
{
|
|
title: '引擎',
|
|
dataIndex: 'modelConfigId',
|
|
width: 160,
|
|
render: (v: string) => {
|
|
const engine = engines.find(e => e.id === v);
|
|
return engine?.name || v;
|
|
},
|
|
},
|
|
{
|
|
title: '分辨率',
|
|
dataIndex: 'resolution',
|
|
width: 80,
|
|
},
|
|
{
|
|
title: '价格系数',
|
|
dataIndex: 'priceRatio',
|
|
width: 90,
|
|
render: (v: number) => <span style={{ color: v >= 2 ? '#f5222d' : v >= 1.5 ? '#faad14' : '#52c41a' }}>{v}</span>,
|
|
},
|
|
{
|
|
title: '基础价格(元)',
|
|
dataIndex: 'basePrice',
|
|
width: 110,
|
|
},
|
|
{
|
|
title: '每秒价格(元)',
|
|
dataIndex: 'perSecondPrice',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '传入视频(元)/每秒',
|
|
dataIndex: 'inputVideoBasePrice',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '传入图片(元)/每张',
|
|
dataIndex: 'inputImageBasePrice',
|
|
width: 120,
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
fixed: 'right',
|
|
width: 150,
|
|
render: (_: any, r: ApiModelPricing) => (
|
|
<Space>
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
|
|
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}>
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Space>
|
|
<div style={{ width: 36, height: 36, borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<DollarOutlined style={{ color: '#fff', fontSize: 18 }} />
|
|
</div>
|
|
<Typography.Text strong style={{ fontSize: 16 }}>API 模型价格配置</Typography.Text>
|
|
<Tag color="purple">{pricings.length} 条</Tag>
|
|
</Space>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()}>添加价格</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card variant="outlined" style={{ borderRadius: 12 }}>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={pricings}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={false}
|
|
scroll={{ x: 1100 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Modal
|
|
title={modal.pricing ? '编辑价格' : '添加价格'}
|
|
open={modal.open}
|
|
onOk={handleSave}
|
|
onCancel={() => { setModal({ open: false, pricing: null }); form.resetFields(); }}
|
|
okText="保存" cancelText="取消" width={560}
|
|
>
|
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item name="genType" label="引擎类型" rules={[{ required: true }]} style={{ flex: 1 }}>
|
|
<Select onChange={() => { form.setFieldsValue({ modelConfigId: undefined, resolution: undefined }); }}>
|
|
<Select.Option value="video">视频</Select.Option>
|
|
<Select.Option value="image">图片</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
<Form.Item name="modelConfigId" label="引擎" rules={[{ required: true }]} style={{ flex: 1 }}>
|
|
<Select placeholder="选择引擎" showSearch optionFilterProp="label">
|
|
{filteredEngines.map(e => (
|
|
<Select.Option key={e.id} value={e.id} label={e.name}>{e.name}</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</div>
|
|
<Form.Item name="resolution" label="分辨率" rules={[{ required: true }]}>
|
|
<Select placeholder="选择分辨率">
|
|
{resolutions.map(r => (
|
|
<Select.Option key={r} value={r}>{r}</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item name="priceRatio" label="价格系数" rules={[{ required: true }]} style={{ flex: 1 }}>
|
|
<InputNumber min={0.01} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="basePrice" label="基础价格(元)" rules={[{ required: true }]} style={{ flex: 1 }}>
|
|
<InputNumber min={0} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</div>
|
|
{genType === 'video' && (
|
|
<Form.Item name="perSecondPrice" label="每秒价格(元)">
|
|
<InputNumber min={0} step={0.01} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
)}
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>传入媒体附加费</Typography.Text>
|
|
<div style={{ display: 'flex', gap: 16, marginTop: 8 }}>
|
|
<Form.Item name="inputVideoBasePrice" label="传入视频(元)/每秒" style={{ flex: 1 }}>
|
|
<InputNumber min={0} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
<Form.Item name="inputImageBasePrice" label="传入图片(元)/每张" style={{ flex: 1 }}>
|
|
<InputNumber min={0} step={0.1} style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</div>
|
|
</Form>
|
|
</Modal>
|
|
</Space>
|
|
);
|
|
};
|
|
|
|
export default AdminApiModelPricings;
|