index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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">確定</van-button>
  98. </div>
  99. </div>
  100. </van-popup>
  101. </div>
  102. </template>
  103. <script setup>
  104. import { useRouter } from 'vue-router'
  105. import { useWalletStore } from "@/stores/modules/walletStore";
  106. import { hotTokens } from '@/api/path/login.api'
  107. import Web3 from "web3";
  108. import pubData from "@/utils/pub.json";
  109. import { showLoadingToast, showToast } from 'vant';
  110. import { showNotify } from 'vant';
  111. const router = useRouter();
  112. const walletStore = useWalletStore();
  113. const web3 = new Web3(walletStore.rpcUrl);
  114. const walletAddress = ref('')
  115. const unitNum = ref('')
  116. const showWallet = ref(false)
  117. const showPassWord = ref(false)
  118. const selectType = ref('');
  119. const hotTokensList = ref([]);
  120. const selecctList = ref({})
  121. const passWord = ref('')
  122. const gasFee = ref('');
  123. const isDisabled = computed(() => {
  124. const num = parseFloat(unitNum.value);
  125. return (
  126. !walletAddress.value || // 钱包地址为空
  127. !unitNum.value || // 输入为空
  128. isNaN(num) || num <= 0 || num >= selecctList.value.balance // 非法数值
  129. );
  130. });
  131. // 获取代币信息
  132. const gethotTokens = async () => {
  133. const {data} = await hotTokens({chain: walletStore.accountName,address:walletStore.account});
  134. hotTokensList.value = data;
  135. selecctList.value = data[0];
  136. }
  137. const goToAddress = () => {
  138. router.push('addressManagement')
  139. }
  140. const selectPop = (type) => {
  141. showWallet.value = true;
  142. selectType.value = type;
  143. }
  144. watch(() => selectType.value, (val) => {
  145. if (val === 2) {
  146. estimateGasFee();
  147. }
  148. });
  149. const estimateGasFee = async () => {
  150. try {
  151. const from = walletStore.account;
  152. const to = walletAddress.value;
  153. const amount = unitNum.value;
  154. if (!from || !to || !amount) return;
  155. const isACC = selecctList.value.name === 'ACC';
  156. let tx;
  157. if (isACC) {
  158. // ① 直接转账
  159. tx = {
  160. from,
  161. to,
  162. value: web3.utils.toWei(amount.toString(), 'ether'),
  163. };
  164. } else {
  165. // ② ERC20 代币转账
  166. const contract = new web3.eth.Contract(pubData, selecctList.value.address);
  167. const value = web3.utils.toWei(amount.toString(), 'ether'); // 注意小数位
  168. tx = {
  169. from,
  170. to: selecctList.value.address,
  171. data: contract.methods.transfer(to, value).encodeABI(),
  172. };
  173. }
  174. const gasPrice = await web3.eth.getGasPrice(); // 单位 Wei
  175. const gasLimit = await web3.eth.estimateGas(tx); // 单位 Gas
  176. const feeWei = BigInt(gasPrice) * BigInt(gasLimit);
  177. gasFee.value = Number(web3.utils.fromWei(feeWei.toString(), 'ether')).toFixed(8);
  178. } catch (err) {
  179. console.error('预估矿工费失败:', err);
  180. gasFee.value = '0.0000';
  181. }
  182. };
  183. const changeList = (item) => {
  184. selecctList.value = item;
  185. showWallet.value = false;
  186. unitNum.value = ''
  187. }
  188. // 确认
  189. const confirm = async () => {
  190. showWallet.value = false;
  191. showPassWord.value = true;
  192. };
  193. // 密码取消
  194. const cancel = () => {
  195. passWord.value = '';
  196. showPassWord.value = false;
  197. }
  198. // 密码确认
  199. const popConfirm = () => {
  200. if(passWord.value != walletStore.accountPassword){
  201. showNotify({ type: 'warning', message: '请输入正确的密码' });
  202. }else{
  203. getData();
  204. }
  205. }
  206. // 请求链
  207. const getData = async () => {
  208. const loading = showLoadingToast({
  209. message: '转账中…',
  210. forbidClick: true,
  211. duration: 0,
  212. });
  213. try {
  214. let receipt;
  215. if (selecctList.value.name === 'ACC') {
  216. // ETH 转账
  217. receipt = await sendETH(
  218. walletStore.privateKey,
  219. walletAddress.value,
  220. unitNum.value
  221. );
  222. } else {
  223. // ERC‑20 代币转账
  224. receipt = await sendToken(
  225. walletStore.privateKey,
  226. selecctList.value.address,
  227. walletAddress.value,
  228. unitNum.value
  229. );
  230. }
  231. showToast({ message: '转账成功', type: 'success' });
  232. console.log('交易哈希:', receipt.transactionHash);
  233. router.push('/wallet');
  234. } catch (err) {
  235. console.error('⚠️ 转账失败:', err);
  236. const msg =
  237. err?.message?.replace('Returned error: ', '') ||
  238. '发生未知错误,请稍后重试';
  239. showToast({ message: `转账失败:${msg}`, type: 'fail' });
  240. } finally {
  241. showWallet.value = false;
  242. }
  243. }
  244. // ========= ETH 转账 =========
  245. const sendETH = async (privateKey, toAddress, amountInEther) => {
  246. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  247. const from = account.address;
  248. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  249. const gasPrice = await web3.eth.getGasPrice();
  250. const tx = {
  251. from,
  252. to: toAddress,
  253. value: web3.utils.toWei(amountInEther.toString(), 'ether'),
  254. gas: 21000,
  255. gasPrice,
  256. nonce,
  257. chainId: await web3.eth.getChainId(),
  258. };
  259. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  260. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  261. };
  262. // ========= ERC‑20 代币转账 =========
  263. const sendToken = async (privateKey, tokenAddress, toAddress, amount) => {
  264. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  265. const from = account.address;
  266. const contract = new web3.eth.Contract(pubData, tokenAddress);
  267. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  268. const gasPrice = await web3.eth.getGasPrice();
  269. const value = web3.utils.toWei(amount.toString(), 'ether');
  270. const txData = contract.methods.transfer(toAddress, value).encodeABI();
  271. const tx = {
  272. from,
  273. to: tokenAddress,
  274. data: txData,
  275. gas: 100000,
  276. gasPrice,
  277. nonce,
  278. chainId: await web3.eth.getChainId(),
  279. };
  280. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  281. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  282. };
  283. onMounted(async ()=>{
  284. gethotTokens();
  285. })
  286. </script>
  287. <style lang="less" scoped>
  288. .container{
  289. display: flex;
  290. flex-direction: column;
  291. height: calc(100vh - 44px);
  292. padding: 17px 17px 33px;
  293. box-sizing: border-box;
  294. .content{
  295. flex: 1;
  296. .card-box{
  297. margin-bottom: 25px;
  298. .card-title{
  299. font-family: PingFang SC, PingFang SC;
  300. font-weight: 400;
  301. font-size: 12px;
  302. color: #8D8D8D;
  303. margin-bottom: 6px;
  304. display: flex;
  305. align-items: center;
  306. justify-content: space-between;
  307. }
  308. .card-input{
  309. background: #F2F2F2;
  310. border-radius: 8px;
  311. border: 1px solid #D8D8D8;
  312. display: flex;
  313. align-items: center;
  314. padding: 18px 17px;
  315. height: 56px;
  316. box-sizing: border-box;
  317. font-family: PingFang SC, PingFang SC;
  318. font-weight: 400;
  319. font-size: 15px;
  320. color: #000000;
  321. .card-text{
  322. margin: 0 5px 0 8px;
  323. }
  324. .card-input-right{
  325. display: flex;
  326. align-items: center;
  327. flex-shrink: 0;
  328. font-family: PingFang SC, PingFang SC;
  329. font-weight: 400;
  330. font-size: 15px;
  331. color: #000000;
  332. margin-left: 17px;
  333. .line{
  334. height: 25px;
  335. border: 1px solid #D8D8D8;
  336. width: 0;
  337. margin: 0 17px;
  338. }
  339. .all{
  340. font-weight: 500;
  341. color: #4765DD;
  342. }
  343. }
  344. .price{
  345. font-size: 12px;
  346. color: #8D8D8D;
  347. margin-top: 1px;
  348. }
  349. :deep(.van-cell) {
  350. background:#F2F2F2 !important;
  351. padding: 0 !important;
  352. }
  353. :deep(.van-field__control) {
  354. font-family: PingFang SC, PingFang SC;
  355. font-weight: 400;
  356. font-size: 15px;
  357. color: #000000;
  358. }
  359. }
  360. }
  361. }
  362. .footer-btn{
  363. height: 40px !important;
  364. line-height: 40px !important;
  365. background: linear-gradient( 90deg, #4765DD 0%, #40A4FB 100%) !important;
  366. border-radius:28px;
  367. font-family: PingFang SC, PingFang SC;
  368. font-weight: 500;
  369. font-size: 15px;
  370. padding: 9px 0;
  371. box-sizing: border-box;
  372. color: #FFFFFF;
  373. }
  374. .pop-content{
  375. display: flex;
  376. flex-direction: column;
  377. .pop-title{
  378. padding: 17px;
  379. border-bottom: 1px solid #F2F2F2;
  380. display: flex;
  381. align-items: center;
  382. font-family: PingFang SC, PingFang SC;
  383. font-weight: 500;
  384. font-size: 17px;
  385. color: #000000;
  386. .title{
  387. flex: 1;
  388. display: flex;
  389. justify-content: center;
  390. }
  391. }
  392. .list-ul{
  393. flex: 1;
  394. display: flex;
  395. flex-direction: column;
  396. overflow: auto;
  397. margin: 16px;
  398. .list-li{
  399. display: flex;
  400. align-items: center;
  401. justify-content: space-between;
  402. font-family: PingFang SC, PingFang SC;
  403. font-weight: 500;
  404. margin-bottom: 16px;
  405. .list-li-lf{
  406. display: flex;
  407. align-items: center;
  408. font-size: 15px;
  409. color: #000000;
  410. }
  411. .list-li-ri{
  412. font-size: 15px;
  413. color: @font-color2;
  414. text-align: right;
  415. .list-li-ri-num{
  416. margin-top: 3px;
  417. font-weight: 400;
  418. font-size: 12px;
  419. color: #8D8D8D;
  420. }
  421. }
  422. }
  423. .list-li:last-child{
  424. margin-bottom: 0;
  425. }
  426. }
  427. .list-ul::-webkit-scrollbar{
  428. width: 0;
  429. }
  430. .pop-detail{
  431. padding: 17px 17px 0;
  432. .pop-detail-title{
  433. font-family: PingFang SC, PingFang SC;
  434. font-weight: 500;
  435. font-size: 17px;
  436. color: #000000;
  437. margin-bottom: 20px;
  438. text-align: center;
  439. }
  440. .pop-detail-cell{
  441. display: flex;
  442. font-family: PingFang SC, PingFang SC;
  443. font-weight: 400;
  444. font-size: 15px;
  445. color: #4F4F4F;
  446. padding: 10px 0;
  447. border-bottom: 1px solid #F2F2F2;
  448. .cell-label{
  449. width: 76px;
  450. flex-shrink: 0;
  451. }
  452. .cell-text{
  453. word-break: break-word;
  454. }
  455. }
  456. .pop-detail-cell:last-child{
  457. border-bottom: 0;
  458. }
  459. .pop-btn{
  460. margin-top: 53px;
  461. .btn{
  462. height: 40px;
  463. line-height: 40px;
  464. border-radius: 50px;
  465. font-family: PingFang SC, PingFang SC;
  466. font-weight: 500;
  467. font-size: 15px;
  468. color: #FFFFFF;
  469. }
  470. }
  471. }
  472. }
  473. }
  474. .pop-content-password{
  475. padding: 27px 35px 25px 34px;
  476. .pop-title-password{
  477. font-family: PingFang SC, PingFang SC;
  478. font-weight: 500;
  479. font-size: 17px;
  480. color: #000000;
  481. text-align: center;
  482. }
  483. .pop-input{
  484. background: #F2F2F2;
  485. border-radius: 8px;
  486. height: 40px;
  487. margin: 21px 0 31px;
  488. }
  489. .pop-btn-password{
  490. display: flex;
  491. justify-content: center;
  492. .btn-password{
  493. width: 83px;
  494. height: 29px;
  495. line-height: 29px;
  496. padding: 5px 0 !important;
  497. border-radius: 6px;
  498. font-family: PingFang SC, PingFang SC;
  499. font-weight: 400;
  500. font-size: 15px;
  501. box-sizing:border-box;
  502. }
  503. .cancel{
  504. margin-right: 17px !important;
  505. border: 1px solid #D8D8D8;
  506. color: #000 !important;
  507. }
  508. .confirm{
  509. background: @theme-color1;
  510. color: #FFF;
  511. font-weight: 500;
  512. }
  513. }
  514. }
  515. :deep(.van-popup--center) {
  516. margin: 0 40px !important;
  517. width: auto !important;
  518. }
  519. </style>