This commit is contained in:
2026-06-05 10:24:19 +08:00
9 changed files with 904 additions and 261 deletions
File diff suppressed because one or more lines are too long
+14 -13
View File
@@ -1,13 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoGen.AI 管理后台</title>
<script type="module" crossorigin src="/assets/index-CllfooUi.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VideoGen.AI 管理后台</title>
<script type="module" crossorigin src="/assets/index-BBz1j0L-.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -3,7 +3,7 @@ import {
Button, Card, Select, Space, Table, Tag, Typography, message,
} from 'antd';
import {
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined,
WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, RollbackOutlined,
} from '@ant-design/icons';
import { getCreditRecords } from '../api';
import { formatDate } from '../utils/formatDate';
@@ -18,6 +18,12 @@ interface CreditRecord {
createdAt: string;
}
const CREDIT_TYPE_MAP: Record<string, { text: string; color: string; icon: React.ReactNode }> = {
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
refund: { text: '退回', color: 'blue', icon: <RollbackOutlined /> },
};
const AdminCreditRecords: React.FC = () => {
const [records, setRecords] = useState<CreditRecord[]>([]);
const [total, setTotal] = useState(0);
@@ -54,11 +60,14 @@ const AdminCreditRecords: React.FC = () => {
},
{
title: '类型', dataIndex: 'type', width: 100,
render: (v: string) => (
<Tag color={v === 'recharge' ? 'green' : 'red'} icon={v === 'recharge' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}>
{v === 'recharge' ? '充值' : '消费'}
</Tag>
),
render: (v: string) => {
const cfg = CREDIT_TYPE_MAP[v] || { text: v || '-', color: 'default', icon: null };
return (
<Tag color={cfg.color} icon={cfg.icon}>
{cfg.text}
</Tag>
);
},
},
{
title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: CreditRecord, b: CreditRecord) => a.amount - b.amount,
@@ -140,6 +149,7 @@ const AdminCreditRecords: React.FC = () => {
{ value: '', label: '全部类型' },
{ value: 'recharge', label: '充值' },
{ value: 'consume', label: '消费' },
{ value: 'refund', label: '退回' },
]}
/>
</Space>
@@ -29,7 +29,6 @@ import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types';
import { formatDate } from '../utils/formatDate';
const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
// const RAW_API_BASE = 'http://ceshi.apiforeign.minzhong.cn';
// 资源 URL 由后端返回相对路径,例如 /generate/images/xxx.png?exp=xxx&sign=xxx。
// 如果 VITE_API_BASE 被配置成 http://host/api 或 /api,这里会去掉 /api,避免拼成 /api/generate/xxx 导致 404。
const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, '');
@@ -323,6 +322,25 @@ const AdminGenerationAiRecords: React.FC = () => {
}, 0);
};
const handleOpenExternalResource = (url?: string | null, type = '素材') => {
if (!url) {
message.warning(`${type}链接为空,暂无法查看`);
return;
}
if (isBlobUrl(url)) {
message.warning('本地临时素材已失效,暂无法查看');
return;
}
if (isUrlExpired(url)) {
message.warning(`${type}链接已超时,请刷新列表或重新搜索后再查看`);
return;
}
window.open(apiUrl(url), '_blank', 'noopener,noreferrer');
};
const columns = useMemo(() => [
{
title: '用户', key: 'user', width: 150,
@@ -649,6 +667,13 @@ const AdminGenerationAiRecords: React.FC = () => {
return (
<div key={refKey} style={{ width: 94 }}>
<div
role="button"
tabIndex={0}
title="点击新页面查看素材"
onClick={() => handleOpenExternalResource(refUrl, isVideo ? '视频素材' : isImage ? '图片素材' : '素材')}
onKeyDown={(e) => {
if (e.key === 'Enter') handleOpenExternalResource(refUrl, isVideo ? '视频素材' : isImage ? '图片素材' : '素材');
}}
style={{
width: 94,
height: 94,
@@ -660,6 +685,7 @@ const AdminGenerationAiRecords: React.FC = () => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
>
{isImage ? (
@@ -687,14 +713,18 @@ const AdminGenerationAiRecords: React.FC = () => {
style={{ display: 'none' }}
/>
<div style={{ color: '#64748b', fontSize: 12, textAlign: 'center' }}>
<VideoCameraOutlined />
<PlayCircleOutlined style={{ fontSize: 18 }} />
<div></div>
<div style={{ fontSize: 10, color: '#94a3b8', marginTop: 2 }}></div>
</div>
</>
) : null}
{!isImage && !isVideo ? (
<div style={{ color: '#64748b', fontSize: 12, textAlign: 'center' }}></div>
<div style={{ color: '#64748b', fontSize: 12, textAlign: 'center' }}>
<div style={{ fontSize: 10, color: '#94a3b8', marginTop: 2 }}></div>
</div>
) : null}
{state === 'checking' && isImage ? (
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -11,7 +11,7 @@ export interface User {
export interface CreditRecord {
id: string;
type: 'consume' | 'recharge';
type: 'consume' | 'recharge' | 'refund';
amount: number;
description: string;
createdAt: string;
@@ -208,9 +208,10 @@ export interface AdminGenerationRecord {
duration?: number;
aspectRatio?: string;
resolution?: string;
status: 'prompt_optimized' | 'generating' | 'completed' | 'failed';
status: 'optimizing' | 'prompt_optimized' | 'generating' | 'completed' | 'failed' | string;
videoUrl?: string;
references?: { url: string; type: string; name: string }[];
videoCoverUrl?: string;
references?: GenerationAIMediaReference[] | null;
creditsCost: number;
textCreditsCost: number;
textTokensUsed: number;
@@ -218,12 +219,12 @@ export interface AdminGenerationRecord {
errorMessage?: string;
createdAt: string;
generatedAt?: string;
genType?: string,
imageSize?: string,
imageUrl?: string,
imageTokensUsed?: number,
imageProportion?: string,
imagePx?: string,
genType?: string;
imageSize?: string;
imageUrl?: string;
imageTokensUsed?: number;
imageProportion?: string;
imagePx?: string;
}
export type GenerationAITaskStatus = 'pending' | 'generating' | 'completed' | 'failed' | string;
+1 -1
View File
@@ -44,7 +44,7 @@ CORS_ORIGINS=["*"]
# RESOURCE
RESOURCE_SIGN_SECRET=EOTpDZsEgkaYWPxgtIedOO0lDlH1moTS2rnSIemjzmO3
RESOURCE_SIGN_EXPIRE_SECONDS=30
RESOURCE_SIGN_EXPIRE_SECONDS=86400
RESOURCE_SIGN_ARG_EXPIRE=exp
RESOURCE_SIGN_ARG_SIGNATURE=sign
+4 -2
View File
@@ -36,7 +36,7 @@ async def send_sms_code(req: SmsSendRequest):
_validate_captcha_token(req.captcha_token)
try:
ok = await generate_and_send_sms(req.phone, req.scene.value)
ok, code = await generate_and_send_sms(req.phone, req.scene.value)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -48,7 +48,9 @@ async def send_sms_code(req: SmsSendRequest):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="短信发送失败,请稍后重试",
)
return SmsResponse(message="验证码已发送", success=True)
message = f"验证码已发送:{code}" if settings.SMS_MOCK else "验证码已发送"
return SmsResponse(message=message, success=True)
@router.post(
+3 -3
View File
@@ -4,7 +4,7 @@ import logging
import random
import time
from datetime import datetime
from typing import Any
from typing import Any, Tuple
from app.config import settings
from app.utils.redis import get_redis
@@ -156,14 +156,14 @@ async def store_sms_code(phone: str, code: str, scene: str = "login", ttl: int |
_sms_code_store[key] = (code, time.time() + ttl_seconds)
async def generate_and_send_sms(phone: str, scene: str = "login") -> bool:
async def generate_and_send_sms(phone: str, scene: str = "login") -> Tuple[bool, str]:
scene = _normalize_scene(scene)
await _check_send_limit(phone, scene)
code = _generate_code()
ok = await send_sms(phone, code, scene)
if ok:
await store_sms_code(phone, code, scene)
return ok
return ok, code
async def verify_sms_code(phone: str, code: str, scene: str = "login") -> bool: