644 lines
24 KiB
TypeScript
644 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { Settings, ExternalLink, Briefcase, Images, Sparkles, Trophy, Building2, Crown, Medal, Award, Edit, LayoutGrid, PlayCircle, Wand2, X, BarChart3, Clapperboard } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { useTheme } from "next-themes";
|
||
import { useState, useEffect, useRef } from "react";
|
||
import { createPortal } from "react-dom";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { ImageAnnotator } from "@/components/image-annotator";
|
||
import { VideoPlayer } from "@/components/video-player";
|
||
import { toast } from "sonner";
|
||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||
|
||
type RankingItem = {
|
||
rank: number;
|
||
userId: string;
|
||
userName: string;
|
||
department: string;
|
||
featuredCount: number;
|
||
};
|
||
|
||
type CustomLink = {
|
||
id: string;
|
||
name: string;
|
||
url: string;
|
||
order: number;
|
||
toolboxGroup?: number; // 1 或 2
|
||
};
|
||
|
||
type TutorialVideo = {
|
||
id: string;
|
||
title: string;
|
||
videoUrl: string;
|
||
coverUrl: string | null;
|
||
description: string | null;
|
||
order: number;
|
||
};
|
||
|
||
type TopNavbarProps = {
|
||
siteName?: string;
|
||
siteLogo?: string;
|
||
siteLogoDark?: string;
|
||
isAdmin?: boolean;
|
||
settingsHref?: string | null;
|
||
customLinks?: CustomLink[];
|
||
toolbox1Name?: string;
|
||
toolbox1Enabled?: boolean;
|
||
toolbox2Name?: string;
|
||
toolbox2Enabled?: boolean;
|
||
tutorialVideos?: TutorialVideo[];
|
||
tutorialEnabled?: boolean;
|
||
rankingEnabled?: boolean;
|
||
productPlanningEnabled?: boolean;
|
||
agentEnabled?: boolean;
|
||
};
|
||
|
||
export function TopNavbar({
|
||
siteName = "AI Chatbot",
|
||
siteLogo,
|
||
siteLogoDark,
|
||
isAdmin = false,
|
||
settingsHref = null,
|
||
customLinks = [],
|
||
toolbox1Name = "工具箱",
|
||
toolbox1Enabled = true,
|
||
toolbox2Name = "工具箱2",
|
||
toolbox2Enabled = false,
|
||
tutorialVideos = [],
|
||
tutorialEnabled = false,
|
||
rankingEnabled = true,
|
||
productPlanningEnabled = true,
|
||
agentEnabled = true,
|
||
}: TopNavbarProps) {
|
||
const router = useRouter();
|
||
const { resolvedTheme } = useTheme();
|
||
const [mounted, setMounted] = useState(false);
|
||
const [ranking, setRanking] = useState<RankingItem[]>([]);
|
||
const [rankingOpen, setRankingOpen] = useState(false);
|
||
const [tutorialMenuOpen, setTutorialMenuOpen] = useState(false);
|
||
const [showTutorialModal, setShowTutorialModal] = useState(false);
|
||
const [currentTutorialVideo, setCurrentTutorialVideo] = useState<TutorialVideo | null>(null);
|
||
const [showImageEditor, setShowImageEditor] = useState(false);
|
||
const [editorImageUrl, setEditorImageUrl] = useState<string>("");
|
||
// 移除 fileInputRef,不再需要外部上传
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
// 获取精选榜单数据
|
||
const fetchRanking = async () => {
|
||
try {
|
||
const res = await fetch("/api/featured/ranking");
|
||
const data = await res.json();
|
||
if (data.ranking) {
|
||
setRanking(data.ranking);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to fetch ranking:", error);
|
||
}
|
||
};
|
||
|
||
// 当下拉菜单打开时获取数据
|
||
useEffect(() => {
|
||
if (rankingOpen) {
|
||
fetchRanking();
|
||
}
|
||
}, [rankingOpen]);
|
||
|
||
// 获取排名图标
|
||
const getRankIcon = (rank: number) => {
|
||
switch (rank) {
|
||
case 1:
|
||
return <Crown className="h-4 w-4 text-yellow-500" />;
|
||
case 2:
|
||
return <Medal className="h-4 w-4 text-gray-400" />;
|
||
case 3:
|
||
return <Award className="h-4 w-4 text-amber-600" />;
|
||
default:
|
||
return <span className="w-4 h-4 text-center text-xs font-bold text-gray-500">{rank}</span>;
|
||
}
|
||
};
|
||
|
||
// 决定显示哪个 LOGO(浅色用普通,深色用暗色)
|
||
const displayLogo = resolvedTheme === "dark"
|
||
? (siteLogoDark && siteLogoDark.trim() !== "" ? siteLogoDark : siteLogo)
|
||
: (siteLogo && siteLogo.trim() !== "" ? siteLogo : siteLogoDark);
|
||
|
||
const getShortLabel = (label: string) => {
|
||
const trimmed = label.trim();
|
||
if (!trimmed) return "";
|
||
if (trimmed === "AI精选") return "精选";
|
||
if (trimmed === "功能教程") return "教程";
|
||
if (trimmed === "精选榜") return "榜单";
|
||
if (trimmed === "图片编辑") return "编辑";
|
||
if (trimmed === "智能体") return "智能";
|
||
if (trimmed === "视频画布") return "视频";
|
||
if (trimmed === "无限画布") return "画布";
|
||
if (trimmed === "我的图册") return "图册";
|
||
if (trimmed === "商品策划") return "策划";
|
||
if (trimmed === "系统设置") return "设置";
|
||
if (trimmed === "工具箱") return "工具";
|
||
if (trimmed === "工具箱2") return "工具";
|
||
return Array.from(trimmed).slice(0, 2).join("");
|
||
};
|
||
|
||
const navItemBaseClass =
|
||
"flex h-12 w-12 flex-col items-center justify-center gap-1 rounded-xl text-gray-800 hover:bg-black/5 transition-colors dark:text-white dark:hover:bg-white/10";
|
||
const navLabelClass = "text-[10px] leading-none font-medium text-gray-700 dark:text-white/90";
|
||
|
||
// 打开图片编辑器 - 直接打开,不触发上传
|
||
const handleOpenImageEditor = () => {
|
||
setEditorImageUrl(""); // 确保初始为空
|
||
setShowImageEditor(true);
|
||
};
|
||
|
||
// 处理图片编辑完成
|
||
const handleImageEditorConfirm = async (annotatedImageUrl: string) => {
|
||
// 如果仍然是 base64 格式,先上传到服务器(ImageAnnotator 应该已经处理了,这里作为保护)
|
||
let finalUrl = annotatedImageUrl;
|
||
if (annotatedImageUrl.startsWith("data:")) {
|
||
try {
|
||
toast.info("正在上传图片...");
|
||
const response = await fetch(annotatedImageUrl);
|
||
const blob = await response.blob();
|
||
const formData = new FormData();
|
||
formData.append("file", blob, `edited-${Date.now()}.png`);
|
||
const uploadResponse = await fetchUploadWithRetry(formData);
|
||
if (uploadResponse.ok) {
|
||
const result = await uploadResponse.json();
|
||
finalUrl = result.url;
|
||
} else {
|
||
toast.error("上传失败");
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
console.error("上传图片失败:", error);
|
||
toast.error("上传失败");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 将编辑后的图片存储到 sessionStorage,以便在聊天页面中使用
|
||
try {
|
||
sessionStorage.setItem("pending-edited-image", finalUrl);
|
||
} catch (error) {
|
||
// sessionStorage不可用(如隐私模式),使用URL参数作为备选方案
|
||
console.warn("sessionStorage不可用,尝试使用URL参数:", error);
|
||
// 如果sessionStorage不可用,我们可以将图片数据编码到URL中
|
||
// 但由于数据可能很大,这里只记录警告,让用户手动添加图片
|
||
// 或者可以显示提示信息
|
||
}
|
||
// 导航到新聊天页面(无论sessionStorage是否成功)
|
||
router.push("/");
|
||
setShowImageEditor(false);
|
||
setEditorImageUrl("");
|
||
};
|
||
|
||
// 取消图片编辑
|
||
const handleImageEditorCancel = () => {
|
||
setShowImageEditor(false);
|
||
setEditorImageUrl("");
|
||
};
|
||
|
||
const navContent = (
|
||
<nav className="fixed left-0 top-0 bottom-0 z-[100] flex w-16 flex-col items-center bg-white/90 backdrop-blur-sm text-gray-900 border-r border-gray-200 shadow-sm dark:bg-black/90 dark:text-white dark:border-white/10">
|
||
{/* 顶部:LOGO + 网站名称 */}
|
||
<a
|
||
href="/"
|
||
className="mt-3 flex flex-col items-center gap-1 transition-opacity hover:opacity-80"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/";
|
||
}}
|
||
>
|
||
{displayLogo && (
|
||
<img
|
||
src={displayLogo}
|
||
alt={siteName}
|
||
className="h-7 w-7 object-contain"
|
||
/>
|
||
)}
|
||
<span className="max-w-[4.5rem] text-center text-[10px] leading-tight line-clamp-2">{siteName}</span>
|
||
</a>
|
||
|
||
{/* 侧边功能区 */}
|
||
<div className="mt-12 flex w-full flex-1 flex-col items-center gap-3 overflow-y-auto px-2 pb-4">
|
||
{/* 工具箱1 (如果启用且有链接) */}
|
||
{toolbox1Enabled && customLinks.filter(l => (l.toolboxGroup || 1) === 1).length > 0 && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`${navItemBaseClass} p-0`}
|
||
title={toolbox1Name}
|
||
type="button"
|
||
>
|
||
<Briefcase className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel(toolbox1Name)}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent side="right" align="center" className="w-[320px] p-2">
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{customLinks
|
||
.filter(l => (l.toolboxGroup || 1) === 1)
|
||
.sort((a, b) => a.order - b.order)
|
||
.map((link) => (
|
||
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
|
||
<a
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
|
||
>
|
||
<ExternalLink className="h-5 w-5 opacity-70" />
|
||
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{/* 工具箱2 (如果启用且有链接) */}
|
||
{toolbox2Enabled && customLinks.filter(l => l.toolboxGroup === 2).length > 0 && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`${navItemBaseClass} p-0`}
|
||
title={toolbox2Name}
|
||
type="button"
|
||
>
|
||
<Briefcase className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel(toolbox2Name)}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent side="right" align="center" className="w-[320px] p-2">
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{customLinks
|
||
.filter(l => l.toolboxGroup === 2)
|
||
.sort((a, b) => a.order - b.order)
|
||
.map((link) => (
|
||
<DropdownMenuItem key={link.id} asChild className="cursor-pointer">
|
||
<a
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex flex-col items-center justify-center gap-2 rounded-md bg-muted/50 p-4 hover:bg-accent hover:text-accent-foreground text-center h-20 transition-colors"
|
||
>
|
||
<ExternalLink className="h-5 w-5 opacity-70" />
|
||
<span className="text-xs font-medium line-clamp-2">{link.name}</span>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{/* AI智能精选 */}
|
||
<a
|
||
href="/featured"
|
||
className={navItemBaseClass}
|
||
title="AI精选"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/featured";
|
||
}}
|
||
>
|
||
<Sparkles className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("AI精选")}</span>
|
||
</a>
|
||
|
||
{productPlanningEnabled && (
|
||
<a
|
||
href="/product-planning"
|
||
className={navItemBaseClass}
|
||
title="商品策划"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/product-planning";
|
||
}}
|
||
>
|
||
<BarChart3 className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("商品策划")}</span>
|
||
</a>
|
||
)}
|
||
|
||
{/* 功能教程 */}
|
||
{tutorialEnabled && tutorialVideos.length > 0 && (
|
||
<DropdownMenu open={tutorialMenuOpen} onOpenChange={setTutorialMenuOpen}>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`${navItemBaseClass} p-0`}
|
||
type="button"
|
||
title="功能教程"
|
||
>
|
||
<PlayCircle className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("功能教程")}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent side="right" align="center" className="w-[320px] p-2">
|
||
<div className="space-y-1 max-h-[400px] overflow-y-auto">
|
||
{tutorialVideos
|
||
.sort((a, b) => a.order - b.order)
|
||
.map((video) => (
|
||
<DropdownMenuItem
|
||
key={video.id}
|
||
className="cursor-pointer p-0"
|
||
onClick={() => {
|
||
setCurrentTutorialVideo(video);
|
||
setShowTutorialModal(true);
|
||
setTutorialMenuOpen(false);
|
||
}}
|
||
>
|
||
<div className="flex items-center gap-3 p-2 w-full hover:bg-accent rounded-md">
|
||
{/* 封面 */}
|
||
<div className="w-16 h-10 bg-muted rounded overflow-hidden flex-shrink-0">
|
||
{video.coverUrl ? (
|
||
<img
|
||
src={video.coverUrl}
|
||
alt={video.title}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-green-500/20 to-blue-500/20">
|
||
<PlayCircle className="h-5 w-5 text-green-500" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* 标题和描述 */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="font-medium text-sm truncate">{video.title}</div>
|
||
{video.description && (
|
||
<div className="text-xs text-muted-foreground truncate">{video.description}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{/* 精选榜单 */}
|
||
{rankingEnabled && (
|
||
<DropdownMenu open={rankingOpen} onOpenChange={setRankingOpen}>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`${navItemBaseClass} p-0`}
|
||
type="button"
|
||
title="精选榜单"
|
||
>
|
||
<Trophy className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("精选榜")}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent side="right" align="center" className="w-[280px] p-3">
|
||
<div className="flex items-center gap-2 mb-3 pb-2 border-b">
|
||
<Trophy className="h-5 w-5 text-orange-500" />
|
||
<span className="font-semibold">精选榜单 TOP5</span>
|
||
</div>
|
||
{ranking.length === 0 ? (
|
||
<div className="text-center text-muted-foreground py-4 text-sm">
|
||
暂无精选数据
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{ranking.map((item) => (
|
||
<div
|
||
key={item.userId}
|
||
className={`flex items-center gap-3 p-2 rounded-lg transition-colors ${
|
||
item.rank === 1 ? "bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20" :
|
||
item.rank === 2 ? "bg-gray-50 dark:bg-gray-800/50" :
|
||
item.rank === 3 ? "bg-amber-50 dark:bg-amber-900/20" :
|
||
"hover:bg-muted"
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-center w-6">
|
||
{getRankIcon(item.rank)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="font-medium text-sm truncate">{item.userName}</p>
|
||
<p className="text-xs text-muted-foreground flex items-center gap-1 truncate">
|
||
<Building2 className="h-3 w-3" />
|
||
{item.department}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<span className="text-lg font-bold text-orange-500">{item.featuredCount}</span>
|
||
<span className="text-xs text-muted-foreground ml-1">精选</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{/* 图片编辑 */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className={`${navItemBaseClass} p-0`}
|
||
onClick={handleOpenImageEditor}
|
||
type="button"
|
||
title="图片编辑"
|
||
>
|
||
<Edit className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("图片编辑")}</span>
|
||
</Button>
|
||
|
||
{/* 智能体模式 */}
|
||
{agentEnabled && (
|
||
<button
|
||
className={navItemBaseClass}
|
||
onClick={() => {
|
||
window.location.href = "/canvas?mode=agent";
|
||
}}
|
||
type="button"
|
||
title="智能体"
|
||
>
|
||
<Wand2 className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("智能体")}</span>
|
||
</button>
|
||
)}
|
||
|
||
<button
|
||
className={navItemBaseClass}
|
||
onClick={() => {
|
||
window.location.href = "/video-canvas";
|
||
}}
|
||
type="button"
|
||
title="视频画布"
|
||
>
|
||
<Clapperboard className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("视频画布")}</span>
|
||
</button>
|
||
|
||
{/* 无限画布 */}
|
||
<button
|
||
className={navItemBaseClass}
|
||
onClick={() => {
|
||
const newId = crypto.randomUUID();
|
||
window.location.href = `/canvas/${newId}`;
|
||
}}
|
||
type="button"
|
||
title="无限画布"
|
||
>
|
||
<LayoutGrid className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("无限画布")}</span>
|
||
</button>
|
||
|
||
{/* 我的图册 */}
|
||
<a
|
||
href="/gallery"
|
||
className={navItemBaseClass}
|
||
title="我的图册"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = "/gallery";
|
||
}}
|
||
>
|
||
<Images className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("我的图册")}</span>
|
||
</a>
|
||
</div>
|
||
|
||
{/* 系统设置(管理员 / 智能体管理员可见)*/}
|
||
{(isAdmin || settingsHref) && (
|
||
<div className="pb-4">
|
||
<a
|
||
href={settingsHref || "/settings"}
|
||
className={navItemBaseClass}
|
||
title="系统设置"
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
window.location.href = settingsHref || "/settings";
|
||
}}
|
||
>
|
||
<Settings className="h-5 w-5" />
|
||
<span className={navLabelClass}>{getShortLabel("系统设置")}</span>
|
||
</a>
|
||
</div>
|
||
)}
|
||
|
||
{/* 图片编辑器 */}
|
||
{showImageEditor && (
|
||
<ImageAnnotator
|
||
imageUrl={editorImageUrl}
|
||
onConfirm={handleImageEditorConfirm}
|
||
submitMode="publish-new-canvas"
|
||
onCancel={handleImageEditorCancel}
|
||
/>
|
||
)}
|
||
</nav>
|
||
);
|
||
|
||
// 使用 Portal 将视频弹窗渲染到 body,确保它在最顶层且不受 nav 的限制
|
||
const videoModal = showTutorialModal && currentTutorialVideo && typeof document !== 'undefined' ? createPortal(
|
||
<div
|
||
className="fixed inset-0 z-[99999] bg-black/95"
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
margin: 0,
|
||
padding: '20px',
|
||
}}
|
||
onClick={() => {
|
||
setShowTutorialModal(false);
|
||
setCurrentTutorialVideo(null);
|
||
}}
|
||
>
|
||
<div
|
||
className="relative w-full max-w-[1200px]"
|
||
style={{ maxHeight: 'calc(100vh - 80px)' }}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{/* 标题栏 - 包含标题和关闭按钮 */}
|
||
<div className="flex items-center justify-between mb-3 px-2">
|
||
<div className="w-8" /> {/* 占位符保持标题居中 */}
|
||
<h3 className="text-white text-lg font-medium truncate flex-1 text-center">{currentTutorialVideo.title}</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowTutorialModal(false);
|
||
setCurrentTutorialVideo(null);
|
||
}}
|
||
className="text-white hover:text-red-400 transition-colors z-10 p-1"
|
||
title="关闭 (ESC)"
|
||
>
|
||
<X className="h-8 w-8" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 视频播放器 */}
|
||
{currentTutorialVideo.videoUrl.includes('youtube.com') || currentTutorialVideo.videoUrl.includes('youtu.be') ? (
|
||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||
<iframe
|
||
src={`${currentTutorialVideo.videoUrl.replace('watch?v=', 'embed/').replace('youtu.be/', 'youtube.com/embed/')}?autoplay=1&rel=0&modestbranding=1`}
|
||
className="w-full h-full"
|
||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||
allowFullScreen
|
||
loading="eager"
|
||
/>
|
||
</div>
|
||
) : currentTutorialVideo.videoUrl.includes('bilibili.com') ? (
|
||
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden shadow-2xl">
|
||
<iframe
|
||
src={`${currentTutorialVideo.videoUrl.replace('www.bilibili.com/video/', 'player.bilibili.com/player.html?bvid=')}&autoplay=1&high_quality=1`}
|
||
className="w-full h-full"
|
||
allowFullScreen
|
||
loading="eager"
|
||
/>
|
||
</div>
|
||
) : (
|
||
<VideoPlayer
|
||
src={currentTutorialVideo.videoUrl}
|
||
poster={currentTutorialVideo.coverUrl || undefined}
|
||
title={currentTutorialVideo.title}
|
||
autoPlay={true}
|
||
className="shadow-2xl"
|
||
/>
|
||
)}
|
||
|
||
{/* 视频描述 */}
|
||
{currentTutorialVideo.description && (
|
||
<p className="text-gray-300 text-sm mt-3 text-center">{currentTutorialVideo.description}</p>
|
||
)}
|
||
</div>
|
||
</div>,
|
||
document.body
|
||
) : null;
|
||
|
||
return (
|
||
<>
|
||
{navContent}
|
||
{videoModal}
|
||
</>
|
||
);
|
||
}
|