li.shaoyi 2 năm trước cách đây
mục cha
commit
c6def78261
28 tập tin đã thay đổi với 744 bổ sung118 xóa
  1. 1 1
      src/business/trade/index.ts
  2. 17 0
      src/constants/order.ts
  3. 1 1
      src/filters/index.ts
  4. 1 1
      src/hooks/echarts/candlestick/index.ts
  5. 1 1
      src/hooks/echarts/smoothedline/index.ts
  6. 1 1
      src/hooks/echarts/timeline/index.ts
  7. 4 3
      src/packages/mobile/assets/themes/global/global.less
  8. 10 3
      src/packages/mobile/components/base/list/index.less
  9. 27 1
      src/packages/mobile/views/market/list/Index.vue
  10. 51 2
      src/packages/mobile/views/order/list/components/presale/detail/Index.vue
  11. 4 4
      src/packages/mobile/views/order/list/components/presale/list/Index.vue
  12. 74 22
      src/packages/mobile/views/presale/detail/Index.vue
  13. 0 0
      src/packages/mobile/views/presale/detail/components/delisting/index.less
  14. 20 6
      src/packages/mobile/views/presale/detail/components/delisting/index.vue
  15. 90 26
      src/packages/mobile/views/presale/detail/index.less
  16. 38 5
      src/packages/mobile/views/presale/list/Index.vue
  17. 16 21
      src/packages/mobile/views/swap/list/Index.vue
  18. 109 2
      src/packages/mobile/views/transfer/detail/Index.vue
  19. 13 0
      src/packages/mobile/views/transfer/detail/components/delisting/index.less
  20. 128 0
      src/packages/mobile/views/transfer/detail/components/delisting/index.vue
  21. 100 0
      src/packages/mobile/views/transfer/detail/index.less
  22. 12 1
      src/packages/mobile/views/transfer/list/Index.vue
  23. 2 0
      src/services/index.ts
  24. 1 1
      src/services/websocket/quote.ts
  25. 1 1
      src/services/websocket/trade.ts
  26. 1 1
      src/stores/modules/enum.ts
  27. 17 12
      src/stores/modules/futures.ts
  28. 4 2
      src/types/model/market.d.ts

+ 1 - 1
src/business/trade/index.ts

