forked from 77media/video-flow
新增 模板快捷入口和定位
This commit is contained in:
parent
7a7339e6e4
commit
7a3366fb2e
@ -94,7 +94,6 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
|||||||
|
|
||||||
setTemplateStoryList(templates);
|
setTemplateStoryList(templates);
|
||||||
setSelectedTemplate(templates[0]);
|
setSelectedTemplate(templates[0]);
|
||||||
console.log(selectedTemplate);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("获取模板列表失败:", err);
|
console.error("获取模板列表失败:", err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
|||||||
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
||||||
import TemplateCard from "./templateCard";
|
import TemplateCard from "./templateCard";
|
||||||
import { AudioRecorder } from "./AudioRecorder";
|
import { AudioRecorder } from "./AudioRecorder";
|
||||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||||
import {
|
import {
|
||||||
@ -51,6 +50,7 @@ import { H5TemplateDrawer } from "./H5TemplateDrawer";
|
|||||||
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
|
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
|
||||||
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
|
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
|
||||||
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
||||||
|
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||||
|
|
||||||
const LauguageOptions = [
|
const LauguageOptions = [
|
||||||
{ value: "english", label: "English", isVip: false, code:'EN' },
|
{ value: "english", label: "English", isVip: false, code:'EN' },
|
||||||
@ -86,7 +86,7 @@ const VideoDurationOptions = [
|
|||||||
* @returns {Function} - 防抖后的函数
|
* @returns {Function} - 防抖后的函数
|
||||||
*/
|
*/
|
||||||
const debounce = (func: Function, wait: number) => {
|
const debounce = (func: Function, wait: number) => {
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
return function executedFunction(...args: any[]) {
|
return function executedFunction(...args: any[]) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@ -108,6 +108,14 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
|
|
||||||
// 模板故事弹窗状态
|
// 模板故事弹窗状态
|
||||||
const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
|
const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
|
||||||
|
// 模板快捷入口:记录初始模板ID与是否自动聚焦
|
||||||
|
const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined);
|
||||||
|
// 复用模板服务:获取模板列表
|
||||||
|
const {
|
||||||
|
templateStoryList,
|
||||||
|
isLoading: isTemplateLoading,
|
||||||
|
getTemplateStoryList,
|
||||||
|
} = useTemplateStoryServiceHook();
|
||||||
|
|
||||||
// 图片故事弹窗状态
|
// 图片故事弹窗状态
|
||||||
const [isPhotoStoryModalOpen, setIsPhotoStoryModalOpen] = useState(false);
|
const [isPhotoStoryModalOpen, setIsPhotoStoryModalOpen] = useState(false);
|
||||||
@ -164,19 +172,19 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onConfigChange = <K extends keyof ConfigOptions>(key: K, value: ConfigOptions[K]) => {
|
const onConfigChange = <K extends keyof ConfigOptions>(key: K, value: ConfigOptions[K]) => {
|
||||||
setConfigOptions((prev) => ({
|
setConfigOptions((prev: ConfigOptions) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
}));
|
}));
|
||||||
if (key === 'videoDuration') {
|
if (key === 'videoDuration') {
|
||||||
// 当选择 8s 时,强制关闭剧本扩展并禁用开关
|
// 当选择 8s 时,强制关闭剧本扩展并禁用开关
|
||||||
if (value === '8s') {
|
if (value === '8s') {
|
||||||
setConfigOptions((prev) => ({
|
setConfigOptions((prev: ConfigOptions) => ({
|
||||||
...prev,
|
...prev,
|
||||||
expansion_mode: false,
|
expansion_mode: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setConfigOptions((prev) => ({
|
setConfigOptions((prev: ConfigOptions) => ({
|
||||||
...prev,
|
...prev,
|
||||||
expansion_mode: true,
|
expansion_mode: true,
|
||||||
}));
|
}));
|
||||||
@ -184,6 +192,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!templateStoryList || templateStoryList.length === 0) {
|
||||||
|
getTemplateStoryList();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCreateVideo = async () => {
|
const handleCreateVideo = async () => {
|
||||||
if (isCreating) return; // 如果正在创建中,直接返回
|
if (isCreating) return; // 如果正在创建中,直接返回
|
||||||
|
|
||||||
@ -279,7 +293,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
|
|
||||||
{/* 输入框和Action按钮 - 只在展开状态显示 */}
|
{/* 输入框和Action按钮 - 只在展开状态显示 */}
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div data-alt="chat-input-box" className="flex flex-col gap-3 w-full">
|
||||||
{/* 第一行:输入框 */}
|
{/* 第一行:输入框 */}
|
||||||
<div className="video-prompt-editor relative flex flex-col gap-3 flex-1 pr-10">
|
<div className="video-prompt-editor relative flex flex-col gap-3 flex-1 pr-10">
|
||||||
{/* 文本输入框 - 改为textarea */}
|
{/* 文本输入框 - 改为textarea */}
|
||||||
@ -335,7 +349,10 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
<button
|
<button
|
||||||
data-alt="template-story-button"
|
data-alt="template-story-button"
|
||||||
className="flex items-center gap-1.5 px-2 py-2 text-white/[0.70] hover:text-white transition-colors"
|
className="flex items-center gap-1.5 px-2 py-2 text-white/[0.70] hover:text-white transition-colors"
|
||||||
onClick={() => setIsTemplateModalOpen(true)}
|
onClick={() => {
|
||||||
|
setInitialTemplateId(undefined);
|
||||||
|
setIsTemplateModalOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LayoutTemplate className="w-4 h-4" />
|
<LayoutTemplate className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -400,7 +417,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
onClick: ({ key }) => onConfigChange('language', key),
|
onClick: ({ key }: { key: string }) => onConfigChange('language', key),
|
||||||
}}
|
}}
|
||||||
trigger={["click"]}
|
trigger={["click"]}
|
||||||
placement="top"
|
placement="top"
|
||||||
@ -426,7 +443,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
size="small"
|
size="small"
|
||||||
checked={configOptions.expansion_mode}
|
checked={configOptions.expansion_mode}
|
||||||
disabled={configOptions.videoDuration === '8s'}
|
disabled={configOptions.videoDuration === '8s'}
|
||||||
onChange={(checked) => onConfigChange('expansion_mode', checked)}
|
onChange={(checked: boolean) => onConfigChange('expansion_mode', checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-xs text-white hidden sm:inline`}>
|
<span className={`text-xs text-white hidden sm:inline`}>
|
||||||
@ -454,7 +471,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
onClick: ({ key }) => onConfigChange('videoDuration', key as string),
|
onClick: ({ key }: { key: string }) => onConfigChange('videoDuration', key as string),
|
||||||
}}
|
}}
|
||||||
trigger={["click"]}
|
trigger={["click"]}
|
||||||
placement="top"
|
placement="top"
|
||||||
@ -490,6 +507,41 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
height={isMobile ? "h-10" : "h-12"}
|
height={isMobile ? "h-10" : "h-12"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 第三行:模板快捷入口水平滚动,超出渐隐遮挡(懒加载+骨架屏) */}
|
||||||
|
<div data-alt="template-quick-entries" className="relative pl-2">
|
||||||
|
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide pr-6 py-1">
|
||||||
|
{isTemplateLoading && (!templateStoryList || templateStoryList.length === 0) ? (
|
||||||
|
// 骨架屏:若正在加载且没有数据
|
||||||
|
Array.from({ length: 6 }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={`skeleton-${idx}`}
|
||||||
|
data-alt={`template-chip-skeleton-${idx}`}
|
||||||
|
className="flex-shrink-0 w-20 h-7 rounded-full bg-white/10 animate-pulse"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
(templateStoryList || []).map((tpl) => (
|
||||||
|
<button
|
||||||
|
key={tpl.id}
|
||||||
|
data-alt={`template-chip-${tpl.id}`}
|
||||||
|
className="flex-shrink-0 px-3 py-1.5 rounded-full bg-white/10 hover:bg-white/20 text-white/80 hover:text-white text-xs transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
// id 映射:优先使用模板的 id;若需要兼容 template_id,则传两者之一
|
||||||
|
setInitialTemplateId(tpl.id || (tpl as any).template_id);
|
||||||
|
setIsTemplateModalOpen(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
const textarea = document.querySelector('textarea');
|
||||||
|
if (textarea) (textarea as HTMLTextAreaElement).focus();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tpl.name}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -501,6 +553,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
configOptions={configOptions}
|
configOptions={configOptions}
|
||||||
isOpen={isTemplateModalOpen}
|
isOpen={isTemplateModalOpen}
|
||||||
onClose={() => setIsTemplateModalOpen(false)}
|
onClose={() => setIsTemplateModalOpen(false)}
|
||||||
|
initialTemplateId={initialTemplateId}
|
||||||
isTemplateCreating={isTemplateCreating}
|
isTemplateCreating={isTemplateCreating}
|
||||||
setIsTemplateCreating={setIsTemplateCreating}
|
setIsTemplateCreating={setIsTemplateCreating}
|
||||||
isRoleGenerating={isRoleGenerating}
|
isRoleGenerating={isRoleGenerating}
|
||||||
@ -514,6 +567,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
|
|||||||
configOptions={configOptions}
|
configOptions={configOptions}
|
||||||
isOpen={isTemplateModalOpen}
|
isOpen={isTemplateModalOpen}
|
||||||
onClose={() => setIsTemplateModalOpen(false)}
|
onClose={() => setIsTemplateModalOpen(false)}
|
||||||
|
initialTemplateId={initialTemplateId}
|
||||||
isTemplateCreating={isTemplateCreating}
|
isTemplateCreating={isTemplateCreating}
|
||||||
setIsTemplateCreating={setIsTemplateCreating}
|
setIsTemplateCreating={setIsTemplateCreating}
|
||||||
isRoleGenerating={isRoleGenerating}
|
isRoleGenerating={isRoleGenerating}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { Drawer, Tooltip, Upload, Image } from "antd";
|
import { Drawer, Tooltip, Upload, Image } from "antd";
|
||||||
|
import type { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface';
|
||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { Clapperboard, Sparkles, LayoutTemplate, ChevronDown, ChevronUp, CheckCircle2 } from "lucide-react";
|
import { Clapperboard, Sparkles, LayoutTemplate, ChevronDown, ChevronUp, CheckCircle2 } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -32,6 +33,8 @@ interface H5TemplateDrawerProps {
|
|||||||
) => void;
|
) => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** 指定初始选中的模板ID,用于从外部快速定位 */
|
||||||
|
initialTemplateId?: string;
|
||||||
configOptions: {
|
configOptions: {
|
||||||
mode: "auto" | "manual";
|
mode: "auto" | "manual";
|
||||||
resolution: "720p" | "1080p" | "4k";
|
resolution: "720p" | "1080p" | "4k";
|
||||||
@ -50,6 +53,7 @@ export const H5TemplateDrawer = ({
|
|||||||
setIsItemGenerating,
|
setIsItemGenerating,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
initialTemplateId,
|
||||||
configOptions,
|
configOptions,
|
||||||
}: H5TemplateDrawerProps) => {
|
}: H5TemplateDrawerProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -76,6 +80,8 @@ export const H5TemplateDrawer = ({
|
|||||||
// 自由输入框布局
|
// 自由输入框布局
|
||||||
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
||||||
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
||||||
|
// 顶部列表所在的实际滚动容器(外层 top-section 才是滚动容器)
|
||||||
|
const topSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// 自由输入框布局
|
// 自由输入框布局
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -94,6 +100,56 @@ export const H5TemplateDrawer = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, getTemplateStoryList]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as Element;
|
const target = event.target as Element;
|
||||||
@ -108,7 +164,7 @@ export const H5TemplateDrawer = ({
|
|||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (!selectedTemplate || isTemplateCreating) return;
|
if (!selectedTemplate || isTemplateCreating) return;
|
||||||
setIsTemplateCreating(true);
|
setIsTemplateCreating(true);
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
try {
|
try {
|
||||||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||||||
if (!User.id) return;
|
if (!User.id) return;
|
||||||
@ -155,6 +211,7 @@ export const H5TemplateDrawer = ({
|
|||||||
<button
|
<button
|
||||||
key={template.id}
|
key={template.id}
|
||||||
data-alt={`template-row-${index}`}
|
data-alt={`template-row-${index}`}
|
||||||
|
data-template-id={(template as any).id || (template as any).template_id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isBottomExpanded) setIsBottomExpanded(true);
|
if (!isBottomExpanded) setIsBottomExpanded(true);
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
@ -287,7 +344,7 @@ export const H5TemplateDrawer = ({
|
|||||||
if (!isLt5M) return false;
|
if (!isLt5M) return false;
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
customRequest={async ({ file, onSuccess, onError }) => {
|
customRequest={async ({ file, onSuccess, onError }: RcCustomRequestOptions) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
const uploadedUrl = await uploadFile(fileObj, () => {});
|
const uploadedUrl = await uploadFile(fileObj, () => {});
|
||||||
@ -414,7 +471,7 @@ export const H5TemplateDrawer = ({
|
|||||||
if (!isLt5M) return false;
|
if (!isLt5M) return false;
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
customRequest={async ({ file, onSuccess, onError }) => {
|
customRequest={async ({ file, onSuccess, onError }: RcCustomRequestOptions) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
const uploadedUrl = await uploadFile(fileObj, () => {});
|
const uploadedUrl = await uploadFile(fileObj, () => {});
|
||||||
@ -516,6 +573,7 @@ export const H5TemplateDrawer = ({
|
|||||||
input Configuration
|
input Configuration
|
||||||
</h3>
|
</h3>
|
||||||
<textarea
|
<textarea
|
||||||
|
data-alt="h5-template-free-input-top"
|
||||||
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
||||||
placeholder={selectedTemplate?.freeInput[0].user_tips || ""}
|
placeholder={selectedTemplate?.freeInput[0].user_tips || ""}
|
||||||
className="w-full flex-1 px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
className="w-full flex-1 px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||||||
@ -540,6 +598,7 @@ export const H5TemplateDrawer = ({
|
|||||||
<div data-alt="free-input" className="flex-1">
|
<div data-alt="free-input" className="flex-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
data-alt="h5-template-free-input-bottom"
|
||||||
value={selectedTemplate.freeInput[0].free_input_text || ""}
|
value={selectedTemplate.freeInput[0].free_input_text || ""}
|
||||||
placeholder={selectedTemplate.freeInput[0].user_tips}
|
placeholder={selectedTemplate.freeInput[0].user_tips}
|
||||||
className="w-full px-3 py-2 pr-12 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
className="w-full px-3 py-2 pr-12 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||||||
@ -559,7 +618,7 @@ export const H5TemplateDrawer = ({
|
|||||||
{/* 横/竖屏选择 */}
|
{/* 横/竖屏选择 */}
|
||||||
<AspectRatioSelector
|
<AspectRatioSelector
|
||||||
value={aspectUI}
|
value={aspectUI}
|
||||||
onChange={setAspectUI}
|
onChange={(v: AspectRatioValue) => setAspectUI(v)}
|
||||||
placement="top"
|
placement="top"
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
@ -604,7 +663,7 @@ export const H5TemplateDrawer = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-alt="drawer-body" className="flex-1 overflow-y-auto">
|
<div data-alt="drawer-body" className="flex-1 overflow-y-auto">
|
||||||
<div data-alt="top-section" className="h-full overflow-y-auto">
|
<div data-alt="top-section" className="h-full overflow-y-auto" ref={topSectionRef}>
|
||||||
{renderTopTemplateList()}
|
{renderTopTemplateList()}
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Clapperboard,
|
Clapperboard,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
import type { UploadRequestOption as RcCustomRequestOptions } from 'rc-upload/lib/interface';
|
||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||||
@ -30,7 +31,7 @@ import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
|||||||
* @returns {Function} - 防抖后的函数
|
* @returns {Function} - 防抖后的函数
|
||||||
*/
|
*/
|
||||||
const debounce = (func: Function, wait: number) => {
|
const debounce = (func: Function, wait: number) => {
|
||||||
let timeout: NodeJS.Timeout;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
return function executedFunction(...args: any[]) {
|
return function executedFunction(...args: any[]) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@ -50,6 +51,8 @@ interface PcTemplateModalProps {
|
|||||||
setIsItemGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void;
|
setIsItemGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** 指定初始选中的模板ID,用于从外部快速定位 */
|
||||||
|
initialTemplateId?: string;
|
||||||
configOptions: {
|
configOptions: {
|
||||||
mode: "auto" | "manual";
|
mode: "auto" | "manual";
|
||||||
resolution: "720p" | "1080p" | "4k";
|
resolution: "720p" | "1080p" | "4k";
|
||||||
@ -70,6 +73,7 @@ export const PcTemplateModal = ({
|
|||||||
setIsItemGenerating,
|
setIsItemGenerating,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
initialTemplateId,
|
||||||
configOptions = {
|
configOptions = {
|
||||||
mode: "auto" as "auto" | "manual",
|
mode: "auto" as "auto" | "manual",
|
||||||
resolution: "720p" as "720p" | "1080p" | "4k",
|
resolution: "720p" as "720p" | "1080p" | "4k",
|
||||||
@ -105,7 +109,7 @@ export const PcTemplateModal = ({
|
|||||||
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
||||||
|
const leftListRef = useRef<HTMLDivElement | null>(null);
|
||||||
// 组件挂载时获取模板列表
|
// 组件挂载时获取模板列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -113,6 +117,17 @@ export const PcTemplateModal = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, getTemplateStoryList]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (selectedTemplate?.storyRole && selectedTemplate.storyRole.length > 0 ||
|
if (selectedTemplate?.storyRole && selectedTemplate.storyRole.length > 0 ||
|
||||||
@ -124,6 +139,36 @@ export const PcTemplateModal = ({
|
|||||||
}
|
}
|
||||||
}, [selectedTemplate])
|
}, [selectedTemplate])
|
||||||
|
|
||||||
|
// 自动聚焦可编辑输入框
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
if (!selectedTemplate) return;
|
||||||
|
// 略微延迟确保 DOM 更新
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const topTextArea = document.querySelector('textarea[data-alt="pc-template-free-input-top"]') as HTMLTextAreaElement | null;
|
||||||
|
const bottomInput = document.querySelector('input[data-alt="pc-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]);
|
||||||
|
|
||||||
|
// 当存在默认选中模板时,将其滚动到顶部
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
if (!selectedTemplate) return;
|
||||||
|
const container = leftListRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const targetId = (selectedTemplate as any).id || (selectedTemplate as any).template_id;
|
||||||
|
const el = container.querySelector(`[data-template-id="${targetId}"]`) as HTMLElement | null;
|
||||||
|
if (el) {
|
||||||
|
container.scrollTo({ top: el.offsetTop - 90, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, [isOpen, selectedTemplate]);
|
||||||
|
|
||||||
// 监听点击外部区域关闭输入框
|
// 监听点击外部区域关闭输入框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@ -155,7 +200,7 @@ export const PcTemplateModal = ({
|
|||||||
if (isTemplateCreating) return;
|
if (isTemplateCreating) return;
|
||||||
|
|
||||||
setIsTemplateCreating(true);
|
setIsTemplateCreating(true);
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取当前用户信息
|
// 获取当前用户信息
|
||||||
@ -167,8 +212,8 @@ export const PcTemplateModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动进度条动画
|
// 启动进度条动画
|
||||||
timer = setInterval(() => {
|
timer = setInterval((): void => {
|
||||||
setLocalLoading((prev) => {
|
setLocalLoading((prev: number) => {
|
||||||
if (prev >= 95) {
|
if (prev >= 95) {
|
||||||
return 95;
|
return 95;
|
||||||
}
|
}
|
||||||
@ -210,12 +255,13 @@ export const PcTemplateModal = ({
|
|||||||
// 模板列表渲染
|
// 模板列表渲染
|
||||||
const templateListRender = () => {
|
const templateListRender = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-1/3 p-4 border-r border-white/[0.1] overflow-y-auto">
|
<div className="w-1/3 p-4 border-r border-white/[0.1] overflow-y-auto" ref={leftListRef}>
|
||||||
<div className="space-y-4 overflow-y-auto template-list-scroll">
|
<div className="space-y-4 overflow-y-auto template-list-scroll">
|
||||||
{templateStoryList.map((template, index) => (
|
{templateStoryList.map((template, index) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
data-alt={`template-card-${index}`}
|
data-alt={`template-card-${index}`}
|
||||||
|
data-template-id={(template as any).id || (template as any).template_id}
|
||||||
className="flex justify-center"
|
className="flex justify-center"
|
||||||
onClick={() => handleTemplateSelect(template)}
|
onClick={() => handleTemplateSelect(template)}
|
||||||
>
|
>
|
||||||
@ -429,12 +475,12 @@ export const PcTemplateModal = ({
|
|||||||
file,
|
file,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
}) => {
|
}: RcCustomRequestOptions) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
const uploadedUrl = await uploadFile(
|
const uploadedUrl = await uploadFile(
|
||||||
fileObj,
|
fileObj,
|
||||||
(progress) => {
|
(progress: number) => {
|
||||||
console.log(`上传进度: ${progress}%`);
|
console.log(`上传进度: ${progress}%`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -620,12 +666,12 @@ export const PcTemplateModal = ({
|
|||||||
file,
|
file,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
}) => {
|
}: RcCustomRequestOptions) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
const uploadedUrl = await uploadFile(
|
const uploadedUrl = await uploadFile(
|
||||||
fileObj,
|
fileObj,
|
||||||
(progress) => {
|
(progress: number) => {
|
||||||
console.log(`上传进度: ${progress}%`);
|
console.log(`上传进度: ${progress}%`);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -666,6 +712,7 @@ export const PcTemplateModal = ({
|
|||||||
input Configuration
|
input Configuration
|
||||||
</h3>
|
</h3>
|
||||||
<textarea
|
<textarea
|
||||||
|
data-alt="pc-template-free-input-top"
|
||||||
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
||||||
placeholder={selectedTemplate?.freeInput[0].user_tips || ""}
|
placeholder={selectedTemplate?.freeInput[0].user_tips || ""}
|
||||||
className="w-full flex-1 px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
className="w-full flex-1 px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||||||
@ -690,6 +737,7 @@ export const PcTemplateModal = ({
|
|||||||
<div className="py-2 flex-1">
|
<div className="py-2 flex-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
data-alt="pc-template-free-input-bottom"
|
||||||
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
||||||
placeholder={selectedTemplate?.freeInput[0].user_tips}
|
placeholder={selectedTemplate?.freeInput[0].user_tips}
|
||||||
className="w-full px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
className="w-full px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||||||
|
|||||||
@ -191,7 +191,7 @@ export default function CreateToVideo2() {
|
|||||||
const renderProjectCard = (project: any) => {
|
const renderProjectCard = (project: any) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LazyLoad once>
|
<LazyLoad key={project.project_id} once>
|
||||||
<div
|
<div
|
||||||
key={project.project_id}
|
key={project.project_id}
|
||||||
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
|
className="group flex flex-col bg-black/20 rounded-lg overflow-hidden cursor-pointer hover:bg-white/5 transition-all duration-300"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user