index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <template>
  2. <div class="container">
  3. <div class="content">
  4. <div class="card-box">
  5. <div class="card-title">选择币种</div>
  6. <div class="card-input" @click="selectPop(1)">
  7. <van-image width="30px" height="30px" round :src="selecctList.logo"/>
  8. <div class="card-text">{{selecctList.name}}</div>
  9. <svg-icon style="width: 16px; height: 16px;" name="down" />
  10. </div>
  11. </div>
  12. <div class="card-box">
  13. <div class="card-title">收货地址
  14. <svg-icon style="width: 16px; height: 16px;" name="address" @click="goToAddress"/>
  15. </div>
  16. <div class="card-input" style="height:100%">
  17. <van-field
  18. type="textarea"
  19. v-model="walletAddress"
  20. placeholder="请输入收货地址"
  21. rows="1"
  22. :autosize="true"
  23. >
  24. <template #right-icon>
  25. <svg-icon style="width: 16px; height: 16px;" name="sm" />
  26. </template>
  27. </van-field>
  28. </div>
  29. </div>
  30. <div class="card-box">
  31. <div class="card-title">
  32. <div>转账数量</div>
  33. <div>可用:{{selecctList.balance}} {{ selecctList.name }}</div>
  34. </div>
  35. <div class="card-input">
  36. <van-field v-model="unitNum" placeholder="0.00" />
  37. <div class="card-input-right">
  38. <div>{{ selecctList.name }}</div>
  39. <div class="line"></div>
  40. <div class="all" @click="unitNum = selecctList.balance">全部</div>
  41. </div>
  42. </div>
  43. </div>
  44. <!-- <div class="card-box">
  45. <div class="card-title">网络费用</div>
  46. <div class="card-input" style="flex-direction: column;align-items: self-start;padding: 8px 17px;">
  47. <div>18.34344398867676764566000ACC</div>
  48. <div class="price">STT0.01</div>
  49. </div>
  50. </div> -->
  51. </div>
  52. <van-button class="footer-btn" type="primary" size="large" @click="selectPop(2)" :disabled="isDisabled">确认</van-button>
  53. <van-popup v-model:show="showWallet" position="bottom" round style="height:400px">
  54. <div class="pop-content" style="height:400px">
  55. <div class="pop-title">
  56. <svg-icon style="width: 24px; height: 24px;" name="left-arrow" @click="showWallet = false"/>
  57. <div class="title">{{selectType == 1?'选择币种':'交易详情'}}</div>
  58. </div>
  59. <div class="list-ul" v-if="selectType == 1">
  60. <div class="list-li" v-for="item in hotTokensList" @click="changeList(item)" :key="item.name">
  61. <div class="list-li-lf">
  62. <van-image width="42px" height="42px" round :src="item.logo"/>
  63. <div style="margin-left: 12px;">{{item.name}}</div>
  64. </div>
  65. <div class="list-li-ri">
  66. <div>{{item.balance}}</div>
  67. <div class="list-li-ri-num">${{item.money}}</div>
  68. </div>
  69. </div>
  70. </div>
  71. <div class="pop-detail" v-if="selectType == 2">
  72. <!-- <div class="pop-detail-title">-{{gasFee}} ACC</div> -->
  73. <div class="pop-detail-cell">
  74. <div class="cell-label">付款地址:</div>
  75. <div class="cell-text">{{walletStore.account}}</div>
  76. </div>
  77. <div class="pop-detail-cell">
  78. <div class="cell-label">收款地址:</div>
  79. <div class="cell-text">{{walletAddress}}</div>
  80. </div>
  81. <div class="pop-detail-cell">
  82. <div class="cell-label">矿工费:</div>
  83. <div class="cell-text">{{ gasFee }} ACC</div>
  84. </div>
  85. <div class="pop-btn">
  86. <van-button class="btn" type="primary" size="large" color="#4765DD" @click="confirm" :disabled="!gasFee || gasFee === '0.0000'">确认</van-button>
  87. </div>
  88. </div>
  89. </div>
  90. </van-popup>
  91. <van-popup v-model:show="showPassWord" :style="{ borderRadius:'25px' }">
  92. <div class="pop-content-password">
  93. <div class="pop-title-password">請輸入密碼</div>
  94. <van-field v-model="passWord" class="pop-input" type="password"/>
  95. <div class="pop-btn-password">
  96. <van-button type="default" class="btn-password cancel" @click="cancel">取消</van-button>
  97. <van-button type="default" class="btn-password confirm" @click="popConfirm" :disabled="isDisabledConfirm">確定</van-button>
  98. </div>
  99. </div>
  100. </van-popup>
  101. </div>
  102. </template>
  103. <script setup>
  104. import { useRouter,useRoute } from 'vue-router'
  105. import { useWalletStore } from "@/stores/modules/walletStore";
  106. import Web3 from "web3";
  107. import pubData from "@/utils/pub.json";
  108. import { showToast } from 'vant';
  109. import { showNotify } from 'vant';
  110. const router = useRouter();
  111. const route = useRoute();
  112. const walletStore = useWalletStore();
  113. const web3 = new Web3(walletStore.rpcUrl);
  114. const walletAddress = ref(route.query.address || '');
  115. const unitNum = ref('')
  116. const showWallet = ref(false)
  117. const showPassWord = ref(false)
  118. const selectType = ref('');
  119. let hotTokensList = walletStore.tokenList;
  120. let selecctList = walletStore.tokenList[0];
  121. const passWord = ref('')
  122. const gasFee = ref('');
  123. const isDisabledConfirm = ref(false)
  124. const isDisabled = computed(() => {
  125. const num = parseFloat(unitNum.value);
  126. return (
  127. !walletAddress.value || // 钱包地址为空
  128. !unitNum.value || // 输入为空
  129. isNaN(num) || num <= 0 || num >= selecctList.balance // 非法数值
  130. );
  131. });
  132. const goToAddress = () => {
  133. router.push('transferAddressManagement')
  134. }
  135. const selectPop = (type) => {
  136. showWallet.value = true;
  137. selectType.value = type;
  138. }
  139. watch(() => selectType.value, (val) => {
  140. if (val === 2) {
  141. estimateGasFee();
  142. }
  143. });
  144. const estimateGasFee = async () => {
  145. try {
  146. const from = walletStore.account;
  147. const to = walletAddress.value;
  148. const amount = unitNum.value;
  149. if (!from || !to || !amount) return;
  150. const isACC = selecctList.name === 'ACC';
  151. let tx;
  152. if (isACC) {
  153. // ① 直接转账
  154. tx = {
  155. from,
  156. to,
  157. value: web3.utils.toWei(amount.toString(), 'ether'),
  158. };
  159. } else {
  160. // ② ERC20 代币转账
  161. const contract = new web3.eth.Contract(pubData, selecctList.address);
  162. const value = web3.utils.toWei(amount.toString(), 'ether'); // 注意小数位
  163. tx = {
  164. from,
  165. to: selecctList.address,
  166. data: contract.methods.transfer(to, value).encodeABI(),
  167. };
  168. }
  169. const gasPrice = await web3.eth.getGasPrice(); // 单位 Wei
  170. const gasLimit = await web3.eth.estimateGas(tx); // 单位 Gas
  171. const feeWei = BigInt(gasPrice) * BigInt(gasLimit);
  172. gasFee.value = Number(web3.utils.fromWei(feeWei.toString(), 'ether')).toFixed(8);
  173. } catch (err) {
  174. console.error('预估矿工费失败:', err);
  175. gasFee.value = '0.0000';
  176. }
  177. };
  178. const changeList = (item) => {
  179. selecctList = item;
  180. showWallet.value = false;
  181. unitNum.value = ''
  182. }
  183. // 确认
  184. const confirm = async () => {
  185. showWallet.value = false;
  186. showPassWord.value = true;
  187. };
  188. // 密码取消
  189. const cancel = () => {
  190. passWord.value = '';
  191. showPassWord.value = false;
  192. }
  193. // 密码确认
  194. const popConfirm = () => {
  195. if(passWord.value != walletStore.accountPassword){
  196. showNotify({ type: 'warning', message: '请输入正确的密码' });
  197. }else{
  198. getData();
  199. }
  200. }
  201. // 请求链
  202. const getData = async () => {
  203. isDisabledConfirm.value = true;
  204. try {
  205. let receipt;
  206. if (selecctList.name === 'ACC') {
  207. // ETH 转账
  208. receipt = await sendETH(
  209. walletStore.privateKey,
  210. walletAddress.value,
  211. unitNum.value
  212. );
  213. } else {
  214. // ERC‑20 代币转账
  215. receipt = await sendToken(
  216. walletStore.privateKey,
  217. selecctList.address,
  218. walletAddress.value,
  219. unitNum.value
  220. );
  221. }
  222. showToast('转账成功');
  223. router.push('/wallet');
  224. } catch (err) {
  225. showToast('转账失败');
  226. } finally {
  227. showWallet.value = false;
  228. isDisabledConfirm.value = false;
  229. }
  230. }
  231. // ========= ETH 转账 =========
  232. const sendETH = async (privateKey, toAddress, amountInEther) => {
  233. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  234. const from = account.address;
  235. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  236. const gasPrice = await web3.eth.getGasPrice();
  237. const tx = {
  238. from,
  239. to: toAddress,
  240. value: web3.utils.toWei(amountInEther.toString(), 'ether'),
  241. gas: 21000,
  242. gasPrice,
  243. nonce,
  244. chainId: await web3.eth.getChainId(),
  245. };
  246. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  247. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  248. };
  249. // ========= ERC‑20 代币转账 =========
  250. const sendToken = async (privateKey, tokenAddress, toAddress, amount) => {
  251. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  252. const from = account.address;
  253. const contract = new web3.eth.Contract(pubData, tokenAddress);
  254. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  255. const gasPrice = await web3.eth.getGasPrice();
  256. const value = web3.utils.toWei(amount.toString(), 'ether');
  257. const txData = contract.methods.transfer(toAddress, value).encodeABI();
  258. const tx = {
  259. from,
  260. to: tokenAddress,
  261. data: txData,
  262. gas: 100000,
  263. gasPrice,
  264. nonce,
  265. chainId: await web3.eth.getChainId(),
  266. };
  267. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  268. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  269. };
  270. </script>
  271. <style lang="less" scoped>
  272. .container{
  273. display: flex;
  274. flex-direction: column;
  275. height: calc(100vh - 44px);
  276. padding: 17px 17px 33px;
  277. box-sizing: border-box;
  278. .content{
  279. flex: 1;
  280. .card-box{
  281. margin-bottom: 25px;
  282. .card-title{
  283. font-family: PingFang SC, PingFang SC;
  284. font-weight: 400;
  285. font-size: 12px;
  286. color: #8D8D8D;
  287. margin-bottom: 6px;
  288. display: flex;
  289. align-items: center;
  290. justify-content: space-between;
  291. }
  292. .card-input{
  293. background: #F2F2F2;
  294. border-radius: 8px;
  295. border: 1px solid #D8D8D8;
  296. display: flex;
  297. align-items: center;
  298. padding: 18px 17px;
  299. height: 56px;
  300. box-sizing: border-box;
  301. font-family: PingFang SC, PingFang SC;
  302. font-weight: 400;
  303. font-size: 15px;
  304. color: #000000;
  305. .card-text{
  306. margin: 0 5px 0 8px;
  307. }
  308. .card-input-right{
  309. display: flex;
  310. align-items: center;
  311. flex-shrink: 0;
  312. font-family: PingFang SC, PingFang SC;
  313. font-weight: 400;
  314. font-size: 15px;
  315. color: #000000;
  316. margin-left: 17px;
  317. .line{
  318. height: 25px;
  319. border: 1px solid #D8D8D8;
  320. width: 0;
  321. margin: 0 17px;
  322. }
  323. .all{
  324. font-weight: 500;
  325. color: #4765DD;
  326. }
  327. }
  328. .price{
  329. font-size: 12px;
  330. color: #8D8D8D;
  331. margin-top: 1px;
  332. }
  333. :deep(.van-cell) {
  334. background:#F2F2F2 !important;
  335. padding: 0 !important;
  336. }
  337. :deep(.van-field__control) {
  338. font-family: PingFang SC, PingFang SC;
  339. font-weight: 400;
  340. font-size: 15px;
  341. color: #000000;
  342. }
  343. }
  344. }
  345. }
  346. .footer-btn{
  347. height: 40px !important;
  348. line-height: 40px !important;
  349. background: linear-gradient( 90deg, #4765DD 0%, #40A4FB 100%) !important;
  350. border-radius:28px;
  351. font-family: PingFang SC, PingFang SC;
  352. font-weight: 500;
  353. font-size: 15px;
  354. padding: 9px 0;
  355. box-sizing: border-box;
  356. color: #FFFFFF;
  357. }
  358. .pop-content{
  359. display: flex;
  360. flex-direction: column;
  361. .pop-title{
  362. padding: 17px;
  363. border-bottom: 1px solid #F2F2F2;
  364. display: flex;
  365. align-items: center;
  366. font-family: PingFang SC, PingFang SC;
  367. font-weight: 500;
  368. font-size: 17px;
  369. color: #000000;
  370. .title{
  371. flex: 1;
  372. display: flex;
  373. justify-content: center;
  374. }
  375. }
  376. .list-ul{
  377. flex: 1;
  378. display: flex;
  379. flex-direction: column;
  380. overflow: auto;
  381. margin: 16px;
  382. .list-li{
  383. display: flex;
  384. align-items: center;
  385. justify-content: space-between;
  386. font-family: PingFang SC, PingFang SC;
  387. font-weight: 500;
  388. margin-bottom: 16px;
  389. .list-li-lf{
  390. display: flex;
  391. align-items: center;
  392. font-size: 15px;
  393. color: #000000;
  394. }
  395. .list-li-ri{
  396. font-size: 15px;
  397. color: @font-color2;
  398. text-align: right;
  399. .list-li-ri-num{
  400. margin-top: 3px;
  401. font-weight: 400;
  402. font-size: 12px;
  403. color: #8D8D8D;
  404. }
  405. }
  406. }
  407. .list-li:last-child{
  408. margin-bottom: 0;
  409. }
  410. }
  411. .list-ul::-webkit-scrollbar{
  412. width: 0;
  413. }
  414. .pop-detail{
  415. padding: 17px 17px 0;
  416. .pop-detail-title{
  417. font-family: PingFang SC, PingFang SC;
  418. font-weight: 500;
  419. font-size: 17px;
  420. color: #000000;
  421. margin-bottom: 20px;
  422. text-align: center;
  423. }
  424. .pop-detail-cell{
  425. display: flex;
  426. font-family: PingFang SC, PingFang SC;
  427. font-weight: 400;
  428. font-size: 15px;
  429. color: #4F4F4F;
  430. padding: 10px 0;
  431. border-bottom: 1px solid #F2F2F2;
  432. .cell-label{
  433. width: 76px;
  434. flex-shrink: 0;
  435. }
  436. .cell-text{
  437. word-break: break-word;
  438. }
  439. }
  440. .pop-detail-cell:last-child{
  441. border-bottom: 0;
  442. }
  443. .pop-btn{
  444. margin-top: 53px;
  445. .btn{
  446. height: 40px;
  447. line-height: 40px;
  448. border-radius: 50px;
  449. font-family: PingFang SC, PingFang SC;
  450. font-weight: 500;
  451. font-size: 15px;
  452. color: #FFFFFF;
  453. }
  454. }
  455. }
  456. }
  457. }
  458. .pop-content-password{
  459. padding: 27px 35px 25px 34px;
  460. .pop-title-password{
  461. font-family: PingFang SC, PingFang SC;
  462. font-weight: 500;
  463. font-size: 17px;
  464. color: #000000;
  465. text-align: center;
  466. }
  467. .pop-input{
  468. background: #F2F2F2;
  469. border-radius: 8px;
  470. height: 40px;
  471. margin: 21px 0 31px;
  472. }
  473. .pop-btn-password{
  474. display: flex;
  475. justify-content: center;
  476. .btn-password{
  477. width: 83px;
  478. height: 29px;
  479. line-height: 29px;
  480. padding: 5px 0 !important;
  481. border-radius: 6px;
  482. font-family: PingFang SC, PingFang SC;
  483. font-weight: 400;
  484. font-size: 15px;
  485. box-sizing:border-box;
  486. }
  487. .cancel{
  488. margin-right: 17px !important;
  489. border: 1px solid #D8D8D8;
  490. color: #000 !important;
  491. }
  492. .confirm{
  493. background: @theme-color1;
  494. color: #FFF;
  495. font-weight: 500;
  496. }
  497. }
  498. }
  499. :deep(.van-popup--center) {
  500. margin: 0 40px !important;
  501. width: auto !important;
  502. }
  503. </style>