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