diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 65b3983..a75503d 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -12,6 +12,7 @@ import { buildTextShadow, formatDate, } from "@/lib/media"; +import { clampTitle } from "@/lib/title"; import { useTitleSubtitleStyles } from "@/hooks/useTitleSubtitleStyles"; import { useMaterials } from "@/hooks/useMaterials"; import { useRefAudios } from "@/hooks/useRefAudios"; @@ -19,6 +20,7 @@ import { useBgm } from "@/hooks/useBgm"; import { useMediaPlayers } from "@/hooks/useMediaPlayers"; import { useGeneratedVideos } from "@/hooks/useGeneratedVideos"; import { useHomePersistence } from "@/hooks/useHomePersistence"; +import { useTitleInput } from "@/hooks/useTitleInput"; import { useAuth } from "@/contexts/AuthContext"; import { useTask } from "@/contexts/TaskContext"; import VideoPreviewModal from "@/components/VideoPreviewModal"; @@ -35,13 +37,6 @@ import { PreviewPanel } from "@/components/home/PreviewPanel"; import { HistoryList } from "@/components/home/HistoryList"; const API_BASE = getApiBaseUrl(); -const TITLE_MAX_LENGTH = 15; -const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH); -const applyTitleLimit = (prev: string, next: string) => { - if (next.length <= TITLE_MAX_LENGTH) return next; - if (prev.length >= TITLE_MAX_LENGTH) return prev; - return next.slice(0, TITLE_MAX_LENGTH); -}; const VOICES = [ { id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" }, @@ -121,8 +116,6 @@ export default function Home() { // 字幕和标题相关状态 const [videoTitle, setVideoTitle] = useState(""); - const isTitleComposingRef = useRef(false); - const titleCommittedRef = useRef(""); const [enableSubtitles, setEnableSubtitles] = useState(true); const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState(""); const [selectedTitleStyleId, setSelectedTitleStyleId] = useState(""); @@ -316,37 +309,17 @@ export default function Home() { selectedRefAudio, }); - useEffect(() => { - if (isTitleComposingRef.current) return; - titleCommittedRef.current = videoTitle; - }, [videoTitle]); - - const commitTitle = (value: string) => { - titleCommittedRef.current = value; - setVideoTitle(value); + const syncTitleToPublish = (value: string) => { if (typeof window !== 'undefined') { localStorage.setItem(`vigent_${storageKey}_publish_title`, value); } }; - const handleTitleChange = (value: string) => { - if (isTitleComposingRef.current) { - setVideoTitle(value); - return; - } - const nextValue = applyTitleLimit(titleCommittedRef.current, value); - commitTitle(nextValue); - }; - - const handleTitleCompositionStart = () => { - isTitleComposingRef.current = true; - }; - - const handleTitleCompositionEnd = (value: string) => { - isTitleComposingRef.current = false; - const nextValue = applyTitleLimit(titleCommittedRef.current, value); - commitTitle(nextValue); - }; + const titleInput = useTitleInput({ + value: videoTitle, + onChange: setVideoTitle, + onCommit: syncTitleToPublish, + }); // 加载素材列表和历史视频 useEffect(() => { @@ -606,11 +579,10 @@ export default function Home() { // 更新首页标题 const nextTitle = clampTitle(data.title || ""); - setVideoTitle(nextTitle); + titleInput.commitValue(nextTitle); // 同步到发布页 localStorage console.log("[Home] 保存到 publish localStorage - title:", data.title, "tags:", data.tags); - localStorage.setItem(`vigent_${storageKey}_publish_title`, nextTitle); localStorage.setItem(`vigent_${storageKey}_publish_tags`, JSON.stringify(data.tags || [])); } catch (err: any) { @@ -748,9 +720,9 @@ export default function Home() { showStylePreview={showStylePreview} onTogglePreview={() => setShowStylePreview((prev) => !prev)} videoTitle={videoTitle} - onTitleChange={handleTitleChange} - onTitleCompositionStart={handleTitleCompositionStart} - onTitleCompositionEnd={handleTitleCompositionEnd} + onTitleChange={titleInput.handleChange} + onTitleCompositionStart={titleInput.handleCompositionStart} + onTitleCompositionEnd={titleInput.handleCompositionEnd} titleStyles={titleStyles} selectedTitleStyleId={selectedTitleStyleId} onSelectTitleStyle={setSelectedTitleStyleId} diff --git a/frontend/src/app/publish/page.tsx b/frontend/src/app/publish/page.tsx index 49873c7..5ee7ef8 100644 --- a/frontend/src/app/publish/page.tsx +++ b/frontend/src/app/publish/page.tsx @@ -1,13 +1,15 @@ "use client"; -import { useState, useEffect, useMemo, useRef } from "react"; +import { useState, useEffect, useMemo } from "react"; import useSWR from 'swr'; import Link from "next/link"; import api from "@/lib/axios"; import { getApiBaseUrl, formatDate, resolveMediaUrl, isAbsoluteUrl } from "@/lib/media"; +import { clampTitle } from "@/lib/title"; import { useAuth } from "@/contexts/AuthContext"; import AccountSettingsDropdown from "@/components/AccountSettingsDropdown"; import VideoPreviewModal from "@/components/VideoPreviewModal"; +import { useTitleInput } from "@/hooks/useTitleInput"; import { ArrowLeft, RotateCcw, @@ -25,13 +27,6 @@ const fetcher = (url: string) => api.get(url).then((res) => res.data); // 动态获取 API 地址:服务端使用 localhost,客户端使用当前域名 const API_BASE = getApiBaseUrl(); -const TITLE_MAX_LENGTH = 15; -const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH); -const applyTitleLimit = (prev: string, next: string) => { - if (next.length <= TITLE_MAX_LENGTH) return next; - if (prev.length >= TITLE_MAX_LENGTH) return prev; - return next.slice(0, TITLE_MAX_LENGTH); -}; interface Account { platform: string; @@ -54,9 +49,7 @@ export default function PublishPage() { const [selectedPlatforms, setSelectedPlatforms] = useState([]); const [title, setTitle] = useState(""); const [tags, setTags] = useState(""); - const isTitleComposingRef = useRef(false); - const titleCommittedRef = useRef(""); - const [isPublishing, setIsPublishing] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); const [publishResults, setPublishResults] = useState([]); const [scheduleMode, setScheduleMode] = useState<"now" | "scheduled">("now"); const [publishTime, setPublishTime] = useState(""); @@ -69,15 +62,10 @@ export default function PublishPage() { // 是否已从 localStorage 恢复完成 const [isRestored, setIsRestored] = useState(false); - useEffect(() => { - if (isTitleComposingRef.current) return; - titleCommittedRef.current = title; - }, [title]); - - const commitTitle = (value: string) => { - titleCommittedRef.current = value; - setTitle(value); - }; + const titleInput = useTitleInput({ + value: title, + onChange: setTitle, + }); // 加载账号和视频列表 useEffect(() => { @@ -100,16 +88,12 @@ export default function PublishPage() { // 从 localStorage 恢复用户输入(等待认证完成后) useEffect(() => { - console.log("[Publish] 恢复检查 - isAuthLoading:", isAuthLoading, "userId:", userId); - if (isAuthLoading) return; - - console.log("[Publish] 开始从 localStorage 恢复数据,storageKey:", storageKey); - // 从 localStorage 恢复用户输入(带用户隔离,未登录用户使用 guest) - const savedTitle = localStorage.getItem(`vigent_${storageKey}_publish_title`); - const savedTags = localStorage.getItem(`vigent_${storageKey}_publish_tags`); - - console.log("[Publish] localStorage 数据:", { savedTitle, savedTags }); - + if (isAuthLoading) return; + + // 从 localStorage 恢复用户输入(带用户隔离,未登录用户使用 guest) + const savedTitle = localStorage.getItem(`vigent_${storageKey}_publish_title`); + const savedTags = localStorage.getItem(`vigent_${storageKey}_publish_tags`); + if (savedTitle) setTitle(clampTitle(savedTitle)); if (savedTags) { // 兼容 JSON 数组格式(AI 生成)和字符串格式(手动输入) @@ -125,10 +109,9 @@ export default function PublishPage() { } } - // 恢复完成后才允许保存 - setIsRestored(true); - console.log("[Publish] 恢复完成,isRestored = true"); - }, [storageKey, isAuthLoading]); + // 恢复完成后才允许保存 + setIsRestored(true); + }, [storageKey, isAuthLoading]); // 保存用户输入到 localStorage(恢复完成后才保存,未登录用户也可保存) useEffect(() => { @@ -537,22 +520,9 @@ export default function PublishPage() { { - if (isTitleComposingRef.current) { - setTitle(e.target.value); - return; - } - const nextValue = applyTitleLimit(titleCommittedRef.current, e.target.value); - commitTitle(nextValue); - }} - onCompositionStart={() => { - isTitleComposingRef.current = true; - }} - onCompositionEnd={(e) => { - isTitleComposingRef.current = false; - const nextValue = applyTitleLimit(titleCommittedRef.current, e.currentTarget.value); - commitTitle(nextValue); - }} + onChange={(e) => titleInput.handleChange(e.target.value)} + onCompositionStart={titleInput.handleCompositionStart} + onCompositionEnd={(e) => titleInput.handleCompositionEnd(e.currentTarget.value)} placeholder="输入视频标题..." className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500" /> diff --git a/frontend/src/hooks/useHomePersistence.ts b/frontend/src/hooks/useHomePersistence.ts index 5c59d45..3cb73ac 100644 --- a/frontend/src/hooks/useHomePersistence.ts +++ b/frontend/src/hooks/useHomePersistence.ts @@ -1,7 +1,5 @@ import { useEffect, useState } from "react"; - -const TITLE_MAX_LENGTH = 15; -const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH); +import { clampTitle } from "@/lib/title"; interface RefAudio { id: string; diff --git a/frontend/src/hooks/useMaterials.ts b/frontend/src/hooks/useMaterials.ts index 7c76b98..31677b7 100644 --- a/frontend/src/hooks/useMaterials.ts +++ b/frontend/src/hooks/useMaterials.ts @@ -20,19 +20,15 @@ export const useMaterials = ({ }: UseMaterialsOptions) => { const [materials, setMaterials] = useState([]); const [fetchError, setFetchError] = useState(null); - const [debugData, setDebugData] = useState(""); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadError, setUploadError] = useState(null); - const [uploadData, setUploadData] = useState(""); const fetchMaterials = useCallback(async () => { try { setFetchError(null); - setDebugData("Loading..."); const { data } = await api.get(`/api/materials?t=${new Date().getTime()}`); - setDebugData(JSON.stringify(data).substring(0, 200)); const nextMaterials = data.materials || []; setMaterials(nextMaterials); @@ -45,7 +41,6 @@ export const useMaterials = ({ } catch (error) { console.error("获取素材失败:", error); setFetchError(String(error)); - setDebugData(`Error: ${String(error)}`); } }, [selectedMaterial, setSelectedMaterial]); @@ -94,7 +89,6 @@ export const useMaterials = ({ setUploadProgress(100); setIsUploading(false); fetchMaterials(); - setUploadData(""); } catch (err: any) { console.error("Upload failed:", err); setIsUploading(false); @@ -108,11 +102,9 @@ export const useMaterials = ({ return { materials, fetchError, - debugData, isUploading, uploadProgress, uploadError, - uploadData, setUploadError, fetchMaterials, deleteMaterial, diff --git a/frontend/src/hooks/useTitleInput.ts b/frontend/src/hooks/useTitleInput.ts new file mode 100644 index 0000000..66205e0 --- /dev/null +++ b/frontend/src/hooks/useTitleInput.ts @@ -0,0 +1,66 @@ +import { useCallback, useEffect, useRef } from "react"; +import { applyTitleLimit, TITLE_MAX_LENGTH } from "@/lib/title"; + +interface UseTitleInputOptions { + value: string; + onChange: (value: string) => void; + onCommit?: (value: string) => void; + maxLength?: number; +} + +export const useTitleInput = ({ + value, + onChange, + onCommit, + maxLength = TITLE_MAX_LENGTH, +}: UseTitleInputOptions) => { + const isComposingRef = useRef(false); + const committedRef = useRef(value); + + useEffect(() => { + if (isComposingRef.current) return; + committedRef.current = value; + }, [value]); + + const commitValue = useCallback( + (nextValue: string) => { + committedRef.current = nextValue; + onChange(nextValue); + onCommit?.(nextValue); + }, + [onChange, onCommit] + ); + + const handleChange = useCallback( + (nextValue: string) => { + if (isComposingRef.current) { + onChange(nextValue); + return; + } + const limited = applyTitleLimit(committedRef.current, nextValue, maxLength); + commitValue(limited); + }, + [maxLength, onChange, commitValue] + ); + + const handleCompositionStart = useCallback(() => { + isComposingRef.current = true; + }, []); + + const handleCompositionEnd = useCallback( + (nextValue: string) => { + isComposingRef.current = false; + const limited = applyTitleLimit(committedRef.current, nextValue, maxLength); + commitValue(limited); + }, + [maxLength, commitValue] + ); + + return { + handleChange, + handleCompositionStart, + handleCompositionEnd, + commitValue, + maxLength, + }; +}; diff --git a/frontend/src/lib/title.ts b/frontend/src/lib/title.ts new file mode 100644 index 0000000..2801d14 --- /dev/null +++ b/frontend/src/lib/title.ts @@ -0,0 +1,14 @@ +export const TITLE_MAX_LENGTH = 15; + +export const clampTitle = (value: string, maxLength: number = TITLE_MAX_LENGTH) => + value.slice(0, maxLength); + +export const applyTitleLimit = ( + prev: string, + next: string, + maxLength: number = TITLE_MAX_LENGTH +) => { + if (next.length <= maxLength) return next; + if (prev.length >= maxLength) return prev; + return next.slice(0, maxLength); +};