From 4f9044ae2a9f2db03bbb916bc5f6dfd12916361d Mon Sep 17 00:00:00 2001
From: 李 <344137771@qq.com>
Date: Wed, 03 Jun 2026 16:10:01 +0800
Subject: [PATCH] 1

---
 src/components/Transform/perpetual-open/index.vue |  324 +++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 260 insertions(+), 64 deletions(-)

diff --git a/src/components/Transform/perpetual-open/index.vue b/src/components/Transform/perpetual-open/index.vue
index 677c61f..9aca94f 100644
--- a/src/components/Transform/perpetual-open/index.vue
+++ b/src/components/Transform/perpetual-open/index.vue
@@ -11,7 +11,7 @@
                                         class="w-5 h-3" />
             <div v-if="showOptions" class="options tabBackground w-36 absolute top-24 left-0 z-10">
               <div class="w-full" @click.stop="handleChoose(item)"
-                   :class="{ 'option-active': form.lever_rate === item.lever_rate }" :value="item.lever_rate"
+                   :class="{ 'option-active': Number(form.lever_rate) === Number(item.lever_rate) }" :value="item.lever_rate"
                    v-for="item in initData.lever" :key="item.id">
                 {{ item.lever_rate }}x
               </div>
@@ -53,7 +53,8 @@
             <div class="h-18 greyBg mb-8 flex pr-5 justify-center rounded-lg textColor items-center"
               v-if="selectIndex == 1">
               <input placeholder="" class="greyBg w-full pl-5 h-16 border-none text-left rounded-lg"
-                :disabled="type / 1 === 1" @focus="focus = true" v-model="form.price" />
+                :disabled="type / 1 === 1" @focus="focus = true" v-model="form.price"
+                @input="onLimitPriceInput" />
               <span class="ml-5">{{ getCurrentUnit() }}</span>
             </div>
 
@@ -63,9 +64,9 @@
                 @click="onReduce">
                 <img src="../../../assets/image/public/reduce.png" alt="add" class="w-8 h-2" />
               </div>
-              <input v-if="selectIndex == 1" :placeholder="$t('张数')" class="border-none greyBg text-center textColor"
+              <input v-if="selectIndex == 1" :placeholder="symbolUpper" class="border-none greyBg text-center textColor"
                 :class="['HK-stocks', 'JP-stocks','UK-stocks','DE-stocks','BZ-stocks'].includes(queryType) ? 'full-input' : 'half-input'"
-                v-model="form.amount" type="number" @input="onInput" />
+                v-model="form.amount" type="number" @input="onInput" @blur="onAmountBlur" />
               <input v-if="selectIndex == 2 && Array.isArray(initFutrue.para) && initFutrue.para.length"
                 :placeholder="($t('最少') + (initFutrue && Array.isArray(initFutrue.para) && initFutrue.para[paraIndex] ? $t('最小金额') + initFutrue.para[paraIndex].buy_min : ''))"
                 class="border-none greyBg text-center textColor"
@@ -82,49 +83,32 @@
             </div>
             <template v-if="selectIndex == 1">
               <div class="mt-10 mb-8 w-full flex justify-between items-center">
-                <span class="text-22 text-grey">{{ $t("可开张数") }}</span>
+                <span class="text-22 text-grey">{{ $t("最大数量") }} ({{ symbolUpper }})</span>
                 <span class="text-22 textColor">
-                  {{ initData.volume }}
-                  {{ $t("张") }}
+                  {{ maxOpenLotsFormatted }}
+                  {{ symbolUpper }}
                 </span>
               </div>
