import type { Awaitable } from "@vueuse/core"; import type { ChartOptions, DeepPartial, HistogramData, IChartApi, ISeriesApi, LayoutOptions, SeriesDataItemTypeMap, SeriesType, Time } from "lightweight-charts"; import type { ThemeMode } from "./useTheme"; import { AreaSeries, BarSeries, BaselineSeries, CandlestickSeries, ColorType, createChart, HistogramSeries, LineSeries } from "lightweight-charts"; import { cloneDeep, mergeWith } from "lodash-es"; export type Series = "Area" | "Bar" | "Baseline" | "Candlestick" | "Histogram" | "Line"; export type TData = SeriesDataItemTypeMap[SeriesType]; export type WeightChartOptions = DeepPartial; export type TradingViewData = (() => Awaitable) | MaybeRefOrGetter; export interface VolumeData extends HistogramData { value: number; color?: string; } export interface TradingViewOptions { theme?: ThemeMode; type?: T; data?: TradingViewData; volumeData?: MaybeRefOrGetter; weightChartOptions?: MaybeRefOrGetter; autosize?: boolean; showVolume?: boolean; } const lightThemeLayout: Partial = { textColor: "black", background: { type: ColorType.Solid, color: "#fff" }, }; const darkThemeLayout: Partial = { textColor: "white", background: { type: ColorType.Solid, color: "#000" }, }; const initializeOptions: Required = { theme: "dark", type: "Candlestick", data: [], volumeData: [], weightChartOptions: { width: 400, height: 300, }, autosize: true, showVolume: true, }; function getChartSeriesDefinition(type: Series) { switch (type) { case "Area": return AreaSeries; case "Bar": return BarSeries; case "Baseline": return BaselineSeries; case "Candlestick": return CandlestickSeries; case "Histogram": return HistogramSeries; case "Line": return LineSeries; default: return CandlestickSeries; } } const { isDark } = useTheme(); export function useTradingView(target: MaybeRefOrGetter, options?: TradingViewOptions) { const opts: Required = mergeWith(cloneDeep(initializeOptions), cloneDeep(options)); const chart = ref(null); const chartSeriesDefinition = getChartSeriesDefinition(opts.type); const chartEl = ref(null); const series = ref | null>(null); const volumeSeries = ref | null>(null); function fitContent() { chart.value?.timeScale().fitContent(); } function resize() { if (!chart.value || !chartEl.value) return; const dimensions = chartEl.value.getBoundingClientRect(); chart.value.resize(dimensions.width, dimensions.height); } function setData(data: TradingViewData) { if (isRef(data)) { series.value?.setData(toValue(data)); } else if (isFunction(data)) { const result = data(); if (isPromise(result)) { result.then((data) => { series.value?.setData(data); fitContent(); }).catch((err) => { console.error("Failed to load trading view data:", err); }); } else { series.value?.setData(result); } } else { series.value?.setData(toValue(data)); } } function setVolumeData(data: MaybeRefOrGetter) { if (!volumeSeries.value) return; const volumeData = toValue(data); volumeSeries.value.setData(volumeData); } function changeTheme(theme: ThemeMode) { if (!chart.value) return; const layout = theme === "dark" ? darkThemeLayout : lightThemeLayout; chart.value.applyOptions({ layout }); } tryOnMounted(() => { const el = unrefElement(target); if (!el) return; chartEl.value = el; const chartOptions = toValue(opts.weightChartOptions); chart.value = createChart(el, chartOptions); series.value = chart.value.addSeries(chartSeriesDefinition, { priceFormat: { type: "price", precision: 1, }, }); // 添加成交量副图 if (opts.showVolume) { volumeSeries.value = chart.value.addSeries(HistogramSeries, { priceFormat: { type: "volume", }, priceScaleId: "", // 设置为右侧价格轴 }); // 配置成交量图表在底部 chart.value.priceScale("").applyOptions({ scaleMargins: { top: 0.8, bottom: 0, }, }); // 设置成交量数据 if (opts.volumeData) { setVolumeData(opts.volumeData); } } watch(isDark, (dark) => { changeTheme(dark ? "dark" : "light"); }, { immediate: true }); setData(opts.data); fitContent(); if (isRef(opts.data)) { watch(opts.data, (newData) => { setData(newData); }, { deep: true }); } if (isRef(opts.volumeData)) { watch(opts.volumeData, (newData) => { setVolumeData(newData); }, { deep: true }); } if (opts.autosize) { useTimeoutFn(() => { resize(); }, 0); window.addEventListener("resize", resize); } }); tryOnUnmounted(() => { chart.value?.remove(); chart.value = null; window.removeEventListener("resize", resize); }); watch(() => toValue(opts.weightChartOptions), (newOptions) => { if (!chart.value) return; chart.value.applyOptions(newOptions); }, { deep: true }); return { chart, series, volumeSeries, fitContent, resize, setVolumeData }; }