Merge branch 'dev' into prod

This commit is contained in:
moux1024 2025-09-25 20:22:54 +08:00
commit c9fc8f2da1
8 changed files with 65 additions and 56 deletions

View File

@ -1,8 +1,6 @@
"use client"; "use client";
import { Dropdown } from "antd";
import { RectangleHorizontal, RectangleVertical } from "lucide-react"; import { RectangleHorizontal, RectangleVertical } from "lucide-react";
import { AspectRatioOptions } from "./types";
export type AspectRatioValue = export type AspectRatioValue =
| "VIDEO_ASPECT_RATIO_LANDSCAPE" | "VIDEO_ASPECT_RATIO_LANDSCAPE"
@ -22,13 +20,13 @@ interface AspectRatioSelectorProps {
} }
/** /**
* A reusable aspect ratio selector (landscape/portrait) using Antd Dropdown. * Aspect ratio selector using Antd Radio.Group with two options: landscape and portrait.
* Shows an icon and label, and calls onChange when a new ratio is chosen. * Uses icons as button content and triggers onChange when selection changes.
* @param {AspectRatioValue} value - current selected value * @param {AspectRatioValue} value - current selected value
* @param {(v: AspectRatioValue) => void} onChange - change handler * @param {(v: AspectRatioValue) => void} onChange - change handler
* @param {string} [className] - optional className for trigger button * @param {string} [className] - optional className for wrapper
* @param {string} [placement] - Dropdown placement, default is top * @param {string} [placement] - kept for backward compatibility (unused)
* @param {string} [dataAlt] - data-alt attribute for the trigger * @param {string} [dataAlt] - data-alt attribute for the wrapper
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
export const AspectRatioSelector = ({ export const AspectRatioSelector = ({
@ -39,45 +37,47 @@ export const AspectRatioSelector = ({
dataAlt = "config-aspect-ratio", dataAlt = "config-aspect-ratio",
}: AspectRatioSelectorProps) => { }: AspectRatioSelectorProps) => {
return ( return (
<Dropdown <div
overlayClassName="aspect-dropdown" data-alt={dataAlt}
menu={{ role="radiogroup"
items: AspectRatioOptions.map((option) => ({ aria-label="aspect-ratio"
key: option.value, className={`inline-flex p-1 items-center rounded-[8px] bg-white/20 ${className || ""}`}
label: (
<div
className={`flex items-center gap-2 px-2 py-2 ${
option.value === value ? "bg-white/[0.12] rounded-md" : ""
}`}
>
{option.value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? (
<RectangleHorizontal className="w-4 h-4" />
) : (
<RectangleVertical className="w-4 h-4" />
)}
<span className="text-sm text-white">{option.label}</span>
</div>
),
})),
onClick: ({ key }) => onChange(key as AspectRatioValue),
}}
trigger={["click"]}
placement={placement}
> >
<button <button
data-alt={dataAlt} type="button"
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2 ${className || ""}`} role="radio"
aria-checked={value === "VIDEO_ASPECT_RATIO_LANDSCAPE"}
onClick={() => onChange("VIDEO_ASPECT_RATIO_LANDSCAPE")}
className="outline-none rounded-[8px]"
> >
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? ( <div
<RectangleHorizontal className={"w-4 h-4"} /> className={`flex items-center justify-center px-1 py-0.5 ${
) : ( value === "VIDEO_ASPECT_RATIO_LANDSCAPE"
<RectangleVertical className={"w-4 h-4"} /> ? "bg-white/60 text-black rounded-[4px]"
)} : "bg-transparent text-white"
<span className="text-sm"> }`}
{value === "VIDEO_ASPECT_RATIO_LANDSCAPE" ? "16:9" : "9:16"} >
</span> <RectangleHorizontal className="w-4 h-4" />
</div>
</button> </button>
</Dropdown> <button
type="button"
role="radio"
aria-checked={value === "VIDEO_ASPECT_RATIO_PORTRAIT"}
onClick={() => onChange("VIDEO_ASPECT_RATIO_PORTRAIT")}
className="outline-none"
>
<div
className={`flex items-center justify-center px-1 py-0.5 ${
value === "VIDEO_ASPECT_RATIO_PORTRAIT"
? "bg-white/60 text-black rounded-[4px]"
: "bg-transparent text-white"
}`}
>
<RectangleVertical className="w-4 h-4" />
</div>
</button>
</div>
); );
}; };

View File

@ -523,11 +523,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
placement="top" placement="top"
> >
<button <button
data-alt={`config-video-duration`} data-alt="config-video-duration"
className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`} className={`flex items-center gap-1 text-white/80 transition-all duration-200 ${isMobile ? 'px-1' : 'px-2'} py-2`}
> >
<Clock className={"w-4 h-4"} /> <Clock className={"w-4 h-4"} />
<span className="text-sm">{configOptions.videoDuration === 'unlimited' ? 'auto' : (isMobile ? configOptions.videoDuration.replace('min', 'm') : configOptions.videoDuration)}</span> <span className="text-sm">{configOptions.videoDuration === 'unlimited' ? 'auto' : (isMobile ? configOptions.videoDuration.replace('min', 'm') : configOptions.videoDuration)}</span>
<ChevronUp className="w-3 h-3 text-white/60" />
</button> </button>
</Dropdown> </Dropdown>

View File

@ -138,9 +138,11 @@ const WorkFlow = React.memo(function WorkFlow() {
setTimeout(() => { setTimeout(() => {
handleTestExportRef.current?.(); handleTestExportRef.current?.();
}, 0); }, 0);
const title = isMobile ? 'editing...' : 'Performing intelligent editing...';
// 显示进度提示并启动超时定时器 // 显示进度提示并启动超时定时器
emitToastShow({ title: 'Performing intelligent editing...', progress: 0 }); emitToastShow({ title: title, progress: 0 });
// 启动自动推进到 90% 的进度8分钟 // 启动自动推进到 90% 的进度8分钟
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current); if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
editingProgressStartRef.current = Date.now(); editingProgressStartRef.current = Date.now();

View File

@ -286,7 +286,7 @@ export function H5MediaViewer({
} }
}; };
return ( return (
<div data-alt="script-content" className="w-full h-full pt-[4rem]"> <div data-alt="script-content" className="w-full h-full">
{scriptData ? ( {scriptData ? (
<> <>
<ScriptRenderer <ScriptRenderer

View File

@ -677,9 +677,7 @@ export const MediaViewer = React.memo(function MediaViewer({
)} )}
{currentSketch.status === 2 && ( {currentSketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center"> <div className="absolute inset-0 bg-red-500/5 flex items-center justify-center">
<div className="text-[#813b9dcc] text-2xl font-bold flex items-center gap-2"> <div className="text-2xl mb-4"></div>
<CircleAlert className="w-10 h-10" />
</div>
</div> </div>
)} )}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */} {/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}

