Files
ViGent/frontend/src/app/publish/page.tsx
2026-01-14 17:49:23 +08:00

339 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
// 动态获取 API 地址:服务端使用 localhost客户端使用当前域名
const API_BASE = typeof window !== 'undefined'
? `http://${window.location.hostname}:8006`
: 'http://localhost:8006';
interface Account {
platform: string;
name: string;
logged_in: boolean;
enabled: boolean;
}
interface Video {
name: string;
path: string;
}
export default function PublishPage() {
const [accounts, setAccounts] = useState<Account[]>([]);
const [videos, setVideos] = useState<Video[]>([]);
const [selectedVideo, setSelectedVideo] = useState<string>("");
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<string>("");
const [isPublishing, setIsPublishing] = useState(false);
const [publishResults, setPublishResults] = useState<any[]>([]);
// 加载账号和视频列表
useEffect(() => {
fetchAccounts();
fetchVideos();
}, []);
const fetchAccounts = async () => {
try {
const res = await fetch(`${API_BASE}/api/publish/accounts`);
const data = await res.json();
setAccounts(data.accounts || []);
} catch (error) {
console.error("获取账号失败:", error);
}
};
const fetchVideos = async () => {
try {
// 获取已生成的视频列表 (从 outputs 目录)
const res = await fetch(`${API_BASE}/api/videos/tasks`);
const data = await res.json();
const completedVideos = data.tasks
?.filter((t: any) => t.status === "completed")
.map((t: any) => ({
name: `${t.task_id}_output.mp4`,
path: `outputs/${t.task_id}_output.mp4`,
})) || [];
setVideos(completedVideos);
if (completedVideos.length > 0) {
setSelectedVideo(completedVideos[0].path);
}
} catch (error) {
console.error("获取视频失败:", error);
}
};
const togglePlatform = (platform: string) => {
if (selectedPlatforms.includes(platform)) {
setSelectedPlatforms(selectedPlatforms.filter((p) => p !== platform));
} else {
setSelectedPlatforms([...selectedPlatforms, platform]);
}
};
const handlePublish = async () => {
if (!selectedVideo || !title || selectedPlatforms.length === 0) {
alert("请选择视频、填写标题并选择至少一个平台");
return;
}
setIsPublishing(true);
setPublishResults([]);
const tagList = tags.split(/[,\s]+/).filter((t) => t.trim());
for (const platform of selectedPlatforms) {
try {
const res = await fetch(`${API_BASE}/api/publish/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
video_path: selectedVideo,
platform,
title,
tags: tagList,
description: "",
}),
});
const result = await res.json();
setPublishResults((prev) => [...prev, result]);
} catch (error) {
setPublishResults((prev) => [
...prev,
{ platform, success: false, message: String(error) },
]);
}
}
setIsPublishing(false);
};
const handleLogin = async (platform: string) => {
alert(
`登录功能需要在服务端执行。\n\n请在终端运行:\ncurl -X POST http://localhost:8006/api/publish/login/${platform}`
);
};
const platformIcons: Record<string, string> = {
douyin: "🎵",
xiaohongshu: "📕",
weixin: "💬",
kuaishou: "⚡",
bilibili: "📺",
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
{/* Header */}
<header className="border-b border-white/10 bg-black/20 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<Link href="/" className="text-2xl font-bold text-white flex items-center gap-3 hover:opacity-80">
<span className="text-3xl">🎬</span>
TalkingHead Agent
</Link>
<nav className="flex gap-4">
<Link
href="/"
className="px-4 py-2 text-gray-400 hover:text-white transition-colors"
>
</Link>
<Link
href="/publish"
className="px-4 py-2 text-white bg-purple-600 rounded-lg"
>
</Link>
</nav>
</div>
</header>
<main className="max-w-6xl mx-auto px-6 py-8">
<h1 className="text-3xl font-bold text-white mb-8">📤 </h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 左侧: 账号管理 */}
<div className="space-y-6">
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
👤
</h2>
<div className="space-y-3">
{accounts.map((account) => (
<div
key={account.platform}
className="flex items-center justify-between p-4 bg-black/30 rounded-xl"
>
<div className="flex items-center gap-3">
<span className="text-2xl">
{platformIcons[account.platform]}
</span>
<div>
<div className="text-white font-medium">
{account.name}
</div>
<div
className={`text-sm ${account.logged_in
? "text-green-400"
: "text-gray-500"
}`}
>
{account.logged_in ? "✓ 已登录" : "未登录"}
</div>
</div>
</div>
<button
onClick={() => handleLogin(account.platform)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${account.logged_in
? "bg-gray-600 text-gray-300"
: "bg-purple-600 hover:bg-purple-700 text-white"
}`}
>
{account.logged_in ? "重新登录" : "登录"}
</button>
</div>
))}
</div>
</div>
</div>
{/* 右侧: 发布表单 */}
<div className="space-y-6">
{/* 选择视频 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4">
🎥
</h2>
{videos.length === 0 ? (
<p className="text-gray-400">
<Link href="/" className="text-purple-400 hover:underline">
</Link>
</p>
) : (
<select
value={selectedVideo}
onChange={(e) => setSelectedVideo(e.target.value)}
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white"
>
{videos.map((v) => (
<option key={v.path} value={v.path}>
{v.name}
</option>
))}
</select>
)}
</div>
{/* 填写信息 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4"> </h2>
<div className="space-y-4">
<div>
<label className="block text-gray-400 text-sm mb-2">
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入视频标题..."
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500"
/>
</div>
<div>
<label className="block text-gray-400 text-sm mb-2">
()
</label>
<input
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="AI, 数字人, 口播..."
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500"
/>
</div>
</div>
</div>
{/* 选择平台 */}
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white mb-4">📱 </h2>
<div className="grid grid-cols-3 gap-3">
{accounts
.filter((a) => a.logged_in)
.map((account) => (
<button
key={account.platform}
onClick={() => togglePlatform(account.platform)}
className={`p-3 rounded-xl border-2 transition-all ${selectedPlatforms.includes(account.platform)
? "border-purple-500 bg-purple-500/20"
: "border-white/10 bg-white/5 hover:border-white/30"
}`}
>
<span className="text-2xl block mb-1">
{platformIcons[account.platform]}
</span>
<span className="text-white text-sm">{account.name}</span>
</button>
))}
</div>
{accounts.filter((a) => a.logged_in).length === 0 && (
<p className="text-gray-400 text-center py-4">
</p>
)}
</div>
{/* 发布按钮 */}
<button
onClick={handlePublish}
disabled={isPublishing || selectedPlatforms.length === 0}
className={`w-full py-4 rounded-xl font-bold text-lg transition-all ${isPublishing || selectedPlatforms.length === 0
? "bg-gray-600 cursor-not-allowed text-gray-400"
: "bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-700 hover:to-teal-700 text-white"
}`}
>
{isPublishing ? "发布中..." : "🚀 一键发布"}
</button>
{/* 发布结果 */}
{publishResults.length > 0 && (
<div className="bg-white/5 rounded-2xl p-6 border border-white/10">
<h2 className="text-lg font-semibold text-white mb-4">
</h2>
<div className="space-y-2">
{publishResults.map((result, i) => (
<div
key={i}
className={`p-3 rounded-lg ${result.success ? "bg-green-500/20" : "bg-red-500/20"
}`}
>
<span className="text-white">
{platformIcons[result.platform]} {result.message}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</main>
</div>
);
}