li.shaoyi 2 yıl önce
ebeveyn
işleme
fb90b0347b
30 değiştirilmiş dosya ile 1058 ekleme ve 536 silme
  1. 1 1
      public/config/appconfig.json
  2. 1 1
      src/components/base/echarts/index.vue
  3. 5 2
      src/constants/chart.ts
  4. 1 1
      src/hooks/echarts/candlestick/index.ts
  5. 2 2
      src/hooks/echarts/candlestick/options.ts
  6. 3 3
      src/packages/mobile/assets/themes/default/default.less
  7. 0 61
      src/packages/mobile/components/modules/echarts-kline/index.vue
  8. 11 0
      src/packages/mobile/components/modules/quote/chart/index.less
  9. 116 0
      src/packages/mobile/components/modules/quote/chart/index.vue
  10. 5 1
      src/packages/mobile/components/modules/quote/chart/kline/index.less
  11. 114 0
      src/packages/mobile/components/modules/quote/chart/kline/index.vue
  12. 1 1
      src/packages/mobile/components/modules/quote/chart/timeline/index.less
  13. 2 2
      src/packages/mobile/components/modules/quote/chart/timeline/index.vue
  14. 20 0
      src/packages/mobile/components/modules/quote/forex/index.less
  15. 111 0
      src/packages/mobile/components/modules/quote/forex/index.vue
  16. 22 64
      src/packages/mobile/components/modules/quote/index.vue
  17. 1 1
      src/packages/mobile/components/modules/quote/price/index.less
  18. 74 0
      src/packages/mobile/components/modules/quote/price/index.vue
  19. 83 0
      src/packages/mobile/components/modules/quote/tik/index.vue
  20. 5 0
      src/packages/mobile/router/index.ts
  21. 32 89
      src/packages/mobile/views/goods/detail/Index.vue
  22. 0 139
      src/packages/mobile/views/goods/detail/components/delisting/Index.vue
  23. 113 85
      src/packages/mobile/views/goods/detail/components/listing/Index.vue
  24. 0 0
      src/packages/mobile/views/goods/trade/components/delisting/index.less
  25. 148 0
      src/packages/mobile/views/goods/trade/components/delisting/index.vue
  26. 104 0
      src/packages/mobile/views/goods/trade/index.vue
  27. 18 79
      src/packages/mobile/views/market/detail/Index.vue
  28. 2 2
      src/packages/mobile/views/swap/detail/Index.vue
  29. 43 1
      src/stores/modules/futures.ts
  30. 20 1
      src/types/model/market.d.ts

+ 1 - 1
public/config/appconfig.json

@@ -1,5 +1,5 @@
 {
   "version": "1.0.0",
   "versionCode": "100000",
-  "apiUrl": "http://192.168.31.158:18240/cfg?key=dev_104"
+  "apiUrl": "http://192.168.31.204:8080/cfg?key=test_204"
 }

+ 1 - 1
src/components/base/echarts/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div ref="chartElement" class="app-echarts" :style="{ width: width, height: height }"></div>
+  <div ref="chartElement" class="app-echarts" :style="{ width, height }"></div>
 </template>
 
 <script lang="ts" setup>

+ 5 - 2
src/constants/chart.ts

