|
|
@@ -0,0 +1,481 @@
|
|
|
+<template>
|
|
|
+ <div class="calculator-page">
|
|
|
+ <div class="nav-bar">
|
|
|
+ <div class="nav-left" @click="$router.back()">
|
|
|
+ <div>
|
|
|
+ <VanIcon size="18" name="arrow-left"/>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="nav-title">计算器</div>
|
|
|
+ <div class="nav-right"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="header-info">
|
|
|
+ <div class="icon-wrapper">
|
|
|
+ <vanIcon size="20" name="bars" />
|
|
|
+ </div>
|
|
|
+ <h2 class="pair-name">BTCUSDT 永续</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tabs-wrapper">
|
|
|
+ <div class="tabs-content pf600">
|
|
|
+ <div
|
|
|
+ v-for="(tab, index) in tabs"
|
|
|
+ :key="index"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: currentTab === index }"
|
|
|
+ @click="currentTab = index"
|
|
|
+ >
|
|
|
+ {{ tab }}
|
|
|
+ <div class="active-bar" v-if="currentTab === index"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 2" class="mode-selectors">
|
|
|
+ <div class="select-box">全仓 ▾</div>
|
|
|
+ <div class="select-box">单向持仓</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="trade-direction">
|
|
|
+ <button
|
|
|
+ class="dir-btn long"
|
|
|
+ :class="{ active: direction === 'long' }"
|
|
|
+ @click="direction = 'long'"
|
|
|
+ >
|
|
|
+ 买入(做多)
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="dir-btn short"
|
|
|
+ :class="{ active: direction === 'short' }"
|
|
|
+ @click="direction = 'short'"
|
|
|
+ >
|
|
|
+ 卖出(做空)
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 5" class="funding-rate-display">
|
|
|
+ <div class="rate-value">0.0030%</div>
|
|
|
+ <div class="rate-label">当前资金费率
|
|
|
+ <VanIcon name="question-o" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab !== 4 && currentTab !== 5" class="leverage-section">
|
|
|
+ <div class="label">倍数</div>
|
|
|
+
|
|
|
+ <div class="leverage-control">
|
|
|
+ <button class="ctrl-btn" @click="stepLeverage(-1)">-</button>
|
|
|
+ <div class="leverage-val">{{ leverage }}x</div>
|
|
|
+ <button class="ctrl-btn" @click="stepLeverage(1)">+</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="slider-area">
|
|
|
+ <LeverageSlider
|
|
|
+ v-model="leverage"
|
|
|
+ :marks="leverageMarks"
|
|
|
+ :color="themeColor"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="leverage-tip">
|
|
|
+ 当前杠杆倍数最高可持有头寸 1,800,000,000 USDT
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <div v-if="currentTab !== 4 && currentTab !== 5" class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.entryPrice">开仓价格</span>
|
|
|
+ <input type="number" v-model="form.entryPrice" />
|
|
|
+ <div class="suffix">USDT <span class="tag-latest">最新</span></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 0">
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.closePrice">平仓价格</span>
|
|
|
+ <input type="number" v-model="form.closePrice" />
|
|
|
+ <div class="suffix">USDT</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.amount">成交数量</span>
|
|
|
+ <input type="number" v-model="form.amount" />
|
|
|
+ <div class="suffix">BTC</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 1">
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.amount">成交数量</span>
|
|
|
+ <input type="number" v-model="form.amount" />
|
|
|
+ <div class="suffix">BTC</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.pnl">预期收益额</span>
|
|
|
+ <input type="number" v-model="form.pnl" />
|
|
|
+ <div class="suffix">USDT</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 2">
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.amount">开仓数量</span>
|
|
|
+ <input type="number" v-model="form.amount" />
|
|
|
+ <div class="suffix">BTC</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.margin">保证金</span>
|
|
|
+ <input type="number" v-model="form.margin" />
|
|
|
+ <div class="suffix">USDT</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 3">
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.balance">账户余额</span>
|
|
|
+ <input type="number" v-model="form.balance" />
|
|
|
+ <div class="suffix">USDT</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 4">
|
|
|
+ <div class="grid-header">
|
|
|
+ <span>开仓价格(USDT)</span>
|
|
|
+ <span>成交数量(BTC)</span>
|
|
|
+ </div>
|
|
|
+ <div class="dynamic-row" v-for="(item, idx) in avgPriceList" :key="idx">
|
|
|
+ <div class="input-container half">
|
|
|
+ <input type="number" v-model="item.price" placeholder="0.00" />
|
|
|
+ </div>
|
|
|
+ <div class="input-container half">
|
|
|
+ <input type="number" v-model="item.amount" placeholder="0.00" />
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; align-items: center;">
|
|
|
+ <div class="remove-btn" @click="removeRow(idx)" v-if="avgPriceList.length > 1">
|
|
|
+ <span>-</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button class="add-position-btn" @click="addRow">增加仓位</button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-if="currentTab === 5">
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.entryPrice">标记价格</span>
|
|
|
+ <input type="number" v-model="form.entryPrice" />
|
|
|
+ <div class="suffix">USDT <span class="tag-latest">最新</span></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-row">
|
|
|
+ <div class="input-container">
|
|
|
+ <span class="placeholder" v-show="!form.amount">持仓数量</span>
|
|
|
+ <input type="number" v-model="form.amount" />
|
|
|
+ <div class="suffix">BTC</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="result-section">
|
|
|
+ <h3>计算结果</h3>
|
|
|
+ <p class="sub-text">实际市场存在变动,计算价格仅供参考</p>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 0">
|
|
|
+ <div class="res-row"><span>保证金</span><span>{{ result.margin }} USDT</span></div>
|
|
|
+ <div class="res-row"><span>收益</span><span :class="resultClass">{{ result.pnl }} USDT</span></div>
|
|
|
+ <div class="res-row"><span>收益率</span><span :class="resultClass">{{ result.roe }}%</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 1">
|
|
|
+ <div class="res-row"><span>目标价格</span><span>-- USDT</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 2">
|
|
|
+ <div class="res-row"><span>强平价格</span><span>-- USDT</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 3">
|
|
|
+ <div class="res-row"><span>可开</span><span>-- USDT</span></div>
|
|
|
+ <div class="res-row"><span>可开</span><span>-- BTC</span></div>
|
|
|
+ <p class="small-tip">此计算最大可开数量时,将不考虑您的开仓损失</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 4">
|
|
|
+ <div class="res-row"><span>开仓均价</span><span>-- USDT</span></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="currentTab === 5">
|
|
|
+ <div class="res-row"><span>资金费用</span><span>-- USDT</span></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="footer-btns">
|
|
|
+ <button class="btn-reset" @click="reset">重置</button>
|
|
|
+ <button class="btn-calc" :style="{ background: themeColor }" @click="calculate">计算</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { Icon as VanIcon } from 'vant';
|
|
|
+import { ref, computed } from 'vue';
|
|
|
+// 请确保路径正确,指向刚才那个更新过的子组件
|
|
|
+import LeverageSlider from './calculator/LeverageSlider.vue';
|
|
|
+
|
|
|
+// 状态定义
|
|
|
+const tabs = ['收益', '目标价格', '强平价格', '可开', '开仓均价', '资金费用'];
|
|
|
+const currentTab = ref(0);
|
|
|
+const direction = ref('long');
|
|
|
+const leverage = ref(10); // 默认从 10x 开始
|
|
|
+
|
|
|
+// ★ 固定阶梯数组 (10, 50, 100, 500, 1000)
|
|
|
+const leverageMarks = [10, 50, 100, 500, 1000];
|
|
|
+
|
|
|
+const form = ref({
|
|
|
+ entryPrice: '',
|
|
|
+ closePrice: '',
|
|
|
+ amount: '',
|
|
|
+ margin: '',
|
|
|
+ balance: '',
|
|
|
+ pnl: ''
|
|
|
+});
|
|
|
+
|
|
|
+const avgPriceList = ref([{ price: '', amount: '' }, { price: '', amount: '' }]);
|
|
|
+const result = ref({ margin: '--', pnl: '--', roe: '--' });
|
|
|
+
|
|
|
+// 计算属性
|
|
|
+const themeColor = computed(() => direction.value === 'long' ? '#2ebd85' : '#f6465d');
|
|
|
+const resultClass = computed(() => {
|
|
|
+ if (parseFloat(result.value.pnl) > 0) return 'text-green';
|
|
|
+ if (parseFloat(result.value.pnl) < 0) return 'text-red';
|
|
|
+ return '';
|
|
|
+});
|
|
|
+
|
|
|
+// ★ 核心修改:步进式调整杠杆 (点击加减号时)
|
|
|
+const stepLeverage = (dir) => {
|
|
|
+ // 1. 找到当前值在数组中的索引
|
|
|
+ let currentIndex = leverageMarks.indexOf(leverage.value);
|
|
|
+
|
|
|
+ // 如果当前值不在数组里(比如手动改成了20),就找一个最近的索引
|
|
|
+ if (currentIndex === -1) {
|
|
|
+ // 简单逻辑:默认归位到 0
|
|
|
+ currentIndex = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 计算下一个索引
|
|
|
+ let nextIndex = currentIndex + dir;
|
|
|
+
|
|
|
+ // 3. 边界限制
|
|
|
+ if (nextIndex < 0) nextIndex = 0;
|
|
|
+ if (nextIndex >= leverageMarks.length) nextIndex = leverageMarks.length - 1;
|
|
|
+
|
|
|
+ // 4. 更新值
|
|
|
+ leverage.value = leverageMarks[nextIndex];
|
|
|
+};
|
|
|
+
|
|
|
+// 简单计算逻辑 (Tab 0 收益)
|
|
|
+const calculate = () => {
|
|
|
+ if (currentTab.value === 0) {
|
|
|
+ const entry = parseFloat(form.value.entryPrice);
|
|
|
+ const close = parseFloat(form.value.closePrice);
|
|
|
+ const amt = parseFloat(form.value.amount);
|
|
|
+ const lev = leverage.value;
|
|
|
+
|
|
|
+ if (entry && close && amt && lev) {
|
|
|
+ const margin = (entry * amt) / lev;
|
|
|
+ let pnl = 0;
|
|
|
+ if (direction.value === 'long') {
|
|
|
+ pnl = (close - entry) * amt;
|
|
|
+ } else {
|
|
|
+ pnl = (entry - close) * amt;
|
|
|
+ }
|
|
|
+ const roe = (pnl / margin) * 100;
|
|
|
+
|
|
|
+ result.value = {
|
|
|
+ margin: margin.toFixed(2),
|
|
|
+ pnl: pnl.toFixed(2),
|
|
|
+ roe: roe.toFixed(2)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 其他辅助函数
|
|
|
+const addRow = () => avgPriceList.value.push({ price: '', amount: '' });
|
|
|
+const removeRow = (idx) => avgPriceList.value.splice(idx, 1);
|
|
|
+const reset = () => {
|
|
|
+ form.value = { entryPrice: '', closePrice: '', amount: '', margin: '', balance: '', pnl: '' };
|
|
|
+ result.value = { margin: '--', pnl: '--', roe: '--' };
|
|
|
+ leverage.value = 10;
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+* {
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.calculator-page {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
|
|
+ background: #fff;
|
|
|
+ min-height: 100vh;
|
|
|
+ padding-bottom: 90px;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+/* 导航 & 头部 */
|
|
|
+.nav-bar { display: flex; justify-content: space-between; align-items: center;
|
|
|
+ height: 44px; padding: 0 16px; }
|
|
|
+.nav-left { font-size: 24px; cursor: pointer; }
|
|
|
+.nav-title { font-size: 17px; font-weight: 600; }
|
|
|
+.nav-right { width: 20px; }
|
|
|
+
|
|
|
+.header-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background-color: #fff;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ margin-bottom: 1px;
|
|
|
+}
|
|
|
+
|
|
|
+.pair-name {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #333;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* Tabs */
|
|
|
+.tabs-wrapper { overflow-x: auto; scrollbar-width: none; margin-bottom: 15px; }
|
|
|
+.tabs-content { display: flex; justify-content: space-between; padding: 0 15px;
|
|
|
+ border-bottom: 1px solid #f5f5f5; }
|
|
|
+.tab-item { white-space: nowrap; padding: 12px 0; font-size: 14px; color: #999; position: relative;
|
|
|
+ cursor: pointer; }
|
|
|
+.tab-item.active { color: #111; font-weight: 600; }
|
|
|
+.active-bar { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 20px;
|
|
|
+ height: 3px; background: #111; border-radius: 2px; }
|
|
|
+
|
|
|
+/* 模式选择 */
|
|
|
+.mode-selectors { display: flex; gap: 10px; padding: 0 16px; margin-bottom: 15px; }
|
|
|
+.select-box { flex: 1; background: #f5f5f5; height: 36px; display: flex; align-items: center;
|
|
|
+ padding: 0 12px; border-radius: 4px; font-size: 14px; color: #333; }
|
|
|
+
|
|
|
+/* 做多做空 */
|
|
|
+.trade-direction { display: flex; gap: 10px; padding: 0 16px; margin-bottom: 20px; }
|
|
|
+.dir-btn { flex: 1; padding: 10px 0; border: none; border-radius: 4px; font-size: 14px;
|
|
|
+ cursor: pointer; background: #f5f5f5; color: #999; transition: all 0.2s; }
|
|
|
+.dir-btn.long.active { background: #2ebd85; color: #fff; font-weight: 600; }
|
|
|
+.dir-btn.short.active { background: #f6465d; color: #fff; font-weight: 600; }
|
|
|
+
|
|
|
+/* 资金费率 */
|
|
|
+.funding-rate-display { text-align: center; padding: 20px 0; }
|
|
|
+.rate-value { font-size: 32px; color: #666; font-weight: 500; }
|
|
|
+.rate-label { font-size: 12px; color: #999; margin-top: 5px; }
|
|
|
+
|
|
|
+/* 杠杆区域 */
|
|
|
+.leverage-section { padding: 0 16px; margin-bottom: 20px; }
|
|
|
+.label { font-size: 12px; color: #666; margin-bottom: 8px; }
|
|
|
+
|
|
|
+/* 蓝色边框输入框 */
|
|
|
+.leverage-control {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f7f8fa;
|
|
|
+ border: 1px solid #2979ff; /* 蓝色边框 */
|
|
|
+ border-radius: 6px;
|
|
|
+ height: 44px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.ctrl-btn {
|
|
|
+ width: 50px;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font-size: 22px;
|
|
|
+ color: #999;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding-bottom: 4px;
|
|
|
+}
|
|
|
+.leverage-val {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.slider-area { padding: 0 10px; }
|
|
|
+.leverage-tip { font-size: 12px; color: #333; margin-top: 10px; }
|
|
|
+
|
|
|
+/* 表单 */
|
|
|
+.form-group { padding: 0 16px; }
|
|
|
+.input-row { margin-bottom: 12px; }
|
|
|
+.input-container { position: relative; display: flex; align-items: center; background: #f5f5f5;
|
|
|
+ height: 44px; border-radius: 4px; padding: 0 12px; }
|
|
|
+.placeholder { position: absolute; color: #bbb; font-size: 14px; pointer-events: none; }
|
|
|
+.input-container input { flex: 1; border: none; background: transparent; outline: none;
|
|
|
+ font-size: 14px; z-index: 1; height: 100%; width: 100%; }
|
|
|
+.suffix { font-size: 14px; color: #666; margin-left: 8px; z-index: 2; white-space: nowrap; }
|
|
|
+.tag-latest { color: #f6465d; margin-left: 4px; }
|
|
|
+
|
|
|
+/* 表格布局 */
|
|
|
+.grid-header { display: flex; font-size: 12px; color: #333; margin-bottom: 8px; }
|
|
|
+.grid-header span { flex: 1; }
|
|
|
+.dynamic-row { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
|
|
|
+.input-container.half { flex: 1; }
|
|
|
+.remove-btn { width: 24px; height: 24px; border: 1px solid #f6465d; color: #f6465d;
|
|
|
+ border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 18px; cursor: pointer; margin-left: 5px; }
|
|
|
+.remove-btn span { margin-bottom: 5px; }
|
|
|
+.add-position-btn { width: 100%; height: 44px; background: #f6465d; color: white;
|
|
|
+ border: none; border-radius: 22px; margin-top: 10px; font-size: 14px; }
|
|
|
+
|
|
|
+/* 结果 */
|
|
|
+.result-section { padding: 20px 16px; }
|
|
|
+.result-section h3 { margin: 0 0 5px; font-size: 16px; font-weight: 600; }
|
|
|
+.sub-text { font-size: 12px; color: #999; margin-bottom: 15px; }
|
|
|
+.res-row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
|
|
|
+.res-row span:first-child { color: #666; }
|
|
|
+.res-row span:last-child { color: #333; font-weight: 500; }
|
|
|
+.text-green { color: #2ebd85; }
|
|
|
+.text-red { color: #f6465d; }
|
|
|
+.small-tip { font-size: 12px; color: #999; margin-top: 5px; }
|
|
|
+
|
|
|
+/* 底部按钮 */
|
|
|
+.footer-btns { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff;
|
|
|
+ padding: 10px 16px 20px; display: flex; gap: 15px; box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
|
|
|
+ box-sizing: border-box; z-index: 100; }
|
|
|
+.btn-reset { flex: 1; height: 44px; border: none; background: #bbb; color: #fff;
|
|
|
+ font-size: 16px; border-radius: 4px; }
|
|
|
+.btn-calc { flex: 2; height: 44px; border: none; color: #fff; font-size: 16px;
|
|
|
+ border-radius: 4px; font-weight: 600; transition: background 0.3s; }
|
|
|
+</style>
|