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