@@ -554,7 +554,7 @@ export function useOrder() {
     const loading = shallowRef(false)
     const formData = reactive<Partial<Proto.OrderReq>>({
         ClientSerialNo: v4(),
-        ClientOrderTime: formatDate(new Date().toISOString(), 'YYYY-MM-DD HH:mm:ss'),
+        ClientOrderTime: formatDate(new Date().toISOString()),
         ClientType: ClientType.Web,
         LoginID: loginStore.loginId,
         AccountID: accountStore.accountId,

+ 17 - 0
src/constants/order.ts

@@ -149,4 +149,21 @@ export function getWRTradeOrderStatusName(value: number) {
  */
 export function getAppointmentModelOutList() {
     return getEnumTypeList('appointmentModelOut')
+}
+
+/**
+ * 获取委托状态列表
+ * @returns 
+ */
+export function getOrderStatusList() {
+    return getEnumTypeList('orderstatus')
+}
+
+/**
+ * 获取委托状态名称
+ * @returns 
+ */
+export function getOrderStatusName(value: number) {
+    const enums = getOrderStatusList()
+    return getEnumTypeName(enums, value)
 }

+ 1 - 1
src/filters/index.ts

@@ -115,7 +115,7 @@ export function handleNumberValue(value: number | string, suffix = '') {
  * @param value 
  * @param decimal 保留小数位
  */
-export function parsePercent(value: number, decimal = 2) {
+export function parsePercent(value = 0, decimal = 2) {
     const val = formatDecimal(value * 100, decimal, false)
     return handleNumberValue(val, '%')
 }

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

@@ -16,7 +16,7 @@ export function useCandlestickChart(goodscode: string) {
     const isEmpty = ref(true);
     const dataIndex = ref(-1); // 当前数据索引值
     const cycleType = ref(ChartCycleType.Minutes);
-    const quote = futuresStore.getQuoteDayInfoByCode(goodscode); // 实时行情
+    const quote = futuresStore.getQuoteInfo(goodscode); // 实时行情
 
     // 当前选中的数据项
     const selectedItem = computed(() => {

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

@@ -13,7 +13,7 @@ export function useSmoothedLineChart(goodscode: string) {
     const loading = ref(false)
     const isEmpty = ref(false)
     const dataIndex = ref(-1); // 当前数据索引值
-    const quote = futuresStore.getQuoteDayInfoByCode(goodscode) // 实时行情
+    const quote = futuresStore.getQuoteInfo(goodscode) // 实时行情
 
     // 当前选中的数据项
     const selectedItem = computed(() => {

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

@@ -14,7 +14,7 @@ export function useTimelineChart(goodscode: string) {
     const loading = ref(false);
     const isEmpty = ref(false);
     const dataIndex = ref(-1); // 当前数据索引值
-    const quote = futuresStore.getQuoteDayInfoByCode(goodscode); // 实时行情
+    const quote = futuresStore.getQuoteInfo(goodscode); // 实时行情
 
     // 当前选中的数据项
     const selectedItem = computed(() => {

+ 4 - 3
src/packages/mobile/assets/themes/global/global.less

@@ -114,13 +114,14 @@
     padding-bottom: 0;
 
     &__box {
+        &:not(:first-child) {
+            margin-top: .2rem;
+        }
+
         background-color: #fff;
         border-radius: .16rem;
         padding: .24rem;
-    }
 
-    &:not(:first-child) {
-        margin-top: .2rem;
     }
 
     &__titlebar {

+ 10 - 3
src/packages/mobile/components/base/list/index.less

@@ -1,15 +1,22 @@
 .app-list {
+    min-width: 100%;
     background-color: #fff;
+    overflow-x: auto;
     padding: 0 .32rem;
 
     &__table {
-        min-width: 100%;
+        width: 100%;
         text-align: center;
 
         th {
             font-size: .24rem;
             font-weight: normal;
             color: #999;
+            white-space: nowrap;
+        }
+
+        td {
+            white-space: nowrap;
         }
     }
 
@@ -29,11 +36,11 @@
     }
 
     &__column {
-        padding: .12rem .08rem;
+        padding: .12rem;
     }
 
     &__header &__column {
-        padding: 0 .08rem;
+        padding: 0 .12rem;
     }
 
     &__column:first-child &__cell {

+ 27 - 1
src/packages/mobile/views/market/list/Index.vue

@@ -4,6 +4,7 @@
             <app-navbar title="参考行情" />
         </template>
         <app-list :columns="columns" :data-list="dataList">
+            <!-- 合约 -->
             <template #goodscode="{ value }">
                 <span @click="$router.push({ name: 'market-detail' })">{{ value }}</span>
             </template>
@@ -13,12 +14,30 @@
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, computed } from 'vue'
+import { parsePercent, handleNumberValue } from '@/filters'
 import { useFuturesStore } from '@/stores'
 import quoteSocket from '@/services/websocket/quote'
 import AppList from '@mobile/components/base/list/index.vue'
 
 const futuresStore = useFuturesStore()
-const dataList = computed(() => futuresStore.quoteList.filter((e) => e.marketid === 99201))
+
+const dataList = computed(() => {
+    const list = futuresStore.quoteList.filter((e) => e.marketid === 99201)
+    return list.map((e) => ({
+        goodscode: e.goodscode,
+        ask: handleNumberValue(e.ask),
+        bid: handleNumberValue(e.bid),
+        last: handleNumberValue(e.last),
+        rise: handleNumberValue(e.rise.toFixed(e.decimalplace)),
+        change: parsePercent(e.change),
+        opened: handleNumberValue(e.opened),
+        presettle: handleNumberValue(e.presettle),
+        lowest: handleNumberValue(e.lowest),
+        highest: handleNumberValue(e.highest),
+        amplitude: parsePercent(e.amplitude),
+    }))
+})
+
 const goodsCodes = dataList.value.map((e) => e.goodscode.toUpperCase())
 const subscribe = quoteSocket.addSubscribe(goodsCodes)
 
@@ -27,6 +46,13 @@ const columns: Model.TableColumn[] = [
     { prop: 'ask', label: '卖价' },
     { prop: 'bid', label: '买价' },
     { prop: 'last', label: '最新价' },
+    { prop: 'rise', label: '涨跌' },
+    { prop: 'change', label: '幅度' },
+    { prop: 'opened', label: '今开' },
+    { prop: 'presettle', label: '昨结' },
+    { prop: 'lowest', label: '最低' },
+    { prop: 'highest', label: '最高' },
+    { prop: 'amplitude', label: '振幅' },
 ]
 
 onMounted(() => subscribe.start())

+ 51 - 2
src/packages/mobile/views/order/list/components/presale/detail/Index.vue

@@ -1,7 +1,56 @@
-<!-- 我的持仓-预售持仓-详情 -->
+<!-- 我的订单-预售认购-详情 -->
 <template>
-    详情
+    <app-modal direction="right" height="100%" v-model:show="showModal">
+        <app-view class="g-form">
+            <template #header>
+                <app-navbar title="预售认购单" @back="closed" />
+            </template>
+            <div class="g-form__container">
+                <CellGroup title="预售认购信息">
+                    <Cell title="商品代码/名称" :value="selectedRow.goodsname" />
+                    <Cell title="认购数量" :value="selectedRow.orderqty" />
+                    <Cell title="认购价" :value="selectedRow.orderprice" />
+                    <Cell title="认购金额" :value="(selectedRow.orderprice * selectedRow.orderqty).toFixed(2)" />
+                    <Cell title="定金方式" :value="selectedRow.presaledepositvalue" />
+                    <Cell title="预售定金" :value="selectedRow.freezemargin" />
+                    <Cell title="发售方" :value="selectedRow.sellname" />
+                    <Cell title="起拍价" :value="selectedRow.startprice" />
+                    <Cell title="开始日期" :value="selectedRow.starttime" />
+                    <Cell title="结束日期" :value="selectedRow.endtime" />
+                    <Cell title="委托状态" :value="getOrderStatusName(selectedRow.orderstatus)" />
+                    <Cell title="委托时间" :value="selectedRow.ordertime" />
+                    <Cell title="预售价" :value="handleNumberValue(selectedRow.tradeprice)" />
+                    <Cell title="订单数量" :value="handleNumberValue(selectedRow.tradeqty)" />
+                    <Cell title="委托单号" :value="selectedRow.orderid" />
+                </CellGroup>
+            </div>
+        </app-view>
+    </app-modal>
 </template>
 
 <script lang="ts" setup>
+import { shallowRef, PropType } from 'vue'
+import { CellGroup, Cell } from 'vant'
+import { handleNumberValue } from '@/filters'
+import { getOrderStatusName } from '@/constants/order'
+import AppModal from '@/components/base/modal/index.vue'
+
+defineProps({
+    selectedRow: {
+        type: Object as PropType<Model.MineTradeOrderDetailsRsp>,
+        required: true,
+    }
+})
+
+const showModal = shallowRef(true)
+
+// 关闭弹窗
+const closed = () => {
+    showModal.value = false
+}
+
+// 暴露组件属性给父组件调用
+defineExpose({
+    closed,
+})
 </script>

+ 4 - 4
src/packages/mobile/views/order/list/components/presale/list/Index.vue

@@ -41,21 +41,20 @@
                         </li>
                         <li>
                             <span>预售价</span>
-                            <span>{{ item.tradeprice }}</span>
+                            <span>{{ handleNumberValue(item.tradeprice) }}</span>
                         </li>
                         <li>
                             <span>订单量</span>
-                            <span>{{ item.tradeqty }}</span>
+                            <span>{{ handleNumberValue(item.tradeqty) }}</span>
                         </li>
                         <li>
                             <span>日期</span>
-                            <span>{{ item.clientordertime }}</span>
+                            <span>{{ formatDate(item.clientordertime, 'YYYY-MM-DD') }}</span>
                         </li>
                     </ul>
                 </div>
                 <div class="g-order-list__btnbar">
                     <Button size="small" @click="showComponent('detail', item)" round>详情</Button>
-                    <Button size="small" @click="showComponent('pickup', item)" round>不足定金</Button>
                 </div>
             </div>
         </div>
@@ -67,6 +66,7 @@
 <script lang="ts" setup>
 import { shallowRef, defineAsyncComponent } from 'vue'
 import { Button } from 'vant'
+import { formatDate, handleNumberValue } from '@/filters'
 import { useComponent } from '@/hooks/component'
 import { useRequest } from '@/hooks/request'
 import { queryMineTradeOrderDetails } from '@/services/api/presale'

+ 74 - 22
src/packages/mobile/views/presale/detail/Index.vue

@@ -1,27 +1,67 @@
 <template>
-    <app-view class="presale-details">
+    <app-view class="g-detail">
         <template #header>
-            <app-navbar title="挂牌详情" />
+            <app-navbar title="商品详情" />
         </template>
-        <div class="presale-details__content">
-            <Swipe class="banner" :autoplay="5000" indicator-color="white" lazy-render>
-                <SwipeItem v-for="(url, index) in detailBanners" :key="index">
-                    <img :src="url" />
-                </SwipeItem>
-            </Swipe>
-            <div class="goods">
-                <h4 class="goods-title">{{ detail.goodsname }}</h4>
+        <Swipe class="g-detail__banner" :autoplay="5000" indicator-color="white" lazy-render>
+            <SwipeItem v-for="(url, index) in detailBanners" :key="index">
+                <img :src="url" />
+            </SwipeItem>
+        </Swipe>
+        <div class="g-detail__info">
+            <div class="pricebar">
+                <div class="pricebar-left">
+                    <span class="tag">起拍价</span>
+                    <span class="unit">¥</span>
+                    <span class="price">{{ detail.startprice?.toFixed(2) }}</span>
+                </div>
+                <div class="pricebar-right">
+                    <span>总量:{{ detail.presaleqty }}</span>
+                </div>
             </div>
-            <div class="gallery">
-                <Divider>商品详情</Divider>
-                <template v-for="(url, index) in detailImages" :key="index">
-                    <img :src="url" alt="" />
-                </template>
+            <div class="tag">
+                <Tag type="danger" round>大宗竞拍</Tag>
             </div>
+            <div class="titlebar">
+                <span>{{ detail.sellname }}</span>
+                <span>{{ detail.goodsname }}</span>
+            </div>
+            <div class="timebar">
+
+            </div>
+        </div>
+        <div class="g-detail__cell">
+            <ul>
+                <li>
+                    <span>预售定金</span>
+                    <span>{{ earnest }}</span>
+                </li>
+                <li>
+                    <span>转让比例</span>
+                    <span>{{ parsePercent(detail.transferdepositratio) }}</span>
+                </li>
+            </ul>
+        </div>
+        <div class="g-detail__cell">
+            <ul>
+                <li>
+                    <span>开始时间</span>
+                    <span>{{ detail.starttime }}</span>
+                </li>
+                <li>
+                    <span>结束时间</span>
+                    <span>{{ detail.endtime }}</span>
+                </li>
+            </ul>
+        </div>
+        <div class="g-detail__desc">
+            <template v-for="(url, index) in detailImages" :key="index">
+                <img :src="url" alt="" />
+            </template>
         </div>
         <template #footer>
-            <div class="g-form__footer">
-                <Button type="primary" block round @click="openComponent('buy')">我要出价</Button>
+            <div class="g-detail__footer">
+                <Button type="primary" block round @click="openComponent('delisting')">我要出价</Button>
             </div>
             <component ref="componentRef" :is="componentMap.get(componentId)" v-bind="{ detail }" @closed="closeComponent"
                 v-if="componentId" />
@@ -31,19 +71,19 @@
 
 <script lang="ts" setup>
 import { computed, defineAsyncComponent } from 'vue'
-import { Swipe, SwipeItem, Divider, Button } from 'vant'
-import { getFileUrl } from '@/filters'
+import { Swipe, SwipeItem, Button, Tag } from 'vant'
+import { getFileUrl, parsePercent } from '@/filters'
 import { useComponent } from '@/hooks/component'
 import { useNavigation } from '@/hooks/navigation'
 
 const componentMap = new Map<string, unknown>([
-    ['buy', defineAsyncComponent(() => import('./components/buy/index.vue'))], // 挂
+    ['delisting', defineAsyncComponent(() => import('./components/delisting/index.vue'))], // 摘
 ])
 
 const { getParamString } = useNavigation()
 const { componentRef, componentId, openComponent, closeComponent } = useComponent()
-const aa = getParamString('item')
-const detail: Model.PresaleAuctionsRsp = JSON.parse(aa.toString() || '{}')
+const qs = getParamString('item')
+const detail: Model.PresaleAuctionsRsp = JSON.parse(qs.toString() || '{}')
 
 // 商品banner
 const detailBanners = computed(() => {
@@ -51,6 +91,18 @@ const detailBanners = computed(() => {
     return bannerpicurl?.split(',').map((path) => getFileUrl(path))
 })
 
+// 预售定金
+const earnest = computed(() => {
+    switch (detail.presaledepositalgorithm) {
+        case 1:
+            return (detail.presaledepositvalue * 100).toFixed(2)
+        case 2:
+            return detail.presaledepositvalue.toFixed(2)
+        default:
+            return '0.0'
+    }
+})
+
 // 商品图片列表
 const detailImages = computed(() => {
     const pictureurls = detail.pictureurls ?? ''

+ 0 - 0
src/packages/mobile/views/presale/detail/components/buy/index.less → src/packages/mobile/views/presale/detail/components/delisting/index.less


+ 20 - 6
src/packages/mobile/views/presale/detail/components/buy/index.vue → src/packages/mobile/views/presale/detail/components/delisting/index.vue

@@ -8,18 +8,19 @@
             </Field>
             <Field name="OrderPrice" :rules="formRules.OrderPrice" label="认购价">
                 <template #input>
-                    <Stepper v-model="formData.OrderPrice" theme="round" :decimal-length="2" :auto-fixed="false"
-                        button-size="22" />
+                    <Stepper v-model="formData.OrderPrice" theme="round" :min="detail.startprice" :decimal-length="2"
+                        :auto-fixed="false" button-size="22" />
                 </template>
             </Field>
             <Field name="OrderQty" :rules="formRules.OrderQty" label="认购数量">
                 <template #input>
-                    <Stepper v-model="formData.OrderQty" theme="round" button-size="22" :auto-fixed="false" integer />
+                    <Stepper v-model="formData.OrderQty" theme="round" :max="detail.presaleqty" button-size="22"
+                        :auto-fixed="false" integer />
                 </template>
             </Field>
             <Field label="预售定金">
                 <template #input>
-                    <span>{{ detail.presaledepositvalue }}</span>
+                    <span>{{ earnest }}</span>
                 </template>
             </Field>
             <Field label="可用资金">
@@ -35,14 +36,14 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef, PropType, onMounted } from 'vue'
+import { shallowRef, PropType, onMounted, computed } from 'vue'
 import { Form, Field, Stepper, Button, FieldRule, FormInstance } from 'vant'
 import { fullloading, dialog } from '@/utils/vant'
 import { useAccountStore } from '@/stores'
 import { useOrder } from '@/business/trade'
 import AppPopup from '@mobile/components/base/popup/index.vue'
 import { BuyOrSell } from '@/constants/order'
-import { EPriceMode } from '@/constants/client'
+import { EPriceMode, EValidType } from '@/constants/client'
 //import AppSelect from '@mobile/components/base/select/index.vue'
 
 const props = defineProps({
@@ -74,6 +75,18 @@ const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
     }],
 }
 
+// 预售定金
+const earnest = computed(() => {
+    switch (props.detail.presaledepositalgorithm) {
+        case 1:
+            return (props.detail.presaledepositvalue * 100).toFixed(2)
+        case 2:
+            return props.detail.presaledepositvalue.toFixed(2)
+        default:
+            return '0.0'
+    }
+})
+
 // 关闭弹窗
 const closed = (isRefresh = false) => {
     refresh.value = isRefresh
@@ -87,6 +100,7 @@ const onSubmit = () => {
     formData.MarketID = marketid
     formData.BuyOrSell = BuyOrSell.Buy
     formData.RelatedID = applyid
+    formData.ValidType = EValidType.VALIDTYPE_YZ
     formData.PriceMode = EPriceMode.PRICEMODE_LIMIT
 
     fullloading((hideLoading) => {

+ 90 - 26
src/packages/mobile/views/presale/detail/index.less

@@ -1,36 +1,100 @@
-.spot-list {
-    .waterfall {
-        display: flex;
-        flex-wrap: wrap;
-        padding: .12rem;
-
-        &-item {
-            width: 50%;
-            padding: .12rem;
-
-            .goods {
-                background-color: #fff;
-                border-radius: .12rem;
-                overflow: hidden;
-
-                &-image {
-                    font-size: 0;
+.g-detail {
+    &__banner {
+        background-color: #ddd;
+        min-height: 3rem;
+
+        .van-swipe-item {
+            height: 4rem;
+            font-size: 0;
+
+            img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+            }
+        }
+    }
+
+    &__info {
+        background-color: #fff;
+        border-bottom-left-radius: .2rem;
+        border-bottom-right-radius: .2rem;
+        padding: .32rem;
+
+        .pricebar {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+
+            &-left {
+                color: red;
+
+                .tag {
+                    font-size: .24rem;
+                }
+
+                .price {
+                    font-size: .44rem;
                 }
+            }
+
+            &-right {
+                font-size: .24rem;
+                color: #999;
+            }
+        }
 
-                &-info {
-                    padding: .2rem;
+        .tag {
+            margin-top: .24rem;
+        }
+
+        .titlebar {
+            font-size: .32rem;
+            font-weight: bold;
+            margin-top: .2rem;
+
+            span {
+                margin-right: .1rem;
+            }
+        }
+
+        .timebar {
+            display: flex;
+            justify-content: space-around;
+        }
+    }
 
-                    &__price {
-                        .unit {
-                            &::before {
-                                content: '¥';
-                            }
+    &__cell {
+        background-color: #fff;
+        border-radius: .2rem;
+        margin-top: .24rem;
+        padding: .2rem .32rem;
 
-                            color: red;
-                        }
+        ul {
+            li {
+                display: flex;
+                padding: .12rem;
+
+                span {
+                    &:first-child {
+                        width: 1.6rem;
+                        color: #999;
+                    }
+
+                    &:last-child {
+                        flex: 1;
                     }
                 }
             }
         }
     }
+
+    &__desc {
+        margin-top: .24rem;
+    }
+
+    &__footer {
+        background-color: #fff;
+        padding: .12rem .24rem;
+    }
 }

+ 38 - 5
src/packages/mobile/views/presale/list/Index.vue

@@ -4,14 +4,15 @@
             <app-navbar title="预售竞拍" :show-back-button="false" />
         </template>
         <Swipe :autoplay="5000" indicator-color="white" lazy-render>
-            <SwipeItem>
-                <img src="http://192.168.31.201:5015/mtp2-onlineopen/uploadFile/20230420/202304201649184005.jpg" />
+            <SwipeItem v-for="(item, index) in bannerList" :key="index">
+                <img :src="getFileUrl(item.attachmenturl)" />
             </SwipeItem>
         </Swipe>
         <div>正在抢购</div>
         <div class="waterfall">
-            <div class="waterfall-item" v-for="(item, index) in dataList" :key="index">
-                <div class="goods" @click="$router.push({ name: 'presale-detail', params: { item: JSON.stringify(item) } })">
+            <div class="waterfall-item" v-for="(item, index) in startList" :key="index">
+                <div class="goods"
+                    @click="$router.push({ name: 'presale-detail', params: { item: JSON.stringify(item) } })">
                     <div class="goods-image">
                         <img :src="getFileUrl(item.attachmenturl)" />
                     </div>
@@ -26,6 +27,23 @@
             </div>
         </div>
         <div>发售历史</div>
+        <div class="waterfall">
+            <div class="waterfall-item" v-for="(item, index) in endList" :key="index">
+                <div class="goods"
+                    @click="$router.push({ name: 'presale-detail', params: { item: JSON.stringify(item) } })">
+                    <div class="goods-image">
+                        <img :src="getFileUrl(item.attachmenturl)" />
+                    </div>
+                    <div class="goods-info">
+                        <div class="goods-info__title">{{ item.goodsname }}</div>
+                        <div class="goods-info__price">
+                            <Tag type="danger">预售价</Tag>
+                            <span class="unit">{{ item.startprice }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
     </app-view>
 </template>
 
@@ -35,9 +53,24 @@ import { getFileUrl } from '@/filters'
 import { useRequest } from '@/hooks/request'
 import { queryPresaleAuctions } from '@/services/api/presale'
 
-const { dataList } = useRequest(queryPresaleAuctions, {
+const { dataList: bannerList } = useRequest(queryPresaleAuctions, {
+    params: {
+        presalemode: 4,
+        presalestatus: 1
+    },
+})
+
+const { dataList: startList } = useRequest(queryPresaleAuctions, {
+    params: {
+        presalemode: 4,
+        presalestatus: 2
+    },
+})
+
+const { dataList: endList } = useRequest(queryPresaleAuctions, {
     params: {
         presalemode: 4,
+        presalestatus: 3
     },
 })
 </script>

+ 16 - 21
src/packages/mobile/views/swap/list/Index.vue

@@ -14,6 +14,7 @@
 <script lang="ts" setup>
 import { computed } from 'vue'
 import { v4 } from 'uuid'
+import { parsePercent } from '@/filters'
 import { useRequest } from '@/hooks/request'
 import { queryQuoteGoodsList } from '@/services/api/swap'
 import { useFuturesStore, useUserStore } from '@/stores'
@@ -37,34 +38,28 @@ const { dataList } = useRequest(queryQuoteGoodsList, {
 })
 
 const tableList = computed(() => {
-    return dataList.value.map(({ goodsid, goodsname, refgoodscode, decimalplace }) => {
-        const quote = futuresStore.getQuoteDayInfoByCode(refgoodscode)
-        const { last = 0, opened = 0, presettle = 0 } = quote.value ?? {}
-        const { change = 0, amplitude = 0 } = futuresStore.quoteList.find(obj => obj.goodscode === refgoodscode) ?? {}
-        const applies = (amplitude*100.0).toFixed(2)
-        const riseValue = change.toFixed(decimalplace)
-        const lastPrice = last.toFixed(decimalplace)
-        const openedPrice = opened.toFixed(decimalplace)
-        const presettlePrice = presettle.toFixed(decimalplace)
+    return dataList.value.map(({ refgoodscode, decimalplace }) => {
+        const quote = futuresStore.getQuoteInfo(refgoodscode)
+        const { rise, change, amplitude } = quote.value ?? {}
 
         return {
-            goodsid,
-            goodsname,
-            lastPrice,
-            riseValue,
-            applies,
-            openedPrice, 
-            presettlePrice,
+            ...quote.value,
+            rise: rise?.toFixed(decimalplace),
+            change: parsePercent(change),
+            amplitude: parsePercent(amplitude),
         }
     })
 })
 
 const columns: Model.TableColumn[] = [
     { prop: 'goodsname', label: '商品/标的' },
-    { prop: 'lastPrice', label: '当前价' },
-    { prop: 'riseValue', label: '涨跌' },
-    { prop: 'applies', label: '幅度' },
-    { prop: 'openedPrice', label: '今开' },
-    { prop: 'presettlePrice', label: '昨结' },
+    { prop: 'last', label: '当前价' },
+    { prop: 'rise', label: '涨跌' },
+    { prop: 'change', label: '幅度' },
+    { prop: 'opened', label: '今开' },
+    { prop: 'presettle', label: '昨结' },
+    { prop: 'lowest', label: '最低' },
+    { prop: 'highest', label: '最高' },
+    { prop: 'amplitude', label: '振幅' },
 ]
 </script>

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

@@ -1,8 +1,115 @@
 <template>
-    <app-view>
+    <app-view class="g-detail">
+        <template #header>
+            <app-navbar title="转让详情" />
+        </template>
+        <Swipe class="g-detail__banner" :autoplay="5000" indicator-color="white" lazy-render>
+            <SwipeItem v-for="(url, index) in detailBanners" :key="index">
+                <img :src="url" />
+            </SwipeItem>
+        </Swipe>
+        <div class="g-detail__info">
+            <div class="pricebar">
+                <div class="pricebar-left">
+                    <span class="tag">起拍价</span>
+                    <span class="unit">¥</span>
+                    <span class="price">{{ detail.startprice?.toFixed(2) }}</span>
+                </div>
+                <div class="pricebar-right">
+                    <span>总量:{{ detail.presaleqty }}</span>
+                </div>
+            </div>
+            <div class="tag">
+                <Tag type="danger" round>大宗竞拍</Tag>
+            </div>
+            <div class="titlebar">
+                <span>{{ detail.sellname }}</span>
+                <span>{{ detail.goodsname }}</span>
+            </div>
+            <div class="timebar">
 
+            </div>
+        </div>
+        <div class="g-detail__cell">
+            <ul>
+                <li>
+                    <span>预售定金</span>
+                    <span>{{ earnest }}</span>
+                </li>
+                <li>
+                    <span>转让比例</span>
+                    <span>{{ parsePercent(detail.transferdepositratio) }}</span>
+                </li>
+            </ul>
+        </div>
+        <div class="g-detail__cell">
+            <ul>
+                <li>
+                    <span>开始时间</span>
+                    <span>{{ detail.starttime }}</span>
+                </li>
+                <li>
+                    <span>结束时间</span>
+                    <span>{{ detail.endtime }}</span>
+                </li>
+            </ul>
+        </div>
+        <div class="g-detail__desc">
+            <template v-for="(url, index) in detailImages" :key="index">
+                <img :src="url" alt="" />
+            </template>
+        </div>
+        <template #footer>
+            <div class="g-detail__footer">
+                <Button type="primary" block round @click="openComponent('delisting')">我要转让</Button>
+            </div>
+            <component ref="componentRef" :is="componentMap.get(componentId)" v-bind="{ detail }" @closed="closeComponent"
+                v-if="componentId" />
+        </template>
     </app-view>
 </template>
 
 <script lang="ts" setup>
-</script>
+import { computed, defineAsyncComponent } from 'vue'
+import { Swipe, SwipeItem, Button, Tag } from 'vant'
+import { getFileUrl, parsePercent } from '@/filters'
+import { useComponent } from '@/hooks/component'
+import { useNavigation } from '@/hooks/navigation'
+
+const componentMap = new Map<string, unknown>([
+    ['delisting', defineAsyncComponent(() => import('./components/delisting/index.vue'))], // 摘牌
+])
+
+const { getParamString } = useNavigation()
+const { componentRef, componentId, openComponent, closeComponent } = useComponent()
+const qs = getParamString('item')
+const detail: Model.PresaleAuctionsRsp = JSON.parse(qs.toString() || '{}')
+
+// 商品banner
+const detailBanners = computed(() => {
+    const bannerpicurl = detail.bannerpicurl ?? ''
+    return bannerpicurl?.split(',').map((path) => getFileUrl(path))
+})
+
+// 预售定金
+const earnest = computed(() => {
+    switch (detail.presaledepositalgorithm) {
+        case 1:
+            return (detail.presaledepositvalue * 100).toFixed(2)
+        case 2:
+            return detail.presaledepositvalue.toFixed(2)
+        default:
+            return '0.0'
+    }
+})
+
+// 商品图片列表
+const detailImages = computed(() => {
+    const pictureurls = detail.pictureurls ?? ''
+    return pictureurls.split(',').map((path) => getFileUrl(path))
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 13 - 0
src/packages/mobile/views/transfer/detail/components/delisting/index.less

@@ -0,0 +1,13 @@
+.supply-demand-listing {
+    &__form {
+        .van-stepper {
+            display: flex;
+            align-items: center;
+            width: 100%;
+
+            &__input {
+                flex: 1;
+            }
+        }
+    }
+}

+ 128 - 0
src/packages/mobile/views/transfer/detail/components/delisting/index.vue

@@ -0,0 +1,128 @@
+<template>
+    <app-popup class="supply-demand-listing" title="出价" v-model:show="showModal" :refresh="refresh">
+        <Form class="supply-demand-listing__form" ref="formRef" @submit="onSubmit">
+            <Field label="起拍价">
+                <template #input>
+                    <span>{{ detail.startprice }}</span>
+                </template>
+            </Field>
+            <Field name="OrderPrice" :rules="formRules.OrderPrice" label="认购价">
+                <template #input>
+                    <Stepper v-model="formData.OrderPrice" theme="round" :min="detail.startprice" :decimal-length="2"
+                        :auto-fixed="false" button-size="22" />
+                </template>
+            </Field>
+            <Field name="OrderQty" :rules="formRules.OrderQty" label="认购数量">
+                <template #input>
+                    <Stepper v-model="formData.OrderQty" theme="round" :max="detail.presaleqty" button-size="22"
+                        :auto-fixed="false" integer />
+                </template>
+            </Field>
+            <Field label="预售定金">
+                <template #input>
+                    <span>{{ earnest }}</span>
+                </template>
+            </Field>
+            <Field label="可用资金">
+                <template #input>
+                    <span>{{ accountStore.avaiableMoney.toFixed(2) }}</span>
+                </template>
+            </Field>
+        </Form>
+        <template #footer>
+            <Button type="primary" block round @click="formRef?.submit">确定</Button>
+        </template>
+    </app-popup>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, PropType, onMounted, computed } from 'vue'
+import { Form, Field, Stepper, Button, FieldRule, FormInstance } from 'vant'
+import { fullloading, dialog } from '@/utils/vant'
+import { useAccountStore } from '@/stores'
+import { useOrder } from '@/business/trade'
+import AppPopup from '@mobile/components/base/popup/index.vue'
+import { BuyOrSell } from '@/constants/order'
+import { EPriceMode, EValidType } from '@/constants/client'
+//import AppSelect from '@mobile/components/base/select/index.vue'
+
+const props = defineProps({
+    detail: {
+        type: Object as PropType<Model.PresaleAuctionsRsp>,
+        required: true
+    },
+})
+
+const { formData, formSubmit } = useOrder()
+const accountStore = useAccountStore()
+const formRef = shallowRef<FormInstance>()
+const refresh = shallowRef(false) // 是否刷新父组件数据
+const showModal = shallowRef(true)
+
+// 表单验证规则
+const formRules: { [key in keyof Proto.OrderReq]?: FieldRule[] } = {
+    OrderPrice: [{
+        message: '请输入价格',
+        validator: () => {
+            return !!formData.OrderPrice
+        }
+    }],
+    OrderQty: [{
+        message: '请输入数量',
+        validator: () => {
+            return !!formData.OrderQty
+        }
+    }],
+}
+
+// 预售定金
+const earnest = computed(() => {
+    switch (props.detail.presaledepositalgorithm) {
+        case 1:
+            return (props.detail.presaledepositvalue * 100).toFixed(2)
+        case 2:
+            return props.detail.presaledepositvalue.toFixed(2)
+        default:
+            return '0.0'
+    }
+})
+
+// 关闭弹窗
+const closed = (isRefresh = false) => {
+    refresh.value = isRefresh
+    showModal.value = false
+}
+
+// 提交摘牌
+const onSubmit = () => {
+    const { applyid, goodsid, marketid } = props.detail
+    formData.GoodsID = goodsid
+    formData.MarketID = marketid
+    formData.BuyOrSell = BuyOrSell.Buy
+    formData.RelatedID = applyid
+    formData.ValidType = EValidType.VALIDTYPE_YZ
+    formData.PriceMode = EPriceMode.PRICEMODE_LIMIT
+
+    fullloading((hideLoading) => {
+        formSubmit().then(() => {
+            hideLoading()
+            dialog('提交成功').then(() => closed(true))
+        }).catch((err) => {
+            hideLoading(err, 'fail')
+        })
+    })
+}
+
+onMounted(() => {
+    formData.OrderPrice = props.detail.startprice
+})
+
+// 暴露组件属性给父组件调用
+defineExpose({
+    closed,
+})
+</script>
+
+<style lang="less">
+@import './index.less';
+</style>

+ 100 - 0
src/packages/mobile/views/transfer/detail/index.less

@@ -0,0 +1,100 @@
+.g-detail {
+    &__banner {
+        background-color: #ddd;
+        min-height: 3rem;
+
+        .van-swipe-item {
+            height: 4rem;
+            font-size: 0;
+
+            img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+            }
+        }
+    }
+
+    &__info {
+        background-color: #fff;
+        border-bottom-left-radius: .2rem;
+        border-bottom-right-radius: .2rem;
+        padding: .32rem;
+
+        .pricebar {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+
+            &-left {
+                color: red;
+
+                .tag {
+                    font-size: .24rem;
+                }
+
+                .price {
+                    font-size: .44rem;
+                }
+            }
+
+            &-right {
+                font-size: .24rem;
+                color: #999;
+            }
+        }
+
+        .tag {
+            margin-top: .24rem;
+        }
+
+        .titlebar {
+            font-size: .32rem;
+            font-weight: bold;
+            margin-top: .2rem;
+
+            span {
+                margin-right: .1rem;
+            }
+        }
+
+        .timebar {
+            display: flex;
+            justify-content: space-around;
+        }
+    }
+
+    &__cell {
+        background-color: #fff;
+        border-radius: .2rem;
+        margin-top: .24rem;
+        padding: .2rem .32rem;
+
+        ul {
+            li {
+                display: flex;
+                padding: .12rem;
+
+                span {
+                    &:first-child {
+                        width: 1.6rem;
+                        color: #999;
+                    }
+
+                    &:last-child {
+                        flex: 1;
+                    }
+                }
+            }
+        }
+    }
+
+    &__desc {
+        margin-top: .24rem;
+    }
+
+    &__footer {
+        background-color: #fff;
+        padding: .12rem .24rem;
+    }
+}

+ 12 - 1
src/packages/mobile/views/transfer/list/Index.vue

@@ -13,12 +13,23 @@
 
 <script lang="ts" setup>
 import { onMounted, computed } from 'vue'
+import { handleNumberValue } from '@/filters'
 import { useFuturesStore } from '@/stores'
 import quoteSocket from '@/services/websocket/quote'
 import AppList from '@mobile/components/base/list/index.vue'
 
 const futuresStore = useFuturesStore()
-const dataList = computed(() => futuresStore.quoteList.filter((e) => e.marketid === 49201))
+
+const dataList = computed(() => {
+    const list = futuresStore.quoteList.filter((e) => e.marketid === 49201)
+    return list.map((e) => ({
+        goodscode: e.goodscode,
+        ask: handleNumberValue(e.ask),
+        last: handleNumberValue(e.last),
+        askvolume: handleNumberValue(e.askvolume),
+    }))
+})
+
 const goodsCodes = dataList.value.map((e) => e.goodscode.toUpperCase())
 const subscribe = quoteSocket.addSubscribe(goodsCodes)
 

+ 2 - 0
src/services/index.ts

@@ -22,6 +22,8 @@ export default new (class {
         tradePort: '',
         tradeUrl: '',
         uploadUrl: '',
+        quoteWS: '',
+        tradeWS: '',
         oem: '',
         iOS: '',
         android: '',

+ 1 - 1
src/services/websocket/quote.ts

@@ -34,7 +34,7 @@ export default new (class {
 
     private async connect() {
         const res = await service.onReady();
-        await this.socket.connect(res.quoteUrl, this.loginStore.token);
+        await this.socket.connect(res.quoteWS, this.loginStore.token);
     }
 
     /**

+ 1 - 1
src/services/websocket/trade.ts

@@ -37,7 +37,7 @@ export default new (class {
 
     private async connect() {
         const res = await service.onReady();
-        await this.socket.connect(res.tradeUrl, this.loginStore.token);
+        await this.socket.connect(res.tradeWS, this.loginStore.token);
     }
 
     /**

+ 1 - 1
src/stores/modules/enum.ts

@@ -12,7 +12,7 @@ export interface EnumType {
     disabled?: boolean;
 }
 
-const enumKeys = ['clientType', 'scoreConfigType', 'accountBusinessCode', 'certificatetype', 'signstatus', 'thjOrderStatus', 'THJDeliveryMode', 'goodsunit', 'WROutInApplyStatus2', 'THJTransferStatus', 'WRTradeOrderStatus', 'THJMarket', 'THJProfitRoleType', 'appointmentModelOut'] as const
+const enumKeys = ['clientType', 'scoreConfigType', 'accountBusinessCode', 'certificatetype', 'signstatus', 'thjOrderStatus', 'THJDeliveryMode', 'goodsunit', 'WROutInApplyStatus2', 'THJTransferStatus', 'WRTradeOrderStatus', 'THJMarket', 'THJProfitRoleType', 'appointmentModelOut', 'orderstatus'] as const
 
 const enumMap = new Map<typeof enumKeys[number], ShallowRef<Model.EnumRsp[]>>()
 

+ 17 - 12
src/stores/modules/futures.ts

@@ -21,7 +21,8 @@ export const useFuturesStore = defineStore(() => {
     // 行情列表
     const quoteList = computed(() => {
         return state.goodsList.reduce((res, cur) => {
-            const { goodscode, goodsname, goodsgroupid, marketid, decimalplace } = cur
+            const { goodsid, goodscode, goodsname, goodsgroupid, marketid, decimalplace } = cur
+            const quoteDayInfo = state.quoteDayList.find((e) => e.goodscode.toUpperCase() === goodscode.toUpperCase())
             const {
                 last = 0,
                 bid = 0,
@@ -38,20 +39,24 @@ export const useFuturesStore = defineStore(() => {
                 highest = 0,
                 lowest = 0,
                 lasttime = '',
-            } = getQuoteDayInfoByCode(goodscode).value ?? {}
+            } = quoteDayInfo ?? {}
 
-            const change = last === 0 ? 0 : last - presettle // 涨跌额/涨跌: 最新价 - 昨结价
+            const rise = last ? last - presettle : 0   // 涨跌额/涨跌: 最新价 - 昨结价
+            const change = presettle ? rise / presettle : 0 // 涨跌幅/幅度: (最新价 - 昨结价) / 昨结价
+            const amplitude = last ? (highest - lowest) / last : 0 // 振幅: (最高价 - 最低价 ) / 最新价
 
             const item: Model.Futures = {
                 marketid,
                 goodsgroupid,
+                goodsid,
                 goodscode,
                 goodsname,
                 decimalplace,
                 last,
                 lasttime,
-                amplitude: change === 0 ? 0 : change / presettle, // 涨跌幅/幅度: (最新价 - 昨结价) / 昨结价
+                rise,
                 change,
+                amplitude,
                 bid,
                 ask,
                 bidvolume,
@@ -102,15 +107,15 @@ export const useFuturesStore = defineStore(() => {
         }
     }
 
-    // 通过 goodscode 获取盘面实时行情
-    const getQuoteDayInfoByCode = (goodscode: string) => {
-        return computed(() => state.quoteDayList.find((e) => e.goodscode.toUpperCase() === goodscode.toUpperCase()))
+    // 通过 goodscode 获取实时行情信息
+    const getQuoteInfo = (goodscode: string) => {
+        return computed(() => quoteList.value.find((e) => e.goodscode.toUpperCase() === goodscode.toUpperCase()))
     }
 
-    // 通过 goodscode 获取商品实时报价
-    const getGoodsPriceByCode = (goodscode: string) => {
+    // 通过 goodscode 获取实时行情报价
+    const getQuotePrice = (goodscode: string) => {
         return computed(() => {
-            const quote = getQuoteDayInfoByCode(goodscode)
+            const quote = getQuoteInfo(goodscode)
             return quote.value?.last ?? 0
         })
     }
@@ -302,8 +307,8 @@ export const useFuturesStore = defineStore(() => {
         ...toRefs(state),
         quoteList,
         getGoodsList,
-        getQuoteDayInfoByCode,
-        getGoodsPriceByCode,
+        getQuoteInfo,
+        getQuotePrice,
         quotePushNotify,
     }
 })

+ 4 - 2
src/types/model/market.d.ts

@@ -43,13 +43,15 @@ declare namespace Model {
     interface Futures {
         marketid: number; // 所属市场ID
         goodsgroupid: number; // 所属商品组ID
+        goodsid: number; // 商品ID
         goodscode: string; // 商品代码
         goodsname: string; // 商品名称
         decimalplace: number;//报价小数位
         last: number; // 最新价
         lasttime: string; // 行情时间(只有现价变化行情时间才变化)
-        amplitude: number; // 涨跌幅
-        change: number; // 涨跌
+        rise: number; // 涨跌
+        change: number; // 涨跌幅
+        amplitude: number; // 振幅
         bid: number; // 买价
         ask: number; // 卖价
         bidvolume: number; // 买量