-              <!-- <vue-slider v-bind="options" v-model="form.amount"></vue-slider> -->
-              <!-- <vue-slider class="mainBox" v-bind="options" :marks="marks" v-model="form.volume" :hide-label="true"  width="90%"
-              :railStyle="{ background: '#404040', height: '4px' }"
-              :processStyle="{ background: '#266BFF', height: '4px' }">
-              <template v-slot:step="{ active }">
-                <div :class="['custom-step', { active }]"></div>
-              </template>
-            </vue-slider>
-            <div style="color: #868D9A" class="mt-36 text-24 w-full flex justify-between items-center">
-              <span class="flex-1 text-left">0%</span>
-              <span class="flex-1 text-left">25%</span>
-              <span class="flex-1 text-center">50%</span>
-              <span class="flex-1 text-right">75%</span>
-              <span class="flex-1 text-right">100%</span>
-            </div> -->
-              <!-- 张数输入 -->
-              <amount-slider ref="sliderRef" :maxAmount="getVolumnByLever()" @getAmount="getAmount"></amount-slider>
+              <!-- 仅保留一个数量滑块;上限随市价/限价委托价对应的 maxOpenLots 变化 -->
+              <amount-slider ref="sliderRef" :maxAmount="maxOpenLots" :amountValue="form.amount"
+                @getAmount="getAmount"></amount-slider>
             </template>
             <template v-if="selectIndex == 1 && userInfo && userInfo.token">
               <div class="flex justify-between mt-8">
                 <div class="text-grey">{{ $t('合约金额') }}</div>
-                <div class="textColor">{{ initData.amount * (form.amount / 1) * form.lever_rate }}
+                <div class="textColor">{{ formatNum4(perpNotionalUsdt) }}
                   <span>{{  getCurrentUnit() }}</span>
                 </div>
               </div>
               <div class="flex justify-between mt-8">
                 <div class="text-grey">{{ $t("保证金") }}</div>
                 <div class="textColor">
-                  {{ (initData.amount * (form.amount / 1)) }} {{ getCurrentUnit() }}
+                  {{ formatNum4(perpMarginUsdt) }} {{ getCurrentUnit() }}
                 </div>
               </div>
               <div class="flex justify-between mt-8">
                 <div class="text-grey">{{ $t("建仓手续费") }}</div>
-                <div class="textColor">{{
-                  userInfo.perpetual_contracts_status === '1' ? initData.fee *
-                (form.amount / 1) : initData.fee * (form.amount / 1) * form.lever_rate
-                }} {{ getCurrentUnit() }}
+                <div class="textColor">{{ formatNum4(perpOpenFeeUsdt) }} {{ getCurrentUnit() }}
                 </div>
               </div>
             </template>
@@ -316,16 +300,13 @@
 
 <script>
 import config from "@/config";
-import { Popup, showToast } from 'vant';
+import { closeDialog, Popup, showToast } from 'vant';
 import { mapGetters } from 'vuex'
-import VueSlider from 'vue-slider-component/dist-css/vue-slider-component.umd.js'
-import 'vue-slider-component/theme/default.css'
 import { _orderOpen, _futrueOrder, _futrueOrderDetail, _getBalance, _futrueOrderInit } from '@/service/trade.api'
 import ContractFutrue from '@/components/Transform/contract-futrue/index.vue'
 import PopupDelivery from "@/components/Transform/popup-delivery/index.vue";
 import { fixDate, throttle } from "@/utils/index.js";
 import AmountSlider from "./amountSlider.vue";
-import "vue-slider-component/theme/default.css";
 import { _getHomeList } from "@/service/home.api";
 import { themeStore } from '@/store/theme';
 import { currentUnit } from '@/utils/coinUnit.js'
@@ -336,7 +317,6 @@
 export default {
   name: "perpetualPosition",
   components: {
-    VueSlider,
     ContractFutrue,
     PopupDelivery,
     AmountSlider,
@@ -403,7 +383,24 @@
       if (JSON.stringify(this.initFutrue.para) != '[]') {
         this.form.para_id = this.initFutrue.para && this.initFutrue.para[this.paraIndex].para_id // 不优雅,不可靠
       }
-
+      if (this.selectIndex / 1 === 1) {
+        this.handleInitSliderOption(true)
+      }
+    },
+    'form.price'() {
+      if (this.selectIndex / 1 === 1) {
+        this.handleInitSliderOption(true)
+      }
+    },
+    'form.lever_rate'() {
+      if (this.selectIndex / 1 === 1) {
+        this.handleInitSliderOption(true)
+      }
+    },
+    type() {
+      if (this.selectIndex / 1 === 1) {
+        this.handleInitSliderOption(true)
+      }
     },
     initOpen: { // 处理滚动条初始值
       deep: true,
@@ -495,8 +492,164 @@
       }
       return this.initFutrue
     },
