在Next.js中接入TradingView图表实践总结

简介: 详细讲解如何在Next.js项目中接入TradingView Charts,包括环境配置、数据馈送实现、自定义指标、主题定制、性能优化等完整流程。

原文地址:https://feinterview.poetries.top/blog/nextjs-tradingview-integration

导语

TradingView 是全球最专业的金融图表可视化库之一,提供了功能强大的 K 线图、指标系统和技术分析工具。在金融行情类 Web 应用中,接入 TradingView 是提升用户体验的首选方案。

本文将基于实际项目代码,系统讲解如何在 Next.js 项目中接入 TradingView Charts,包括环境配置、Datafeed 数据馈送实现、自定义指标开发、主题样式定制、以及关键的性能优化策略。

一、项目准备与环境配置

1.1 获取 TradingView 图表库

TradingView 图表库需要从官方获取授权后下载。获取后将文件放置在项目的 public/static/charting_library 目录下:

public/
  └── static/
      └── charting_library/
          ├── charting_library.standalone.js
          └── bundles/
              ├── *.js
              └── *.css

1.2 组件目录结构

src/components/Tradingview/
├── index.tsx              # 主组件
├── datafeed.ts            # 数据馈送实现
├── widgetOpts.tsx         # 图表配置选项
├── widgetMethods.ts       # 图表方法工具
├── theme.ts               # 主题配置
├── constant.ts           # 常量定义
└── customIndicators/      # 自定义指标
    ├── ma.ts
    ├── macd.ts
    ├── kdj.ts
    └── customerRSI.ts

二、核心组件实现

2.1 主组件:TradingView 图表容器

// src/components/Tradingview/index.tsx
import { useEffect, useRef, useState } from 'react'
import { widget } from 'public/static/charting_library'
import { useStores } from '@/context/mobxProvider'
import { STORAGE_GET_CHART_PROPS, STORAGE_REMOVE_CHART_PROPS, ThemeConst } from './constant'
import { ColorType, applyOverrides, createWatermarkLogo, setCSSCustomProperty, setChartStyleProperties } from './widgetMethods'
import getWidgetOpts from './widgetOpts'
import { useConfig } from '@/context/configProvider'
import { useRouter } from 'next/router'
import stores from '@/stores'
import { observer } from 'mobx-react'
import { STORAGE_SET_TRADINGVIEW_RESOLUTION } from '@/utils/storage'


