forked from 77media/video-flow
样式再调整
This commit is contained in:
parent
a4a41da618
commit
8fa0adfdc4
16
app/ScreenAdapter.tsx
Normal file
16
app/ScreenAdapter.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { createScreenAdapter } from '@/utils/tools';
|
||||||
|
|
||||||
|
export function ScreenAdapter() {
|
||||||
|
useEffect(() => {
|
||||||
|
// 页面加载时应用
|
||||||
|
window.addEventListener('load', createScreenAdapter)
|
||||||
|
|
||||||
|
// 窗口大小改变时重新应用
|
||||||
|
window.addEventListener('resize', createScreenAdapter)
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null; // 这个组件不渲染任何内容
|
||||||
|
}
|
||||||
@ -2,6 +2,9 @@ import './globals.css';
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Providers } from '@/components/providers';
|
import { Providers } from '@/components/providers';
|
||||||
import { ConfigProvider, theme } from 'antd';
|
import { ConfigProvider, theme } from 'antd';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { createScreenAdapter } from '@/utils/tools';
|
||||||
|
import { ScreenAdapter } from './ScreenAdapter';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'AI Movie Flow - Create Amazing Videos with AI',
|
title: 'AI Movie Flow - Create Amazing Videos with AI',
|
||||||
@ -29,6 +32,7 @@ export default function RootLayout({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Providers>
|
<Providers>
|
||||||
|
<ScreenAdapter />
|
||||||
{children}
|
{children}
|
||||||
</Providers>
|
</Providers>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@ -84,7 +84,9 @@ const RenderTemplateStoryMode = ({
|
|||||||
// 本地加载状态,用于 UI 反馈
|
// 本地加载状态,用于 UI 反馈
|
||||||
const [localLoading, setLocalLoading] = useState(0);
|
const [localLoading, setLocalLoading] = useState(0);
|
||||||
// 控制输入框显示状态
|
// 控制输入框显示状态
|
||||||
const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>({});
|
const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// 组件挂载时获取模板列表
|
// 组件挂载时获取模板列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -98,15 +100,18 @@ const RenderTemplateStoryMode = ({
|
|||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as Element;
|
const target = event.target as Element;
|
||||||
// 检查是否点击了输入框相关的元素
|
// 检查是否点击了输入框相关的元素
|
||||||
if (!target.closest('.ant-tooltip') && !target.closest('[data-alt*="field-ai-button"]')) {
|
if (
|
||||||
|
!target.closest(".ant-tooltip") &&
|
||||||
|
!target.closest('[data-alt*="field-ai-button"]')
|
||||||
|
) {
|
||||||
// 关闭所有打开的输入框
|
// 关闭所有打开的输入框
|
||||||
setInputVisible({});
|
setInputVisible({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -164,7 +169,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
const templateListRender = () => {
|
const templateListRender = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-1/3 p-4 border-r border-white/[0.1]">
|
<div className="w-1/3 p-4 border-r border-white/[0.1]">
|
||||||
<div className="space-y-4 max-h-[700px] overflow-y-auto pr-3 template-list-scroll">
|
<div className="space-y-4 max-h-[700px] overflow-y-auto template-list-scroll">
|
||||||
{templateStoryList.map((template, index) => (
|
{templateStoryList.map((template, index) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
@ -195,17 +200,11 @@ const RenderTemplateStoryMode = ({
|
|||||||
<div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]">
|
<div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]">
|
||||||
{/* 左侧图片 */}
|
{/* 左侧图片 */}
|
||||||
<div className="w-1/4">
|
<div className="w-1/4">
|
||||||
<div
|
<Image
|
||||||
data-alt="template-preview-image"
|
src={selectedTemplate.image_url[0]}
|
||||||
className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer"
|
alt={selectedTemplate.name}
|
||||||
>
|
className="w-4 h-5 !object-contain transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
||||||
<img
|
/>
|
||||||
src={selectedTemplate.image_url[0]}
|
|
||||||
alt={selectedTemplate.name}
|
|
||||||
className="w-full h-full object-contain transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧信息 - 增加文本渲染空间 */}
|
{/* 右侧信息 - 增加文本渲染空间 */}
|
||||||
@ -217,7 +216,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
{selectedTemplate.name}
|
{selectedTemplate.name}
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-y-auto max-h-96 pr-3"
|
className="flex-1 overflow-y-auto max-h-96 "
|
||||||
style={{
|
style={{
|
||||||
scrollbarWidth: "thin",
|
scrollbarWidth: "thin",
|
||||||
scrollbarColor: "rgba(156,163,175,0.2) rgba(0,0,0,0)",
|
scrollbarColor: "rgba(156,163,175,0.2) rgba(0,0,0,0)",
|
||||||
@ -235,7 +234,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
{/* 变量字段填写区域 */}
|
{/* 变量字段填写区域 */}
|
||||||
{selectedTemplate?.fillable_content &&
|
{selectedTemplate?.fillable_content &&
|
||||||
selectedTemplate.fillable_content.length > 0 && (
|
selectedTemplate.fillable_content.length > 0 && (
|
||||||
<div className="p-4 border-t border-white/10">
|
<div className="pt-2 border-t border-white/10">
|
||||||
<h3
|
<h3
|
||||||
data-alt="variables-section-title"
|
data-alt="variables-section-title"
|
||||||
className="text-lg font-semibold text-white mb-4"
|
className="text-lg font-semibold text-white mb-4"
|
||||||
@ -275,7 +274,10 @@ const RenderTemplateStoryMode = ({
|
|||||||
field.field_name,
|
field.field_name,
|
||||||
field.value || ""
|
field.value || ""
|
||||||
);
|
);
|
||||||
setInputVisible(prev => ({ ...prev, [field.field_name]: false }));
|
setInputVisible((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.field_name]: false,
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
icon={<Sparkles className="w-4 h-4" />}
|
icon={<Sparkles className="w-4 h-4" />}
|
||||||
width="w-8"
|
width="w-8"
|
||||||
@ -289,14 +291,19 @@ const RenderTemplateStoryMode = ({
|
|||||||
root: "max-w-none",
|
root: "max-w-none",
|
||||||
}}
|
}}
|
||||||
open={inputVisible[field.field_name]}
|
open={inputVisible[field.field_name]}
|
||||||
onOpenChange={(visible) => setInputVisible(prev => ({ ...prev, [field.field_name]: visible }))}
|
onOpenChange={(visible) =>
|
||||||
|
setInputVisible((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.field_name]: visible,
|
||||||
|
}))
|
||||||
|
}
|
||||||
trigger="contextMenu"
|
trigger="contextMenu"
|
||||||
styles={{ root: { zIndex: 1000 } }}
|
styles={{ root: { zIndex: 1000 } }}
|
||||||
>
|
>
|
||||||
{/* 图片 */}
|
{/* 图片 */}
|
||||||
<div
|
<div
|
||||||
data-alt={`field-thumbnail-${index}`}
|
data-alt={`field-thumbnail-${index}`}
|
||||||
className="w-24 h-24 rounded-xl overflow-hidden border border-white/20 bg-white/[0.05] flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
|
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
@ -328,7 +335,12 @@ const RenderTemplateStoryMode = ({
|
|||||||
<Tooltip title="AI generate image" placement="top">
|
<Tooltip title="AI generate image" placement="top">
|
||||||
<button
|
<button
|
||||||
data-alt={`field-ai-button-${index}`}
|
data-alt={`field-ai-button-${index}`}
|
||||||
onClick={() => setInputVisible(prev => ({ ...prev, [field.field_name]: !prev[field.field_name] }))}
|
onClick={() =>
|
||||||
|
setInputVisible((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field.field_name]: !prev[field.field_name],
|
||||||
|
}))
|
||||||
|
}
|
||||||
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3.5 h-3.5" />
|
<Sparkles className="w-3.5 h-3.5" />
|
||||||
@ -352,7 +364,11 @@ const RenderTemplateStoryMode = ({
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
customRequest={async ({ file, onSuccess, onError }) => {
|
customRequest={async ({
|
||||||
|
file,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
const uploadedUrl = await uploadFile(
|
const uploadedUrl = await uploadFile(
|
||||||
|
|||||||
@ -12,10 +12,10 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background" id="app">
|
||||||
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
|
||||||
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
|
import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
|
||||||
|
|
||||||
export function parseScriptEntity(text: string):ScriptSlice {
|
export function parseScriptEntity(text: string): ScriptSlice {
|
||||||
const scriptSlice = new ScriptSlice(
|
const scriptSlice = new ScriptSlice(
|
||||||
// 生成唯一ID,单次使用即可
|
// 生成唯一ID,单次使用即可
|
||||||
`${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
`${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
||||||
ScriptSliceType.text,
|
ScriptSliceType.text,
|
||||||
text,
|
text,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
return scriptSlice;
|
return scriptSlice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 节流函数,限制函数在指定时间间隔内只执行一次
|
* @description 节流函数,限制函数在指定时间间隔内只执行一次
|
||||||
* @param {Function} func - 需要被节流的函数
|
* @param {Function} func - 需要被节流的函数
|
||||||
@ -22,16 +21,57 @@ export function parseScriptEntity(text: string):ScriptSlice {
|
|||||||
* const throttledFn = throttle(() => { console.log('触发'); }, 1000);
|
* const throttledFn = throttle(() => { console.log('触发'); }, 1000);
|
||||||
* window.addEventListener('resize', throttledFn);
|
* window.addEventListener('resize', throttledFn);
|
||||||
*/
|
*/
|
||||||
export function throttle<T extends (...args: any[]) => any>(func: T, delay: number=100): (...args: Parameters<T>) => void {
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
if (typeof delay !== 'number' || delay < 0) {
|
func: T,
|
||||||
throw new Error('throttle: 第二个参数必须是非负数');
|
delay: number = 100
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
if (typeof delay !== "number" || delay < 0) {
|
||||||
|
throw new Error("throttle: 第二个参数必须是非负数");
|
||||||
|
}
|
||||||
|
let lastCall = 0;
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCall >= delay) {
|
||||||
|
lastCall = now;
|
||||||
|
func(...args);
|
||||||
}
|
}
|
||||||
let lastCall = 0;
|
};
|
||||||
return (...args: Parameters<T>) => {
|
}
|
||||||
const now = Date.now();
|
/**
|
||||||
if (now - lastCall >= delay) {
|
* 创建1920x1080屏幕适配的CSS样式
|
||||||
lastCall = now;
|
* 自动计算缩放比例并应用到页面元素
|
||||||
func(...args);
|
*/
|
||||||
}
|
export function createScreenAdapter(): void {
|
||||||
};
|
// 获取当前窗口尺寸
|
||||||
|
const currentWidth = window.innerWidth;
|
||||||
|
const currentHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// 计算缩放比例 (1920x1080)
|
||||||
|
const wScale = currentWidth / 1920;
|
||||||
|
const hScale = currentHeight / 1080;
|
||||||
|
|
||||||
|
// 检查app节点是否存在
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
if (!app) {
|
||||||
|
console.error("未找到app节点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建样式元素
|
||||||
|
const style = document.createElement("style");
|
||||||
|
|
||||||
|
// 设置CSS样式
|
||||||
|
style.textContent = `
|
||||||
|
body {
|
||||||
|
#app {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform: scale(${wScale}, ${hScale});
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 将样式添加到head
|
||||||
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user