This commit is contained in:
Kevin Wong
2026-02-04 17:19:24 +08:00
parent 31469ca01d
commit aaa8088c82
6 changed files with 113 additions and 101 deletions

View File

@@ -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<string>("");
const isTitleComposingRef = useRef(false);
const titleCommittedRef = useRef("");
const [enableSubtitles, setEnableSubtitles] = useState<boolean>(true);
const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState<string>("");
const [selectedTitleStyleId, setSelectedTitleStyleId] = useState<string>("");
@@ -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}

View File

@@ -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<string[]>([]);
const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<string>("");
const isTitleComposingRef = useRef(false);
const titleCommittedRef = useRef("");
const [isPublishing, setIsPublishing] = useState(false);
const [isPublishing, setIsPublishing] = useState(false);
const [publishResults, setPublishResults] = useState<any[]>([]);
const [scheduleMode, setScheduleMode] = useState<"now" | "scheduled">("now");
const [publishTime, setPublishTime] = useState<string>("");
@@ -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() {
<input
type="text"
value={title}
onChange={(e) => {
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"
/>

View File

@@ -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;

View File

@@ -20,19 +20,15 @@ export const useMaterials = ({
}: UseMaterialsOptions) => {
const [materials, setMaterials] = useState<Material[]>([]);
const [fetchError, setFetchError] = useState<string | null>(null);
const [debugData, setDebugData] = useState<string>("");
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadError, setUploadError] = useState<string | null>(null);
const [uploadData, setUploadData] = useState<string>("");
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,

View File

@@ -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,
};
};

14
frontend/src/lib/title.ts Normal file
View File

@@ -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);
};