const Tradingview = () => {
  const chartContainerRef = useRef<HTMLDivElement>()
  const { ws } = useStores()
  const { isMobile, isPc } = useConfig()
  const router = useRouter()
  const [isChartLoading, setIsChartLoading] = useState(true)
  const [loading, setLoading] = useState(true)


  const query = {
    ...router.query,
    ...getInjectParams()
  } as any


  const datafeedParams = {
    setActiveSymbolInfo: ws.setActiveSymbolInfo,
    removeActiveSymbol: ws.removeActiveSymbol,
    getDataFeedBarCallback: ws.getDataFeedBarCallback,
    dataSourceCode: query.dataSourceCode
  }


  const params = {
    symbol: (query.symbolName || 'BTCUSDT') as string,
    locale: (query.locale || 'en') as LanguageCode,
    theme: (query.theme || 'light') as ThemeName,
    colorType: Number(query.colorType || 1) as ColorType,
    isMobile,
    bgGradientStartColor: query.bgGradientStartColor ? `#${query.bgGradientStartColor}` : '',
    bgGradientEndColor: query.bgGradientEndColor ? `#${query.bgGradientEndColor}` : ''
  }


  useEffect(() => {
    console.log('Tradingview组件初始化')
    const showBottomMACD = Number(query.showBottomMACD || 1)
    const chartType = (query.chartType !== '' ? Number(query.chartType || 1) : 1) as ChartStyle
    const theme = params.theme


    // 切换主题时清除本地缓存,避免颜色闪烁
    const defaultBgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
    if (theme && defaultBgColor !== STORAGE_GET_CHART_PROPS('paneProperties.background')) {
      STORAGE_REMOVE_CHART_PROPS()
    }


    const widgetOptions = getWidgetOpts(params, chartContainerRef.current, datafeedParams)
    const tvWidget = new widget(widgetOptions)


    setTimeout(() => {
      setLoading(false)
    }, 200)


    tvWidget.onChartReady(async () => {
      setIsChartLoading(false)


      // 动态设置 CSS 变量
      setCSSCustomProperty({ tvWidget, theme })


      // 监听时间周期变化
      tvWidget
        .activeChart()
        .onIntervalChanged()
        .subscribe(null, (interval, timeframeObj) => {
          // 记录当前分辨率
          STORAGE_SET_TRADINGVIEW_RESOLUTION(interval)


          // 日周月级别使用 UTC 时区,分钟级别使用上海时区
          if (['D', 'W', 'M', 'Y'].some((item) => interval.endsWith(item))) {
            tvWidget.activeChart().getTimezoneApi().setTimezone('Etc/UTC')
          } else {
            tvWidget.activeChart().getTimezoneApi().setTimezone('Asia/Shanghai')
          }


          ws.activeSymbolInfo.onResetCacheNeededCallback?.()
          setTimeout(() => {
            tvWidget.activeChart().resetData()
          }, 100)
        })


      // 默认显示 MACD 指标
      if (showBottomMACD === 1) {
        tvWidget.activeChart().createStudy(
          'MACD',
          false,
          false,
          { in_0: 12, in_1: 26, in_3: 'close', in_2: 9 },
          {
            'Histogram.color.3': 'rgba(197, 71, 71, 0.7188)',
            showLabelsOnPriceScale: !!isPc
          }
        )
      }


      // 创建自定义 MA 指标
      tvWidget.activeChart().createStudy('Customer Moving Average', false, false, {}, { showLabelsOnPriceScale: false })


      // 动态切换主题
      if (query.theme && !params.bgGradientStartColor) {
        await tvWidget.changeTheme(theme)
      }


      // 设置 K 线柱样式(绿涨红跌 / 红涨绿跌)
      setChartStyleProperties({ colorType: params.colorType, tvWidget })


      // 应用覆盖样式
      applyOverrides({
        tvWidget,
        chartType,
        bgGradientStartColor: params.bgGradientStartColor,
        bgGradientEndColor: params.bgGradientEndColor
      })


      // 添加水印 Logo
      if (query.hideWatermarkLogo !== '0' && query.watermarkLogoUrl) {
        createWatermarkLogo(query.watermarkLogoUrl)
      }


      // 记录实例
      ws.setTvWidget(tvWidget)
      window.tvWidget = tvWidget
    })


    return () => {
      tvWidget.remove()
      mitt.off('symbol_change')
    }
  }, [router.query])


  return (
    <div style={
  { position: 'relative' }}>
      <div id="tradingview" ref={chartContainerRef} style={
  { height: 'calc(100vh - 60px)', opacity: loading ? 0 : 1 }} />
      {isChartLoading && (
        <div className="loading-container">
          <div className="loading"></div>
        </div>
      )}
    </div>
  )
}


export default observer(Tradingview)

2.2 Datafeed 数据馈送实现

Datafeed 是 TradingView 与后端数据交互的核心接口,需要实现以下方法:

// src/components/Tradingview/datafeed.ts
class DataFeedBase {
  configuration: DatafeedConfiguration


  constructor(props: Partial<ChartingLibraryWidgetOptions>) {
    this.configuration = {
      supports_time: true,
      supports_timescale_marks: true,
      supports_marks: true,
      // 支持的分辨率
      supported_resolutions: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M'],
      intraday_multipliers: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M']
    } as DatafeedConfiguration


    this.setActiveSymbolInfo = props.setActiveSymbolInfo
    this.removeActiveSymbol = props.removeActiveSymbol
    this.getDataFeedBarCallback = props.getDataFeedBarCallback
    this.isZh = props.locale === 'zh_TW'
  }


