339 lines
16 KiB
TypeScript
339 lines
16 KiB
TypeScript
"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>
|
||
);
|
||
}
|