From 7a3366fb2e05cb37908dc38a0397d5f93c16799b Mon Sep 17 00:00:00 2001 From: moux1024 <403053463@qq.com> Date: Tue, 23 Sep 2025 17:05:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E5=85=A5=E5=8F=A3=E5=92=8C=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interaction/templateStoryService.ts | 1 - components/ChatInputBox/ChatInputBox.tsx | 74 ++++++++++++++++--- components/ChatInputBox/H5TemplateDrawer.tsx | 71 ++++++++++++++++-- components/ChatInputBox/PcTemplateModal.tsx | 70 +++++++++++++++--- components/pages/create-to-video2.tsx | 2 +- 5 files changed, 189 insertions(+), 29 deletions(-) diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts index 7a35770..03bc35c 100644 --- a/app/service/Interaction/templateStoryService.ts +++ b/app/service/Interaction/templateStoryService.ts @@ -94,7 +94,6 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { setTemplateStoryList(templates); setSelectedTemplate(templates[0]); - console.log(selectedTemplate); } catch (err) { console.error("获取模板列表失败:", err); } finally { diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx index 5fa4d55..94092e0 100644 --- a/components/ChatInputBox/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -34,7 +34,6 @@ import { StoryTemplateEntity } from "@/app/service/domain/Entities"; import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService"; import TemplateCard from "./templateCard"; import { AudioRecorder } from "./AudioRecorder"; -import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"; import { useRouter } from "next/navigation"; import { createMovieProjectV1 } from "@/api/video_flow"; import { @@ -51,6 +50,7 @@ import { H5TemplateDrawer } from "./H5TemplateDrawer"; import { PcPhotoStoryModal } from "./PcPhotoStoryModal"; import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer"; import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector"; +import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"; const LauguageOptions = [ { value: "english", label: "English", isVip: false, code:'EN' }, @@ -86,7 +86,7 @@ const VideoDurationOptions = [ * @returns {Function} - 防抖后的函数 */ const debounce = (func: Function, wait: number) => { - let timeout: NodeJS.Timeout; + let timeout: ReturnType; return function executedFunction(...args: any[]) { const later = () => { clearTimeout(timeout); @@ -108,6 +108,14 @@ export function ChatInputBox({ noData }: { noData: boolean }) { // 模板故事弹窗状态 const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false); + // 模板快捷入口:记录初始模板ID与是否自动聚焦 + const [initialTemplateId, setInitialTemplateId] = useState(undefined); + // 复用模板服务:获取模板列表 + const { + templateStoryList, + isLoading: isTemplateLoading, + getTemplateStoryList, + } = useTemplateStoryServiceHook(); // 图片故事弹窗状态 const [isPhotoStoryModalOpen, setIsPhotoStoryModalOpen] = useState(false); @@ -164,19 +172,19 @@ export function ChatInputBox({ noData }: { noData: boolean }) { }, []); const onConfigChange = (key: K, value: ConfigOptions[K]) => { - setConfigOptions((prev) => ({ + setConfigOptions((prev: ConfigOptions) => ({ ...prev, [key]: value, })); if (key === 'videoDuration') { // 当选择 8s 时,强制关闭剧本扩展并禁用开关 if (value === '8s') { - setConfigOptions((prev) => ({ + setConfigOptions((prev: ConfigOptions) => ({ ...prev, expansion_mode: false, })); } else { - setConfigOptions((prev) => ({ + setConfigOptions((prev: ConfigOptions) => ({ ...prev, expansion_mode: true, })); @@ -184,6 +192,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) { } }; + useEffect(() => { + if (!templateStoryList || templateStoryList.length === 0) { + getTemplateStoryList(); + } + }, []); + const handleCreateVideo = async () => { if (isCreating) return; // 如果正在创建中,直接返回 @@ -279,7 +293,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { {/* 输入框和Action按钮 - 只在展开状态显示 */} {!isExpanded && ( -
+
{/* 第一行:输入框 */}
{/* 文本输入框 - 改为textarea */} @@ -335,7 +349,10 @@ export function ChatInputBox({ noData }: { noData: boolean }) { @@ -400,7 +417,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
), })), - onClick: ({ key }) => onConfigChange('language', key), + onClick: ({ key }: { key: string }) => onConfigChange('language', key), }} trigger={["click"]} placement="top" @@ -426,7 +443,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { size="small" checked={configOptions.expansion_mode} disabled={configOptions.videoDuration === '8s'} - onChange={(checked) => onConfigChange('expansion_mode', checked)} + onChange={(checked: boolean) => onConfigChange('expansion_mode', checked)} />
), })), - onClick: ({ key }) => onConfigChange('videoDuration', key as string), + onClick: ({ key }: { key: string }) => onConfigChange('videoDuration', key as string), }} trigger={["click"]} placement="top" @@ -490,6 +507,41 @@ export function ChatInputBox({ noData }: { noData: boolean }) { height={isMobile ? "h-10" : "h-12"} /> + + {/* 第三行:模板快捷入口水平滚动,超出渐隐遮挡(懒加载+骨架屏) */} +
+
+ {isTemplateLoading && (!templateStoryList || templateStoryList.length === 0) ? ( + // 骨架屏:若正在加载且没有数据 + Array.from({ length: 6 }).map((_, idx) => ( +
+ )) + ) : ( + (templateStoryList || []).map((tpl) => ( + + )) + )} +
+
)} @@ -501,6 +553,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { configOptions={configOptions} isOpen={isTemplateModalOpen} onClose={() => setIsTemplateModalOpen(false)} + initialTemplateId={initialTemplateId} isTemplateCreating={isTemplateCreating} setIsTemplateCreating={setIsTemplateCreating} isRoleGenerating={isRoleGenerating} @@ -514,6 +567,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) { configOptions={configOptions} isOpen={isTemplateModalOpen} onClose={() => setIsTemplateModalOpen(false)} + initialTemplateId={initialTemplateId} isTemplateCreating={isTemplateCreating} setIsTemplateCreating={setIsTemplateCreating} isRoleGenerating={isRoleGenerating} diff --git a/components/ChatInputBox/H5TemplateDrawer.tsx b/components/ChatInputBox/H5TemplateDrawer.tsx index 562b406..176ad32 100644 --- a/components/ChatInputBox/H5TemplateDrawer.tsx +++ b/components/ChatInputBox/H5TemplateDrawer.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { Drawer, Tooltip, Upload, Image } from "antd"; +import type { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface'; import { UploadOutlined } from "@ant-design/icons"; import { Clapperboard, Sparkles, LayoutTemplate, ChevronDown, ChevronUp, CheckCircle2 } from "lucide-react"; import { useRouter } from "next/navigation"; @@ -32,6 +33,8 @@ interface H5TemplateDrawerProps { ) => void; isOpen: boolean; onClose: () => void; + /** 指定初始选中的模板ID,用于从外部快速定位 */ + initialTemplateId?: string; configOptions: { mode: "auto" | "manual"; resolution: "720p" | "1080p" | "4k"; @@ -50,6 +53,7 @@ export const H5TemplateDrawer = ({ setIsItemGenerating, isOpen, onClose, + initialTemplateId, configOptions, }: H5TemplateDrawerProps) => { const router = useRouter(); @@ -76,6 +80,8 @@ export const H5TemplateDrawer = ({ // 自由输入框布局 const [freeInputLayout, setFreeInputLayout] = useState('bottom'); const [aspectUI, setAspectUI] = useState("VIDEO_ASPECT_RATIO_LANDSCAPE"); + // 顶部列表所在的实际滚动容器(外层 top-section 才是滚动容器) + const topSectionRef = useRef(null); // 自由输入框布局 useEffect(() => { @@ -94,6 +100,56 @@ export const H5TemplateDrawer = ({ } }, [isOpen, getTemplateStoryList]); + // 当列表加载后,根据 initialTemplateId 自动选中 + useEffect(() => { + if (!isOpen) return; + if (!initialTemplateId) return; + if (!templateStoryList || templateStoryList.length === 0) return; + const target = templateStoryList.find(t => t.id === initialTemplateId || t.template_id === initialTemplateId); + if (target) { + setSelectedTemplate(target); + } + }, [isOpen, initialTemplateId, templateStoryList, setSelectedTemplate]); + + // 自动聚焦可编辑输入框 + useEffect(() => { + if (!isOpen) return; + if (!selectedTemplate) return; + const timer = setTimeout(() => { + const topTextArea = document.querySelector('textarea[data-alt="h5-template-free-input-top"]') as HTMLTextAreaElement | null; + const bottomInput = document.querySelector('input[data-alt="h5-template-free-input-bottom"]') as HTMLInputElement | null; + if (freeInputLayout === 'top' && topTextArea) { + topTextArea.focus(); + } else if (freeInputLayout === 'bottom' && bottomInput) { + bottomInput.focus(); + } + }, 50); + return () => clearTimeout(timer); + }, [isOpen, selectedTemplate, freeInputLayout]); + + // 当存在默认选中模板时,将其滚动到顶部(以外层 top-section 为滚动容器) + useEffect(() => { + if (!isOpen) return; + if (!selectedTemplate) return; + const container = topSectionRef.current; + if (!container) return; + // 延迟一帧确保子节点渲染 + const tid = setTimeout(() => { + const targetId = (selectedTemplate as any).id || (selectedTemplate as any).template_id; + const el = container.querySelector(`[data-template-id="${targetId}"]`) as HTMLElement | null; + if (el) { + // 计算相对容器的 offsetTop + const containerTop = container.getBoundingClientRect().top; + const elTop = el.getBoundingClientRect().top; + const offset = elTop - containerTop + container.scrollTop; + const adjust = 16; // 向下偏移一些,让目标项不贴顶 + const targetTop = Math.max(0, offset - adjust); + container.scrollTo({ top: targetTop, behavior: 'smooth' }); + } + }, 0); + return () => clearTimeout(tid); + }, [isOpen, selectedTemplate]); + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; @@ -108,7 +164,7 @@ export const H5TemplateDrawer = ({ const handleConfirm = async () => { if (!selectedTemplate || isTemplateCreating) return; setIsTemplateCreating(true); - let timer: NodeJS.Timeout | null = null; + let timer: ReturnType | null = null; try { const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); if (!User.id) return; @@ -155,6 +211,7 @@ export const H5TemplateDrawer = ({