解决app/api代码合并冲突
This commit is contained in:
@@ -14,6 +14,7 @@ import InitialInfo from './pages/InitialInfo';
|
||||
import RemoveLens from './pages/RemoveLens';
|
||||
import GeneratedRecord from './pages/GeneratedRecord';
|
||||
import AuthorizationPage from './pages/AuthorizationPage';
|
||||
import RemoveInfo from './pages/RemoveInfo';
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +97,7 @@ const App = () => {
|
||||
<Route path="initial" element={<InitialReplication />} />
|
||||
<Route path="initial/:creatID/initialinfo" element={<InitialInfo />} />
|
||||
<Route path="removelens" element={<RemoveLens />} />
|
||||
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
||||
<Route path="generated" element={<GeneratedRecord />} />
|
||||
<Route path="authorization" element={<AuthorizationPage />} />
|
||||
|
||||
|
||||
@@ -262,6 +262,27 @@ export async function getMenuConfigs(): Promise<any[]> {
|
||||
export async function getRechargePackages(): Promise<any[]> {
|
||||
return api.get('/recharge-packages');
|
||||
}
|
||||
|
||||
export async function getPaymentMethods(): Promise<{ alipay: boolean; wechat: boolean }> {
|
||||
return api.get('/payments/methods');
|
||||
}
|
||||
|
||||
export async function createRechargeOrder(planId: string, method: string = 'wechat'): Promise<any> {
|
||||
return api.post('/payments/recharge', { plan: planId, method });
|
||||
}
|
||||
|
||||
export async function getPaymentOrders(): Promise<any[]> {
|
||||
return api.get('/payments/orders');
|
||||
}
|
||||
|
||||
export async function getPaymentOrder(orderNo: string): Promise<any> {
|
||||
return api.get(`/payments/orders/${orderNo}`);
|
||||
}
|
||||
|
||||
export async function cancelPaymentOrder(orderNo: string): Promise<void> {
|
||||
return api.post(`/payments/orders/${orderNo}/cancel`);
|
||||
}
|
||||
|
||||
export async function getCreditRatios(): Promise<any[]> {
|
||||
return api.get('/credits/ratios');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography } from 'antd';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio } from 'antd';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
FireFilled,
|
||||
CrownFilled,
|
||||
BankFilled,
|
||||
QrcodeOutlined,
|
||||
CloseOutlined,
|
||||
WechatOutlined,
|
||||
AlipayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
import { getMenuConfigs, getRechargePackages, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
|
||||
import NotificationPopup from '../NotificationPopup';
|
||||
|
||||
interface MenuConfig {
|
||||
@@ -88,7 +89,17 @@ const AppLayout: React.FC = () => {
|
||||
const [siteName, setSiteName] = useState('VideoGen.AI');
|
||||
const [siteLogo, setSiteLogo] = useState('');
|
||||
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
|
||||
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null);
|
||||
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string; method: string } | null>(null);
|
||||
const [paymentMethod, setPaymentMethod] = useState<string>('alipay');
|
||||
const [paying, setPaying] = useState(false);
|
||||
const [countdown, setCountdown] = useState(180); // 默认180秒超时
|
||||
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const currentOrderNoRef = useRef<string | null>(null);
|
||||
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
|
||||
|
||||
// LocalStorage keys
|
||||
const PENDING_ORDER_KEY = 'pending_payment_order';
|
||||
|
||||
// 监听预览弹窗状态,关闭浮动按钮
|
||||
useEffect(() => {
|
||||
@@ -114,6 +125,57 @@ const AppLayout: React.FC = () => {
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 检查并恢复待处理的支付订单
|
||||
useEffect(() => {
|
||||
const checkPendingOrder = async () => {
|
||||
const savedOrderStr = localStorage.getItem(PENDING_ORDER_KEY);
|
||||
if (savedOrderStr) {
|
||||
try {
|
||||
const savedOrder = JSON.parse(savedOrderStr);
|
||||
// 查询订单状态
|
||||
const order = await getPaymentOrder(savedOrder.orderNo);
|
||||
if (order.status === 'pending') {
|
||||
// 订单仍然待支付,恢复弹窗
|
||||
setCurrentPaymentInfo({
|
||||
price: savedOrder.price,
|
||||
credits: savedOrder.credits,
|
||||
qrCode: savedOrder.qrCode,
|
||||
method: savedOrder.method,
|
||||
});
|
||||
currentOrderNoRef.current = savedOrder.orderNo;
|
||||
// 计算剩余时间
|
||||
const now = Date.now();
|
||||
const createdAt = new Date(savedOrder.createdAt).getTime();
|
||||
const timeoutSeconds = savedOrder.timeoutSeconds || 180;
|
||||
const elapsedSeconds = Math.floor((now - createdAt) / 1000);
|
||||
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
|
||||
|
||||
if (remainingSeconds > 0) {
|
||||
setQrCodeModalOpen(true);
|
||||
startPolling(savedOrder.orderNo, remainingSeconds);
|
||||
} else {
|
||||
// 已超时,清除
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
}
|
||||
} else if (order.status === 'paid') {
|
||||
// 已支付
|
||||
message.success('支付成功!积分已到账');
|
||||
useAuthStore.getState().refreshUser();
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
} else {
|
||||
// 订单已取消或其他状态,清除
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
}
|
||||
} catch {
|
||||
// 查询失败,清除
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkPendingOrder();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getMenuConfigs().then(data => {
|
||||
let items = data.filter((m: any) => m.is_active !== false && m.isActive !== false);
|
||||
@@ -136,6 +198,12 @@ const AppLayout: React.FC = () => {
|
||||
getRechargePackages().then(data => {
|
||||
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
|
||||
}).catch(() => {});
|
||||
getPaymentMethods().then(data => {
|
||||
setEnabledMethods(data);
|
||||
// Auto-select the first enabled method
|
||||
if (data.alipay) setPaymentMethod('alipay');
|
||||
else if (data.wechat) setPaymentMethod('wechat');
|
||||
}).catch(() => {});
|
||||
loadNotifications();
|
||||
}, [user]);
|
||||
|
||||
@@ -173,6 +241,68 @@ const AppLayout: React.FC = () => {
|
||||
setRechargeModalOpen(true);
|
||||
};
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
}
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
countdownTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
|
||||
stopPolling();
|
||||
setCountdown(timeoutSeconds);
|
||||
|
||||
// 订单状态轮询(每2秒查询一次,只查询当前订单
|
||||
const pollingTimer = setInterval(async () => {
|
||||
try {
|
||||
const order = await getPaymentOrder(orderNo);
|
||||
if (order.status === 'paid') {
|
||||
stopPolling();
|
||||
currentOrderNoRef.current = null;
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
message.success('支付成功!积分已到账');
|
||||
useAuthStore.getState().refreshUser();
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
setSelectedPlan(null);
|
||||
} else if (order.status === 'cancelled') {
|
||||
stopPolling();
|
||||
currentOrderNoRef.current = null;
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
}
|
||||
} catch {
|
||||
// ignore polling errors
|
||||
}
|
||||
}, 2000);
|
||||
pollingTimerRef.current = pollingTimer;
|
||||
|
||||
// 倒计时
|
||||
const countdownTimer = setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
// 超时自动取消
|
||||
stopPolling();
|
||||
if (currentOrderNoRef.current) {
|
||||
cancelPaymentOrder(currentOrderNoRef.current).catch(() => {});
|
||||
currentOrderNoRef.current = null;
|
||||
}
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
message.warning('订单已超时,请重新充值');
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
setSelectedPlan(null);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
countdownTimerRef.current = countdownTimer;
|
||||
}, [stopPolling]);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
{/* Desktop Sidebar */}
|
||||
@@ -516,28 +646,90 @@ const AppLayout: React.FC = () => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
|
||||
{/* Payment method selection */}
|
||||
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
|
||||
<div style={{ marginTop: 20, marginBottom: 8, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
|
||||
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
|
||||
⚠️ 暂无可用的支付方式,请联系管理员开启支付功能
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginTop: 20, marginBottom: 8 }}>
|
||||
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}>选择支付方式</Typography.Text>
|
||||
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
|
||||
style={{ display: 'flex', gap: 12 }}>
|
||||
{enabledMethods.alipay && (
|
||||
<Radio.Button value="alipay" style={{
|
||||
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
|
||||
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
||||
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
|
||||
}}>
|
||||
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
|
||||
支付宝
|
||||
</Radio.Button>
|
||||
)}
|
||||
{enabledMethods.wechat && (
|
||||
<Radio.Button value="wechat" style={{
|
||||
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
|
||||
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
|
||||
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
|
||||
}}>
|
||||
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
|
||||
微信支付
|
||||
</Radio.Button>
|
||||
)}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}>取消</Button>
|
||||
<Button type="primary" size="large" disabled={!selectedPlan}
|
||||
onClick={() => {
|
||||
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
|
||||
onClick={async () => {
|
||||
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
|
||||
if (plan) {
|
||||
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
|
||||
// 生成随机支付内容(模拟微信支付订单号)
|
||||
const orderId = `WX${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||
const paymentContent = JSON.stringify({
|
||||
orderId,
|
||||
amount: plan.price,
|
||||
credits: totalCredits,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
setCurrentPaymentInfo({
|
||||
price: plan.price,
|
||||
credits: totalCredits,
|
||||
qrCode: paymentContent
|
||||
});
|
||||
setRechargeModalOpen(false);
|
||||
setQrCodeModalOpen(true);
|
||||
if (!plan) return;
|
||||
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
|
||||
try {
|
||||
setPaying(true);
|
||||
const order = await createRechargeOrder(plan.id, paymentMethod);
|
||||
if (order.paymentMethod === 'alipay' && order.qrUrl) {
|
||||
// Alipay: show the real QR code URL from the backend
|
||||
const paymentInfo = {
|
||||
price: plan.price,
|
||||
credits: totalCredits,
|
||||
qrCode: order.qrUrl,
|
||||
method: 'alipay',
|
||||
};
|
||||
setCurrentPaymentInfo(paymentInfo);
|
||||
setRechargeModalOpen(false);
|
||||
setQrCodeModalOpen(true);
|
||||
currentOrderNoRef.current = order.orderNo;
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
|
||||
orderNo: order.orderNo,
|
||||
price: plan.price,
|
||||
credits: totalCredits,
|
||||
qrCode: order.qrUrl,
|
||||
method: 'alipay',
|
||||
createdAt: order.createdAt || new Date().toISOString(),
|
||||
timeoutSeconds: 180,
|
||||
}));
|
||||
|
||||
// Start polling for payment status
|
||||
startPolling(order.orderNo);
|
||||
} else {
|
||||
// WeChat or mock mode (mock auto-completes, no QR needed)
|
||||
message.success('充值成功!积分已到账');
|
||||
useAuthStore.getState().refreshUser();
|
||||
setRechargeModalOpen(false);
|
||||
setSelectedPlan(null);
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '创建订单失败,请重试');
|
||||
} finally {
|
||||
setPaying(false);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
@@ -554,7 +746,17 @@ const AppLayout: React.FC = () => {
|
||||
{/* QR Code Payment Modal */}
|
||||
<Modal
|
||||
open={qrCodeModalOpen}
|
||||
onCancel={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
|
||||
onCancel={async () => {
|
||||
stopPolling();
|
||||
// Mark order as cancelled if it's still pending
|
||||
if (currentOrderNoRef.current) {
|
||||
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
|
||||
currentOrderNoRef.current = null;
|
||||
}
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
}}
|
||||
footer={null}
|
||||
width={400}
|
||||
closable={false}
|
||||
@@ -567,15 +769,25 @@ const AppLayout: React.FC = () => {
|
||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
<div style={{
|
||||
width: 48, height: 48,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
background: currentPaymentInfo?.method === 'alipay'
|
||||
? 'linear-gradient(135deg, #1677ff, #0958d9)'
|
||||
: 'linear-gradient(135deg, #07c160, #06ae56)',
|
||||
borderRadius: 16,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
margin: '0 auto 12px',
|
||||
}}>
|
||||
<QrcodeOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
{currentPaymentInfo?.method === 'alipay'
|
||||
? <AlipayCircleOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
: <WechatOutlined style={{ fontSize: 24, color: '#fff' }} />}
|
||||
</div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>微信支付</Typography.Title>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>请使用微信扫描二维码完成支付</Typography.Text>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
{currentPaymentInfo?.method === 'alipay' ? '支付宝支付' : '微信支付'}
|
||||
</Typography.Title>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>
|
||||
{currentPaymentInfo?.method === 'alipay'
|
||||
? '请使用支付宝扫描二维码完成支付'
|
||||
: '请使用微信扫描二维码完成支付'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* QR Code */}
|
||||
@@ -622,6 +834,23 @@ const AppLayout: React.FC = () => {
|
||||
}}>
|
||||
购买 {currentPaymentInfo?.credits || 0} 积分
|
||||
</div>
|
||||
{/* 倒计时显示 */}
|
||||
<div style={{
|
||||
marginTop: 12,
|
||||
padding: '8px 16px',
|
||||
background: countdown <= 30 ? '#fef2f2' : '#f0f9ff',
|
||||
borderRadius: 8,
|
||||
border: countdown <= 30 ? '1px solid #fecaca' : '1px solid #bae6fd',
|
||||
display: 'inline-block',
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: countdown <= 30 ? '#dc2626' : '#0284c7',
|
||||
}}>
|
||||
订单将在 <span style={{ fontSize: 16, fontWeight: 800 }}>{countdown}</span> 秒后关闭
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -640,31 +869,24 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Footer Buttons */}
|
||||
<div style={{ marginTop: 20, display: 'flex', gap: 12 }}>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
|
||||
style={{ flex: 1, borderRadius: 10 }}
|
||||
>
|
||||
取消支付
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
message.success('支付成功!积分已到账');
|
||||
block
|
||||
onClick={async () => {
|
||||
stopPolling();
|
||||
if (currentOrderNoRef.current) {
|
||||
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {}
|
||||
currentOrderNoRef.current = null;
|
||||
}
|
||||
localStorage.removeItem(PENDING_ORDER_KEY);
|
||||
setQrCodeModalOpen(false);
|
||||
setCurrentPaymentInfo(null);
|
||||
setSelectedPlan(null);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
border: 'none',
|
||||
}}
|
||||
style={{ borderRadius: 10 }}
|
||||
>
|
||||
已完成支付
|
||||
取消支付
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1066,10 +1066,10 @@ const GeneratedRecord: React.FC = () => {
|
||||
<div>
|
||||
<Button
|
||||
|
||||
style={{width:'100%', borderRadius: 8 ,marginTop:20,color:'#4c49cc'}}
|
||||
style={{ width: '100%', borderRadius: 8, marginTop: 20, color: '#4c49cc' }}
|
||||
>
|
||||
|
||||
推送巨量引擎后台
|
||||
推送媒体后台
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ function InitialInfo() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ height: '94vh', background: '#f8fafc' }}>
|
||||
<div style={{ height: 'calc(94vh)', padding: 24, }}>
|
||||
<div style={{ height: 'calc(94vh)', }}>
|
||||
<div style={{ height: '100%', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
|
||||
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
|
||||
<div style={{ borderBottom: '1px solid #e2e8f0', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px', boxSizing: 'border-box' }}>
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button, Table, Tag, Drawer, Input, Upload, message } from 'antd';
|
||||
import { ArrowLeftOutlined, PlayCircleOutlined, XOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
function RemoveInfo() {
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [currentSegment, setCurrentSegment] = useState<number | null>(null);
|
||||
const [productName, setProductName] = useState('');
|
||||
const [productSellingPoint, setProductSellingPoint] = useState('');
|
||||
const [productImage, setProductImage] = useState('');
|
||||
const [detailImage, setDetailImage] = useState('');
|
||||
|
||||
const handleGenerate = (segmentId: number) => {
|
||||
setCurrentSegment(segmentId);
|
||||
setDrawerVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseDrawer = () => {
|
||||
setDrawerVisible(false);
|
||||
setCurrentSegment(null);
|
||||
setProductName('');
|
||||
setProductSellingPoint('');
|
||||
setProductImage('');
|
||||
setDetailImage('');
|
||||
};
|
||||
|
||||
const handleProductImageChange: any = (info: any) => {
|
||||
if (info.fileList.length > 0) {
|
||||
const file = info.fileList[0];
|
||||
if (file.originFileObj) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setProductImage(e.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file.originFileObj);
|
||||
}
|
||||
} else {
|
||||
setProductImage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDetailImageChange: any = (info: any) => {
|
||||
if (info.fileList.length > 0) {
|
||||
const file = info.fileList[0];
|
||||
if (file.originFileObj) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setDetailImage(e.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file.originFileObj);
|
||||
}
|
||||
} else {
|
||||
setDetailImage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualGenerate = () => {
|
||||
// 必填校验
|
||||
if (!productImage) {
|
||||
message.warning('请上传产品图');
|
||||
return;
|
||||
}
|
||||
if (!productName.trim()) {
|
||||
message.warning('请输入产品名称');
|
||||
return;
|
||||
}
|
||||
if (!productSellingPoint.trim()) {
|
||||
message.warning('请输入产品卖点');
|
||||
return;
|
||||
}
|
||||
|
||||
// 输出内容
|
||||
console.log('手动生成 - 片段', currentSegment);
|
||||
console.log('产品图:', productImage);
|
||||
console.log('细节图:', detailImage);
|
||||
console.log('产品名称:', productName);
|
||||
console.log('产品卖点:', productSellingPoint);
|
||||
|
||||
message.success(`手动生成成功!片段: ${currentSegment}`);
|
||||
};
|
||||
|
||||
const mockData = {
|
||||
productName: '返回',
|
||||
uploadTime: '2026-06-09 09:01:16',
|
||||
sellingPoints: ['一键匹配', '连麦聊天'],
|
||||
audience: '123123',
|
||||
audienceAnalysis: '123123',
|
||||
videoUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=320&h=180&fit=crop',
|
||||
segments: [
|
||||
{
|
||||
key: '1',
|
||||
id: 1,
|
||||
timeRange: '00:00 - 00:03',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=120&h=80&fit=crop',
|
||||
content: '11111',
|
||||
lines: 'qqqqqqqqqqq',
|
||||
contentStrategy: '展示礼盒'
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
id: 2,
|
||||
timeRange: '00:03 - 00:06',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=80&fit=crop',
|
||||
content: '1231231231',
|
||||
lines: 'qqqqqqqqqqq',
|
||||
contentStrategy: '开箱展示'
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
id: 3,
|
||||
timeRange: '00:06 - 00:09',
|
||||
thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=120&h=80&fit=crop',
|
||||
content: '123123',
|
||||
lines: 'qqqqqqqqqqq',
|
||||
contentStrategy: '取出产品'
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '片段',
|
||||
width: 100,
|
||||
render: (text: any, record: any) => (
|
||||
<div>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, color: '#333' }}>片段{record.id}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>{record.timeRange}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '片段视频',
|
||||
width: 120,
|
||||
render: (text: any, record: any) => (
|
||||
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden' }}>
|
||||
<img
|
||||
src={record.thumbnail}
|
||||
alt={`片段${record.id}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 28,
|
||||
height: 28,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<PlayCircleOutlined style={{ fontSize: 16, color: '#fff' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '画面内容',
|
||||
width: 250,
|
||||
|
||||
render: (text: any, record: any) => (
|
||||
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
|
||||
{record.content}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '台词',
|
||||
width: 250,
|
||||
|
||||
render: (text: any, record: any) => (
|
||||
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
|
||||
{record.lines}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '内容策略',
|
||||
width: 120,
|
||||
align: 'left' as const,
|
||||
render: (text: any, record: any) => (
|
||||
<div style={{ fontSize: 14, color: '#333', fontWeight: 500 }}>
|
||||
{record.contentStrategy || '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '素材',
|
||||
width: 140,
|
||||
align: 'center' as const,
|
||||
render: (text: any, record: any) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#999', padding: '12px 24px', border: '1px dashed #ddd', borderRadius: 4 }}>
|
||||
等待生成
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleGenerate(record.id)}
|
||||
style={{ color: '#656efa', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
视频生成
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{ minHeight: '94vh', background: '#f5f5f5' }}>
|
||||
<div style={{ background: '#fff', padding: '16px 24px 0 24px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ fontSize: 16, color: '#666' }}
|
||||
/>
|
||||
<span style={{ fontSize: 18, fontWeight: 600 }}>{mockData.productName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ }}>
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: '10px 20px 20px 20px', marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 30, marginBottom: 12 }}>
|
||||
<div style={{ position: 'relative', width: 280, height: 160, borderRadius: 8, overflow: 'hidden', flexShrink: 0 }}>
|
||||
<img
|
||||
src={mockData.videoUrl}
|
||||
alt="视频缩略图"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<PlayCircleOutlined style={{ fontSize: 28, color: '#fff' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}>视频总结</h2>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<span style={{ color: '#999', fontSize: 12 }}>上传时间: {mockData.uploadTime}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style={{ display: 'flex', marginBottom: 12 }}>
|
||||
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>产品名称:</span>
|
||||
<span style={{ color: '#333', fontWeight: 500 }}>{mockData.productName}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', marginBottom: 12 }}>
|
||||
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>卖点词:</span>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{mockData.sellingPoints.map((point, index) => (
|
||||
<Tag key={index} color="purple" style={{ fontSize: 12 }}>
|
||||
{point}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', marginBottom: 12 }}>
|
||||
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>受众群体:</span>
|
||||
<span style={{ color: '#333' }}>{mockData.audience}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>受众分析:</span>
|
||||
<span style={{ color: '#333' }}>{mockData.audienceAnalysis}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, overflow: 'hidden' }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={mockData.segments}
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
rowKey="key"
|
||||
scroll={{ y: 520 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<span>
|
||||
<span style={{ color: '#6366f1' }}>智能视频复刻</span>
|
||||
<span style={{ color: '#6366f1' }}>-片段{currentSegment}</span>
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<XOutlined />}
|
||||
onClick={handleCloseDrawer}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
closable={false}
|
||||
onClose={handleCloseDrawer}
|
||||
open={drawerVisible}
|
||||
width={480}
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<div>
|
||||
<label style={{ fontWeight: 400, color: '#333', marginBottom: 12, display: 'block' }}>
|
||||
产品白底图 <span style={{ color: '#ff4d4f' }}>*</span>
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
onChange={handleProductImageChange}
|
||||
maxCount={1}
|
||||
accept="image/*"
|
||||
style={{ width: 140, height: 140 }}
|
||||
>
|
||||
{!productImage && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
||||
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<span style={{ fontSize: 12, color: '#999' }}>产品图 *</span>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
onChange={handleDetailImageChange}
|
||||
maxCount={1}
|
||||
accept="image/*"
|
||||
style={{ width: 140, height: 140 }}
|
||||
>
|
||||
{!detailImage && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
|
||||
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<span style={{ fontSize: 12, color: '#999' }}>细节图</span>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
|
||||
产品名称
|
||||
</label>
|
||||
<Input
|
||||
value={productName}
|
||||
onChange={(e) => setProductName(e.target.value)}
|
||||
placeholder="请输入产品名称"
|
||||
style={{ height: 48, borderRadius: 8 }}
|
||||
maxLength={10}
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
|
||||
产品卖点
|
||||
</label>
|
||||
<TextArea
|
||||
value={productSellingPoint}
|
||||
onChange={(e) => setProductSellingPoint(e.target.value)}
|
||||
placeholder="请输入产品卖点"
|
||||
style={{ borderRadius: 8 }}
|
||||
maxLength={100}
|
||||
showCount
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 24 }}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleManualGenerate}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
borderColor: '#6366f1',
|
||||
color: '#6366f1',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
手动生成
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveInfo;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CalculatorOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface CreditRatio {
|
||||
id: string;
|
||||
modelName: string;
|
||||
resolution: string;
|
||||
ratio: number;
|
||||
baseCredits: number;
|
||||
perSecondCredits: number;
|
||||
}
|
||||
|
||||
const MOCK_RATIOS: CreditRatio[] = [
|
||||
{ id: 'cr-1', modelName: 'GPT-4o', resolution: '720p', ratio: 1.0, baseCredits: 60, perSecondCredits: 2 },
|
||||
{ id: 'cr-2', modelName: 'GPT-4o', resolution: '1080p', ratio: 1.5, baseCredits: 90, perSecondCredits: 3 },
|
||||
{ id: 'cr-3', modelName: 'GPT-4o', resolution: '4K', ratio: 2.5, baseCredits: 150, perSecondCredits: 5 },
|
||||
{ id: 'cr-4', modelName: 'DeepSeek-V3', resolution: '720p', ratio: 0.8, baseCredits: 48, perSecondCredits: 2 },
|
||||
{ id: 'cr-5', modelName: 'DeepSeek-V3', resolution: '1080p', ratio: 1.2, baseCredits: 72, perSecondCredits: 3 },
|
||||
{ id: 'cr-6', modelName: 'DeepSeek-V3', resolution: '4K', ratio: 2.0, baseCredits: 120, perSecondCredits: 4 },
|
||||
{ id: 'cr-7', modelName: '通用', resolution: '720p', ratio: 1.0, baseCredits: 60, perSecondCredits: 2 },
|
||||
{ id: 'cr-8', modelName: '通用', resolution: '1080p', ratio: 1.5, baseCredits: 90, perSecondCredits: 3 },
|
||||
{ id: 'cr-9', modelName: '通用', resolution: '4K', ratio: 2.5, baseCredits: 150, perSecondCredits: 5 },
|
||||
];
|
||||
|
||||
const AdminCreditRatios: React.FC = () => {
|
||||
const [ratios, setRatios] = useState<CreditRatio[]>(MOCK_RATIOS);
|
||||
const [modal, setModal] = useState<{ open: boolean; ratio: CreditRatio | null }>({ open: false, ratio: null });
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (modal.ratio) {
|
||||
setRatios(prev => prev.map(r => r.id === modal.ratio!.id ? { ...r, ...values } : r));
|
||||
message.success('已更新');
|
||||
} else {
|
||||
setRatios(prev => [...prev, { id: `cr-${Date.now()}`, ...values }]);
|
||||
message.success('已添加');
|
||||
}
|
||||
setModal({ open: false, ratio: null });
|
||||
form.resetFields();
|
||||
} catch { /* validation */ }
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setRatios(prev => prev.filter(r => r.id !== id));
|
||||
message.success('已删除');
|
||||
};
|
||||
|
||||
const openEdit = (ratio?: CreditRatio) => {
|
||||
setModal({ open: true, ratio: ratio || null });
|
||||
if (ratio) form.setFieldsValue(ratio);
|
||||
else { form.resetFields(); form.setFieldsValue({ ratio: 1.0, baseCredits: 60, perSecondCredits: 2 }); }
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模型', dataIndex: 'modelName', width: 150,
|
||||
render: (v: string) => <Tag color="purple">{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '分辨率', dataIndex: 'resolution', width: 100,
|
||||
render: (v: string) => {
|
||||
const colors: Record<string, string> = { '720p': 'default', '1080p': 'blue', '4K': 'gold' };
|
||||
return <Tag color={colors[v] || 'default'}>{v}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '倍率', dataIndex: 'ratio', width: 100, sorter: (a: CreditRatio, b: CreditRatio) => a.ratio - b.ratio,
|
||||
render: (v: number) => (
|
||||
<Typography.Text strong style={{ color: v >= 2 ? '#ef4444' : v >= 1.5 ? '#f59e0b' : '#10b981' }}>
|
||||
x{v}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '基础积分', dataIndex: 'baseCredits', width: 100,
|
||||
render: (v: number) => <Typography.Text>{v} 积分</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '每秒积分', dataIndex: 'perSecondCredits', width: 100,
|
||||
render: (v: number) => <Typography.Text>{v} 积分/秒</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '示例计算 (15秒)', key: 'example', width: 120,
|
||||
render: (_: any, r: CreditRatio) => {
|
||||
const total = Math.round((r.baseCredits + r.perSecondCredits * 15) * r.ratio);
|
||||
return <Typography.Text strong style={{ color: '#6366f1' }}>{total} 积分</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 150, fixed: 'right' as const,
|
||||
render: (_: any, r: CreditRatio) => (
|
||||
<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>
|
||||
<CalculatorOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>积分比例配置</Typography.Text>
|
||||
<Tag color="purple">{ratios.length} 条规则</Tag>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()} style={{ borderRadius: 8 }}>
|
||||
添加比例
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16, fontSize: 13 }}>
|
||||
积分计算公式:(基础积分 + 每秒积分 x 视频时长) x 模型倍率
|
||||
</Typography.Text>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={ratios}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={<Space><CalculatorOutlined />{modal.ratio ? '编辑比例' : '添加比例'}</Space>}
|
||||
open={modal.open}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModal({ open: false, ratio: null }); form.resetFields(); }}
|
||||
okText="保存" cancelText="取消" width={480}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="modelName" label="模型" rules={[{ required: true }]}>
|
||||
<Select size="large" options={[
|
||||
{ value: 'GPT-4o', label: 'GPT-4o' },
|
||||
{ value: 'DeepSeek-V3', label: 'DeepSeek-V3' },
|
||||
{ value: '通用', label: '通用 (默认)' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="resolution" label="分辨率" rules={[{ required: true }]}>
|
||||
<Select size="large" options={[
|
||||
{ value: '720p', label: '720p' },
|
||||
{ value: '1080p', label: '1080p' },
|
||||
{ value: '4K', label: '4K' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="ratio" label="倍率" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<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 }]}>
|
||||
<InputNumber min={0} max={1000} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="perSecondCredits" label="每秒积分" style={{ flex: 1 }} rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={100} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCreditRatios;
|
||||
@@ -1,143 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, DatePicker, Select, Space, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface CreditRecord {
|
||||
id: string;
|
||||
username: string;
|
||||
type: 'recharge' | 'consume';
|
||||
amount: number;
|
||||
balanceAfter: number;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const MOCK_RECORDS: CreditRecord[] = [
|
||||
{ id: 'cr-1', username: 'videomaker', type: 'recharge', amount: 3000, balanceAfter: 3000, description: '会员充值赠送', createdAt: '2026-04-28 10:00:00' },
|
||||
{ id: 'cr-2', username: 'videomaker', type: 'consume', amount: -120, balanceAfter: 2880, description: '提示词优化 - 电商广告视频', createdAt: '2026-04-29 14:22:00' },
|
||||
{ id: 'cr-3', username: 'designer', type: 'recharge', amount: 2000, balanceAfter: 2000, description: '进阶包充值', createdAt: '2026-04-29 16:00:00' },
|
||||
{ id: 'cr-4', username: 'videomaker', type: 'consume', amount: -80, balanceAfter: 2800, description: '提示词优化 - 教育课程视频', createdAt: '2026-04-30 09:15:00' },
|
||||
{ id: 'cr-5', username: 'designer', type: 'consume', amount: -100, balanceAfter: 1900, description: '提示词优化 - 品牌故事视频', createdAt: '2026-05-01 11:30:00' },
|
||||
{ id: 'cr-6', username: 'marketer', type: 'recharge', amount: 5000, balanceAfter: 5000, description: '专业包充值', createdAt: '2026-05-02 08:00:00' },
|
||||
{ id: 'cr-7', username: 'videomaker', type: 'recharge', amount: 500, balanceAfter: 3300, description: '活动赠送积分', createdAt: '2026-05-02 11:00:00' },
|
||||
{ id: 'cr-8', username: 'marketer', type: 'consume', amount: -120, balanceAfter: 4880, description: '提示词优化 - 产品宣传视频', createdAt: '2026-05-03 15:20:00' },
|
||||
{ id: 'cr-9', username: 'editor', type: 'recharge', amount: 1500, balanceAfter: 1500, description: '体验包充值', createdAt: '2026-05-04 10:00:00' },
|
||||
{ id: 'cr-10', username: 'designer', type: 'consume', amount: -200, balanceAfter: 1700, description: '提示词优化 - 游戏预告片', createdAt: '2026-05-05 14:45:00' },
|
||||
];
|
||||
|
||||
const AdminCreditRecords: React.FC = () => {
|
||||
const [records, setRecords] = useState<CreditRecord[]>(MOCK_RECORDS);
|
||||
const [typeFilter, setTypeFilter] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const filtered = typeFilter ? records.filter(r => r.type === typeFilter) : records;
|
||||
|
||||
const totalRecharge = records.filter(r => r.type === 'recharge').reduce((s, r) => s + r.amount, 0);
|
||||
const totalConsume = records.filter(r => r.type === 'consume').reduce((s, r) => s + Math.abs(r.amount), 0);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户', dataIndex: 'username', width: 120,
|
||||
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '类型', dataIndex: 'type', width: 100,
|
||||
render: (v: string) => (
|
||||
<Tag color={v === 'recharge' ? 'green' : 'red'} icon={v === 'recharge' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}>
|
||||
{v === 'recharge' ? '充值' : '消费'}
|
||||
</Tag>
|
||||
),
|
||||
filters: [
|
||||
{ text: '充值', value: 'recharge' },
|
||||
{ text: '消费', value: 'consume' },
|
||||
],
|
||||
onFilter: (value: any, record: CreditRecord) => record.type === value,
|
||||
},
|
||||
{
|
||||
title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: CreditRecord, b: CreditRecord) => a.amount - b.amount,
|
||||
render: (v: number) => (
|
||||
<Typography.Text strong style={{ color: v > 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
|
||||
{v > 0 ? '+' : ''}{v.toLocaleString()}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '变动后余额', dataIndex: 'balanceAfter', width: 120,
|
||||
render: (v: number) => <Typography.Text type="secondary">{v.toLocaleString()}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '说明', dataIndex: 'description', ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '时间', dataIndex: 'createdAt', width: 160,
|
||||
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{v}</Typography.Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Summary Cards */}
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
|
||||
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(16,185,129,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#10b981',
|
||||
}}><ArrowUpOutlined /></div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>总充值</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{totalRecharge.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(239,68,68,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#ef4444',
|
||||
}}><ArrowDownOutlined /></div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>总消费</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{totalConsume.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered={false} style={{ flex: 1, borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(99,102,241,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#6366f1',
|
||||
}}><WalletOutlined /></div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>交易笔数</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#1a1a2e' }}>{records.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 条记录` }}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCreditRecords;
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Col, Row, Statistic, Typography, Table, Tag } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
ProjectOutlined,
|
||||
PlayCircleOutlined,
|
||||
DollarOutlined,
|
||||
ThunderboltOutlined,
|
||||
ArrowUpOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getAdminStats } from '../../api';
|
||||
import type { AdminStats } from '../../types';
|
||||
|
||||
const AdminDashboard: React.FC = () => {
|
||||
const [stats, setStats] = useState<AdminStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const data = await getAdminStats();
|
||||
setStats(data);
|
||||
setLoading(false);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const statCards = stats ? [
|
||||
{ title: '总用户数', value: stats.totalUsers, icon: <UserOutlined />, color: '#6366f1', bg: 'rgba(99,102,241,0.08)' },
|
||||
{ title: '总项目数', value: stats.totalProjects, icon: <ProjectOutlined />, color: '#06b6d4', bg: 'rgba(6,182,212,0.08)' },
|
||||
{ title: '总生成次数', value: stats.totalGenerations, icon: <PlayCircleOutlined />, color: '#10b981', bg: 'rgba(16,185,129,0.08)' },
|
||||
{ title: '总收入(元)', value: stats.totalRevenue, icon: <DollarOutlined />, color: '#f59e0b', bg: 'rgba(245,158,11,0.08)', prefix: '¥' },
|
||||
{ title: '今日消耗积分', value: stats.creditsConsumedToday, icon: <ThunderboltOutlined />, color: '#ef4444', bg: 'rgba(239,68,68,0.08)' },
|
||||
] : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Stats Cards */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{statCards.map((s, i) => (
|
||||
<Col xs={12} sm={8} lg={i < 4 ? 6 : 24} key={s.title}>
|
||||
<Card bordered={false} loading={loading}
|
||||
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: s.bg, display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: s.color, flexShrink: 0,
|
||||
}}>
|
||||
{s.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12, marginBottom: 2 }}>{s.title}</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#1a1a2e' }}>
|
||||
{s.prefix}{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* Quick Info */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="系统信息" bordered={false}
|
||||
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[
|
||||
{ label: '平台名称', value: 'VideoGen.AI' },
|
||||
{ label: 'API版本', value: 'v1.0.0' },
|
||||
{ label: '数据库', value: 'SQLite (本地开发)' },
|
||||
{ label: 'LLM模式', value: 'Mock (模拟)' },
|
||||
{ label: '视频引擎', value: 'Seedance 2.0' },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #f5f6fa' }}>
|
||||
<Typography.Text type="secondary">{item.label}</Typography.Text>
|
||||
<Typography.Text strong>{item.value}</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="充值套餐" bordered={false}
|
||||
style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[
|
||||
{ name: '体验包', credits: 500, price: 49, color: '#f59e0b' },
|
||||
{ name: '进阶包', credits: 2000, price: 168, color: '#6366f1', hot: true },
|
||||
{ name: '专业包', credits: 5000, price: 388, color: '#06b6d4' },
|
||||
{ name: '企业包', credits: 20000, price: 1280, color: '#10b981' },
|
||||
].map(p => (
|
||||
<div key={p.name} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #f5f6fa' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: p.color }} />
|
||||
<Typography.Text strong>{p.name}</Typography.Text>
|
||||
{p.hot && <Tag color="purple" style={{ fontSize: 10, lineHeight: '16px' }}>热门</Tag>}
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong style={{ color: p.color }}>¥{p.price}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>{p.credits.toLocaleString()}积分</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
@@ -1,336 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
AppstoreOutlined, PlusOutlined, EditOutlined, DeleteOutlined, MinusCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface OptionGroup {
|
||||
name: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
interface IndustryItem {
|
||||
id: string;
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
skills: string[];
|
||||
optionGroups: OptionGroup[];
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
const MOCK_INDUSTRIES: IndustryItem[] = [
|
||||
{
|
||||
id: 'ind-1', key: 'ecommerce', label: '电商', description: '电商直播、产品展示、促销活动',
|
||||
skills: ['你是一位专业的电商视频文案专家,擅长将产品卖点转化为视觉语言,注重画面节奏和消费者心理'],
|
||||
optionGroups: [
|
||||
{ name: '视频风格', options: ['实拍展示', '3D动画', '混剪快闪', '沉浸体验'] },
|
||||
{ name: '目标受众', options: ['年轻女性', '家庭用户', '商务人士', '学生群体'] },
|
||||
],
|
||||
isActive: true, sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'ind-2', key: 'education', label: '教育', description: '在线课程、知识付费、培训',
|
||||
skills: ['你是一位专业的教育视频策划专家,擅长将复杂知识点转化为生动易懂的视觉叙事'],
|
||||
optionGroups: [
|
||||
{ name: '课程类型', options: ['知识讲解', '操作演示', '故事叙事', '互动问答'] },
|
||||
],
|
||||
isActive: true, sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'ind-3', key: 'gaming', label: '游戏', description: '游戏预告、赛事宣传、角色展示',
|
||||
skills: ['你是一位专业的游戏视频创意专家,擅长打造震撼视觉体验和沉浸式叙事'],
|
||||
optionGroups: [
|
||||
{ name: '游戏类型', options: ['RPG', 'FPS', 'MOBA', '休闲'] },
|
||||
{ name: '视频类型', options: ['预告片', '宣传片', '教程', '赛事回顾'] },
|
||||
],
|
||||
isActive: true, sortOrder: 3,
|
||||
},
|
||||
{
|
||||
id: 'ind-4', key: 'medical', label: '医疗', description: '医疗健康、药品宣传、科普',
|
||||
skills: ['你是一位专业的医疗健康视频文案专家,擅长将医学知识转化为通俗易懂的视觉内容'],
|
||||
optionGroups: [],
|
||||
isActive: true, sortOrder: 4,
|
||||
},
|
||||
{
|
||||
id: 'ind-5', key: 'finance', label: '金融', description: '理财产品、保险、银行服务',
|
||||
skills: ['你是一位专业的金融视频文案专家,擅长将复杂的金融产品转化为易于理解的视觉表达'],
|
||||
optionGroups: [],
|
||||
isActive: true, sortOrder: 5,
|
||||
},
|
||||
{
|
||||
id: 'ind-6', key: 'realestate', label: '房产', description: '楼盘展示、户型介绍、周边配套',
|
||||
skills: ['你是一位专业的房产视频策划专家,擅长通过镜头语言展现空间美感和生活场景'],
|
||||
optionGroups: [
|
||||
{ name: '展示方式', options: ['航拍全景', '室内漫游', '样板间', '周边实景'] },
|
||||
],
|
||||
isActive: true, sortOrder: 6,
|
||||
},
|
||||
{
|
||||
id: 'ind-7', key: 'food', label: '餐饮', description: '美食制作、餐厅宣传、食材展示',
|
||||
skills: ['你是一位专业的美食视频创意专家,擅长用镜头捕捉食物的色香味,营造食欲感'],
|
||||
optionGroups: [
|
||||
{ name: '拍摄风格', options: ['特写慢放', '制作过程', '美食探店', '食材溯源'] },
|
||||
],
|
||||
isActive: true, sortOrder: 7,
|
||||
},
|
||||
{
|
||||
id: 'ind-8', key: 'travel', label: '旅游', description: '景点宣传、酒店推荐、旅行攻略',
|
||||
skills: ['你是一位专业的旅游视频文案专家,擅长用镜头语言展现目的地魅力和旅行体验'],
|
||||
optionGroups: [
|
||||
{ name: '内容形式', options: ['Vlog', '攻略指南', '风景大片', '人文记录'] },
|
||||
],
|
||||
isActive: true, sortOrder: 8,
|
||||
},
|
||||
{
|
||||
id: 'ind-9', key: 'tech', label: '科技', description: '科技产品、SaaS服务、AI应用',
|
||||
skills: ['你是一位专业的科技视频策划专家,擅长将技术概念转化为直观的视觉演示'],
|
||||
optionGroups: [
|
||||
{ name: '演示方式', options: ['产品演示', '对比评测', '概念解析', '场景模拟'] },
|
||||
],
|
||||
isActive: true, sortOrder: 9,
|
||||
},
|
||||
{
|
||||
id: 'ind-10', key: 'other', label: '其他', description: '通用行业',
|
||||
skills: ['你是一位专业的视频导演和文案专家,擅长将主题转化为富有感染力的视觉叙事'],
|
||||
optionGroups: [],
|
||||
isActive: true, sortOrder: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const AdminIndustries: React.FC = () => {
|
||||
const [industries, setIndustries] = useState<IndustryItem[]>(MOCK_INDUSTRIES);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [modal, setModal] = useState<{ open: boolean; item: IndustryItem | null }>({ open: false, item: null });
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
const skills = values.skills_wentutujie?.trim() ? [values.skills_wentutujie.trim()] : [];
|
||||
const optionGroups: OptionGroup[] = (values.optionGroups || [])
|
||||
.filter((g: any) => g?.name?.trim())
|
||||
.map((g: any) => ({
|
||||
name: g.name.trim(),
|
||||
options: (g.options || []).filter((o: string) => o?.trim()),
|
||||
}))
|
||||
.filter((g: OptionGroup) => g.options.length > 0);
|
||||
|
||||
if (modal.item) {
|
||||
setIndustries(prev => prev.map(i => i.id === modal.item!.id ? { ...i, ...values, skills, optionGroups } : i));
|
||||
message.success('已更新');
|
||||
} else {
|
||||
const newItem: IndustryItem = {
|
||||
id: `ind-${Date.now()}`,
|
||||
key: values.key,
|
||||
label: values.label,
|
||||
description: values.description || '',
|
||||
skills,
|
||||
optionGroups,
|
||||
isActive: values.isActive !== false,
|
||||
sortOrder: industries.length + 1,
|
||||
};
|
||||
setIndustries(prev => [...prev, newItem]);
|
||||
message.success('已添加');
|
||||
}
|
||||
setModal({ open: false, item: null });
|
||||
form.resetFields();
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) return;
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setIndustries(prev => prev.filter(i => i.id !== id));
|
||||
message.success('已删除');
|
||||
};
|
||||
|
||||
const openEdit = (item?: IndustryItem) => {
|
||||
setModal({ open: true, item: item || null });
|
||||
if (item) {
|
||||
form.setFieldsValue({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
skills_wentutujie: item.skills[0] || '',
|
||||
optionGroups: item.optionGroups.length > 0 ? item.optionGroups : [{ name: '', options: [] }],
|
||||
isActive: item.isActive,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ isActive: true, optionGroups: [{ name: '', options: [] }] });
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '行业', key: 'industry', width: 160,
|
||||
render: (_: any, r: IndustryItem) => (
|
||||
<div>
|
||||
<Typography.Text strong>{r.label}</Typography.Text>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.key}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '描述', dataIndex: 'description', ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '选项配置', key: 'optionGroups', width: 260,
|
||||
render: (_: any, r: IndustryItem) => {
|
||||
if (!r.optionGroups || r.optionGroups.length === 0) {
|
||||
return <Typography.Text style={{ fontSize: 12, color: '#cbd5e1' }}>未配置</Typography.Text>;
|
||||
}
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{r.optionGroups.map((g, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Tag color="blue" style={{ margin: 0, fontSize: 11 }}>{g.name}</Tag>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#64748b' }}>
|
||||
{g.options.slice(0, 3).join('、')}{g.options.length > 3 ? `...${g.options.length}项` : ''}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '文图理解提示词', dataIndex: 'skills', width: 200,
|
||||
render: (skills: string[]) => (
|
||||
<Typography.Text ellipsis style={{ fontSize: 12 }}>
|
||||
{skills[0] || '-'}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
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: IndustryItem) => (
|
||||
<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>
|
||||
<AppstoreOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>行业与技能配置</Typography.Text>
|
||||
<Tag color="purple">{industries.length} 个行业</Tag>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()} style={{ borderRadius: 8 }}>
|
||||
添加行业
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={industries}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={<Space><AppstoreOutlined />{modal.item ? '编辑行业' : '添加行业'}</Space>}
|
||||
open={modal.open}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }}
|
||||
okText="保存" cancelText="取消" width={640}
|
||||
confirmLoading={saving}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="key" label="行业标识" style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请输入标识' }]}>
|
||||
<Input placeholder="例如:ecommerce" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="label" label="行业名称" style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="例如:电商" size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="行业描述">
|
||||
<Input.TextArea rows={2} placeholder="行业描述" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="skills_wentutujie" label="文图理解提示词" extra="用于LLM优化提示词的系统指令,根据行业特点引导AI理解文案与画面的关系">
|
||||
<Input.TextArea rows={3} placeholder="请输入文图理解提示词,例如: 你是一位专业的电商视频文案专家,擅长将产品卖点转化为视觉语言" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Option Groups */}
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Typography.Text strong style={{ fontSize: 13 }}>行业选项配置</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', marginLeft: 8 }}>
|
||||
添加选项组,每组包含名称和多个选项,前台将显示为下拉选择
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Form.List name="optionGroups">
|
||||
{(fields, { add, remove }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 16 }}>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} style={{
|
||||
display: 'flex', gap: 8, alignItems: 'flex-start',
|
||||
padding: '10px 12px', borderRadius: 10,
|
||||
background: '#f8f9fc', border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<Form.Item {...restField} name={[name, 'name']} label="选项名称" style={{ marginBottom: 0 }}
|
||||
rules={[{ required: true, message: '请输入选项名称' }]}>
|
||||
<Input placeholder="例如:视频风格" size="middle" style={{ borderRadius: 8 }} />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'options']} label="选项内容" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
mode="tags"
|
||||
size="middle"
|
||||
placeholder="输入选项后回车添加"
|
||||
style={{ borderRadius: 8 }}
|
||||
tokenSeparators={[',', ',', '、']}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => remove(name)}
|
||||
style={{ color: '#ef4444', fontSize: 16, marginTop: 34, cursor: 'pointer', flexShrink: 0 }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="dashed" onClick={() => add()} block
|
||||
icon={<PlusOutlined />}
|
||||
style={{ borderRadius: 8, height: 36 }}
|
||||
>
|
||||
添加选项组
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Form.Item name="isActive" label="启用状态" valuePropName="checked" initialValue={true}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminIndustries;
|
||||
@@ -1,163 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Layout, Menu, Avatar, Typography, Space, Dropdown, Spin } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
RobotOutlined,
|
||||
SettingOutlined,
|
||||
BellOutlined,
|
||||
ThunderboltOutlined,
|
||||
LogoutOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
WalletOutlined,
|
||||
CalculatorOutlined,
|
||||
DollarOutlined,
|
||||
AppstoreOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation, Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
const AdminLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, loading, logout } = useAuthStore();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/admin/login" replace />;
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/admin', icon: <DashboardOutlined />, label: '数据概览' },
|
||||
{ key: '/admin/users', icon: <UserOutlined />, label: '用户管理' },
|
||||
{ key: '/admin/credit-records', icon: <WalletOutlined />, label: '交易流水' },
|
||||
{ key: '/admin/models', icon: <RobotOutlined />, label: '模型配置' },
|
||||
{ key: '/admin/credit-ratios', icon: <CalculatorOutlined />, label: '积分比例' },
|
||||
{ key: '/admin/video-engines', icon: <PlayCircleOutlined />, label: '视频引擎' },
|
||||
{ key: '/admin/industries', icon: <AppstoreOutlined />, label: '行业配置' },
|
||||
{ key: '/admin/payment', icon: <DollarOutlined />, label: '支付配置' },
|
||||
{ key: '/admin/settings', icon: <SettingOutlined />, label: '系统设置' },
|
||||
{ key: '/admin/notifications', icon: <BellOutlined />, label: '消息推送' },
|
||||
];
|
||||
|
||||
const selectedKey = location.pathname;
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
width={220}
|
||||
theme="dark"
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #0f0f23 0%, #1a1a35 100%)',
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div style={{
|
||||
height: 64, display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', gap: 10,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<ThunderboltOutlined style={{ fontSize: 16, color: '#fff' }} />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span style={{ color: '#f1f5f9', fontSize: 15, fontWeight: 700 }}>
|
||||
管理后台
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Menu */}
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
style={{ background: 'transparent', borderRight: 0, marginTop: 8 }}
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
{/* User block */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 48, left: 0, right: 0,
|
||||
padding: collapsed ? '12px 8px' : '12px 16px',
|
||||
borderTop: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<Dropdown menu={{
|
||||
items: [
|
||||
{ key: 'front', icon: <ThunderboltOutlined />, label: '返回前台' },
|
||||
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', danger: true },
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'front') navigate('/projects');
|
||||
else if (key === 'logout') { logout(); navigate('/admin/login'); }
|
||||
},
|
||||
}} placement="topRight" arrow>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
gap: 10, padding: '8px 10px', borderRadius: 10,
|
||||
cursor: 'pointer', background: 'rgba(255,255,255,0.04)',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
<Avatar size={28} icon={<UserOutlined />}
|
||||
style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }} />
|
||||
{!collapsed && (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: '#e2e8f0', fontSize: 12, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{user?.username}
|
||||
</div>
|
||||
<div style={{ color: 'rgba(148,163,184,0.5)', fontSize: 10 }}>管理员</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
height: 56, background: '#fff', borderBottom: '1px solid #f0f0f5',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '0 24px',
|
||||
}}>
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>
|
||||
{menuItems.find(m => m.key === selectedKey)?.label || '管理后台'}
|
||||
</Typography.Text>
|
||||
<Space>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{user?.username}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<Content style={{ padding: 24, background: '#f5f6fa', overflow: 'auto' }}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLayout;
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Card, Form, Input, message, Typography } from 'antd';
|
||||
import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
|
||||
const AdminLoginPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuthStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogin = async (values: { username: string; password: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(values.username, values.password);
|
||||
message.success('登录成功');
|
||||
navigate('/admin');
|
||||
} catch {
|
||||
message.error('登录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a35 50%, #0f0f23 100%)',
|
||||
}}>
|
||||
<Card bordered={false} style={{
|
||||
width: 420, borderRadius: 16, boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
{/* Logo */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 14, margin: '0 auto 16px',
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 8px 24px rgba(99,102,241,0.3)',
|
||||
}}>
|
||||
<ThunderboltOutlined style={{ fontSize: 26, color: '#fff' }} />
|
||||
</div>
|
||||
<Typography.Title level={3} style={{ margin: 0 }}>
|
||||
VideoGen<span style={{ color: '#6366f1' }}>.AI</span>
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">管理后台</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form onFinish={handleLogin} layout="vertical" initialValues={{ username: 'admin', password: 'admin123' }}>
|
||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input placeholder="用户名" size="large" prefix={<UserOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password placeholder="密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 8 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block size="large"
|
||||
style={{
|
||||
borderRadius: 10, fontWeight: 600, height: 44,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
border: 'none',
|
||||
}}>
|
||||
登录管理后台
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
演示账号: admin / admin123
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLoginPage;
|
||||
@@ -1,222 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
RobotOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getModelConfigs, saveModelConfig, deleteModelConfig } from '../../api';
|
||||
import type { ModelConfig } from '../../types';
|
||||
|
||||
const AdminModels: React.FC = () => {
|
||||
const [models, setModels] = useState<ModelConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [modal, setModal] = useState<{ open: boolean; model: ModelConfig | null }>({ open: false, model: null });
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const data = await getModelConfigs();
|
||||
setModels(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await saveModelConfig({
|
||||
...modal.model,
|
||||
...values,
|
||||
id: modal.model?.id,
|
||||
});
|
||||
message.success(modal.model?.id ? '模型配置已更新' : '模型配置已添加');
|
||||
setModal({ open: false, model: null });
|
||||
form.resetFields();
|
||||
load();
|
||||
} catch { /* validation */ }
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await deleteModelConfig(id);
|
||||
message.success('模型配置已删除');
|
||||
load();
|
||||
};
|
||||
|
||||
const openEdit = (model?: ModelConfig) => {
|
||||
setModal({ open: true, model: model || null });
|
||||
if (model) {
|
||||
form.setFieldsValue(model);
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
provider: 'sdk',
|
||||
weight: 1,
|
||||
maxTokens: 4096,
|
||||
temperature: 0.7,
|
||||
isActive: true,
|
||||
priority: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '模型名称', dataIndex: 'name', width: 150,
|
||||
render: (v: string, r: ModelConfig) => (
|
||||
<Space>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: r.isActive
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: 'linear-gradient(135deg, #94a3b8, #cbd5e1)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontSize: 14,
|
||||
}}>
|
||||
<RobotOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{v}</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.modelName}</div>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '提供商', dataIndex: 'provider', width: 140,
|
||||
render: (v: string) => {
|
||||
const labelMap: Record<string, string> = {
|
||||
sdk: 'SDK模式',
|
||||
openai_compatible: 'OpenAI兼容',
|
||||
mock: 'Mock模式',
|
||||
};
|
||||
return <Tag color={v === 'mock' ? 'default' : 'blue'}>{labelMap[v] || v}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'API地址', dataIndex: 'apiBase', width: 200,
|
||||
render: (v: string) => (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis>
|
||||
{v || '-'}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '权重', dataIndex: 'weight', width: 80, sorter: (a: ModelConfig, b: ModelConfig) => a.weight - b.weight,
|
||||
},
|
||||
{
|
||||
title: 'Max Tokens', dataIndex: 'maxTokens', width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Temperature', dataIndex: 'temperature', width: 100,
|
||||
render: (v: number) => v.toFixed(1),
|
||||
},
|
||||
{
|
||||
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: ModelConfig) => (
|
||||
<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 }}>
|
||||
<Typography.Text type="secondary">
|
||||
共 {models.length} 个模型配置,按权重进行加权随机调度
|
||||
</Typography.Text>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()}
|
||||
style={{ borderRadius: 8 }}>
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={models}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
title={<Space><RobotOutlined />{modal.model?.id ? '编辑模型' : '添加模型'}</Space>}
|
||||
open={modal.open}
|
||||
onOk={handleSave}
|
||||
onCancel={() => { setModal({ open: false, model: null }); form.resetFields(); }}
|
||||
okText="确认" cancelText="取消" width={560}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="显示名称"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}>
|
||||
<Input placeholder="例如:GPT-4o" size="large" />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="provider" label="提供商" style={{ flex: 1 }}
|
||||
rules={[{ required: true }]}>
|
||||
<Select size="large" options={[
|
||||
{ value: 'sdk', label: 'SDK模式' },
|
||||
{ value: 'openai_compatible', label: 'OpenAI兼容' },
|
||||
{ value: 'mock', label: 'Mock模式' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="modelName" label="模型标识" style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请输入模型标识' }]}>
|
||||
<Input placeholder="例如:gpt-4o" size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="apiBase" label="API地址">
|
||||
<Input placeholder="https://api.openai.com/v1" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="apiKey" label="API Key">
|
||||
<Input.Password placeholder="sk-****" size="large" />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="weight" label="权重" style={{ flex: 1 }}
|
||||
rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="maxTokens" label="Max Tokens" style={{ flex: 1 }}
|
||||
rules={[{ required: true }]}>
|
||||
<InputNumber min={256} max={128000} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="temperature" label="Temperature" style={{ flex: 1 }}
|
||||
rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={2} step={0.1} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="priority" label="优先级" style={{ flex: 1 }}
|
||||
rules={[{ required: true }]}>
|
||||
<InputNumber min={0} max={10} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="isActive" label="启用" valuePropName="checked" style={{ flex: 1, paddingTop: 30 }}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminModels;
|
||||
@@ -1,166 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message,
|
||||
} from 'antd';
|
||||
import {
|
||||
BellOutlined, PlusOutlined, DeleteOutlined, SendOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface NotificationRecord {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
target: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const MOCK_NOTIFICATIONS: NotificationRecord[] = [
|
||||
{ id: 'n-1', title: '系统上线通知', content: 'VideoGen.AI 平台正式上线!', type: 'system', target: '全部用户', createdAt: '2026-05-01 09:00:00' },
|
||||
{ id: 'n-2', title: '积分充值优惠', content: '限时活动:充值进阶包额外赠送200积分', type: 'credit', target: '全部用户', createdAt: '2026-05-03 10:00:00' },
|
||||
{ id: 'n-3', title: '账户审核通过', content: '您的账户已通过实名审核', type: 'system', target: 'videomaker', createdAt: '2026-05-05 14:00:00' },
|
||||
];
|
||||
|
||||
const MOCK_USERS = [
|
||||
{ id: 'u-001', username: 'videomaker' },
|
||||
{ id: 'u-002', username: 'designer' },
|
||||
{ id: 'u-003', username: 'marketer' },
|
||||
{ id: 'u-004', username: 'editor' },
|
||||
];
|
||||
|
||||
const AdminNotificationManager: React.FC = () => {
|
||||
const [notifications, setNotifications] = useState<NotificationRecord[]>(MOCK_NOTIFICATIONS);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSend = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const newRecord: NotificationRecord = {
|
||||
id: `n-${Date.now()}`,
|
||||
title: values.title,
|
||||
content: values.content,
|
||||
type: values.type,
|
||||
target: values.target_user_id
|
||||
? MOCK_USERS.find(u => u.id === values.target_user_id)?.username || '指定用户'
|
||||
: '全部用户',
|
||||
createdAt: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
setNotifications(prev => [newRecord, ...prev]);
|
||||
message.success('消息已发送');
|
||||
setModalOpen(false);
|
||||
form.resetFields();
|
||||
} catch { /* validation */ }
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
message.success('已删除');
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'system': return 'blue';
|
||||
case 'credit': return 'orange';
|
||||
case 'promo': return 'purple';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '标题', dataIndex: 'title', width: 200,
|
||||
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '内容', dataIndex: 'content', ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '类型', dataIndex: 'type', width: 80,
|
||||
render: (v: string) => {
|
||||
const labels: Record<string, string> = { system: '系统', credit: '积分', promo: '活动' };
|
||||
return <Tag color={getTypeColor(v)}>{labels[v] || v}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '发送目标', dataIndex: 'target', width: 120,
|
||||
render: (v: string) => (
|
||||
<Tag color={v === '全部用户' ? 'green' : 'blue'}>{v}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '发送时间', dataIndex: 'createdAt', width: 160,
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 80,
|
||||
render: (_: any, r: NotificationRecord) => (
|
||||
<Popconfirm title="确定删除该消息?" onConfirm={() => handleDelete(r.id)}>
|
||||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<BellOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>消息推送管理</Typography.Text>
|
||||
<Tag color="purple">{notifications.length} 条消息</Tag>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)}
|
||||
style={{ borderRadius: 8 }}>
|
||||
发送新消息
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={notifications}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 条消息` }}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Send Notification Modal */}
|
||||
<Modal
|
||||
title={<Space><SendOutlined />发送消息</Space>}
|
||||
open={modalOpen}
|
||||
onOk={handleSend}
|
||||
onCancel={() => { setModalOpen(false); form.resetFields(); }}
|
||||
okText="发送" cancelText="取消" width={520}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="title" label="消息标题"
|
||||
rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入消息标题" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="消息内容"
|
||||
rules={[{ required: true, message: '请输入内容' }]}>
|
||||
<Input.TextArea rows={4} placeholder="请输入消息内容" size="large" />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item name="type" label="消息类型" style={{ flex: 1 }}
|
||||
initialValue="system" rules={[{ required: true }]}>
|
||||
<Select size="large" options={[
|
||||
{ value: 'system', label: '系统通知' },
|
||||
{ value: 'credit', label: '积分通知' },
|
||||
{ value: 'promo', label: '活动通知' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="target_user_id" label="发送目标" style={{ flex: 1 }}
|
||||
extra="留空则发送给全部用户">
|
||||
<Select size="large" allowClear placeholder="全部用户"
|
||||
options={MOCK_USERS.map(u => ({ value: u.id, label: u.username }))} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNotificationManager;
|
||||
@@ -1,114 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Empty, Space, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
BellOutlined, CheckOutlined, InfoCircleOutlined, CreditCardOutlined, ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getNotifications } from '../../api';
|
||||
import type { AdminNotification } from '../../types';
|
||||
|
||||
const AdminNotifications: React.FC = () => {
|
||||
const [notifications, setNotifications] = useState<AdminNotification[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const data = await getNotifications();
|
||||
setNotifications(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'system': return <InfoCircleOutlined style={{ color: '#6366f1' }} />;
|
||||
case 'credit': return <CreditCardOutlined style={{ color: '#f59e0b' }} />;
|
||||
default: return <ExclamationCircleOutlined style={{ color: '#94a3b8' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'system': return <Tag color="blue">系统</Tag>;
|
||||
case 'credit': return <Tag color="orange">积分</Tag>;
|
||||
default: return <Tag>其他</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<BellOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>消息通知</Typography.Text>
|
||||
<Tag color="purple">{notifications.filter(n => !n.isRead).length} 条未读</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{notifications.length === 0 ? (
|
||||
<Empty description="暂无通知" />
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{notifications.map(n => (
|
||||
<Card
|
||||
key={n.id}
|
||||
size="small"
|
||||
bordered
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
borderColor: n.isRead ? '#f0f0f5' : '#e0e7ff',
|
||||
background: n.isRead ? '#fff' : '#fafbff',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: 10, flexShrink: 0,
|
||||
background: n.isRead ? '#f8fafc' : 'rgba(99,102,241,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 18,
|
||||
}}>
|
||||
{getTypeIcon(n.type)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14 }}>
|
||||
{n.title}
|
||||
</Typography.Text>
|
||||
{getTypeLabel(n.type)}
|
||||
{!n.isRead && (
|
||||
<Tag color="red" style={{ fontSize: 10 }}>未读</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 13, marginBottom: 4, lineHeight: 1.6 }}>
|
||||
{n.content}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{n.createdAt}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{!n.isRead && (
|
||||
<Button type="text" size="small" icon={<CheckOutlined />}
|
||||
style={{ flexShrink: 0, color: '#6366f1' }}
|
||||
onClick={() => {
|
||||
setNotifications(prev =>
|
||||
prev.map(item => item.id === n.id ? { ...item, isRead: true } : item)
|
||||
);
|
||||
}}>
|
||||
标记已读
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNotifications;
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, message, Switch, Typography, Divider,
|
||||
} from 'antd';
|
||||
import {
|
||||
SaveOutlined, WechatOutlined, AlipayCircleOutlined, DollarOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface PaymentSetting {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
secret?: boolean;
|
||||
}
|
||||
|
||||
const AdminPaymentConfig: React.FC = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [wechatEnabled, setWechatEnabled] = useState(false);
|
||||
const [alipayEnabled, setAlipayEnabled] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
message.success('支付配置已保存');
|
||||
setSaving(false);
|
||||
} catch { setSaving(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
{/* WeChat Pay */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(7,193,96,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 22, color: '#07c160',
|
||||
}}><WechatOutlined /></div>
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>微信支付</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>微信商户号支付配置</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={wechatEnabled} onChange={setWechatEnabled} checkedChildren="已启用" unCheckedChildren="未启用" />
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical" initialValues={{
|
||||
wechat_mch_id: '',
|
||||
wechat_api_key: '',
|
||||
wechat_cert_path: '',
|
||||
wechat_notify_url: '',
|
||||
}}>
|
||||
<Form.Item name="wechat_mch_id" label="商户号 (MchID)">
|
||||
<Input placeholder="微信支付商户号" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="wechat_api_key" label="API密钥">
|
||||
<Input.Password placeholder="微信支付API密钥" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="wechat_cert_path" label="证书路径">
|
||||
<Input placeholder="apiclient_cert.pem 路径" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="wechat_notify_url" label="回调地址">
|
||||
<Input placeholder="https://yourdomain.com/api/payments/wechat/callback" size="large" disabled={!wechatEnabled} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* Alipay */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(0,122,255,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 22, color: '#007aff',
|
||||
}}><AlipayCircleOutlined /></div>
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>支付宝</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>支付宝应用支付配置</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={alipayEnabled} onChange={setAlipayEnabled} checkedChildren="已启用" unCheckedChildren="未启用" />
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical" initialValues={{
|
||||
alipay_app_id: '',
|
||||
alipay_private_key: '',
|
||||
alipay_public_key: '',
|
||||
alipay_notify_url: '',
|
||||
}}>
|
||||
<Form.Item name="alipay_app_id" label="AppID">
|
||||
<Input placeholder="支付宝应用AppID" size="large" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_private_key" label="应用私钥">
|
||||
<Input.TextArea rows={3} placeholder="支付宝应用私钥 (PKCS8格式)" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_public_key" label="支付宝公钥">
|
||||
<Input.TextArea rows={3} placeholder="支付宝公钥" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
<Form.Item name="alipay_notify_url" label="回调地址">
|
||||
<Input placeholder="https://yourdomain.com/api/payments/alipay/callback" size="large" disabled={!alipayEnabled} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* Recharge Packages */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}
|
||||
title={<span><DollarOutlined style={{ marginRight: 8 }} />充值套餐</span>}>
|
||||
{[
|
||||
{ name: '体验包', credits: 500, price: 49, color: '#f59e0b' },
|
||||
{ name: '进阶包', credits: 2000, price: 168, color: '#6366f1' },
|
||||
{ name: '专业包', credits: 5000, price: 388, color: '#06b6d4' },
|
||||
{ name: '企业包', credits: 20000, price: 1280, color: '#10b981' },
|
||||
].map(p => (
|
||||
<div key={p.name} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '12px 0', borderBottom: '1px solid #f5f6fa',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: p.color }} />
|
||||
<Typography.Text strong>{p.name}</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong style={{ color: p.color, fontSize: 16 }}>¥{p.price}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>{p.credits.toLocaleString()} 积分</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11, marginLeft: 4 }}>({(p.price / p.credits * 100).toFixed(1)}元/百积分)</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}
|
||||
size="large" style={{ borderRadius: 8, minWidth: 140 }}>
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPaymentConfig;
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, message, Space, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
SettingOutlined, SaveOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getSystemConfigs, updateSystemConfig } from '../../api';
|
||||
import type { SystemConfig } from '../../types';
|
||||
|
||||
const AdminSettings: React.FC = () => {
|
||||
const [configs, setConfigs] = useState<SystemConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const data = await getSystemConfigs();
|
||||
setConfigs(data);
|
||||
const formValues: Record<string, string> = {};
|
||||
data.forEach(c => { formValues[c.key] = c.value; });
|
||||
form.setFieldsValue(formValues);
|
||||
setLoading(false);
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
for (const config of configs) {
|
||||
const newVal = values[config.key];
|
||||
if (newVal !== config.value) {
|
||||
await updateSystemConfig(config.id, newVal);
|
||||
}
|
||||
}
|
||||
message.success('系统配置已保存');
|
||||
const data = await getSystemConfigs();
|
||||
setConfigs(data);
|
||||
setSaving(false);
|
||||
} catch {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const groupedConfigs: Record<string, SystemConfig[]> = {
|
||||
'站点信息': configs.filter(c => c.key.startsWith('site_')),
|
||||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||||
};
|
||||
|
||||
const getFieldDescription = (config: SystemConfig): string => {
|
||||
const descMap: Record<string, string> = {
|
||||
site_name: '平台显示名称,将展示在页面标题和导航栏',
|
||||
site_logo: '平台Logo图片URL,建议尺寸 200x40px',
|
||||
seo_title: '搜索引擎结果中显示的标题',
|
||||
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
|
||||
seo_keywords: '用逗号分隔的关键词列表',
|
||||
};
|
||||
return descMap[config.key] || config.description || '';
|
||||
};
|
||||
|
||||
const getFieldComponent = (config: SystemConfig) => {
|
||||
if (config.key === 'seo_description') {
|
||||
return <Input.TextArea rows={3} placeholder={config.description} size="large" />;
|
||||
}
|
||||
if (config.key === 'seo_keywords') {
|
||||
return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />;
|
||||
}
|
||||
return <Input placeholder={config.description} size="large" />;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Card loading bordered={false} style={{ borderRadius: 12 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(99,102,241,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#6366f1',
|
||||
}}>
|
||||
<SettingOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>系统设置</Typography.Title>
|
||||
<Typography.Text type="secondary">管理站点基础信息和SEO配置</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
{Object.entries(groupedConfigs).map(([group, items]) => (
|
||||
<div key={group} style={{ marginBottom: 24 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||||
{group}
|
||||
</Typography.Text>
|
||||
{items.map(config => (
|
||||
<Form.Item
|
||||
key={config.id}
|
||||
name={config.key}
|
||||
label={<span style={{ fontWeight: 500 }}>{config.description}</span>}
|
||||
extra={getFieldDescription(config)}
|
||||
>
|
||||
{getFieldComponent(config)}
|
||||
</Form.Item>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}
|
||||
size="large" style={{ borderRadius: 8, minWidth: 140 }}>
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSettings;
|
||||
@@ -1,182 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Space, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getAdminUsers, adjustCredits, toggleUserStatus } from '../../api';
|
||||
import type { AdminUser } from '../../types';
|
||||
|
||||
const AdminUsers: React.FC = () => {
|
||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
const data = await getAdminUsers(search || undefined);
|
||||
setUsers(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const handleSearch = () => load();
|
||||
|
||||
const handleAdjustCredits = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const { user } = creditModal;
|
||||
if (!user) return;
|
||||
await adjustCredits(user.id, values.amount, values.reason);
|
||||
message.success(`已${values.amount > 0 ? '增加' : '扣除'} ${Math.abs(values.amount)} 积分`);
|
||||
setCreditModal({ open: false, user: null });
|
||||
form.resetFields();
|
||||
load();
|
||||
} catch { /* validation */ }
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (user: AdminUser) => {
|
||||
await toggleUserStatus(user.id, !user.isActive);
|
||||
message.success(user.isActive ? '已禁用该用户' : '已启用该用户');
|
||||
load();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '用户', key: 'user', width: 200,
|
||||
render: (_: any, r: AdminUser) => (
|
||||
<Space>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: r.isAdmin ? 'linear-gradient(135deg, #f59e0b, #f97316)' : 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontSize: 13, fontWeight: 700,
|
||||
}}>
|
||||
{r.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>
|
||||
{r.username}
|
||||
{r.isAdmin && <Tag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>管理员</Tag>}
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.email}</div>
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '积分余额', dataIndex: 'credits', width: 120, sorter: (a: AdminUser, b: AdminUser) => a.credits - b.credits,
|
||||
render: (v: number) => (
|
||||
<Typography.Text strong style={{ color: v > 0 ? '#10b981' : '#ef4444', fontSize: 15 }}>
|
||||
{v.toLocaleString()}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '手机号', dataIndex: 'phone', width: 130,
|
||||
render: (v: string) => <Typography.Text type="secondary">{v || '-'}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'isActive', width: 80,
|
||||
render: (v: boolean) => (
|
||||
<Tag color={v ? 'green' : 'red'}>{v ? '正常' : '禁用'}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '注册时间', dataIndex: 'createdAt', width: 120,
|
||||
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '最后登录', dataIndex: 'lastLoginAt', width: 140,
|
||||
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{v || '-'}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 200, fixed: 'right' as const,
|
||||
render: (_: any, r: AdminUser) => (
|
||||
<Space size={4}>
|
||||
<Button type="link" size="small" icon={<WalletOutlined />}
|
||||
onClick={() => { setCreditModal({ open: true, user: r }); form.resetFields(); }}>
|
||||
调整积分
|
||||
</Button>
|
||||
{!r.isAdmin && (
|
||||
<Popconfirm
|
||||
title={r.isActive ? '确定禁用该用户?' : '确定启用该用户?'}
|
||||
onConfirm={() => handleToggleStatus(r)}
|
||||
>
|
||||
<Button type="link" size="small" danger={r.isActive}
|
||||
icon={r.isActive ? <StopOutlined /> : <CheckCircleOutlined />}>
|
||||
{r.isActive ? '禁用' : '启用'}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
{/* Search bar */}
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="搜索用户名或邮箱"
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 280, borderRadius: 8 }}
|
||||
allowClear
|
||||
/>
|
||||
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>搜索</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 个用户` }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Adjust Credits Modal */}
|
||||
<Modal
|
||||
title={<Space><WalletOutlined />调整积分 - {creditModal.user?.username}</Space>}
|
||||
open={creditModal.open}
|
||||
onOk={handleAdjustCredits}
|
||||
onCancel={() => { setCreditModal({ open: false, user: null }); form.resetFields(); }}
|
||||
okText="确认" cancelText="取消" width={440}
|
||||
>
|
||||
<div style={{ marginBottom: 16, padding: '12px 16px', background: '#f8fafc', borderRadius: 8 }}>
|
||||
<span style={{ color: '#64748b' }}>当前积分:</span>
|
||||
<span style={{ fontWeight: 800, fontSize: 18, color: '#6366f1' }}>
|
||||
{creditModal.user?.credits.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="amount" label="积分变动"
|
||||
rules={[{ required: true, message: '请输入积分数量' }]}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
placeholder="正数增加,负数扣除"
|
||||
formatter={v => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="reason" label="原因"
|
||||
rules={[{ required: true, message: '请输入调整原因' }]}>
|
||||
<Input.TextArea rows={2} placeholder="请输入调整原因" size="large" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminUsers;
|
||||
@@ -1,214 +0,0 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user