-    coudBuyVolume() { // 可买数量
-      return Math.floor((this.initOpen.volume / 1) / this.form.lever_rate)
+    /**
+     * 永续下单可用 USDT:与行情页「账户余额」一致,优先 userInfo.balance(钱包 USDT);
+     * openview 返回的 volume 在部分接口里并非可用保证金,仅用 initOpen.volume 会导致可开数量极小。
+     */
+    contractAvailableUsdt() {
+      const fromUser = parseFloat(this.userInfo && this.userInfo.balance)
+      if (fromUser > 0 && isFinite(fromUser)) return fromUser
+      const fromOpen = parseFloat(this.initOpen.volume)
+      if (fromOpen > 0 && isFinite(fromOpen)) return fromOpen
+      return 0
+    },
+    /**
+     * 最大可开数量(币本位):
+     * - 市价:按最新成交价/行情价折算;
+     * - 限价:按输入委托价折算。
+     * 公式:maxQty = balance * lever / (price * (1 + feeRate))
+     * 其中 feeRate 来自开仓初始化接口(initOpen.fee),不再写死 0.01。
+     */
+    maxOpenLots() {
+      if (this.selectIndex / 1 !== 1) {
+        return Math.max(0, Number(this.initOpen.volume) || 0)
+      }
+      if (!this.userInfo || !this.userInfo.token) return 0
+      const balance = this.contractAvailableUsdt
+      if (!(balance > 0) || !isFinite(balance)) return 0
+
+      const lever = Math.max(1, Number(this.form.lever_rate) || 1)
+      if (!(lever > 0) || !isFinite(lever)) return 0
+
+      const markPrice = parseFloat(this.price)
+      const limitP = parseFloat(this.form.price)
+      const isLimit = String(this.type) === '2' || Number(this.type) === 2
+
+      let priceForMax
+      if (isLimit) {
+        if (limitP > 0 && isFinite(limitP)) {
+          priceForMax = limitP
+        } else {
+          return 0
+        }
+      } else if (markPrice > 0 && isFinite(markPrice)) {
+        priceForMax = markPrice
+      } else if (limitP > 0 && isFinite(limitP)) {
+        priceForMax = limitP
+      } else {
+        return 0
+      }
+
+      // 与 maxOpenableBaseQuantity 对齐:
+      // costPerUnit = marginPerUnit + feePerUnit
+      // marginPerUnit = price / L
+      const marginPerUnit = priceForMax / lever
+      const feeNum = Number(this.initOpen && this.initOpen.fee)
+      let feePerUnit = 0
+      if (Number.isFinite(feeNum) && feeNum > 0) {
+        // 小于 0.05 按费率(名义 × 费率 × 杠杆);否则按每单位绝对费用 × 杠杆
+        feePerUnit = feeNum < 0.05 ? priceForMax * feeNum * lever : feeNum * lever
+      }
+      const costPerUnit = marginPerUnit + feePerUnit
+      if (!Number.isFinite(costPerUnit) || costPerUnit <= 0) return 0
+
+      let maxQty = balance / costPerUnit
+      const serverVol = Number(this.initOpen && this.initOpen.volume)
+      // fee 为小数费率时,沿用快捷式与旧逻辑一致
+      if (
+        Number.isFinite(serverVol) &&
+        serverVol > 0 &&
+        Number.isFinite(priceForMax) &&
+        priceForMax > 0 &&
+        Number.isFinite(feeNum) &&
+        feeNum > 0 &&
+        feeNum < 0.05
+      ) {
+        maxQty = ((balance * lever) / (1 + feeNum * lever)) / priceForMax
+      }
+
+      if (!Number.isFinite(maxQty) || maxQty <= 0) return 0
+      if (maxQty <= 1e-10) return 0
+
+      const minLot = Number(
+        this.initOpen && (
+          this.initOpen.buy_min ??
+          this.initOpen.min_qty ??
+          this.initOpen.min_volume ??
+          this.initOpen.min
+        )
+      )
+      if (Number.isFinite(minLot) && minLot > 0 && maxQty < minLot) return 0
+
+      // 保留计算精度,不做四舍五入、不做截断
+      return maxQty
+    },
+    /** 最大可开数量展示为四位小数 */
+    maxOpenLotsFormatted() {
+      const n = this.maxOpenLots
+      if (!(n >= 0) || !isFinite(n)) return '0'
+      // 展示原始精度(不四舍五入、不截断)
+      return String(n)
+    },
+    coudBuyVolume() {
+      return this.maxOpenLots
+    },
+    /** 当前交易品种大写,作数量单位展示 */
+    symbolUpper() {
+      const s = this.symbol || (this.$route && this.$route.params && this.$route.params.symbol) || ''
+      return String(s).trim().toUpperCase() || '--'
+    },
+    /** 永续展示用有效价:限价用委托价;市价用行情价(与 maxOpenLots 口径一致) */
+    perpCalcPrice() {
+      if (this.selectIndex / 1 !== 1) return 0
+      const isLimit = String(this.type) === '2' || Number(this.type) === 2
+      const fp = parseFloat(this.form.price)
+      const mp = parseFloat(this.price)
+      if (isLimit) {
+        if (fp > 0 && isFinite(fp)) return fp
+        return (mp > 0 && isFinite(mp)) ? mp : 0
+      }
+      if (mp > 0 && isFinite(mp)) return mp
+      return (fp > 0 && isFinite(fp)) ? fp : 0
+    },
+    perpCalcQty() {
+      const q = parseFloat(this.form.amount)
+      return (q > 0 && isFinite(q)) ? q : 0
+    },
+    perpCalcLever() {
+      const L = parseFloat(this.form.lever_rate)
+      return (L > 0 && isFinite(L)) ? L : 1
+    },
+
+    /** 扣除建仓手续费后的有效数量(按当前价格折算) */
+    perpNetQtyAfterFee() {
+      const qty = this.perpCalcQty
+      const p = this.perpCalcPrice
+      if (!(qty > 0) || !(p > 0) || !isFinite(qty) || !isFinite(p)) return 0
+      const feeInQty = this.perpOpenFeeUsdt / p
+      const netQty = qty - feeInQty
+      return netQty > 0 && isFinite(netQty) ? netQty : 0
+    },
+    /** 合约名义价值 ≈ 价格 × 扣费后数量(USDT) */
+    perpNotionalUsdt() {
+      return this.perpCalcPrice * this.perpNetQtyAfterFee
+    },
+    /** 保证金 ≈ 名义价值 ÷ 杠杆,切换杠杆会立刻变 */
+    perpMarginUsdt() {
+      const L = this.perpCalcLever
+      return L > 0 ? (this.perpNotionalUsdt / L) : 0
+    },
+    /** 建仓手续费:与接口约定一致,依赖数量与杠杆 */
+    perpOpenFeeUsdt() {
+      const qty = this.perpCalcQty
+      const fee = parseFloat(this.initOpen.fee) || 0
+      const lever = this.perpCalcLever
+      const price = this.perpCalcPrice
+      if (!(qty > 0) || !isFinite(qty)) return 0
+      if (String(this.userInfo.perpetual_contracts_status) === '1') {
+        return fee * qty
+      }
+      return fee * qty * price / lever
     },
   },
   data() {
@@ -572,6 +725,13 @@
     this.clearTimeout()
   },
   methods: {
+    /** 限价委托价变化:刷新滑块上限与 options.max(maxOpenLots 为计算属性会随 form.price 自动变) */
+    onLimitPriceInput() {
+      this.$nextTick(() => {
+        if (this.selectIndex / 1 !== 1) return
+        this.handleInitSliderOption(true)
+      })
+    },
     //获取张数
     getAmount(val) {
       if (!val) {
@@ -582,6 +742,13 @@
     getCurrentUnit() {
       return currentUnit(this.queryType)
     },
+    formatNum4(n) {
+      const v = parseFloat(n)
+      if (!isFinite(v)) return '0.0000'
+      const factor = 10000
+      const truncated = Math.trunc(v * factor) / factor
+      return truncated.toFixed(4)
+    },
     onSelect(item) {
       this.actions.map((item) => {
         item.className = ''
@@ -590,11 +757,8 @@
       this.showType = item.value;
       this.$emit("changeValueBack", this.showType);
     },
-    // 获取张数,数据转换
     getVolumnByLever() {
-      let vol;
-      vol = this.initOpen.volume / 1;
-      return Math.floor(vol);
+      return this.maxOpenLots
     },
     selectItemArry(item) {
       this.dataArrValue = item.value;
@@ -705,38 +869,52 @@
     },
     handleInitSliderOption(amount) {
       if (!amount) {
-        // 金额是否需要变动
         this.form.amount = "";
       }
-      console.log(this.initOpen.volume, this.form.lever_rate);
-      let vol;
-      vol = this.initOpen.volume / 1;
-      this.options.max = Math.floor(vol);
-      console.log("this.options.max", this.options.max);
-      if (this.options.max > 0) {
-        this.options.disabled = false;
+      let vol
+      if (this.selectIndex / 1 === 1) {
+        vol = this.maxOpenLots
       } else {
-        this.options.disabled = true;
+        vol = Math.floor(Number(this.initOpen.volume) || 0)
+      }
+      this.options.max = vol
+      if (this.options.max > 0) {
+        this.options.disabled = false
+      } else {
+        this.options.disabled = true
+      }
+      if (this.selectIndex / 1 === 1) {
+        const amt = parseFloat(this.form.amount)
+        const mx = parseFloat(this.options.max)
+        if (isFinite(amt) && isFinite(mx) && amt > mx + 1e-10) {
+          this.form.amount = mx
+        }
       }
     },
     handleChoose(item) {
-      this.showOptions = !this.showOptions
-      this.form.lever_rate = item.lever_rate
-      console.log('handleChoose')
-      this.handleInitSliderOption()
+      // 选中后关闭下拉;杠杆统一为数字,避免 "100" 与 100 比较不一致
+      this.showOptions = false
+      this.form.lever_rate = Number(item.lever_rate)
+      this.getAmount()
+      // 勿调用 handleInitSliderOption() 无参:会清空 form.amount,导致合约金额/与价格相关展示错乱
+      // 杠杆变化由 watch form.lever_rate 触发 handleInitSliderOption(true),保留已填数量并刷新上限
     },
     onAdd() { // +
       if (this.options.max === 0) {
         return
       }
-      if (this.form.amount === this.options.max) {
-        return;
-      }
-      this.form.amount++
+      const cur = parseFloat(this.form.amount) || 0
+      const mx = parseFloat(this.options.max) || 0
+      if (cur >= mx - 1e-8) return
+      const next = Math.min(Math.floor((cur + 1) * 10000) / 10000, mx)
+      this.form.amount = next
     },
     onReduce() { // -
-      if (this.form.amount > 1) {
-        this.form.amount--
+      const cur = parseFloat(this.form.amount) || 0
+      if (cur > 1) {
+        this.form.amount = Math.floor((cur - 1) * 10000) / 10000
+      } else if (cur > 0) {
+        this.form.amount = 0
       }
     },
     onParaId(evt) { // 交割日期
@@ -749,9 +927,27 @@
       // console.log(1111111, this.form.amount, this.selectIndex)
       if (this.selectIndex == 1 && this.options.max == 0) {
         this.form.amount = this.form.amount / 1
-      } else if (this.selectIndex == 1 && this.form.amount / 1 > this.options.max / 1) {
-        this.form.amount = this.options.max / 1
+      } else if (this.selectIndex == 1) {
+        const amt = parseFloat(this.form.amount)
+        const mx = parseFloat(this.options.max)
+        if (isFinite(amt) && isFinite(mx) && amt > mx) {
+          this.form.amount = mx
+        }
       }
+    },
+    /** 数量展示:截断四位小数(市价/限价均按当前 perpCalcPrice 算下方合约金额) */
+    onAmountBlur() {
+      if (this.selectIndex / 1 !== 1) return
+      const raw = this.form.amount
+      if (raw === '' || raw === null || raw === undefined) return
+      const v = parseFloat(raw)
+      if (!isFinite(v)) {
+        this.form.amount = ''
+        return
+      }
+      const factor = 10000
+      const truncated = Math.trunc(v * factor) / factor
+      this.form.amount = truncated === 0 ? '' : truncated
     },
     //价格类型下拉框切换
     selectBtn() {
@@ -809,7 +1005,7 @@
       }
       if (!this.form.amount) {
         if (this.selectIndex == 1) {
-          showToast(this.$t('请输入合约张数'))
+          showToast(`${this.$t('请输入').trim()} ${this.symbolUpper}`)
         } else {
           showToast(this.$t('请输入金额'))
         }

--
Gitblit v1.9.3