  // 图表初始化时调用,设置支持的配置
  onReady(callback) {
    setTimeout(() => {
      callback(this.configuration)
    }, 0)
  }


  // 解析品种信息
  async resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension) {
    const resolution = String(STORAGE_GET_TRADINGVIEW_RESOLUTION() || '')
    const ENV = getEnv()
    const urlPrefix = ENV.isApp ? getInjectParams().baseUrl : ''


    let symbolInfo
    if (!ENV.isApp) {
      // HTTP 请求获取品种信息
      const res = await request(`${urlPrefix}/api/trade-core/coreApi/symbols/symbol/detail?symbol=${symbolName}`)
      symbolInfo = res?.data || {}
    } else {
      // APP 内获取 RN 传递的数据
      symbolInfo = {
        ...(ENV?.injectParams?.symbolInfo || {}),
        ...(stores.global.symbolInfo || {})
      }
    }


    const currentSymbol = {
      ...symbolInfo,
      precision: symbolInfo?.symbolDecimal || 2,
      description: symbolInfo?.remark || '',
      exchange: '',
      session: '24x7',
      name: symbolInfo.symbol,
      dataSourceCode: symbolInfo.dataSourceCode
    }


    const commonSymbolInfo = {
      has_intraday: true,
      has_daily: true,
      has_weekly_and_monthly: true,
      intraday_multipliers: this.configuration.intraday_multipliers,
      supported_resolutions: this.configuration.supported_resolutions,
      data_status: 'streaming',
      format: 'price',
      minmov: 1,
      pricescale: Math.pow(10, currentSymbol.precision),
      ticker: currentSymbol?.name
    } as LibrarySymbolInfo


    const currentSymbolInfo = {
      ...commonSymbolInfo,
      ...currentSymbol,
      description: this.isZh ? currentSymbol.description : currentSymbol?.name,
      exchange: this.isZh ? currentSymbol?.exchange : '',
      session: '0000-0000|0000-0000:1234567;1',
      timezone: ['D', 'W', 'M', 'Y'].some((item) => resolution.endsWith(item)) ? 'Etc/UTC' : 'Asia/Shanghai'
    } as LibrarySymbolInfo


    setTimeout(() => {
      onSymbolResolvedCallback(currentSymbolInfo)
    }, 0)
  }


  // 搜索品种
  searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
    const keyword = userInput || ''
    const resultArr = symbolInfoArr
      .filter((item) => item.name.includes(keyword))
      .map((item) => ({
        symbol: item.name,
        name: item.name,
        full_name: `${item.name}`,
        description: this.isZh ? item.description : item.name,
        exchange: this.isZh ? item.exchange : '',
        type: item.type,
        ticker: item.name
      }))


    setTimeout(() => {
      onResultReadyCallback(resultArr)
    }, 0)
  }


  // 获取 K 线历史数据(核心方法)
  getBars(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) {
    const { from, to, firstDataRequest, countBack } = periodParams
    this.setActiveSymbolInfo({ symbolInfo, resolution })
    this.getDataFeedBarCallback({
      symbolInfo,
      resolution,
      from,
      to,
      countBack,
      onHistoryCallback,
      onErrorCallback,
      firstDataRequest
    })
  }


  // 订阅实时数据更新
  subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
    this.setActiveSymbolInfo({
      symbolInfo,
      resolution,
      onRealtimeCallback,
      subscriberUID,
      onResetCacheNeededCallback
    })
    mitt.on('symbol_change', () => {
      onResetCacheNeededCallback()
    })
  }


  // 取消订阅
  unsubscribeBars(subscriberUID) {
    this.removeActiveSymbol(subscriberUID)
  }
}


export default DataFeedBase

2.3 图表配置选项

// src/components/Tradingview/widgetOpts.tsx
import ma from './customIndicators/ma'


