209 lines
5.5 KiB
TypeScript
209 lines
5.5 KiB
TypeScript
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<ChartOptions>;
|
|
|
|
export type TradingViewData = (() => Awaitable<TData[]>) | MaybeRefOrGetter<TData[]>;
|
|
|
|
export interface VolumeData extends HistogramData {
|
|
value: number;
|
|
color?: string;
|
|
}
|
|
|
|
export interface TradingViewOptions<T = Series> {
|
|
theme?: ThemeMode;
|
|
type?: T;
|
|
data?: TradingViewData;
|
|
volumeData?: MaybeRefOrGetter<VolumeData[]>;
|
|
weightChartOptions?: MaybeRefOrGetter<WeightChartOptions>;
|
|
autosize?: boolean;
|
|
showVolume?: boolean;
|
|
}
|
|
|
|
const lightThemeLayout: Partial<LayoutOptions> = {
|
|
textColor: "black",
|
|
background: { type: ColorType.Solid, color: "#fff" },
|
|
};
|
|
|
|
const darkThemeLayout: Partial<LayoutOptions> = {
|
|
textColor: "white",
|
|
background: { type: ColorType.Solid, color: "#000" },
|
|
};
|
|
|
|
const initializeOptions: Required<TradingViewOptions> = {
|
|
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<HTMLElement | null>, options?: TradingViewOptions) {
|
|
const opts: Required<TradingViewOptions> = mergeWith(cloneDeep(initializeOptions), cloneDeep(options));
|
|
const chart = ref<IChartApi | null>(null);
|
|
const chartSeriesDefinition = getChartSeriesDefinition(opts.type);
|
|
const chartEl = ref<HTMLElement | null>(null);
|
|
const series = ref<ISeriesApi<SeriesType> | null>(null);
|
|
const volumeSeries = ref<ISeriesApi<"Histogram"> | 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<VolumeData[]>) {
|
|
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 };
|
|
}
|