Kaynağa Gözat

合并代码,解决冲突

huangbin 4 yıl önce
ebeveyn
işleme
3081d064f6

+ 75 - 0
src/common/components/echart/echart-base/index.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class="echart" :style="{ width: width, height: height }">
+        <div ref="chartElement" class="echart__container"></div>
+    </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, onMounted, PropType, watch, watchEffect } from 'vue';
+import { EChartsOption } from 'echarts';
+import { useEchart } from './setup';
+
+export default defineComponent({
+    name: 'Echart',
+    props: {
+        // 图表宽度
+        width: {
+            type: String,
+            default: '100%',
+        },
+        // 图表高度
+        height: {
+            type: String,
+            default: '100%',
+        },
+        // 图表配置项
+        options: {
+            type: Object as PropType<EChartsOption>,
+            required: true,
+        },
+        loading: {
+            type: Boolean,
+            default: true,
+        },
+    },
+    setup(props, context) {
+        // 图表容器元素
+        const chartElement = ref<HTMLElement>();
+
+        onMounted(() => {
+            const { echart, setOptions, showLoading } = useEchart(chartElement.value!, context);
+
+            watchEffect(() => {
+                if (props.loading) {
+                    echart.clear();
+                    showLoading(true);
+                }
+            });
+
+            watch(
+                () => props.options,
+                (option) => {
+                    // 设置图表配置项
+                    setOptions(option);
+                }
+            );
+        });
+
+        return {
+            chartElement,
+        };
+    },
+});
+</script>
+
+<style lang="less">
+.echart {
+    position: relative;
+    overflow: hidden;
+
+    &__container {
+        width: 100%;
+        height: 100%;
+    }
+}
+</style>

+ 99 - 0
src/common/components/echart/echart-base/setup.ts

@@ -0,0 +1,99 @@
+import { onActivated, onDeactivated, onUnmounted, SetupContext } from "vue";
+import { debounce } from "@/utils/time"
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+import * as echarts from 'echarts'
+
+// 创建 iframe 对象
+const createIframe = (parent: HTMLElement): HTMLIFrameElement => {
+    const iframe = document.createElement('iframe');
+    // 设置 iframe 样式
+    iframe.style.cssText = 'position: absolute; z-index: -1000; left: 0; top: 0; width: 100%; height: 100%; border: 0; visibility: hidden; pointer-events: none;';
+    // 添加 iframe 节点
+    parent.appendChild(iframe);
+
+    return iframe;
+}
+
+type UseEchart = {
+    echart: echarts.ECharts,
+    setOptions: (options: echarts.EChartsOption, notMerge?: boolean) => void,
+    showLoading: (loading: boolean) => void,
+}
+
+export function useEchart(el: HTMLElement, ctx: SetupContext): UseEchart {
+    const iframe = createIframe(el.parentElement || el);
+    // 初始化图表
+    const echart = echarts.init(el);
+
+    // 显示加载动画
+    const showLoading = (loading: boolean) => {
+        if (loading) {
+            // 当前主题
+            const theme = getTheme();
+            switch (theme.value) {
+                case ThemeEnum.default:
+                case ThemeEnum.dark:
+                    echart.showLoading({
+                        text: '加载中...',
+                        textColor: '#fff',
+                        color: 'rgba(255, 255, 255, 0.75)',
+                        maskColor: 'transparent',
+                    });
+                    break;
+                default:
+                    echart.showLoading({
+                        text: '加载中...',
+                        maskColor: 'transparent',
+                    });
+            }
+        }
+    };
+
+    // 图表配置项
+    const setOptions = (options: echarts.EChartsOption, notMerge = false) => {
+        echart.setOption(options, notMerge);
+        echart.hideLoading();
+        ctx.emit('update:loading', false);
+    };
+
+    const onresize = () => {
+        debounce(() => {
+            // 如果 canvas 宽高和父元素不一致,重新渲染
+            if (echart.getWidth() !== el.clientWidth || echart.getHeight() !== el.clientHeight) {
+                echart.resize && echart.resize();
+            }
+        }, 50)
+    };
+
+    const addEventListener = () => {
+        // 监听 iframe 变化重置图表尺寸
+        iframe.contentWindow?.addEventListener('resize', onresize);
+    }
+
+    const removeEventListener = () => {
+        // 移除 iframe 监听事件
+        iframe.contentWindow?.removeEventListener('resize', onresize);
+    }
+
+    addEventListener();
+
+    onUnmounted(() => {
+        removeEventListener();
+    })
+
+    // 针对 keepalive 缓存组件
+    onActivated(() => {
+        addEventListener();
+    })
+
+    // 针对 keepalive 缓存组件
+    onDeactivated(() => {
+        removeEventListener();
+    })
+
+    return {
+        echart,
+        setOptions,
+        showLoading,
+    }
+}

+ 125 - 0
src/common/components/echart/echart-kline/index.vue

@@ -0,0 +1,125 @@
+<template>
+    <echart-base :options="options" v-model:loading="loading"></echart-base>
+</template>
+
+<script lang="ts">
+import { defineComponent, PropType, ref, watch, watchEffect } from 'vue';
+import { QueryHistoryDatasRsp, QueryQuoteDayRsp, QueryHistoryDatas, CycleType } from '@/services/go/quote/interface';
+import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
+import { handleEchart, ChartData } from './setup';
+import EchartBase from '../echart-base/index.vue';
+import moment from 'moment';
+
+export default defineComponent({
+    name: 'EchartKline',
+    components: {
+        EchartBase,
+    },
+    props: {
+        // 周期类型
+        cycleType: {
+            type: Number as PropType<CycleType>,
+            required: true,
+        },
+        // 实时行情数据
+        quoteData: {
+            type: Object as PropType<QueryQuoteDayRsp>,
+            required: true,
+        },
+    },
+    setup(props) {
+        const loading = ref(true);
+        const { chartData, options, updateOptions, initOptions } = handleEchart();
+
+        // 处理图表数据
+        const handleData = (rawData: QueryHistoryDatasRsp[]): ChartData => {
+            const datas: number[][] = [],
+                times: string[] = [];
+
+            rawData.forEach((item) => {
+                const { o, c, h, l, ts } = item;
+                const t = moment(ts).format('YYYY/MM/DD');
+                times.push(t);
+                datas.push([o, c, h, l]);
+            });
+
+            const { last, lasttime: lastTime } = props.quoteData;
+
+            return {
+                datas,
+                times,
+                last,
+                lastTime,
+                ma5: calcMA(rawData, 5),
+                ma10: calcMA(rawData, 10),
+                ma15: calcMA(rawData, 15),
+            };
+        };
+
+        // 计算平均线
+        const calcMA = (rawData: QueryHistoryDatasRsp[], count: number) => {
+            const result: string[] = [];
+            if (rawData.length >= count) {
+                // 所有非补充数据的索引id
+                const dataIndexs = rawData.reduce((prev: number[], cur, index) => {
+                    if (!cur.f) prev.push(index);
+                    return prev;
+                }, []);
+                // 均线起始位置
+                const startIndex = dataIndexs[count - 1];
+
+                rawData.forEach((item, index) => {
+                    if (index < startIndex) {
+                        result.push('-');
+                    } else {
+                        const j = dataIndexs.findIndex((val) => val === index);
+                        // 判断是否补充数据
+                        if (j === -1) {
+                            // 取上个平均值
+                            result.push(result[result.length - 1]);
+                        } else {
+                            // 向后取MA数
+                            const splitIndexs = dataIndexs.slice(j - (count - 1), j + 1);
+                            // 计算总价
+                            const total = splitIndexs.reduce((sum, val) => sum + rawData[val].c, 0);
+                            // 计算均线
+                            result.push((total / count).toFixed(2));
+                        }
+                    }
+                });
+            }
+            return result;
+        };
+
+        // 监听周期变化
+        watchEffect(() => {
+            loading.value = true;
+            const params: QueryHistoryDatas = {
+                cycleType: props.cycleType,
+                goodsCode: props.quoteData.goodscode,
+                isAsc: true,
+            };
+            // 查询K线数据
+            queryHistoryDatas(params).then((res) => {
+                chartData.value = handleData(res);
+                initOptions();
+            });
+        });
+
+        watch(
+            () => props.quoteData.last,
+            (val) => {
+                if (!loading.value) {
+                    chartData.value.last = val;
+                    updateOptions();
+                }
+            }
+        );
+
+        return {
+            loading,
+            options,
+        };
+    },
+});
+</script>