View File

@ -298,10 +298,8 @@ export function ThumbnailGrid({
</div> </div>
)} )}
{sketch.status === 2 && ( {sketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/5 flex items-center justify-center"> <div className="absolute inset-0 bg-red-500/5 flex items-center justify-center z-20">
<div className="text-[#813b9dcc] text-xl font-bold flex items-center gap-2"> <div className="text-2xl mb-4"></div>
<CircleAlert className="w-10 h-10" />
</div>
</div> </div>
)} )}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */} {/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}

View File

@ -7,6 +7,7 @@ import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect'; import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit'; import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector'; import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
import { useDeviceType } from '@/hooks/useDeviceType';
interface UseWorkflowDataProps { interface UseWorkflowDataProps {
onEditPlanGenerated?: () => void; onEditPlanGenerated?: () => void;
@ -37,6 +38,8 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
} }
}; };
const { isMobile, isTablet, isDesktop } = useDeviceType();
const cutUrl = process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai'; const cutUrl = process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai';
console.log('cutUrl', cutUrl); console.log('cutUrl', cutUrl);
@ -158,7 +161,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
return; return;
} }
// 显示生成剪辑计划进度提示 // 显示生成剪辑计划进度提示
emitToastShow({ title: `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 }); emitToastShow({ title: isMobile ? 'Preparing for editing...' : `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
// 平滑推进到 80%,后续阶段接管 // 平滑推进到 80%,后续阶段接管
const start = Date.now(); const start = Date.now();
const duration = 3 * 60 * 1000; // 3分钟推进到 80% const duration = 3 * 60 * 1000; // 3分钟推进到 80%
@ -198,7 +201,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
setIsGenerateEditPlan(false); setIsGenerateEditPlan(false);
// 显示失败提示,并在稍后隐藏 // 显示失败提示,并在稍后隐藏
emitToastShow({ title: 'Editing plan generation failed. Retrying later.', progress: 0 }); // emitToastShow({ title: isMobile ? 'Editing plan generation failed. Retrying later.' : 'Editing plan generation failed. Retrying later.', progress: 0 });
setTimeout(() => { setTimeout(() => {
emitToastHide(); emitToastHide();
setIsLoadingGenerateEditPlan(false); setIsLoadingGenerateEditPlan(false);
@ -419,7 +422,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) { if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
setIsAnalyzing(true); setIsAnalyzing(true);
// 显示准备剪辑计划的提示 // 显示准备剪辑计划的提示
emitToastShow({ title: 'Preparing intelligent editing plan...', progress: 0 }); emitToastShow({ title: isMobile ? 'Preparing for editing...' : 'Preparing intelligent editing plan...', progress: 0 });
} }
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) { if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {

View File

@ -10,7 +10,14 @@
*/ */
const localPost = async <T>(url: string, data: any): Promise<T> => { const localPost = async <T>(url: string, data: any): Promise<T> => {
try { try {
const response = await fetch(url, { // 使用环境变量中的 BASE_URL生产要求使用 NEXT_PUBLIC_BASE_URL
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || '';
const isAbsolute = /^https?:\/\//i.test(url);
const normalizedBase = baseUrl.replace(/\/$/, '');
const normalizedPath = url.startsWith('/') ? url : `/${url}`;
const fullUrl = isAbsolute ? url : `${normalizedBase}${normalizedPath}`;
const response = await fetch(fullUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',