export default function getWidgetOpts(props, containerRef: any, datafeedParams: any): ChartingLibraryWidgetOptions {
  const ENV = getEnv()
  const theme = props.theme
  const bgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
  const toolbar_bg = theme === 'dark' ? ThemeConst.black : '#fff'


  // 禁用的功能
  const disabled_features: ChartingLibraryFeatureset[] = [
    'header_compare',
    'symbol_search_hot_key',
    'study_templates',
    'header_saveload',
    'save_shortcut',
    'header_undo_redo',
    'symbol_info',
    'timeframes_toolbar',
    'scales_date_format',
    'header_fullscreen_button',
    'display_market_status'
  ]


  // 移动端额外禁用
  if (props.isMobile) {
    disabled_features.push(
      'header_symbol_search',
      'context_menus',
      'show_chart_property_page',
      'header_screenshot',
      'adaptive_logo',
      'left_toolbar'
    )
  }


  const widgetOptions: ChartingLibraryWidgetOptions = {
    fullscreen: true,
    autosize: true,
    timezone: 'exchange',
    library_path: `${ENV.isApp ? '.' : ''}/static/charting_library/`,
    datafeed: new DataFeedBase(datafeedParams),
    symbol: props.symbol,
    client_id: 'tradingview.com',
    user_id: 'public_user_id',
    locale: props.locale as LanguageCode,
    interval: isPC() ? '15' : '1',
    theme,
    toolbar_bg,
    container: containerRef,
    symbol_search_request_delay: 1000,
    auto_save_delay: 5,
    study_count_limit: 5,
    allow_symbol_change: true,
    overrides: {
      'paneProperties.background': `${bgColor}`
    },
    disabled_features,
    enabled_features: ['hide_resolution_in_legend', 'display_legend_on_all_charts'],
    custom_css_url: ENV.isApp ? `./styles/index.css` : `/static/styles/index.css`,
    favorites: {
      intervals: ['1', '5', '15', '30', '60']
    },
    custom_indicators_getter: function (PineJS) {
      return Promise.resolve([ma(PineJS)])
    },
    loading_screen: {
      backgroundColor: 'transparent',
      foregroundColor: 'transparent'
    }
  }


  return widgetOptions
}

三、K线数据与WebSocket实时更新

3.1 WebSocket Store 实现

// src/stores/ws.ts
class WsStore {
  tvWidget = null
  @observable lastbar = {}
  @observable activeSymbolInfo = {}


  // HTTP 获取历史 K 线数据
  getHttpHistoryBars = async (symbolInfo, resolution, from, to, countBack, firstDataRequest) => {
    const klineType =
      {
        1: '1min',
        5: '5min',
        15: '15min',
        30: '30min',
        60: '60min',
        240: '4hour',
        '1D': '1day',
        '1W': '1week',
        '1M': '1mon'
      }[resolution] || '1min'


    const res = await request.get(`${url}/api/trade-market/marketApi/kline/symbol/klineList`, {
      params: {
        symbol: symbolInfo.symbol,
        first: firstDataRequest,
        current: 1,
        size: document.documentElement.clientWidth >= 1200 ? 500 : 200,
        klineType,
        klineTime: to * 1000
      }
    })


    const list = res?.data || []
    return list
      .map((item) => {
        const [klineTime, open, high, low, close] = (item || '').split(',')
        return {
          open: Number(open),
          close: Number(close),
          high: Number(high),
          low: Number(low),
          time: resolution.includes('M') ? Number(klineTime) + 8 * 60 * 60 * 1000 : Number(klineTime)
        }
      })
      .reverse()
  }