+ 288 - 0
src/common/components/echart/echart-kline/setup.ts

@@ -0,0 +1,288 @@
+import { ref, watch } from "vue";
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+import { EChartsOption } from 'echarts';
+
+// 命名待优化
+type Colors = {
+    backgroundColor: string, // 图表背景颜色
+    axisPointerLabelColor: string,
+    legendTextColor: string,
+    xAxisLineColor: string
+    yAxisLineColor: string,
+    seriesMarkLabelColor: string,
+    seriesMarkLineColor: string,
+}
+
+// 图表数据
+export type ChartData = {
+    datas: number[][], // 历史数据
+    times: string[], // 历史日期
+    last: number, // 最新行情价
+    lastTime: string, // 最新行情时间
+    ma5: (number | string)[], //均线数据
+    ma10: (number | string)[], //均线数据
+    ma15: (number | string)[], //均线数据
+}
+
+export function handleEchart() {
+    const options = ref<EChartsOption>();
+    // 当前主题
+    const theme = getTheme();
+    // 图表数据
+    const chartData = ref<ChartData>({
+        datas: [],
+        times: [],
+        last: 0,
+        lastTime: '',
+        ma5: [],
+        ma10: [],
+        ma15: [],
+    });
+
+    // 初始化图表配置
+    const initOptions = () => {
+        const { datas, times, last, ma5, ma10, ma15 } = chartData.value;
+        options.value = {
+            legend: {
+                //图例控件,点击图例控制哪些系列不显示
+                type: 'scroll',
+                itemWidth: 14,
+                itemHeight: 2,
+                left: '5%',
+                top: 0,
+                textStyle: {
+                    fontSize: 12,
+                },
+            },
+            // 悬浮框
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    type: 'cross',
+                },
+                backgroundColor: 'rgba(255,255,255,.9)',
+                borderWidth: 1,
+                borderRadius: 3,
+                textStyle: {
+                    color: '#4d535c',
+                },
+                // eslint-disable-next-line
+                formatter: (params: any) => {
+                    const firstParams = params[0];
+                    const data = firstParams.data;
+                    const result = `<div><span style="display: inline-block; width: 40px">日期:</span><span style="display: inline-block; width: 100px; text-align: right">${firstParams.name}</span></div>
+    <div><span style="display: inline-block; width: 40px">开盘价:</span><span style="display: inline-block; width: 100px; text-align: right">${data[1]}</span></div>
+    <div><span style="display: inline-block; width: 40px">收盘价:</span><span style="display: inline-block; width: 100px; text-align: right">${data[2]}</span></div>
+    <div><span style="display: inline-block; width: 40px">最低价:</span><span style="display: inline-block; width: 100px; text-align: right">${data[3]}</span></div>
+    <div><span style="display: inline-block; width: 40px">最高价:</span><span style="display: inline-block; width: 100px; text-align: right">${data[4]}</span></div>`;
+                    return result;
+                },
+            },
+            grid: {
+                top: '8%',
+                left: '8%',
+                right: '8%',
+                bottom: '8%',
+            },
+            xAxis: {
+                type: 'category',
+                // X轴时间线
+                data: times,
+                splitLine: {
+                    // 坐标分隔线
+                    show: true,
+                },
+            },
+            yAxis: {
+                scale: true,
+            },
+            dataZoom: [
+                {
+                    type: 'inside',
+                    // 起始显示K线条数(最新200条)
+                    startValue: times.length - 200,
+                    endValue: times.length,
+                    // 限制窗口缩放显示最少数据条数
+                    minValueSpan: 30,
+                },
+                {
+                    show: false,
+                    type: 'slider',
+                },
+            ],
+            series: [
+                {
+                    name: 'K线',
+                    type: 'candlestick',
+                    // Y轴数据
+                    data: datas,
+                    markLine: {
+                        animation: false,
+                        // 标线两端图标
+                        symbol: 'none',
+                        // 标线标签样式
+                        label: {
+                            fontWeight: 'bold',
+                            position: 'end',
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            type: 'dashed',
+                        },
+                        data: [
+                            {
+                                // 最新价
+                                yAxis: last,
+                            },
+                        ],
+                    },
+                },
+                {
+                    name: 'MA5',
+                    type: 'line',
+                    data: ma5,
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'MA10',
+                    type: 'line',
+                    data: ma10,
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+                {
+                    name: 'MA15',
+                    type: 'line',
+                    data: ma15,
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        };
+
+        // 先用 setTimeout 处理,待优化深度合并
+        setTimeout(() => {
+            options.value = getColors(theme.value);
+        }, 0);
+    };
+
+    // 动态更新数据
+    const updateOptions = () => {
+        const { datas, last } = chartData.value;
+        if (datas.length) {
+            options.value = {
+                series: [
+                    {
+                        markLine: {
+                            data: [
+                                {
+                                    // 最新价
+                                    yAxis: last,
+                                },
+                            ],
+                        },
+                    },
+                ],
+            };
+        }
+    };
+
+    // 设置图表样式
+    const setColors = (colors: Colors): EChartsOption => {
+        return {
+            // 图表背景颜色
+            backgroundColor: colors.backgroundColor,
+            axisPointer: {
+                label: {
+                    color: colors.axisPointerLabelColor,
+                },
+            },
+            legend: {
+                textStyle: {
+                    color: colors.legendTextColor,
+                },
+            },
+            xAxis: {
+                splitLine: {
+                    lineStyle: {
+                        // 坐标分隔线颜色
+                        color: colors.xAxisLineColor,
+                    },
+                },
+            },
+            yAxis: {
+                splitLine: {
+                    lineStyle: {
+                        // 坐标分隔线颜色
+                        color: colors.xAxisLineColor,
+                    },
+                },
+            },
+            series: [
+                {
+                    markLine: {
+                        // 标线标签样式
+                        label: {
+                            color: colors.seriesMarkLabelColor,
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            color: colors.seriesMarkLineColor,
+                        },
+                    },
+                }
+            ]
+        }
+    }
+
+    // 获取图表样式配置
+    const getColors = (theme: ThemeEnum) => {
+        switch (theme) {
+            case ThemeEnum.default:
+            case ThemeEnum.dark:
+                return setColors({
+                    backgroundColor: 'transparent',
+                    axisPointerLabelColor: '#fff',
+                    legendTextColor: '#0e99e2',
+                    xAxisLineColor: '#171B1D',
+                    yAxisLineColor: '#171B1D',
+                    seriesMarkLabelColor: '#3C454B',
+                    seriesMarkLineColor: '#33393D',
+                });
+            case ThemeEnum.light:
+                return setColors({
+                    backgroundColor: 'transparent',
+                    axisPointerLabelColor: '#fff',
+                    legendTextColor: '#FC9618',
+                    xAxisLineColor: '#DAE5EC',
+                    yAxisLineColor: '#DAE5EC',
+                    seriesMarkLabelColor: '#ACB8C0',
+                    seriesMarkLineColor: '#ACB8C0',
+                });
+        }
+    }
+
+    watch(theme, (val) => {
+        options.value = getColors(val);
+    });
+
+    return {
+        chartData,
+        options,
+        initOptions,
+        updateOptions,
+    }
+}

+ 150 - 0
src/common/components/echart/echart-timeline/index.vue

@@ -0,0 +1,150 @@
+<template>
+    <echart-base :options="options" v-model:loading="loading"></echart-base>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watch, PropType } from 'vue';
+import { QueryTSDataRsp, QueryQuoteDayRsp } from '@/services/go/quote/interface';
+import { QueryTSData } from '@/services/go/quote';
+import { getGoodsByCode } from '@/services/bus/goods';
+import { handleEchart, ChartData } from './setup';
+import EchartBase from '../echart-base/index.vue';
+import { getRangeTime } from '@/utils/time';
+import { toDecimalFull } from '@/utils/number';
+
+export default defineComponent({
+    name: 'EchartTime',
+    components: {
+        EchartBase,
+    },
+    props: {
+        // 实时行情数据
+        quoteData: {
+            type: Object as PropType<QueryQuoteDayRsp>,
+            required: true,
+        },
+    },
+    setup(props) {
+        const loading = ref(true);
+        const { chartData, options, updateOptions, initOptions } = handleEchart();
+
+        // 处理图表数据
+        const handleData = (rawData: QueryTSDataRsp): ChartData => {
+            const datas = rawData.historyDatas!.map((item) => item.c),
+                startTime = rawData.startTime!,
+                endTime = rawData.endTime!,
+                times = getRangeTime(startTime, endTime, 'HH:mm', 'm'),
+                yestclose = rawData.preSettle!,
+                decimal = rawData.decimalPlace!;
+
+            const { last, lasttime: lastTime } = props.quoteData;
+            const { min, max } = calcDataLine(datas, yestclose, last);
+
+            return {
+                datas,
+                times,
+                yestclose,
+                last,
+                lastTime,
+                decimal,
+                min,
+                max,
+                ma5: calcMA(rawData, 5),
+            };
+        };
+
+        // 计算图表最高低指标线
+        const calcDataLine = (datas: number[], yestclose: number, last: number) => {
+            let max = Math.max(...datas, last); // 取历历史数据最高价
+            let min = Math.min(...datas, last); // 取历史数据最低价
+            const a = yestclose - min; // 计算收盘价和最低价的差值
+            const b = max - yestclose; // 计算收盘价和最高价的差值
+
+            // 比较差值大小
+            if (a > b) {
+                max = yestclose + a;
+                if (last > max) {
+                    const c = last - max;
+                    max += c;
+                    min -= c;
+                }
+            } else {
+                min = min - (b - a);
+                if (min > last) {
+                    const c = min - last;
+                    max += c;
+                    min -= c;
+                }
+            }
+
+            return {
+                max: max + max * 0.01,
+                min: min - min * 0.01,
+            };
+        };
+
+        // 计算平均线
+        const calcMA = (rawData: QueryTSDataRsp, count: number) => {
+            const datas = rawData.historyDatas!,
+                decimal = rawData.decimalPlace!;
+
+            const result: string[] = [];
+            if (datas.length >= count) {
+                // 所有非补充数据的索引id
+                const dataIndexs = datas.reduce((prev: number[], cur, index) => {
+                    if (!cur.f) prev.push(index);
+                    return prev;
+                }, []);
+                // 均线起始位置
+                const startIndex = dataIndexs[count - 1];
+
+                datas.forEach((item, index) => {
+                    if (index < startIndex) {
+                        result.push('-');
+                    } else {
+                        const j = dataIndexs.findIndex((val) => val === index);
+                        // 判断是否补充数据
+                        if (j === -1) {
+                            // 取上个平均值
+                            result.push(result[result.length - 1]);
+                        } else {
+                            // 向后取MA数
+                            const splitIndexs = dataIndexs.slice(j - (count - 1), j + 1);
+                            // 计算总价
+                            const total = splitIndexs.reduce((sum, val) => sum + datas[val].c, 0);
+                            // 计算均线
+                            const average = toDecimalFull(total / count, decimal);
+                            result.push(average);
+                        }
+                    }
+                });
+            }
+            return result;
+        };
+
+        // 查询分时数据
+        QueryTSData(props.quoteData.goodscode).then((res) => {
+            chartData.value = handleData(res);
+            initOptions();
+        });
+
+        watch(
+            () => props.quoteData.last,
+            (val) => {
+                if (!loading.value) {
+                    const { min, max } = calcDataLine(chartData.value.datas, chartData.value.yestclose, val);
+                    chartData.value.last = val;
+                    chartData.value.min = min;
+                    chartData.value.max = max;
+                    updateOptions();
+                }
+            }
+        );
+
+        return {
+            loading,
+            options,
+        };
+    },
+});
+</script>

