From 22ea3dd0dbf7ceac28115e92e43435567fa4027e Mon Sep 17 00:00:00 2001 From: Kevin Wong Date: Wed, 4 Feb 2026 16:54:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/DevLogs/Day17.md | 3 - backend/assets/styles/subtitle.json | 6 +- backend/assets/styles/title.json | 30 ++++---- frontend/src/app/page.tsx | 50 ++++++++++++- frontend/src/app/publish/page.tsx | 70 ++++++++++++++----- .../src/components/home/MaterialSelector.tsx | 6 +- .../components/home/TitleSubtitlePanel.tsx | 8 ++- .../src/components/home/VoiceSelector.tsx | 2 +- frontend/src/hooks/useHomePersistence.ts | 5 +- 9 files changed, 132 insertions(+), 48 deletions(-) diff --git a/Docs/DevLogs/Day17.md b/Docs/DevLogs/Day17.md index 1d71b30..0e07b08 100644 --- a/Docs/DevLogs/Day17.md +++ b/Docs/DevLogs/Day17.md @@ -27,9 +27,6 @@ - `frontend/src/app/page.tsx` - `frontend/src/hooks/useHomePersistence.ts` ---- - -## 🧩 前端 UI 拆分 (11:00) ## 🧩 前端 UI 拆分 (09:10) ### 内容 diff --git a/backend/assets/styles/subtitle.json b/backend/assets/styles/subtitle.json index dfcb52e..82f17d4 100644 --- a/backend/assets/styles/subtitle.json +++ b/backend/assets/styles/subtitle.json @@ -2,9 +2,9 @@ { "id": "subtitle_classic_yellow", "label": "经典黄字", - "font_file": "title/思源黑体/SourceHanSansCN-Bold思源黑体免费.otf", - "font_family": "SourceHanSansCN-Bold", - "font_size": 52, + "font_file": "DingTalk JinBuTi.ttf", + "font_family": "DingTalkJinBuTi", + "font_size": 60, "highlight_color": "#FFE600", "normal_color": "#FFFFFF", "stroke_color": "#000000", diff --git a/backend/assets/styles/title.json b/backend/assets/styles/title.json index 3f8a6d8..dbafbcb 100644 --- a/backend/assets/styles/title.json +++ b/backend/assets/styles/title.json @@ -1,4 +1,18 @@ [ + { + "id": "title_pop", + "label": "站酷快乐体", + "font_file": "title/站酷快乐体.ttf", + "font_family": "ZCoolHappy", + "font_size": 90, + "color": "#FFFFFF", + "stroke_color": "#000000", + "stroke_size": 8, + "letter_spacing": 5, + "top_margin": 62, + "font_weight": 900, + "is_default": true + }, { "id": "title_bold_white", "label": "黑体大标题", @@ -11,7 +25,7 @@ "letter_spacing": 4, "top_margin": 60, "font_weight": 900, - "is_default": true + "is_default": false }, { "id": "title_serif_gold", @@ -40,19 +54,5 @@ "top_margin": 60, "font_weight": 900, "is_default": false - }, - { - "id": "title_pop", - "label": "站酷快乐体", - "font_file": "title/站酷快乐体.ttf", - "font_family": "ZCoolHappy", - "font_size": 74, - "color": "#FFFFFF", - "stroke_color": "#000000", - "stroke_size": 8, - "letter_spacing": 5, - "top_margin": 62, - "font_weight": 900, - "is_default": false } ] diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 79b64e1..65b3983 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -35,6 +35,13 @@ 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: "云溪 (男声-年轻)" }, @@ -114,6 +121,8 @@ 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(""); @@ -307,6 +316,38 @@ export default function Home() { selectedRefAudio, }); + useEffect(() => { + if (isTitleComposingRef.current) return; + titleCommittedRef.current = videoTitle; + }, [videoTitle]); + + const commitTitle = (value: string) => { + titleCommittedRef.current = value; + setVideoTitle(value); + 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); + }; + // 加载素材列表和历史视频 useEffect(() => { if (isAuthLoading) return; @@ -564,11 +605,12 @@ export default function Home() { console.log("[Home] AI生成结果:", data); // 更新首页标题 - setVideoTitle(data.title || ""); + const nextTitle = clampTitle(data.title || ""); + setVideoTitle(nextTitle); // 同步到发布页 localStorage console.log("[Home] 保存到 publish localStorage - title:", data.title, "tags:", data.tags); - localStorage.setItem(`vigent_${storageKey}_publish_title`, data.title || ""); + localStorage.setItem(`vigent_${storageKey}_publish_title`, nextTitle); localStorage.setItem(`vigent_${storageKey}_publish_tags`, JSON.stringify(data.tags || [])); } catch (err: any) { @@ -706,7 +748,9 @@ export default function Home() { showStylePreview={showStylePreview} onTogglePreview={() => setShowStylePreview((prev) => !prev)} videoTitle={videoTitle} - onTitleChange={setVideoTitle} + onTitleChange={handleTitleChange} + onTitleCompositionStart={handleTitleCompositionStart} + onTitleCompositionEnd={handleTitleCompositionEnd} titleStyles={titleStyles} selectedTitleStyleId={selectedTitleStyleId} onSelectTitleStyle={setSelectedTitleStyleId} diff --git a/frontend/src/app/publish/page.tsx b/frontend/src/app/publish/page.tsx index 967c19f..49873c7 100644 --- a/frontend/src/app/publish/page.tsx +++ b/frontend/src/app/publish/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useRef } from "react"; import useSWR from 'swr'; import Link from "next/link"; import api from "@/lib/axios"; @@ -25,6 +25,13 @@ 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; @@ -38,15 +45,17 @@ interface Video { path: string; } -export default function PublishPage() { +export default function PublishPage() { const [accounts, setAccounts] = useState([]); const [videos, setVideos] = useState([]); const [selectedVideo, setSelectedVideo] = useState(""); const [videoFilter, setVideoFilter] = useState(""); const [previewVideoUrl, setPreviewVideoUrl] = useState(null); - const [selectedPlatforms, setSelectedPlatforms] = useState([]); - const [title, setTitle] = useState(""); - const [tags, setTags] = useState(""); + 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 [publishResults, setPublishResults] = useState([]); const [scheduleMode, setScheduleMode] = useState<"now" | "scheduled">("now"); @@ -57,8 +66,18 @@ export default function PublishPage() { // 使用全局认证状态 const { userId, isLoading: isAuthLoading } = useAuth(); - // 是否已从 localStorage 恢复完成 - const [isRestored, setIsRestored] = useState(false); + // 是否已从 localStorage 恢复完成 + const [isRestored, setIsRestored] = useState(false); + + useEffect(() => { + if (isTitleComposingRef.current) return; + titleCommittedRef.current = title; + }, [title]); + + const commitTitle = (value: string) => { + titleCommittedRef.current = value; + setTitle(value); + }; // 加载账号和视频列表 useEffect(() => { @@ -91,7 +110,7 @@ export default function PublishPage() { console.log("[Publish] localStorage 数据:", { savedTitle, savedTags }); - if (savedTitle) setTitle(savedTitle); + if (savedTitle) setTitle(clampTitle(savedTitle)); if (savedTags) { // 兼容 JSON 数组格式(AI 生成)和字符串格式(手动输入) try { @@ -288,7 +307,7 @@ export default function PublishPage() { return videos.filter((v) => v.name.toLowerCase().includes(query)); }, [videos, videoFilter]); - return ( + return (
setPreviewVideoUrl(null)} @@ -515,13 +534,28 @@ export default function PublishPage() { - setTitle(e.target.value)} - placeholder="输入视频标题..." - className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500" - /> + { + 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); + }} + 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/components/home/MaterialSelector.tsx b/frontend/src/components/home/MaterialSelector.tsx index 18e928a..94fa1c0 100644 --- a/frontend/src/components/home/MaterialSelector.tsx +++ b/frontend/src/components/home/MaterialSelector.tsx @@ -46,7 +46,7 @@ export function MaterialSelector({