  // 更新最后一条 K 线
  updateBar = (socketData, currentSymbol) => {
    const precision = currentSymbol.precision
    const lastBar = this.lastbar
    const resolution = currentSymbol.resolution
    const serverTime = socketData?.priceData?.id / 1000
    const bid = socketData?.priceData?.buy


    let rounded = serverTime
    if (!isNaN(resolution) || resolution.includes('D')) {
      const coeff = (resolution.includes('D') ? 1440 : Number(resolution)) * 60
      rounded = Math.floor(serverTime / coeff) * coeff
    }


    const lastBarSec = lastBar?.time / 1000


    if (rounded > lastBarSec) {
      // 新建 K 线
      return {
        time: rounded * 1000,
        open: Number(bid),
        high: Number(bid),
        low: Number(bid),
        close: Number(bid)
      }
    } else {
      // 更新当前 K 线
      return {
        time: lastBar.time,
        open: lastBar.open,
        high: Math.max(lastBar.high, Number(bid)),
        low: Math.min(lastBar.low, Number(bid)),
        close: Number(bid)
      }
    }
  }


  // 处理 WebSocket 消息
  @action
  message(res) {
    if (res?.header?.msgId === 'symbol') {
      const quoteBody = this.parseQuoteBodyData(res?.body)
      if (quoteBody?.symbol === this.activeSymbolInfo?.symbolInfo?.name) {
        const newLastBar = this.updateBar(quoteBody, {
          resolution: this.activeSymbolInfo.resolution,
          precision: this.activeSymbolInfo.symbolInfo.precision,
          symbolInfo: this.activeSymbolInfo.symbolInfo
        })
        if (newLastBar) {
          this.activeSymbolInfo.onRealtimeCallback?.(newLastBar)
          this.lastbar = newLastBar
        }
      }
    }
  }


  // Datafeed 回调
  @action
  getDataFeedBarCallback = (obj = {}) => {
    const { symbolInfo, resolution, firstDataRequest, from, to, countBack, onHistoryCallback } = obj
    this.getHttpHistoryBars(symbolInfo, resolution, from, to, countBack, firstDataRequest).then((bars) => {
      if (bars?.length) {
        onHistoryCallback(bars, { noData: false })
        this.lastbar = bars.at(-1)
      } else {
        onHistoryCallback(bars, { noData: true })
      }
    })
  }
}


export default wsStore

四、自定义指标开发

4.1 自定义 MA 指标示例

// src/components/Tradingview/customIndicators/ma.ts
const customerMovingAverage = (PineJS: PineJS) => {
  const indicators: CustomIndicator = {
    name: 'Customer Moving Average',
    metainfo: {
      _metainfoVersion: 51,
      id: 'Customer Moving Average@tv-basicstudies-1',
      name: 'Customer Moving Average',
      description: 'Customer Moving Average',
      shortDescription: 'MA',
      is_price_study: true,
      isCustomIndicator: true,
      format: { type: 'price' },
      defaults: {
        styles: {
          plot_0: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#FF0000' },
          plot_1: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#00FF00' },
          plot_2: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#00FFFF' }
        },
        inputs: { in_0: 5, in_1: 10, in_2: 30 },
        precision: 4
      },
      plots: [
        { id: 'plot_0', type: 'line' },
        { id: 'plot_1', type: 'line' },
        { id: 'plot_2', type: 'line' }
      ],
      inputs: [
        { id: 'in_0', name: 'Length', defval: 9, type: 'integer', min: 1, max: 1e4 },
        { id: 'in_1', name: 'Length1', defval: 10, type: 'integer', min: 1, max: 1e4 },
        { id: 'in_2', name: 'Length2', defval: 30, type: 'integer', min: 1, max: 1e4 }
      ]
    },
    constructor: function (this: LibraryPineStudy<IPineStudyResult>) {
      this.main = function (context, inputCallback) {
        const close = PineJS.Std.close(context)
        const len1 = inputCallback(0)
        const len2 = inputCallback(1)
        const len3 = inputCallback(2)


        const value1 = PineJS.Std.sma(close, len1, context)
        const value2 = PineJS.Std.sma(close, len2, context)
        const value3 = PineJS.Std.sma(close, len3, context)


        return [
          { value: value1, offset: 0 },
          { value: value2, offset: 0 },
          { value: value3, offset: 0 }
        ]
      }
    }
  }
  return indicators
}