+ 392 - 0
src/common/components/echart/echart-timeline/setup.ts

@@ -0,0 +1,392 @@
+import { ref, watch } from "vue";
+import { toDecimalFull } from '@/utils/number';
+import { getTheme, ThemeEnum } from '@/common/config/theme';
+import { EChartsOption } from 'echarts';
+import * as echarts from 'echarts';
+
+// 命名待优化
+type Colors = {
+    backgroundColor: string, // 图表背景颜色
+    axisPointerLabelColor: string,
+    legendTextColor: string,
+    xAxisLineColor: string
+    yAxisLineColor: string,
+    seriesMarkLabelColor: string,
+    seriesMarkLineColor: string,
+    upColor: string,
+    downColor: string,
+    seriesLineColor: string,
+    seriesAreaGradients: echarts.LinearGradientObject,
+}
+
+// 图表数据
+export type ChartData = {
+    datas: number[], // 历史数据
+    times: string[], // 历史日期
+    yestclose: number, // 昨日收盘价
+    last: number, // 最新行情价
+    lastTime: string, // 最新行情时间
+    decimal: number, // 保留小数位
+    min: number, // Y轴最低指标线
+    max: number, // Y轴最高指标线
+    ma5: (number | string)[], //均线数据
+}
+
+export function handleEchart() {
+    const options = ref<EChartsOption>({});
+    // 当前主题
+    const theme = getTheme();
+    // 图表数据
+    const chartData = ref<ChartData>({
+        datas: [],
+        times: [],
+        yestclose: 0,
+        last: 0,
+        lastTime: '',
+        decimal: 0,
+        min: 0,
+        max: 0,
+        ma5: []
+    });
+
+    // 计算涨跌幅百分比
+    const calcRatio = (val: number) => {
+        const result = (Number(val) - chartData.value.yestclose) / chartData.value.yestclose * 100;
+        return toDecimalFull(result) + '%';
+    }
+
+    // 初始化图表配置
+    const initOptions = () => {
+        const { datas, times, yestclose, last, min, max, ma5, decimal } = chartData.value;
+        options.value = {
+            legend: {
+                //图例控件,点击图例控制哪些系列不显示
+                type: 'scroll',
+                itemWidth: 14,
+                itemHeight: 2,
+                left: '5%',
+                top: 0,
+                textStyle: {
+                    fontSize: 12,
+                },
+            },
+            // 悬浮框
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    type: 'cross',
+                },
+                backgroundColor: 'rgba(255,255,255,.9)',
+                borderWidth: 1,
+                borderRadius: 3,
+                textStyle: {
+                    color: '#4d535c',
+                },
+                // eslint-disable-next-line
+                formatter: (params: any) => {
+                    const i = params[0].dataIndex;
+                    const result = `
+    <div><span style="display: inline-block; width: 40px">当前价:</span><span style="display: inline-block; width: 100px; text-align: right">${datas[i]}</span></div>
+    <div><span style="display: inline-block; width: 40px">均价:</span><span style="display: inline-block; width: 100px; text-align: right">${ma5[i]}</span></div>
+    <div><span style="display: inline-block; width: 40px">涨幅:</span><span style="display: inline-block; width: 100px; text-align: right">${calcRatio(datas[i])}</span></div>`;
+                    return result;
+                },
+            },
+            grid: {
+                top: '8%',
+                left: '8%',
+                right: '8%',
+                bottom: '8%',
+            },
+            xAxis: {
+                type: 'category',
+                // X轴时间线
+                data: times,
+                splitLine: {
+                    // 坐标分隔线
+                    show: true,
+                },
+            },
+            yAxis: [
+                // Y轴左侧标签
+                {
+                    id: 'leftPrice',
+                    scale: true,
+                    min: min,
+                    max: max,
+                    axisLabel: {
+                        formatter: (val: number) => toDecimalFull(val, decimal),
+                    }
+                },
+                // Y轴右侧标签
+                {
+                    id: 'rightRatio',
+                    scale: true,
+                    min: min,
+                    max: max,
+                    splitLine: {
+                        show: false,
+                    },
+                    axisLabel: {
+                        formatter: (val: number) => calcRatio(val),
+                    }
+                },
+            ],
+            // series 中不指定 yAxisId 或 yAxisIndex 默认关联 yAxis 第一个配置,xAxis 配置同理
+            series: [
+                {
+                    name: '当前价',
+                    type: 'line',
+                    yAxisId: 'leftPrice',
+                    data: chartData.value.datas,
+                    smooth: true,
+                    symbol: 'circle', //中时有小圆点
+                    lineStyle: {
+                        opacity: 0.8,
+                        width: 1,
+                    },
+                    markLine: {
+                        // 标线两端图标
+                        symbol: 'none',
+                        // 标线标签样式
+                        data: [
+                            {
+                                // 最新价
+                                yAxis: last,
+                            },
+                            {
+                                // 昨结价
+                                yAxis: yestclose,
+                                lineStyle: {
+                                    color: '#333',
+                                },
+                                label: {
+                                    show: false,
+                                }
+                            },
+                        ],
+                    },
+                },
+                {
+                    type: 'line',
+                    yAxisId: 'rightRatio', // 关联Y轴右侧标签
+                    data: datas,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 0,
+                    },
+                },
+                {
+                    name: '均价',
+                    type: 'line',
+                    data: ma5,
+                    smooth: true,
+                    symbol: 'none',
+                    lineStyle: {
+                        width: 1,
+                        opacity: 0.8,
+                    },
+                },
+            ],
+        };
+
+        // 先用 setTimeout 处理,待优化深度合并
+        setTimeout(() => {
+            options.value = getColors(theme.value);
+        }, 0);
+    };
+
+    // 动态更新数据
+    const updateOptions = () => {
+        const { datas, last, yestclose, min, max } = chartData.value;
+        if (datas.length) {
+            options.value = {
+                yAxis: [
+                    // Y轴左侧标签
+                    {
+                        id: 'leftPrice',
+                        min: min,
+                        max: max,
+                    },
+                    // Y轴右侧标签
+                    {
+                        id: 'rightRatio',
+                        min: min,
+                        max: max,
+                    },
+                ],
+                series: [
+                    {
+                        markLine: {
+                            data: [
+                                {
+                                    // 最新价
+                                    yAxis: last,
+                                },
+                                {
+                                    // 昨结价
+                                    yAxis: yestclose,
+                                    lineStyle: {
+                                        color: '#333',
+                                    },
+                                    label: {
+                                        show: false,
+                                    }
+                                },
+                            ],
+                        },
+                    },
+                ],
+            };
+        }
+    };
+
+    // 设置图表样式
+    const setColors = (colors: Colors): EChartsOption => {
+        const { yestclose } = chartData.value;
+        return {
+            // 图表背景颜色
+            backgroundColor: colors.backgroundColor,
+            axisPointer: {
+                label: {
+                    color: colors.axisPointerLabelColor,
+                },
+            },
+            legend: {
+                textStyle: {
+                    color: colors.legendTextColor,
+                },
+            },
+            xAxis: {
+                splitLine: {
+                    lineStyle: {
+                        // 坐标分隔线颜色
+                        color: colors.xAxisLineColor,
+                    },
+                },
+            },
+            yAxis: [
+                // Y轴左侧标签
+                {
+                    id: 'leftPrice',
+                    axisLabel: {
+                        color: (val: any) => {
+                            if (val > yestclose) return colors.upColor;
+                            if (val < yestclose) return colors.downColor;
+                            return '#3C454B';
+                        },
+                    },
+                    splitLine: {
+                        lineStyle: {
+                            // 坐标分隔线颜色
+                            color: colors.yAxisLineColor,
+                        },
+                    },
+                },
+                // Y轴右侧标签
+                {
+                    id: 'rightRatio',
+                    axisLabel: {
+                        color: (val: any) => {
+                            if (val > yestclose) return colors.upColor;
+                            if (val < yestclose) return colors.downColor;
+                            return '#3C454B';
+                        },
+                    },
+                },
+            ],
+            series: [
+                {
+                    lineStyle: {
+                        color: colors.seriesLineColor,
+                    },
+                    areaStyle: {
+                        color: colors.seriesAreaGradients,
+                        shadowColor: 'rgba(0, 0, 0, 0.1)',
+                        shadowBlur: 10,
+                    },
+                    markLine: {
+                        // 标线标签样式
+                        label: {
+                            color: colors.seriesMarkLabelColor,
+                        },
+                        // 标线样式
+                        lineStyle: {
+                            color: colors.seriesMarkLineColor,
+                        },
+                    },
+                }
+            ]
+        }
+    }
+
+    // 获取图表样式配置
+    const getColors = (theme: ThemeEnum) => {
+        switch (theme) {
+            case ThemeEnum.default:
+            case ThemeEnum.dark:
+                return setColors({
+                    backgroundColor: 'transparent',
+                    axisPointerLabelColor: '#fff',
+                    legendTextColor: '#0e99e2',
+                    xAxisLineColor: '#171B1D',
+                    yAxisLineColor: '#171B1D',
+                    seriesMarkLabelColor: '#3C454B',
+                    seriesMarkLineColor: '#33393D',
+                    upColor: '#FF2B2B',
+                    downColor: '#1FF195',
+                    seriesLineColor: '#39afe6',
+                    seriesAreaGradients: new echarts.graphic.LinearGradient(0, 0, 0, 1,
+                        [
+                            {
+                                offset: 0,
+                                color: 'rgba(0, 136, 212, 0.7)',
+                            },
+                            {
+                                offset: 0.8,
+                                color: 'rgba(0, 136, 212, 0.02)',
+                            },
+                        ],
+                        false
+                    ),
+                });
+            case ThemeEnum.light:
+                return setColors({
+                    backgroundColor: 'transparent',
+                    axisPointerLabelColor: '#fff',
+                    legendTextColor: '#FC9618',
+                    xAxisLineColor: '#DAE5EC',
+                    yAxisLineColor: '#DAE5EC',
+                    seriesMarkLabelColor: '#666',
+                    seriesMarkLineColor: '#ACB8C0',
+                    upColor: '#FF2B2B',
+                    downColor: '#00A843',
+                    seriesLineColor: '#3864d7',
+                    seriesAreaGradients: new echarts.graphic.LinearGradient(0, 0, 0, 1,
+                        [
+                            {
+                                offset: 0,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                            {
+                                offset: 1,
+                                color: 'rgba(0, 136, 212, 0.3)',
+                            },
+                        ],
+                        false
+                    ),
+                });
+        }
+    }
+
+    watch(theme, (val) => {
+        options.value = getColors(val);
+    });
+
+    return {
+        chartData,
+        options,
+        initOptions,
+        updateOptions,
+    }
+}

