更新
This commit is contained in:
@@ -27,9 +27,6 @@
|
||||
- `frontend/src/app/page.tsx`
|
||||
- `frontend/src/hooks/useHomePersistence.ts`
|
||||
|
||||
---
|
||||
|
||||
## 🧩 前端 UI 拆分 (11:00)
|
||||
## 🧩 前端 UI 拆分 (09:10)
|
||||
|
||||
### 内容
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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<string>("");
|
||||
const isTitleComposingRef = useRef(false);
|
||||
const titleCommittedRef = useRef("");
|
||||
const [enableSubtitles, setEnableSubtitles] = useState<boolean>(true);
|
||||
const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState<string>("");
|
||||
const [selectedTitleStyleId, setSelectedTitleStyleId] = useState<string>("");
|
||||
@@ -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}
|
||||
|
||||
@@ -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<Account[]>([]);
|
||||
const [videos, setVideos] = useState<Video[]>([]);
|
||||
const [selectedVideo, setSelectedVideo] = useState<string>("");
|
||||
const [videoFilter, setVideoFilter] = useState<string>("");
|
||||
const [previewVideoUrl, setPreviewVideoUrl] = useState<string | null>(null);
|
||||
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [tags, setTags] = useState<string>("");
|
||||
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 [publishResults, setPublishResults] = useState<any[]>([]);
|
||||
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 (
|
||||
<div className="min-h-dvh">
|
||||
<VideoPreviewModal
|
||||
onClose={() => setPreviewVideoUrl(null)}
|
||||
@@ -515,13 +534,28 @@ export default function PublishPage() {
|
||||
<label className="block text-gray-400 text-sm mb-2">
|
||||
标题
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="输入视频标题..."
|
||||
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500"
|
||||
/>
|
||||
<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);
|
||||
}}
|
||||
placeholder="输入视频标题..."
|
||||
className="w-full p-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-gray-400 text-sm mb-2">
|
||||
@@ -663,5 +697,5 @@ export default function PublishPage() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function MaterialSelector({
|
||||
<div className="bg-white/5 rounded-2xl p-4 sm:p-6 border border-white/10 backdrop-blur-sm">
|
||||
<div className="flex justify-between items-center gap-2 mb-4">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2 whitespace-nowrap">
|
||||
📹 选择素材视频
|
||||
📹 视频素材
|
||||
<span className="ml-1 text-[11px] sm:text-xs text-gray-400/90 font-normal">
|
||||
(上传自拍视频)
|
||||
</span>
|
||||
@@ -112,9 +112,9 @@ export function MaterialSelector({
|
||||
) : materials.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<div className="text-5xl mb-4">📁</div>
|
||||
<p>暂无素材视频</p>
|
||||
<p>暂无视频素材</p>
|
||||
<p className="text-sm mt-2">
|
||||
点击上方「📤 上传视频」按钮添加素材
|
||||
点击上方「📤 上传视频」按钮添加视频素材
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="text-sm text-gray-300 mb-2 block">片头标题(可选)</label>
|
||||
<label className="text-sm text-gray-300 mb-2 block">片头标题(限制15个字)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={videoTitle}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function VoiceSelector({
|
||||
return (
|
||||
<div className="bg-white/5 rounded-2xl p-6 border border-white/10 backdrop-blur-sm">
|
||||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
🎙️ 选择配音方式
|
||||
🎙️ 配音方式
|
||||
</h2>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user