li.shaoyi 2 lat temu
rodzic
commit
f472cf0b54

+ 1 - 1
app/electron.build.js

@@ -30,7 +30,7 @@ axios.get(appConfig.apiUrl).then((res) => {
 
     // https://www.electron.build/configuration/configuration
     const config = {
-        appId: appConfig.appId ?? 'com.electron.app',
+        appId: (appConfig.appId ?? 'com.electron.app') + (isBeta ? '_beta' : ''),
         productName: appConfig.appName + (isBeta ? '模拟盘' : ''),
         directories: {
             output: outputDir

+ 2 - 2
app/package.json

@@ -1,6 +1,6 @@
 {
-  "name": "trading-beta",
-  "version": "1.0.14",
+  "name": "trading",
+  "version": "1.0.24",
   "main": "main.js",
   "dependencies": {
     "electron-updater": "^6.1.4",

+ 2 - 2
oem/qxst/config/appconfig.json

@@ -1,8 +1,8 @@
 {
   "appId": "com.muchinfo.qxst",
   "appName": "贵州茶交数字化中心",
-  "version": "1.0.23",
-  "versionCode": "100023",
+  "version": "1.0.24",
+  "versionCode": "100024",
   "apiUrl": "http://192.168.31.204:8080/cfg?key=test_204",
   "tradeChannel": "ws",
   "modules": [

+ 2 - 2
src/components/base/echarts/core.ts

@@ -3,7 +3,7 @@
  */
 import * as echarts from 'echarts/core'
 import { CandlestickChart, CandlestickSeriesOption, BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
-import { MarkLineComponent, DatasetComponent, DataZoomComponent, GridComponent, GridComponentOption, TooltipComponent, DataZoomComponentOption } from 'echarts/components'
+import { MarkLineComponent, DatasetComponent, DataZoomComponent, GridComponent, GridComponentOption, TooltipComponent, DataZoomComponentOption, DatasetComponentOption } from 'echarts/components'
 import { CanvasRenderer } from 'echarts/renderers'
 
 echarts.use([
@@ -18,7 +18,7 @@ echarts.use([
     TooltipComponent
 ])
 
-type ECOption = echarts.ComposeOption<CandlestickSeriesOption | BarSeriesOption | LineSeriesOption | GridComponentOption | DataZoomComponentOption>
+type ECOption = echarts.ComposeOption<CandlestickSeriesOption | BarSeriesOption | LineSeriesOption | GridComponentOption | DataZoomComponentOption | DatasetComponentOption>
 
 export {
     echarts,

+ 9 - 2
src/components/base/echarts/index.ts

@@ -48,8 +48,15 @@ export function useEcharts() {
 
             // chart.on('dataZoom', () => {
             //     const option: ECOption = chart.getOption()
-            //     const dataZoom = option.dataZoom
-            //     console.log(dataZoom)
+            //     const dz = Array.isArray(option.dataZoom) ? option.dataZoom[0] : option.dataZoom
+            //     const xa = Array.isArray(option.xAxis) ? option.xAxis[0] : option.xAxis
+
+            //     if (dz && xa) {
+            //         const n = Number(dz.endValue) - Number(dz.startValue)
+            //         xa.axisLabel.interval = Math.trunc(n / 8)
+            //     }
+
+            //     chart.setOption(option)
             // })
 
             // 图表重置大小

+ 246 - 0
src/components/base/echarts@next/@next.ts

@@ -0,0 +1,246 @@
+import { reactive, toRefs, computed } from 'vue'
+import { queryHistoryDatas } from '@/services/api/quote'
+import { ChartCycleType } from '@/constants/chart'
+import { echarts, ECOption } from '@/components/base/echarts/core'
+import { useFuturesStore } from '@/stores'
+import { useDataset } from '@/hooks/echarts/candlestick/dataset'
+import moment from 'moment'
+
+export function useCandlestick(goodsCode: string) {
+    const { dataset, handleData, clearData } = useDataset()
+    const { quoteWatch } = useFuturesStore()
+
+    const state = reactive({
+        loading: false,
+        dataIndex: -1,
+        cycleType: ChartCycleType.Minutes
+    })
+
+    // 当前选中的数据项
+    const selectedItem = computed(() => {
+        if (state.dataIndex > -1 && dataset.candlestick.source.length > state.dataIndex) {
+            const { open, close, highest, lowest, ma5, ma10, ma15 } = dataset.candlestick.source[state.dataIndex]
+            const { macd, dif, dea } = dataset.macd.source[state.dataIndex]
+            const { vol } = dataset.vol.source[state.dataIndex]
+            const { k, d, j } = dataset.kdj.source[state.dataIndex]
+            const { cci } = dataset.cci.source[state.dataIndex]
+            return { open, close, highest, lowest, ma5, ma10, ma15, macd, dif, dea, vol, k, d, j, cci }
+        } else {
+            return { open: '--', close: '--', highest: '--', lowest: '--', ma5: '--', ma10: '--', ma15: '--', macd: '--', dif: '--', dea: '--', vol: '--', k: '--', d: '--', j: '--', cci: '--' }
+        }
+    })
+
+    // 获取历史行情
+    const getHistoryData = async () => {
+        try {
+            clearData()
+            state.dataIndex = -1
+            state.loading = true
+            const res = await queryHistoryDatas({
+                data: {
+                    goodsCode,
+                    cycleType: state.cycleType,
+                    count: 1440,
+                }
+            })
+            // 日期升序排序
+            const data = res.data.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf())
+            state.dataIndex = data.length - 1
+            handleData(data)
+        } finally {
+            state.loading = false
+        }
+    }
+
+    let chartInstance: echarts.ECharts
+    let asyncHistoryData = getHistoryData()
+
+    const createCandlestick = (chart: echarts.ECharts) => {
+        chartInstance = chart
+        asyncHistoryData.then(() => {
+            const { source, dimensions } = dataset.candlestick
+            const opt: ECOption = {
+                grid: [
+                    {
+                        top: '0',
+                        height: "70%"
+                    },
+                    {
+                        top: '70%',
+                        height: "18%"
+                    }
+                ],
+                xAxis: [
+                    {
+                        type: 'category',
+                        axisLabel: {
+                            formatter: (val: string) => {
+                                if (val) {
+                                    switch (state.cycleType) {
+                                        case ChartCycleType.Day: {
+                                            return moment(val).format('YYYY-MM-DD')
+                                        }
+                                        default: {
+                                            return moment(val).format('HH:mm')
+                                        }
+                                    }
+                                }
+                                return ''
+                            }
+                        }
+                    },
+                    {
+                        type: 'category',
+                        gridIndex: 1
+                    }
+                ],
+                yAxis: [
+                    {
+                        type: 'value',
+                        scale: true,
+                    },
+                    {
+                        type: 'value',
+                        scale: true,
+                        gridIndex: 1
+                    }
+                ],
+                dataZoom: {
+                    type: 'inside',
+                    startValue: source.length - 60, // 起始显示K线条数(最新60条)
+                    end: 100,
+                    minValueSpan: 30, // 限制窗口缩放显示最少数据条数
+                    maxValueSpan: 200, // 限制窗口缩放显示最大数据条数
+                    xAxisIndex: [0, 1]
+                },
+                dataset: {
+                    dimensions,
+                    source,
+                },
+                series: [
+                    {
+                        name: 'KLine',
+                        type: 'candlestick'
+                    },
+                    {
+                        name: 'MACD',
+                        type: 'bar',
+                        xAxisIndex: 1,
+                        yAxisIndex: 1,
+                    }
+                ]
+            }
+            chart.setOption(opt)
+        })
+    }
+
+    const initChart = (cycleType: ChartCycleType) => {
+        state.cycleType = cycleType
+        asyncHistoryData = getHistoryData()
+    }
+
+    /**
+     * 格式化日期
+     * @param value 
+     * @returns 
+     */
+    const formatDate = (value: string) => {
+        switch (state.cycleType) {
+            case ChartCycleType.Day: {
+                return moment(value).format('YYYY-MM-DD')
+            }
+            case ChartCycleType.Hours2:
+            case ChartCycleType.Hours4: {
+                return moment(value).format('YYYY-MM-DD HH:00:00')
+            }
+            default: {
+                return moment(value).format('YYYY-MM-DD HH:mm:00')
+            }
+        }
+    }
+
+    quoteWatch(goodsCode, (q) => {
+        if (chartInstance) {
+            asyncHistoryData.then(() => {
+                const { last, lasttime } = q
+                if (last && lasttime) {
+                    const { candlestick, macd, vol, kdj, cci } = dataset
+                    const lastIndex = candlestick.source.length - 1 // 历史数据最后索引位置
+
+                    const oldTime = lastIndex === -1 ? moment(lasttime) : moment(candlestick.source[lastIndex].date) // 历史行情最后时间
+                    const diffTime = moment(lasttime).valueOf() - oldTime.valueOf() // 计算时间差
+
+
+                    // 判断时间差是否大于周期时间
+                    if (lastIndex === -1 || diffTime > 60 * 1000) {
+                        const newtime = formatDate(lasttime)
+                        // 新增K线数据
+                        candlestick.source.push({
+                            date: newtime,
+                            open: last,
+                            close: last,
+                            lowest: last,
+                            highest: last,
+                            ma5: '-',
+                            ma10: '-',
+                            ma15: '-',
+                        })
+
+                        // 新增MACD数据
+                        macd.source.push({
+                            date: newtime,
+                            ema12: 0,
+                            ema26: 0,
+                            dif: 0,
+                            dea: 0,
+                            macd: 0,
+                        })
+
+                        // 新增VOL数据
+                        vol.source.push({
+                            date: newtime,
+                            vol: 0,
+                        })
+
+                        // 新增KDJ数据
+                        kdj.source.push({
+                            date: newtime,
+                            k: '-',
+                            d: '-',
+                            j: '-',
+                        })
+
+                        // 新增CCI数据
+                        cci.source.push({
+                            date: newtime,
+                            cci: '-',
+                        })
+                    } else {
+                        // 更新列表中最后一条记录的数据
+                        const record = candlestick.source[lastIndex]
+                        if (record.lowest > last) {
+                            record.lowest = last // 更新最低价
+                        }
+                        if (record.highest < last) {
+                            record.highest = last // 更新最高价
+                        }
+                        record.close = last // 更新收盘价
+                    }
+
+                    // chartInstance.setOption({
+                    //     dataset: {
+                    //         source: candlestick.source
+                    //     }
+                    // })
+                }
+            })
+        }
+    })
+
+    return {
+        ...toRefs(state),
+        selectedItem,
+        createCandlestick,
+        initChart
+    }
+}

+ 26 - 0
src/components/base/echarts@next/core.ts

@@ -0,0 +1,26 @@
+/**
+ * 按需引入https://echarts.apache.org/zh/tutorial.html#%E5%9C%A8%E6%89%93%E5%8C%85%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8%20ECharts
+ */
+import * as echarts from 'echarts/core'
+import { CandlestickChart, CandlestickSeriesOption, BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
+import { MarkLineComponent, DatasetComponent, DataZoomComponent, GridComponent, GridComponentOption, TooltipComponent, DataZoomComponentOption } from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+echarts.use([
+    LineChart,
+    MarkLineComponent,
+    DataZoomComponent,
+    DatasetComponent,
+    GridComponent,
+    BarChart,
+    CandlestickChart,
+    CanvasRenderer,
+    TooltipComponent
+])
+
+type ECOption = echarts.ComposeOption<CandlestickSeriesOption | BarSeriesOption | LineSeriesOption | GridComponentOption | DataZoomComponentOption>
+
+export {
+    echarts,
+    ECOption,
+}

+ 43 - 0
src/components/base/echarts@next/demo.vue

@@ -0,0 +1,43 @@
+<template>
+    <app-echarts v-model:dataIndex="dataIndex" @ready="onReady" />
+</template>
+
+<script lang="ts" setup>
+import { PropType, watch } from 'vue'
+import { echarts } from '@/components/base/echarts/core'
+import { ChartCycleType } from '@/constants/chart'
+import { useCandlestick } from './@next'
+import AppEcharts from './index.vue'
+
+const props = defineProps({
+    goodsCode: {
+        type: String,
+        default: '',
+    },
+    // 周期类型
+    cycleType: {
+        type: Number as PropType<ChartCycleType>,
+        default: ChartCycleType.Minutes,
+    },
+    // 是否显示指标
+    showIndicator: {
+        type: Boolean,
+        default: true,
+    },
+})
+
+const { initChart, createCandlestick, dataIndex } = useCandlestick('XAUUSD')
+
+const onReady = (chart: echarts.ECharts) => {
+    createCandlestick(chart)
+}
+
+// 监听周期选择变化
+watch(() => props.cycleType, (val) => {
+    initChart(val)
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 36 - 0
src/components/base/echarts@next/index.less

@@ -0,0 +1,36 @@
+.app-echarts {
+    flex: 1;
+
+    .tooltip {
+        &-title {
+            text-align: left;
+        }
+
+        &-item {
+            display: flex;
+            align-items: center;
+
+            span:first-child {
+                display: table-cell;
+                width: 40px;
+                text-align: left;
+                vertical-align: middle;
+            }
+
+            span:last-child {
+                font-weight: bold;
+                text-align: right;
+                margin-left: 20px;
+            }
+
+            i {
+                display: inline-block;
+                font-size: 0;
+                margin-right: 8px;
+                border-radius: 50%;
+                width: 7px;
+                height: 7px;
+            }
+        }
+    }
+}

+ 84 - 0
src/components/base/echarts@next/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div ref="chartRef" class="app-echarts" :style="{ width, height }"></div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, PropType, onMounted, onUnmounted } from 'vue'
+import { timerInterceptor } from '@/utils/timer'
+import { echarts, ECOption } from './core'
+import ResizeObserver from 'resize-observer-polyfill'
+
+const props = defineProps({
+  // 图表宽度
+  width: {
+    type: String,
+    default: '1000px',
+  },
+  // 图表高度
+  height: {
+    type: String,
+    default: '600px',
+  },
+  // 图表配置项
+  option: {
+    type: Object as PropType<ECOption>
+  },
+  dataIndex: {
+    type: Number,
+    default: 0,
+  }
+})
+
+const emit = defineEmits(['ready', 'update:dataIndex'])
+const chartRef = shallowRef()
+
+// 默认配置项
+const defaultOption: ECOption = {
+  backgroundColor: 'transparent',
+  animation: false,
+  grid: {
+    top: 20,
+    bottom: 40,
+  },
+  axisPointer: {
+    show: true,
+    triggerTooltip: false,
+  },
+}
+
+onMounted(() => {
+  const el = chartRef.value
+  const chart = echarts.init(el) // chart 实例
+  chart.setOption(props.option ?? defaultOption)
+  chart.off('mousemove')
+
+  // 监听鼠标滑动
+  chart.getZr().on('mousemove', (params) => {
+    const pointInPixel = [params.offsetX, params.offsetY]
+    if (chart.containPixel('grid', pointInPixel)) {
+      const pointInGrid = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel)
+      const dataIndex = pointInGrid[0]
+      emit('update:dataIndex', dataIndex)
+    }
+  })
+
+  // 重置图表尺寸
+  const resize = timerInterceptor.setDebounce(() => {
+    chart.resize && chart.resize()
+  }, 50)
+
+  // 监听元素变化
+  const resizeObserver = new ResizeObserver(resize)
+  resizeObserver.observe(el)
+
+  emit('ready', chart)
+  onUnmounted(() => {
+    resizeObserver.unobserve(el)
+    chart.dispose()
+  })
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 70 - 0
src/components/base/hqchart/index.vue

@@ -0,0 +1,70 @@
+<template>
+    <div ref="chartRef" class="app-hqchart" :style="{ width, height }"></div>
+</template>
+  
+<script lang="ts" setup>
+import { shallowRef, onMounted, onUnmounted } from 'vue'
+import { Chart } from 'hqchart'
+import { timerInterceptor } from '@/utils/timer'
+import ResizeObserver from 'resize-observer-polyfill'
+
+const props = defineProps({
+    // 图表宽度
+    width: {
+        type: String,
+        default: '100%',
+    },
+    // 图表高度
+    height: {
+        type: String,
+        default: '100%',
+    },
+    // 图表配置项
+    option: {
+        type: Object
+    },
+    dataIndex: {
+        type: Number,
+        default: 0,
+    }
+})
+
+const emit = defineEmits(['ready', 'update:dataIndex'])
+const chartRef = shallowRef()
+
+const resource = Chart.JSChart.GetResource()
+resource.FrameLogo.Text = ''
+
+// https://blog.csdn.net/jones2000/article/details/104122774
+Chart.JSConsole.Chart.Log = () => ({})
+Chart.JSConsole.Complier.Log = () => ({})
+
+// 默认配置项
+const defaultOption = {
+    Symbol: 'default', // 股票代码(必填)
+    IsAutoUpdate: false,
+    IsApiPeriod: true, // 每次切换周期请求接口数据
+}
+
+onMounted(() => {
+    const el = chartRef.value
+    const chartInstance = Chart.JSChart.Init(el)
+    chartInstance.SetOption(props.option ?? defaultOption)
+
+    // 自适应图表大小
+    const resize = timerInterceptor.setDebounce(() => {
+        chartInstance.OnSize()
+    }, 50)
+
+    // 监听元素变化
+    const resizeObserver = new ResizeObserver(resize)
+    resizeObserver.observe(el)
+
+    emit('ready', chartInstance)
+
+    onUnmounted(() => {
+        resizeObserver.unobserve(el)
+        chartInstance.ClearChart()
+    })
+})
+</script>

+ 0 - 2
src/hooks/echarts/candlestick/options.ts

@@ -71,8 +71,6 @@ export function useOptions(dataset: EchartsDataset) {
                 axisLabel: {
                     color: '#777',
                     margin: 12,
-                    showMaxLabel:true,
-                    showMinLabel:true,
                     formatter: (val: string) => {
                         if (val) {
                             switch (options.cycleType) {

+ 69 - 168
src/packages/mobile/components/modules/chart/index.vue

@@ -1,5 +1,5 @@
 <template>
-    <div ref="chartRef" style="width:375px;height: 300px;background-color: #fff;"></div>
+    <HQChart :option="chartOption" width="1000px" height="600px" @ready="onReady" />
     <ul>
         <li @click="changePeriod(4)">1分钟</li>
         <li @click="changePeriod(5)">5分钟</li>
@@ -7,13 +7,12 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, onMounted } from 'vue'
-import { Chart } from 'hqchart'
+import { shallowRef, onMounted, onUnmounted } from 'vue'
 import { ChartCycleType } from '@/constants/chart'
-import { queryHistoryDatas } from '@/services/api/quote'
 import { useFuturesStore } from '@/stores'
-import moment from 'moment'
+import { useKLine, NetworkFilterData, NetworkFilterCallback } from './kline'
 import quoteSocket from '@/services/websocket/quote'
+import HQChart from '@/components/base/hqchart/index.vue'
 
 const props = defineProps({
     goodsCode: {
@@ -22,189 +21,91 @@ const props = defineProps({
     }
 })
 
-const { getGoodsQuote, quoteWatch } = useFuturesStore()
-const quote = getGoodsQuote(props.goodsCode)
-const subscribe = quoteSocket.createSubscribe()
-
-const chartRef = shallowRef()
-const chartCycleType = shallowRef(ChartCycleType.Minutes) // 周期类型
+const { cycleType, networkFilter, updateLastData } = useKLine(props.goodsCode)
+const { quoteWatch } = useFuturesStore()
+const chartSymbol = '600000.sh' // 股票代码(必填)
 const chartInstance = shallowRef() // 图表实例
+const subscribe = quoteSocket.createSubscribe()
 
-subscribe.start(props.goodsCode)
-
-let updateChart: (opt: { symbol: string; data: (string | number)[][]; }) => void = () => ({})
+// https://blog.csdn.net/jones2000/article/details/90272733
+const chartOption = {
+    Symbol: chartSymbol,
+    Type: '历史K线图',
+    IsAutoUpdate: false,
+    IsApiPeriod: true, // 每次切换周期请求接口数据
+    NetworkFilter: (data: NetworkFilterData, callback: NetworkFilterCallback) => networkFilter(data, callback),
+    Windows: [
+        { Index: 'MA', Modify: false, Change: false },
+        { Index: 'VOL', Modify: false, Change: false }
+    ],
+    IsShowCorssCursorInfo: true,
+    Border: {
+        Left: 1,
+        Right: 1, //右边间距
+        Top: 25,
+        Bottom: 25,
+    },
+    KLine: {
+        Right: 1,
+        // [30001-32000) 秒周期
+        // 0=日线 1=周线 2=月线 3=年线 9=季线  [40001-50000) 自定义日线
+        // 4=1分钟 5=5分钟 6=15分钟 7=30分钟 8=60分钟 11=120分钟 12=240分钟 [20001-30000) 自定义分钟
+        Period: 4,
+        PageSize: 50,
+        IsShowTooltip: true,
+    },
+    KLineTitle:
+    {
+        IsShowName: false, // 不显示股票名称
+        IsShowSettingInfo: false // 不显示周期/复权
+    },
+    Frame: [
+        {
+            Custom: [
+                {
+                    Type: 0,
+                    Position: 'right',
+                }
+            ],
+            IsShowRightText: false // 是否显示Y轴右侧刻度
+        },
+        {
+            IsShowRightText: false // 是否显示Y轴右侧刻度
+        }
+    ]
+}
 
 // 切换周期
 const changePeriod = (period: number) => {
     switch (period) {
         case 1:
-            chartCycleType.value = ChartCycleType.Week
+            cycleType.value = ChartCycleType.Week
             break
         case 4:
-            chartCycleType.value = ChartCycleType.Minutes
+            cycleType.value = ChartCycleType.Minutes
             break
         case 5:
-            chartCycleType.value = ChartCycleType.Minutes5
+            cycleType.value = ChartCycleType.Minutes5
             break
     }
-    //chartInstance.value.ChangeSymbol('600000.sh') // 切换股票
+    //chartInstance.value.ChangeSymbol(chartSymbol) // 切换股票
     chartInstance.value.ChangePeriod(period) // 切换周期
 }
 
-
-const historyData = shallowRef<Model.HistoryDatasRsp[]>([])
-
-
-const reqeustHistoryData = () => {
-    // 获取历史行情
-    queryHistoryDatas({
-        data: {
-            cycleType: chartCycleType.value,
-            goodsCode: props.goodsCode,
-            count: 1440,
-        }
-    }).then((res) => {
-        const data = res.data.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf())
-        historyData.value = data
-
-        updateChart({
-            symbol: '600000.sh', // 股票代码(必填)
-            data: data.map((e) => ([
-                moment(e.ts).format('YYYYMMDD'),
-                quote.value?.preclose ?? 0,
-                e.o,
-                e.h,
-                e.l,
-                e.c,
-                e.tv,
-                e.tt,
-                moment(e.ts).format('HHmm'),
-            ]))
-        })
-
-
-        console.log('chartInstance', chartInstance.value)
-    })
+const onReady = (chart: unknown) => {
+    chartInstance.value = chart
 }
 
 quoteWatch(props.goodsCode, (q) => {
-    const { last, lasttime } = q
-    if (last && lasttime) {
-
-        const lastIndex = historyData.value.length - 1 // 历史数据最后索引位置
-
-        const oldTime = lastIndex === -1 ? moment(lasttime) : moment(historyData.value[lastIndex].ts) // 历史行情最后时间
-        const diffTime = moment(lasttime).valueOf() - oldTime.valueOf() // 计算时间差
-
-
-
-
-        // 判断时间差是否大于周期时间
-        if (lastIndex === -1 || diffTime > 60 * 1000) {
-            historyData.value.push({
-                o: last,
-                h: last,
-                l: last,
-                c: last,
-                tv: 0,
-                tt: 0,
-                hv: 0,
-                s: 0,
-                ts: lasttime,
-                f: false
-            })
-        } else {
-            // 更新列表中最后一条记录的数据
-            const record = historyData.value[lastIndex]
-            if (record.l > last) {
-                record.l = last // 更新最低价
-            }
-            if (record.h < last) {
-                record.h = last // 更新最高价
-            }
-            record.c = last // 更新收盘价
-        }
-
-
-
-
-
-        // updateChart({
-        //     symbol: '600000.sh',
-        //     data: historyData.value.map((e) => ([
-        //         moment(e.ts).format('YYYYMMDD'),
-        //         quote.value?.preclose ?? 0,
-        //         e.o,
-        //         e.h,
-        //         e.l,
-        //         e.c,
-        //         e.tv,
-        //         e.tt,
-        //         moment(e.ts).format('HHmm'),
-        //     ]))
-        // })
+    const data = updateLastData(q)
+    if (chartInstance.value && data.length) {
+        chartInstance.value.JSChartContainer.RecvMinuteRealtimeDataV2({
+            symbol: chartSymbol,
+            data
+        })
     }
 })
 
-onMounted(() => {
-    // https://blog.csdn.net/jones2000/article/details/90272733
-    const option = {
-        Symbol: '600000.sh', // 股票代码(必填)
-        Type: '历史K线图',
-        IsAutoUpdate: false,
-        IsApiPeriod: true, // 每次切换周期请求接口数据
-        NetworkFilter: (data: { Name: string; Explain: string; PreventDefault: boolean; }, callback: (opt: { symbol: string; data: (string | number)[][]; }) => void) => {
-            data.PreventDefault = true
-            // https://blog.csdn.net/jones2000/article/details/100557649
-            switch (data.Name) {
-                case 'KLineChartContainer::ReqeustHistoryMinuteData':
-                    updateChart = callback
-                    reqeustHistoryData()
-                    break
-                case 'KLineChartContainer::RequestHistoryData':
-                    updateChart = callback
-                    reqeustHistoryData()
-                    break
-            }
-        },
-        Windows: [
-            { Index: 'MA', Modify: false, Change: false },
-            { Index: 'VOL', Modify: false, Change: false }
-        ],
-        IsShowCorssCursorInfo: true,
-        Border: {
-            Left: 1,
-            Right: 1, //右边间距
-            Top: 25,
-            Bottom: 25,
-        },
-        KLine: {
-            Right: 1,
-            // [30001-32000) 秒周期
-            // 0=日线 1=周线 2=月线 3=年线 9=季线  [40001-50000) 自定义日线
-            // 4=1分钟 5=5分钟 6=15分钟 7=30分钟 8=60分钟 11=120分钟 12=240分钟 [20001-30000) 自定义分钟
-            Period: 4,
-            PageSize: 50,
-            IsShowTooltip: true
-        },
-        Frame: [
-            {
-                IsShowRightText: false // 是否显示Y轴右侧刻度
-            },
-            {
-                IsShowRightText: false // 是否显示Y轴右侧刻度
-            }
-        ]
-    }
-
-    const resource = Chart.JSChart.GetResource()
-    resource.FrameLogo.Text = ''
-
-    // https://blog.csdn.net/jones2000/article/details/104122774
-    Chart.JSConsole.Chart.Log = () => ({})
-    Chart.JSConsole.Complier.Log = () => ({})
-
-    chartInstance.value = Chart.JSChart.Init(chartRef.value)
-    chartInstance.value.SetOption(option)
-})
+onMounted(() => subscribe.start(props.goodsCode))
+onUnmounted(() => subscribe.stop())
 </script>

+ 179 - 0
src/packages/mobile/components/modules/chart/kline.ts

@@ -0,0 +1,179 @@
+import { reactive, toRefs, shallowRef } from 'vue'
+import { ChartCycleType } from '@/constants/chart'
+import { queryHistoryDatas } from '@/services/api/quote'
+import moment from 'moment'
+
+// https://blog.csdn.net/jones2000/article/details/100181279
+export interface NetworkFilterData {
+    Name: string; // 原始的类名::函数名
+    Explain: string;
+    PreventDefault: boolean; // 是否阻止默认网路协议发送, 默认false, 如果设置成true, HQChart就不会再请求数据
+    Request: {
+        Data: {
+            symbol: string;
+            count: number;
+            field: string[];
+            klineDrawType: number;
+            period: number;
+            right: number;
+        }
+    }
+}
+
+// https://blog.csdn.net/jones2000/article/details/100557649
+export interface HistoryMinuteData {
+    name?: string; // 股票名称
+    symbol: string; // 股票代码
+    data: number[][]; // 分钟数据
+}
+
+export type NetworkFilterCallback = (historyMinuteData: HistoryMinuteData) => void
+
+export function useKLine(goodsCode: string) {
+    const historyData = shallowRef<Model.HistoryDatasRsp[]>([])
+
+    const state = reactive({
+        loading: false,
+        cycleType: ChartCycleType.Minutes
+    })
+
+    const networkFilter = (data: NetworkFilterData, callback: NetworkFilterCallback) => {
+        const symbol = data.Request.Data.symbol
+        data.PreventDefault = true
+
+        switch (data.Name) {
+            case 'KLineChartContainer::ReqeustHistoryMinuteData':
+                getHistoryData().then(() => {
+                    callback({
+                        symbol,
+                        data: handleChartData(historyData.value)
+                    })
+                })
+                break
+            case 'KLineChartContainer::RequestHistoryData':
+                break
+        }
+    }
+
+    // 获取历史行情
+    const getHistoryData = async () => {
+        const res = await queryHistoryDatas({
+            data: {
+                cycleType: state.cycleType,
+                goodsCode,
+                count: 1440,
+            }
+        })
+        const data = res.data.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf())
+        historyData.value = data
+    }
+
+    // 处理图表数据
+    const handleChartData = (data: Model.HistoryDatasRsp[]) => {
+        return data.map((e, i) => {
+            const date = Number(moment(e.ts).format('YYYYMMDD'))
+            const time = Number(moment(e.ts).format('HHmm'))
+            const preclose = data[i - 1] // 上一个收盘价
+            const close = preclose ? preclose.c : e.c
+
+            return [
+                date, // 日期
+                close, // 前收盘
+                e.o, // 开盘价
+                e.h, // 最高
+                e.l, // 最低
+                e.c, // 收盘
+                e.tv, // 成交量
+                e.tt, // 成交金额
+                time, // 时间
+                e.hv // 持仓量
+            ]
+        })
+    }
+
+    // 获取周期毫秒数
+    const getCycleMilliseconds = () => {
+        const milliseconds = 60 * 1000; // 一分钟毫秒数
+        switch (state.cycleType) {
+            case ChartCycleType.Minutes5: {
+                return milliseconds * 5;
+            }
+            case ChartCycleType.Minutes30: {
+                return milliseconds * 30;
+            }
+            case ChartCycleType.Minutes60: {
+                return milliseconds * 60;
+            }
+            case ChartCycleType.Hours2: {
+                return milliseconds * 2 * 60;
+            }
+            case ChartCycleType.Hours4: {
+                return milliseconds * 4 * 60;
+            }
+            case ChartCycleType.Day: {
+                return milliseconds * 24 * 60;
+            }
+            case ChartCycleType.Week: {
+                return milliseconds * 24 * 60 * 7;
+            }
+            case ChartCycleType.Month: {
+                return milliseconds * 24 * 60 * 30;
+            }
+            default: {
+                return milliseconds;
+            }
+        }
+    }
+
+    // 更新最新数据
+    const updateLastData = (q: Partial<Model.QuoteDayRsp>) => {
+        const { last, lasttime } = q
+        if (last && lasttime) {
+            const lastIndex = historyData.value.length - 1 // 历史数据最后索引位置
+            const cycleMilliseconds = getCycleMilliseconds()
+
+            const oldTime = lastIndex === -1 ? moment(lasttime) : moment(historyData.value[lastIndex].ts) // 历史行情最后时间
+            const diffTime = moment(lasttime).valueOf() - oldTime.valueOf() // 计算时间差
+
+            // 判断时间差是否大于周期时间
+            if (lastIndex === -1 || diffTime > cycleMilliseconds) {
+                historyData.value.push({
+                    o: last,
+                    h: last,
+                    l: last,
+                    c: last,
+                    tv: q.lastvolume ?? 0,
+                    tt: q.Lastturnover ?? 0,
+                    hv: q.holdvolume ?? 0,
+                    s: q.settle ?? 0,
+                    ts: lasttime,
+                    f: false
+                })
+            } else {
+                // 更新列表中最后一条记录的数据
+                const record = historyData.value[lastIndex]
+                if (record.l > last) {
+                    record.l = last // 更新最低价
+                }
+                if (record.h < last) {
+                    record.h = last // 更新最高价
+                }
+                record.c = last // 更新收盘价
+                record.tv = q.lastvolume ?? record.tv
+                record.tt = q.Lastturnover ?? record.tt
+                record.hv = q.holdvolume ?? record.hv
+                record.s = q.settle ?? record.s
+            }
+
+            const lastValue = historyData.value.slice(-1)
+            return handleChartData(lastValue)
+        }
+        return []
+    }
+
+    return {
+        ...toRefs(state),
+        networkFilter,
+        updateLastData
+    }
+}

+ 5 - 0
src/packages/mobile/components/modules/chart/option.ts

@@ -0,0 +1,5 @@
+export function useOption() {
+    return {
+
+    }
+}

+ 2 - 0
src/packages/pc/components/layouts/page/index.vue

@@ -26,6 +26,7 @@
         </div>
       </div>
     </div>
+    <!-- <HQChart /> -->
     <app-footer class="app-page__footer" :style="footerStyles" @tab-change="onTabChange">
       <div class="iconbar">
         <span @click="minimize">
@@ -75,6 +76,7 @@ import AppHeader from '../header/index.vue'
 import AppFooter from '../footer/index.vue'
 import AppNavbar from '../navbar/index.vue'
 import AppSidebar from '../sidebar/index.vue'
+//import HQChart from '@mobile/components/modules/chart/index.vue'
 
 const globalStore = useGlobalStore()
 const loginStore = useLoginStore()

+ 10 - 10
src/types/model/quote.d.ts

@@ -151,16 +151,16 @@ declare namespace Model {
 
     /** 查询行情历史数据 响应 */
     interface HistoryDatasRsp {
-        o: number;
-        h: number;
-        l: number;
-        c: number;
-        tv: number;
-        tt: number;
-        hv: number;
-        s: number;
-        ts: string;
-        f: boolean;
+        o: number; // 开盘价
+        h: number; // 最高价
+        l: number; // 最低价
+        c: number; // 收盘价
+        tv: number; // 总量(成交量)
+        tt: number; // 总金额
+        hv: number; // 持仓量
+        s: number; // 结算价,日线周期(包括)以上才有
+        ts: string; // 时间
+        f: boolean; // 是否补充数据
     }
 
     /** 分时历史数据 响应 */