+ 7 - 0
src/common/components/echart/index.ts

@@ -0,0 +1,7 @@
+import EchartKline from './echart-kline/index.vue';
+import EchartTime from './echart-timeline/index.vue';
+
+export {
+    EchartKline,
+    EchartTime,
+}

+ 4 - 4
src/services/go/quote/index.ts

@@ -6,7 +6,7 @@ import * as type from './interface';
  * @param param QueryHistoryDatas
  * @returns QueryHistoryDatasRsp
  */
-export function QueryHistoryDatas(param: type.QueryHistoryDatas): Promise<type.QueryHistoryDatasRsp> {
+export function QueryHistoryDatas(param: type.QueryHistoryDatas): Promise<type.QueryHistoryDatasRsp[]> {
     return commonSearch_go('/Quote/QueryHistoryDatas', param)
 }
 
@@ -33,9 +33,9 @@ export function QueryQuoteDay(goodsCodes: string): Promise<string> {
 
 /**
  * 分时图数据查询
- * @param goodsCodes 商品代码
+ * @param goodsCode 商品代码
  * @returns
  */
-export function QueryTSData(goodsCodes: string): Promise<type.QueryTSDataRsp> {
-    return commonSearch_go('/Quote/QueryTSData', { goodsCodes })
+export function QueryTSData(goodsCode: string): Promise<type.QueryTSDataRsp> {
+    return commonSearch_go('/Quote/QueryTSData', { goodsCode })
 }

+ 149 - 148
src/services/go/quote/interface.ts

@@ -1,12 +1,12 @@
-// 周期类型, 0-秒 1: 1分钟 2: 5分钟 3: 30分钟 4: 60分钟 120: 2小时 240: 4小时 11: 日线
-export type CycleType = 0 | 1 | 2 | 3 | 120 | 240 | 11;
+// 周期类型,-1:分时 0-秒 1: 1分钟 2: 5分钟 3: 30分钟 4: 60分钟 120: 2小时 240: 4小时 11: 日线
+export type CycleType = -1 | 0 | 1 | 2 | 3 | 4 | 120 | 240 | 11;
 
 export interface QueryHistoryTikDatas {
     goodsCode: string, // 商品代码
     startTime?: string, // 开始时间,格式:yyyy-MM-dd HH:mm:ss
     endTime?: string, // 结束时间,格式:yyyy-MM-dd HH:mm:ss
     count?: number, // 条数
-    isAsc? : boolean, // 是否按时间顺序排序(默认为时间倒序排序)
+    isAsc?: boolean, // 是否按时间顺序排序(默认为时间倒序排序)
 }
 
 export interface QueryHistoryDatas extends QueryHistoryTikDatas {
@@ -15,6 +15,7 @@ export interface QueryHistoryDatas extends QueryHistoryTikDatas {
 
 export interface QueryHistoryDatasRsp {
     c: number; //收盘价
+    f: boolean; //是否补充数据
     h: number; //最高价
     hv: number; //持仓量
     l: number; //最低价
@@ -53,155 +54,155 @@ export interface historyDatas {
     tv: number;
 }
 export interface QueryTSDataRsp {
-    Count: number;
-  decimalPlace: number;
-  endTime: string;
-  goodsCode: string;
-   outGoodsCode: string;
-  preSettle: number;
-  startTime: string;
-  tradeDate: string;
-  historyDatas: historyDatas[],
-  RunSteps: RunSteps[],
+    Count?: number;
+    decimalPlace?: number;
+    endTime?: string;
+    goodsCode?: string;
+    outGoodsCode?: string;
+    preSettle?: number;
+    startTime?: string;
+    tradeDate?: string;
+    historyDatas?: historyDatas[];
+    RunSteps?: RunSteps[];
 }
 
 export interface RunSteps {
-      end: string;
-      endflag: number;
-      endtime: string;
-      endweekday: number;
-      groupid: number;
-      runstep: number;
-      sectionid: number;
-      start: string;
-      startflag: number;
-      starttime: string;
-      startweekday: number;
-      tradeweekday: number;
+    end: string;
+    endflag: number;
+    endtime: string;
+    endweekday: number;
+    groupid: number;
+    runstep: number;
+    sectionid: number;
+    start: string;
+    startflag: number;
+    starttime: string;
+    startweekday: number;
+    tradeweekday: number;
 }
 
 export interface QueryQuoteDayRsp {
-  ask: number;
-  ask1number: number;
-  ask2: number;
-  ask3: number;
-  ask4: number;
-  ask5: number;
-  ask6: number;
-  ask7: number;
-  ask8: number;
-  ask9: number;
-  askorderid: number;
-  askorderid2: number;
-  askorderid3: number;
-  askorderid4: number;
-  askorderid5: number;
-  askordervolume: number;
-  askordervolume1number: number;
-  askordervolume2: number;
-  askordervolume3: number;
-  askordervolume4: number;
-  askordervolume5: number;
-  askordervolume6: number;
-  askordervolume7: number;
-  askordervolume8: number;
-  askordervolume9: number;
-  askqueueinfo :string;
-  askvolume: number;
-  askvolume1number: number;
-  askvolume2: number;
-  askvolume3: number;
-  askvolume4: number;
-  askvolume5: number;
-  askvolume6: number;
-  askvolume7: number;
-  askvolume8: number;
-  askvolume9: number;
-  averageprice: number;
-  bid: number;
-  bid1number: number;
-  bid2: number;
-  bid3: number;
-  bid4: number;
-  bid5: number;
-  bid6: number;
-  bid7: number;
-  bid8: number;
-  bid9: number;
-  bidorderid: number;
-  bidorderid2: number;
-  bidorderid3: number;
-  bidorderid4: number;
-  bidorderid5: number;
-  bidordervolume: number;
-  bidordervolume1number: number;
-  bidordervolume2: number;
-  bidordervolume3: number;
-  bidordervolume4: number;
-  bidordervolume5: number;
-  bidordervolume6: number;
-  bidordervolume7: number;
-  bidordervolume8: number;
-  bidordervolume9: number;
-  bidqueueinfo: string;
-  bidvolume: number;
-  bidvolume1number: number;
-  bidvolume2: number;
-  bidvolume3: number;
-  bidvolume4: number;
-  bidvolume5: number;
-  bidvolume6: number;
-  bidvolume7: number;
-  bidvolume8: number;
-  bidvolume9: number;
-  calloptionpremiums: number;
-  calloptionpremiums2: number;
-  calloptionpremiums3: number;
-  calloptionpremiums4: number;
-  calloptionpremiums5: number;
-  cleartime: number;
-  exchangecode: number;
-  exchangedate: number;
-  goodscode: string;
-  grepmarketprice: number;
-  highest: number;
-  holdincrement: number;
-  holdvolume: number;
-  iep: number;
-  iev: number;
-  inventory: number;
-  iscleared: number;
-  issettled: number;
-  last: number;
-  lastlot: number;
-  lasttime: string;
-  lastturnover: number;
-  lastvolume: number;
-  limitdown: number;
-  limitup: number;
-  lowest: number;
-  nontotalholdervolume: number;
-  nontotallot: number;
-  nontotalturnover: number;
-  nontotalvolume: number;
-  opened: number;
-  opentime: string;
-  orderid: number;
-  preclose: number;
-  preholdvolume: number;
-  presettle: number;
-  publictradetype: string;
-  putoptionpremiums: number;
-  putoptionpremiums2: number;
-  putoptionpremiums3: number;
-  putoptionpremiums4: number;
-  putoptionpremiums5: number;
-  settle: number;
-  strikeprice: number;
-  totalaskvolume: number;
-  totalbidvolume: number;
-  totallot: number;
-  totalturnover: number;
-  totalvolume: number;
-  utclasttime: string;
+    ask: number;
+    ask1number: number;
+    ask2: number;
+    ask3: number;
+    ask4: number;
+    ask5: number;
+    ask6: number;
+    ask7: number;
+    ask8: number;
+    ask9: number;
+    askorderid: number;
+    askorderid2: number;
+    askorderid3: number;
+    askorderid4: number;
+    askorderid5: number;
+    askordervolume: number;
+    askordervolume1number: number;
+    askordervolume2: number;
+    askordervolume3: number;
+    askordervolume4: number;
+    askordervolume5: number;
+    askordervolume6: number;
+    askordervolume7: number;
+    askordervolume8: number;
+    askordervolume9: number;
+    askqueueinfo: string;
+    askvolume: number;
+    askvolume1number: number;
+    askvolume2: number;
+    askvolume3: number;
+    askvolume4: number;
+    askvolume5: number;
+    askvolume6: number;
+    askvolume7: number;
+    askvolume8: number;
+    askvolume9: number;
+    averageprice: number;
+    bid: number;
+    bid1number: number;
+    bid2: number;
+    bid3: number;
+    bid4: number;
+    bid5: number;
+    bid6: number;
+    bid7: number;
+    bid8: number;
+    bid9: number;
+    bidorderid: number;
+    bidorderid2: number;
+    bidorderid3: number;
+    bidorderid4: number;
+    bidorderid5: number;
+    bidordervolume: number;
+    bidordervolume1number: number;
+    bidordervolume2: number;
+    bidordervolume3: number;
+    bidordervolume4: number;
+    bidordervolume5: number;
+    bidordervolume6: number;
+    bidordervolume7: number;
+    bidordervolume8: number;
+    bidordervolume9: number;
+    bidqueueinfo: string;
+    bidvolume: number;
+    bidvolume1number: number;
+    bidvolume2: number;
+    bidvolume3: number;
+    bidvolume4: number;
+    bidvolume5: number;
+    bidvolume6: number;
+    bidvolume7: number;
+    bidvolume8: number;
+    bidvolume9: number;
+    calloptionpremiums: number;
+    calloptionpremiums2: number;
+    calloptionpremiums3: number;
+    calloptionpremiums4: number;
+    calloptionpremiums5: number;
+    cleartime: number;
+    exchangecode: number;
+    exchangedate: number;
+    goodscode: string;
+    grepmarketprice: number;
+    highest: number;
+    holdincrement: number;
+    holdvolume: number;
+    iep: number;
+    iev: number;
+    inventory: number;
+    iscleared: number;
+    issettled: number;
+    last: number;
+    lastlot: number;
+    lasttime: string;
+    lastturnover: number;
+    lastvolume: number;
+    limitdown: number;
+    limitup: number;
+    lowest: number;
+    nontotalholdervolume: number;
+    nontotallot: number;
+    nontotalturnover: number;
+    nontotalvolume: number;
+    opened: number;
+    opentime: string;
+    orderid: number;
+    preclose: number;
+    preholdvolume: number;
+    presettle: number;
+    publictradetype: string;
+    putoptionpremiums: number;
+    putoptionpremiums2: number;
+    putoptionpremiums3: number;
+    putoptionpremiums4: number;
+    putoptionpremiums5: number;
+    settle: number;
+    strikeprice: number;
+    totalaskvolume: number;
+    totalbidvolume: number;
+    totallot: number;
+    totalturnover: number;
+    totalvolume: number;
+    utclasttime: string;
 }

+ 27 - 0
src/utils/number/index.ts

@@ -19,3 +19,30 @@ export function getDecimalsNum(val: any, decimal = 2, maxCount = 6) {
     }
     return result
 }
+
+/**
+ * 强制保留小数位,零位四舍五入取整
+ * @param val 
+ * @param decimal 需要保留小数位 默认2 
+ * @returns 
+ */
+export function toDecimalFull(val: number, decimal = 2) {
+    if (decimal <= 0) {
+        return Math.round(val).toString();
+    } else {
+        let str = val.toString();
+        const num = str.indexOf('.');
+        if (num < 0) {
+            // 小数位自动补零
+            if (decimal > 0) {
+                const count = str.length;
+                str += '.';
+                while (str.length <= count + decimal) {
+                    str += '0';
+                }
+            }
+            return str;
+        }
+        return val.toFixed(decimal);
+    }
+}

+ 0 - 14
src/utils/qt/common.ts

@@ -1,17 +1,3 @@
-import timerUtil, { TimeoutTimerNames } from '@/utils/timer/timerUtil';
-/**
- * 防抖(debounce)
- * @param fn 需要防抖的函数
- * @param wait 毫秒,防抖期限值
- * @returns
- */
-export function debounce(fn: () => void, wait: number, timer: keyof TimeoutTimerNames = 'debounce'): void {
-    return (function () {
-        timerUtil.clearTimeout(timer);
-        timerUtil.setTimeout(fn, wait, timer);
-    })();
-}
-
 /**
  * 生成UUID
  */

+ 32 - 14
src/utils/time/index.ts

@@ -1,4 +1,5 @@
-import moment, { Moment } from "moment";
+import timerUtil, { TimeoutTimerNames } from '@/utils/timer/timerUtil';
+import moment, { DurationInputArg1, Moment, unitOfTime } from "moment";
 
 /**
  * 获取number类型时间戳
@@ -14,26 +15,43 @@ type Time = string | Moment | Date
  * @param val1 时间1
  * @param val2 时间2
  * @param type 时间格式化类型
+ * @param unit 时间单位
+ * @param amount 时间区间间隔数
  * @returns string[]类型
  */
-export function getRangeTime(val1: Time, val2: Time, type = 'YYYYMMDD'): string[] {
-    const fn = (val: Time) => moment(val).format(type)
-    const result: string[] = []
-    // 处理开始时间和结束时间
-    let startTime = fn(val1)
-    let endTime = fn(val2)
-    const isSame = () => moment(startTime).isSame(moment(endTime))
+export function getRangeTime(val1: Time, val2: Time, type = 'YYYYMMDD', unit: unitOfTime.DurationConstructor = 'd', amount: DurationInputArg1 = 1): string[] {
+    const fn = (val: Moment) => val.format(type);
+    // 处理开始时间和结束时间
+    let startTime = moment(val1);
+    let endTime = moment(val2);
+
+    const result: string[] = [];
+    const isSame = () => startTime.isSame(endTime, unit);
+
     if (isSame()) {
-        return [startTime, startTime]
+        return [fn(startTime), fn(startTime)];
     } else {
-        if (moment(startTime).isAfter(moment(endTime))) {
+        if (startTime.isAfter(endTime)) {
             [startTime, endTime] = [endTime, startTime]
         }
         while (!isSame()) {
-            result.push(startTime)
-            startTime = moment(startTime).add(1, 'd').format(type)
+            result.push(fn(startTime));
+            startTime = startTime.add(amount, unit);
         }
-        result.push(endTime)
-        return result
+        result.push(fn(endTime));
+        return result;
     }
+}
+
+/**
+ * 防抖(debounce)
+ * @param fn 需要防抖的函数
+ * @param wait 毫秒,防抖期限值
+ * @returns
+ */
+export function debounce(fn: () => void, wait: number, timer: keyof TimeoutTimerNames = 'debounce'): void {
+    return (function () {
+        timerUtil.clearTimeout(timer);
+        timerUtil.setTimeout(fn, wait, timer);
+    })();
 }

+ 163 - 0
src/views/market/spot_trade/spot_trade_reference_market/components/chart/index.less

@@ -0,0 +1,163 @@
+.chart-container {
+    [theme='light'] & {
+        --bgcolor: #fff;
+        --tab-border-color: #dae5ec;
+        --tab-checked-color: #0866b8;
+        --tab-checked-bgcolor: #d4e0ff;
+        --slider-border-color: #b2c4dd;
+        --slider-bgcolor: #edf2f7;
+        --slider-button-color: #b2c4dd;
+    }
+
+    display: flex;
+    width: 100%;
+    height: calc(100% - 41px);
+    background-color: var(--bgcolor, #0e0e0f);
+
+    .chart-content {
+        display: flex;
+        flex-direction: column;
+        flex: 1;
+        height: 100%;
+
+        &__main {
+            flex: 1;
+        }
+
+        &__header {
+            padding: 4px 0;
+        }
+
+        &__tabs {
+            .ant-radio-button-wrapper {
+                height: 22px;
+                line-height: 20px;
+                color: #7a8a94;
+                border-color: var(--tab-border-color, #22292c) !important;
+                background-color: transparent;
+
+                &:not(:first-child)::before {
+                    background-color: var(--tab-border-color, #22292c) !important;
+                }
+            }
+            .ant-radio-button-wrapper-checked {
+                color: var(--tab-checked-color, #0866b8);
+                background-color: var(--tab-checked-bgcolor, #0e2f4c);
+
+                &:not(.ant-radio-button-wrapper-disabled)::before {
+                    background-color: var(--tab-border-color, #22292c) !important;
+                }
+
+                &:not(.ant-radio-button-wrapper-disabled):focus-within {
+                    box-shadow: none;
+                }
+            }
+        }
+    }
+
+    .chart-slider {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 6px;
+        border-left: 1px solid var(--slider-border-color, #33393d);
+        border-right: 1px solid var(--slider-border-color, #33393d);
+        background-color: var(--slider-bgcolor, #0e0e0f);
+
+        &__button {
+            width: 2px;
+            height: 30px;
+            font-size: 0;
+            background-color: var(--slider-button-color, #3c454b);
+            border-radius: 1px;
+        }
+    }
+
+    .chart-tips {
+        width: 300px;
+        height: 100%;
+
+        &__nav {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+
+            .content {
+                &:first-child:not(:last-child) {
+                    margin-right: 16px;
+                }
+
+                &--left {
+                    font-size: 16px;
+                    color: #3c454b;
+                }
+
+                &--right {
+                    font-size: 24px;
+                    color: #fc9618;
+                }
+            }
+        }
+
+        &__last {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+
+            .content {
+                &--left {
+                    font-size: 24px;
+                    margin-right: 16px;
+                }
+
+                &--right {
+                    display: flex;
+                    flex-direction: column;
+                    align-items: flex-start;
+                }
+            }
+        }
+
+        &__volume {
+            .ant-row {
+                border-top: 1px solid #dae5ec;
+                padding: 8px;
+
+                .ant-col {
+                    text-align: left;
+
+                    &:first-child {
+                        color: #acb8c0;
+                    }
+
+                    &:last-child {
+                        color: #3c454b;
+                        text-align: right;
+                    }
+                }
+            }
+        }
+
+        &__info {
+            border-top: 1px solid #dae5ec;
+            padding: 8px 8px 8px 0;
+
+            .ant-row {
+                margin-top: 8px;
+
+                .ant-col {
+                    text-align: left;
+
+                    &:nth-child(odd) {
+                        color: #acb8c0;
+                        padding-left: 8px;
+                    }
+
+                    &:nth-child(even) {
+                        text-align: right;
+                    }
+                }
+            }
+        }
+    }
+}

+ 155 - 44
src/views/market/spot_trade/spot_trade_reference_market/components/chart/index.vue

@@ -1,83 +1,194 @@
 <template>
-  <!-- 交易图表  -->
-  <div class="chart-container">
-    <div class="chart-content"></div>
-    <div class="chart-tips">
-      <div>
-        <div class="chartRow"
-             v-for="(item, index) in tradedList"
-             :key="index + '11'">
-          <div class="left">{{formatTime(item.TS, 'hm')}}</div>
-          <div class="middle">{{item.PE}}</div>
-          <div class="right">{{item.Vol}}</div>
+    <!-- 交易图表  -->
+    <div class="chart-container">
+        <div class="chart-content">
+            <div class="chart-content__header">
+                <a-radio-group class="chart-content__tabs" v-model:value="activeCycleType" @change="selectCycleType">
+                    <template v-for="item in chartType" :key="item.type">
+                        <a-radio-button :value="item.type">{{ item.name }}</a-radio-button>
+                    </template>
+                </a-radio-group>
+            </div>
+            <echart-time class="chart-content__main" :quote-data="selectedRow" v-if="activeCycleType === -1"></echart-time>
+            <echart-kline class="chart-content__main" :quote-data="selectedRow" :cycle-type="activeCycleType" v-else></echart-kline>
+            <!-- <component :is="componentId" v-if="componentId" :series-data="chartData" :quote-data="selectedRow"></component> -->
+            <div class="chart-content__footer"></div>
+        </div>
+        <div class="chart-slider">
+            <div class="chart-slider__button"></div>
+        </div>
+        <div class="chart-tips">
+            <div>
+                <div class="chart-tips__nav">
+                    <div class="content content--left">{{ selectedRow.goodscode }}</div>
+                    <div class="content content--right">{{ selectedRow.goodsname }}</div>
+                </div>
+                <div class="chart-tips__last">
+                    <div :class="['content content--left', priceColor]">{{ selectedRow.last }}</div>
+                    <div class="content content--right">
+                        <span :class="priceColor">{{ quoteChange(selectedRow) }}</span>
+                        <span :class="priceColor">{{ quoteAmplitude(selectedRow) }}</span>
+                    </div>
+                </div>
+                <div class="chart-tips__volume">
+                    <a-row>
+                        <a-col :span="8">卖一</a-col>
+                        <a-col :span="8">{{ selectedRow.ask }}</a-col>
+                        <a-col :span="8">{{ selectedRow.askvolume }}</a-col>
+                    </a-row>
+                    <a-row>
+                        <a-col :span="8">买一</a-col>
+                        <a-col :span="8">{{ selectedRow.bid }}</a-col>
+                        <a-col :span="8">{{ selectedRow.bidvolume }}</a-col>
+                    </a-row>
+                </div>
+            </div>
+            <!-- <div>
+                <div class="chartRow" v-for="(item, index) in tradedList" :key="index + '11'">
+                    <div class="left">{{ formatTime(item.TS, 'hm') }}</div>
+                    <div class="middle">{{ item.PE }}</div>
+                    <div class="right">{{ item.Vol }}</div>
+                </div>
+            </div> -->
+            <div class="chart-tips__info">
+                <a-row>
+                    <a-col :span="4">最新</a-col>
+                    <a-col :span="8">{{ selectedRow.last }}</a-col>
+                    <a-col :span="4">均价</a-col>
+                    <a-col :span="8">{{ selectedRow.averageprice }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">涨跌</a-col>
+                    <a-col :span="8">{{ quoteChange(selectedRow) }}</a-col>
+                    <a-col :span="4">今开</a-col>
+                    <a-col :span="8">{{ selectedRow.opened }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">涨幅</a-col>
+                    <a-col :span="8">{{ quoteAmplitude(selectedRow) }}</a-col>
+                    <a-col :span="4">最高</a-col>
+                    <a-col :span="8">{{ selectedRow.highest }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">总量</a-col>
+                    <a-col :span="8">{{ selectedRow.totalvolume }}</a-col>
+                    <a-col :span="4">最低</a-col>
+                    <a-col :span="8">{{ selectedRow.lowest }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">金额</a-col>
+                    <a-col :span="8">{{ selectedRow.totalturnover }}</a-col>
+                    <a-col :span="4">量比</a-col>
+                    <a-col :span="8">{{ '--' }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">涨停</a-col>
+                    <a-col :span="8">{{ selectedRow.limitup }}</a-col>
+                    <a-col :span="4">跌停</a-col>
+                    <a-col :span="8">{{ selectedRow.limitdown }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">外盘</a-col>
+                    <a-col :span="8">{{ selectedRow.totalbidvolume }}</a-col>
+                    <a-col :span="4">内盘</a-col>
+                    <a-col :span="8">{{ selectedRow.totalaskvolume }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">持仓</a-col>
+                    <a-col :span="8">{{ selectedRow.holdvolume }}</a-col>
+                    <a-col :span="4">结算</a-col>
+                    <a-col :span="8">{{ selectedRow.settle }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">仓差</a-col>
+                    <a-col :span="8">{{ '--' }}</a-col>
+                    <a-col :span="4">前结</a-col>
+                    <a-col :span="8">{{ '--' }}</a-col>
+                </a-row>
+                <a-row>
+                    <a-col :span="4">日增</a-col>
+                    <a-col :span="8">{{ '--' }}</a-col>
+                    <a-col :span="4">开平</a-col>
+                    <a-col :span="8">{{ '--' }}</a-col>
+                </a-row>
+            </div>
+            <!-- <div @click="watchMore" class="watchMore">查看更多</div> -->
         </div>
-      </div>
-      <div @click="watchMore"
-           class="watchMore">查看更多</div>
     </div>
-  </div>
 </template>
 
 <script lang="ts">
-import { defineAsyncComponent, defineComponent } from '@/common/export/commonTable';
+import { defineComponent } from '@/common/export/commonTable';
 import { _closeModal } from '@/common/setup/modal/modal';
-import { PropType, ref } from 'vue';
-
-import { QueryHistoryTikDatasRsp, QueryQuoteDayRsp } from '@/services/go/quote/interface';
-import { QueryDeliveryRelationRsp } from '@/services/go/delivery/interface';
+import { PropType, ref, computed } from 'vue';
+import { QueryQuoteDayRsp, CycleType } from '@/services/go/quote/interface';
 import { QueryHistoryTikDatas } from '@/services/go/quote';
-import { WrTradeOrderDetailReq } from '@/services/go/wrtrade/interface';
-import { useQueryData } from '@/common/setup/request';
-import { queryWrTradeOrderDetail } from '@/services/go/wrtrade';
 import { formatTime } from '@/common/methods';
 import { ComponentType } from '../setup';
+import { EchartKline, EchartTime } from '@/common/components/echart';
+import { handleQuotePriceColor, quoteChange, quoteAmplitude } from '@/common/setup/table/tableQuote';
 
 export default defineComponent({
     emits: ['cancel', 'update'],
     name: 'stock-exchange',
+    components: {
+        EchartKline,
+        EchartTime,
+    },
     props: {
         selectedRow: {
             type: Object as PropType<QueryQuoteDayRsp>,
             default: {},
         },
     },
-
     setup(props, context) {
-        const loading = ref<boolean>(false);
         const { visible, cancel } = _closeModal(context);
-        const activeKey = ref('1');
+        const activeCycleType = ref<CycleType>(-1);
         function watchMore() {
             context.emit('update', ComponentType.tradeDetail);
         }
         const { goodscode } = props.selectedRow;
 
+        const priceColor = computed(() => {
+            return handleQuotePriceColor(props.selectedRow.last, props.selectedRow.presettle);
+        });
+
+        // 周期类型
+        const chartType = [
+            { name: '分时', type: -1 },
+            { name: '1分钟', type: 1 },
+            { name: '5分钟', type: 2 },
+            { name: '30分钟', type: 3 },
+            { name: '60分钟', type: 4 },
+            { name: '4小时', type: 240 },
+            { name: '日 K', type: 11 },
+        ];
+
+        // 选择图表周期类型
+        const selectCycleType = () => {
+            // console.log(activeCycleType.value);
+        };
+
         // 成交
-        const { list: tradedList } = useQueryData(QueryHistoryTikDatas, { goodsCode: goodscode });
+        // const { list: tradedList } = useQueryData(QueryHistoryTikDatas, { goodsCode: goodscode });
+
         return {
             cancel,
             visible,
-
-            tradedList,
-            loading,
-            activeKey,
+            chartType,
+            tradedList: [],
+            activeCycleType,
             watchMore,
             formatTime,
+            selectCycleType,
+            quoteChange,
+            quoteAmplitude,
+            priceColor,
         };
     },
 });
 </script>
+
 <style lang="less">
-.chart-container {
-    display: flex;
-    width: 100%;
-    height: calc(100% - 41px);
-    .chart-content {
-        flex: 1;
-        height: 100%;
-    }
-    .chart-tips {
-        width: 300px;
-        height: 100%;
-    }
-}
+@import './index.less';
 </style>

+ 60 - 75
src/views/market/spot_trade/spot_trade_reference_market/components/container/index.vue

@@ -1,83 +1,68 @@
 <template>
-  <!-- 买卖大厅 -->
-  <div class="buy-sell-market">
-    <div class="buy-sell-market-title">
-      <a class="backIcon"
-         @click="cancelAction">
-        <LeftOutlined />
-      </a>
-      <div class="titleBtn">
-        <div class="name">{{selectedRow.goodscode}} {{selectedRow.goodsname}}</div>
-        <div class="arrowRightIcon"></div>
-      </div>
-      <div class="inlineBar">
-        <div class="valNums bdf1 ml10">
-          <!-- 最新价 -->
-          <div class="firstNum start "
-               :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)">
-            {{selectedRow.last}}</div>
-          <div class="
-               lastNum
-               start">
-            <!-- 涨跌值 -->
-            <div :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)">
-              {{quoteChange(selectedRow, selectedRow.decimalplace)}}</div>
-            <!-- 涨跌幅 -->
-            <div class="ml20"
-                 :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)">
-              {{quoteAmplituOfVibration(selectedRow, selectedRow.decimalplace)}}</div>
-          </div>
-        </div>
-        <div class="priceBar ml20">
-          <div class="inlineBar start">
-            <div class="greenBar green">
-              <div class="numBlock ml15">
-                <div class="first">卖价</div>
-                <div class="last"
-                     :class="handleQuotePriceColor(selectedRow.ask, selectedRow.presettle)">
-                  {{selectedRow.ask}}</div>
-              </div>
-              <div class="numBlock">
-                <div class="first">卖量</div>
-                <div class="last">{{selectedRow.askvolume}}</div>
-              </div>
-            </div>
-            <div class="greenBar green">
-              <div class="numBlock ml15">
-                <div class="first">卖价</div>
-                <div class="last">{{selectedRow.sellprice}}</div>
-              </div>
-              <div class="numBlock">
-                <div class="first">卖量</div>
-                <div class="last">{{selectedRow.sellqty}}</div>
-              </div>
+    <!-- 买卖大厅 -->
+    <div class="buy-sell-market">
+        <div class="buy-sell-market-title">
+            <a class="backIcon" @click="cancelAction">
+                <LeftOutlined />
+            </a>
+            <div class="titleBtn">
+                <div class="name">{{ selectedRow.goodscode }} {{ selectedRow.goodsname }}</div>
+                <div class="arrowRightIcon"></div>
             </div>
-          </div>
-          <div class="inlineBar start">
-            <div class="redBar red1">
-              <div class="numBlock">
-                <div class="first">买价</div>
-                <div class="last"
-                     :class="handleQuotePriceColor(selectedRow.bid, selectedRow.presettle)">
-                  {{selectedRow.bid}}</div>
-              </div>
-              <div class="numBlock">
-                <div class="first">买量</div>
-                <div class="last">{{selectedRow.bidvolume}}</div>
-              </div>
+            <div class="inlineBar">
+                <div class="valNums bdf1 ml10">
+                    <!-- 最新价 -->
+                    <div class="firstNum start" :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)"> {{ selectedRow.last }}</div>
+                    <div class="lastNum start">
+                        <!-- 涨跌值 -->
+                        <div :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)"> {{ quoteChange(selectedRow, selectedRow.decimalplace) }}</div>
+                        <!-- 涨跌幅 -->
+                        <div class="ml20" :class="handleQuotePriceColor(selectedRow.last, selectedRow.presettle)"> {{ quoteAmplituOfVibration(selectedRow, selectedRow.decimalplace) }}</div>
+                    </div>
+                </div>
+                <div class="priceBar ml20">
+                    <div class="inlineBar start">
+                        <div class="greenBar green">
+                            <div class="numBlock ml15">
+                                <div class="first">卖价</div>
+                                <div class="last" :class="handleQuotePriceColor(selectedRow.ask, selectedRow.presettle)"> {{ selectedRow.ask }}</div>
+                            </div>
+                            <div class="numBlock">
+                                <div class="first">卖量</div>
+                                <div class="last">{{ selectedRow.askvolume }}</div>
+                            </div>
+                        </div>
+                        <div class="greenBar green">
+                            <div class="numBlock ml15">
+                                <div class="first">卖价</div>
+                                <div class="last">{{ selectedRow.sellprice }}</div>
+                            </div>
+                            <div class="numBlock">
+                                <div class="first">卖量</div>
+                                <div class="last">{{ selectedRow.sellqty }}</div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="inlineBar start">
+                        <div class="redBar red1">
+                            <div class="numBlock">
+                                <div class="first">买价</div>
+                                <div class="last" :class="handleQuotePriceColor(selectedRow.bid, selectedRow.presettle)"> {{ selectedRow.bid }}</div>
+                            </div>
+                            <div class="numBlock">
+                                <div class="first">买量</div>
+                                <div class="last">{{ selectedRow.bidvolume }}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
             </div>
-          </div>
         </div>
-      </div>
+        <!-- 交易图表 -->
+        <Chart v-if="showComponentsId === ComponentType.chart" @update="changeComponent" :selectedRow="selectedRow" />
+        <!-- 成交明细 -->
+        <StockExchange :selectedRow="selectedRow" v-if="showComponentsId === ComponentType.tradeDetail" />
     </div>
-    <!-- 交易图表 -->
-    <Chart v-if="showComponentsId === ComponentType.chart"
-           @update="changeComponent"
-           :selectedRow="selectedRow" />
-    <!-- 成交明细 -->
-    <StockExchange :selectedRow="selectedRow"
-                   v-if="showComponentsId === ComponentType.tradeDetail" />
-  </div>
 </template>
 
 <script lang="ts">