Calculator.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  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. {{ tab }}
  27. <div class="active-bar" v-if="currentTab === index"></div>
  28. </div>
  29. </div>
  30. </div>
  31. <div v-if="currentTab === 2" class="mode-selectors">
  32. <div class="select-box">全仓 ▾</div>
  33. <div class="select-box">单向持仓</div>
  34. </div>
  35. <div class="trade-direction">
  36. <button
  37. class="dir-btn long"
  38. :class="{ active: direction === 'long' }"
  39. @click="direction = 'long'">
  40. 买入
  41. </button>
  42. <button
  43. class="dir-btn short"
  44. :class="{ active: direction === 'short' }"
  45. @click="direction = 'short'">
  46. 卖出
  47. </button>
  48. </div>
  49. <div v-if="currentTab === 5" class="funding-rate-display">
  50. <div class="rate-value">0.0030%</div>
  51. <div class="rate-label" @click="hyu">
  52. 当前资金费率
  53. <VanIcon name="question-o" />
  54. </div>
  55. </div>
  56. <div v-if="currentTab !== 4 && currentTab !== 5" class="leverage-section">
  57. <div class="label">倍数</div>
  58. <div class="leverage-control">
  59. <button class="ctrl-btn" @click="stepLeverage(-1)">-</button>
  60. <div class="leverage-val">{{ leverage }}x</div>
  61. <button class="ctrl-btn" @click="stepLeverage(1)">+</button>
  62. </div>
  63. <div class="slider-area">
  64. <LeverageSlider v-model="leverage" :marks="leverageMarks" :color="themeColor" />
  65. </div>
  66. <div class="leverage-tip">当前杠杆倍数最高可持有头寸 1,800,000,000 USDT</div>
  67. </div>
  68. <div class="form-group">
  69. <div v-if="currentTab !== 4 && currentTab !== 5" class="input-row">
  70. <div class="input-container">
  71. <span class="placeholder" v-show="!form.entryPrice">开仓价格</span>
  72. <input type="number" v-model="form.entryPrice" />
  73. <div class="suffix">USDT <span class="tag-latest">最新</span></div>
  74. </div>
  75. </div>
  76. <template v-if="currentTab === 0">
  77. <div class="input-row">
  78. <div class="input-container">
  79. <span class="placeholder" v-show="!form.closePrice">平仓价格</span>
  80. <input type="number" v-model="form.closePrice" />
  81. <div class="suffix">USDT</div>
  82. </div>
  83. </div>
  84. <div class="input-row">
  85. <div class="input-container">
  86. <span class="placeholder" v-show="!form.amount">成交数量</span>
  87. <input type="number" v-model="form.amount" />
  88. <div class="suffix">BTC</div>
  89. </div>
  90. </div>
  91. </template>
  92. <template v-if="currentTab === 1">
  93. <div class="input-row">
  94. <div class="input-container">
  95. <span class="placeholder" v-show="!form.amount">成交数量</span>
  96. <input type="number" v-model="form.amount" />
  97. <div class="suffix">BTC</div>
  98. </div>
  99. </div>
  100. <div class="input-row">
  101. <div class="input-container">
  102. <span class="placeholder" v-show="!form.pnl">预期收益额</span>
  103. <input type="number" v-model="form.pnl" />
  104. <div class="suffix">USDT</div>
  105. </div>
  106. </div>
  107. </template>
  108. <template v-if="currentTab === 2">
  109. <div class="input-row">
  110. <div class="input-container">
  111. <span class="placeholder" v-show="!form.amount">开仓数量</span>
  112. <input type="number" v-model="form.amount" />
  113. <div class="suffix">BTC</div>
  114. </div>
  115. </div>
  116. <div class="input-row">
  117. <div class="input-container">
  118. <span class="placeholder" v-show="!form.margin">保证金</span>
  119. <input type="number" v-model="form.margin" />
  120. <div class="suffix">USDT</div>
  121. </div>
  122. </div>
  123. </template>
  124. <template v-if="currentTab === 3">
  125. <div class="input-row">
  126. <div class="input-container">
  127. <span class="placeholder" v-show="!form.balance">账户余额</span>
  128. <input type="number" v-model="form.balance" />
  129. <div class="suffix">USDT</div>
  130. </div>
  131. </div>
  132. </template>
  133. <template v-if="currentTab === 4">
  134. <div class="grid-header">
  135. <span>开仓价格(USDT)</span>
  136. <span>成交数量(BTC)</span>
  137. </div>
  138. <div class="dynamic-row" v-for="(item, idx) in avgPriceList" :key="idx">
  139. <div class="input-container half">
  140. <input type="number" v-model="item.price" placeholder="0.00" />
  141. </div>
  142. <div class="input-container half">
  143. <input type="number" v-model="item.amount" placeholder="0.00" />
  144. </div>
  145. <div style="display: flex; align-items: center">
  146. <div
  147. class="remove-btn"
  148. @click="removeRow(idx)"
  149. v-if="avgPriceList.length > 1">
  150. <span>-</span>
  151. </div>
  152. </div>
  153. </div>
  154. <button class="add-position-btn" @click="addRow">增加仓位</button>
  155. </template>
  156. <template v-if="currentTab === 5">
  157. <div class="input-row">
  158. <div class="input-container">
  159. <span class="placeholder" v-show="!form.entryPrice">标记价格</span>
  160. <input type="number" v-model="form.entryPrice" />
  161. <div class="suffix">USDT <span class="tag-latest">最新</span></div>
  162. </div>
  163. </div>
  164. <div class="input-row">
  165. <div class="input-container">
  166. <span class="placeholder" v-show="!form.amount">持仓数量</span>
  167. <input type="number" v-model="form.amount" />
  168. <div class="suffix">BTC</div>
  169. </div>
  170. </div>
  171. </template>
  172. </div>
  173. <div class="result-section">
  174. <h3>计算结果</h3>
  175. <p class="sub-text">实际市场存在变动,计算价格仅供参考</p>
  176. <div v-if="currentTab === 0">
  177. <div class="res-row">
  178. <span>保证金</span><span>{{ result.margin }} USDT</span>
  179. </div>
  180. <div class="res-row">
  181. <span>收益</span><span :class="resultClass">{{ result.pnl }} USDT</span>
  182. </div>
  183. <div class="res-row">
  184. <span>收益率</span><span :class="resultClass">{{ result.roe }}%</span>
  185. </div>
  186. </div>
  187. <div v-if="currentTab === 1">
  188. <div class="res-row"><span>目标价格</span><span>-- USDT</span></div>
  189. </div>
  190. <div v-if="currentTab === 2">
  191. <div class="res-row"><span>强平价格</span><span>-- USDT</span></div>
  192. </div>
  193. <div v-if="currentTab === 3">
  194. <div class="res-row"><span>可开</span><span>-- USDT</span></div>
  195. <div class="res-row"><span>可开</span><span>-- BTC</span></div>
  196. <p class="small-tip">此计算最大可开数量时,将不考虑您的开仓损失</p>
  197. </div>
  198. <div v-if="currentTab === 4">
  199. <div class="res-row"><span>开仓均价</span><span>-- USDT</span></div>
  200. </div>
  201. <div v-if="currentTab === 5">
  202. <div class="res-row"><span>资金费用</span><span>-- USDT</span></div>
  203. </div>
  204. </div>
  205. <div class="footer-btns">
  206. <button class="btn-reset" @click="reset">重置</button>
  207. <button class="btn-calc" :style="{ background: themeColor }" @click="calculate">
  208. 计算
  209. </button>
  210. </div>
  211. <div>
  212. <ProfitAndLossPrompt v-model:visible="showModal"></ProfitAndLossPrompt>
  213. </div>
  214. </div>
  215. </template>
  216. <script setup>
  217. import { Icon as VanIcon } from "vant";
  218. import { ref, computed, defineAsyncComponent } from "vue";
  219. // 请确保路径正确,指向刚才那个更新过的子组件
  220. const LeverageSlider = defineAsyncComponent(() =>
  221. import("./calculator/LeverageSlider.vue")
  222. );
  223. const ProfitAndLossPrompt = defineAsyncComponent(() =>
  224. import("./calculator/FundingRateReminder.vue")
  225. );
  226. //资金费率提示
  227. const showModal = ref(false);
  228. const hyu = () => {
  229. console.log("kkkkkkk");
  230. showModal.value = true;
  231. };
  232. // 状态定义
  233. const tabs = ["收益", "目标价格", "强平价格", "可开", "开仓均价", "资金费用"];
  234. const currentTab = ref(0);
  235. const direction = ref("long");
  236. const leverage = ref(10); // 默认从 10x 开始
  237. // ★ 固定阶梯数组 (10, 50, 100, 500, 1000)
  238. const leverageMarks = [10, 50, 100, 500, 1000];
  239. const form = ref({
  240. entryPrice: "",
  241. closePrice: "",
  242. amount: "",
  243. margin: "",
  244. balance: "",
  245. pnl: "",
  246. });
  247. const avgPriceList = ref([
  248. { price: "", amount: "" },
  249. { price: "", amount: "" },
  250. ]);
  251. const result = ref({ margin: "--", pnl: "--", roe: "--" });
  252. // 计算属性
  253. const themeColor = computed(() => (direction.value === "long" ? "#2ebd85" : "#f6465d"));
  254. const resultClass = computed(() => {
  255. if (parseFloat(result.value.pnl) > 0) return "text-green";
  256. if (parseFloat(result.value.pnl) < 0) return "text-red";
  257. return "";
  258. });
  259. // ★ 核心修改:步进式调整杠杆 (点击加减号时)
  260. const stepLeverage = (dir) => {
  261. // 1. 找到当前值在数组中的索引
  262. let currentIndex = leverageMarks.indexOf(leverage.value);
  263. // 如果当前值不在数组里(比如手动改成了20),就找一个最近的索引
  264. if (currentIndex === -1) {
  265. // 简单逻辑:默认归位到 0
  266. currentIndex = 0;
  267. }
  268. // 2. 计算下一个索引
  269. let nextIndex = currentIndex + dir;
  270. // 3. 边界限制
  271. if (nextIndex < 0) nextIndex = 0;
  272. if (nextIndex >= leverageMarks.length) nextIndex = leverageMarks.length - 1;
  273. // 4. 更新值
  274. leverage.value = leverageMarks[nextIndex];
  275. };
  276. // 简单计算逻辑 (Tab 0 收益)
  277. const calculate = () => {
  278. if (currentTab.value === 0) {
  279. const entry = parseFloat(form.value.entryPrice);
  280. const close = parseFloat(form.value.closePrice);
  281. const amt = parseFloat(form.value.amount);
  282. const lev = leverage.value;
  283. if (entry && close && amt && lev) {
  284. const margin = (entry * amt) / lev;
  285. let pnl = 0;
  286. if (direction.value === "long") {
  287. pnl = (close - entry) * amt;
  288. } else {
  289. pnl = (entry - close) * amt;
  290. }
  291. const roe = (pnl / margin) * 100;
  292. result.value = {
  293. margin: margin.toFixed(2),
  294. pnl: pnl.toFixed(2),
  295. roe: roe.toFixed(2),
  296. };
  297. }
  298. }
  299. };
  300. // 其他辅助函数
  301. const addRow = () => avgPriceList.value.push({ price: "", amount: "" });
  302. const removeRow = (idx) => avgPriceList.value.splice(idx, 1);
  303. const reset = () => {
  304. form.value = {
  305. entryPrice: "",
  306. closePrice: "",
  307. amount: "",
  308. margin: "",
  309. balance: "",
  310. pnl: "",
  311. };
  312. result.value = { margin: "--", pnl: "--", roe: "--" };
  313. leverage.value = 10;
  314. };
  315. </script>
  316. <style scoped>
  317. * {
  318. box-sizing: border-box;
  319. }
  320. .calculator-page {
  321. font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
  322. background: #fff;
  323. min-height: 100vh;
  324. /* 这里的 padding-bottom: 90px 很重要,为了防止底部内容被固定的 footer 遮挡,保留它 */
  325. padding-bottom: 90px;
  326. color: #333;
  327. z-index: 200;
  328. /* 删掉了 position: fixed/top/left */
  329. display: flex;
  330. flex-direction: column;
  331. width: 100%;
  332. }
  333. /* 导航 & 头部 */
  334. .nav-bar {
  335. display: flex;
  336. justify-content: space-between;
  337. align-items: center;
  338. height: 44px;
  339. padding: 0 16px;
  340. /* 新增以下 3 行,实现吸顶效果 */
  341. position: sticky;
  342. top: 0;
  343. background: #fff; /* 防止透明背景导致内容重叠 */
  344. z-index: 100;
  345. }
  346. .nav-left {
  347. font-size: 24px;
  348. cursor: pointer;
  349. }
  350. .nav-title {
  351. font-size: 18px;
  352. font-weight: 600;
  353. }
  354. .nav-right {
  355. width: 20px;
  356. }
  357. .header-info {
  358. display: flex;
  359. align-items: center;
  360. padding: 0px 15px;
  361. background-color: #fff;
  362. width: 100%;
  363. }
  364. .icon-wrapper {
  365. display: flex;
  366. align-items: center;
  367. justify-content: center;
  368. width: 24px;
  369. height: 24px;
  370. margin-bottom: 1px;
  371. }
  372. .pair-name {
  373. margin: 0;
  374. font-size: 18px;
  375. font-weight: bold;
  376. color: #333;
  377. line-height: 1;
  378. }
  379. /* Tabs */
  380. .tabs-wrapper {
  381. overflow-x: auto;
  382. scrollbar-width: none;
  383. margin-bottom: 15px;
  384. }
  385. .tabs-content {
  386. display: flex;
  387. justify-content: space-between;
  388. padding: 0 15px;
  389. border-bottom: 1px solid #f5f5f5;
  390. }
  391. .tab-item {
  392. white-space: nowrap;
  393. padding: 12px 0;
  394. font-size: 14px;
  395. color: #999;
  396. position: relative;
  397. cursor: pointer;
  398. }
  399. .tab-item.active {
  400. color: #111;
  401. font-weight: 600;
  402. }
  403. .active-bar {
  404. position: absolute;
  405. bottom: 0;
  406. left: 50%;
  407. transform: translateX(-50%);
  408. width: 20px;
  409. height: 3px;
  410. background: #111;
  411. border-radius: 2px;
  412. }
  413. /* 模式选择 */
  414. .mode-selectors {
  415. display: flex;
  416. gap: 10px;
  417. padding: 0 16px;
  418. margin-bottom: 15px;
  419. }
  420. .select-box {
  421. flex: 1;
  422. background: #f5f5f5;
  423. height: 36px;
  424. display: flex;
  425. align-items: center;
  426. padding: 0 12px;
  427. border-radius: 4px;
  428. font-size: 14px;
  429. color: #333;
  430. }
  431. /* 做多做空 */
  432. .trade-direction {
  433. display: flex;
  434. gap: 10px;
  435. padding: 0 16px;
  436. margin-bottom: 20px;
  437. }
  438. .dir-btn {
  439. flex: 1;
  440. padding: 10px 0;
  441. border: none;
  442. border-radius: 4px;
  443. font-size: 14px;
  444. cursor: pointer;
  445. background: #f5f5f5;
  446. color: #999;
  447. transition: all 0.2s;
  448. }
  449. .dir-btn.long.active {
  450. background: #2ebd85;
  451. color: #fff;
  452. font-weight: 600;
  453. }
  454. .dir-btn.short.active {
  455. background: #f6465d;
  456. color: #fff;
  457. font-weight: 600;
  458. }
  459. /* 资金费率 */
  460. .funding-rate-display {
  461. text-align: center;
  462. padding: 20px 0;
  463. }
  464. .rate-value {
  465. font-size: 32px;
  466. color: #666;
  467. font-weight: 500;
  468. }
  469. .rate-label {
  470. font-size: 12px;
  471. color: #999;
  472. margin-top: 5px;
  473. }
  474. /* 杠杆区域 */
  475. .leverage-section {
  476. padding: 0 16px;
  477. margin-bottom: 20px;
  478. }
  479. .label {
  480. font-size: 12px;
  481. color: #666;
  482. margin-bottom: 8px;
  483. }
  484. /* 蓝色边框输入框 */
  485. .leverage-control {
  486. display: flex;
  487. align-items: center;
  488. background: #f7f8fa;
  489. border-radius: 6px;
  490. height: 44px;
  491. margin-bottom: 10px;
  492. overflow: hidden;
  493. }
  494. .ctrl-btn {
  495. width: 50px;
  496. height: 100%;
  497. border: none;
  498. background: transparent;
  499. font-size: 22px;
  500. color: #999;
  501. cursor: pointer;
  502. display: flex;
  503. align-items: center;
  504. justify-content: center;
  505. padding-bottom: 4px;
  506. }
  507. .leverage-val {
  508. flex: 1;
  509. text-align: center;
  510. font-size: 16px;
  511. font-weight: 500;
  512. color: #333;
  513. }
  514. .slider-area {
  515. padding: 0 10px;
  516. }
  517. .leverage-tip {
  518. font-size: 12px;
  519. color: #333;
  520. margin-top: 10px;
  521. }
  522. /* 表单 */
  523. .form-group {
  524. padding: 0 16px;
  525. }
  526. .input-row {
  527. margin-bottom: 12px;
  528. }
  529. .input-container {
  530. position: relative;
  531. display: flex;
  532. align-items: center;
  533. background: #f5f5f5;
  534. height: 44px;
  535. border-radius: 4px;
  536. padding: 0 12px;
  537. }
  538. .placeholder {
  539. position: absolute;
  540. color: #bbb;
  541. font-size: 14px;
  542. pointer-events: none;
  543. }
  544. .input-container input {
  545. flex: 1;
  546. border: none;
  547. background: transparent;
  548. outline: none;
  549. font-size: 14px;
  550. z-index: 1;
  551. height: 100%;
  552. width: 100%;
  553. }
  554. .suffix {
  555. font-size: 14px;
  556. color: #666;
  557. margin-left: 8px;
  558. z-index: 2;
  559. white-space: nowrap;
  560. }
  561. .tag-latest {
  562. color: #f6465d;
  563. margin-left: 4px;
  564. }
  565. /* 表格布局 */
  566. .grid-header {
  567. display: flex;
  568. font-size: 12px;
  569. color: #333;
  570. margin-bottom: 8px;
  571. }
  572. .grid-header span {
  573. flex: 1;
  574. }
  575. .dynamic-row {
  576. display: flex;
  577. gap: 10px;
  578. margin-bottom: 10px;
  579. align-items: center;
  580. }
  581. .input-container.half {
  582. flex: 1;
  583. }
  584. .remove-btn {
  585. width: 24px;
  586. height: 24px;
  587. border: 1px solid #f6465d;
  588. color: #f6465d;
  589. border-radius: 50%;
  590. display: flex;
  591. align-items: center;
  592. justify-content: center;
  593. font-size: 18px;
  594. cursor: pointer;
  595. margin-left: 5px;
  596. }
  597. .remove-btn span {
  598. margin-bottom: 5px;
  599. }
  600. .add-position-btn {
  601. width: 100%;
  602. height: 44px;
  603. background: #f6465d;
  604. color: white;
  605. border: none;
  606. border-radius: 22px;
  607. margin-top: 10px;
  608. font-size: 14px;
  609. }
  610. /* 结果 */
  611. .result-section {
  612. padding: 20px 16px;
  613. }
  614. .result-section h3 {
  615. margin: 0 0 5px;
  616. font-size: 16px;
  617. font-weight: 600;
  618. }
  619. .sub-text {
  620. font-size: 12px;
  621. color: #999;
  622. margin-bottom: 15px;
  623. }
  624. .res-row {
  625. display: flex;
  626. justify-content: space-between;
  627. margin-bottom: 12px;
  628. font-size: 14px;
  629. }
  630. .res-row span:first-child {
  631. color: #666;
  632. }
  633. .res-row span:last-child {
  634. color: #333;
  635. font-weight: 500;
  636. }
  637. .text-green {
  638. color: #2ebd85;
  639. }
  640. .text-red {
  641. color: #f6465d;
  642. }
  643. .small-tip {
  644. font-size: 12px;
  645. color: #999;
  646. margin-top: 5px;
  647. }
  648. /* 底部按钮 */
  649. .footer-btns {
  650. position: fixed;
  651. bottom: 0;
  652. left: 0;
  653. width: 100%;
  654. background: #fff;
  655. padding: 10px 16px 20px;
  656. display: flex;
  657. gap: 15px;
  658. box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
  659. box-sizing: border-box;
  660. z-index: 100;
  661. }
  662. .btn-reset {
  663. flex: 1;
  664. height: 44px;
  665. border: none;
  666. background: #bbb;
  667. color: #fff;
  668. font-size: 16px;
  669. border-radius: 4px;
  670. }
  671. .btn-calc {
  672. flex: 2;
  673. height: 44px;
  674. border: none;
  675. color: #fff;
  676. font-size: 16px;
  677. border-radius: 4px;
  678. font-weight: 600;
  679. transition: background 0.3s;
  680. }
  681. </style>