export default customerMovingAverage

五、主题与样式定制

5.1 主题配置

// src/components/Tradingview/theme.ts
export const getTradingviewThemeCssVar = (theme: ThemeName) => {
  const primary = ThemeConst.primary
  const textPrimary = ThemeConst.textPrimary
  const isDark = theme === 'dark'


  return {
    '--tv-color-toolbar-button-text': '#7B7E80',
    '--tv-color-toolbar-button-text-active': textPrimary,
    '--tv-color-toolbar-button-text-active-hover': textPrimary,
    '--tv-color-toolbar-toggle-button-background-active': primary,
    '--tv-color-toolbar-toggle-button-background-active-hover': primary,
    '--tv-color-popup-element-text-active': '#131722',
    '--tv-color-popup-element-background-active': '#f0f3fa',
    ...(isDark ? { '--tv-color-pane-background': ThemeConst.black } : {})
  }
}

5.2 K线颜色与涨跌色设置

// src/components/Tradingview/widgetMethods.ts
export type ColorType = 1 | 2 // 1绿涨红跌 2红涨绿跌


export function setChartStyleProperties(props: { colorType: ColorType; tvWidget: IChartingLibraryWidget }) {
  const { colorType, tvWidget } = props
  const red = ThemeConst.red // #C54747
  const green = ThemeConst.green // #45A48A


  let upColor = Number(colorType) === 2 ? red : green
  let downColor = Number(colorType) === 2 ? green : red


  // 蜡烛图样式
  tvWidget.chart().getSeries().setChartStyleProperties(1, {
    upColor,
    downColor,
    wickUpColor: upColor,
    wickDownColor: downColor,
    borderUpColor: upColor,
    borderDownColor: downColor
  })


  // 空心蜡烛图样式
  tvWidget.chart().getSeries().setChartStyleProperties(9, {
    upColor,
    downColor,
    wickUpColor: upColor,
    wickDownColor: downColor,
    borderUpColor: upColor,
    borderDownColor: downColor
  })
}

六、性能优化策略

6.1 数据加载优化

// 1. 按需加载历史数据
getHttpHistoryBars = async (symbolInfo, resolution, from, to, countBack, firstDataRequest) => {
  const size = document.documentElement.clientWidth >= 1200 ? 500 : 200
  // 根据屏幕宽度调整加载数量,移动端减少请求数据量
}


// 2. 数据缓存策略
@action
getDataFeedBarCallback = (obj = {}) => {
  const { firstDataRequest } = obj


  if (firstDataRequest) {
    // 首次请求完整数据
    this.getHttpHistoryBars(symbolInfo, resolution, from, to, countBack, true)
  } else {
    // 后续请求只获取增量数据
    this.getHttpHistoryBars(symbolInfo, resolution, from, this.lastBarTime, countBack, false)
  }
}

6.2 WebSocket 连接优化

// 使用 reconnecting-websocket 实现自动重连
this.socket = new ReconnectingWebSocket(wsUrl, ['WebSocket', token], {
  minReconnectionDelay: 1,
  connectionTimeout: 3000,
  maxEnqueuedMessages: 0,
  maxRetries: 10000
})


// 心跳保活
startHeartbeat() {
  this.heartbeatInterval = setInterval(() => {
    this.send({}, { msgId: 'heartbeat' })
  }, 20000)
}

6.3 图表渲染优化

// 1. 使用 loading 状态避免闪烁
const [loading, setLoading] = useState(true)
setTimeout(() => {
  setLoading(false)
}, 200)


// 2. 延迟初始化避免阻塞
useEffect(() => {
  // 延迟加载图表
  setTimeout(() => {
    const tvWidget = new widget(widgetOptions)
  }, 100)
}, [])


// 3. 缓存主题配置
const defaultBgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
if (theme && defaultBgColor !== STORAGE_GET_CHART_PROPS('paneProperties.background')) {
  STORAGE_REMOVE_CHART_PROPS()
}

