From e57d9e6c6f13cf1dedd6e9304e6983a7cc0c8b34 Mon Sep 17 00:00:00 2001 From: Zixin Zhou Date: Tue, 26 Aug 2025 21:24:39 +0800 Subject: [PATCH 1/7] say hello to pay system --- app/dashboard/page.tsx | 67 ++++++++ app/payment-success/page.tsx | 236 ++++++++++++++++++++++++++ app/pricing/page.tsx | 282 ++++++++++++++++++++++++++++++++ components/layout/top-bar.tsx | 10 ++ components/pages/home-page2.tsx | 86 +++++++++- lib/stripe.ts | 114 +++++++++++++ 6 files changed, 793 insertions(+), 2 deletions(-) create mode 100644 app/payment-success/page.tsx create mode 100644 app/pricing/page.tsx create mode 100644 lib/stripe.ts diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 7cfcec1..6047257 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -19,6 +19,10 @@ export default function DashboardPage() { const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false); const [lastUpdateTime, setLastUpdateTime] = useState(null); const [connectionStatus, setConnectionStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking'); + + // 支付成功状态 + const [showPaymentSuccess, setShowPaymentSuccess] = useState(false); + const [paymentData, setPaymentData] = useState(null); // 使用 ref 来存储最新的状态,避免定时器闭包问题 const stateRef = useRef({ isUsingMockData, dashboardData }); @@ -31,6 +35,40 @@ export default function DashboardPage() { + // 检测支付成功 + useEffect(() => { + const sessionId = searchParams.get('session_id'); + const payment = searchParams.get('payment'); + + if (sessionId && payment === 'success') { + // 显示支付成功提示 + setShowPaymentSuccess(true); + + // 获取支付详情 + fetchPaymentDetails(sessionId); + + // 清除URL参数,避免刷新页面时重复显示 + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete('session_id'); + newUrl.searchParams.delete('payment'); + window.history.replaceState({}, '', newUrl.pathname); + } + }, [searchParams]); + + // 获取支付详情 + const fetchPaymentDetails = async (sessionId: string) => { + try { + const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=test_user_123`); + const result = await response.json(); + + if (result.successful && result.data) { + setPaymentData(result.data); + } + } catch (error) { + console.error('获取支付详情失败:', error); + } + }; + // 初始加载数据 const fetchInitialData = async () => { try { @@ -370,6 +408,35 @@ export default function DashboardPage() { return (
+ {/* 支付成功提示 */} + {showPaymentSuccess && paymentData && ( +
+
+
+ + + +
+
+

支付成功!

+

+ 您的订阅已激活,订单号: {paymentData.biz_order_no} +

+
+
+ +
+
+
+ )} + {/* 后台刷新指示器 - 优化用户体验 */} {isBackgroundRefreshing && (
diff --git a/app/payment-success/page.tsx b/app/payment-success/page.tsx new file mode 100644 index 0000000..66963da --- /dev/null +++ b/app/payment-success/page.tsx @@ -0,0 +1,236 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { CheckCircle, Loader2, XCircle } from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; + +interface PaymentStatus { + payment_status: 'pending' | 'success' | 'fail'; + biz_order_no: string; + pay_time?: string; + subscription?: { + plan_name: string; + plan_display_name: string; + status: string; + current_period_end?: string; + }; +} + +export default function PaymentSuccessPage() { + const searchParams = useSearchParams(); + const sessionId = searchParams.get('session_id'); + + const [status, setStatus] = useState<'loading' | 'success' | 'failed' | 'timeout'>('loading'); + const [paymentData, setPaymentData] = useState(null); + const [attempts, setAttempts] = useState(0); + + useEffect(() => { + if (!sessionId) { + setStatus('failed'); + return; + } + + const pollPaymentStatus = async () => { + const maxAttempts = 30; // 最多轮询30次 + const interval = 2000; // 每2秒轮询一次 + + for (let i = 0; i < maxAttempts; i++) { + setAttempts(i + 1); + + try { + // 使用新的Checkout Session状态查询 + const { getCheckoutSessionStatus } = await import('@/lib/stripe'); + const result = await getCheckoutSessionStatus(sessionId, 'test_user_123'); // 临时测试用户ID + + if (result.successful && result.data) { + setPaymentData(result.data); + + if (result.data.payment_status === 'success') { + setStatus('success'); + return; + } else if (result.data.payment_status === 'fail') { + setStatus('failed'); + return; + } + } + + // 等待下次轮询 + await new Promise(resolve => setTimeout(resolve, interval)); + } catch (error) { + console.error('轮询Checkout Session状态失败:', error); + } + } + + // 轮询超时 + setStatus('timeout'); + }; + + pollPaymentStatus(); + }, [sessionId]); + + const renderContent = () => { + switch (status) { + case 'loading': + return ( + + +
+ +
+ 处理中... + + 正在确认您的支付,请稍候 +
+ 尝试次数: {attempts}/30 +
+
+ +

+ 请不要关闭此页面,我们正在处理您的订阅 +

+
+
+ ); + + case 'success': + return ( + + +
+ +
+ 支付成功! + + 您的订阅已激活 + +
+ + {paymentData?.subscription && ( +
+

+ {paymentData.subscription.plan_display_name} 套餐 +

+

+ 状态: {paymentData.subscription.status} +

+ {paymentData.subscription.current_period_end && ( +

+ 有效期至: {new Date(paymentData.subscription.current_period_end).toLocaleDateString()} +

+ )} +
+ )} + +
+

订单号: {paymentData?.biz_order_no}

+ {paymentData?.pay_time && ( +

支付时间: {new Date(paymentData.pay_time).toLocaleString()}

+ )} +
+ +
+ + +
+
+
+ ); + + case 'failed': + return ( + + +
+ +
+ 支付失败 + + 很抱歉,您的支付未能成功完成 + +
+ +

+ 请检查您的支付信息或稍后重试 +

+ +
+ + +
+
+
+ ); + + case 'timeout': + return ( + + +
+ +
+ 处理中 + + 支付正在处理中,请稍后查看 + +
+ +

+ 您的支付可能仍在处理中,请稍后检查您的订阅状态 +

+ +
+ + +
+
+
+ ); + + default: + return null; + } + }; + + return ( +
+
+ {renderContent()} +
+
+ ); +} diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx new file mode 100644 index 0000000..4a8316d --- /dev/null +++ b/app/pricing/page.tsx @@ -0,0 +1,282 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Check, ArrowLeft } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; + +export default function PricingPage() { + const router = useRouter(); + const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly'); + const [plans, setPlans] = useState([]); + + // 从后端获取订阅计划数据 + useEffect(() => { + const loadPlans = async () => { + try { + const plansData = await fetchSubscriptionPlans(); + setPlans(plansData); + } catch (err) { + console.error('加载订阅计划失败:', err); + } + }; + + loadPlans(); + }, []); + + // 转换后端数据为前端显示格式,保持原有的数据结构 + const transformPlanForDisplay = (plan: SubscriptionPlan) => { + const monthlyPrice = plan.price_monthly / 100; // 后端存储的是分,转换为元 + const yearlyPrice = plan.price_yearly / 100; + + return { + name: plan.name, + displayName: plan.display_name, + price: { + monthly: monthlyPrice, + yearly: yearlyPrice + }, + description: plan.description, + features: plan.features || [], + popular: plan.is_popular, // 使用后端返回的 is_popular 字段 + buttonText: plan.is_free ? 'Start Free Trial' : 'Subscribe', + buttonVariant: plan.is_free ? 'outline' as const : 'default' as const + }; + }; + + const handleSubscribe = async (planName: string) => { + if (planName === 'hobby') { + return; + } + + try { + // 使用新的Checkout Session方案(更简单!) + const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe'); + + // 从localStorage获取当前用户信息 + const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); + + if (!User.id) { + throw new Error("无法获取用户ID,请重新登录"); + } + + // 1. 创建Checkout Session + const result = await createCheckoutSession({ + user_id: String(User.id), + plan_name: planName, + billing_cycle: billingCycle + }); + + if (!result.successful || !result.data) { + throw new Error("create checkout session failed"); + } + + // 2. 直接跳转到Stripe托管页面(就这么简单!) + redirectToCheckout(result.data.checkout_url); + + } catch (error) { + throw new Error("create checkout session failed, please try again later"); + } + }; + + // 如果还没有加载到数据,显示加载状态但保持原有样式 + if (plans.length === 0) { + return ( +
+
+
+ +
+ +
+

Pricing

+

Choose the plan that suits you best

+ +
+ + +
+
+ +
+
+

正在加载订阅计划...

+
+
+
+
+ ); + } + + return ( +
+ {/* Main Content */} +
+ {/* Back Button */} +
+ +
+ +
+

Pricing

+

Choose the plan that suits you best

+ + {/* Billing Toggle */} +
+ + +
+
+ + {/* Plans Section */} +
+
+ {plans.map((plan) => { + const displayPlan = transformPlanForDisplay(plan); + + return ( +
+ {/* 预留标签空间,确保所有卡片组合高度一致 */} +
+ {displayPlan.popular && ( +
+ Most Popular +
+ )} +
+ + + + + {displayPlan.displayName} + +
+
+ {displayPlan.price[billingCycle] === 0 ? ( + 'Free' + ) : ( + <> + ${displayPlan.price[billingCycle]} + + /{billingCycle === 'monthly' ? 'month' : 'year'} + + + )} +
+
+ + + {displayPlan.description} + +
+ + +
+ {displayPlan.features.map((feature, index) => ( +
+
+ +
+ {feature} +
+ ))} +
+ + +
+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 1023125..c894d5c 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -73,6 +73,16 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
+ {/* Pricing Link */} + + {/* Notifications */} +
+
+ + )} +
{/* 工具栏-列表形式切换 */}
@@ -188,6 +256,20 @@ export function HomePage2() { />
+ {/* Pricing 入口 */} +
+ +
{ + e.stopPropagation(); + router.push('/pricing'); + }}> + + Pricing +
+
+
+
; + +export interface CreateCheckoutSessionRequest { + user_id: string; + plan_name: string; + billing_cycle: 'monthly' | 'yearly'; +} + +export interface CreateCheckoutSessionData { + checkout_url: string; + session_id: string; + biz_order_no: string; + amount: number; + currency: string; +} + +export type CreateCheckoutSessionResponse = ApiResponse; + +/** + * 获取订阅计划列表 + * 从后端API获取所有活跃的订阅计划,后端已经过滤了活跃计划 + */ +export async function fetchSubscriptionPlans(): Promise { + try { + const response = await get>('/api/subscription/plans'); + + if (!response.successful || !response.data) { + throw new Error(response.message || '获取订阅计划失败'); + } + + // 后端已经过滤了活跃计划,直接按排序顺序排列 + const sortedPlans = response.data.sort((a, b) => a.sort_order - b.sort_order); + + return sortedPlans; + } catch (error) { + console.error('获取订阅计划失败:', error); + throw error; + } +} + +/** + * 创建Checkout Session(推荐方案) + * + * 这是更简单的支付方案: + * 1. 调用此函数获取checkout_url + * 2. 直接跳转到checkout_url + * 3. 用户在Stripe页面完成支付 + * 4. 支付成功后自动跳转回success_url + */ +export async function createCheckoutSession( + request: CreateCheckoutSessionRequest +): Promise { + try { + return await post('/api/payment/checkoutDeepControl', request); + } catch (error) { + console.error('创建Checkout Session失败:', error); + throw error; + } +} + +/** + * 查询Checkout Session状态 + */ +export async function getCheckoutSessionStatus( + sessionId: string, + userId: string +): Promise { + try { + return await get(`/api/payment/checkout-status/${sessionId}?user_id=${userId}`); + } catch (error) { + console.error('查询Checkout Session状态失败:', error); + throw error; + } +} + +/** + * 简单的跳转到Checkout页面的工具函数 + */ +export function redirectToCheckout(checkoutUrl: string) { + if (typeof window !== 'undefined') { + window.location.href = checkoutUrl; + } +} \ No newline at end of file From 13e482ade315104529d53ebb4339dfce0f42f6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 26 Aug 2025 23:53:23 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BC=98=E5=8C=96chatbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/SmartChatBox/api.ts | 231 ++------------------------------- components/layout/top-bar.tsx | 140 ++++++++++++++++---- 2 files changed, 123 insertions(+), 248 deletions(-) diff --git a/components/SmartChatBox/api.ts b/components/SmartChatBox/api.ts index c24b255..7632e90 100644 --- a/components/SmartChatBox/api.ts +++ b/components/SmartChatBox/api.ts @@ -18,225 +18,16 @@ import { } from "./types"; import { post } from "@/api/request"; -// Mock 数据 -const MOCK_MESSAGES: RealApiMessage[] = [ - // 用户发送剧本 +// 空消息 默认展示 +const EMPTY_MESSAGES: RealApiMessage[] = [ { id: 1, - role: 'user', - content: JSON.stringify([{ - type: 'text', - content: '我想拍一个关于一个小女孩和她的机器人朋友的故事,故事发生在未来世界。' - }]), - created_at: '2024-03-20T10:00:00Z', - function_name: undefined, - custom_data: undefined, - status: 'success', - intent_type: 'chat' - }, - // 项目初始化 - { - id: 2, - role: 'system', - content: '我会帮您创建一个温馨感人的科幻短片,讲述人工智能与人类情感的故事。', - created_at: '2024-03-20T10:00:10Z', - function_name: 'create_project', - custom_data: { - project_data: { - script: '小女孩和机器人朋友的故事' - } - }, - status: 'success', - intent_type: 'procedure' - }, - // 剧本总结 - { - id: 3, - role: 'system', - content: '故事概要:在2045年的未来城市,10岁的小女孩艾米丽收到了一个特别的生日礼物——一个具有高度情感智能的机器人伙伴"小星"。随着时间推移,他们建立了深厚的友谊。当小星因能源耗尽即将永久关闭时,艾米丽想尽办法寻找解决方案,最终通过她的坚持和创意,成功为小星找到了新的能源,让这段跨越人机界限的友谊得以延续。', - created_at: '2024-03-20T10:01:00Z', - function_name: 'generate_script_summary', - custom_data: { - summary: '一个关于友谊和希望的温暖故事' - }, - status: 'success', - intent_type: 'procedure' - }, - // 角色生成 - 艾米丽 - { - id: 4, - role: 'system', - content: '主角艾米丽的形象已生成', - created_at: '2024-03-20T10:02:00Z', - function_name: 'generate_character', - custom_data: { - character_name: '艾米丽', - image_path: 'https://picsum.photos/seed/emily/300/400', - completed_count: 1, - total_count: 2 - }, - status: 'success', - intent_type: 'procedure' - }, - // 角色生成 - 小星 - { - id: 5, - role: 'system', - content: '机器人小星的形象已生成', - created_at: '2024-03-20T10:03:00Z', - function_name: 'generate_character', - custom_data: { - character_name: '小星', - image_path: 'https://picsum.photos/seed/robot/300/400', - completed_count: 2, - total_count: 2 - }, - status: 'success', - intent_type: 'procedure' - }, - // 场景生成 - 未来城市 - { - id: 6, - role: 'system', - content: '未来城市场景设计完成', - created_at: '2024-03-20T10:04:00Z', - function_name: 'generate_sketch', - custom_data: { - sketch_name: '未来城市街景', - image_path: 'https://picsum.photos/seed/city/600/400', - completed_count: 1, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 场景生成 - 艾米丽的房间 - { - id: 7, - role: 'system', - content: '艾米丽的未来风格卧室设计完成', - created_at: '2024-03-20T10:05:00Z', - function_name: 'generate_sketch', - custom_data: { - sketch_name: '艾米丽的卧室', - image_path: 'https://picsum.photos/seed/room/600/400', - completed_count: 2, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 场景生成 - 实验室 - { - id: 8, - role: 'system', - content: '高科技实验室场景设计完成', - created_at: '2024-03-20T10:06:00Z', - function_name: 'generate_sketch', - custom_data: { - sketch_name: '未来实验室', - image_path: 'https://picsum.photos/seed/lab/600/400', - completed_count: 3, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 分镜生成 - 相遇 - { - id: 9, - role: 'system', - content: '第一个分镜:艾米丽收到礼物时的场景', - created_at: '2024-03-20T10:07:00Z', - function_name: 'generate_shot_sketch', - custom_data: { - shot_type: '中景', - atmosphere: '温馨、期待', - key_action: '艾米丽惊喜地打开礼物盒,小星缓缓启动', - url: 'https://picsum.photos/seed/shot1/600/400', - completed_count: 1, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 分镜生成 - 危机 - { - id: 10, - role: 'system', - content: '第二个分镜:小星能源耗尽的场景', - created_at: '2024-03-20T10:08:00Z', - function_name: 'generate_shot_sketch', - custom_data: { - shot_type: '特写', - atmosphere: '紧张、担忧', - key_action: '小星的能源指示灯闪烁微弱,艾米丽神情焦急', - url: 'https://picsum.photos/seed/shot2/600/400', - completed_count: 2, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 分镜生成 - 解决 - { - id: 11, - role: 'system', - content: '第三个分镜:找到新能源解决方案的场景', - created_at: '2024-03-20T10:09:00Z', - function_name: 'generate_shot_sketch', - custom_data: { - shot_type: '全景', - atmosphere: '欢欣、胜利', - key_action: '实验室中艾米丽成功激活新能源,小星重新焕发活力', - url: 'https://picsum.photos/seed/shot3/600/400', - completed_count: 3, - total_count: 3 - }, - status: 'success', - intent_type: 'procedure' - }, - // 分镜视频生成 - { - id: 11.1, - role: 'system', - content: '分镜视频生成完成', - created_at: '2024-03-20T10:10:00Z', - function_name: 'generate_video', - custom_data: { - prompt_json: { - core_atmosphere: '欢欣、胜利', - }, - urls: ['https://cdn.qikongjian.com/faces/1755798635_facefusion_output_1755798635.mp4'], - completed_count: 1, - total_count: 1 - }, - status: 'success', - intent_type: 'procedure' - }, - // 用户反馈 - { - id: 12, - role: 'user', - content: JSON.stringify([{ - type: 'text', - content: '这个故事设计太棒了!特别喜欢艾米丽和小星的互动场景。' - }]), - created_at: '2024-03-20T10:10:00Z', - function_name: undefined, - custom_data: undefined, - status: 'success', - intent_type: 'function_call' - }, - // 助手回复 - { - id: 13, role: 'assistant', content: JSON.stringify([{ type: 'text', - content: '谢谢您的肯定!我们可以继续优化任何场景或角色设计,您觉得有什么地方需要调整吗?' + content: '🌟欢迎来到 MovieFlow 🎬✨\n快把您的创意告诉我吧~💡\n我是您的专属AI小伙伴🤖,可以帮您:\n🎭 生成专属演员形象\n📽️ 搭建场景 & 分镜\n🎞️ 完成整部视频创作\n\n一起开启奇妙的创作之旅吧!❤️' }]), - created_at: '2024-03-20T10:10:10Z', + created_at: new Date().toISOString(), function_name: undefined, custom_data: undefined, status: 'success', @@ -494,13 +285,13 @@ export async function fetchMessages( } // 转换消息并按时间排序 - // if (response.data.messages.length === 0) { - // return { - // messages: MOCK_MESSAGES.map(transformMessage), - // hasMore: false, - // totalCount: 0 - // }; - // } + if (response.data.messages.length === 0) { + return { + messages: EMPTY_MESSAGES.map(transformMessage), + hasMore: false, + totalCount: 0 + }; + } return { messages: response.data.messages .map(transformMessage) diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index c894d5c..7391168 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -3,28 +3,46 @@ import '../pages/style/top-bar.css'; import { Button } from '@/components/ui/button'; import { GradientText } from '@/components/ui/gradient-text'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { useTheme } from 'next-themes'; import { Sun, Moon, User, - Settings, + Sparkles, LogOut, Bell, - PanelsLeftBottom + PanelsLeftBottom, + Library } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; import { useRouter } from 'next/navigation'; +import React, { useRef, useEffect } from 'react'; export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onToggleSidebar: () => void }) { const { theme, setTheme } = useTheme(); const router = useRouter(); + const [isOpen, setIsOpen] = React.useState(false); + const menuRef = useRef(null); + const buttonRef = useRef(null); + const currentUser = localStorage.getItem('currentUser'); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); const handleAnimationEnd = (event: React.AnimationEvent) => { const element = event.currentTarget; @@ -99,25 +117,91 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT */} {/* User Menu */} - - - - - - - - Settings - - - - - Log out - - - +
+ + + + {isOpen && ( + + {/* User Info */} +
+
+
+ A +
+
+

admin-live

+

admin-live.com

+
+
+
+ + {/* AI Points */} +
+
+ + 100 点数 +
+ +
+ + {/* Menu Items */} +
+ router.push('/my-library')} + data-alt="my-library-button" + > + + 我的库 + + + { + // 处理退出登录 + setIsOpen(false); + }} + data-alt="logout-button" + > + + 退出账号 + + + {/* Footer */} +
+
隐私权和条款 · 许可
+
250819215404 | 2025/8/20 06:00:50
+
+
+
+ )} +
+
From e7718340c499479b0b4271c0a58be1d22e0719ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Wed, 27 Aug 2025 00:15:11 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=B1=82=E7=BA=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/home-page2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index 56c0231..50c4ec7 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -189,7 +189,7 @@ export function HomePage2() {
{/* 工具栏-列表形式切换 */} -
+
{ e.stopPropagation(); From c56043d85c2e97512ecd3e614eba39e3e4b4e057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Wed, 27 Aug 2025 20:11:08 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E6=9D=83=E9=99=90=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/errorHandle.ts | 40 +++++++- api/request.ts | 77 +++++++++++--- app/globals.css | 4 + components/SmartChatBox/MessageRenderer.tsx | 2 + components/SmartChatBox/api.ts | 67 +++++++----- components/SmartChatBox/types.ts | 5 +- components/layout/top-bar.tsx | 5 +- utils/notifications.tsx | 108 ++++++++++++++++++++ 8 files changed, 260 insertions(+), 48 deletions(-) create mode 100644 utils/notifications.tsx diff --git a/api/errorHandle.ts b/api/errorHandle.ts index 3adaafd..6fdac06 100644 --- a/api/errorHandle.ts +++ b/api/errorHandle.ts @@ -16,22 +16,52 @@ const HTTP_ERROR_MESSAGES: Record = { 503: "Service temporarily unavailable, please try again later.", 504: "Gateway timeout, please try again later.", }; + /** * 默认错误提示信息 */ -const DEFAULT_ERROR_MESSAGE = - "Please try again if the network is abnormal. If it happens again, please contact us."; +const DEFAULT_ERROR_MESSAGE = "网络异常,请重试。如果问题持续存在,请联系我们。"; /** - * 根据错误码显示对应的提示信息 + * 特殊错误码的处理函数 + */ +const ERROR_HANDLERS: Record void> = { + 401: () => { + // 清除本地存储的 token + localStorage.removeItem('token'); + // 跳转到登录页面 + window.location.href = '/login'; + }, + 403: () => { + // 显示积分不足通知 + import('../utils/notifications').then(({ showInsufficientPointsNotification }) => { + showInsufficientPointsNotification(); + }); + } +}; + +/** + * 根据错误码显示对应的提示信息并执行相应处理 * @param code - HTTP错误码 * @param customMessage - 自定义错误信息(可选) */ export const errorHandle = debounce( (code: number, customMessage?: string): void => { - const errorMessage = + const errorMessage = customMessage || HTTP_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGE; - message.error(errorMessage); + + // 显示错误提示 + message.error({ + content: errorMessage, + duration: 3, + className: 'custom-error-message' + }); + + // 执行特殊错误码的处理函数 + const handler = ERROR_HANDLERS[code]; + if (handler) { + handler(); + } }, 100 ); diff --git a/api/request.ts b/api/request.ts index 4f86d78..73a19bd 100644 --- a/api/request.ts +++ b/api/request.ts @@ -2,6 +2,25 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosR import { message } from "antd"; import { BASE_URL } from './constants' import { errorHandle } from './errorHandle'; + +/** + * 统一的错误处理函数 + * @param error - 错误对象 + * @param defaultMessage - 默认错误信息 + */ +const handleRequestError = (error: any, defaultMessage: string = '请求失败') => { + if (error.response) { + const { status, data } = error.response; + const errorMessage = data?.message || defaultMessage; + errorHandle(status, errorMessage); + } else if (error.request) { + // 请求已发出但没有收到响应 + errorHandle(0, '网络请求失败,请检查网络连接'); + } else { + // 请求配置出错 + errorHandle(0, error.message || defaultMessage); + } +}; // 创建 axios 实例 const request: AxiosInstance = axios.create({ baseURL: BASE_URL, // 设置基础URL @@ -29,23 +48,32 @@ request.interceptors.request.use( // 响应拦截器 request.interceptors.response.use( (response: AxiosResponse) => { - // 直接返回响应数据 - if (response.data?.code !=0) { - // TODO 暂时固定报错信息,后续根据后端返回的错误码进行处理 - errorHandle(0); + // 检查业务状态码 + if (response.data?.code !== 0) { + // 处理业务层面的错误 + const businessCode = response.data?.code; + const errorMessage = response.data?.message; + + // 特殊处理 401 和 403 业务状态码 + if (businessCode === 401) { + errorHandle(401, errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + if (businessCode === 403) { + errorHandle(403, errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + // 其他业务错误 + errorHandle(0, errorMessage); + return Promise.reject(new Error(errorMessage)); } + return response.data; }, (error) => { - if (error.response) { - errorHandle(error.response.status); - } else if (error.request) { - // 请求已发出但没有收到响应 - errorHandle(0); - } else { - // 检修 - console.error(error); - } + handleRequestError(error); return Promise.reject(error); } ); @@ -86,8 +114,23 @@ export async function streamJsonPost( body: JSON.stringify(body), }); + // 处理 HTTP 错误状态 if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const error = { + response: { + status: response.status, + data: { message: await response.text().then(text => { + try { + const data = JSON.parse(text); + return data.message || `HTTP error! status: ${response.status}`; + } catch { + return `HTTP error! status: ${response.status}`; + } + })} + } + }; + handleRequestError(error); + throw error; } if (!response.body) { @@ -207,7 +250,8 @@ export const stream = async ({ const response = await request(config); onComplete?.(); return response; - } catch (error) { + } catch (error: any) { + handleRequestError(error, '流式请求失败'); onError?.(error); throw error; } @@ -239,8 +283,9 @@ export const downloadStream = async ( window.URL.revokeObjectURL(downloadUrl); return response; - } catch (error) { + } catch (error: any) { console.error('文件下载失败:', error); + handleRequestError(error, '文件下载失败'); throw error; } }; diff --git a/app/globals.css b/app/globals.css index 6916fe7..a37fc43 100644 --- a/app/globals.css +++ b/app/globals.css @@ -239,3 +239,7 @@ body { .animate-fade-in { animation: fade-in 0.2s ease-out forwards; } + +.ant-notification-notice-wrapper { + background: transparent !important; +} \ No newline at end of file diff --git a/components/SmartChatBox/MessageRenderer.tsx b/components/SmartChatBox/MessageRenderer.tsx index f158c7c..510b509 100644 --- a/components/SmartChatBox/MessageRenderer.tsx +++ b/components/SmartChatBox/MessageRenderer.tsx @@ -124,6 +124,8 @@ export function MessageRenderer({ msg }: MessageRendererProps) { ); case "progress": return ; + case "link": + return {b.text}; default: return null; } diff --git a/components/SmartChatBox/api.ts b/components/SmartChatBox/api.ts index 7632e90..e96e942 100644 --- a/components/SmartChatBox/api.ts +++ b/components/SmartChatBox/api.ts @@ -25,7 +25,7 @@ const EMPTY_MESSAGES: RealApiMessage[] = [ role: 'assistant', content: JSON.stringify([{ type: 'text', - content: '🌟欢迎来到 MovieFlow 🎬✨\n快把您的创意告诉我吧~💡\n我是您的专属AI小伙伴🤖,可以帮您:\n🎭 生成专属演员形象\n📽️ 搭建场景 & 分镜\n🎞️ 完成整部视频创作\n\n一起开启奇妙的创作之旅吧!❤️' + content: '🌟Welcome to MovieFlow 🎬✨\nTell me your idea~💡\nI am your AI assistant🤖, I can help you:\n🎭 Generate actor images\n📽️ Generate scene & shot sketches\n🎞️ Complete video creation\n\nLet\'s start our creative journey together!❤️' }]), created_at: new Date().toISOString(), function_name: undefined, @@ -35,6 +35,19 @@ const EMPTY_MESSAGES: RealApiMessage[] = [ } ]; +// 用户积分不足消息 +const NoEnoughCreditsMessageBlocks: MessageBlock[] = [ + { + type: 'text', + text: 'Insufficient credits.' + }, + { + type: 'link', + text: 'Upgrade to continue.', + url: '/pricing' + } +]; + /** * 类型守卫函数 */ @@ -195,7 +208,7 @@ function transformSystemMessage( */ function transformMessage(apiMessage: RealApiMessage): ChatMessage { try { - const { id, role, content, created_at, function_name, custom_data, status, intent_type } = apiMessage; + const { id, role, content, created_at, function_name, custom_data, status, intent_type, error_message } = apiMessage; let message: ChatMessage = { id: id ? id.toString() : Date.now().toString(), role: role, @@ -205,30 +218,36 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage { status: status || 'success', }; - if (role === 'assistant' || role === 'user') { - try { - const contentObj = JSON.parse(content); - const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj]; - contentArray.forEach((c: ApiMessageContent) => { - if (c.type === "text") { - message.blocks.push({ type: "text", text: c.content }); - } else if (c.type === "image") { - message.blocks.push({ type: "image", url: c.content }); - } else if (c.type === "video") { - message.blocks.push({ type: "video", url: c.content }); - } else if (c.type === "audio") { - message.blocks.push({ type: "audio", url: c.content }); - } - }); - } catch (error) { - // 如果 JSON 解析失败,将整个 content 作为文本内容 + if (error_message && error_message === 'no enough credits') { + message.blocks = NoEnoughCreditsMessageBlocks; + } else { + if (role === 'assistant' || role === 'user') { + try { + const contentObj = JSON.parse(content); + const contentArray = Array.isArray(contentObj) ? contentObj : [contentObj]; + contentArray.forEach((c: ApiMessageContent) => { + if (c.type === "text") { + message.blocks.push({ type: "text", text: c.content }); + } else if (c.type === "image") { + message.blocks.push({ type: "image", url: c.content }); + } else if (c.type === "video") { + message.blocks.push({ type: "video", url: c.content }); + } else if (c.type === "audio") { + message.blocks.push({ type: "audio", url: c.content }); + } else if (c.type === "link") { + message.blocks.push({ type: "link", text: c.content, url: c.url || '' }); + } + }); + } catch (error) { + // 如果 JSON 解析失败,将整个 content 作为文本内容 + message.blocks.push({ type: "text", text: content }); + } + } else if (role === 'system' && function_name && custom_data) { + // 处理系统消息 + message.blocks = transformSystemMessage(function_name, content, custom_data); + } else { message.blocks.push({ type: "text", text: content }); } - } else if (role === 'system' && function_name && custom_data) { - // 处理系统消息 - message.blocks = transformSystemMessage(function_name, content, custom_data); - } else { - message.blocks.push({ type: "text", text: content }); } // 如果没有有效的 blocks,至少添加一个文本块 diff --git a/components/SmartChatBox/types.ts b/components/SmartChatBox/types.ts index 66a2fbb..3587c56 100644 --- a/components/SmartChatBox/types.ts +++ b/components/SmartChatBox/types.ts @@ -7,7 +7,8 @@ export type MessageBlock = | { type: "image"; url: string; alt?: string } | { type: "video"; url: string; poster?: string } | { type: "audio"; url: string } - | { type: "progress"; value: number; total?: number; label?: string }; + | { type: "progress"; value: number; total?: number; label?: string } + | { type: "link"; text: string; url: string }; export interface ChatMessage { id: string; @@ -127,6 +128,7 @@ export interface ShotVideoGeneration { export interface ApiMessageContent { type: ContentType; content: string; + url?: string; } export interface RealApiMessage { @@ -138,4 +140,5 @@ export interface RealApiMessage { custom_data?: ProjectInit | ScriptSummary | CharacterGeneration | SketchGeneration | ShotSketchGeneration | ShotVideoGeneration; status: MessageStatus; intent_type: IntentType; + error_message?: string; } \ No newline at end of file diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index 7391168..cf8082f 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -25,6 +25,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT const menuRef = useRef(null); const buttonRef = useRef(null); const currentUser = localStorage.getItem('currentUser'); + const [openModal, setOpenModal] = React.useState(false); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -102,9 +103,9 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT {/* Notifications */} - + */} {/* Theme Toggle */} {/* +
+ ), + duration: 5, + placement: 'topRight', + style: darkGlassStyle, + className: 'dark-glass-notification', + closeIcon: ( + + ), + }); +}; + +/** + * 全局配置通知样式 + */ +notification.config({ + maxCount: 3, // 最多同时显示3个通知 +}); From df7d8c8a0ddb1957a6f3d6ea051e092ade44f111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Wed, 27 Aug 2025 20:22:56 +0800 Subject: [PATCH 5/7] =?UTF-8?q?chatbox=20=E8=8B=B1=E6=96=87=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/SmartChatBox/api.ts | 48 +++++++++++++++++----------------- utils/notifications.tsx | 6 ++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/components/SmartChatBox/api.ts b/components/SmartChatBox/api.ts index e96e942..d1a67a6 100644 --- a/components/SmartChatBox/api.ts +++ b/components/SmartChatBox/api.ts @@ -90,7 +90,7 @@ function transformSystemMessage( if (isProjectInit(customData)) { blocks = [{ type: 'text', - text: `🎬 根据您输入的 "${customData.project_data.script}",我已完成项目的初始化。\n${content}` + text: `🎬 According to your input "${customData.project_data.script}", I have completed the initialization of the project.\n${content}` }]; } break; @@ -98,7 +98,7 @@ function transformSystemMessage( case 'generate_script_summary': if (isScriptSummary(customData)) { blocks = [ - { type: 'text', text: `🎬 剧本摘要生成完成\n\n${customData.summary}\n\n${content}` } + { type: 'text', text: `🎬 I have completed the script summary generation.\n\n${customData.summary}\n\n${content}` } ]; } break; @@ -107,18 +107,18 @@ function transformSystemMessage( if (isCharacterGeneration(customData)) { blocks = [{ type: 'text', - text: `🎭 演员 "${customData.character_name}" 已就位` + text: `🎭 Actor "${customData.character_name}" is ready.` }, { type: 'image', url: customData.image_path }, { type: 'text', - text: '图片中演员形象仅供参考,后续可根据视频生成后进行调整。' + text: 'The actor image is for reference only, and can be adjusted after the video is generated.' }, { type: 'progress', value: customData.completed_count, total: customData.total_count, - label: `已完成 ${customData.completed_count} 个演员,共有 ${customData.total_count} 个` + label: `Completed ${customData.completed_count} actors, total ${customData.total_count} actors` }, { type: 'text', text: `\n${content}` @@ -130,18 +130,18 @@ function transformSystemMessage( if (isSketchGeneration(customData)) { blocks = [{ type: 'text', - text: `🎨 场景 "${customData.sketch_name}" 参考图片已生成 \n` + text: `🎨 Scene "${customData.sketch_name}" reference image generated \n` }, { type: 'image', url: customData.image_path }, { type: 'text', - text: '图片中场景仅供参考,后续可根据视频生成后进行调整。' + text: 'The scene image is for reference only, and can be adjusted after the video is generated.' }, { type: 'progress', value: customData.completed_count, total: customData.total_count, - label: `已完成 ${customData.completed_count} 个场景,共有 ${customData.total_count} 个` + label: `Completed ${customData.completed_count} scenes, total ${customData.total_count} scenes` }, { type: 'text', text: `\n${content}` @@ -153,18 +153,18 @@ function transformSystemMessage( if (isShotSketchGeneration(customData)) { blocks = [{ type: 'text', - text: `🎬 故事板静帧生成 \n镜头类型:${customData.shot_type}\n氛围:${customData.atmosphere}\n关键动作:${customData.key_action}` + text: `🎬 Storyboard static frame generation \nShot type: ${customData.shot_type}\nAtmosphere: ${customData.atmosphere}\nKey action: ${customData.key_action}` }, { type: 'image', url: customData.url }, { type: 'text', - text: '图片中故事板静帧仅供参考,后续可根据视频生成后进行调整。' + text: 'The storyboard static frame image is for reference only, and can be adjusted after the video is generated.' }, { type: 'progress', value: customData.completed_count, total: customData.total_count, - label: `已完成 ${customData.completed_count} 个故事板静帧,共有 ${customData.total_count} 个` + label: `Completed ${customData.completed_count} storyboard static frames, total ${customData.total_count} storyboard static frames` }, { type: 'text', text: `\n${content}` @@ -176,7 +176,7 @@ function transformSystemMessage( if (isShotVideoGeneration(customData)) { blocks.push({ type: 'text', - text: `🎬 该分镜下包含${customData.urls.length} 个视频。 \n核心氛围:${customData.prompt_json.core_atmosphere}` + text: `🎬 There are ${customData.urls.length} videos in this shot. \nCore atmosphere: ${customData.prompt_json.core_atmosphere}` }); customData.urls.forEach((url: string) => { blocks.push({ @@ -186,12 +186,12 @@ function transformSystemMessage( }); blocks.push({ type: 'text', - text: '后续可在剪辑线上进行编辑。' + text: 'You can edit the video on the editing line later.' }, { type: 'progress', value: customData.completed_count, total: customData.total_count, - label: `已完成 ${customData.completed_count} 个分镜,共有 ${customData.total_count} 个分镜` + label: `Completed ${customData.completed_count} shots, total ${customData.total_count} shots` }, { type: 'text', text: `\n${content}` @@ -252,18 +252,18 @@ function transformMessage(apiMessage: RealApiMessage): ChatMessage { // 如果没有有效的 blocks,至少添加一个文本块 if (message.blocks.length === 0) { - message.blocks.push({ type: "text", text: "无内容" }); + message.blocks.push({ type: "text", text: "No content" }); } return message; } catch (error) { - console.error("转换消息格式失败:", error, apiMessage); + console.error("Failed to transform message format:", error, apiMessage); // 返回一个带有错误信息的消息 return { id: new Date().getTime().toString(), role: apiMessage.role, createdAt: new Date(apiMessage.created_at).getTime(), - blocks: [{ type: "text", text: "消息格式错误" }], + blocks: [{ type: "text", text: "Message format error" }], chatType: 'chat', status: 'error', }; @@ -289,13 +289,13 @@ export async function fetchMessages( }; try { - console.log('发送历史消息请求:', request); + console.log('Send history message request:', request); const response = await post>("/intelligent/history", request); - console.log('收到历史消息响应:', response); + console.log('Receive history message response:', response); // 确保 response.data 和 messages 存在 if (!response.data || !response.data.messages) { - console.error('历史消息响应格式错误:', response); + console.error('History message response format error:', response); return { messages: [], hasMore: false, @@ -319,7 +319,7 @@ export async function fetchMessages( totalCount: response.data.total_count }; } catch (error) { - console.error("获取消息历史失败:", error); + console.error("Failed to get message history:", error); throw error; } } @@ -360,10 +360,10 @@ export async function sendMessage( } try { - console.log('发送消息请求:', request); + console.log('Send message request:', request); await post>("/intelligent/chat", request); } catch (error) { - console.error("发送消息失败:", error); + console.error("Send message failed:", error); throw error; } } @@ -377,5 +377,5 @@ export async function retryMessage( ): Promise { // TODO: 实现实际的重试逻辑,可能需要保存原始消息内容 // 这里简单重用发送消息的接口 - return sendMessage([{ type: "text", text: "重试消息" }], config); + return sendMessage([{ type: "text", text: "Retry message" }], config); } \ No newline at end of file diff --git a/utils/notifications.tsx b/utils/notifications.tsx index 1db196c..a516df2 100644 --- a/utils/notifications.tsx +++ b/utils/notifications.tsx @@ -64,15 +64,15 @@ export const showInsufficientPointsNotification = () => { description: (

- 积分不足提醒 + Insufficient credits reminder

-

您的积分余额不足,无法继续使用该功能

+

Your credits are insufficient, please upgrade to continue.

), From 6022c6ec252342f6f597c100c59cf165a2958b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Wed, 27 Aug 2025 22:57:52 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/request.ts | 2 +- components/SmartChatBox/SmartChatBox.tsx | 4 +- components/layout/dashboard-layout.tsx | 1 - components/layout/top-bar.tsx | 141 ++++++++++++++------- components/pages/login.tsx | 4 + components/ui/FloatingGlassPanel.tsx | 5 +- lib/api.ts | 152 ----------------------- 7 files changed, 109 insertions(+), 200 deletions(-) delete mode 100644 lib/api.ts diff --git a/api/request.ts b/api/request.ts index 73a19bd..d04ed06 100644 --- a/api/request.ts +++ b/api/request.ts @@ -104,7 +104,7 @@ export async function streamJsonPost( onJson: (json: T) => void ) { try { - const token = localStorage?.getItem('token') || 'mock-token'; + const token = localStorage?.getItem('token') || ''; const response = await fetch(`${BASE_URL}${url}`, { method: 'POST', headers: { diff --git a/components/SmartChatBox/SmartChatBox.tsx b/components/SmartChatBox/SmartChatBox.tsx index a9fae09..5c05de1 100644 --- a/components/SmartChatBox/SmartChatBox.tsx +++ b/components/SmartChatBox/SmartChatBox.tsx @@ -133,8 +133,8 @@ export default function SmartChatBox({ Chat {/* System push toggle */} - setSidebarCollapsed(!sidebarCollapsed)} /> {children}
diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index cf8082f..7e246fe 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -10,40 +10,73 @@ import { User, Sparkles, LogOut, - Bell, - PanelsLeftBottom, - Library } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; +import ReactDOM from 'react-dom'; import { useRouter } from 'next/navigation'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useLayoutEffect } from 'react'; +import { logoutUser } from '@/lib/auth'; + + +interface User { + id: string; + name: string; + email: string; + avatar: string; +} export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onToggleSidebar: () => void }) { - const { theme, setTheme } = useTheme(); const router = useRouter(); const [isOpen, setIsOpen] = React.useState(false); const menuRef = useRef(null); const buttonRef = useRef(null); - const currentUser = localStorage.getItem('currentUser'); - const [openModal, setOpenModal] = React.useState(false); + const currentUser: User = JSON.parse(localStorage.getItem('currentUser') || '{}'); + const [mounted, setMounted] = React.useState(false); + useLayoutEffect(() => { + console.log('Setting mounted state'); + setMounted(true); + return () => console.log('Cleanup mounted effect'); + }, []); + + + // 处理点击事件 useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - menuRef.current && - !menuRef.current.contains(event.target as Node) && - buttonRef.current && - !buttonRef.current.contains(event.target as Node) - ) { + if (!isOpen) return; + + let isClickStartedInside = false; + + const handleMouseDown = (event: MouseEvent) => { + const target = event.target as Node; + isClickStartedInside = !!( + menuRef.current?.contains(target) || + buttonRef.current?.contains(target) + ); + }; + + const handleMouseUp = (event: MouseEvent) => { + const target = event.target as Node; + const isClickEndedInside = !!( + menuRef.current?.contains(target) || + buttonRef.current?.contains(target) + ); + + // 只有当点击开始和结束都在外部时才关闭 + if (!isClickStartedInside && !isClickEndedInside) { setIsOpen(false); } + isClickStartedInside = false; }; - document.addEventListener('mousedown', handleClickOutside); + // 在冒泡阶段处理事件 + document.addEventListener('mousedown', handleMouseDown); + document.addEventListener('mouseup', handleMouseUp); + return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('mouseup', handleMouseUp); }; - }, []); + }, [isOpen]); const handleAnimationEnd = (event: React.AnimationEvent) => { const element = event.currentTarget; @@ -56,14 +89,11 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT }; return ( -
-
+
+
-
router.push('/')} onMouseEnter={handleMouseEnter} onAnimationEnd={handleAnimationEnd} @@ -88,6 +118,12 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT + {/* beta标签 */} +
+ + Beta + +
@@ -118,37 +154,56 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT */} {/* User Menu */} -
+
- - {isOpen && ( + {mounted && isOpen ? ReactDOM.createPortal( e.stopPropagation()} > {/* User Info */}
- A + {currentUser.name ? currentUser.name.charAt(0) : ''}
-
-

admin-live

-

admin-live.com

+
+

{currentUser.name}

+

{currentUser.email}

+
+
{ + logoutUser(); + }} + title="退出登录" + > +
@@ -157,27 +212,28 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
- 100 点数 + 100 credits
{/* Menu Items */}
- router.push('/my-library')} data-alt="my-library-button" > - 我的库 + My Library - 退出账号 - + Logout + */} {/* Footer */}
-
隐私权和条款 · 许可
+
Privacy Policy · Terms of Service
250819215404 | 2025/8/20 06:00:50
- )} - + , document.body) + : null}
+
); } \ No newline at end of file diff --git a/components/pages/login.tsx b/components/pages/login.tsx index 2d806d4..2f58b39 100644 --- a/components/pages/login.tsx +++ b/components/pages/login.tsx @@ -81,6 +81,10 @@ export default function Login() { endPercentage={70} /> + {/* beta标签 */} + + Beta +
diff --git a/components/ui/FloatingGlassPanel.tsx b/components/ui/FloatingGlassPanel.tsx index 588efcf..f8470bc 100644 --- a/components/ui/FloatingGlassPanel.tsx +++ b/components/ui/FloatingGlassPanel.tsx @@ -11,9 +11,10 @@ type FloatingGlassPanelProps = { width?: string; r_key?: string | number; panel_style?: React.CSSProperties; + className?: string; }; -export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style, clickMaskClose = true }: FloatingGlassPanelProps) { +export default function FloatingGlassPanel({ open, onClose, children, width = '320px', r_key, panel_style, clickMaskClose = true, className }: FloatingGlassPanelProps) { // 定义弹出动画 const bounceAnimation = { scale: [0.95, 1.02, 0.98, 1], @@ -23,7 +24,7 @@ export default function FloatingGlassPanel({ open, onClose, children, width = '3 return ( {open && ( -
+
=> { - const token = getToken(); - - // 构建请求头 - const headers: Record = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - ...(options.headers as Record), - }; - - // 添加token到请求头(如果存在) - if (token) { - headers['X-EASE-ADMIN-TOKEN'] = token; - } - - try { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...options, - headers, - }); - - // 检查响应状态 - if (!response.ok) { - if (response.status === 401) { - // Token过期或无效 - clearAuthData(); - window.location.href = '/login'; - throw new Error('身份验证失败,请重新登录'); - } - throw new Error(`请求失败: ${response.status}`); - } - - const data = await response.json(); - - // 检查业务状态码 - if (data.code === '401' || data.status === 401) { - clearAuthData(); - window.location.href = '/login'; - throw new Error('身份验证失败,请重新登录'); - } - - return data; - } catch (error) { - console.error('API request failed:', error); - throw error; - } -}; - -/** - * GET请求 - */ -export const apiGet = (endpoint: string, options: RequestInit = {}) => { - return apiRequest(endpoint, { - ...options, - method: 'GET', - }); -}; - -/** - * POST请求 - */ -export const apiPost = (endpoint: string, data?: any, options: RequestInit = {}) => { - return apiRequest(endpoint, { - ...options, - method: 'POST', - body: data ? JSON.stringify(data) : undefined, - }); -}; - -/** - * PUT请求 - */ -export const apiPut = (endpoint: string, data?: any, options: RequestInit = {}) => { - return apiRequest(endpoint, { - ...options, - method: 'PUT', - body: data ? JSON.stringify(data) : undefined, - }); -}; - -/** - * DELETE请求 - */ -export const apiDelete = (endpoint: string, options: RequestInit = {}) => { - return apiRequest(endpoint, { - ...options, - method: 'DELETE', - }); -}; - -/** - * 文件上传请求 - */ -export const apiUpload = async (endpoint: string, formData: FormData, options: RequestInit = {}) => { - const token = getToken(); - - // 构建请求头(文件上传时不设置Content-Type,让浏览器自动设置) - const headers: Record = { - 'Accept': 'application/json', - ...(options.headers as Record), - }; - - // 添加token到请求头(如果存在) - if (token) { - headers['X-EASE-ADMIN-TOKEN'] = token; - } - - try { - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...options, - method: 'POST', - headers, - body: formData, - }); - - // 检查响应状态 - if (!response.ok) { - if (response.status === 401) { - // Token过期或无效 - clearAuthData(); - window.location.href = '/login'; - throw new Error('身份验证失败,请重新登录'); - } - throw new Error(`请求失败: ${response.status}`); - } - - const data = await response.json(); - - // 检查业务状态码 - if (data.code === '401' || data.status === 401) { - clearAuthData(); - window.location.href = '/login'; - throw new Error('身份验证失败,请重新登录'); - } - - return data; - } catch (error) { - console.error('API request failed:', error); - throw error; - } -}; \ No newline at end of file From d734179302ab3c2a7a2aafe7f03ea150b026e441 Mon Sep 17 00:00:00 2001 From: Zixin Zhou Date: Wed, 27 Aug 2025 23:56:27 +0800 Subject: [PATCH 7/7] update pay --- app/dashboard/page.tsx | 3 ++- app/payment-success/page.tsx | 3 ++- app/pricing/page.tsx | 32 ++++++++++++++++---------------- components/pages/home-page2.tsx | 17 ++++++----------- lib/stripe.ts | 6 +++--- 5 files changed, 29 insertions(+), 32 deletions(-) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 6047257..036c12f 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -58,7 +58,8 @@ export default function DashboardPage() { // 获取支付详情 const fetchPaymentDetails = async (sessionId: string) => { try { - const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=test_user_123`); + const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); + const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=${User.id}`); const result = await response.json(); if (result.successful && result.data) { diff --git a/app/payment-success/page.tsx b/app/payment-success/page.tsx index 66963da..589d424 100644 --- a/app/payment-success/page.tsx +++ b/app/payment-success/page.tsx @@ -42,7 +42,8 @@ export default function PaymentSuccessPage() { try { // 使用新的Checkout Session状态查询 const { getCheckoutSessionStatus } = await import('@/lib/stripe'); - const result = await getCheckoutSessionStatus(sessionId, 'test_user_123'); // 临时测试用户ID + const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); + const result = await getCheckoutSessionStatus(sessionId, User.id); if (result.successful && result.data) { setPaymentData(result.data); diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 4a8316d..7f61a3a 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -9,7 +9,7 @@ import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe'; export default function PricingPage() { const router = useRouter(); - const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly'); + const [billingCycle, setBillingCycle] = useState<'month' | 'year'>('month'); const [plans, setPlans] = useState([]); // 从后端获取订阅计划数据 @@ -28,15 +28,15 @@ export default function PricingPage() { // 转换后端数据为前端显示格式,保持原有的数据结构 const transformPlanForDisplay = (plan: SubscriptionPlan) => { - const monthlyPrice = plan.price_monthly / 100; // 后端存储的是分,转换为元 - const yearlyPrice = plan.price_yearly / 100; + const monthlyPrice = plan.price_month / 100; // 后端存储的是分,转换为元 + const yearlyPrice = plan.price_year / 100; return { name: plan.name, displayName: plan.display_name, price: { - monthly: monthlyPrice, - yearly: yearlyPrice + month: monthlyPrice, + year: yearlyPrice }, description: plan.description, features: plan.features || [], @@ -103,16 +103,16 @@ export default function PricingPage() {