界面优化
This commit is contained in:
@@ -1,19 +1,33 @@
|
|||||||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import re
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filename(filename: str) -> str:
|
||||||
|
"""清理文件名,移除不安全字符"""
|
||||||
|
# 移除路径分隔符和特殊字符
|
||||||
|
safe_name = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
||||||
|
# 限制长度
|
||||||
|
if len(safe_name) > 100:
|
||||||
|
ext = Path(safe_name).suffix
|
||||||
|
safe_name = safe_name[:100 - len(ext)] + ext
|
||||||
|
return safe_name
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def upload_material(file: UploadFile = File(...)):
|
async def upload_material(file: UploadFile = File(...)):
|
||||||
if not file.filename.lower().endswith(('.mp4', '.mov', '.avi')):
|
if not file.filename.lower().endswith(('.mp4', '.mov', '.avi')):
|
||||||
raise HTTPException(400, "Invalid format")
|
raise HTTPException(400, "Invalid format")
|
||||||
|
|
||||||
file_id = str(uuid.uuid4())
|
# 使用时间戳+原始文件名(保留原始名称,避免冲突)
|
||||||
ext = Path(file.filename).suffix
|
timestamp = int(time.time())
|
||||||
save_path = settings.UPLOAD_DIR / "materials" / f"{file_id}{ext}"
|
safe_name = sanitize_filename(file.filename)
|
||||||
|
save_path = settings.UPLOAD_DIR / "materials" / f"{timestamp}_{safe_name}"
|
||||||
|
|
||||||
# Save file
|
# Save file
|
||||||
with open(save_path, "wb") as buffer:
|
with open(save_path, "wb") as buffer:
|
||||||
@@ -21,11 +35,14 @@ async def upload_material(file: UploadFile = File(...)):
|
|||||||
|
|
||||||
# Calculate size
|
# Calculate size
|
||||||
size_mb = save_path.stat().st_size / (1024 * 1024)
|
size_mb = save_path.stat().st_size / (1024 * 1024)
|
||||||
|
|
||||||
|
# 提取显示名称(去掉时间戳前缀)
|
||||||
|
display_name = safe_name
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": file_id,
|
"id": save_path.stem,
|
||||||
"name": file.filename,
|
"name": display_name,
|
||||||
"path": f"uploads/materials/{file_id}{ext}",
|
"path": f"uploads/materials/{save_path.name}",
|
||||||
"size_mb": size_mb,
|
"size_mb": size_mb,
|
||||||
"type": "video"
|
"type": "video"
|
||||||
}
|
}
|
||||||
@@ -38,9 +55,16 @@ async def list_materials():
|
|||||||
for f in materials_dir.glob("*"):
|
for f in materials_dir.glob("*"):
|
||||||
try:
|
try:
|
||||||
stat = f.stat()
|
stat = f.stat()
|
||||||
|
# 提取显示名称:去掉时间戳前缀 (格式: {timestamp}_{原始文件名})
|
||||||
|
display_name = f.name
|
||||||
|
if '_' in f.name:
|
||||||
|
parts = f.name.split('_', 1)
|
||||||
|
if parts[0].isdigit():
|
||||||
|
display_name = parts[1] # 原始文件名
|
||||||
|
|
||||||
files.append({
|
files.append({
|
||||||
"id": f.stem,
|
"id": f.stem,
|
||||||
"name": f.name,
|
"name": display_name,
|
||||||
"path": f"uploads/materials/{f.name}",
|
"path": f"uploads/materials/{f.name}",
|
||||||
"size_mb": stat.st_size / (1024 * 1024),
|
"size_mb": stat.st_size / (1024 * 1024),
|
||||||
"type": "video",
|
"type": "video",
|
||||||
@@ -51,3 +75,26 @@ async def list_materials():
|
|||||||
# Sort by creation time desc
|
# Sort by creation time desc
|
||||||
files.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
files.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
||||||
return {"materials": files}
|
return {"materials": files}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{material_id}")
|
||||||
|
async def delete_material(material_id: str):
|
||||||
|
"""删除素材文件"""
|
||||||
|
materials_dir = settings.UPLOAD_DIR / "materials"
|
||||||
|
|
||||||
|
# 查找匹配的文件(ID 是文件名不含扩展名)
|
||||||
|
found = None
|
||||||
|
for f in materials_dir.glob("*"):
|
||||||
|
if f.stem == material_id:
|
||||||
|
found = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise HTTPException(404, "Material not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
found.unlink()
|
||||||
|
return {"success": True, "message": "素材已删除"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"删除失败: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
@@ -141,3 +141,58 @@ async def lipsync_health():
|
|||||||
"""获取 LipSync 服务健康状态"""
|
"""获取 LipSync 服务健康状态"""
|
||||||
lipsync = _get_lipsync_service()
|
lipsync = _get_lipsync_service()
|
||||||
return await lipsync.check_health()
|
return await lipsync.check_health()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/generated")
|
||||||
|
async def list_generated_videos():
|
||||||
|
"""从文件系统读取生成的视频列表(持久化)"""
|
||||||
|
output_dir = settings.OUTPUT_DIR
|
||||||
|
videos = []
|
||||||
|
|
||||||
|
if output_dir.exists():
|
||||||
|
for f in output_dir.glob("*_output.mp4"):
|
||||||
|
try:
|
||||||
|
stat = f.stat()
|
||||||
|
videos.append({
|
||||||
|
"id": f.stem,
|
||||||
|
"name": f.name,
|
||||||
|
"path": f"/outputs/{f.name}",
|
||||||
|
"size_mb": stat.st_size / (1024 * 1024),
|
||||||
|
"created_at": stat.st_ctime
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by creation time desc (newest first)
|
||||||
|
videos.sort(key=lambda x: x.get("created_at", 0), reverse=True)
|
||||||
|
return {"videos": videos}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/generated/{video_id}")
|
||||||
|
async def delete_generated_video(video_id: str):
|
||||||
|
"""删除生成的视频"""
|
||||||
|
output_dir = settings.OUTPUT_DIR
|
||||||
|
|
||||||
|
# 查找匹配的文件
|
||||||
|
found = None
|
||||||
|
for f in output_dir.glob("*.mp4"):
|
||||||
|
if f.stem == video_id:
|
||||||
|
found = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise HTTPException(404, "Video not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
found.unlink()
|
||||||
|
# 同时删除相关的临时文件(如果存在)
|
||||||
|
task_id = video_id.replace("_output", "")
|
||||||
|
for suffix in ["_audio.mp3", "_lipsync.mp4"]:
|
||||||
|
temp_file = output_dir / f"{task_id}{suffix}"
|
||||||
|
if temp_file.exists():
|
||||||
|
temp_file.unlink()
|
||||||
|
|
||||||
|
return {"success": True, "message": "视频已删除"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"删除失败: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,63 @@ body {
|
|||||||
|
|
||||||
/* 隐藏滚动条但保留滚动功能 */
|
/* 隐藏滚动条但保留滚动功能 */
|
||||||
html {
|
html {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
-ms-overflow-style: none; /* IE 和 Edge */
|
/* Firefox */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE 和 Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
html::-webkit-scrollbar {
|
html::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari, Opera */
|
display: none;
|
||||||
|
/* Chrome, Safari, Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义滚动条样式 - 深色主题 */
|
||||||
|
.custom-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(147, 51, 234, 0.5) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(147, 51, 234, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(147, 51, 234, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 完全隐藏滚动条 */
|
||||||
|
.hide-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义 select 下拉菜单 */
|
||||||
|
.custom-select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%239ca3af' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 12px center;
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select option {
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
@@ -26,6 +26,14 @@ interface Task {
|
|||||||
download_url?: string;
|
download_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GeneratedVideo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size_mb: number;
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [materials, setMaterials] = useState<Material[]>([]);
|
const [materials, setMaterials] = useState<Material[]>([]);
|
||||||
const [selectedMaterial, setSelectedMaterial] = useState<string>("");
|
const [selectedMaterial, setSelectedMaterial] = useState<string>("");
|
||||||
@@ -41,6 +49,8 @@ export default function Home() {
|
|||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([]);
|
||||||
|
const [selectedVideoId, setSelectedVideoId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 可选音色
|
// 可选音色
|
||||||
const voices = [
|
const voices = [
|
||||||
@@ -51,9 +61,10 @@ export default function Home() {
|
|||||||
{ id: "zh-CN-XiaoyiNeural", name: "晓伊 (女声-温柔)" },
|
{ id: "zh-CN-XiaoyiNeural", name: "晓伊 (女声-温柔)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 加载素材列表
|
// 加载素材列表和历史视频
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMaterials();
|
fetchMaterials();
|
||||||
|
fetchGeneratedVideos();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchMaterials = async () => {
|
const fetchMaterials = async () => {
|
||||||
@@ -87,6 +98,60 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取已生成的视频列表(持久化)
|
||||||
|
const fetchGeneratedVideos = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/videos/generated`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setGeneratedVideos(data.videos || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取历史视频失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除素材
|
||||||
|
const deleteMaterial = async (materialId: string) => {
|
||||||
|
if (!confirm("确定要删除这个素材吗?")) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/materials/${materialId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
fetchMaterials();
|
||||||
|
if (selectedMaterial === materialId) {
|
||||||
|
setSelectedMaterial("");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("删除失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("删除失败: " + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除生成的视频
|
||||||
|
const deleteVideo = async (videoId: string) => {
|
||||||
|
if (!confirm("确定要删除这个视频吗?")) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/api/videos/generated/${videoId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
fetchGeneratedVideos();
|
||||||
|
if (selectedVideoId === videoId) {
|
||||||
|
setSelectedVideoId(null);
|
||||||
|
setGeneratedVideo(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("删除失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("删除失败: " + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 上传视频
|
// 上传视频
|
||||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -181,6 +246,7 @@ export default function Home() {
|
|||||||
if (taskData.status === "completed") {
|
if (taskData.status === "completed") {
|
||||||
setGeneratedVideo(`${API_BASE}${taskData.download_url}`);
|
setGeneratedVideo(`${API_BASE}${taskData.download_url}`);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
|
fetchGeneratedVideos(); // 刷新历史视频列表
|
||||||
} else if (taskData.status === "failed") {
|
} else if (taskData.status === "failed") {
|
||||||
alert("视频生成失败: " + taskData.message);
|
alert("视频生成失败: " + taskData.message);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
@@ -320,21 +386,35 @@ export default function Home() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{materials.map((m) => (
|
{materials.map((m) => (
|
||||||
<button
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
onClick={() => setSelectedMaterial(m.id)}
|
className={`p-4 rounded-xl border-2 transition-all text-left relative group ${selectedMaterial === m.id
|
||||||
className={`p-4 rounded-xl border-2 transition-all text-left ${selectedMaterial === m.id
|
|
||||||
? "border-purple-500 bg-purple-500/20"
|
? "border-purple-500 bg-purple-500/20"
|
||||||
: "border-white/10 bg-white/5 hover:border-white/30"
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-white font-medium truncate">
|
<button
|
||||||
{m.scene || m.name}
|
onClick={() => setSelectedMaterial(m.id)}
|
||||||
</div>
|
className="w-full text-left"
|
||||||
<div className="text-gray-400 text-sm mt-1">
|
>
|
||||||
{m.size_mb.toFixed(1)} MB
|
<div className="text-white font-medium truncate pr-6">
|
||||||
</div>
|
{m.scene || m.name}
|
||||||
</button>
|
</div>
|
||||||
|
<div className="text-gray-400 text-sm mt-1">
|
||||||
|
{m.size_mb.toFixed(1)} MB
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteMaterial(m.id);
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 p-1 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="删除素材"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -471,16 +551,66 @@ export default function Home() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 历史视频列表 */}
|
||||||
|
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||||
|
📂 历史视频
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={fetchGeneratedVideos}
|
||||||
|
className="px-3 py-1 text-xs bg-white/10 hover:bg-white/20 rounded text-gray-300"
|
||||||
|
>
|
||||||
|
🔄 刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{generatedVideos.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-gray-500">
|
||||||
|
<p>暂无生成的视频</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-64 overflow-y-auto hide-scrollbar">
|
||||||
|
{generatedVideos.map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedVideoId === v.id
|
||||||
|
? "border-purple-500 bg-purple-500/20"
|
||||||
|
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedVideoId(v.id);
|
||||||
|
setGeneratedVideo(`${API_BASE}${v.path}`);
|
||||||
|
}}
|
||||||
|
className="flex-1 text-left"
|
||||||
|
>
|
||||||
|
<div className="text-white text-sm truncate">
|
||||||
|
{new Date(v.created_at * 1000).toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-xs">
|
||||||
|
{v.size_mb.toFixed(1)} MB
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteVideo(v.id);
|
||||||
|
}}
|
||||||
|
className="p-1 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="删除视频"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="border-t border-white/10 mt-12">
|
|
||||||
<div className="max-w-6xl mx-auto px-6 py-4 text-center text-gray-500 text-sm">
|
|
||||||
ViGent - 基于 MuseTalk + EdgeTTS
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,20 +52,18 @@ export default function PublishPage() {
|
|||||||
|
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async () => {
|
||||||
try {
|
try {
|
||||||
// 获取已生成的视频列表 (从 outputs 目录)
|
// 使用持久化的视频列表 API(从文件系统读取)
|
||||||
const res = await fetch(`${API_BASE}/api/videos/tasks`);
|
const res = await fetch(`${API_BASE}/api/videos/generated`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
const completedVideos = data.tasks
|
const videos = (data.videos || []).map((v: any) => ({
|
||||||
?.filter((t: any) => t.status === "completed")
|
name: new Date(v.created_at * 1000).toLocaleString('zh-CN') + ` (${v.size_mb.toFixed(1)}MB)`,
|
||||||
.map((t: any) => ({
|
path: v.path.startsWith('/') ? v.path.slice(1) : v.path, // 移除开头的 /
|
||||||
name: `${t.task_id}_output.mp4`,
|
}));
|
||||||
path: `outputs/${t.task_id}_output.mp4`,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
setVideos(completedVideos);
|
setVideos(videos);
|
||||||
if (completedVideos.length > 0) {
|
if (videos.length > 0) {
|
||||||
setSelectedVideo(completedVideos[0].path);
|
setSelectedVideo(videos[0].path);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取视频失败:", error);
|
console.error("获取视频失败:", error);
|
||||||
@@ -281,7 +279,7 @@ export default function PublishPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedVideo}
|
value={selectedVideo}
|
||||||
onChange={(e) => setSelectedVideo(e.target.value)}
|
onChange={(e) => setSelectedVideo(e.target.value)}
|
||||||
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white"
|
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white custom-select cursor-pointer hover:border-purple-500/50 transition-colors"
|
||||||
>
|
>
|
||||||
{videos.map((v) => (
|
{videos.map((v) => (
|
||||||
<option key={v.path} value={v.path}>
|
<option key={v.path} value={v.path}>
|
||||||
|
|||||||
Reference in New Issue
Block a user