6.4 内存管理与清理

useEffect(() => {
  return () => {
    // 组件卸载时清理
    tvWidget.remove() // 销毁图表实例
    mitt.off('symbol_change') // 取消事件订阅
    this.stopHeartbeat() // 停止心跳
    this.socket?.close() // 关闭 WebSocket
  }
}, [])

七、常见问题与解决方案

7.1 主题切换不生效

// 问题:切换主题后图表颜色不变
// 解决:清除本地缓存 + 动态调用 changeTheme


// 1. 切换主题时清除缓存
STORAGE_REMOVE_CHART_PROPS()


// 2. 动态切换主题
tvWidget.changeTheme(theme)


// 3. 设置 CSS 变量
setCSSCustomProperty({ tvWidget, theme })

7.2 数据请求重复

// 问题:多次调用 getBars
// 解决:使用 lastBarTime 缓存截止时间


this.lastBarTime = bars[0]?.time / 1000
if (this.lastBarTime === bars[0]?.time / 1000) {
  this.datafeedBarCallbackObj.onHistoryCallback([], { noData: true })
}

7.3 移动端适配

// 移动端禁用多余功能
if (props.isMobile) {
  disabled_features.push('header_symbol_search', 'context_menus', 'show_chart_property_page', 'header_screenshot', 'left_toolbar')
}


// 禁止双指缩放
document.body.addEventListener(
  'touchstart',
  (e) => {
    if (e.touches.length > 1) {
      e.preventDefault()
    }
  },
  { passive: false }
)

八、完整调用示例

// src/pages/index.tsx
import Tradingview from '@/components/Tradingview'


export default function ChartPage() {
  return (
    <div>
      <Tradingview />
    </div>
  )
}

URL 参数说明:

  • symbolName: 交易品种,如 BTCUSDT
  • theme: 主题,light 或 dark
  • locale: 语言,如 en、zh_TW
  • colorType: 涨跌颜色,1 绿涨红跌,2 红涨绿跌
  • chartType: 图表类型,1 蜡烛图、2 折线图等

总结

本文详细讲解了 Next.js 项目中接入 TradingView 图表的完整方案,涵盖了:

  1. 环境配置:类型定义、目录结构
  2. 核心实现:主组件、Datafeed、配置选项
  3. 数据交互:HTTP 历史数据 + WebSocket 实时更新
  4. 自定义开发:自定义指标、主题定制
  5. 性能优化:数据加载、WebSocket、渲染优化、内存管理
  6. 常见问题:主题切换、数据重复、移动端适配

通过以上方案,可以在 Next.js 项目中快速构建专业的金融图表应用。如需更高级的功能(如图表保存加载、自定义交易品种等),可以参考 TradingView 官方文档

