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 && (
-
{
- onDownloadCurrent();
- onClose();
- }}
- >
-
- {isFinalStage ? 'Download Final Video' : 'Download Current Video'}
-
- )}
{
- onDownloadAll();
+ onDownloadCurrent(withWatermark);
onClose();
}}
>
-
- Download All Videos ({totalVideos})
+
+ Download Video