10.10综合交易所原始源码_移动端
1
7 days ago 4f9044ae2a9f2db03bbb916bc5f6dfd12916361d
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('请输入金额'))
        }