From 153d0d49f82a85b8687ef865e300ac262c228d83 Mon Sep 17 00:00:00 2001 From: moux1024 <403053463@qq.com> Date: Sat, 11 Oct 2025 22:30:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E4=B8=8B=E8=BD=BD=E6=8E=A5=E5=8F=A3,?= =?UTF-8?q?=E6=9B=B4=E6=96=B0pricing=E7=9B=B8=E5=85=B3=E7=9A=84=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pricing/page.tsx | 15 ++- components/pages/create-to-video2.tsx | 40 +++++-- components/pages/home-page2.tsx | 15 ++- components/pages/usage-view.tsx | 1 + components/pages/work-flow/H5MediaViewer.tsx | 48 ++++---- .../work-flow/download-options-modal.tsx | 103 +++++++++++++----- 6 files changed, 162 insertions(+), 60 deletions(-) diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 383355b..f3721ab 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -77,6 +77,19 @@ function HomeModule5() { }[] >(() => { 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 { title: plan.display_name || plan.name, price: @@ -86,7 +99,7 @@ function HomeModule5() { originalPrice: plan.price_month / 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!`, - credits: plan.description, + credits: creditsText, buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", issubscribed: plan.is_subscribed, features: plan.features || [], diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index aed75d9..e4274a2 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useRef, useCallback } from 'react'; +import type { MouseEvent } from 'react'; import { Loader2, Download } from 'lucide-react'; import { useRouter } from 'next/navigation'; 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 { motion } from 'framer-motion'; 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 debounce from 'lodash/debounce'; @@ -263,6 +266,34 @@ export default function CreateToVideo2() { }, []); 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 计算纵横比 const getAspectRatio = () => { switch (project.aspect_ratio) { @@ -320,12 +351,7 @@ export default function CreateToVideo2() { {(project.final_video_url || project.final_simple_video_url) && (
- diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index c4a12a2..ad1e3f2 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -1223,6 +1223,19 @@ function HomeModule5() { }[] >(() => { 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 { title: plan.display_name || plan.name, price: @@ -1232,7 +1245,7 @@ function HomeModule5() { originalPrice: plan.price_month / 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!`, - credits: plan.description, + credits: creditsText, buttonText: plan.is_free ? "Try For Free" : "Subscribe Now", issubscribed: plan.is_subscribed, features: plan.features || [], diff --git a/components/pages/usage-view.tsx b/components/pages/usage-view.tsx index bb629e4..695fdf3 100644 --- a/components/pages/usage-view.tsx +++ b/components/pages/usage-view.tsx @@ -181,6 +181,7 @@ const UsageView: React.FC = () => { const formatSource = useCallback((source: string | undefined) => { if (!source) return '-'; const map: Record = { + video_download: 'Video Download', video_generation: 'Video Generation', manual_admin: 'Manual (Admin)', subscription: 'Subscription', diff --git a/components/pages/work-flow/H5MediaViewer.tsx b/components/pages/work-flow/H5MediaViewer.tsx index 804beef..a2f6206 100644 --- a/components/pages/work-flow/H5MediaViewer.tsx +++ b/components/pages/work-flow/H5MediaViewer.tsx @@ -9,7 +9,9 @@ import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import ScriptLoading from './script-loading'; import { GlassIconButton } from '@/components/ui/glass-icon-button'; import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools'; +import { post } from '@/api/request'; import { Drawer } from 'antd'; +import { useSearchParams } from 'next/navigation'; import error_image from '@/public/assets/error.webp'; import { showDownloadOptionsModal } from './download-options-modal'; @@ -84,7 +86,8 @@ export function H5MediaViewer({ const [isPlaying, setIsPlaying] = useState(false); const [isCatalogOpen, setIsCatalogOpen] = useState(false); const [isEdgeBrowser, setIsEdgeBrowser] = useState(false); - + const searchParams = useSearchParams(); + const episodeId = searchParams.get('episodeId') || ''; /** 解析形如 "16:9" 的比例字符串 */ const parseAspect = (input?: string): { w: number; h: number } => { const parts = (typeof input === 'string' ? input.split(':') : []); @@ -474,18 +477,19 @@ export function H5MediaViewer({ currentVideoIndex: hasFinalVideo ? activeIndex + 1 : activeIndex, totalVideos, isCurrentVideoFailed, - onDownloadCurrent: async () => { - if (hasUrl) { - await downloadVideo(current.urls[0]); - } - }, - onDownloadAll: async () => { - const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); - if (hasFinalVideo) { - all.push(taskObject.final.url); - } - await downloadAllVideos(all); + projectId: episodeId, + videoId: current?.video_id, + onDownloadCurrent: async (withWatermark: boolean) => { + if (!current?.video_id) return; + const json: any = await post('/movie/download_video', { + project_id: episodeId, + video_id: current.video_id, + watermark: !withWatermark + }); + const url = json?.data?.download_url as string | undefined; + if (url) await downloadVideo(url); }, + onDownloadAll: ()=>{} }); }} /> @@ -523,18 +527,16 @@ export function H5MediaViewer({ totalVideos, isCurrentVideoFailed: false, isFinalStage: true, - onDownloadCurrent: async () => { - if (finalUrl) { - await downloadVideo(finalUrl); - } - }, - onDownloadAll: async () => { - const all = (taskObject.videos?.data ?? []).flatMap((v: any) => v?.urls ?? []); - if (finalUrl) { - all.push(finalUrl); - } - await downloadAllVideos(all); + projectId: episodeId, + onDownloadCurrent: async (withWatermark: boolean) => { + const json: any = await post('/movie/download_video', { + project_id: episodeId, + watermark: !withWatermark + }); + const url = json?.data?.download_url as string | undefined; + if (url) await downloadVideo(url); }, + onDownloadAll: ()=>{} }); }} /> diff --git a/components/pages/work-flow/download-options-modal.tsx b/components/pages/work-flow/download-options-modal.tsx index 1904059..adf630f 100644 --- a/components/pages/work-flow/download-options-modal.tsx +++ b/components/pages/work-flow/download-options-modal.tsx @@ -1,12 +1,14 @@ '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 { X, Download, ArrowDownWideNarrow } from 'lucide-react'; +import { baseUrl } from '@/lib/env'; interface DownloadOptionsModalProps { - onDownloadCurrent: () => void; - onDownloadAll: () => void; + onDownloadCurrent: (withWatermark: boolean) => void; + onDownloadAll: (withWatermark: boolean) => void; onClose: () => void; currentVideoIndex: number; totalVideos: number; @@ -14,6 +16,10 @@ interface DownloadOptionsModalProps { isCurrentVideoFailed: boolean; /** 是否为最终视频阶段 */ isFinalStage?: boolean; + /** 项目ID */ + projectId?: string; + /** 视频ID(分镜视频可用) */ + videoId?: string; } /** @@ -21,8 +27,10 @@ interface DownloadOptionsModalProps { * @param {DownloadOptionsModalProps} props - modal properties. */ 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(null); + const [withWatermark, setWithWatermark] = useState(true); + const [baseAmount, setBaseAmount] = useState(0); useEffect(() => { 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 (
Choose your download preference

- - {!isCurrentVideoFailed && ( -
-
Current video
-
{currentVideoIndex + 1} / {totalVideos}
+
+ {baseAmount && baseAmount !== 0 ? ( + -{baseAmount} credits + ) : ( + free + )} +
+
+
+ setWithWatermark(!e.target.checked)} + /> + without watermark
- )} +
+ + {/* stats-info hidden temporarily due to no batch billing support */}
- {!isCurrentVideoFailed && ( - - )}