index.vue 18 KB

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