Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -30,7 +30,7 @@ const AdminContactRequests: React.FC = () => {
|
||||
const { user } = useAdminStore();
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!user?.isAdmin) return;
|
||||
// if (!user?.isAdmin) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const query = new URLSearchParams();
|
||||
@@ -39,7 +39,7 @@ const AdminContactRequests: React.FC = () => {
|
||||
if (isHandledFilter !== null) {
|
||||
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);
|
||||
setTotal(res.total);
|
||||
} catch (err: any) {
|
||||
@@ -55,7 +55,7 @@ const AdminContactRequests: React.FC = () => {
|
||||
|
||||
const handleMarkHandled = async (id: string) => {
|
||||
try {
|
||||
await api.put(`/contact/requests/${id}/handle`);
|
||||
await api.put(`/admin/contact-requests/${id}/handle`);
|
||||
message.success('已标记为处理');
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
@@ -65,7 +65,7 @@ const AdminContactRequests: React.FC = () => {
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await api.delete(`/contact/requests/${id}`);
|
||||
await api.delete(`/admin/contact-requests/${id}`);
|
||||
message.success('已删除');
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -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.menu_config import router as menu_config_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.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(menu_config_router)
|
||||
router.include_router(admin_upload_router)
|
||||
router.include_router(admin_contact_router)
|
||||
|
||||
@@ -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": "删除成功"}
|
||||
@@ -1,14 +1,14 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.contact_request import ContactRequest
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/contact", tags=["contact"])
|
||||
@@ -62,94 +62,4 @@ async def create_contact_request(
|
||||
detail="每个账号每天只能提交一次联系我们"
|
||||
)
|
||||
|
||||
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": "删除成功"}
|
||||
return {"message": "提交成功,我们会尽快与您联系"}
|
||||
@@ -2,7 +2,6 @@ import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
@@ -112,44 +111,27 @@ async def optimize_prompt(
|
||||
await db.commit()
|
||||
|
||||
if configs:
|
||||
total_weight = sum(c.weight for c in configs)
|
||||
r = random.uniform(0, total_weight)
|
||||
cumulative = 0
|
||||
selected = configs[0]
|
||||
for c in configs:
|
||||
cumulative += c.weight
|
||||
if r <= cumulative:
|
||||
selected = c
|
||||
break
|
||||
# 按 priority 从大到小依次尝试,跳过 mock,失败则用下一个
|
||||
for selected in configs:
|
||||
if selected.provider == "mock":
|
||||
continue
|
||||
if selected.provider in ("openai_compatible", "sdk"):
|
||||
try:
|
||||
return await _call_openai_compatible(
|
||||
selected, original_prompt, db, user_id, industry_key, duration,
|
||||
references=references,
|
||||
gen_type=gen_type,
|
||||
image_size=image_size,
|
||||
image_proportion=image_proportion,
|
||||
image_px=image_px,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if selected.provider == "mock":
|
||||
# 所有真实模型都失败,降级到 mock
|
||||
mock_cfg = next((c for c in configs if c.provider == "mock"), None)
|
||||
if mock_cfg:
|
||||
return _mock_optimize(original_prompt, gen_type)
|
||||
elif selected.provider in ("openai_compatible", "sdk"):
|
||||
try:
|
||||
return await _call_openai_compatible(
|
||||
selected, original_prompt, db, user_id, industry_key, duration,
|
||||
references=references,
|
||||
gen_type=gen_type,
|
||||
image_size=image_size,
|
||||
image_proportion=image_proportion,
|
||||
image_px=image_px,
|
||||
)
|
||||
except Exception:
|
||||
for c in configs:
|
||||
if c.id == selected.id or c.provider == "mock":
|
||||
continue
|
||||
try:
|
||||
return await _call_openai_compatible(
|
||||
c, original_prompt, db, user_id, industry_key, duration,
|
||||
references=references,
|
||||
gen_type=gen_type,
|
||||
image_size=image_size,
|
||||
image_proportion=image_proportion,
|
||||
image_px=image_px,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
raise
|
||||
|
||||
if settings.LLM_MOCK:
|
||||
return _mock_optimize(original_prompt, gen_type)
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+64
-8
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -28,8 +28,8 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DYI2idb2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKeRPhR_.css">
|
||||
<script type="module" crossorigin src="/assets/index-C9TClkR9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
</head>
|
||||
<body>
|
||||
<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 NotificationPopup from '../NotificationPopup';
|
||||
import './AppLayout.css';
|
||||
import bg1 from '../../assets/bg1.png';
|
||||
|
||||
|
||||
// ── ResourceCapacity 类型(与 src/types/index.ts 保持一致) ──
|
||||
type ResourceCapacityData = {
|
||||
@@ -153,9 +155,9 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
|
||||
{data.enabled ? (
|
||||
<></>
|
||||
) : (
|
||||
|
||||
|
||||
<span style={{ fontWeight: 500, color: '#1e293b' }}>
|
||||
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1', marginRight: 4 }} />
|
||||
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#ef4444' : '#6366f1', marginRight: 4 }} />
|
||||
|
||||
当前使用 {usedAuto.val} {usedAuto.unit}
|
||||
</span>
|
||||
@@ -163,7 +165,7 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
|
||||
</div>
|
||||
{data.enabled && (
|
||||
<>
|
||||
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
@@ -183,10 +185,10 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
|
||||
fontSize: 12,
|
||||
color: isOver ? '#000000ff' : '#000000ff',
|
||||
padding: '0 8px',
|
||||
|
||||
|
||||
}}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#000000ff' :'#000000ff' }} />
|
||||
<DatabaseOutlined style={{ fontSize: 12, color: isOver ? '#000000ff' : '#000000ff' }} />
|
||||
{rawPercent.toFixed(1)}%
|
||||
</span>
|
||||
{data.enabled ? (
|
||||
@@ -206,7 +208,7 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
|
||||
: `剩余 ${available.toFixed(2)} ${unit}`}
|
||||
</span> */}
|
||||
{/* <span>{rawPercent.toFixed(1)}%</span> */}
|
||||
|
||||
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -216,16 +218,16 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: `${barPercent}%`,
|
||||
height: '100%',
|
||||
|
||||
|
||||
background: rawPercent < 50
|
||||
? 'linear-gradient(90deg, #279951, #b7fad0)'
|
||||
: rawPercent < 85
|
||||
@@ -829,7 +831,10 @@ const AppLayout: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Layout style={{
|
||||
minHeight: '100vh',
|
||||
|
||||
}}>
|
||||
<div className="desktop-sidebar" style={{
|
||||
width: sidebarW, position: 'fixed', left: 16, top: 16, bottom: 16, zIndex: 100,
|
||||
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
|
||||
@@ -953,7 +958,7 @@ const AppLayout: React.FC = () => {
|
||||
animation: 'borderShimmer 2.5s linear infinite',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
|
||||
<Avatar size={36} icon={<UserOutlined />}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
@@ -979,7 +984,7 @@ const AppLayout: React.FC = () => {
|
||||
animation: 'creditGlow 1.5s ease-in-out infinite',
|
||||
}}>积分: {user?.credits || 0}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="user-card-arrow" style={{
|
||||
fontSize: 12,
|
||||
color: '#6366f1',
|
||||
@@ -989,7 +994,7 @@ const AppLayout: React.FC = () => {
|
||||
}}>
|
||||
<MenuOutlined />
|
||||
</div>
|
||||
|
||||
|
||||
<style>{`
|
||||
@keyframes arrowFlash {
|
||||
0%, 100% {
|
||||
@@ -1063,7 +1068,7 @@ const AppLayout: React.FC = () => {
|
||||
<div style={{
|
||||
boxSizing: 'border-box',
|
||||
height: '100%',
|
||||
background: '#ffffffff',
|
||||
background: '#f1f2f3',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
|
||||
minHeight: '100%',
|
||||
@@ -1307,7 +1312,7 @@ const AppLayout: React.FC = () => {
|
||||
footer={null}>
|
||||
<Tabs defaultActiveKey="profile">
|
||||
<Tabs.TabPane tab="个人信息" key="profile">
|
||||
<Form form={usernameForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => {}}>
|
||||
<Form form={usernameForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => { }}>
|
||||
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }, { min: 3, message: '用户名至少3位' }]}>
|
||||
<Input placeholder="请输入用户名" size="large" prefix={<UserOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
|
||||
</Form.Item>
|
||||
@@ -1317,7 +1322,7 @@ const AppLayout: React.FC = () => {
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="修改密码" key="password">
|
||||
<Form form={pwdForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => {}}>
|
||||
<Form form={pwdForm} layout="vertical" style={{ marginTop: 20 }} onValuesChange={() => { }}>
|
||||
<Form.Item name="oldPwd" label="原密码" rules={[{ required: true, message: '请输入原密码' }]}>
|
||||
<Input.Password placeholder="请输入原密码" size="large" prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -65,8 +65,9 @@ const PROGRESS_RATE = 0.6;
|
||||
const calculateProgressValue = (createdAt?: string): number => {
|
||||
if (!createdAt) return 0;
|
||||
const createdTime = new Date(createdAt).getTime();
|
||||
if (isNaN(createdTime)) return 0;
|
||||
const now = Date.now();
|
||||
const elapsedSeconds = (now - createdTime) / 1000;
|
||||
const elapsedSeconds = Math.max(0, (now - createdTime) / 1000);
|
||||
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
||||
return 99;
|
||||
}
|
||||
@@ -189,6 +190,71 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
const [fullProgressItems, setFullProgressItems] = useState<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) => {
|
||||
if (progress >= 100) {
|
||||
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}
|
||||
</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 }} />}
|
||||
<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)} />
|
||||
|
||||
@@ -1079,7 +1079,7 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
|
||||
console.log(newMessage);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2023,9 +2023,11 @@ const AIChatPage: React.FC = () => {
|
||||
margin: '-24px -32px -32px',
|
||||
borderRadius: 22,
|
||||
height: 'calc(100vh - 34px)',
|
||||
background: '#fff',
|
||||
background: 'rgba(255, 255, 255, 0.52)',
|
||||
|
||||
|
||||
overflow: 'hidden',
|
||||
|
||||
}}>
|
||||
{/* 隐藏的音频播放器 */}
|
||||
<audio
|
||||
@@ -2035,95 +2037,18 @@ const AIChatPage: React.FC = () => {
|
||||
onEnded={() => setPlayingAudioUrl(null)}
|
||||
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={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
@@ -2133,6 +2058,7 @@ const AIChatPage: React.FC = () => {
|
||||
border: '1px solid rgba(231, 234, 240, 0.82)',
|
||||
// boxShadow: '0 16px 44px rgba(31, 41, 55, 0.06)',
|
||||
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
|
||||
|
||||
}}>
|
||||
<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 }} />
|
||||
@@ -2166,6 +2092,7 @@ const AIChatPage: React.FC = () => {
|
||||
overflow: 'hidden',
|
||||
minHeight: 0,
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
{/* 空状态 - 没有对话或当前对话没有消息时显示 */}
|
||||
@@ -2205,8 +2132,9 @@ const AIChatPage: React.FC = () => {
|
||||
paddingRight: 10,
|
||||
paddingBottom: 18,
|
||||
// background: 'linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0))',
|
||||
background: '#fff',
|
||||
// background: '#fff',
|
||||
// borderRadius: 22,
|
||||
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
const target = e.currentTarget;
|
||||
@@ -2262,7 +2190,7 @@ const AIChatPage: React.FC = () => {
|
||||
{/* 消息气泡 */}
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.92)',
|
||||
background: 'rgb(255, 255, 255)',
|
||||
backdropFilter: 'blur(18px)',
|
||||
borderRadius: '22px',
|
||||
padding: '14px 16px',
|
||||
@@ -2824,7 +2752,8 @@ const AIChatPage: React.FC = () => {
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: '#ffffff',
|
||||
background: 'rgba(255, 255, 255, 0.52)',
|
||||
|
||||
borderRadius: 24,
|
||||
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',
|
||||
|
||||
@@ -2153,13 +2153,16 @@ const GeneratePage: React.FC = () => {
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{references
|
||||
.filter(
|
||||
{(() => {
|
||||
const filtered = references.filter(
|
||||
(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
|
||||
key={i}
|
||||
key={originalIndex}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -2187,7 +2190,8 @@ const GeneratePage: React.FC = () => {
|
||||
if (!textarea || !textarea.value) return;
|
||||
const val = textarea.value;
|
||||
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);
|
||||
form.setFieldValue("prompt", newVal);
|
||||
setShowMention(false);
|
||||
@@ -2228,13 +2232,15 @@ const GeneratePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text style={{ fontSize: 13 }}>
|
||||
{ref.name}
|
||||
{ref.name || (ref.type === "image" ? `图片${i + 1}` : `视频${i + 1}`)}
|
||||
</Typography.Text>
|
||||
<Tag style={{ marginLeft: "auto", fontSize: 11 }}>
|
||||
{ref.type === "image" ? "图片" : "视频"}
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2787,7 +2793,7 @@ const GeneratePage: React.FC = () => {
|
||||
{recordStates[currentRecord.id] === "generating" && (
|
||||
<span>
|
||||
{mediaType === "image" ? "图片" : "视频"}生成中,请稍候...
|
||||
<span
|
||||
{/* <span
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
@@ -2797,7 +2803,7 @@ const GeneratePage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
{Math.round(generationDisplayProgress[currentRecord.id] || 0)}%
|
||||
</span>
|
||||
</span> */}
|
||||
</span>
|
||||
)}
|
||||
{recordStates[currentRecord.id] === "done" &&
|
||||
|
||||
@@ -342,8 +342,8 @@ const HomePage: React.FC = () => {
|
||||
const aiEntries = [
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: 24 }} />,
|
||||
title: '行业提示词优化',
|
||||
description: '根据项目行业进行图文理解优化提示词',
|
||||
title: '行业智造',
|
||||
description: '新建项目、设置图文视频参数、核对信息并生成素材',
|
||||
action: '立即创作',
|
||||
path: '/projects',
|
||||
},
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, getone, getReplicationDetail, deleteHotOpeningReplicationTask } from '../api';
|
||||
import bg1 from '../assets/bg1.png';
|
||||
|
||||
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
@@ -450,7 +452,6 @@ const GenerateConver: React.FC = () => {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'stretch',
|
||||
gap: '2%',
|
||||
// background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
|
||||
position: 'relative',
|
||||
// padding: '20px 32px',
|
||||
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,10 +478,11 @@ const GenerateConver: React.FC = () => {
|
||||
border: '1px solid rgba(99, 102, 241, 0.08)',
|
||||
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 ,
|
||||
paddingBottom: 12,
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 16,
|
||||
paddingBottom: 12,
|
||||
|
||||
}}>
|
||||
}}>
|
||||
{/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} /> */}
|
||||
<div>
|
||||
<h2 style={{
|
||||
@@ -514,7 +521,13 @@ const GenerateConver: React.FC = () => {
|
||||
</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 ? (
|
||||
<div style={{ height: '100%', padding: '24px', }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
@@ -526,7 +539,6 @@ const GenerateConver: React.FC = () => {
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.9)',
|
||||
border: '1px solid rgba(99, 102, 241, 0.6)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: 16,
|
||||
@@ -546,7 +558,10 @@ const GenerateConver: React.FC = () => {
|
||||
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 ? (
|
||||
|
||||
|
||||
@@ -830,9 +845,20 @@ const GenerateConver: React.FC = () => {
|
||||
</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 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a1a2e;
|
||||
background-image: url(/backimage.png);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -24,6 +21,12 @@
|
||||
object-fit: cover;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
/* 视频加载前占位 — 纯色背景,不显示默认图 */
|
||||
.login-bg-placeholder {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.login-bg-overlay {
|
||||
|
||||
@@ -48,6 +48,7 @@ const LoginPage: React.FC = () => {
|
||||
const [siteName, setSiteName] = useState(initialInfo.siteName);
|
||||
const [siteLogo, setSiteLogo] = useState(initialInfo.siteLogo);
|
||||
const [loginBgVideo, setLoginBgVideo] = useState('');
|
||||
const [mediaReady, setMediaReady] = useState(false);
|
||||
const [agreementPrivacyUrl, setAgreementPrivacyUrl] = useState('');
|
||||
const [siteCopyright, setSiteCopyright] = useState('');
|
||||
|
||||
@@ -288,15 +289,12 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
{/* 背景视频/动图全屏铺满 */}
|
||||
{loginBgVideo && (loginBgVideo.toLowerCase().endsWith('.gif') || loginBgVideo.toLowerCase().endsWith('.webp')) ? (
|
||||
<img className="login-bg-video" src={loginBgVideo} alt="" />
|
||||
) : 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'} />
|
||||
</video>
|
||||
) : null}
|
||||
<div className="login-bg-overlay" />
|
||||
{/* 背景视频/动图全屏铺满 — 预加载完成后再显示页面 */}
|
||||
<BackgroundVideo src={loginBgVideo} onReady={() => setMediaReady(true)} />
|
||||
<div className="login-bg-overlay" style={{ opacity: mediaReady ? 1 : 0 }} />
|
||||
|
||||
{/* 内容区 — 媒体加载完成后淡入 */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', opacity: mediaReady ? 1 : 0, transition: 'opacity 0.5s ease', pointerEvents: mediaReady ? 'auto' : 'none' }}>
|
||||
|
||||
{/* 左上角 slogan */}
|
||||
<div className="login-slogan">
|
||||
@@ -364,6 +362,7 @@ const LoginPage: React.FC = () => {
|
||||
disabled={!loginSliderVerified}
|
||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||
<Button disabled={countdown > 0}
|
||||
style={{fontSize: 14,fontWeight: 400}}
|
||||
onClick={() => {
|
||||
if (loginShowResend) {
|
||||
setLoginSliderVerified(false);
|
||||
@@ -411,6 +410,7 @@ const LoginPage: React.FC = () => {
|
||||
disabled={!sliderVerified}
|
||||
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
|
||||
<Button disabled={regCountdown > 0}
|
||||
style={{fontSize: 14,fontWeight: 400}}
|
||||
onClick={() => {
|
||||
if (showResend) {
|
||||
setSliderVerified(false);
|
||||
@@ -502,10 +502,55 @@ const LoginPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>{/* end media-ready wrapper */}
|
||||
</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<{
|
||||
onSuccess: () => void;
|
||||
isVerified: boolean;
|
||||
|
||||
@@ -121,7 +121,7 @@ const ProjectsPage: React.FC = () => {
|
||||
backgroundClip: 'text',
|
||||
// textAlign: 'center',
|
||||
}}>
|
||||
依托平台行业专属提示词优化
|
||||
垂直行业提示词智能优化(新手专属,一键生成标准素材)
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
|
||||
共 {projects.length} 个项目 · 按行业分类管理
|
||||
|
||||
@@ -179,13 +179,14 @@ export default function VideoFrameExtractor() {
|
||||
borderRadius: 20,
|
||||
minHeight: 'calc(100vh - 34px)',
|
||||
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',
|
||||
padding: '20px 32px',
|
||||
backgroundImage: `url(${bg1})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%',
|
||||
backgroundPosition: 'center',
|
||||
// backgroundImage: `url(${bg1})`,
|
||||
// backgroundRepeat: 'no-repeat',
|
||||
// backgroundSize: '100% 100%',
|
||||
// backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user