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>
|
||||||
<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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 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": "删除成功"}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
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>
|
||||||
<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)} />
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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" &&
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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} 个项目 · 按行业分类管理
|
||||||
|
|||||||
@@ -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',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user