管理后台生成记录创作记录修复操作显示BUG
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>
|
||||
<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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user