更新 使用新的下载接口,更新pricing相关的展示

This commit is contained in:
moux1024 2025-10-11 22:30:25 +08:00
parent c38b6ad483
commit 153d0d49f8
6 changed files with 162 additions and 60 deletions

View File

@ -77,6 +77,19 @@ function HomeModule5() {
}[] }[]
>(() => { >(() => {
return plans.map((plan) => { return plans.map((plan) => {
const rawDescription = plan.description ?? '';
let creditsText: string = rawDescription;
try {
const parsed = JSON.parse(rawDescription) as { period: 'month' | 'year'; content: string }[];
if (Array.isArray(parsed)) {
const match = parsed.find((item) => item && item.period === billingType);
if (match && typeof match.content === 'string') {
creditsText = match.content;
}
}
} catch {
// If not valid JSON, keep original string
}
return { return {
title: plan.display_name || plan.name, title: plan.display_name || plan.name,
price: price:
@ -86,7 +99,7 @@ function HomeModule5() {
originalPrice: plan.price_month / 100, originalPrice: plan.price_month / 100,
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100, monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100,
discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`, discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`,
credits: plan.description, credits: creditsText,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed, issubscribed: plan.is_subscribed,
features: plan.features || [], features: plan.features || [],

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import type { MouseEvent } from 'react';
import { Loader2, Download } from 'lucide-react'; import { Loader2, Download } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import './style/create-to-video2.css'; import './style/create-to-video2.css';
@ -11,7 +12,9 @@ import cover_image1 from '@/public/assets/cover_image3.jpg';
import cover_image2 from '@/public/assets/cover_image_shu.jpg'; import cover_image2 from '@/public/assets/cover_image_shu.jpg';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Tooltip, Button } from 'antd'; import { Tooltip, Button } from 'antd';
import { downloadVideo, getFirstFrame } from '@/utils/tools'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
import { showDownloadOptionsModal } from '@/components/pages/work-flow/download-options-modal';
import { post } from '@/api/request';
import Masonry from 'react-masonry-css'; import Masonry from 'react-masonry-css';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
@ -263,6 +266,34 @@ export default function CreateToVideo2() {
}, []); }, []);
const renderProjectCard = (project: MovieProject): JSX.Element => { const renderProjectCard = (project: MovieProject): JSX.Element => {
const handleDownloadClick = async (e: MouseEvent, project: MovieProject) => {
console.log(project);
e.stopPropagation();
showDownloadOptionsModal({
currentVideoIndex: 0,
totalVideos: 1,
isCurrentVideoFailed: false,
isFinalStage: true,
projectId: project.project_id,
onDownloadCurrent: async (withWatermark: boolean) => {
setIsLoadingDownloadBtn(true);
try {
const json: any = await post('/movie/download_video', {
project_id: project.project_id,
watermark: !withWatermark
});
const url = json?.data?.download_url as string | undefined;
if (url) {
await downloadVideo(url);
}
} finally {
setIsLoadingDownloadBtn(false);
}
},
onDownloadAll: ()=>{}
});
};
// 根据 aspect_ratio 计算纵横比 // 根据 aspect_ratio 计算纵横比
const getAspectRatio = () => { const getAspectRatio = () => {
switch (project.aspect_ratio) { switch (project.aspect_ratio) {
@ -320,12 +351,7 @@ export default function CreateToVideo2() {
{(project.final_video_url || project.final_simple_video_url) && ( {(project.final_video_url || project.final_simple_video_url) && (
<div className="absolute top-1 right-1"> <div className="absolute top-1 right-1">
<Tooltip placement="top" title="Download"> <Tooltip placement="top" title="Download">
<Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={async (e) => { <Button size="small" type="text" disabled={isLoadingDownloadBtn} className="w-[2.5rem] h-[2.5rem] rounded-full items-center justify-center p-0 hidden group-hover:flex transition-all duration-300 hover:bg-white/15" onClick={(e) => handleDownloadClick(e, project)}>
e.stopPropagation(); // 阻止事件冒泡
setIsLoadingDownloadBtn(true);
await downloadVideo(project.final_video_url || project.final_simple_video_url);
setIsLoadingDownloadBtn(false);
}}>
{isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />} {isLoadingDownloadBtn ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : <Download className="w-4 h-4 text-white" />}
</Button> </Button>
</Tooltip> </Tooltip>

View File

@ -1223,6 +1223,19 @@ function HomeModule5() {
}[] }[]
>(() => { >(() => {
return plans.map((plan) => { return plans.map((plan) => {
const rawDescription = plan.description ?? '';
let creditsText: string = rawDescription;
try {
const parsed = JSON.parse(rawDescription) as { period: 'month' | 'year'; content: string }[];
if (Array.isArray(parsed)) {
const match = parsed.find((item) => item && item.period === billingType);
if (match && typeof match.content === 'string') {
creditsText = match.content;
}
}
} catch {
// If not valid JSON, keep original string
}
return { return {
title: plan.display_name || plan.name, title: plan.display_name || plan.name,
price: price:
@ -1232,7 +1245,7 @@ function HomeModule5() {
originalPrice: plan.price_month / 100, originalPrice: plan.price_month / 100,
monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100, monthlyPrice: billingType === "month" ? 0 : Math.round(plan.price_year / 12) / 100,
discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`, discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`,
credits: plan.description, credits: creditsText,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed, issubscribed: plan.is_subscribed,
features: plan.features || [], features: plan.features || [],

View File

@ -181,6 +181,7 @@ const UsageView: React.FC = () => {
const formatSource = useCallback((source: string | undefined) => { const formatSource = useCallback((source: string | undefined) => {
if (!source) return '-'; if (!source) return '-';
const map: Record<string, string> = { const map: Record<string, string> = {
video_download: 'Video Download',
video_generation: 'Video Generation', video_generation: 'Video Generation',
manual_admin: 'Manual (Admin)', manual_admin: 'Manual (Admin)',
subscription: 'Subscription', subscription: 'Subscription',

View File

@ -9,7 +9,9 @@ import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import ScriptLoading from './script-loading'; import ScriptLoading from './script-loading';
import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
import { post } from '@/api/request';
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import { useSearchParams } from 'next/navigation';
import error_image from '@/public/assets/error.webp'; import error_image from '@/public/assets/error.webp';
import { showDownloadOptionsModal } from './download-options-modal'; import { showDownloadOptionsModal } from './download-options-modal';
@ -84,7 +86,8 @@ export function H5MediaViewer({
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false); const [isCatalogOpen, setIsCatalogOpen] = useState<boolean>(false);
const [isEdgeBrowser, setIsEdgeBrowser] = useState<boolean>(false); const [isEdgeBrowser, setIsEdgeBrowser] = useState<boolean>(false);
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId') || '';
/** 解析形如 "16:9" 的比例字符串 */ /** 解析形如 "16:9" 的比例字符串 */
const parseAspect = (input?: string): { w: number; h: number } => { const parseAspect = (input?: string): { w: number; h: number } => {
const parts = (typeof input === 'string' ? input.split(':') : []); const parts = (typeof input === 'string' ? input.split(':') : []);
@ -474,18 +477,19 @@ export function H5MediaViewer({
currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex, currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex,
totalVideos, totalVideos,
isCurrentVideoFailed, isCurrentVideoFailed,
onDownloadCurrent: async () => { projectId: episodeId,
if (hasUrl) { videoId: current?.video_id,
await downloadVideo(current.urls[0]); onDownloadCurrent: async (withWatermark: boolean) => {
} if (!current?.video_id) return;
}, const json: any = await post('/movie/download_video', {
onDownloadAll: async () => { project_id: episodeId,
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); video_id: current.video_id,
if (hasFinalVideo) { watermark: !withWatermark
all.push(taskObject.final.url); });
} const url = json?.data?.download_url as string | undefined;
await downloadAllVideos(all); if (url) await downloadVideo(url);
}, },
onDownloadAll: ()=>{}
}); });
}} }}
/> />
@ -523,18 +527,16 @@ export function H5MediaViewer({
totalVideos, totalVideos,
isCurrentVideoFailed: false, isCurrentVideoFailed: false,
isFinalStage: true, isFinalStage: true,
onDownloadCurrent: async () => { projectId: episodeId,
if (finalUrl) { onDownloadCurrent: async (withWatermark: boolean) => {
await downloadVideo(finalUrl); const json: any = await post('/movie/download_video', {
} project_id: episodeId,
}, watermark: !withWatermark
onDownloadAll: async () => { });
const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); const url = json?.data?.download_url as string | undefined;
if (finalUrl) { if (url) await downloadVideo(url);
all.push(finalUrl);
}
await downloadAllVideos(all);
}, },
onDownloadAll: ()=>{}
}); });
}} }}
/> />

View File

@ -1,12 +1,14 @@
'use client'; 'use client';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Checkbox } from 'antd';
import { createRoot, Root } from 'react-dom/client'; import { createRoot, Root } from 'react-dom/client';
import { X, Download, ArrowDownWideNarrow } from 'lucide-react'; import { X, Download, ArrowDownWideNarrow } from 'lucide-react';
import { baseUrl } from '@/lib/env';
interface DownloadOptionsModalProps { interface DownloadOptionsModalProps {
onDownloadCurrent: () => void; onDownloadCurrent: (withWatermark: boolean) => void;
onDownloadAll: () => void; onDownloadAll: (withWatermark: boolean) => void;
onClose: () => void; onClose: () => void;
currentVideoIndex: number; currentVideoIndex: number;
totalVideos: number; totalVideos: number;
@ -14,6 +16,10 @@ interface DownloadOptionsModalProps {
isCurrentVideoFailed: boolean; isCurrentVideoFailed: boolean;
/** 是否为最终视频阶段 */ /** 是否为最终视频阶段 */
isFinalStage?: boolean; isFinalStage?: boolean;
/** 项目ID */
projectId?: string;
/** 视频ID分镜视频可用 */
videoId?: string;
} }
/** /**
@ -21,8 +27,10 @@ interface DownloadOptionsModalProps {
* @param {DownloadOptionsModalProps} props - modal properties. * @param {DownloadOptionsModalProps} props - modal properties.
*/ */
function DownloadOptionsModal(props: DownloadOptionsModalProps) { function DownloadOptionsModal(props: DownloadOptionsModalProps) {
const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false } = props; const { onDownloadCurrent, onDownloadAll, onClose, currentVideoIndex, totalVideos, isCurrentVideoFailed, isFinalStage = false, projectId, videoId } = props;
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [withWatermark, setWithWatermark] = useState<boolean>(true);
const [baseAmount, setBaseAmount] = useState<number>(0);
useEffect(() => { useEffect(() => {
const originalOverflow = document.body.style.overflow; const originalOverflow = document.body.style.overflow;
@ -32,6 +40,46 @@ function DownloadOptionsModal(props: DownloadOptionsModalProps) {
}; };
}, []); }, []);
// 监听水印选择变化,请求价格信息
useEffect(() => {
let aborted = false;
const checkBalance = async () => {
try {
if (!projectId) {
setBaseAmount(0);
return;
}
const token = typeof window !== 'undefined' ? (localStorage?.getItem('token') || '') : '';
const res = await fetch(`${baseUrl}/movie/download_video`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: JSON.stringify({
project_id: projectId,
video_id: videoId,
watermark: !withWatermark,
check_balance: true
})
});
if (!res.ok) {
if (!aborted) setBaseAmount(0);
return;
}
const json = await res.json().catch(() => null);
const amount = json?.data?.base_amount;
if (!aborted) setBaseAmount(Number.isFinite(amount) ? Number(amount) : 0);
} catch {
if (!aborted) setBaseAmount(0);
}
};
void checkBalance();
return () => {
aborted = true;
};
}, [withWatermark]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@ -67,39 +115,38 @@ function DownloadOptionsModal(props: DownloadOptionsModalProps) {
<p data-alt="modal-description" className="text-sm text-white/80 text-center"> <p data-alt="modal-description" className="text-sm text-white/80 text-center">
Choose your download preference Choose your download preference
</p> </p>
<div data-alt="price-indicator" className="mt-2 text-center text-sm font-medium">
{!isCurrentVideoFailed && ( {baseAmount && baseAmount !== 0 ? (
<div data-alt="stats-info" className="mt-3 rounded-lg bg-white/5 border border-white/10 p-3 text-center"> <span className="text-red-400">-{baseAmount} credits</span>
<div className="text-xs text-white/60">Current video</div> ) : (
<div className="text-sm font-medium">{currentVideoIndex + 1} / {totalVideos}</div> <span className="text-green-400">free</span>
)}
</div>
<div data-alt="watermark-select" className="mt-3 flex items-center justify-center">
<div data-alt="watermark-toggle" className="inline-flex items-center gap-2 text-sm text-white/90">
<Checkbox
data-alt="watermark-checkbox"
checked={!withWatermark}
onChange={(e) => setWithWatermark(!e.target.checked)}
/>
<span>without watermark</span>
</div> </div>
)} </div>
{/* stats-info hidden temporarily due to no batch billing support */}
</div> </div>
<div data-alt="modal-actions" className="mt-6 space-y-3"> <div data-alt="modal-actions" className="mt-6 space-y-3">
{!isCurrentVideoFailed && (
<button
data-alt="download-current-button"
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
onClick={() => {
onDownloadCurrent();
onClose();
}}
>
<Download size={16} />
{isFinalStage ? 'Download Final Video' : 'Download Current Video'}
</button>
)}
<button <button
data-alt="download-all-button" data-alt="download-single-button"
className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-500/60 to-purple-600/60 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2 border border-purple-400/30" className="w-full px-4 py-2.5 rounded-lg bg-gradient-to-br from-purple-600/80 to-purple-700/80 hover:from-purple-500/80 hover:to-purple-600/80 text-white font-medium transition-all flex items-center justify-center gap-2"
onClick={() => { onClick={() => {
onDownloadAll(); onDownloadCurrent(withWatermark);
onClose(); onClose();
}} }}
> >
<ArrowDownWideNarrow size={16} /> <Download size={16} />
Download All Videos ({totalVideos}) Download Video
</button> </button>
</div> </div>
</div> </div>