新首页侧边栏增加功能提示并修复教程播放
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-15 01:40:34 +08:00
parent d147524fc3
commit 9cf1762f69
2 changed files with 135 additions and 25 deletions

View File

@ -335,6 +335,8 @@ export function LovartHome({
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [showImageEditor, setShowImageEditor] = useState(false);
const [editorImageUrl, setEditorImageUrl] = useState("");
const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false);
const [currentTutorialVideo, setCurrentTutorialVideo] = useState<TutorialVideo | null>(null);
const [typedPreviewText, setTypedPreviewText] = useState("");
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
const projectSectionRef = useRef<HTMLDivElement | null>(null);
@ -576,6 +578,31 @@ export function LovartHome({
.sort((a, b) => a.order - b.order),
[navLinksData?.links]
);
const tutorialVideos = useMemo(() => {
const configured = (tutorialVideosData?.videos || [])
.filter((video) => video.isEnabled !== false && Boolean(video.videoUrl))
.sort((a, b) => a.order - b.order);
if (configured.length > 0) {
return configured;
}
const fallbackUrl = String(siteSettingsData?.tutorialVideoUrl || "").trim();
if (!fallbackUrl) {
return [] as TutorialVideo[];
}
return [
{
id: "fallback-tutorial",
title: "功能教程",
videoUrl: fallbackUrl,
coverUrl: null,
description: "默认教程视频",
order: 0,
isEnabled: true,
},
];
}, [siteSettingsData?.tutorialVideoUrl, tutorialVideosData?.videos]);
const canUseGoogleLogin = Boolean(
authSettingsData?.isGoogleEnabled && authSettingsData?.isGoogleConfigured
@ -913,24 +940,13 @@ export function LovartHome({
setEditorImageUrl("");
};
const handleOpenTutorial = () => {
if (!isAuthenticated) {
openLoginDialog();
return;
}
const firstTutorial =
tutorialVideosData?.videos
?.filter((video) => video.isEnabled !== false && video.videoUrl)
?.sort((a, b) => a.order - b.order)?.[0] || null;
const tutorialUrl = firstTutorial?.videoUrl || siteSettingsData?.tutorialVideoUrl || "";
if (!tutorialUrl) {
const handleOpenTutorial = (video: TutorialVideo) => {
if (!video?.videoUrl) {
toast.info("暂无教程视频");
return;
}
window.open(tutorialUrl, "_blank", "noopener,noreferrer");
setCurrentTutorialVideo(video);
setTutorialDialogOpen(true);
};
const handleCreateDulupayOrder = async (packageId: string) => {
@ -1290,6 +1306,7 @@ export function LovartHome({
type="button"
onClick={openNewCanvas}
className="flex h-[52px] w-[52px] items-center justify-center rounded-full border border-black/10 bg-white shadow"
title="新建项目"
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white">
<Plus className="h-4 w-4" />
@ -1301,6 +1318,7 @@ export function LovartHome({
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full bg-black/5"
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
title="首页"
>
<Home className="h-[18px] w-[18px]" />
</button>
@ -1308,6 +1326,7 @@ export function LovartHome({
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={openAllProjectsInPage}
title="项目列表"
>
<Folder className="h-[18px] w-[18px]" />
</button>
@ -1315,6 +1334,7 @@ export function LovartHome({
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={() => toast.info("个人主页模块后续开放")}
title="个人主页"
>
<User className="h-[18px] w-[18px]" />
</button>
@ -1322,18 +1342,63 @@ export function LovartHome({
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={() => toast.info("帮助中心后续开放")}
title="帮助中心"
>
<Info className="h-[18px] w-[18px]" />
</button>
{siteSettingsData?.tutorialEnabled ? (
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={handleOpenTutorial}
title="视频教程"
>
<PlayCircle className="h-[18px] w-[18px]" />
</button>
<HoverCard openDelay={80} closeDelay={100}>
<HoverCardTrigger asChild>
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
title="视频教程"
>
<PlayCircle className="h-[18px] w-[18px]" />
</button>
</HoverCardTrigger>
<HoverCardContent side="right" align="start" className="w-[300px] rounded-2xl p-2">
<div className="px-2 pb-1 pt-1 text-xs font-medium text-black/50">视频教程</div>
<div className="max-h-[380px] space-y-1 overflow-y-auto">
{tutorialVideos.length > 0 ? (
tutorialVideos.map((video) => (
<button
key={video.id}
type="button"
className="flex w-full items-center gap-2.5 rounded-xl p-2 text-left hover:bg-black/[0.04]"
onClick={() => handleOpenTutorial(video)}
>
<div className="h-10 w-16 flex-shrink-0 overflow-hidden rounded-lg bg-black/[0.06]">
{video.coverUrl ? (
<img
src={video.coverUrl}
alt={video.title}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<PlayCircle className="h-4 w-4 text-black/45" />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-black/85">
{video.title}
</div>
{video.description ? (
<div className="truncate text-[11px] text-black/45">
{video.description}
</div>
) : null}
</div>
</button>
))
) : (
<div className="px-2 py-3 text-[12px] text-black/45">暂无教程视频</div>
)}
</div>
</HoverCardContent>
</HoverCard>
) : null}
<button
type="button"
@ -1410,6 +1475,7 @@ export function LovartHome({
type="button"
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
onClick={() => router.push("/settings")}
title="系统设置"
>
<Settings className="h-[18px] w-[18px]" />
</button>
@ -1737,9 +1803,46 @@ export function LovartHome({
imageUrl={editorImageUrl}
onConfirm={handleImageEditorConfirm}
onCancel={handleImageEditorCancel}
onHomeClick={handleImageEditorCancel}
/>
) : null}
<Dialog
open={tutorialDialogOpen}
onOpenChange={(open) => {
setTutorialDialogOpen(open);
if (!open) {
setCurrentTutorialVideo(null);
}
}}
>
<DialogContent className="w-[92vw] max-w-[980px] overflow-hidden rounded-[20px] border-black/10 bg-black p-0 text-white">
<DialogHeader className="border-b border-white/10 px-4 py-3">
<DialogTitle className="truncate text-[14px] font-medium text-white">
{currentTutorialVideo?.title || "功能教程"}
</DialogTitle>
</DialogHeader>
<div className="aspect-video w-full bg-black">
{currentTutorialVideo?.videoUrl ? (
<video
key={currentTutorialVideo.videoUrl}
className="h-full w-full"
controls
autoPlay
preload="metadata"
src={currentTutorialVideo.videoUrl}
poster={currentTutorialVideo.coverUrl || undefined}
/>
) : null}
</div>
{currentTutorialVideo?.description ? (
<div className="px-4 pb-4 pt-3 text-[12px] text-white/70">
{currentTutorialVideo.description}
</div>
) : null}
</DialogContent>
</Dialog>
{loginDialogOpen ? (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 p-4 backdrop-blur-[1px]"

View File

@ -31,6 +31,7 @@ interface ImageAnnotatorProps {
imageUrl?: string;
onConfirm: (annotatedImageUrl: string) => void;
onCancel: () => void;
onHomeClick?: () => void;
initialResizeMode?: boolean; // 是否直接进入尺寸调整模式
}
@ -48,7 +49,7 @@ const COLORS = [
const LINE_WIDTHS = [2, 4, 6, 8];
export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMode = false }: ImageAnnotatorProps) {
export function ImageAnnotator({ imageUrl, onConfirm, onCancel, onHomeClick, initialResizeMode = false }: ImageAnnotatorProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
@ -1384,7 +1385,13 @@ export function ImageAnnotator({ imageUrl, onConfirm, onCancel, initialResizeMod
<Link
href="/"
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
if (onHomeClick) {
e.preventDefault();
onHomeClick();
}
}}
>
{siteLogoDark || siteLogo ? (
<>