@@ -9,7 +9,10 @@ export enum ChartCycleType {
     Minutes60 = 4, // 60分钟
     Hours2 = 120, // 2小时
     Hours4 = 240, // 4小时
-    Days = 11, // 日线
+    Day = 11, // 日线
+    Week = 12, // 周线
+    Month = 13, // 月线
+    Year = 14, // 年线
 }
 
 /**
@@ -35,7 +38,7 @@ export function getChartCycleTypeList() {
         { label: '60分钟', value: ChartCycleType.Minutes60 },
         { label: '2小时', value: ChartCycleType.Hours2 },
         { label: '4小时', value: ChartCycleType.Hours4 },
-        { label: '日线', value: ChartCycleType.Days },
+        { label: '日线', value: ChartCycleType.Day },
     ]
 }
 

+ 1 - 1
src/hooks/echarts/candlestick/index.ts

@@ -104,7 +104,7 @@ export function useCandlestickChart(goodscode: string) {
             case ChartCycleType.Hours4: {
                 return milliseconds * 4 * 60;
             }
-            case ChartCycleType.Days: {
+            case ChartCycleType.Day: {
                 return milliseconds * 24 * 60;
             }
             default: {

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

@@ -68,12 +68,12 @@ export function useOptions(dataset: EchartsDataset) {
             xAxis: {
                 type: 'category',
                 axisLabel: {
-                    formatter: (val: string) => moment(val).format('YYYY/MM/DD'),
+                    formatter: (val: string) => moment(val).format('YYYY-MM-DD'),
                     margin: 12,
                 },
                 axisPointer: {
                     label: {
-                        formatter: (params) => moment(params.value).format('YYYY/MM/DD HH:mm:ss'),
+                        formatter: (params) => moment(params.value).format('YYYY-MM-DD HH:mm:ss'),
                     }
                 },
                 axisTick: {

+ 3 - 3
src/packages/mobile/assets/themes/default/default.less

@@ -51,12 +51,12 @@
                 align-items: center;
                 color: #626675;
                 cursor: pointer;
-                border-radius: 4px;
+                border-radius: .04rem;
                 background-color: #f0f0f1;
-                padding: 8px 16px;
+                padding: .08rem .16rem;
 
                 &:not(:first-child) {
-                    margin-left: 10px;
+                    margin-left: .1rem;
                 }
 
                 &.is-active {

+ 0 - 61
src/packages/mobile/components/modules/echarts-kline/index.vue

@@ -1,61 +0,0 @@
-<template>
-    <div class="app-echats-kline">
-        <template v-if="loading">
-            <div class="app-echats-kline__tip">正在加载...</div>
-        </template>
-        <template v-else-if="isEmpty">
-            <div class="app-echats-kline__tip">暂无数据</div>
-        </template>
-        <template v-else>
-            <div class="app-echats-kline__container">
-                <ul class="legend">
-                    <li class="legend-item">开: {{ selectedItem.open }}</li>
-                    <li class="legend-item">收: {{ selectedItem.close }}</li>
-                    <li class="legend-item">高: {{ selectedItem.highest }}</li>
-                    <li class="legend-item">低: {{ selectedItem.lowest }}</li>
-                    <li class="legend-item">MA5: {{ selectedItem.ma5 }}</li>
-                    <li class="legend-item">MA10: {{ selectedItem.ma10 }}</li>
-                    <li class="legend-item">MA15: {{ selectedItem.ma15 }}</li>
-                </ul>
-                <app-echarts :option="options.candlestick" v-model:dataIndex="dataIndex" @ready="initOptions" />
-            </div>
-        </template>
-    </div>
-</template>
-
-<script lang="ts" setup>
-import { PropType, watch } from 'vue'
-import { ChartCycleType } from '@/constants/chart'
-import { useCandlestickChart } from '@/hooks/echarts/candlestick'
-import AppEcharts from '@/components/base/echarts/index.vue'
-
-const props = defineProps({
-    goodscode: {
-        type: String,
-        default: '',
-    },
-    // 周期类型
-    cycleType: {
-        type: Number as PropType<ChartCycleType>,
-        default: ChartCycleType.Minutes,
-    },
-    // 是否显示指标
-    showIndicator: {
-        type: Boolean,
-        default: true,
-    },
-})
-
-const { loading, dataIndex, isEmpty, options, selectedItem, initData, initOptions } = useCandlestickChart(props.goodscode);
-
-// 监听周期选择变化
-watch(() => props.cycleType, (val) => {
-    initData(val);
-}, {
-    immediate: true
-})
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>

+ 11 - 0
src/packages/mobile/components/modules/quote/chart/index.less

@@ -0,0 +1,11 @@
+.app-quote-chart {
+    &__header {
+        display: flex;
+        align-items: center;
+        background-color: #f2f2f2;
+
+        .van-tabs {
+            flex: 1;
+        }
+    }
+}

+ 116 - 0
src/packages/mobile/components/modules/quote/chart/index.vue

@@ -0,0 +1,116 @@
+<template>
+    <div class="app-quote-chart">
+        <div class="app-quote-chart__header">
+            <Tabs v-model:active="tabIndex" :before-change="onTabChange">
+                <Tab title="分时" />
+                <Tab>
+                    <template #title>
+                        <Popover :actions="actions" @select="onPopoverSelect">
+                            <template #reference>{{ selectedText || '分钟' }}</template>
+                        </Popover>
+                    </template>
+                </Tab>
+                <Tab title="日线" />
+                <Tab title="周线" />
+                <Tab title="月线" />
+            </Tabs>
+            <Icon name="setting-o" style="padding: 0 .32rem;" v-if="false" />
+        </div>
+        <TimeLine v-bind="{ goodsCode }" @ready="onReady" v-if="tabIndex === 0" />
+        <KLine v-bind="{ goodsCode, cycleType }" v-else />
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef } from 'vue'
+import { Tab, Tabs, Popover, Icon, PopoverAction } from 'vant'
+import { ChartCycleType } from '@/constants/chart'
+import TimeLine from './timeline/index.vue'
+import KLine from './kline/index.vue'
+
+defineProps({
+    goodsCode: {
+        type: String,
+        required: true
+    }
+})
+
+const emit = defineEmits(['ready'])
+const tabIndex = shallowRef(0) // 当前选中的标签
+const selectedText = shallowRef('') // 当前选中的分钟线
+const cycleType = shallowRef(ChartCycleType.Minutes) // 图表周期类型
+
+const actions = [
+    { text: '1分钟' },
+    { text: '5分钟', },
+    { text: '30分钟', },
+    { text: '60分钟', },
+    { text: '2小时', },
+    { text: '4小时', }
+]
+
+// 切换图表
+const onTabChange = (index: number) => {
+    switch (index) {
+        case 1: {
+            return false
+        }
+        case 2: {
+            cycleType.value = ChartCycleType.Day
+            return true
+        }
+        case 3: {
+            cycleType.value = ChartCycleType.Week
+            return true
+        }
+        case 4: {
+            cycleType.value = ChartCycleType.Month
+            return true
+        }
+        default: {
+            return true
+        }
+    }
+}
+
+// 切换分钟
+const onPopoverSelect = (action: PopoverAction) => {
+    tabIndex.value = 1
+    selectedText.value = action.text
+
+    switch (action.text) {
+        case '1分钟': {
+            cycleType.value = ChartCycleType.Minutes
+            break
+        }
+        case '5分钟': {
+            cycleType.value = ChartCycleType.Minutes5
+            break
+        }
+        case '30分钟': {
+            cycleType.value = ChartCycleType.Minutes30
+            break
+        }
+        case '60分钟': {
+            cycleType.value = ChartCycleType.Minutes60
+            break
+        }
+        case '2小时': {
+            cycleType.value = ChartCycleType.Hours2
+            break
+        }
+        case '4小时': {
+            cycleType.value = ChartCycleType.Hours4
+            break
+        }
+    }
+}
+
+const onReady = (startTime: string, endTime: string) => {
+    emit('ready', startTime, endTime)
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 5 - 1
src/packages/mobile/components/modules/echarts-kline/index.less → src/packages/mobile/components/modules/quote/chart/kline/index.less

@@ -1,7 +1,7 @@
 .app-echats-kline {
     display: flex;
     flex-direction: column;
-    height: 250px;
+    height: 8rem;
 
     &__tip {
         margin: auto;
@@ -30,4 +30,8 @@
             }
         }
     }
+
+    .app-tabs {
+        height: 3rem;
+    }
 }

+ 114 - 0
src/packages/mobile/components/modules/quote/chart/kline/index.vue

@@ -0,0 +1,114 @@
+<template>
+    <div class="app-echats-kline">
+        <div class="app-echats-kline__tip" v-if="loading">正在加载...</div>
+        <div class="app-echats-kline__tip" v-else-if="isEmpty">暂无数据</div>
+        <template v-else>
+            <div class="app-echats-kline__container">
+                <ul class="legend">
+                    <li class="legend-item">开: {{ selectedItem.open }}</li>
+                    <li class="legend-item">收: {{ selectedItem.close }}</li>
+                    <li class="legend-item">高: {{ selectedItem.highest }}</li>
+                    <li class="legend-item">低: {{ selectedItem.lowest }}</li>
+                    <li class="legend-item">MA5: {{ selectedItem.ma5 }}</li>
+                    <li class="legend-item">MA10: {{ selectedItem.ma10 }}</li>
+                    <li class="legend-item">MA15: {{ selectedItem.ma15 }}</li>
+                </ul>
+                <app-echarts :option="options.candlestick" v-model:dataIndex="dataIndex" @ready="mainReady" />
+            </div>
+            <app-tabs direction="bottom" :data-list="chartSeriesTypeList" @change="tabChange" v-if="showIndicator">
+                <!-- MACD -->
+                <div class="app-echats-kline__container" v-if="activeSeriesType === ChartSeriesType.MACD">
+                    <ul class="legend">
+                        <li class="legend-item">MACD: {{ selectedItem.macd }}</li>
+                        <li class="legend-item">DIF: {{ selectedItem.dif }}</li>
+                        <li class="legend-item">DEA: {{ selectedItem.dea }}</li>
+                    </ul>
+                    <app-echarts :option="options.macd" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+                </div>
+                <!-- VOL -->
+                <div class="app-echats-kline__container" v-if="activeSeriesType === ChartSeriesType.VOL">
+                    <ul class="legend">
+                        <li class="legend-item">VOL: {{ selectedItem.vol }}</li>
+                    </ul>
+                    <app-echarts :option="options.vol" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+                </div>
+                <!-- KDJ -->
+                <div class="app-echats-kline__container" v-if="activeSeriesType === ChartSeriesType.KDJ">
+                    <ul class="legend">
+                        <li class="legend-item">K: {{ selectedItem.k }}</li>
+                        <li class="legend-item">D: {{ selectedItem.d }}</li>
+                        <li class="legend-item">J: {{ selectedItem.j }}</li>
+                    </ul>
+                    <app-echarts :option="options.kdj" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+                </div>
+                <!-- CCI -->
+                <div class="app-echats-kline__container" v-if="activeSeriesType === ChartSeriesType.CCI">
+                    <ul class="legend">
+                        <li class="legend-item">CCI: {{ selectedItem.cci }}</li>
+                    </ul>
+                    <app-echarts :option="options.cci" v-model:dataIndex="dataIndex" @ready="indicatorReady" />
+                </div>
+            </app-tabs>
+        </template>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { PropType, watch, shallowRef } from 'vue'
+import { echarts } from '@/components/base/echarts/core'
+import { ChartCycleType, ChartSeriesType, getChartSeriesTypeList } from '@/constants/chart'
+import { useCandlestickChart } from '@/hooks/echarts/candlestick'
+import AppEcharts from '@/components/base/echarts/index.vue'
+import AppTabs from '@/components/base/tabs/index.vue'
+
+const props = defineProps({
+    goodsCode: {
+        type: String,
+        default: '',
+    },
+    // 周期类型
+    cycleType: {
+        type: Number as PropType<ChartCycleType>,
+        default: ChartCycleType.Minutes,
+    },
+    // 是否显示指标
+    showIndicator: {
+        type: Boolean,
+        default: true,
+    },
+})
+
+const { loading, dataIndex, isEmpty, options, selectedItem, initData, initOptions } = useCandlestickChart(props.goodsCode);
+const activeSeriesType = shallowRef(ChartSeriesType.MACD); // 当前选中的指标
+const chartGroup = new Map<string, echarts.ECharts>(); // 图表联动实例组
+const chartSeriesTypeList = getChartSeriesTypeList();
+
+// 指标切换
+const tabChange = (index: number) => {
+    activeSeriesType.value = chartSeriesTypeList[index].value;
+    setTimeout(() => {
+        initOptions();
+    }, 0);
+}
+
+const mainReady = (chart: echarts.ECharts) => {
+    chartGroup.set('main', chart);
+    initOptions();
+}
+
+const indicatorReady = (chart: echarts.ECharts) => {
+    chartGroup.set('indicator', chart);
+    echarts.connect([...chartGroup.values()]); // 图表联动
+}
+
+// 监听周期选择变化
+watch(() => props.cycleType, (val) => {
+    initData(val);
+}, {
+    immediate: true
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 1 - 1
src/packages/mobile/components/modules/echarts-timeline/index.less → src/packages/mobile/components/modules/quote/chart/timeline/index.less

@@ -1,7 +1,7 @@
 .app-echats-timeline {
     display: flex;
     flex-direction: column;
-    height: 250px;
+    height: 300px;
 
     &__tip {
         margin: auto;

+ 2 - 2
src/packages/mobile/components/modules/echarts-timeline/index.vue → src/packages/mobile/components/modules/quote/chart/timeline/index.vue

@@ -22,13 +22,13 @@ import { useTimelineChart } from '@/hooks/echarts/timeline'
 import AppEcharts from '@/components/base/echarts/index.vue'
 
 const props = defineProps({
-    goodscode: {
+    goodsCode: {
         type: String,
         default: '',
     },
 })
 
-const { loading, dataIndex, isEmpty, options, selectedItem, initOptions } = useTimelineChart(props.goodscode)
+const { loading, dataIndex, isEmpty, options, selectedItem, initOptions } = useTimelineChart(props.goodsCode)
 </script>
 
 <style lang="less">

+ 20 - 0
src/packages/mobile/components/modules/quote/forex/index.less

@@ -0,0 +1,20 @@
+.app-quote-forex {
+    display: flex;
+
+    dl {
+        flex: 1;
+
+        dt {
+            display: flex;
+            justify-content: space-between;
+        }
+
+        dd {
+            display: flex;
+
+            span {
+                flex: 1;
+            }
+        }
+    }
+}

+ 111 - 0
src/packages/mobile/components/modules/quote/forex/index.vue

@@ -0,0 +1,111 @@
+<template>
+    <div class="app-quote-forex">
+        <dl>
+            <dt>
+                <span>卖挂牌</span>
+                <span @click="navigateTo(BuyOrSell.Sell)" v-if="showMore">更多</span>
+            </dt>
+            <dd @click="onSellClick(1)">
+                <span>卖一</span>
+                <span>{{ handleNumberValue(quote?.ask) }}</span>
+                <span>{{ handleNumberValue(quote?.askvolume) }}</span>
+            </dd>
+            <dd @click="onSellClick(2)">
+                <span>卖二</span>
+                <span>{{ handleNumberValue(quote?.ask2) }}</span>
+                <span>{{ handleNumberValue(quote?.askvolume2) }}</span>
+            </dd>
+            <dd @click="onSellClick(3)">
+                <span>卖三</span>
+                <span>{{ handleNumberValue(quote?.ask3) }}</span>
+                <span>{{ handleNumberValue(quote?.askvolume3) }}</span>
+            </dd>
+            <dd @click="onSellClick(4)">
+                <span>卖四</span>
+                <span>{{ handleNumberValue(quote?.ask4) }}</span>
+                <span>{{ handleNumberValue(quote?.askvolume4) }}</span>
+            </dd>
+            <dd @click="onSellClick(5)">
+                <span>卖五</span>
+                <span>{{ handleNumberValue(quote?.ask5) }}</span>
+                <span>{{ handleNumberValue(quote?.askvolume5) }}</span>
+            </dd>
+        </dl>
+        <dl>
+            <dt>
+                <span>买挂牌</span>
+                <span @click="navigateTo(BuyOrSell.Buy)" v-if="showMore">更多</span>
+            </dt>
+            <dd @click="onBuyClick(1)">
+                <span>买一</span>
+                <span>{{ handleNumberValue(quote?.bid) }}</span>
+                <span>{{ handleNumberValue(quote?.bidvolume) }}</span>
+            </dd>
+            <dd @click="onBuyClick(2)">
+                <span>买二</span>
+                <span>{{ handleNumberValue(quote?.bid2) }}</span>
+                <span>{{ handleNumberValue(quote?.bidvolume2) }}</span>
+            </dd>
+            <dd @click="onBuyClick(3)">
+                <span>买三</span>
+                <span>{{ handleNumberValue(quote?.bid3) }}</span>
+                <span>{{ handleNumberValue(quote?.bidvolume3) }}</span>
+            </dd>
+            <dd @click="onBuyClick(4)">
+                <span>买四</span>
+                <span>{{ handleNumberValue(quote?.bid4) }}</span>
+                <span>{{ handleNumberValue(quote?.bidvolume4) }}</span>
+            </dd>
+            <dd @click="onBuyClick(5)">
+                <span>买五</span>
+                <span>{{ handleNumberValue(quote?.bid5) }}</span>
+                <span>{{ handleNumberValue(quote?.bidvolume5) }}</span>
+            </dd>
+        </dl>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { handleNumberValue } from '@/filters'
+import { useNavigation } from '@/hooks/navigation'
+import { BuyOrSell } from '@/constants/order'
+import { useFuturesStore } from '@/stores'
+
+const props = defineProps({
+    goodsCode: {
+        type: String,
+        required: true
+    },
+    showMore: {
+        type: Boolean,
+        default: true
+    }
+})
+
+const emit = defineEmits(['sellclick', 'buyclick'])
+const { router } = useNavigation()
+const futuresStore = useFuturesStore()
+const quote = futuresStore.getQuoteInfo(props.goodsCode)
+
+const onBuyClick = (index: number) => {
+    emit('buyclick', index)
+}
+
+const onSellClick = (index: number) => {
+    emit('sellclick', index)
+}
+
+const navigateTo = (buyorsell: BuyOrSell) => {
+    router.push({
+        name: 'goods-trade',
+        query: {
+            goodsid: quote.value?.goodsid,
+            buyorsell,
+        }
+    })
+}
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 22 - 64
src/packages/mobile/components/modules/quote/index.vue

@@ -1,80 +1,38 @@
 <template>
     <div class="app-quote">
-        <div class="app-quote__price" v-if="quote">
-            <div class="wrap-left">
-                <div class="price">
-                    <span :class="quote.lastColor">{{ handleNumberValue(quote.last) }}</span>
-                    <span :class="quote.lastColor">{{ handleNumberValue(quote.rise.toFixed(quote.decimalplace)) }}</span>
-                    <span :class="quote.lastColor">{{ parsePercent(quote.change) }}</span>
-                </div>
-                <div class="time">
-                    <span>{{ formatDate(quote.lasttime, 'MM-DD HH:mm:ss') }}</span>
-                </div>
-            </div>
-            <div class="wrap-right">
-                <slot></slot>
-            </div>
-        </div>
-        <div class="app-quote__info" v-if="quote">
-            <ul>
-                <li>
-                    <span>买价</span>
-                    <span :class="quote.bidColor">{{ handleNumberValue(quote.bid) }}</span>
-                </li>
-                <li>
-                    <span>开盘</span>
-                    <span>{{ handleNumberValue(quote.opened) }}</span>
-                </li>
-                <li>
-                    <span>最高</span>
-                    <span :class="quote.highestColor">{{ handleNumberValue(quote.highest) }}</span>
-                </li>
-                <li>
-                    <span>卖价</span>
-                    <span :class="quote.askColor">{{ handleNumberValue(quote.ask) }}</span>
-                </li>
-                <li>
-                    <span>昨结</span>
-                    <span>{{ handleNumberValue(quote.presettle) }}</span>
-                </li>
-                <li>
-                    <span>最低</span>
-                    <span :class="quote.lowestColor">{{ handleNumberValue(quote.lowest) }}</span>
-                </li>
-                <li>
-                    <span>涨停</span>
-                    <span>{{ handleNumberValue(quote.limitup) }}</span>
-                </li>
-                <li>
-                    <span>跌停</span>
-                    <span>{{ handleNumberValue(quote.limitdown) }}</span>
-                </li>
-            </ul>
-        </div>
+        <Price v-bind="{ goodsCode }" />
+        <Chart v-bind="{ goodsCode }" @ready="onReady" />
+        <Forex v-bind="{ goodsCode }" v-if="showForex" />
+        <Tik v-bind="{ goodsCode, startTime, endTime }" />
     </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue'
-import { parsePercent, handleNumberValue, formatDate } from '@/filters'
-import { useFuturesStore } from '@/stores'
+import { shallowRef, onMounted, onUnmounted } from 'vue'
+import { formatDate } from '@/filters'
 import quoteSocket from '@/services/websocket/quote'
+import Price from './price/index.vue'
+import Chart from './chart/index.vue'
+import Forex from './forex/index.vue'
+import Tik from './tik/index.vue'
 
 const props = defineProps({
-    goodscode: {
+    goodsCode: {
         type: String,
         required: true
-    }
+    },
+    showForex: Boolean,
 })
 
-const futuresStore = useFuturesStore()
-const quote = futuresStore.getQuoteInfo(props.goodscode)
-const subscribe = quoteSocket.addSubscribe([props.goodscode])
+const subscribe = quoteSocket.addSubscribe([props.goodsCode])
+const startTime = shallowRef<string>()
+const endTime = shallowRef<string>()
+
+const onReady = (start: string, end: string) => {
+    startTime.value = formatDate(start)
+    endTime.value = formatDate(end)
+}
 
 onMounted(() => subscribe.start())
 onUnmounted(() => subscribe.stop())
-</script>
-
-<style lang="less">
-@import './index.less';
-</style>
+</script>

+ 1 - 1
src/packages/mobile/components/modules/quote/index.less → src/packages/mobile/components/modules/quote/price/index.less

@@ -1,4 +1,4 @@
-.app-quote {
+.app-quote-price {
     color: #666;
     background-color: #fff;
     padding: .2rem 0;

+ 74 - 0
src/packages/mobile/components/modules/quote/price/index.vue

@@ -0,0 +1,74 @@
+<template>
+    <div class="app-quote-price">
+        <div class="app-quote-price__price" v-if="quote">
+            <div class="wrap-left">
+                <div class="price">
+                    <span :class="quote.lastColor">{{ handleNumberValue(quote.last) }}</span>
+                    <span :class="quote.lastColor">{{ handleNumberValue(quote.rise.toFixed(quote.decimalplace)) }}</span>
+                    <span :class="quote.lastColor">{{ parsePercent(quote.change) }}</span>
+                </div>
+                <div class="time">
+                    <span>{{ formatDate(quote.lasttime, 'MM-DD HH:mm:ss') }}</span>
+                </div>
+            </div>
+            <div class="wrap-right">
+                <slot></slot>
+            </div>
+        </div>
+        <div class="app-quote-price__info" v-if="quote">
+            <ul>
+                <li>
+                    <span>买价</span>
+                    <span :class="quote.bidColor">{{ handleNumberValue(quote.bid) }}</span>
+                </li>
+                <li>
+                    <span>开盘</span>
+                    <span>{{ handleNumberValue(quote.opened) }}</span>
+                </li>
+                <li>
+                    <span>最高</span>
+                    <span :class="quote.highestColor">{{ handleNumberValue(quote.highest) }}</span>
+                </li>
+                <li>
+                    <span>卖价</span>
+                    <span :class="quote.askColor">{{ handleNumberValue(quote.ask) }}</span>
+                </li>
+                <li>
+                    <span>昨结</span>
+                    <span>{{ handleNumberValue(quote.presettle) }}</span>
+                </li>
+                <li>
+                    <span>最低</span>
+                    <span :class="quote.lowestColor">{{ handleNumberValue(quote.lowest) }}</span>
+                </li>
+                <li>
+                    <span>涨停</span>
+                    <span>{{ handleNumberValue(quote.limitup) }}</span>
+                </li>
+                <li>
+                    <span>跌停</span>
+                    <span>{{ handleNumberValue(quote.limitdown) }}</span>
+                </li>
+            </ul>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { parsePercent, handleNumberValue, formatDate } from '@/filters'
+import { useFuturesStore } from '@/stores'
+
+const props = defineProps({
+    goodsCode: {
+        type: String,
+        required: true
+    }
+})
+
+const futuresStore = useFuturesStore()
+const quote = futuresStore.getQuoteInfo(props.goodsCode)
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 83 - 0
src/packages/mobile/components/modules/quote/tik/index.vue

@@ -0,0 +1,83 @@
+<template>
+    <AppList :columns="columns" :data-list="dataList">
+        <template #TS="{ value }">
+            {{ formatDate(value, 'HH:mm:ss') }}
+        </template>
+        <template #PE="{ value }">
+            <span>{{ value }}</span>
+        </template>
+    </AppList>
+</template>
+
+<script lang="ts" setup>
+import { ref, watchEffect, watch } from 'vue'
+import { formatDate } from '@/filters'
+import { useRequest } from '@/hooks/request'
+import { queryHistoryTikDatas } from '@/services/api/market'
+import { useFuturesStore } from '@/stores'
+import AppList from '@mobile/components/base/list/index.vue'
+
+const props = defineProps({
+    goodsCode: {
+        type: String,
+        required: true
+    },
+    startTime: String,
+    endTime: String,
+})
+
+const futuresStore = useFuturesStore()
+const quote = futuresStore.getQuoteInfo(props.goodsCode)
+const dataList = ref<Model.HistoryTikDatasRsp[]>([])
+
+const columns: Model.TableColumn[] = [
+    { prop: 'TS', label: '时间' },
+    { prop: 'PE', label: '最新价' },
+    { prop: 'Vol', label: '现量' },
+]
+
+const { run } = useRequest(queryHistoryTikDatas, {
+    manual: true,
+    params: {
+        goodsCode: props.goodsCode,
+        count: 10
+    },
+    onSuccess: (res) => {
+        dataList.value = res.data
+    }
+})
+
+watchEffect(() => {
+    if (props.startTime && props.endTime) {
+        run({
+            startTime: formatDate(props.startTime),
+            endTime: formatDate(props.endTime),
+        })
+    }
+})
+
+watch(() => quote.value?.last, () => {
+    if (quote.value) {
+        const list = dataList.value
+        if (list.length > 9) {
+            // 移除列表最后一条记录
+            list.pop()
+        }
+        // 向列表开头添加新纪录
+        list.unshift({
+            AV: 0,
+            Ask: 0,
+            BV: 0,
+            Bid: 0,
+            HI: 0,
+            HV: 0,
+            PE: quote.value.last,
+            TDR: 0,
+            TK: 0,
+            TS: quote.value.lasttime,
+            TT: 0,
+            Vol: quote.value.lastvolume,
+        })
+    }
+})
+</script>

+ 5 - 0
src/packages/mobile/router/index.ts

@@ -183,6 +183,11 @@ const routes: Array<RouteRecordRaw> = [
         name: 'goods-detail',
         component: () => import('../views/goods/detail/Index.vue'),
       },
+      {
+        path: 'trade',
+        name: 'goods-trade',
+        component: () => import('../views/goods/trade/index.vue'),
+      },
     ],
   },
   {

+ 32 - 89
src/packages/mobile/views/goods/detail/Index.vue

@@ -1,117 +1,60 @@
 <template>
     <app-view class="goods-detail">
         <template #header>
-            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '挂牌大厅'">
-                <template #right>
-                    <Icon name="add" size=".4rem" @click="onListing" />
-                </template>
-                <template #footer v-if="quote">
-                    <app-quote :goodscode="quote.goodscode" style="margin-bottom: .2rem;" />
-                </template>
-            </app-navbar>
+            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '挂牌大厅'" />
         </template>
-        <app-pull-refresh ref="pullRefreshRef" v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
-            :page-count="pageCount" @refresh="onRefresh">
-            <Tabs v-model:active="tabIndex" @click="onTabChange">
-                <Tab title="买大厅" :name="BuyOrSell.Buy" />
-                <Tab title="卖大厅" :name="BuyOrSell.Sell" />
-            </Tabs>
-            <div class="trade-section sell" v-if="dataList.length">
-                <app-list :columns="columns" :data-list="dataList">
-                    <template #username="{ row }">
-                        <span>{{ row.userid }}/{{ row.username }}</span>
-                    </template>
-                    <template #operate="{ row }">
-                        <Button size="small" type="primary" round @click="onDelisting(row)">
-                            {{ tabIndex === BuyOrSell.Buy ? '卖出' : '买入' }}
-                        </Button>
-                    </template>
-                </app-list>
+        <component :is="Price" v-bind="{ goodsCode }" />
+        <component :is="Chart" v-bind="{ goodsCode }" @ready="onReady" />
+        <component :is="Forex" v-bind="{ goodsCode }" />
+        <component :is="Tik" v-bind="{ goodsCode, startTime, endTime }" />
+        <template #footer>
+            <div class="g-form__footer">
+                <Button block round type="primary" @click="onListing(EBuildType.BUILDTYPE_OPEN)">订立</Button>
+                <Button block round type="primary" @click="onListing(EBuildType.BUILDTYPE_CLOSE)">转让</Button>
             </div>
-        </app-pull-refresh>
-        <component ref="componentRef" :is="componentMap.get(componentId)" v-bind="{ selectedRow, goodsid }"
-            @closed="closeComponent" v-if="componentId" />
+            <component ref="componentRef" :is="componentMap.get(componentId)" v-bind="{ goodsCode, buildType }"
+                @closed="closeComponent" v-if="componentId" />
+        </template>
     </app-view>
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, defineAsyncComponent } from 'vue'
-import { Tab, Tabs, Button, showToast, Icon } from 'vant'
-import { useRequest } from '@/hooks/request'
+import { shallowRef, defineAsyncComponent, computed } from 'vue'
+import { Button } from 'vant'
+import { formatDate } from '@/filters'
 import { useNavigation } from '@/hooks/navigation'
 import { useComponent } from '@/hooks/component'
-import { BuyOrSell } from '@/constants/order'
-import { queryWrTradeOrderDetail } from '@/services/api/transfer'
-import { useLoginStore, useFuturesStore } from '@/stores'
-import AppQuote from '@mobile/components/modules/quote/index.vue'
-import AppList from '@mobile/components/base/list/index.vue'
-import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
+import { EBuildType } from '@/constants/client'
+import { useFuturesStore } from '@/stores'
+
+const Price = defineAsyncComponent(() => import('@mobile/components/modules/quote/price/index.vue'))
+const Chart = defineAsyncComponent(() => import('@mobile/components/modules/quote/chart/index.vue'))
+const Forex = defineAsyncComponent(() => import('@mobile/components/modules/quote/forex/index.vue'))
+const Tik = defineAsyncComponent(() => import('@mobile/components/modules/quote/tik/index.vue'))
 
 const componentMap = new Map<string, unknown>([
-    ['delisting', defineAsyncComponent(() => import('./components/delisting/Index.vue'))],
     ['listing', defineAsyncComponent(() => import('./components/listing/Index.vue'))],
 ])
 
 const { getQueryStringToNumber } = useNavigation()
-const loginStore = useLoginStore()
 const futuresStore = useFuturesStore()
 const goodsid = getQueryStringToNumber('goodsid')
 const quote = futuresStore.getQuoteInfo(goodsid)
 
-const tabIndex = shallowRef(BuyOrSell.Buy)
-const selectedRow = shallowRef<Model.WrTradeOrderDetailRsp>()
-const error = shallowRef(false)
-const dataList = shallowRef<Model.WrTradeOrderDetailRsp[]>([])
-
-const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onTabChange())
-
-const { pageIndex, loading, run, pageCount } = useRequest(queryWrTradeOrderDetail, {
-    params: {
-        pagesize: 20,
-        goodsid,
-        buyorsell: tabIndex.value
-    },
-    onSuccess: (res) => {
-        if (pageIndex.value === 1) {
-            dataList.value = []
-        }
-        dataList.value.push(...res.data)
-    },
-    onError: () => {
-        error.value = true
-    }
-})
-
-const onRefresh = () => {
-    run({
-        buyorsell: tabIndex.value,
-    })
-}
-
-const onTabChange = () => {
-    /// 重置为1
-    pageIndex.value = 1
-    onRefresh()
-}
+const goodsCode = computed(() => quote.value?.goodscode ?? '')
+const startTime = shallowRef<string>()
+const endTime = shallowRef<string>()
+const buildType = shallowRef<EBuildType>() // 挂牌类型
 
-const columns: Model.TableColumn[] = [
-    { prop: 'orderprice', label: '价格' },
-    { prop: 'orderqty', label: '数量' },
-    { prop: 'username', label: '挂牌方' },
-    { prop: 'operate', label: '摘牌' },
-]
+const { componentRef, componentId, openComponent, closeComponent } = useComponent()
 
-const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
-    selectedRow.value = row
-    /// 不能与自己成交
-    if (row.userid === loginStore.userId) {
-        showToast('不能与自己成交')
-        return
-    }
-    openComponent('delisting')
+const onReady = (start: string, end: string) => {
+    startTime.value = formatDate(start)
+    endTime.value = formatDate(end)
 }
 
-const onListing = () => {
+const onListing = (type: EBuildType) => {
+    buildType.value = type
     openComponent('listing')
 }
 </script>

+ 0 - 139
src/packages/mobile/views/goods/detail/components/delisting/Index.vue

@@ -1,139 +0,0 @@
-<template>
-    <app-popup :title="'摘牌'" v-model:show="showModal" :refresh="refresh">
-        <Form class="goods-delisting__form" ref="formRef" @submit="onSubmit" v-if="props">
-            <Field label="交易账户">
-                <template #input>
-                    <span>{{ accountStore.accountId }}</span>
-                </template>
-            </Field>
-            <Field label="订单商品" v-if="quote">
-                <template #input>
-                    <span>{{ quote.goodscode }}/{{ quote.goodsname }}</span>
-                </template>
-            </Field>
-            <Field label="挂牌方">
-                <template #input>
-                    <span>{{ selectedRow.username }}</span>
-                </template>
-            </Field>
-            <Field label="挂牌价格">
-                <template #input>
-                    <span>{{ selectedRow.orderprice }}</span>
-                </template>
-            </Field>
-            <Field label="剩余数量">
-                <template #input>
-                    <span>{{ selectedRow.orderqty }}</span>
-                </template>
-            </Field>
-            <Field label="摘牌方向">
-                <template #input>
-                    <span>{{ getBuyOrSellName(selectedRow.buyorsell) }}</span>
-                </template>
-            </Field>
-            <Field label="挂牌类型">
-                <template #input>
-                    <RadioGroup v-model="buildType" direction='horizontal'>
-                        <Radio :name="1">订立</Radio>
-                        <Radio :name="2">转让</Radio>
-                    </RadioGroup>
-                </template>
-            </Field>
-            <Field name="OrderQty" :rules="formRules.OrderQty" label="摘牌数量">
-                <template #input>
-                    <Stepper v-model="formData.OrderQty" input-width="100" theme="round" button-size="22" :min="0" :step="1"
-                        :max="selectedRow.orderqty" :auto-fixed="false" integer />
-                </template>
-            </Field>
-        </Form>
-        <template #footer>
-            <Button type="primary" block round @click="formRef?.submit">
-                {{ selectedRow.buyorsell === BuyOrSell.Buy ? '卖出' : '买入' }}
-            </Button>
-        </template>
-    </app-popup>
-</template>
-
-<script lang="ts" setup>
-import { shallowRef, PropType } from 'vue'
-import { Form, FormInstance, Button, Field, RadioGroup, Radio, FieldRule, Stepper } from 'vant'
-import { BuyOrSell } from '@/constants/order'
-import { useAccountStore, useFuturesStore } from '@/stores'
-import { EBuildType, EDelistingType, EListingSelectType, EOrderOperateType, EPriceMode, EValidType } from '@/constants/client'
-import { useOrder } from '@/business/trade'
-import { fullloading, dialog } from '@/utils/vant'
-import AppPopup from '@mobile/components/base/popup/index.vue'
-import { getBuyOrSellName } from '@/constants/order'
-
-const props = defineProps({
-    selectedRow: {
-        type: Object as PropType<Model.WrTradeOrderDetailRsp>,
-        required: true
-    },
-})
-
-const accountStore = useAccountStore()
-const futuresStore = useFuturesStore()
-const { formData, formSubmit } = useOrder()
-
-/// 挂牌类型
-const buildType = shallowRef(EBuildType.BUILDTYPE_OPEN)
-const quote = futuresStore.getQuoteInfo(props.selectedRow.goodsid)
-const formRef = shallowRef<FormInstance>()
-const showModal = shallowRef(true)
-const refresh = shallowRef(false) // 是否刷新父组件数据
-
-// 表单验证规则
-const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
-    OrderQty: [{
-        message: '请输入摘牌数量',
-        validator: () => {
-            return !!formData.OrderQty
-        }
-    }],
-}
-
-// 提交摘牌
-const onSubmit = () => {
-    const { goodsid, orderid, orderprice, buyorsell } = props.selectedRow
-    const { marketid = 0 } = quote.value ?? {}
-
-    /// 获取对应的市场ID
-    formData.MarketID = marketid
-    formData.PriceMode = EPriceMode.PRICEMODE_LIMIT
-    formData.RelatedID = orderid
-    formData.OrderPrice = orderprice
-    formData.BuyOrSell = buyorsell === BuyOrSell.Buy ? BuyOrSell.Sell : BuyOrSell.Buy
-    formData.GoodsID = goodsid
-    formData.ListingSelectType = EListingSelectType.LISTINGSELECTTYPE_DELISTING
-    formData.DelistingType = EDelistingType.DELISTINGTYPE_SELECTED
-    formData.BuildType = EBuildType.BUILDTYPE_OPEN
-    formData.ValidType = EValidType.VALIDTYPE_DR
-    formData.OperateType = EOrderOperateType.ORDEROPERATETYPE_NORMAL
-    formData.BuildType = buildType.value
-
-    fullloading((hideLoading) => {
-        formSubmit().then(() => {
-            hideLoading()
-            dialog('摘牌提交成功。').then(() => closed(true))
-        }).catch((err) => {
-            hideLoading(err, 'fail')
-        })
-    })
-}
-
-// 关闭弹窗
-const closed = (isRefresh = false) => {
-    refresh.value = isRefresh
-    showModal.value = false
-}
-
-// 暴露组件属性给父组件调用
-defineExpose({
-    closed,
-})
-</script>
-
-<style lang="less">
-@import './Index.less';
-</style>

+ 113 - 85
src/packages/mobile/views/goods/detail/components/listing/Index.vue

@@ -1,87 +1,139 @@
 <template>
-    <app-popup class="goods-listing" :title="'挂牌'" v-model:show="showModal" :refresh="refresh">
-        <Form class="goods-listing__form" ref="formRef" @submit="onSubmit">
-            <Tabs class="van-tabs--list" v-model:active="formData.BuyOrSell" :swipe-threshold="2" @change="onTabChange">
-                <Tab title="挂牌买入" :name="BuyOrSell.Buy" />
-                <Tab title="挂牌卖出" :name="BuyOrSell.Sell" />
-            </Tabs>
-            <Field label="交易账户">
-                <template #input>
-                    <span>{{ accountStore.accountId }}</span>
-                </template>
-            </Field>
-            <Field label="订单商品" v-if="quote">
-                <template #input>
-                    <span>{{ quote.goodscode }}/{{ quote.goodsname }}</span>
-                </template>
-            </Field>
-            <Field label="挂牌类型">
-                <template #input>
-                    <RadioGroup v-model="buildType" direction='horizontal'>
-                        <Radio :name="1">订立</Radio>
-                        <Radio :name="2">转让</Radio>
-                    </RadioGroup>
-                </template>
-            </Field>
-            <Field name="OrderPrice" :rules="formRules.OrderPrice" label="价格">
-                <template #input>
-                    <Stepper v-model="formData.OrderPrice" input-width="100" theme="round" button-size="22"
-                        :auto-fixed="false" :decimal-length="quote?.decimalplace" />
-                </template>
-            </Field>
-            <Field name="OrderQty" :rules="formRules.OrderQty" label="数量">
-                <template #input>
-                    <Stepper v-model="formData.OrderQty" input-width="100" theme="round" button-size="22"
-                        :auto-fixed="false" integer />
-                </template>
-            </Field>
-        </Form>
-        <template #footer>
-            <Button type="primary" block round @click="formRef?.submit">确定</Button>
-        </template>
-    </app-popup>
+    <app-modal direction="right" height="100%" v-model:show="showModal" :refresh="refresh">
+        <app-view class="g-form">
+            <template #header>
+                <app-navbar title="挂牌" @back="closed" />
+            </template>
+            <component :is="Price" v-bind="{ goodsCode }" />
+            <component :is="Forex" v-bind="{ goodsCode, showMore: false }" />
+            <Form ref="formRef" class="g-form__container" @submit="onSubmit">
+                <CellGroup inset>
+                    <Field name="OrderPrice" :rules="formRules.OrderPrice" label="价格">
+                        <template #input>
+                            <Stepper v-model="formData.OrderPrice" input-width="100" theme="round" button-size="22"
+                                :auto-fixed="false" :decimal-length="quote?.decimalplace" :step="priceStep" />
+                        </template>
+                    </Field>
+                    <Field name="OrderQty" :rules="formRules.OrderQty"
+                        :label="`数量(${getGoodsUnitName(quote?.goodunitid)})`">
+                        <template #input>
+                            <div>
+                                <Stepper v-model="orderQty" input-width="100" theme="round" button-size="22"
+                                    :auto-fixed="false" :step="qtyStep" integer />
+                                <RadioGroup v-model="qtyStep" direction="horizontal">
+                                    <Radio v-for="(value, index) in qtyStepList" :key="index" :name="value">{{ value }}
+                                    </Radio>
+                                </RadioGroup>
+                            </div>
+                        </template>
+                    </Field>
+                </CellGroup>
+            </Form>
+            <template #footer>
+                <div class="g-form__footer">
+                    <template v-if="buildType === EBuildType.BUILDTYPE_OPEN">
+                        <Button type="primary" block round @click="onBeforeSubmit(BuyOrSell.Buy)">订立买入</Button>
+                        <Button type="primary" block round @click="onBeforeSubmit(BuyOrSell.Sell)">订立卖出</Button>
+                    </template>
+                    <template v-if="buildType === EBuildType.BUILDTYPE_CLOSE">
+                        <Button type="primary" block round @click="onBeforeSubmit(BuyOrSell.Buy)">转让买入</Button>
+                        <Button type="primary" block round @click="onBeforeSubmit(BuyOrSell.Sell)">转让卖出</Button>
+                    </template>
+                </div>
+            </template>
+        </app-view>
+    </app-modal>
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, onMounted } from 'vue'
-import { Form, FormInstance, Button, FieldRule, Field, Tab, Tabs, Stepper, Radio, RadioGroup } from 'vant'
+import { shallowRef, defineAsyncComponent, onMounted, computed } from 'vue'
+import { Form, FormInstance, Button, CellGroup, FieldRule, Field, Stepper, RadioGroup, Radio } from 'vant'
+import { getGoodsUnitName } from '@/constants/unit'
 import { BuyOrSell } from '@/constants/order'
 import { useOrder } from '@/business/trade'
 import { fullloading, dialog } from '@/utils/vant'
-import { useAccountStore, useFuturesStore } from '@/stores'
+import { useFuturesStore } from '@/stores'
 import { EPriceMode, EListingSelectType, EDelistingType, EBuildType, EValidType, EOrderOperateType } from '@/constants/client'
-import AppPopup from '@mobile/components/base/popup/index.vue'
+import AppModal from '@/components/base/modal/index.vue'
+
+const Price = defineAsyncComponent(() => import('@mobile/components/modules/quote/price/index.vue'))
+const Forex = defineAsyncComponent(() => import('@mobile/components/modules/quote/forex/index.vue'))
 
 const props = defineProps({
-    goodsid: {
-        type: Number,
+    goodsCode: {
+        type: String,
         required: true
     },
+    buildType: {
+        type: Number,
+        required: true
+    }
 })
 
-const accountStore = useAccountStore()
+const { formData, formSubmit } = useOrder()
 const futuresStore = useFuturesStore()
-const quote = futuresStore.getQuoteInfo(props.goodsid)
+const quote = futuresStore.getQuoteInfo(props.goodsCode)
 const formRef = shallowRef<FormInstance>()
 const showModal = shallowRef(true)
 const refresh = shallowRef(true) // 是否刷新父组件数据
-/// 挂牌类型
-const buildType = shallowRef(EBuildType.BUILDTYPE_OPEN)
-const { formData, formSubmit } = useOrder()
+const orderQty = shallowRef(1) // 数量
+const qtyStep = shallowRef(1) // 数量步长
+
+// 价格步长
+const priceStep = computed(() => {
+    const { quoteminunit = 0, decimalplace = 0 } = quote.value ?? {}
+    if (quoteminunit) {
+        return quoteminunit / Math.pow(10, decimalplace * -1)
+    }
+    return 1
+})
+
+// 数量步长列表
+const qtyStepList = computed(() => {
+    const { agreeunit = 0 } = quote.value ?? {}
+    return [
+        agreeunit * 1,
+        agreeunit * 5,
+        agreeunit * 10,
+        agreeunit * 20,
+        agreeunit * 30,
+        agreeunit * 50,
+    ]
+})
+
+// 表单验证规则
+const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
+    OrderPrice: [{
+        message: '请输入价格',
+        validator: () => {
+            return !!formData.OrderPrice
+        }
+    }],
+    OrderQty: [{
+        validator: () => {
+            if (orderQty.value) {
+                const { agreeunit = 0 } = quote.value ?? {}
+                return orderQty.value % agreeunit === 0 ? true : '只能输入合约乘数的整数倍'
+            }
+            return '请输入数量'
+        }
+    }],
+}
 
 // 提交挂牌
 const onSubmit = () => {
-    const { marketid = 0 } = quote.value ?? {}
+    const { marketid = 0, goodsid = 0, agreeunit = 0 } = quote.value ?? {}
     /// 获取对应的市场ID
     formData.MarketID = marketid
     formData.PriceMode = EPriceMode.PRICEMODE_LIMIT
-    formData.GoodsID = props.goodsid
+    formData.GoodsID = goodsid
     formData.ListingSelectType = EListingSelectType.LISTINGSELECTTYPE_DELISTINGTHENLISTING
     formData.DelistingType = EDelistingType.DELISTINGTYPE_PRICE
     formData.BuildType = EBuildType.BUILDTYPE_OPEN
     formData.ValidType = EValidType.VALIDTYPE_DR
     formData.OperateType = EOrderOperateType.ORDEROPERATETYPE_NORMAL
-    formData.BuildType = buildType.value
+    formData.BuildType = props.buildType
+    formData.OrderQty = orderQty.value / agreeunit // 数量÷合约乘数
 
     fullloading((hideLoading) => {
         formSubmit().then(() => {
@@ -93,34 +145,9 @@ const onSubmit = () => {
     })
 }
 
-// 表单验证规则
-const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
-    OrderPrice: [{
-        message: '请输入价格',
-        validator: () => {
-            return !!formData.OrderPrice
-        }
-    }],
-    OrderQty: [{
-        message: '请输入挂牌数量',
-        validator: () => {
-            return !!formData.OrderQty
-        }
-    }],
-}
-
-const onTabChange = () => {
-    const { ask = 0, bid = 0, presettle = 0 } = quote.value ?? {}
-    switch (formData.BuyOrSell) {
-        case BuyOrSell.Buy:
-            formData.OrderPrice = ask || presettle
-            break
-        case BuyOrSell.Sell:
-            formData.OrderPrice = bid || presettle
-            break
-        default:
-            formData.OrderPrice = presettle
-    }
+const onBeforeSubmit = (buyOrSell: BuyOrSell) => {
+    formData.BuyOrSell = buyOrSell
+    formRef.value?.submit()
 }
 
 // 关闭弹窗
@@ -130,8 +157,9 @@ const closed = (isRefresh = true) => {
 }
 
 onMounted(() => {
-    formData.BuyOrSell = BuyOrSell.Buy
-    onTabChange()
+    const { last = 0, agreeunit = 0 } = quote.value ?? {}
+    formData.OrderPrice = last
+    orderQty.value = agreeunit
 })
 
 // 暴露组件属性给父组件调用

+ 0 - 0
src/packages/mobile/views/goods/detail/components/delisting/Index.less → src/packages/mobile/views/goods/trade/components/delisting/index.less


+ 148 - 0
src/packages/mobile/views/goods/trade/components/delisting/index.vue

@@ -0,0 +1,148 @@
+<template>
+    <app-modal direction="right" height="100%" v-model:show="showModal" :refresh="refresh">
+        <app-view class="g-form">
+            <template #header>
+                <app-navbar title="摘牌" @back="closed" />
+            </template>
+            <Form ref="formRef" class="g-form__container" @submit="onSubmit">
+                <CellGroup inset>
+                    <Field label="交易账户">
+                        <template #input>
+                            <span>{{ accountStore.accountId }}</span>
+                        </template>
+                    </Field>
+                    <Field label="订单商品" v-if="quote">
+                        <template #input>
+                            <span>{{ quote.goodscode }}/{{ quote.goodsname }}</span>
+                        </template>
+                    </Field>
+                    <Field label="挂牌方">
+                        <template #input>
+                            <span>{{ selectedRow.username }}</span>
+                        </template>
+                    </Field>
+                    <Field label="挂牌价格">
+                        <template #input>
+                            <span>{{ selectedRow.orderprice }}</span>
+                        </template>
+                    </Field>
+                    <Field label="剩余数量">
+                        <template #input>
+                            <span>{{ selectedRow.orderqty }}</span>
+                        </template>
+                    </Field>
+                    <Field label="摘牌方向">
+                        <template #input>
+                            <span>{{ getBuyOrSellName(selectedRow.buyorsell) }}</span>
+                        </template>
+                    </Field>
+                    <Field label="挂牌类型">
+                        <template #input>
+                            <RadioGroup v-model="buildType" direction='horizontal'>
+                                <Radio :name="1">订立</Radio>
+                                <Radio :name="2">转让</Radio>
+                            </RadioGroup>
+                        </template>
+                    </Field>
+                    <Field name="OrderQty" :rules="formRules.OrderQty" label="摘牌数量">
+                        <template #input>
+                            <Stepper v-model="formData.OrderQty" input-width="100" theme="round" button-size="22" :min="0"
+                                :step="1" :max="selectedRow.orderqty" :auto-fixed="false" integer />
+                        </template>
+                    </Field>
+                </CellGroup>
+            </Form>
+            <template #footer>
+                <div class="g-form__footer">
+                    <Button type="primary" block round @click="formRef?.submit">
+                        {{ selectedRow.buyorsell === BuyOrSell.Buy ? '卖出' : '买入' }}
+                    </Button>
+                </div>
+            </template>
+        </app-view>
+    </app-modal>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, PropType } from 'vue'
+import { Form, FormInstance, Button, CellGroup, Field, RadioGroup, Radio, FieldRule, Stepper } from 'vant'
+import { BuyOrSell } from '@/constants/order'
+import { useAccountStore, useFuturesStore } from '@/stores'
+import { EBuildType, EDelistingType, EListingSelectType, EOrderOperateType, EPriceMode, EValidType } from '@/constants/client'
+import { useOrder } from '@/business/trade'
+import { fullloading, dialog } from '@/utils/vant'
+import AppModal from '@/components/base/modal/index.vue'
+import { getBuyOrSellName } from '@/constants/order'
+
+const props = defineProps({
+    selectedRow: {
+        type: Object as PropType<Model.WrTradeOrderDetailRsp>,
+        required: true
+    },
+})
+
+const accountStore = useAccountStore()
+const futuresStore = useFuturesStore()
+const { formData, formSubmit } = useOrder()
+
+/// 挂牌类型
+const buildType = shallowRef(EBuildType.BUILDTYPE_OPEN)
+const quote = futuresStore.getQuoteInfo(props.selectedRow.goodsid)
+const formRef = shallowRef<FormInstance>()
+const showModal = shallowRef(true)
+const refresh = shallowRef(false) // 是否刷新父组件数据
+
+// 表单验证规则
+const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
+    OrderQty: [{
+        message: '请输入摘牌数量',
+        validator: () => {
+            return !!formData.OrderQty
+        }
+    }],
+}
+
+// 提交摘牌
+const onSubmit = () => {
+    const { goodsid, orderid, orderprice, buyorsell } = props.selectedRow
+    const { marketid = 0 } = quote.value ?? {}
+
+    /// 获取对应的市场ID
+    formData.MarketID = marketid
+    formData.PriceMode = EPriceMode.PRICEMODE_LIMIT
+    formData.RelatedID = orderid
+    formData.OrderPrice = orderprice
+    formData.BuyOrSell = buyorsell === BuyOrSell.Buy ? BuyOrSell.Sell : BuyOrSell.Buy
+    formData.GoodsID = goodsid
+    formData.ListingSelectType = EListingSelectType.LISTINGSELECTTYPE_DELISTING
+    formData.DelistingType = EDelistingType.DELISTINGTYPE_SELECTED
+    formData.BuildType = EBuildType.BUILDTYPE_OPEN
+    formData.ValidType = EValidType.VALIDTYPE_DR
+    formData.OperateType = EOrderOperateType.ORDEROPERATETYPE_NORMAL
+    formData.BuildType = buildType.value
+
+    fullloading((hideLoading) => {
+        formSubmit().then(() => {
+            hideLoading()
+            dialog('摘牌提交成功。').then(() => closed(true))
+        }).catch((err) => {
+            hideLoading(err, 'fail')
+        })
+    })
+}
+
+// 关闭弹窗
+const closed = (isRefresh = false) => {
+    refresh.value = isRefresh
+    showModal.value = false
+}
+
+// 暴露组件属性给父组件调用
+defineExpose({
+    closed,
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 104 - 0
src/packages/mobile/views/goods/trade/index.vue

@@ -0,0 +1,104 @@
+<template>
+    <app-view class="g-form">
+        <template #header>
+            <app-navbar :title="quote ? quote.goodscode + '/' + quote.goodsname : '买卖大厅'" />
+        </template>
+        <app-pull-refresh ref="pullRefreshRef" v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
+            :page-count="pageCount" @refresh="onRefresh">
+            <Tabs v-model:active="tabIndex" @click="onTabChange">
+                <Tab title="买大厅" :name="BuyOrSell.Buy" />
+                <Tab title="卖大厅" :name="BuyOrSell.Sell" />
+            </Tabs>
+            <div class="trade-section sell" v-if="dataList.length">
+                <app-list :columns="columns" :data-list="dataList">
+                    <template #username="{ row }">
+                        <span>{{ row.userid }}/{{ row.username }}</span>
+                    </template>
+                    <template #operate="{ row }">
+                        <Button size="small" type="primary" round @click="onDelisting(row)">
+                            {{ tabIndex === BuyOrSell.Buy ? '卖出' : '买入' }}
+                        </Button>
+                    </template>
+                </app-list>
+            </div>
+        </app-pull-refresh>
+        <component ref="componentRef" :is="componentMap.get(componentId)" v-bind="{ selectedRow }" @closed="closeComponent"
+            v-if="componentId" />
+    </app-view>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, defineAsyncComponent } from 'vue'
+import { showToast, Tabs, Tab, Button } from 'vant'
+import { useNavigation } from '@/hooks/navigation'
+import { useComponent } from '@/hooks/component'
+import { useRequest } from '@/hooks/request'
+import { BuyOrSell } from '@/constants/order'
+import { queryWrTradeOrderDetail } from '@/services/api/transfer'
+import { useLoginStore, useFuturesStore } from '@/stores'
+import AppList from '@mobile/components/base/list/index.vue'
+import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
+
+const componentMap = new Map<string, unknown>([
+    ['delisting', defineAsyncComponent(() => import('./components/delisting/index.vue'))],
+])
+
+const { getQueryStringToNumber } = useNavigation()
+const goodsid = getQueryStringToNumber('goodsid')
+const buyorsell = getQueryStringToNumber('buyorsell')
+const loginStore = useLoginStore()
+const futuresStore = useFuturesStore()
+const quote = futuresStore.getQuoteInfo(goodsid)
+
+const tabIndex = shallowRef(buyorsell)
+const selectedRow = shallowRef<Model.WrTradeOrderDetailRsp>()
+const error = shallowRef(false)
+const dataList = shallowRef<Model.WrTradeOrderDetailRsp[]>([])
+
+const columns: Model.TableColumn[] = [
+    { prop: 'orderprice', label: '价格' },
+    { prop: 'orderqty', label: '数量' },
+    { prop: 'username', label: '挂牌方' },
+    { prop: 'operate', label: '摘牌' },
+]
+
+const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => onTabChange())
+
+const { pageIndex, loading, run, pageCount } = useRequest(queryWrTradeOrderDetail, {
+    params: {
+        pagesize: 20,
+        goodsid,
+        buyorsell: tabIndex.value
+    },
+    onSuccess: (res) => {
+        if (pageIndex.value === 1) {
+            dataList.value = []
+        }
+        dataList.value.push(...res.data)
+    },
+    onError: () => {
+        error.value = true
+    }
+})
+
+const onRefresh = () => {
+    run({
+        buyorsell: tabIndex.value,
+    })
+}
+
+const onTabChange = () => {
+    pageIndex.value = 1
+    onRefresh()
+}
+
+const onDelisting = (row: Model.WrTradeOrderDetailRsp) => {
+    selectedRow.value = row
+    /// 不能与自己成交
+    if (row.userid === loginStore.userId) {
+        showToast('不能与自己成交')
+        return
+    }
+    openComponent('delisting')
+}
+</script>

+ 18 - 79
src/packages/mobile/views/market/detail/Index.vue

@@ -1,101 +1,40 @@
 <template>
     <app-view class="market-detail">
         <template #header>
-            <app-navbar :title="quote?.goodsname ?? '商品详情'" />
+            <app-navbar :title="futuresStore.getGoodsName(goodsCode) ?? '商品详情'" />
         </template>
-        <app-quote :goodscode="goodscode" />
-        <Tabs v-model:active="active">
-            <template v-for="(item, index) in getChartCycleTypeList()" :key="index">
-                <Tab :title="item.label" :name="item.value" />
-            </template>
-        </Tabs>
-        <component :is="componentMap.get('echartsTimeline')" v-bind="{ goodscode }" @ready="onReady"
-            v-if="active === ChartCycleType.Time" />
-        <component :is="componentMap.get('echartsKline')" v-bind="{ goodscode, cycleType: active }" v-else />
-        <AppList :columns="columns" :data-list="dataList" style="border-top: 1px solid #eee;">
-            <template #TS="{ value }">
-                {{ formatDate(value, 'HH:mm:ss') }}
-            </template>
-            <template #PE="{ value }">
-                <span>{{ value }}</span>
-            </template>
-        </AppList>
+        <component :is="Price" v-bind="{ goodsCode }" />
+        <component :is="Chart" v-bind="{ goodsCode }" @ready="onReady" />
+        <component :is="Tik" v-bind="{ goodsCode, startTime, endTime }" />
     </app-view>
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, defineAsyncComponent, watch } from 'vue'
-import { Tab, Tabs } from 'vant'
+import { shallowRef, onMounted, onUnmounted, defineAsyncComponent } from 'vue'
 import { formatDate } from '@/filters'
-import { getChartCycleTypeList, ChartCycleType } from '@/constants/chart'
 import { useNavigation } from '@/hooks/navigation'
-import { useRequest } from '@/hooks/request'
-import { queryHistoryTikDatas } from '@/services/api/market'
 import { useFuturesStore } from '@/stores'
-import AppList from '@mobile/components/base/list/index.vue'
-import AppQuote from '@mobile/components/modules/quote/index.vue'
+import quoteSocket from '@/services/websocket/quote'
 
-const componentMap = new Map<string, unknown>([
-    ['echartsTimeline', defineAsyncComponent(() => import('@mobile/components/modules/echarts-timeline/index.vue'))],
-    ['echartsKline', defineAsyncComponent(() => import('@mobile/components/modules/echarts-kline/index.vue'))]
-])
+const Price = defineAsyncComponent(() => import('@mobile/components/modules/quote/price/index.vue'))
+const Chart = defineAsyncComponent(() => import('@mobile/components/modules/quote/chart/index.vue'))
+const Tik = defineAsyncComponent(() => import('@mobile/components/modules/quote/tik/index.vue'))
 
 const { getQueryString } = useNavigation()
+const goodsCode = getQueryString('goodscode')
 const futuresStore = useFuturesStore()
-const goodscode = getQueryString('goodscode')
-const active = shallowRef(ChartCycleType.Time)
+const subscribe = quoteSocket.addSubscribe([goodsCode])
 
-const quote = futuresStore.getQuoteInfo(goodscode)
-const dataList = ref<Model.HistoryTikDatasRsp[]>([])
+const startTime = shallowRef<string>()
+const endTime = shallowRef<string>()
 
-const { run } = useRequest(queryHistoryTikDatas, {
-    manual: true,
-    params: {
-        goodsCode: goodscode,
-        count: 20
-    },
-    onSuccess: (res) => {
-        dataList.value = res.data
-    }
-})
-
-const columns: Model.TableColumn[] = [
-    { prop: 'TS', label: '时间' },
-    { prop: 'PE', label: '最新价' },
-    { prop: 'Vol', label: '现量' },
-]
-
-const onReady = (startTime: string, endTime: string) => {
-    run({
-        startTime: formatDate(startTime),
-        endTime: formatDate(endTime),
-    })
+const onReady = (start: string, end: string) => {
+    startTime.value = formatDate(start)
+    endTime.value = formatDate(end)
 }
 
-watch(() => quote.value?.last, () => {
-    if (quote.value) {
-        const list = dataList.value
-        if (list.length > 19) {
-            // 移除列表最后一条记录
-            list.pop()
-        }
-        // 向列表开头添加新纪录
-        list.unshift({
-            AV: 0,
-            Ask: 0,
-            BV: 0,
-            Bid: 0,
-            HI: 0,
-            HV: 0,
-            PE: quote.value.last,
-            TDR: 0,
-            TK: 0,
-            TS: quote.value.lasttime,
-            TT: 0,
-            Vol: quote.value.lastvolume,
-        })
-    }
-})
+onMounted(() => subscribe.start())
+onUnmounted(() => subscribe.stop())
 </script>
 
 <style lang="less">

+ 2 - 2
src/packages/mobile/views/swap/detail/Index.vue

@@ -8,7 +8,7 @@
                     </div>
                 </template>
                 <template #footer>
-                    <app-quote :goodscode="item.refgoodscode" style="margin-bottom: .2rem;" />
+                    <app-quote :goodsCode="item.refgoodscode" style="margin-bottom: .2rem;" />
                 </template>
             </app-navbar>
         </template>
@@ -47,7 +47,7 @@ import { queryTjmdTradeOrderDetail } from '@/services/api/swap'
 import { useLoginStore } from '@/stores'
 import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
 import AppList from '@mobile/components/base/list/index.vue'
-import AppQuote from '@mobile/components/modules/quote/index.vue'
+import AppQuote from '@mobile/components/modules/quote/price/index.vue'
 
 const componentMap = new Map<string, unknown>([
     ['delisting', defineAsyncComponent(() => import('./components/delisting/Index.vue'))],

+ 43 - 1
src/stores/modules/futures.ts

@@ -21,15 +21,31 @@ export const useFuturesStore = defineStore(() => {
     // 行情列表
     const quoteList = computed(() => {
         return state.goodsList.reduce((res, cur) => {
-            const { goodsid, goodscode, goodsname, goodsgroupid, marketid, decimalplace } = cur
+            const { agreeunit, goodsid, goodscode, goodsname, goodunitid, goodsgroupid, marketid, decimalplace, quoteminunit } = cur
             const quoteDayInfo = state.quoteDayList.find((e) => e.goodscode.toUpperCase() === goodscode.toUpperCase())
             const {
                 averageprice = 0,
                 last = 0,
                 bid = 0,
+                bid2 = 0,
+                bid3 = 0,
+                bid4 = 0,
+                bid5 = 0,
                 ask = 0,
+                ask2 = 0,
+                ask3 = 0,
+                ask4 = 0,
+                ask5 = 0,
                 bidvolume = 0,
+                bidvolume2 = 0,
+                bidvolume3 = 0,
+                bidvolume4 = 0,
+                bidvolume5 = 0,
                 askvolume = 0,
+                askvolume2 = 0,
+                askvolume3 = 0,
+                askvolume4 = 0,
+                askvolume5 = 0,
                 totalvolume = 0,
                 lastvolume = 0,
                 holdvolume = 0,
@@ -62,12 +78,14 @@ export const useFuturesStore = defineStore(() => {
             }
 
             const item: Model.Futures = {
+                agreeunit,
                 averageprice,
                 marketid,
                 goodsgroupid,
                 goodsid,
                 goodscode,
                 goodsname,
+                goodunitid,
                 decimalplace,
                 last,
                 lasttime,
@@ -75,9 +93,25 @@ export const useFuturesStore = defineStore(() => {
                 change,
                 amplitude,
                 bid,
+                bid2,
+                bid3,
+                bid4,
+                bid5,
                 ask,
+                ask2,
+                ask3,
+                ask4,
+                ask5,
                 bidvolume,
+                bidvolume2,
+                bidvolume3,
+                bidvolume4,
+                bidvolume5,
                 askvolume,
+                askvolume2,
+                askvolume3,
+                askvolume4,
+                askvolume5,
                 totalvolume,
                 lastvolume,
                 holdvolume,
@@ -91,6 +125,7 @@ export const useFuturesStore = defineStore(() => {
                 settle,
                 limitup,
                 limitdown,
+                quoteminunit,
                 bidColor: handleColor(bid),
                 askColor: handleColor(ask),
                 lastColor: handleColor(last),
@@ -147,6 +182,12 @@ export const useFuturesStore = defineStore(() => {
         })
     }
 
+    // 获取商品名称
+    const getGoodsName = (code?: string | number) => {
+        const quote = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code)
+        return quote?.goodsname
+    }
+
     // 获取商品市场ID
     const getGoodsMarket = (code?: string | number) => {
         const quote = state.goodsList.find((e) => e.goodscode === code || e.goodsid === code)
@@ -344,6 +385,7 @@ export const useFuturesStore = defineStore(() => {
         getGoodsList,
         getQuoteInfo,
         getQuotePrice,
+        getGoodsName,
         getGoodsMarket,
         quotePushNotify,
     }

+ 20 - 1
src/types/model/market.d.ts

@@ -66,12 +66,14 @@ declare namespace Model {
 
     /** 期货行情 */
     interface Futures {
+        agreeunit: number;//合约单位
         averageprice: number; // 均价
         marketid: number; // 所属市场ID
         goodsgroupid: number; // 所属商品组ID
-        goodsid: number; // 商品ID
+        goodsid: number; // 商品ID 
         goodscode: string; // 商品代码
         goodsname: string; // 商品名称
+        goodunitid: number;//报价单位ID
         decimalplace: number;//报价小数位
         last: number; // 最新价
         lasttime: string; // 行情时间(只有现价变化行情时间才变化)
@@ -79,9 +81,25 @@ declare namespace Model {
         change: number; // 涨跌幅
         amplitude: number; // 振幅
         bid: number; // 买价
+        bid2: number; // 买2
+        bid3: number; // 买3
+        bid4: number; // 买4
+        bid5: number; // 买5
         ask: number; // 卖价
+        ask2: number; // 卖2
+        ask3: number; // 卖3
+        ask4: number; // 卖4
+        ask5: number; // 卖5
         bidvolume: number; // 买量
+        bidvolume2: number; // 买量2
+        bidvolume3: number; // 买量3
+        bidvolume4: number; // 买量4
+        bidvolume5: number; // 买量5
         askvolume: number; // 卖量
+        askvolume2: number; // 卖量2
+        askvolume3: number; // 卖量3
+        askvolume4: number; // 卖量4
+        askvolume5: number; // 卖量5
         totalvolume: number; // 总量
         lastvolume: number; // 现量
         holdvolume: number; // 持仓量
@@ -101,5 +119,6 @@ declare namespace Model {
         settle: number; // 结算价
         limitdown: number; // 跌停价
         limitup: number; // 涨停价
+        quoteminunit: number;//行情最小变动单位 [整数,报价小数位一起使用]
     }
 }