From 581e874b0d0e30d5bb6f46323444cec2a220a65f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 26 Nov 2025 10:11:02 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=8F=90=E7=A4=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Button/index.tsx | 76 +-- src/components/Button2/index.tsx | 53 -- src/components/Generating/index.tsx | 37 +- src/components/Image/index.tsx | 28 +- src/components/Join/index.js | 73 --- src/components/Join/index.tsx | 73 --- src/components/Logos/index.js | 53 -- src/components/Logos/index.tsx | 53 -- src/components/Notification/index.js | 49 -- src/components/Notification/index.tsx | 49 -- src/components/Section/index.tsx | 116 ++-- src/components/Services/index.js | 195 ------- src/components/Services/index.tsx | 195 ------- src/hooks/useSubscriptionEvents.js | 394 -------------- src/hooks/useSubscriptionEvents.ts | 382 +++++++++++++ src/types/static-assets.d.ts | 44 ++ .../Dashboard/components/CalendarPanel.tsx | 4 +- src/views/Dashboard/components/PlansPanel.tsx | 2 +- .../Dashboard/components/ReviewsPanel.tsx | 2 +- src/views/DataBrowser/KLineChartView.tsx | 15 +- src/views/DataBrowser/TradingViewChart.tsx | 508 ------------------ src/views/DataBrowser/index.tsx | 8 +- src/views/Pricing/index.tsx | 2 +- 23 files changed, 587 insertions(+), 1824 deletions(-) delete mode 100644 src/components/Button2/index.tsx delete mode 100644 src/components/Join/index.js delete mode 100644 src/components/Join/index.tsx delete mode 100644 src/components/Logos/index.js delete mode 100644 src/components/Logos/index.tsx delete mode 100644 src/components/Notification/index.js delete mode 100644 src/components/Notification/index.tsx delete mode 100644 src/components/Services/index.js delete mode 100644 src/components/Services/index.tsx delete mode 100644 src/hooks/useSubscriptionEvents.js create mode 100644 src/hooks/useSubscriptionEvents.ts create mode 100644 src/types/static-assets.d.ts delete mode 100644 src/views/DataBrowser/TradingViewChart.tsx diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 8c05f339..6f964b59 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,40 +1,52 @@ -import { Link } from "react-router-dom"; -import { svgs } from "./svgs"; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { svgs } from './svgs'; -const Button = ({ - className, - href, - onClick, - children, - px, - white, +interface ButtonProps { + className?: string; + href?: string; + onClick?: () => void; + children?: React.ReactNode; + px?: string; + white?: boolean; + isPrimary?: boolean; + isSecondary?: boolean; +} + +const Button: React.FC = ({ + className, + href, + onClick, + children, + px, + white, }) => { - const classes = `button relative inline-flex items-center justify-center h-11 ${ - px || "px-7" - } ${white ? "text-n-8" : "text-n-1"} transition-colors hover:text-color-1 ${ - className || "" - }`; + const classes = `button relative inline-flex items-center justify-center h-11 ${ + px || 'px-7' + } ${white ? 'text-n-8' : 'text-n-1'} transition-colors hover:text-color-1 ${ + className || '' + }`; - const spanClasses = `relative z-10`; + const spanClasses = `relative z-10`; - return href ? ( - href.startsWith("mailto:") ? ( - - {children} - {svgs(white)} - - ) : ( - - {children} - {svgs(white)} - - ) + return href ? ( + href.startsWith('mailto:') ? ( + + {children} + {svgs(white)} + ) : ( - - ); + + {children} + {svgs(white)} + + ) + ) : ( + + ); }; export default Button; diff --git a/src/components/Button2/index.tsx b/src/components/Button2/index.tsx deleted file mode 100644 index 22f74066..00000000 --- a/src/components/Button2/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import Link, { LinkProps } from "next/link"; - -type CommonProps = { - className?: string; - children?: React.ReactNode; - isPrimary?: boolean; - isSecondary?: boolean; -}; - -type ButtonAsButton = { - as?: "button"; -} & React.ButtonHTMLAttributes; - -type ButtonAsAnchor = { - as: "a"; -} & React.AnchorHTMLAttributes; - -type ButtonAsLink = { - as: "link"; -} & LinkProps; - -type ButtonProps = CommonProps & - (ButtonAsButton | ButtonAsAnchor | ButtonAsLink); - -const Button: React.FC = ({ - className, - children, - isPrimary, - isSecondary, - as = "button", - ...props -}) => { - const isLink = as === "link"; - const Component: React.ElementType = isLink ? Link : as; - - return ( - - {children} - - ); -}; - -export default Button; diff --git a/src/components/Generating/index.tsx b/src/components/Generating/index.tsx index 37f80776..044093b7 100644 --- a/src/components/Generating/index.tsx +++ b/src/components/Generating/index.tsx @@ -1,20 +1,25 @@ -import Image from "../Image"; +import React from 'react'; +import Image from '../Image'; -const Generating = ({ className }) => ( -
- Loading - AI is generating| -
+interface GeneratingProps { + className?: string; +} + +const Generating: React.FC = ({ className }) => ( +
+ Loading + AI is generating| +
); export default Generating; diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 13104389..f6a68ae5 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -1,17 +1,21 @@ -import { useState } from "react"; +import React, { useState } from 'react'; -const Image = ({ className, ...props }) => { - const [loaded, setLoaded] = useState(false); +interface ImageProps extends React.ImgHTMLAttributes { + className?: string; +} - return ( - setLoaded(true)} - {...props} - /> - ); +const Image: React.FC = ({ className, ...props }) => { + const [loaded, setLoaded] = useState(false); + + return ( + setLoaded(true)} + {...props} + /> + ); }; export default Image; diff --git a/src/components/Join/index.js b/src/components/Join/index.js deleted file mode 100644 index d69c468f..00000000 --- a/src/components/Join/index.js +++ /dev/null @@ -1,73 +0,0 @@ -import Section from "@/components/Section"; -import Image from "@/components/Image"; -import Button from "@/components/Button"; - -type JoinProps = {}; - -const Join = ({}: JoinProps) => ( -
-
-
-
-

- Be part of the future of{" "} - - Brainwave - Curve - -

-

- Unleash the power of AI within Brainwave. Upgrade your - productivity with Brainwave, the open AI chat app. -

- -
-
-
-
-
-
-
-
-
- Gradient -
-
-
-
-
- Shapes 1 -
-
- Shapes 2 -
-
-); - -export default Join; diff --git a/src/components/Join/index.tsx b/src/components/Join/index.tsx deleted file mode 100644 index d69c468f..00000000 --- a/src/components/Join/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import Section from "@/components/Section"; -import Image from "@/components/Image"; -import Button from "@/components/Button"; - -type JoinProps = {}; - -const Join = ({}: JoinProps) => ( -
-
-
-
-

- Be part of the future of{" "} - - Brainwave - Curve - -

-

- Unleash the power of AI within Brainwave. Upgrade your - productivity with Brainwave, the open AI chat app. -

- -
-
-
-
-
-
-
-
-
- Gradient -
-
-
-
-
- Shapes 1 -
-
- Shapes 2 -
-
-); - -export default Join; diff --git a/src/components/Logos/index.js b/src/components/Logos/index.js deleted file mode 100644 index 015ed205..00000000 --- a/src/components/Logos/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import Image from "../Image"; - -const Logos = ({ className }) => ( -
-
- Helping people create beautiful content at -
-
    -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
-
-); - -export default Logos; diff --git a/src/components/Logos/index.tsx b/src/components/Logos/index.tsx deleted file mode 100644 index 015ed205..00000000 --- a/src/components/Logos/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Image from "../Image"; - -const Logos = ({ className }) => ( -
-
- Helping people create beautiful content at -
-
    -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
  • - Logo 3 -
  • -
-
-); - -export default Logos; diff --git a/src/components/Notification/index.js b/src/components/Notification/index.js deleted file mode 100644 index 47697f90..00000000 --- a/src/components/Notification/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import Image from "../Image"; - -const Notification = ({ className, title }) => ( -
-
- Image -
-
-
{title}
-
-
    - {[ - "/images/notification/image-4.png", - "/images/notification/image-3.png", - "/images/notification/image-2.png", - ].map((item, index) => ( -
  • - {item} -
  • - ))} -
-
1m ago
-
-
-
-); - -export default Notification; diff --git a/src/components/Notification/index.tsx b/src/components/Notification/index.tsx deleted file mode 100644 index 47697f90..00000000 --- a/src/components/Notification/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import Image from "../Image"; - -const Notification = ({ className, title }) => ( -
-
- Image -
-
-
{title}
-
-
    - {[ - "/images/notification/image-4.png", - "/images/notification/image-3.png", - "/images/notification/image-2.png", - ].map((item, index) => ( -
  • - {item} -
  • - ))} -
-
1m ago
-
-
-
-); - -export default Notification; diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx index 455ee6bd..551dad25 100644 --- a/src/components/Section/index.tsx +++ b/src/components/Section/index.tsx @@ -1,57 +1,67 @@ -const Section = ({ - className, - crosses, - crossesOffset, - customPaddings, - children, +import React from 'react'; + +interface SectionProps { + className?: string; + crosses?: boolean; + crossesOffset?: string; + customPaddings?: string; + children?: React.ReactNode; +} + +const Section: React.FC = ({ + className, + crosses, + crossesOffset, + customPaddings, + children, }) => ( -
- {children} -
-
- {crosses && ( - <> -
- - - - - - - - )} -
+
+ {children} +
+
+ {crosses && ( + <> +
+ + + + + + + + )} +
); export default Section; diff --git a/src/components/Services/index.js b/src/components/Services/index.js deleted file mode 100644 index dc0bf5e2..00000000 --- a/src/components/Services/index.js +++ /dev/null @@ -1,195 +0,0 @@ -import Section from "@/components/Section"; -import Generating from "@/components/Generating"; -import Image from "@/components/Image"; -import Heading from "@/components/Heading"; - -type ServicesProps = { - containerClassName?: string; -}; - -const Services = ({ containerClassName }: ServicesProps) => ( -
-
- -
-
-
- Smartest AI -
-
-

Smartest AI

-

- Brainwave unlocks the potential of AI-powered - applications -

-
    - {[ - "Photo generating", - "Photo enhance", - "Seamless Integration", - ].map((item, index) => ( -
  • - Check -

    {item}

    -
  • - ))} -
-
- -
-
-
-
- Smartest AI -
-
-

Photo editing

-

- {`Automatically enhance your photos using our AI app's - photo editing feature. Try it now!`} -

-
-
- Hey Brainwave, enhance this photo - - - -
-
-
-
-

Video generation

-

- The world’s most powerful AI photo and video art - generation engine.What will you create? -

-
    - {[ - "/images/icons/recording-03.svg", - "/images/icons/recording-01.svg", - "/images/icons/disc-02.svg", - "/images/icons/chrome-cast.svg", - "/images/icons/sliders-04.svg", - ].map((item, index) => ( -
  • -
    - {item} -
    -
  • - ))} -
-
-
- Smartest AI -
- Video generated! -
- Brainwave -
-
- just now -
- - - -
-
- - - -
-
-
-
-
-
-
-
- Gradient -
-
-
-
-); - -export default Services; diff --git a/src/components/Services/index.tsx b/src/components/Services/index.tsx deleted file mode 100644 index dc0bf5e2..00000000 --- a/src/components/Services/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import Section from "@/components/Section"; -import Generating from "@/components/Generating"; -import Image from "@/components/Image"; -import Heading from "@/components/Heading"; - -type ServicesProps = { - containerClassName?: string; -}; - -const Services = ({ containerClassName }: ServicesProps) => ( -
-
- -
-
-
- Smartest AI -
-
-

Smartest AI

-

- Brainwave unlocks the potential of AI-powered - applications -

-
    - {[ - "Photo generating", - "Photo enhance", - "Seamless Integration", - ].map((item, index) => ( -
  • - Check -

    {item}

    -
  • - ))} -
-
- -
-
-
-
- Smartest AI -
-
-

Photo editing

-

- {`Automatically enhance your photos using our AI app's - photo editing feature. Try it now!`} -

-
-
- Hey Brainwave, enhance this photo - - - -
-
-
-
-

Video generation

-

- The world’s most powerful AI photo and video art - generation engine.What will you create? -

-
    - {[ - "/images/icons/recording-03.svg", - "/images/icons/recording-01.svg", - "/images/icons/disc-02.svg", - "/images/icons/chrome-cast.svg", - "/images/icons/sliders-04.svg", - ].map((item, index) => ( -
  • -
    - {item} -
    -
  • - ))} -
-
-
- Smartest AI -
- Video generated! -
- Brainwave -
-
- just now -
- - - -
-
- - - -
-
-
-
-
-
-
-
- Gradient -
-
-
-
-); - -export default Services; diff --git a/src/hooks/useSubscriptionEvents.js b/src/hooks/useSubscriptionEvents.js deleted file mode 100644 index c6fdfe3f..00000000 --- a/src/hooks/useSubscriptionEvents.js +++ /dev/null @@ -1,394 +0,0 @@ -// src/hooks/useSubscriptionEvents.js -// 订阅和支付事件追踪 Hook - -import { useCallback } from 'react'; -import { usePostHogTrack } from './usePostHogRedux'; -import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants'; -import { logger } from '../utils/logger'; - -/** - * 订阅和支付事件追踪 Hook - * @param {Object} options - 配置选项 - * @param {Object} options.currentSubscription - 当前订阅信息 - * @returns {Object} 事件追踪处理函数集合 - */ -export const useSubscriptionEvents = ({ currentSubscription = null } = {}) => { - const { track } = usePostHogTrack(); - - /** - * 追踪付费墙展示 - * @param {string} feature - 被限制的功能名称 - * @param {string} requiredPlan - 需要的订阅计划 - * @param {string} triggerLocation - 触发位置 - */ - const trackPaywallShown = useCallback((feature, requiredPlan = 'pro', triggerLocation = '') => { - if (!feature) { - logger.warn('useSubscriptionEvents', 'trackPaywallShown: feature is required'); - return; - } - - track(REVENUE_EVENTS.PAYWALL_SHOWN, { - feature, - required_plan: requiredPlan, - current_plan: currentSubscription?.plan || 'free', - trigger_location: triggerLocation, - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '🚧 Paywall Shown', { - feature, - requiredPlan, - triggerLocation, - }); - }, [track, currentSubscription]); - - /** - * 追踪付费墙关闭 - * @param {string} feature - 功能名称 - * @param {string} closeMethod - 关闭方式 ('dismiss' | 'upgrade_clicked' | 'back_button') - */ - const trackPaywallDismissed = useCallback((feature, closeMethod = 'dismiss') => { - if (!feature) { - logger.warn('useSubscriptionEvents', 'trackPaywallDismissed: feature is required'); - return; - } - - track(REVENUE_EVENTS.PAYWALL_DISMISSED, { - feature, - close_method: closeMethod, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '❌ Paywall Dismissed', { - feature, - closeMethod, - }); - }, [track, currentSubscription]); - - /** - * 追踪升级按钮点击 - * @param {string} targetPlan - 目标订阅计划 - * @param {string} source - 来源位置 - * @param {string} feature - 关联的功能(如果从付费墙点击) - */ - const trackUpgradePlanClicked = useCallback((targetPlan = 'pro', source = '', feature = '') => { - track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, { - current_plan: currentSubscription?.plan || 'free', - target_plan: targetPlan, - source, - feature: feature || null, - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '⬆️ Upgrade Plan Clicked', { - currentPlan: currentSubscription?.plan, - targetPlan, - source, - feature, - }); - }, [track, currentSubscription]); - - /** - * 追踪订阅页面查看 - * @param {string} source - 来源 - */ - const trackSubscriptionPageViewed = useCallback((source = '') => { - track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, { - current_plan: currentSubscription?.plan || 'free', - subscription_status: currentSubscription?.status || 'unknown', - is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free', - source, - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '💳 Subscription Page Viewed', { - currentPlan: currentSubscription?.plan, - source, - }); - }, [track, currentSubscription]); - - /** - * 追踪定价计划查看 - * @param {string} planName - 计划名称 ('free' | 'pro' | 'enterprise') - * @param {number} price - 价格 - */ - const trackPricingPlanViewed = useCallback((planName, price = 0) => { - if (!planName) { - logger.warn('useSubscriptionEvents', 'trackPricingPlanViewed: planName is required'); - return; - } - - track('Pricing Plan Viewed', { - plan_name: planName, - price, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '👀 Pricing Plan Viewed', { - planName, - price, - }); - }, [track, currentSubscription]); - - /** - * 追踪定价计划选择 - * @param {string} planName - 选择的计划名称 - * @param {string} billingCycle - 计费周期 ('monthly' | 'yearly') - * @param {number} price - 价格 - */ - const trackPricingPlanSelected = useCallback((planName, billingCycle = 'monthly', price = 0) => { - if (!planName) { - logger.warn('useSubscriptionEvents', 'trackPricingPlanSelected: planName is required'); - return; - } - - track('Pricing Plan Selected', { - plan_name: planName, - billing_cycle: billingCycle, - price, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '✅ Pricing Plan Selected', { - planName, - billingCycle, - price, - }); - }, [track, currentSubscription]); - - /** - * 追踪支付页面查看 - * @param {string} planName - 购买的计划 - * @param {number} amount - 支付金额 - */ - const trackPaymentPageViewed = useCallback((planName, amount = 0) => { - track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, { - plan_name: planName, - amount, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '💰 Payment Page Viewed', { - planName, - amount, - }); - }, [track, currentSubscription]); - - /** - * 追踪支付方式选择 - * @param {string} paymentMethod - 支付方式 ('wechat_pay' | 'alipay' | 'credit_card') - * @param {number} amount - 支付金额 - */ - const trackPaymentMethodSelected = useCallback((paymentMethod, amount = 0) => { - if (!paymentMethod) { - logger.warn('useSubscriptionEvents', 'trackPaymentMethodSelected: paymentMethod is required'); - return; - } - - track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, { - payment_method: paymentMethod, - amount, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '💳 Payment Method Selected', { - paymentMethod, - amount, - }); - }, [track, currentSubscription]); - - /** - * 追踪支付发起 - * @param {Object} paymentInfo - 支付信息 - * @param {string} paymentInfo.planName - 计划名称 - * @param {string} paymentInfo.paymentMethod - 支付方式 - * @param {number} paymentInfo.amount - 金额 - * @param {string} paymentInfo.billingCycle - 计费周期 - * @param {string} paymentInfo.orderId - 订单ID - */ - const trackPaymentInitiated = useCallback((paymentInfo = {}) => { - track(REVENUE_EVENTS.PAYMENT_INITIATED, { - plan_name: paymentInfo.planName, - payment_method: paymentInfo.paymentMethod, - amount: paymentInfo.amount, - billing_cycle: paymentInfo.billingCycle, - order_id: paymentInfo.orderId, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '🚀 Payment Initiated', { - planName: paymentInfo.planName, - amount: paymentInfo.amount, - paymentMethod: paymentInfo.paymentMethod, - }); - }, [track, currentSubscription]); - - /** - * 追踪支付成功 - * @param {Object} paymentInfo - 支付信息 - */ - const trackPaymentSuccessful = useCallback((paymentInfo = {}) => { - track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, { - plan_name: paymentInfo.planName, - payment_method: paymentInfo.paymentMethod, - amount: paymentInfo.amount, - billing_cycle: paymentInfo.billingCycle, - order_id: paymentInfo.orderId, - transaction_id: paymentInfo.transactionId, - previous_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '✅ Payment Successful', { - planName: paymentInfo.planName, - amount: paymentInfo.amount, - orderId: paymentInfo.orderId, - }); - }, [track, currentSubscription]); - - /** - * 追踪支付失败 - * @param {Object} paymentInfo - 支付信息 - * @param {string} errorReason - 失败原因 - */ - const trackPaymentFailed = useCallback((paymentInfo = {}, errorReason = '') => { - track(REVENUE_EVENTS.PAYMENT_FAILED, { - plan_name: paymentInfo.planName, - payment_method: paymentInfo.paymentMethod, - amount: paymentInfo.amount, - error_reason: errorReason, - order_id: paymentInfo.orderId, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '❌ Payment Failed', { - planName: paymentInfo.planName, - errorReason, - orderId: paymentInfo.orderId, - }); - }, [track, currentSubscription]); - - /** - * 追踪订阅创建成功 - * @param {Object} subscription - 订阅信息 - */ - const trackSubscriptionCreated = useCallback((subscription = {}) => { - track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, { - plan_name: subscription.plan, - billing_cycle: subscription.billingCycle, - amount: subscription.amount, - start_date: subscription.startDate, - end_date: subscription.endDate, - previous_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '🎉 Subscription Created', { - plan: subscription.plan, - billingCycle: subscription.billingCycle, - }); - }, [track, currentSubscription]); - - /** - * 追踪订阅续费 - * @param {Object} subscription - 订阅信息 - */ - const trackSubscriptionRenewed = useCallback((subscription = {}) => { - track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, { - plan_name: subscription.plan, - amount: subscription.amount, - previous_end_date: subscription.previousEndDate, - new_end_date: subscription.newEndDate, - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '🔄 Subscription Renewed', { - plan: subscription.plan, - amount: subscription.amount, - }); - }, [track]); - - /** - * 追踪订阅取消 - * @param {string} reason - 取消原因 - * @param {boolean} cancelImmediately - 是否立即取消 - */ - const trackSubscriptionCancelled = useCallback((reason = '', cancelImmediately = false) => { - track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, { - plan_name: currentSubscription?.plan, - reason, - has_reason: Boolean(reason), - cancel_immediately: cancelImmediately, - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', '🚫 Subscription Cancelled', { - plan: currentSubscription?.plan, - reason, - cancelImmediately, - }); - }, [track, currentSubscription]); - - /** - * 追踪优惠券应用 - * @param {string} couponCode - 优惠券代码 - * @param {number} discountAmount - 折扣金额 - * @param {boolean} success - 是否成功 - */ - const trackCouponApplied = useCallback((couponCode, discountAmount = 0, success = true) => { - if (!couponCode) { - logger.warn('useSubscriptionEvents', 'trackCouponApplied: couponCode is required'); - return; - } - - track('Coupon Applied', { - coupon_code: couponCode, - discount_amount: discountAmount, - success, - current_plan: currentSubscription?.plan || 'free', - timestamp: new Date().toISOString(), - }); - - logger.debug('useSubscriptionEvents', success ? '🎟️ Coupon Applied' : '❌ Coupon Failed', { - couponCode, - discountAmount, - success, - }); - }, [track, currentSubscription]); - - return { - // 付费墙事件 - trackPaywallShown, - trackPaywallDismissed, - trackUpgradePlanClicked, - - // 订阅页面事件 - trackSubscriptionPageViewed, - trackPricingPlanViewed, - trackPricingPlanSelected, - - // 支付流程事件 - trackPaymentPageViewed, - trackPaymentMethodSelected, - trackPaymentInitiated, - trackPaymentSuccessful, - trackPaymentFailed, - - // 订阅管理事件 - trackSubscriptionCreated, - trackSubscriptionRenewed, - trackSubscriptionCancelled, - - // 优惠券事件 - trackCouponApplied, - }; -}; - -export default useSubscriptionEvents; diff --git a/src/hooks/useSubscriptionEvents.ts b/src/hooks/useSubscriptionEvents.ts new file mode 100644 index 00000000..0acc44c7 --- /dev/null +++ b/src/hooks/useSubscriptionEvents.ts @@ -0,0 +1,382 @@ +// src/hooks/useSubscriptionEvents.ts +// 订阅和支付事件追踪 Hook + +import { useCallback } from 'react'; +import { usePostHogTrack } from './usePostHogRedux'; +import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants'; + +/** + * 当前订阅信息 + */ +interface SubscriptionInfo { + plan?: string; + status?: string; +} + +/** + * useSubscriptionEvents Hook 配置选项 + */ +interface UseSubscriptionEventsOptions { + currentSubscription?: SubscriptionInfo | null; +} + +/** + * 支付信息 + */ +interface PaymentInfo { + planName?: string; + paymentMethod?: string; + amount?: number; + billingCycle?: string; + orderId?: string; + transactionId?: string; +} + +/** + * 订阅信息 + */ +interface SubscriptionData { + plan?: string; + billingCycle?: string; + amount?: number; + startDate?: string; + endDate?: string; + previousEndDate?: string; + newEndDate?: string; +} + +/** + * useSubscriptionEvents Hook 返回值 + */ +interface UseSubscriptionEventsReturn { + trackPaywallShown: (feature: string, requiredPlan?: string, triggerLocation?: string) => void; + trackPaywallDismissed: (feature: string, closeMethod?: string) => void; + trackUpgradePlanClicked: (targetPlan?: string, source?: string, feature?: string) => void; + trackSubscriptionPageViewed: (source?: string) => void; + trackPricingPlanViewed: (planName: string, price?: number) => void; + trackPricingPlanSelected: (planName: string, billingCycle?: string, price?: number) => void; + trackPaymentPageViewed: (planName: string, amount?: number) => void; + trackPaymentMethodSelected: (paymentMethod: string, amount?: number) => void; + trackPaymentInitiated: (paymentInfo?: PaymentInfo) => void; + trackPaymentSuccessful: (paymentInfo?: PaymentInfo) => void; + trackPaymentFailed: (paymentInfo?: PaymentInfo, errorReason?: string) => void; + trackSubscriptionCreated: (subscription?: SubscriptionData) => void; + trackSubscriptionRenewed: (subscription?: SubscriptionData) => void; + trackSubscriptionCancelled: (reason?: string, cancelImmediately?: boolean) => void; + trackCouponApplied: (couponCode: string, discountAmount?: number, success?: boolean) => void; +} + +/** + * 订阅和支付事件追踪 Hook + * @param options - 配置选项 + * @returns 事件追踪处理函数集合 + */ +export const useSubscriptionEvents = ({ + currentSubscription = null, +}: UseSubscriptionEventsOptions = {}): UseSubscriptionEventsReturn => { + const { track } = usePostHogTrack(); + + /** + * 追踪付费墙展示 + */ + const trackPaywallShown = useCallback( + (feature: string, requiredPlan: string = 'pro', triggerLocation: string = '') => { + if (!feature) { + console.warn('useSubscriptionEvents: trackPaywallShown - feature is required'); + return; + } + + track(REVENUE_EVENTS.PAYWALL_SHOWN, { + feature, + required_plan: requiredPlan, + current_plan: currentSubscription?.plan || 'free', + trigger_location: triggerLocation, + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪付费墙关闭 + */ + const trackPaywallDismissed = useCallback( + (feature: string, closeMethod: string = 'dismiss') => { + if (!feature) { + console.warn('useSubscriptionEvents: trackPaywallDismissed - feature is required'); + return; + } + + track(REVENUE_EVENTS.PAYWALL_DISMISSED, { + feature, + close_method: closeMethod, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪升级按钮点击 + */ + const trackUpgradePlanClicked = useCallback( + (targetPlan: string = 'pro', source: string = '', feature: string = '') => { + track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, { + current_plan: currentSubscription?.plan || 'free', + target_plan: targetPlan, + source, + feature: feature || null, + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪订阅页面查看 + */ + const trackSubscriptionPageViewed = useCallback( + (source: string = '') => { + track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, { + current_plan: currentSubscription?.plan || 'free', + subscription_status: currentSubscription?.status || 'unknown', + is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free', + source, + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪定价计划查看 + */ + const trackPricingPlanViewed = useCallback( + (planName: string, price: number = 0) => { + if (!planName) { + console.warn('useSubscriptionEvents: trackPricingPlanViewed - planName is required'); + return; + } + + track('Pricing Plan Viewed', { + plan_name: planName, + price, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪定价计划选择 + */ + const trackPricingPlanSelected = useCallback( + (planName: string, billingCycle: string = 'monthly', price: number = 0) => { + if (!planName) { + console.warn('useSubscriptionEvents: trackPricingPlanSelected - planName is required'); + return; + } + + track('Pricing Plan Selected', { + plan_name: planName, + billing_cycle: billingCycle, + price, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪支付页面查看 + */ + const trackPaymentPageViewed = useCallback( + (planName: string, amount: number = 0) => { + track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, { + plan_name: planName, + amount, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪支付方式选择 + */ + const trackPaymentMethodSelected = useCallback( + (paymentMethod: string, amount: number = 0) => { + if (!paymentMethod) { + console.warn('useSubscriptionEvents: trackPaymentMethodSelected - paymentMethod is required'); + return; + } + + track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, { + payment_method: paymentMethod, + amount, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪支付发起 + */ + const trackPaymentInitiated = useCallback( + (paymentInfo: PaymentInfo = {}) => { + track(REVENUE_EVENTS.PAYMENT_INITIATED, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + billing_cycle: paymentInfo.billingCycle, + order_id: paymentInfo.orderId, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪支付成功 + */ + const trackPaymentSuccessful = useCallback( + (paymentInfo: PaymentInfo = {}) => { + track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + billing_cycle: paymentInfo.billingCycle, + order_id: paymentInfo.orderId, + transaction_id: paymentInfo.transactionId, + previous_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪支付失败 + */ + const trackPaymentFailed = useCallback( + (paymentInfo: PaymentInfo = {}, errorReason: string = '') => { + track(REVENUE_EVENTS.PAYMENT_FAILED, { + plan_name: paymentInfo.planName, + payment_method: paymentInfo.paymentMethod, + amount: paymentInfo.amount, + error_reason: errorReason, + order_id: paymentInfo.orderId, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪订阅创建成功 + */ + const trackSubscriptionCreated = useCallback( + (subscription: SubscriptionData = {}) => { + track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, { + plan_name: subscription.plan, + billing_cycle: subscription.billingCycle, + amount: subscription.amount, + start_date: subscription.startDate, + end_date: subscription.endDate, + previous_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪订阅续费 + */ + const trackSubscriptionRenewed = useCallback( + (subscription: SubscriptionData = {}) => { + track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, { + plan_name: subscription.plan, + amount: subscription.amount, + previous_end_date: subscription.previousEndDate, + new_end_date: subscription.newEndDate, + timestamp: new Date().toISOString(), + }); + }, + [track] + ); + + /** + * 追踪订阅取消 + */ + const trackSubscriptionCancelled = useCallback( + (reason: string = '', cancelImmediately: boolean = false) => { + track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, { + plan_name: currentSubscription?.plan, + reason, + has_reason: Boolean(reason), + cancel_immediately: cancelImmediately, + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + /** + * 追踪优惠券应用 + */ + const trackCouponApplied = useCallback( + (couponCode: string, discountAmount: number = 0, success: boolean = true) => { + if (!couponCode) { + console.warn('useSubscriptionEvents: trackCouponApplied - couponCode is required'); + return; + } + + track('Coupon Applied', { + coupon_code: couponCode, + discount_amount: discountAmount, + success, + current_plan: currentSubscription?.plan || 'free', + timestamp: new Date().toISOString(), + }); + }, + [track, currentSubscription] + ); + + return { + // 付费墙事件 + trackPaywallShown, + trackPaywallDismissed, + trackUpgradePlanClicked, + + // 订阅页面事件 + trackSubscriptionPageViewed, + trackPricingPlanViewed, + trackPricingPlanSelected, + + // 支付流程事件 + trackPaymentPageViewed, + trackPaymentMethodSelected, + trackPaymentInitiated, + trackPaymentSuccessful, + trackPaymentFailed, + + // 订阅管理事件 + trackSubscriptionCreated, + trackSubscriptionRenewed, + trackSubscriptionCancelled, + + // 优惠券事件 + trackCouponApplied, + }; +}; + +export default useSubscriptionEvents; diff --git a/src/types/static-assets.d.ts b/src/types/static-assets.d.ts new file mode 100644 index 00000000..dd2aca5f --- /dev/null +++ b/src/types/static-assets.d.ts @@ -0,0 +1,44 @@ +/** + * 静态资源模块声明 + * 允许 TypeScript 正确处理静态资源导入 + */ + +declare module '*.png' { + const content: string; + export default content; +} + +declare module '*.jpg' { + const content: string; + export default content; +} + +declare module '*.jpeg' { + const content: string; + export default content; +} + +declare module '*.gif' { + const content: string; + export default content; +} + +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.webp' { + const content: string; + export default content; +} + +declare module '*.ico' { + const content: string; + export default content; +} + +declare module '*.bmp' { + const content: string; + export default content; +} diff --git a/src/views/Dashboard/components/CalendarPanel.tsx b/src/views/Dashboard/components/CalendarPanel.tsx index 4732d8fb..4d2a57c3 100644 --- a/src/views/Dashboard/components/CalendarPanel.tsx +++ b/src/views/Dashboard/components/CalendarPanel.tsx @@ -44,8 +44,8 @@ import { import FullCalendar from '@fullcalendar/react'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; -import { DateClickArg } from '@fullcalendar/interaction'; -import { EventClickArg } from '@fullcalendar/common'; +import type { DateClickArg } from '@fullcalendar/interaction'; +import type { EventClickArg } from '@fullcalendar/core'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/zh-cn'; diff --git a/src/views/Dashboard/components/PlansPanel.tsx b/src/views/Dashboard/components/PlansPanel.tsx index b4e4ffe7..0e01fe23 100644 --- a/src/views/Dashboard/components/PlansPanel.tsx +++ b/src/views/Dashboard/components/PlansPanel.tsx @@ -247,7 +247,7 @@ export const PlansPanel: React.FC = () => { }; // 渲染单个卡片 - const renderCard = (item: InvestmentEvent): JSX.Element => { + const renderCard = (item: InvestmentEvent): React.ReactElement => { const statusInfo = getStatusInfo(item.status); return ( diff --git a/src/views/Dashboard/components/ReviewsPanel.tsx b/src/views/Dashboard/components/ReviewsPanel.tsx index 851151be..3bee24c6 100644 --- a/src/views/Dashboard/components/ReviewsPanel.tsx +++ b/src/views/Dashboard/components/ReviewsPanel.tsx @@ -247,7 +247,7 @@ export const ReviewsPanel: React.FC = () => { }; // 渲染单个卡片 - const renderCard = (item: InvestmentEvent): JSX.Element => { + const renderCard = (item: InvestmentEvent): React.ReactElement => { const statusInfo = getStatusInfo(item.status); return ( diff --git a/src/views/DataBrowser/KLineChartView.tsx b/src/views/DataBrowser/KLineChartView.tsx index e08ac730..5a3f9f5b 100644 --- a/src/views/DataBrowser/KLineChartView.tsx +++ b/src/views/DataBrowser/KLineChartView.tsx @@ -78,14 +78,13 @@ const KLineChartView: React.FC = ({ }, }, candle: { - type: 'line', // 使用折线图模式 - line: { - upColor: themeColors.primary.gold, - downColor: themeColors.primary.gold, - style: 'solid', - size: 2, + type: 'area' as const, // 使用面积图模式 + area: { + lineColor: themeColors.primary.gold, + lineSize: 2, + backgroundColor: [`${themeColors.primary.gold}30`, `${themeColors.primary.gold}05`], }, - }, + } as any, crosshair: { horizontal: { line: { @@ -148,7 +147,7 @@ const KLineChartView: React.FC = ({ .sort((a, b) => a.timestamp - b.timestamp); // 设置数据 - chart?.applyNewData(chartData); + (chart as any)?.applyNewData(chartData); chartRef.current = chart; diff --git a/src/views/DataBrowser/TradingViewChart.tsx b/src/views/DataBrowser/TradingViewChart.tsx deleted file mode 100644 index 90f37f53..00000000 --- a/src/views/DataBrowser/TradingViewChart.tsx +++ /dev/null @@ -1,508 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { - Box, - VStack, - HStack, - Text, - Button, - ButtonGroup, - Flex, - Icon, - useColorMode, - Tooltip, -} from '@chakra-ui/react'; -import { createChart, LineSeries } from 'lightweight-charts'; -import type { IChartApi, ISeriesApi, LineData, Time } from 'lightweight-charts'; -import { - FaExpand, - FaCompress, - FaCamera, - FaRedo, - FaCog, -} from 'react-icons/fa'; -import { MetricDataPoint } from '@services/categoryService'; - -// 黑金主题配色 -const themeColors = { - bg: { - primary: '#0a0a0a', - secondary: '#1a1a1a', - card: '#1e1e1e', - }, - text: { - primary: '#ffffff', - secondary: '#b8b8b8', - muted: '#808080', - gold: '#D4AF37', - }, - border: { - default: 'rgba(255, 255, 255, 0.1)', - gold: 'rgba(212, 175, 55, 0.3)', - }, - primary: { - gold: '#D4AF37', - goldLight: '#F4E3A7', - }, -}; - -interface TradingViewChartProps { - data: MetricDataPoint[]; - metricName: string; - unit: string; - frequency: string; -} - -type TimeRange = '1M' | '3M' | '6M' | '1Y' | 'YTD' | 'ALL'; - -const TradingViewChart: React.FC = ({ - data, - metricName, - unit, - frequency, -}) => { - const chartContainerRef = useRef(null); - const chartRef = useRef(null); - const lineSeriesRef = useRef | null>(null); - const [isFullscreen, setIsFullscreen] = useState(false); - const [selectedRange, setSelectedRange] = useState('ALL'); - const { colorMode } = useColorMode(); - - // 初始化图表 - useEffect(() => { - if (!chartContainerRef.current || data.length === 0) return; - - try { - // 创建图表 (lightweight-charts 5.0 标准 API) - const chart = createChart(chartContainerRef.current, { - width: chartContainerRef.current.clientWidth, - height: 500, - layout: { - background: { type: 'solid', color: themeColors.bg.card }, - textColor: themeColors.text.secondary, - }, - grid: { - vertLines: { - color: 'rgba(255, 255, 255, 0.05)', - }, - horzLines: { - color: 'rgba(255, 255, 255, 0.05)', - }, - }, - crosshair: { - vertLine: { - color: themeColors.primary.gold, - width: 1, - style: 3, // 虚线 - labelBackgroundColor: themeColors.primary.gold, - }, - horzLine: { - color: themeColors.primary.gold, - width: 1, - style: 3, - labelBackgroundColor: themeColors.primary.gold, - }, - }, - rightPriceScale: { - borderColor: themeColors.border.default, - }, - timeScale: { - borderColor: themeColors.border.default, - timeVisible: true, - secondsVisible: false, - rightOffset: 12, - barSpacing: 6, // 增加条形间距,减少拥挤 - fixLeftEdge: false, - lockVisibleTimeRangeOnResize: true, - rightBarStaysOnScroll: true, - borderVisible: true, - visible: true, - // 控制时间标签的最小间距(像素) - tickMarkMaxCharacterLength: 8, - }, - localization: { - locale: 'en-US', - // 使用 ISO 日期格式,强制显示 YYYY-MM-DD - dateFormat: 'dd MMM \'yy', // 这会被我们的自定义格式化器覆盖 - }, - handleScroll: { - mouseWheel: true, - pressedMouseMove: true, - }, - handleScale: { - axisPressedMouseMove: true, - mouseWheel: true, - pinch: true, - }, - }); - - // 设置时间轴的自定义格式化器(强制显示 YYYY-MM-DD) - chart.applyOptions({ - localization: { - timeFormatter: (time) => { - // time 可能是字符串 'YYYY-MM-DD' 或时间戳 - if (typeof time === 'string') { - return time; // 直接返回 YYYY-MM-DD 字符串 - } - - // 如果是时间戳,转换为 YYYY-MM-DD - const date = new Date(time * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - }, - }, - }); - - // 创建折线系列 (lightweight-charts 5.0 使用 addSeries 方法) - // 第一个参数是 series 类本身(不是实例) - const lineSeries = chart.addSeries(LineSeries, { - color: themeColors.primary.gold, - lineWidth: 2, - crosshairMarkerVisible: true, - crosshairMarkerRadius: 6, - crosshairMarkerBorderColor: themeColors.primary.goldLight, - crosshairMarkerBackgroundColor: themeColors.primary.gold, - lastValueVisible: true, - priceLineVisible: true, - priceLineColor: themeColors.primary.gold, - priceLineWidth: 1, - priceLineStyle: 3, // 虚线 - title: metricName, - }); - - // 转换数据格式 - // lightweight-charts 5.0 需要 YYYY-MM-DD 格式的字符串作为 time - const chartData: LineData[] = data - .filter((item) => item.value !== null) - .map((item) => { - // 确保日期格式为 YYYY-MM-DD - const dateStr = item.date.trim(); - return { - time: dateStr as Time, - value: item.value as number, - }; - }) - .sort((a, b) => { - // 确保时间从左到右递增 - const timeA = new Date(a.time as string).getTime(); - const timeB = new Date(b.time as string).getTime(); - return timeA - timeB; - }); - - // 设置数据 - lineSeries.setData(chartData); - - // 自动缩放到合适的视图 - chart.timeScale().fitContent(); - - chartRef.current = chart; - lineSeriesRef.current = lineSeries; - - // 响应式调整 - const handleResize = () => { - if (chartContainerRef.current && chart) { - chart.applyOptions({ - width: chartContainerRef.current.clientWidth, - }); - } - }; - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - chart.remove(); - }; - } catch (error) { - console.error('❌ TradingView Chart 初始化失败:', error); - console.error('Error details:', { - message: error.message, - stack: error.stack, - createChartType: typeof createChart, - LineSeriesType: typeof LineSeries, - }); - // 重新抛出错误让 ErrorBoundary 捕获 - throw error; - } - }, [data, metricName]); - - // 时间范围筛选 - const handleTimeRangeChange = (range: TimeRange) => { - setSelectedRange(range); - - if (!chartRef.current || data.length === 0) return; - - const now = new Date(); - let startDate: Date; - - switch (range) { - case '1M': - startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); - break; - case '3M': - startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()); - break; - case '6M': - startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate()); - break; - case '1Y': - startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()); - break; - case 'YTD': - startDate = new Date(now.getFullYear(), 0, 1); // 当年1月1日 - break; - case 'ALL': - default: - chartRef.current.timeScale().fitContent(); - return; - } - - // 设置可见范围 - const startTimestamp = startDate.getTime() / 1000; - const endTimestamp = now.getTime() / 1000; - - chartRef.current.timeScale().setVisibleRange({ - from: startTimestamp as Time, - to: endTimestamp as Time, - }); - }; - - // 重置缩放 - const handleReset = () => { - if (chartRef.current) { - chartRef.current.timeScale().fitContent(); - setSelectedRange('ALL'); - } - }; - - // 截图功能 - const handleScreenshot = () => { - if (!chartRef.current) return; - - const canvas = chartContainerRef.current?.querySelector('canvas'); - if (!canvas) return; - - canvas.toBlob((blob) => { - if (!blob) return; - - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${metricName}_${new Date().toISOString().split('T')[0]}.png`; - link.click(); - URL.revokeObjectURL(url); - }); - }; - - // 全屏切换 - const toggleFullscreen = () => { - if (!chartContainerRef.current) return; - - if (!isFullscreen) { - if (chartContainerRef.current.requestFullscreen) { - chartContainerRef.current.requestFullscreen(); - } - } else { - if (document.exitFullscreen) { - document.exitFullscreen(); - } - } - setIsFullscreen(!isFullscreen); - }; - - // 计算统计数据 - const stats = React.useMemo(() => { - const values = data.filter((item) => item.value !== null).map((item) => item.value as number); - - if (values.length === 0) { - return { min: 0, max: 0, avg: 0, latest: 0, change: 0, changePercent: 0 }; - } - - const min = Math.min(...values); - const max = Math.max(...values); - const avg = values.reduce((sum, val) => sum + val, 0) / values.length; - const latest = values[values.length - 1]; - const first = values[0]; - const change = latest - first; - const changePercent = first !== 0 ? (change / first) * 100 : 0; - - return { min, max, avg, latest, change, changePercent }; - }, [data]); - - // 格式化数字 - const formatNumber = (num: number) => { - if (Math.abs(num) >= 1e9) { - return (num / 1e9).toFixed(2) + 'B'; - } - if (Math.abs(num) >= 1e6) { - return (num / 1e6).toFixed(2) + 'M'; - } - if (Math.abs(num) >= 1e3) { - return (num / 1e3).toFixed(2) + 'K'; - } - return num.toFixed(2); - }; - - return ( - - {/* 工具栏 */} - - {/* 时间范围选择 */} - - {(['1M', '3M', '6M', '1Y', 'YTD', 'ALL'] as TimeRange[]).map((range) => ( - - ))} - - - {/* 图表操作 */} - - - - - - - - - - - - - - {/* 统计数据 */} - - - - 最新值 - - - {formatNumber(stats.latest)} {unit} - - = 0 ? '#00ff88' : '#ff4444'} - fontSize="xs" - fontWeight="bold" - > - {stats.change >= 0 ? '+' : ''} - {formatNumber(stats.change)} ({stats.changePercent.toFixed(2)}%) - - - - - - 平均值 - - - {formatNumber(stats.avg)} {unit} - - - - - - 最高值 - - - {formatNumber(stats.max)} {unit} - - - - - - 最低值 - - - {formatNumber(stats.min)} {unit} - - - - - - 数据点数 - - - {data.filter((item) => item.value !== null).length} - - - - - - 频率 - - - {frequency} - - - - - {/* 图表容器 */} - - - {/* 提示信息 */} - - - 💡 提示:滚动鼠标滚轮缩放,拖拽移动视图 - - 数据来源: {metricName} - - - ); -}; - -export default TradingViewChart; diff --git a/src/views/DataBrowser/index.tsx b/src/views/DataBrowser/index.tsx index 62fa6d2b..834dc822 100644 --- a/src/views/DataBrowser/index.tsx +++ b/src/views/DataBrowser/index.tsx @@ -696,18 +696,20 @@ const DataBrowser: React.FC = () => { p={3} cursor="pointer" bg="transparent" - _hover={{ bg: themeColors.bg.cardHover }} + _hover={{ + bg: themeColors.bg.cardHover, + borderLeftColor: themeColors.primary.gold, + }} borderRadius="md" borderLeftWidth="3px" borderLeftColor="transparent" - _hover={{ borderLeftColor: themeColors.primary.gold }} transition="all 0.2s" onClick={() => { // 转换搜索结果为 TreeMetric 格式 const metric: TreeMetric = { metric_id: result.metric_id, metric_name: result.metric_name, - source: result.source, + source: result.source as 'SMM' | 'Mysteel', frequency: result.frequency, unit: result.unit, description: result.description, diff --git a/src/views/Pricing/index.tsx b/src/views/Pricing/index.tsx index 9d31258f..a3fa0e8c 100644 --- a/src/views/Pricing/index.tsx +++ b/src/views/Pricing/index.tsx @@ -1,5 +1,5 @@ import { motion } from "framer-motion"; -import Button from "@/components/Button2"; +import Button from "@/components/Button"; import { pricing } from "./content";