Files
video-gen/video-gen-app/src/pages/admin/AdminVideoEngines.tsx
T
2026-05-25 17:08:18 +08:00

215 lines
8.0 KiB
TypeScript

import React, { useState } from 'react';
import {
Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
} from 'antd';
import {
PlayCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
} from '@ant-design/icons';
interface VideoEngine {
id: string;
name: string;
provider: string;
apiBase: string;
apiKey: string;
modelName: string;
supportedRatios: string[];
supportedResolutions: string[];
maxDuration: number;
isActive: boolean;
priority: number;
}
const MOCK_ENGINES: VideoEngine[] = [
{
id: 've-1', name: 'Seedance 2.0', provider: 'seedance',
apiBase: 'https://ark.cn-beijing.volces.com/api/v3',
apiKey: 'sk-****', modelName: 'seedance-2.0',
supportedRatios: ['16:9', '9:16', '1:1', '4:3'],
supportedResolutions: ['720p', '1080p', '4K'],
maxDuration: 60, isActive: true, priority: 1,
},
];
const AdminVideoEngines: React.FC = () => {
const [engines, setEngines] = useState<VideoEngine[]>(MOCK_ENGINES);
const [modal, setModal] = useState<{ open: boolean; engine: VideoEngine | null }>({ open: false, engine: null });
const [form] = Form.useForm();
const handleSave = async () => {
try {
const values = await form.validateFields();
if (modal.engine) {
setEngines(prev => prev.map(e => e.id === modal.engine!.id ? { ...e, ...values } : e));
message.success('已更新');
} else {
const newEngine: VideoEngine = {
id: `ve-${Date.now()}`,
...values,
};
setEngines(prev => [...prev, newEngine]);
message.success('已添加');
}
setModal({ open: false, engine: null });
form.resetFields();
} catch { /* validation */ }
};
const handleDelete = (id: string) => {
setEngines(prev => prev.filter(e => e.id !== id));
message.success('已删除');
};
const openEdit = (engine?: VideoEngine) => {
setModal({ open: true, engine: engine || null });
if (engine) {
form.setFieldsValue(engine);
} else {
form.resetFields();
form.setFieldsValue({
isActive: true, priority: 0, maxDuration: 60,
supportedRatios: ['16:9', '9:16', '1:1'],
supportedResolutions: ['720p', '1080p'],
});
}
};
const columns = [
{
title: '引擎名称', key: 'name', width: 180,
render: (_: any, r: VideoEngine) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 36, height: 36, borderRadius: 8,
background: r.isActive
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: 'linear-gradient(135deg, #94a3b8, #cbd5e1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 16,
}}><PlayCircleOutlined /></div>
<div>
<Typography.Text strong>{r.name}</Typography.Text>
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.provider}</div>
</div>
</div>
),
},
{
title: 'API地址', dataIndex: 'apiBase', width: 250,
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis>{v}</Typography.Text>,
},
{
title: '支持比例', dataIndex: 'supportedRatios', width: 180,
render: (ratios: string[]) => ratios.map(r => <Tag key={r}>{r}</Tag>),
},
{
title: '支持分辨率', dataIndex: 'supportedResolutions', width: 150,
render: (res: string[]) => res.map(r => <Tag key={r} color="blue">{r}</Tag>),
},
{
title: '最大时长', dataIndex: 'maxDuration', width: 80,
render: (v: number) => `${v}s`,
},
{
title: '状态', dataIndex: 'isActive', width: 80,
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag>,
},
{
title: '操作', key: 'action', width: 150, fixed: 'right' as const,
render: (_: any, r: VideoEngine) => (
<Space size={4}>
<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 (
<div>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Space>
<PlayCircleOutlined style={{ fontSize: 18, color: '#6366f1' }} />
<Typography.Text strong style={{ fontSize: 16 }}>视频引擎配置</Typography.Text>
<Tag color="purple">{engines.length} 个引擎</Tag>
</Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()} style={{ borderRadius: 8 }}>
添加引擎
</Button>
</div>
<Table
columns={columns}
dataSource={engines}
rowKey="id"
pagination={false}
scroll={{ x: 900 }}
/>
</Card>
<Modal
title={<Space><PlayCircleOutlined />{modal.engine ? '编辑引擎' : '添加引擎'}</Space>}
open={modal.open}
onOk={handleSave}
onCancel={() => { setModal({ open: false, engine: null }); form.resetFields(); }}
okText="保存" cancelText="取消" width={560}
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item name="name" label="引擎名称" style={{ flex: 1 }}
rules={[{ required: true }]}>
<Input placeholder="Seedance 2.0" size="large" />
</Form.Item>
<Form.Item name="provider" label="提供商" style={{ flex: 1 }}
rules={[{ required: true }]}>
<Select size="large" options={[
{ value: 'seedance', label: 'Seedance (火山引擎)' },
{ value: 'kling', label: 'Kling (快手)' },
{ value: 'runway', label: 'Runway' },
{ value: 'pika', label: 'Pika' },
]} />
</Form.Item>
</div>
<Form.Item name="apiBase" label="API地址" rules={[{ required: true }]}>
<Input placeholder="https://ark.cn-beijing.volces.com/api/v3" size="large" />
</Form.Item>
<Form.Item name="apiKey" label="API Key">
<Input.Password placeholder="sk-****" size="large" />
</Form.Item>
<Form.Item name="modelName" label="模型名称">
<Input placeholder="seedance-2.0" size="large" />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item name="supportedRatios" label="支持比例" style={{ flex: 1 }}>
<Select mode="multiple" size="large" options={[
{ value: '16:9' }, { value: '9:16' }, { value: '1:1' }, { value: '4:3' },
]} />
</Form.Item>
<Form.Item name="supportedResolutions" label="支持分辨率" style={{ flex: 1 }}>
<Select mode="multiple" size="large" options={[
{ value: '720p' }, { value: '1080p' }, { value: '4K' },
]} />
</Form.Item>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item name="maxDuration" label="最大时长(秒)" style={{ flex: 1 }}>
<InputNumber min={5} max={300} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item name="priority" label="优先级" style={{ flex: 1 }}>
<InputNumber min={0} max={10} style={{ width: '100%' }} size="large" />
</Form.Item>
<Form.Item name="isActive" label="启用" valuePropName="checked" style={{ paddingTop: 30 }}>
<Switch />
</Form.Item>
</div>
</Form>
</Modal>
</div>
);
};
export default AdminVideoEngines;