This commit is contained in:
2026-07-20 13:48:27 +08:00
20 changed files with 403 additions and 296 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-CEW5ggCs.js"></script> <script type="module" crossorigin src="/assets/index-CzqFKsZY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
@@ -30,7 +30,7 @@ const AdminContactRequests: React.FC = () => {
const { user } = useAdminStore(); const { user } = useAdminStore();
const fetchData = async () => { const fetchData = async () => {
if (!user?.isAdmin) return; // if (!user?.isAdmin) return;
setLoading(true); setLoading(true);
try { try {
const query = new URLSearchParams(); const query = new URLSearchParams();
@@ -39,7 +39,7 @@ const AdminContactRequests: React.FC = () => {
if (isHandledFilter !== null) { if (isHandledFilter !== null) {
query.set('is_handled', String(isHandledFilter)); query.set('is_handled', String(isHandledFilter));
} }
const res = await api.get<{ items: ContactRequest[]; total: number }>(`/contact/requests?${query.toString()}`); const res = await api.get<{ items: ContactRequest[]; total: number }>(`/admin/contact-requests?${query.toString()}`);
setData(res.items); setData(res.items);
setTotal(res.total); setTotal(res.total);
} catch (err: any) { } catch (err: any) {
@@ -55,7 +55,7 @@ const AdminContactRequests: React.FC = () => {
const handleMarkHandled = async (id: string) => { const handleMarkHandled = async (id: string) => {
try { try {
await api.put(`/contact/requests/${id}/handle`); await api.put(`/admin/contact-requests/${id}/handle`);
message.success('已标记为处理'); message.success('已标记为处理');
fetchData(); fetchData();
} catch (err: any) { } catch (err: any) {
@@ -65,7 +65,7 @@ const AdminContactRequests: React.FC = () => {
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
try { try {
await api.delete(`/contact/requests/${id}`); await api.delete(`/admin/contact-requests/${id}`);
message.success('已删除'); message.success('已删除');
fetchData(); fetchData();
} catch (err: any) { } catch (err: any) {
+2
View File
@@ -9,6 +9,7 @@ from app.api.admin.private_portrait import router as private_portrait_router
from app.api.admin.recharge_package import router as recharge_package_router from app.api.admin.recharge_package import router as recharge_package_router
from app.api.admin.menu_config import router as menu_config_router from app.api.admin.menu_config import router as menu_config_router
from app.api.admin.upload import router as admin_upload_router from app.api.admin.upload import router as admin_upload_router
from app.api.admin.contact import router as admin_contact_router
router = APIRouter() router = APIRouter()
router.include_router(video_prompt_schema_config_router) router.include_router(video_prompt_schema_config_router)
@@ -20,3 +21,4 @@ router.include_router(private_portrait_router)
router.include_router(recharge_package_router) router.include_router(recharge_package_router)
router.include_router(menu_config_router) router.include_router(menu_config_router)
router.include_router(admin_upload_router) router.include_router(admin_upload_router)
router.include_router(admin_contact_router)
+75
View File
@@ -0,0 +1,75 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_admin_user, get_db
from app.models.contact_request import ContactRequest
from app.models.user import User
from app.schemas.contact import ContactRequestListOut, ContactRequestOut
router = APIRouter(prefix="/admin/contact-requests", tags=["admin-contact-requests"])
@router.get("", response_model=ContactRequestListOut)
async def list_contact_requests(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=200),
is_handled: bool | None = Query(None),
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
query = select(ContactRequest)
count_query = select(func.count(ContactRequest.id))
if is_handled is not None:
query = query.where(ContactRequest.is_handled == is_handled)
count_query = count_query.where(ContactRequest.is_handled == is_handled)
query = query.order_by(ContactRequest.created_at.desc())
offset = (page - 1) * page_size
result = await db.execute(query.offset(offset).limit(page_size))
items = result.scalars().all()
total = (await db.execute(count_query)).scalar_one()
return {"items": items, "total": total}
@router.get("/{request_id}", response_model=ContactRequestOut)
async def get_contact_request(
request_id: str,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="记录不存在")
return item
@router.put("/{request_id}/handle")
async def mark_contact_handled(
request_id: str,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="记录不存在")
item.is_handled = True
await db.commit()
await db.refresh(item)
return {"message": "已标记为处理"}
@router.delete("/{request_id}")
async def delete_contact_request(
request_id: str,
admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="记录不存在")
await db.delete(item)
await db.commit()
return {"message": "删除成功"}
+2 -92
View File
@@ -1,14 +1,14 @@
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func, select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, get_current_user from app.dependencies import get_db, get_current_user
from app.models.contact_request import ContactRequest from app.models.contact_request import ContactRequest
from app.models.user import User from app.models.user import User
from app.schemas.contact import ContactRequestCreate, ContactRequestListOut, ContactRequestOut from app.schemas.contact import ContactRequestCreate
from app.utils.id_gen import generate_id from app.utils.id_gen import generate_id
router = APIRouter(prefix="/contact", tags=["contact"]) router = APIRouter(prefix="/contact", tags=["contact"])
@@ -63,93 +63,3 @@ async def create_contact_request(
) )
return {"message": "提交成功,我们会尽快与您联系"} return {"message": "提交成功,我们会尽快与您联系"}
@router.get("/requests", summary="获取联系请求列表", response_model=ContactRequestListOut)
async def get_contact_requests(
page: int = 1,
page_size: int = 20,
is_handled: bool | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
query = select(ContactRequest)
count_query = select(func.count(ContactRequest.id))
if is_handled is not None:
query = query.where(ContactRequest.is_handled == is_handled)
count_query = count_query.where(ContactRequest.is_handled == is_handled)
query = query.order_by(ContactRequest.created_at.desc())
offset = (page - 1) * page_size
result = await db.execute(query.offset(offset).limit(page_size))
items = result.scalars().all()
total = (await db.execute(count_query)).scalar_one()
return {"items": items, "total": total}
@router.get("/requests/{request_id}", summary="获取联系请求详情", response_model=ContactRequestOut)
async def get_contact_request(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
return contact_request
@router.put("/requests/{request_id}/handle", summary="标记为已处理")
async def mark_as_handled(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
contact_request.is_handled = True
await db.commit()
await db.refresh(contact_request)
return {"message": "已标记为处理"}
@router.delete("/requests/{request_id}", summary="删除联系请求")
async def delete_contact_request(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
await db.delete(contact_request)
await db.commit()
return {"message": "删除成功"}
+9 -27
View File
@@ -2,7 +2,6 @@ import base64
import json import json
import mimetypes import mimetypes
import os import os
import random
from datetime import datetime from datetime import datetime
import httpx import httpx
@@ -112,19 +111,11 @@ async def optimize_prompt(
await db.commit() await db.commit()
if configs: if configs:
total_weight = sum(c.weight for c in configs) # 按 priority 从大到小依次尝试,跳过 mock,失败则用下一个
r = random.uniform(0, total_weight) for selected in configs:
cumulative = 0
selected = configs[0]
for c in configs:
cumulative += c.weight
if r <= cumulative:
selected = c
break
if selected.provider == "mock": if selected.provider == "mock":
return _mock_optimize(original_prompt, gen_type) continue
elif selected.provider in ("openai_compatible", "sdk"): if selected.provider in ("openai_compatible", "sdk"):
try: try:
return await _call_openai_compatible( return await _call_openai_compatible(
selected, original_prompt, db, user_id, industry_key, duration, selected, original_prompt, db, user_id, industry_key, duration,
@@ -135,21 +126,12 @@ async def optimize_prompt(
image_px=image_px, image_px=image_px,
) )
except Exception: except Exception:
for c in configs:
if c.id == selected.id or c.provider == "mock":
continue continue
try:
return await _call_openai_compatible( # 所有真实模型都失败,降级到 mock
c, original_prompt, db, user_id, industry_key, duration, mock_cfg = next((c for c in configs if c.provider == "mock"), None)
references=references, if mock_cfg:
gen_type=gen_type, return _mock_optimize(original_prompt, gen_type)
image_size=image_size,
image_proportion=image_proportion,
image_px=image_px,
)
except Exception:
continue
raise
if settings.LLM_MOCK: if settings.LLM_MOCK:
return _mock_optimize(original_prompt, gen_type) return _mock_optimize(original_prompt, gen_type)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -28,8 +28,8 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-DYI2idb2.js"></script> <script type="module" crossorigin src="/assets/index-C9TClkR9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CKeRPhR_.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -66,6 +66,8 @@ import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api'; import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import NotificationPopup from '../NotificationPopup'; import NotificationPopup from '../NotificationPopup';
import './AppLayout.css'; import './AppLayout.css';
import bg1 from '../../assets/bg1.png';
// ── ResourceCapacity 类型(与 src/types/index.ts 保持一致) ── // ── ResourceCapacity 类型(与 src/types/index.ts 保持一致) ──
type ResourceCapacityData = { type ResourceCapacityData = {
@@ -829,7 +831,10 @@ const AppLayout: React.FC = () => {
}; };
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{
minHeight: '100vh',
}}>
<div className="desktop-sidebar" style={{ <div className="desktop-sidebar" style={{
width: sidebarW, position: 'fixed', left: 16, top: 16, bottom: 16, zIndex: 100, width: sidebarW, position: 'fixed', left: 16, top: 16, bottom: 16, zIndex: 100,
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)', background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
@@ -1063,7 +1068,7 @@ const AppLayout: React.FC = () => {
<div style={{ <div style={{
boxSizing: 'border-box', boxSizing: 'border-box',
height: '100%', height: '100%',
background: '#ffffffff', background: '#f1f2f3',
borderRadius: '20px', borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)', boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
minHeight: '100%', minHeight: '100%',
@@ -65,8 +65,9 @@ const PROGRESS_RATE = 0.6;
const calculateProgressValue = (createdAt?: string): number => { const calculateProgressValue = (createdAt?: string): number => {
if (!createdAt) return 0; if (!createdAt) return 0;
const createdTime = new Date(createdAt).getTime(); const createdTime = new Date(createdAt).getTime();
if (isNaN(createdTime)) return 0;
const now = Date.now(); const now = Date.now();
const elapsedSeconds = (now - createdTime) / 1000; const elapsedSeconds = Math.max(0, (now - createdTime) / 1000);
if (elapsedSeconds >= MAX_DURATION_SECONDS) { if (elapsedSeconds >= MAX_DURATION_SECONDS) {
return 99; return 99;
} }
@@ -189,6 +190,71 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
const [fullProgressItems, setFullProgressItems] = useState<Set<string>>(new Set()); const [fullProgressItems, setFullProgressItems] = useState<Set<string>>(new Set());
const processedItems = useRef<Set<string>>(new Set()); const processedItems = useRef<Set<string>>(new Set());
useEffect(() => {
const styleId = 'gen-task-resource-grid-animations';
if (document.getElementById(styleId)) return;
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.gen-task-loading-bg {
position: relative;
overflow: hidden;
background: linear-gradient(135deg, #faf8ff 0%, #f5f3ff 100%);
}
.gen-task-loading-bg::before,
.gen-task-loading-bg::after,
.gen-task-loading-bg .gen-task-aurora-3 {
content: '';
position: absolute;
border-radius: 50%;
filter: blur(30px);
pointer-events: none;
z-index: 0;
}
.gen-task-loading-bg::before {
width: 220px;
height: 220px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.45) 0%, rgba(168, 85, 247, 0.25) 50%, transparent 100%);
animation: genTaskAurora1 6s linear infinite;
}
.gen-task-loading-bg::after {
width: 240px;
height: 240px;
background: radial-gradient(circle, rgba(168, 85, 247, 0.35) 0%, rgba(192, 132, 252, 0.2) 50%, transparent 100%);
animation: genTaskAurora2 7.5s linear infinite;
}
.gen-task-loading-bg .gen-task-aurora-3 {
width: 180px;
height: 180px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.15) 50%, transparent 100%);
animation: genTaskAurora3 5s linear infinite;
}
.gen-task-loading-bg > * { position: relative; z-index: 1; }
@keyframes genTaskAurora1 {
0% { transform: translate(-100px, -80px); }
25% { transform: translate(100px, -60px); }
50% { transform: translate(120px, 80px); }
75% { transform: translate(-60px, 100px); }
100% { transform: translate(-100px, -80px); }
}
@keyframes genTaskAurora2 {
0% { transform: translate(120px, 100px); }
25% { transform: translate(-80px, 120px); }
50% { transform: translate(-100px, -60px); }
75% { transform: translate(80px, -80px); }
100% { transform: translate(120px, 100px); }
}
@keyframes genTaskAurora3 {
0% { transform: translate(60px, -40px); }
25% { transform: translate(-40px, 60px); }
50% { transform: translate(40px, 100px); }
75% { transform: translate(100px, -20px); }
100% { transform: translate(60px, -40px); }
}
`;
document.head.appendChild(style);
}, []);
const handleProgressChange = (itemId: string, progress: number) => { const handleProgressChange = (itemId: string, progress: number) => {
if (progress >= 100) { if (progress >= 100) {
setFullProgressItems(prev => { setFullProgressItems(prev => {
@@ -271,7 +337,8 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
{isVideo ? <PlayCircleFilled style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', fontSize: items.length > 2 ? 28 : 46, color: 'rgba(255,255,255,.92)', filter: 'drop-shadow(0 4px 10px rgba(0,0,0,.28))' }} /> : null} {isVideo ? <PlayCircleFilled style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', fontSize: items.length > 2 ? 28 : 46, color: 'rgba(255,255,255,.92)', filter: 'drop-shadow(0 4px 10px rgba(0,0,0,.28))' }} /> : null}
</button> </button>
) : ( ) : (
<div style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 7, padding: 8, textAlign: 'center' }}> <div className={pending || isFinishing ? 'gen-task-loading-bg' : ''} style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 7, padding: 8, textAlign: 'center' }}>
{pending || isFinishing ? <span className="gen-task-aurora-3" /> : null}
{pending || isFinishing ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />} {pending || isFinishing ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />}
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span> <span style={{ fontSize: items.length > 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span>
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} onProgressChange={(progress) => handleProgressChange(itemId, progress)} /> <ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} onProgressChange={(progress) => handleProgressChange(itemId, progress)} />
+19 -90
View File
@@ -2023,9 +2023,11 @@ const AIChatPage: React.FC = () => {
margin: '-24px -32px -32px', margin: '-24px -32px -32px',
borderRadius: 22, borderRadius: 22,
height: 'calc(100vh - 34px)', height: 'calc(100vh - 34px)',
background: '#fff', background: 'rgba(255, 255, 255, 0.52)',
overflow: 'hidden', overflow: 'hidden',
}}> }}>
{/* 隐藏的音频播放器 */} {/* 隐藏的音频播放器 */}
<audio <audio
@@ -2035,95 +2037,18 @@ const AIChatPage: React.FC = () => {
onEnded={() => setPlayingAudioUrl(null)} onEnded={() => setPlayingAudioUrl(null)}
style={{ display: 'none' }} style={{ display: 'none' }}
/> />
{/* 左侧边栏 - 对话列表(已隐藏,保留代码) */}
{false && (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={220}
style={{
background: 'rgba(255,255,255,0.7)',
backdropFilter: 'blur(16px)',
borderRight: '1px solid rgba(139, 92, 246, 0.08)',
}}
>
<div style={{ padding: 12 }}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{ marginBottom: 12, width: '100%' }}
/>
{!collapsed && (
<Button
block
type="primary"
icon={<PlusOutlined />}
onClick={handleNewChat}
style={{ marginBottom: 16, borderRadius: 8 }}
>
</Button>
)}
{!collapsed && conversations.length > 0 && (
<div style={{ maxHeight: 'calc(100vh - 120px)', overflowY: 'auto' }}>
{conversations.map((conversation) => (
<div
key={conversation.id}
onClick={() => handleSelectChat(conversation.id)}
style={{
display: 'flex',
alignItems: 'center',
padding: '10px 12px',
marginBottom: 4,
borderRadius: 10,
cursor: 'pointer',
background: currentConversationId === conversation.id ? '#FAFBFC' : 'transparent',
border: currentConversationId === conversation.id ? '1px solid #ede9fe' : 'none',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = currentConversationId === conversation.id ? '#FAFBFC' : '#FFFFFF';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = currentConversationId === conversation.id ? '#FAFBFC' : 'transparent';
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#2f3440', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{conversation.title}
</p>
<p style={{ margin: 2, fontSize: 11, color: '#98a2b3', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{conversation.lastMessage || '暂无消息'}
</p>
</div>
<Popconfirm
title="确定删除此对话?"
onConfirm={() => handleDeleteChat(conversation.id)}
okText="确定"
cancelText="取消"
>
<Button
type="text"
icon={<DeleteOutlined />}
style={{ color: '#98a2b3', padding: 4 }}
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</div>
))}
</div>
)}
</div>
</Sider>
)}
{/* 主内容区 */} {/* 主内容区 */}
<Layout style={{ display: 'flex', flex: 1, background: '#fff' }}> <Layout style={{
display: 'flex', flex: 1,
background: 'rgba(255, 255, 255, 0.52)',
// backgroundImage: `url(${bg1})`,
// backgroundRepeat: 'no-repeat',
// backgroundSize: '100% 100%',
// backgroundPosition: 'center',
}}>
{/* 头部 - 显示对话标题和模型信息 */} {/* 头部 - 显示对话标题和模型信息 */}
<div className="animate-fadeInUp" style={{ <div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
@@ -2133,6 +2058,7 @@ const AIChatPage: React.FC = () => {
border: '1px solid rgba(231, 234, 240, 0.82)', border: '1px solid rgba(231, 234, 240, 0.82)',
// boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)', // boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12, position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}> }}>
<div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 16 }}> <div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: '15%', height: 1, background: 'linear-gradient(90deg, transparent, rgba(117,106,136,0.18), rgba(232,227,236,0.48), transparent)', borderRadius: 1 }} /> <div style={{ width: '15%', height: 1, background: 'linear-gradient(90deg, transparent, rgba(117,106,136,0.18), rgba(232,227,236,0.48), transparent)', borderRadius: 1 }} />
@@ -2166,6 +2092,7 @@ const AIChatPage: React.FC = () => {
overflow: 'hidden', overflow: 'hidden',
minHeight: 0, minHeight: 0,
}} }}
> >
{/* 空状态 - 没有对话或当前对话没有消息时显示 */} {/* 空状态 - 没有对话或当前对话没有消息时显示 */}
@@ -2205,8 +2132,9 @@ const AIChatPage: React.FC = () => {
paddingRight: 10, paddingRight: 10,
paddingBottom: 18, paddingBottom: 18,
// background: 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0))', // background: 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0))',
background: '#fff', // background: '#fff',
// borderRadius: 22, // borderRadius: 22,
}} }}
onScroll={(e) => { onScroll={(e) => {
const target = e.currentTarget; const target = e.currentTarget;
@@ -2262,7 +2190,7 @@ const AIChatPage: React.FC = () => {
{/* 消息气泡 */} {/* 消息气泡 */}
<div <div
style={{ style={{
background: 'rgba(255, 255, 255, 0.92)', background: 'rgb(255, 255, 255)',
backdropFilter: 'blur(18px)', backdropFilter: 'blur(18px)',
borderRadius: '22px', borderRadius: '22px',
padding: '14px 16px', padding: '14px 16px',
@@ -2824,7 +2752,8 @@ const AIChatPage: React.FC = () => {
<div <div
style={{ style={{
position: 'relative', position: 'relative',
background: '#ffffff', background: 'rgba(255, 255, 255, 0.52)',
borderRadius: 24, borderRadius: 24,
padding: '18px 72px 16px 20px', padding: '18px 72px 16px 20px',
boxShadow: '0 22px 64px rgba(31, 41, 55, 0.08), 0 1px 0 rgba(255,255,255,0.98) inset', boxShadow: '0 22px 64px rgba(31, 41, 55, 0.08), 0 1px 0 rgba(255,255,255,0.98) inset',
+16 -10
View File
@@ -2153,13 +2153,16 @@ const GeneratePage: React.FC = () => {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{references {(() => {
.filter( const filtered = references.filter(
(r) => !mentionFilter || r.name.includes(mentionFilter), (r) => !mentionFilter || r.name.includes(mentionFilter),
) );
.map((ref, i) => ( const displayList = filtered.length > 0 ? filtered : references;
return displayList.map((ref, originalIndex) => {
const i = references.indexOf(ref);
return (
<div <div
key={i} key={originalIndex}
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -2187,7 +2190,8 @@ const GeneratePage: React.FC = () => {
if (!textarea || !textarea.value) return; if (!textarea || !textarea.value) return;
const val = textarea.value; const val = textarea.value;
const lastAt = val.lastIndexOf("@"); const lastAt = val.lastIndexOf("@");
const newVal = val.slice(0, lastAt) + `@${ref.name} `; const displayName = ref.name || (ref.type === "image" ? `图片${i + 1}` : `视频${i + 1}`);
const newVal = val.slice(0, lastAt) + `@${displayName} `;
setPromptText(newVal); setPromptText(newVal);
form.setFieldValue("prompt", newVal); form.setFieldValue("prompt", newVal);
setShowMention(false); setShowMention(false);
@@ -2228,13 +2232,15 @@ const GeneratePage: React.FC = () => {
)} )}
</div> </div>
<Typography.Text style={{ fontSize: 13 }}> <Typography.Text style={{ fontSize: 13 }}>
{ref.name} {ref.name || (ref.type === "image" ? `图片${i + 1}` : `视频${i + 1}`)}
</Typography.Text> </Typography.Text>
<Tag style={{ marginLeft: "auto", fontSize: 11 }}> <Tag style={{ marginLeft: "auto", fontSize: 11 }}>
{ref.type === "image" ? "图片" : "视频"} {ref.type === "image" ? "图片" : "视频"}
</Tag> </Tag>
</div> </div>
))} );
});
})()}
</div> </div>
)} )}
@@ -2787,7 +2793,7 @@ const GeneratePage: React.FC = () => {
{recordStates[currentRecord.id] === "generating" && ( {recordStates[currentRecord.id] === "generating" && (
<span> <span>
{mediaType === "image" ? "图片" : "视频"}... {mediaType === "image" ? "图片" : "视频"}...
<span {/* <span
style={{ style={{
marginLeft: 8, marginLeft: 8,
fontSize: 14, fontSize: 14,
@@ -2797,7 +2803,7 @@ const GeneratePage: React.FC = () => {
}} }}
> >
{Math.round(generationDisplayProgress[currentRecord.id] || 0)}% {Math.round(generationDisplayProgress[currentRecord.id] || 0)}%
</span> </span> */}
</span> </span>
)} )}
{recordStates[currentRecord.id] === "done" && {recordStates[currentRecord.id] === "done" &&
+2 -2
View File
@@ -342,8 +342,8 @@ const HomePage: React.FC = () => {
const aiEntries = [ const aiEntries = [
{ {
icon: <FileTextOutlined style={{ fontSize: 24 }} />, icon: <FileTextOutlined style={{ fontSize: 24 }} />,
title: '行业提示词优化', title: '行业智造',
description: '根据项目行业进行图文理解优化提示词', description: '新建项目、设置图文视频参数、核对信息并生成素材',
action: '立即创作', action: '立即创作',
path: '/projects', path: '/projects',
}, },
+34 -8
View File
@@ -20,6 +20,8 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail, deleteHotOpeningReplicationTask } from '../api'; import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail, deleteHotOpeningReplicationTask } from '../api';
import bg1 from '../assets/bg1.png';
const { Header, Content } = Layout; const { Header, Content } = Layout;
@@ -450,7 +452,6 @@ const GenerateConver: React.FC = () => {
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'stretch', alignItems: 'stretch',
gap: '2%', gap: '2%',
// background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
position: 'relative', position: 'relative',
// padding: '20px 32px', // padding: '20px 32px',
boxSizing: 'border-box', boxSizing: 'border-box',
@@ -458,7 +459,12 @@ const GenerateConver: React.FC = () => {
}} }}
> >
<div className="replication-preview" style={{ width: '70%', background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(20px)', display: 'flex', flexDirection: 'column', borderRadius: 20, overflow: 'hidden', border: '1px solid rgba(99, 102, 241, 0.1)', boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)', position: 'relative', zIndex: 10 }}> <div className="replication-preview" style={{ width: '70%', background: '#f1f2f3', backdropFilter: 'blur(20px)', display: 'flex', flexDirection: 'column', borderRadius: 20, overflow: 'hidden',
// border: '1px solid rgba(99, 102, 241, 0.1)',
// boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
position: 'relative', zIndex: 10,
}}>
@@ -472,7 +478,8 @@ const GenerateConver: React.FC = () => {
border: '1px solid rgba(99, 102, 241, 0.08)', border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12, position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 , <div style={{
display: 'flex', alignItems: 'center', gap: 16,
paddingBottom: 12, paddingBottom: 12,
}}> }}>
@@ -514,7 +521,13 @@ const GenerateConver: React.FC = () => {
</div> </div>
{/* 左侧预览区域 */} {/* 左侧预览区域 */}
<div style={{ flex: 1, background: 'rgba(248, 250, 252, 0.5)', borderRight: '1px solid rgba(99, 102, 241, 0.08)', overflowY: 'auto' }}> <div style={{
flex: 1,
background: 'transparent',
borderRight: '1px solid rgba(99, 102, 241, 0.08)', overflowY: 'auto',
scrollbarWidth: 'none',
}}>
{cardData.length > 0 ? ( {cardData.length > 0 ? (
<div style={{ height: '100%', padding: '24px', }}> <div style={{ height: '100%', padding: '24px', }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
@@ -526,7 +539,6 @@ const GenerateConver: React.FC = () => {
<div <div
key={item.id} key={item.id}
style={{ style={{
background: 'rgba(255,255,255,0.9)',
border: '1px solid rgba(99, 102, 241, 0.6)', border: '1px solid rgba(99, 102, 241, 0.6)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
borderRadius: 16, borderRadius: 16,
@@ -546,7 +558,10 @@ const GenerateConver: React.FC = () => {
e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.06)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.06)';
}} }}
> >
<div style={{ position: 'relative', aspectRatio: '1/1', background: '#f8fafc' }}> <div style={{
position: 'relative', aspectRatio: '1/1',
// background: '#f8fafc'
}}>
{item.finalVideoUrl ? ( {item.finalVideoUrl ? (
@@ -830,9 +845,20 @@ const GenerateConver: React.FC = () => {
</div> </div>
<div className="replication-form" style={{ flex: 1, overflow: 'auto', width: '28%', background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(20px)', borderRadius: 20, border: '1px solid rgba(99, 102, 241, 0.1)', boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)', position: 'relative', zIndex: 10 }}> <div className="replication-form" style={{
flex: 1, overflow: 'auto', width: '28%',
background: 'transparent',
backdropFilter: 'blur(20px)', borderRadius: 20,
// border: '1px solid rgba(99, 102, 241, 0.1)',
// boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
position: 'relative', zIndex: 10
}}>
{/* 右侧表单区域 */} {/* 右侧表单区域 */}
<div className="replication-form-content" style={{ width: '100%', height: '100%', background: 'rgba(248, 250, 252, 0.5)', padding: 24, overflowY: 'auto' }}> <div className="replication-form-content" style={{
width: '100%', height: '100%',
background: 'rgba(258, 250, 252, 0.2)',
padding: 24, overflowY: 'auto'
}}>
{/* 上传视频 */} {/* 上传视频 */}
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}> <p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
+6 -3
View File
@@ -8,9 +8,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #1a1a2e; background: #1a1a2e;
background-image: url(/backimage.png);
background-size: cover;
background-position: center;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -24,6 +21,12 @@
object-fit: cover; object-fit: cover;
z-index: 0; z-index: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.6s ease;
}
/* 视频加载前占位 — 纯色背景,不显示默认图 */
.login-bg-placeholder {
background: #1a1a2e;
} }
.login-bg-overlay { .login-bg-overlay {
+54 -9
View File
@@ -48,6 +48,7 @@ const LoginPage: React.FC = () => {
const [siteName, setSiteName] = useState(initialInfo.siteName); const [siteName, setSiteName] = useState(initialInfo.siteName);
const [siteLogo, setSiteLogo] = useState(initialInfo.siteLogo); const [siteLogo, setSiteLogo] = useState(initialInfo.siteLogo);
const [loginBgVideo, setLoginBgVideo] = useState(''); const [loginBgVideo, setLoginBgVideo] = useState('');
const [mediaReady, setMediaReady] = useState(false);
const [agreementPrivacyUrl, setAgreementPrivacyUrl] = useState(''); const [agreementPrivacyUrl, setAgreementPrivacyUrl] = useState('');
const [siteCopyright, setSiteCopyright] = useState(''); const [siteCopyright, setSiteCopyright] = useState('');
@@ -288,15 +289,12 @@ const LoginPage: React.FC = () => {
return ( return (
<div className="login-page"> <div className="login-page">
{/* 背景视频/动图全屏铺满 */} {/* 背景视频/动图全屏铺满 — 预加载完成后再显示页面 */}
{loginBgVideo && (loginBgVideo.toLowerCase().endsWith('.gif') || loginBgVideo.toLowerCase().endsWith('.webp')) ? ( <BackgroundVideo src={loginBgVideo} onReady={() => setMediaReady(true)} />
<img className="login-bg-video" src={loginBgVideo} alt="" /> <div className="login-bg-overlay" style={{ opacity: mediaReady ? 1 : 0 }} />
) : loginBgVideo ? (
<video className="login-bg-video" autoPlay loop muted playsInline preload="auto"> {/* 内容区 — 媒体加载完成后淡入 */}
<source src={loginBgVideo} type={loginBgVideo.endsWith('.webm') ? 'video/webm' : loginBgVideo.endsWith('.mov') ? 'video/quicktime' : 'video/mp4'} /> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', opacity: mediaReady ? 1 : 0, transition: 'opacity 0.5s ease', pointerEvents: mediaReady ? 'auto' : 'none' }}>
</video>
) : null}
<div className="login-bg-overlay" />
{/* 左上角 slogan */} {/* 左上角 slogan */}
<div className="login-slogan"> <div className="login-slogan">
@@ -364,6 +362,7 @@ const LoginPage: React.FC = () => {
disabled={!loginSliderVerified} disabled={!loginSliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} /> style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={countdown > 0} <Button disabled={countdown > 0}
style={{fontSize: 14,fontWeight: 400}}
onClick={() => { onClick={() => {
if (loginShowResend) { if (loginShowResend) {
setLoginSliderVerified(false); setLoginSliderVerified(false);
@@ -411,6 +410,7 @@ const LoginPage: React.FC = () => {
disabled={!sliderVerified} disabled={!sliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} /> style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={regCountdown > 0} <Button disabled={regCountdown > 0}
style={{fontSize: 14,fontWeight: 400}}
onClick={() => { onClick={() => {
if (showResend) { if (showResend) {
setSliderVerified(false); setSliderVerified(false);
@@ -502,10 +502,55 @@ const LoginPage: React.FC = () => {
</div> </div>
</div> </div>
)} )}
</div>{/* end media-ready wrapper */}
</div> </div>
); );
}; };
const BackgroundVideo: React.FC<{ src: string; onReady: () => void }> = ({ src, onReady }) => {
const [ready, setReady] = useState(false);
const vidRef = React.useRef<HTMLVideoElement>(null);
const handleReady = () => {
if (ready) return;
setReady(true);
vidRef.current?.play().catch(() => {});
onReady();
};
if (!src) { onReady(); return null; }
const isGif = src.toLowerCase().endsWith('.gif');
const isWebp = src.toLowerCase().endsWith('.webp');
if (isGif || isWebp) {
return <img className="login-bg-video" src={src} alt="" onLoad={handleReady} onError={handleReady} />;
}
return (
<>
{/* 视频加载前显示占位色,不显示默认背景图 */}
{!ready && <div className="login-bg-video login-bg-placeholder" />}
<video
ref={vidRef}
className="login-bg-video"
style={{ opacity: ready ? 1 : 0 }}
autoPlay
loop
muted
playsInline
preload="auto"
onCanPlayThrough={handleReady}
onCanPlay={handleReady}
onError={handleReady}
>
<source src={src} type={src.endsWith('.webm') ? 'video/webm' : src.endsWith('.mov') ? 'video/quicktime' : 'video/mp4'} />
</video>
</>
);
};
const SliderVerify: React.FC<{ const SliderVerify: React.FC<{
onSuccess: () => void; onSuccess: () => void;
isVerified: boolean; isVerified: boolean;
+1 -1
View File
@@ -121,7 +121,7 @@ const ProjectsPage: React.FC = () => {
backgroundClip: 'text', backgroundClip: 'text',
// textAlign: 'center', // textAlign: 'center',
}}> }}>
</h2> </h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}> <p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
{projects.length} · {projects.length} ·
+6 -5
View File
@@ -179,13 +179,14 @@ export default function VideoFrameExtractor() {
borderRadius: 20, borderRadius: 20,
minHeight: 'calc(100vh - 34px)', minHeight: 'calc(100vh - 34px)',
overflow: 'auto', overflow: 'auto',
background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)', background: 'transparent',
// background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
position: 'relative', position: 'relative',
padding: '20px 32px', padding: '20px 32px',
backgroundImage: `url(${bg1})`, // backgroundImage: `url(${bg1})`,
backgroundRepeat: 'no-repeat', // backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%', // backgroundSize: '100% 100%',
backgroundPosition: 'center', // backgroundPosition: 'center',
}} }}
> >