目录
相关文章
|
20小时前
|
人工智能 运维 安全
玩转OpenClaw 13000+Skill!OpenClaw阿里云/本地部署+ClawHub Skill库选择使用指南
ClawHub作为OpenClaw(原Clawdbot)的官方Skill注册中心,已汇聚超过13000个社区贡献的技能插件,覆盖代码开发、自动化运维、内容创作、知识管理等全场景需求。这些Skill如同乐高积木,能让OpenClaw从基础AI助手,快速升级为适配特定场景的专业工具——对开发者而言,它是代码协作与部署的得力帮手;对内容创作者来说,它是多媒体生成与编辑的创意伙伴;对研究者而言,它是信息检索与知识沉淀的高效工具。
229 3
|
22小时前
|
人工智能 监控 区块链
保姆级图文教学!OpenClaw(Clawdbot)阿里云/本地部署+7大场景70个真实案例 效率翻倍指南
OpenClaw(原Clawdbot、Moltbot)的爆火,不在于其基础的对话能力,而在于它“自主执行任务”的核心特性——通过70个经过社区验证的真实案例,覆盖内容创作、记忆管理、夜间自动化、金融监控等8大核心场景,真正实现“你睡觉、AI干活”的高效模式。无论是医生将医学通讯转为通勤播客,还是开发者让AI夜间清理GitHub过期Issue,OpenClaw都在通过场景化落地,重新定义AI助手的价值。
57 4
保姆级图文教学!OpenClaw(Clawdbot)阿里云/本地部署+7大场景70个真实案例 效率翻倍指南
|
1天前
|
人工智能 IDE 算法
Prompt、Skill、Agent、MCP 到底啥区别?一篇讲透 AI 工作体系
本文用生动比喻为测试新人厘清AI核心概念:大模型是“天才员工”,Prompt是临时口头交代,Agent是自主干活的模式,Skill是可复用的SOP手册,MCP是连接系统的“门禁卡”,IDE是智能办公室,Claude Code则是终端特种兵。重在构建AI工作体系,而非死记定义。
|
22小时前
|
人工智能 数据挖掘 程序员
Claude Skills:如何将提示词升级为可复用技能
深入解析 Claude Skills 的核心原理、渐进披露架构和最佳实践,手把手教你创建自定义技能,实现从临时提示词到可复用资产的升级
33 1
|
18小时前
|
SQL 关系型数据库 MySQL
阿里云数据库多少钱?2026最新RDS收费价格(MySQL、PG、SQL Server及MariaDB)
阿里云RDS数据库2026最新价格:MySQL倚天版低至88元/年,SQL Server 2核4G仅299元/年,PostgreSQL标准版227.99元/年。支持MySQL、SQL Server、PostgreSQL、MariaDB四大引擎,安全稳定、弹性伸缩,高性价比上云首选。(239字)
|
18小时前
|
人工智能 JavaScript iOS开发
2026年OpenClaw必备Skill榜单:10000+技能精选,附阿里云/本地部署教程
OpenClaw(原Clawdbot、Moltbot)的核心魅力,在于其开放且丰富的Skill生态——截至2026年3月,ClawHub平台已汇聚超过10000个社区构建的技能插件,覆盖基础工具、生产力提升、知识管理、搜索研究、媒体创作等全场景需求。这些Skill如同给AI助手装上“功能翅膀”,让原本只能简单对话的工具,变身能处理邮件、管理项目、创作内容、控制智能家居的全能助手。
103 8
|
22小时前
|
人工智能 自然语言处理 前端开发
不会代码也能建站?AI生成网站完整入门指南
不会写代码也能建站?AI生成网站正大幅降低门槛!只需描述需求(如“个人作品集”),工具如lynxcode即可自动生成页面结构,用户仅需调整内容与布局。几分钟即可上线简易官网,适合个人、小团队快速验证想法,无需服务器配置或编程基础。
|
23小时前
|
JSON Linux Shell
飞牛OS 防火墙错误设置无法访问
本指南介绍飞牛OS密码重置与防火墙异常修复方法:通过GRUB修改启动参数重置root密码;停用防火墙服务,备份并清空`fw.conf`配置文件,重启服务后即可在Web后台重新配置防火墙规则。(239字)
31 0
|
23小时前
|
API
GitHub 首页信息流太乱?分享几个“直达核心”的隐藏入口和优化方案
GitHub UI 越来越“社交化”,核心功能反被淹没!本文分享实用技巧:直达热榜(/trending)、星标库(/stars)、Fork 仓库的快捷路径;推荐关闭动态流、安装 Refined GitHub 插件降噪;搭配 GitHub CLI 和 Desktop 提升效率。回归代码本质,高效用 GitHub。
23 0
|
23小时前
|
存储 弹性计算 人工智能
阿里云ECS 99元套餐重磅延期,多场景优惠福利全解析
阿里云ECS 99元套餐重磅延期至2027年3月31日!新老用户同享:2核2G实例低至99元/年,另含建站礼包、安全防护包、ECS+RDS/OSS组合加购价99元等多重福利,助力开发者与企业低成本高效上云。
50 0

热门文章

最新文章