Calculator.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <template>
  2. <div class="calculator-page">
  3. <div class="nav-bar">
  4. <div class="nav-left" @click="$router.back()">
  5. <div>
  6. <VanIcon size="18" name="arrow-left"/>
  7. </div>
  8. </div>
  9. <div class="nav-title">计算器</div>
  10. <div class="nav-right"></div>
  11. </div>
  12. <div class="header-info">
  13. <div class="icon-wrapper">
  14. <vanIcon size="20" name="bars" />
  15. </div>
  16. <h2 class="pair-name">BTCUSDT 永续</h2>
  17. </div>
  18. <div class="tabs-wrapper">
  19. <div class="tabs-content pf600">
  20. <div
  21. v-for="(tab, index) in tabs"
  22. :key="index"
  23. class="tab-item"
  24. :class="{ active: currentTab === index }"
  25. @click="currentTab = index"
  26. >
  27. {{ tab }}
  28. <div class="active-bar" v-if="currentTab === index"></div>
  29. </div>
  30. </div>
  31. </div>
  32. <div v-if="currentTab === 2" class="mode-selectors">
  33. <div class="select-box">全仓 ▾</div>
  34. <div class="select-box">单向持仓</div>
  35. </div>
  36. <div class="trade-direction">
  37. <button
  38. class="dir-btn long"
  39. :class="{ active: direction === 'long' }"
  40. @click="direction = 'long'"
  41. >
  42. 买入(做多)
  43. </button>
  44. <button
  45. class="dir-btn short"
  46. :class="{ active: direction === 'short' }"
  47. @click="direction = 'short'"
  48. >
  49. 卖出(做空)
  50. </button>
  51. </div>
  52. <div v-if="currentTab === 5" class="funding-rate-display">
  53. <div class="rate-value">0.0030%</div>
  54. <div class="rate-label" @click="showModal=true">当前资金费率
  55. <VanIcon name="question-o" />
  56. </div>
  57. </div>
  58. <div v-if="currentTab !== 4 && currentTab !== 5" class="leverage-section">
  59. <div class="label">倍数</div>
  60. <div class="leverage-control">
  61. <button class="ctrl-btn" @click="stepLeverage(-1)">-</button>
  62. <div class="leverage-val">{{ leverage }}x</div>
  63. <button class="ctrl-btn" @click="stepLeverage(1)">+</button>
  64. </div>
  65. <div class="slider-area">
  66. <LeverageSlider
  67. v-model="leverage"
  68. :marks="leverageMarks"
  69. :color="themeColor"
  70. />
  71. </div>
  72. <div class="leverage-tip">
  73. 当前杠杆倍数最高可持有头寸 1,800,000,000 USDT
  74. </div>
  75. </div>
  76. <div class="form-group">
  77. <div v-if="currentTab !== 4 && currentTab !== 5" class="input-row">
  78. <div class="input-container">
  79. <span class="placeholder" v-show="!form.entryPrice">开仓价格</span>
  80. <input type="number" v-model="form.entryPrice" />
  81. <div class="suffix">USDT <span class="tag-latest">最新</span></div>
  82. </div>
  83. </div>
  84. <template v-if="currentTab === 0">
  85. <div class="input-row">
  86. <div class="input-container">
  87. <span class="placeholder" v-show="!form.closePrice">平仓价格</span>
  88. <input type="number" v-model="form.closePrice" />
  89. <div class="suffix">USDT</div>
  90. </div>
  91. </div>
  92. <div class="input-row">
  93. <div class="input-container">
  94. <span class="placeholder" v-show="!form.amount">成交数量</span>
  95. <input type="number" v-model="form.amount" />
  96. <div class="suffix">BTC</div>
  97. </div>
  98. </div>
  99. </template>
  100. <template v-if="currentTab === 1">
  101. <div class="input-row">
  102. <div class="input-container">
  103. <span class="placeholder" v-show="!form.amount">成交数量</span>
  104. <input type="number" v-model="form.amount" />
  105. <div class="suffix">BTC</div>
  106. </div>
  107. </div>
  108. <div class="input-row">
  109. <div class="input-container">
  110. <span class="placeholder" v-show="!form.pnl">预期收益额</span>
  111. <input type="number" v-model="form.pnl" />
  112. <div class="suffix">USDT</div>
  113. </div>
  114. </div>
  115. </template>
  116. <template v-if="currentTab === 2">
  117. <div class="input-row">
  118. <div class="input-container">
  119. <span class="placeholder" v-show="!form.amount">开仓数量</span>
  120. <input type="number" v-model="form.amount" />
  121. <div class="suffix">BTC</div>
  122. </div>
  123. </div>
  124. <div class="input-row">
  125. <div class="input-container">
  126. <span class="placeholder" v-show="!form.margin">保证金</span>
  127. <input type="number" v-model="form.margin" />
  128. <div class="suffix">USDT</div>
  129. </div>
  130. </div>
  131. </template>
  132. <template v-if="currentTab === 3">
  133. <div class="input-row">
  134. <div class="input-container">
  135. <span class="placeholder" v-show="!form.balance">账户余额</span>
  136. <input type="number" v-model="form.balance" />
  137. <div class="suffix">USDT</div>
  138. </div>
  139. </div>
  140. </template>
  141. <template v-if="currentTab === 4">
  142. <div class="grid-header">
  143. <span>开仓价格(USDT)</span>
  144. <span>成交数量(BTC)</span>
  145. </div>
  146. <div class="dynamic-row" v-for="(item, idx) in avgPriceList" :key="idx">
  147. <div class="input-container half">
  148. <input type="number" v-model="item.price" placeholder="0.00" />
  149. </div>
  150. <div class="input-container half">
  151. <input type="number" v-model="item.amount" placeholder="0.00" />
  152. </div>
  153. <div style="display: flex; align-items: center;">
  154. <div class="remove-btn" @click="removeRow(idx)" v-if="avgPriceList.length > 1">
  155. <span>-</span>
  156. </div>
  157. </div>
  158. </div>
  159. <button class="add-position-btn" @click="addRow">增加仓位</button>
  160. </template>
  161. <template v-if="currentTab === 5">
  162. <div class="input-row">
  163. <div class="input-container">
  164. <span class="placeholder" v-show="!form.entryPrice">标记价格</span>
  165. <input type="number" v-model="form.entryPrice" />
  166. <div class="suffix">USDT <span class="tag-latest">最新</span></div>
  167. </div>
  168. </div>
  169. <div class="input-row">
  170. <div class="input-container">
  171. <span class="placeholder" v-show="!form.amount">持仓数量</span>
  172. <input type="number" v-model="form.amount" />
  173. <div class="suffix">BTC</div>
  174. </div>
  175. </div>
  176. </template>
  177. </div>
  178. <div class="result-section">
  179. <h3>计算结果</h3>
  180. <p class="sub-text">实际市场存在变动,计算价格仅供参考</p>
  181. <div v-if="currentTab === 0">
  182. <div class="res-row"><span>保证金</span><span>{{ result.margin }} USDT</span></div>
  183. <div class="res-row"><span>收益</span><span :class="resultClass">{{ result.pnl }} USDT</span></div>
  184. <div class="res-row"><span>收益率</span><span :class="resultClass">{{ result.roe }}%</span></div>
  185. </div>
  186. <div v-if="currentTab === 1">
  187. <div class="res-row"><span>目标价格</span><span>-- USDT</span></div>
  188. </div>
  189. <div v-if="currentTab === 2">
  190. <div class="res-row"><span>强平价格</span><span>-- USDT</span></div>
  191. </div>
  192. <div v-if="currentTab === 3">
  193. <div class="res-row"><span>可开</span><span>-- USDT</span></div>
  194. <div class="res-row"><span>可开</span><span>-- BTC</span></div>
  195. <p class="small-tip">此计算最大可开数量时,将不考虑您的开仓损失</p>
  196. </div>
  197. <div v-if="currentTab === 4">
  198. <div class="res-row"><span>开仓均价</span><span>-- USDT</span></div>
  199. </div>
  200. <div v-if="currentTab === 5">
  201. <div class="res-row"><span>资金费用</span><span>-- USDT</span></div>
  202. </div>
  203. </div>
  204. <div class="footer-btns">
  205. <button class="btn-reset" @click="reset">重置</button>
  206. <button class="btn-calc" :style="{ background: themeColor }" @click="calculate">计算</button>
  207. </div>
  208. <div>
  209. <ProfitAndLossPrompt
  210. v-model:visible="showModal"
  211. ></ProfitAndLossPrompt>
  212. </div>
  213. </div>
  214. </template>
  215. <script setup>
  216. import { Icon as VanIcon } from 'vant';
  217. import {ref, computed, defineAsyncComponent} from 'vue';
  218. // 请确保路径正确,指向刚才那个更新过的子组件
  219. const LeverageSlider = defineAsyncComponent(()=>import('./calculator/LeverageSlider.vue'));
  220. const ProfitAndLossPrompt = defineAsyncComponent(() => import('./calculator/FundingRateReminder.vue'));
  221. //资金费率提示
  222. const showModal = ref(false);
  223. // 状态定义
  224. const tabs = ['收益', '目标价格', '强平价格', '可开', '开仓均价', '资金费用'];
  225. const currentTab = ref(0);
  226. const direction = ref('long');
  227. const leverage = ref(10); // 默认从 10x 开始
  228. // ★ 固定阶梯数组 (10, 50, 100, 500, 1000)
  229. const leverageMarks = [10, 50, 100, 500, 1000];
  230. const form = ref({
  231. entryPrice: '',
  232. closePrice: '',
  233. amount: '',
  234. margin: '',
  235. balance: '',
  236. pnl: ''
  237. });
  238. const avgPriceList = ref([{ price: '', amount: '' }, { price: '', amount: '' }]);
  239. const result = ref({ margin: '--', pnl: '--', roe: '--' });
  240. // 计算属性
  241. const themeColor = computed(() => direction.value === 'long' ? '#2ebd85' : '#f6465d');
  242. const resultClass = computed(() => {
  243. if (parseFloat(result.value.pnl) > 0) return 'text-green';
  244. if (parseFloat(result.value.pnl) < 0) return 'text-red';
  245. return '';
  246. });
  247. // ★ 核心修改:步进式调整杠杆 (点击加减号时)
  248. const stepLeverage = (dir) => {
  249. // 1. 找到当前值在数组中的索引
  250. let currentIndex = leverageMarks.indexOf(leverage.value);
  251. // 如果当前值不在数组里(比如手动改成了20),就找一个最近的索引
  252. if (currentIndex === -1) {
  253. // 简单逻辑:默认归位到 0
  254. currentIndex = 0;
  255. }
  256. // 2. 计算下一个索引
  257. let nextIndex = currentIndex + dir;
  258. // 3. 边界限制
  259. if (nextIndex < 0) nextIndex = 0;
  260. if (nextIndex >= leverageMarks.length) nextIndex = leverageMarks.length - 1;
  261. // 4. 更新值
  262. leverage.value = leverageMarks[nextIndex];
  263. };
  264. // 简单计算逻辑 (Tab 0 收益)
  265. const calculate = () => {
  266. if (currentTab.value === 0) {
  267. const entry = parseFloat(form.value.entryPrice);
  268. const close = parseFloat(form.value.closePrice);
  269. const amt = parseFloat(form.value.amount);
  270. const lev = leverage.value;
  271. if (entry && close && amt && lev) {
  272. const margin = (entry * amt) / lev;
  273. let pnl = 0;
  274. if (direction.value === 'long') {
  275. pnl = (close - entry) * amt;
  276. } else {
  277. pnl = (entry - close) * amt;
  278. }
  279. const roe = (pnl / margin) * 100;
  280. result.value = {
  281. margin: margin.toFixed(2),
  282. pnl: pnl.toFixed(2),
  283. roe: roe.toFixed(2)
  284. };
  285. }
  286. }
  287. };
  288. // 其他辅助函数
  289. const addRow = () => avgPriceList.value.push({ price: '', amount: '' });
  290. const removeRow = (idx) => avgPriceList.value.splice(idx, 1);
  291. const reset = () => {
  292. form.value = { entryPrice: '', closePrice: '', amount: '', margin: '', balance: '', pnl: '' };
  293. result.value = { margin: '--', pnl: '--', roe: '--' };
  294. leverage.value = 10;
  295. };
  296. </script>
  297. <style scoped>
  298. * {
  299. box-sizing: border-box;
  300. }
  301. .calculator-page {
  302. font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
  303. background: #fff;
  304. min-height: 100vh;
  305. padding-bottom: 90px;
  306. color: #333;
  307. z-index: 200;
  308. position: fixed; /* 固定定位 */
  309. top: 0;
  310. left: 0;
  311. display: flex;
  312. flex-direction: column;
  313. width: 100%;
  314. }
  315. /* 导航 & 头部 */
  316. .nav-bar { display: flex; justify-content: space-between; align-items: center;
  317. height: 44px; padding: 0 16px; }
  318. .nav-left { font-size: 24px; cursor: pointer; }
  319. .nav-title { font-size: 18px; font-weight: 600; }
  320. .nav-right { width: 20px; }
  321. .header-info {
  322. display: flex;
  323. align-items: center;
  324. padding: 8px 12px;
  325. background-color: #fff;
  326. width: 100%;
  327. }
  328. .icon-wrapper {
  329. display: flex;
  330. align-items: center;
  331. justify-content: center;
  332. width: 24px;
  333. height: 24px;
  334. margin-bottom: 1px;
  335. }
  336. .pair-name {
  337. margin: 0;
  338. font-size: 18px;
  339. font-weight: bold;
  340. color: #333;
  341. line-height: 1;
  342. }
  343. /* Tabs */
  344. .tabs-wrapper { overflow-x: auto; scrollbar-width: none; margin-bottom: 15px; }
  345. .tabs-content { display: flex; justify-content: space-between; padding: 0 15px;
  346. border-bottom: 1px solid #f5f5f5; }
  347. .tab-item { white-space: nowrap; padding: 12px 0; font-size: 14px; color: #999; position: relative;
  348. cursor: pointer; }
  349. .tab-item.active { color: #111; font-weight: 600; }
  350. .active-bar { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 20px;
  351. height: 3px; background: #111; border-radius: 2px; }
  352. /* 模式选择 */
  353. .mode-selectors { display: flex; gap: 10px; padding: 0 16px; margin-bottom: 15px; }
  354. .select-box { flex: 1; background: #f5f5f5; height: 36px; display: flex; align-items: center;
  355. padding: 0 12px; border-radius: 4px; font-size: 14px; color: #333; }
  356. /* 做多做空 */
  357. .trade-direction { display: flex; gap: 10px; padding: 0 16px; margin-bottom: 20px; }
  358. .dir-btn { flex: 1; padding: 10px 0; border: none; border-radius: 4px; font-size: 14px;
  359. cursor: pointer; background: #f5f5f5; color: #999; transition: all 0.2s; }
  360. .dir-btn.long.active { background: #2ebd85; color: #fff; font-weight: 600; }
  361. .dir-btn.short.active { background: #f6465d; color: #fff; font-weight: 600; }
  362. /* 资金费率 */
  363. .funding-rate-display { text-align: center; padding: 20px 0; }
  364. .rate-value { font-size: 32px; color: #666; font-weight: 500; }
  365. .rate-label { font-size: 12px; color: #999; margin-top: 5px; }
  366. /* 杠杆区域 */
  367. .leverage-section { padding: 0 16px; margin-bottom: 20px; }
  368. .label { font-size: 12px; color: #666; margin-bottom: 8px; }
  369. /* 蓝色边框输入框 */
  370. .leverage-control {
  371. display: flex;
  372. align-items: center;
  373. background: #f7f8fa;
  374. border-radius: 6px;
  375. height: 44px;
  376. margin-bottom: 10px;
  377. overflow: hidden;
  378. }
  379. .ctrl-btn {
  380. width: 50px;
  381. height: 100%;
  382. border: none;
  383. background: transparent;
  384. font-size: 22px;
  385. color: #999;
  386. cursor: pointer;
  387. display: flex;
  388. align-items: center;
  389. justify-content: center;
  390. padding-bottom: 4px;
  391. }
  392. .leverage-val {
  393. flex: 1;
  394. text-align: center;
  395. font-size: 16px;
  396. font-weight: 500;
  397. color: #333;
  398. }
  399. .slider-area { padding: 0 10px; }
  400. .leverage-tip { font-size: 12px; color: #333; margin-top: 10px; }
  401. /* 表单 */
  402. .form-group { padding: 0 16px; }
  403. .input-row { margin-bottom: 12px; }
  404. .input-container { position: relative; display: flex; align-items: center; background: #f5f5f5;
  405. height: 44px; border-radius: 4px; padding: 0 12px; }
  406. .placeholder { position: absolute; color: #bbb; font-size: 14px; pointer-events: none; }
  407. .input-container input { flex: 1; border: none; background: transparent; outline: none;
  408. font-size: 14px; z-index: 1; height: 100%; width: 100%; }
  409. .suffix { font-size: 14px; color: #666; margin-left: 8px; z-index: 2; white-space: nowrap; }
  410. .tag-latest { color: #f6465d; margin-left: 4px; }
  411. /* 表格布局 */
  412. .grid-header { display: flex; font-size: 12px; color: #333; margin-bottom: 8px; }
  413. .grid-header span { flex: 1; }
  414. .dynamic-row { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
  415. .input-container.half { flex: 1; }
  416. .remove-btn { width: 24px; height: 24px; border: 1px solid #f6465d; color: #f6465d;
  417. border-radius: 50%; display: flex; align-items: center; justify-content: center;
  418. font-size: 18px; cursor: pointer; margin-left: 5px; }
  419. .remove-btn span { margin-bottom: 5px; }
  420. .add-position-btn { width: 100%; height: 44px; background: #f6465d; color: white;
  421. border: none; border-radius: 22px; margin-top: 10px; font-size: 14px; }
  422. /* 结果 */
  423. .result-section { padding: 20px 16px; }
  424. .result-section h3 { margin: 0 0 5px; font-size: 16px; font-weight: 600; }
  425. .sub-text { font-size: 12px; color: #999; margin-bottom: 15px; }
  426. .res-row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
  427. .res-row span:first-child { color: #666; }
  428. .res-row span:last-child { color: #333; font-weight: 500; }
  429. .text-green { color: #2ebd85; }
  430. .text-red { color: #f6465d; }
  431. .small-tip { font-size: 12px; color: #999; margin-top: 5px; }
  432. /* 底部按钮 */
  433. .footer-btns { position: fixed; bottom: 0; left: 0; width: 100%; background: #fff;
  434. padding: 10px 16px 20px; display: flex; gap: 15px; box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
  435. box-sizing: border-box; z-index: 100; }
  436. .btn-reset { flex: 1; height: 44px; border: none; background: #bbb; color: #fff;
  437. font-size: 16px; border-radius: 4px; }
  438. .btn-calc { flex: 2; height: 44px; border: none; color: #fff; font-size: 16px;
  439. border-radius: 4px; font-weight: 600; transition: background 0.3s; }
  440. </style>