- 📹 选择素材视频 + 📹 视频素材 (上传自拍视频) @@ -112,9 +112,9 @@ export function MaterialSelector({ ) : materials.length === 0 ? (
📁
-

暂无素材视频

+

暂无视频素材

- 点击上方「📤 上传视频」按钮添加素材 + 点击上方「📤 上传视频」按钮添加视频素材

) : ( diff --git a/frontend/src/components/home/TitleSubtitlePanel.tsx b/frontend/src/components/home/TitleSubtitlePanel.tsx index c27f3e9..1e922fb 100644 --- a/frontend/src/components/home/TitleSubtitlePanel.tsx +++ b/frontend/src/components/home/TitleSubtitlePanel.tsx @@ -36,6 +36,8 @@ interface TitleSubtitlePanelProps { onTogglePreview: () => void; videoTitle: string; onTitleChange: (value: string) => void; + onTitleCompositionStart?: () => void; + onTitleCompositionEnd?: (value: string) => void; titleStyles: TitleStyleOption[]; selectedTitleStyleId: string; onSelectTitleStyle: (id: string) => void; @@ -63,6 +65,8 @@ export function TitleSubtitlePanel({ onTogglePreview, videoTitle, onTitleChange, + onTitleCompositionStart, + onTitleCompositionEnd, titleStyles, selectedTitleStyleId, onSelectTitleStyle, @@ -209,11 +213,13 @@ export function TitleSubtitlePanel({ )}
- + onTitleChange(e.target.value)} + onCompositionStart={onTitleCompositionStart} + onCompositionEnd={(e) => onTitleCompositionEnd?.(e.currentTarget.value)} placeholder="输入视频标题,将在片头显示" className="w-full px-3 sm:px-4 py-2 text-sm sm:text-base bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors" /> diff --git a/frontend/src/components/home/VoiceSelector.tsx b/frontend/src/components/home/VoiceSelector.tsx index 4cc325d..9dd5e5e 100644 --- a/frontend/src/components/home/VoiceSelector.tsx +++ b/frontend/src/components/home/VoiceSelector.tsx @@ -26,7 +26,7 @@ export function VoiceSelector({ return (

- 🎙️ 选择配音方式 + 🎙️ 配音方式

diff --git a/frontend/src/hooks/useHomePersistence.ts b/frontend/src/hooks/useHomePersistence.ts index 0d14fff..5c59d45 100644 --- a/frontend/src/hooks/useHomePersistence.ts +++ b/frontend/src/hooks/useHomePersistence.ts @@ -1,5 +1,8 @@ import { useEffect, useState } from "react"; +const TITLE_MAX_LENGTH = 15; +const clampTitle = (value: string) => value.slice(0, TITLE_MAX_LENGTH); + interface RefAudio { id: string; name: string; @@ -101,7 +104,7 @@ export const useHomePersistence = ({ const savedEnableBgm = localStorage.getItem(`vigent_${storageKey}_enableBgm`); setText(savedText || "大家好,欢迎来到我的频道,今天给大家分享一些有趣的内容。"); - setVideoTitle(savedTitle || ""); + setVideoTitle(savedTitle ? clampTitle(savedTitle) : ""); setEnableSubtitles(savedSubtitles !== null ? savedSubtitles === 'true' : true); setTtsMode((savedTtsMode as 'edgetts' | 'voiceclone') || 'edgetts'); setVoice(savedVoice || "zh-CN-YunxiNeural");