123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531 |
- <template>
- <div class="container">
- <div class="content">
- <div class="card-box">
- <div class="card-title">选择币种</div>
- <div class="card-input" @click="selectPop(1)">
- <van-image width="30px" height="30px" round :src="selecctList.logo"/>
- <div class="card-text">{{selecctList.name}}</div>
- <svg-icon style="width: 16px; height: 16px;" name="down" />
- </div>
- </div>
- <div class="card-box">
- <div class="card-title">收货地址
- <svg-icon style="width: 16px; height: 16px;" name="address" @click="goToAddress"/>
- </div>
- <div class="card-input" style="height:100%">
- <van-field
- type="textarea"
- v-model="walletAddress"
- placeholder="请输入收货地址"
- rows="1"
- :autosize="true"
- >
- <template #right-icon>
- <svg-icon style="width: 16px; height: 16px;" name="sm" />
- </template>
- </van-field>
- </div>
- </div>
- <div class="card-box">
- <div class="card-title">
- <div>转账数量</div>
- <div>可用:{{selecctList.balance}} {{ selecctList.name }}</div>
- </div>
- <div class="card-input">
- <van-field v-model="unitNum" placeholder="0.00" />
- <div class="card-input-right">
- <div>{{ selecctList.name }}</div>
- <div class="line"></div>
- <div class="all" @click="unitNum = selecctList.balance">全部</div>
- </div>
- </div>
- </div>
- <!-- <div class="card-box">
- <div class="card-title">网络费用</div>
- <div class="card-input" style="flex-direction: column;align-items: self-start;padding: 8px 17px;">
- <div>18.34344398867676764566000ACC</div>
- <div class="price">STT0.01</div>
- </div>
- </div> -->
- </div>
- <van-button class="footer-btn" type="primary" size="large" @click="selectPop(2)" :disabled="isDisabled">确认</van-button>
- <van-popup v-model:show="showWallet" position="bottom" round style="height:400px">
- <div class="pop-content" style="height:400px">
- <div class="pop-title">
- <svg-icon style="width: 24px; height: 24px;" name="left-arrow" @click="showWallet = false"/>
- <div class="title">{{selectType == 1?'选择币种':'交易详情'}}</div>
- </div>
- <div class="list-ul" v-if="selectType == 1">
- <div class="list-li" v-for="item in hotTokensList" @click="changeList(item)" :key="item.name">
- <div class="list-li-lf">
- <van-image width="42px" height="42px" round :src="item.logo"/>
- <div style="margin-left: 12px;">{{item.name}}</div>
- </div>
- <div class="list-li-ri">
- <div>{{item.balance}}</div>
- <div class="list-li-ri-num">${{item.money}}</div>
- </div>
- </div>
- </div>
- <div class="pop-detail" v-if="selectType == 2">
- <div class="pop-detail-title">-{{gasFee}} ACC</div>
- <div class="pop-detail-cell">
- <div class="cell-label">付款地址:</div>
- <div class="cell-text">{{walletStore.account}}</div>
- </div>
- <div class="pop-detail-cell">
- <div class="cell-label">收款地址:</div>
- <div class="cell-text">{{walletAddress}}</div>
- </div>
- <div class="pop-detail-cell">
- <div class="cell-label">矿工费:</div>
- <div class="cell-text">{{ gasFee }} ACC</div>
- </div>
- <div class="pop-btn">
- <van-button class="btn" type="primary" size="large" color="#4765DD" @click="confirm" :disabled="!gasFee || gasFee === '0.0000'">确认</van-button>
- </div>
- </div>
- </div>
- </van-popup>
- <van-popup v-model:show="showPassWord" :style="{ borderRadius:'25px' }">
- <div class="pop-content-password">
- <div class="pop-title-password">請輸入密碼</div>
- <van-field v-model="passWord" class="pop-input" type="password"/>
- <div class="pop-btn-password">
- <van-button type="default" class="btn-password cancel" @click="cancel">取消</van-button>
- <van-button type="default" class="btn-password confirm" @click="popConfirm">確定</van-button>
- </div>
- </div>
- </van-popup>
- </div>
- </template>
- <script setup>
- import { useRouter } from 'vue-router'
- import { useWalletStore } from "@/stores/modules/walletStore";
- import Web3 from "web3";
- import pubData from "@/utils/pub.json";
- import { showLoadingToast, showToast } from 'vant';
- import { showNotify } from 'vant';
- const router = useRouter();
- const walletStore = useWalletStore();
- const web3 = new Web3(walletStore.rpcUrl);
- const walletAddress = ref('')
- const unitNum = ref('')
- const showWallet = ref(false)
- const showPassWord = ref(false)
- const selectType = ref('');
- const hotTokensList = ref([]);
- const selecctList = ref({})
- const passWord = ref('')
- const gasFee = ref('');
- const isDisabled = computed(() => {
- const num = parseFloat(unitNum.value);
- return (
- !walletAddress.value || // 钱包地址为空
- !unitNum.value || // 输入为空
- isNaN(num) || num <= 0 || num >= selecctList.value.balance // 非法数值
- );
- });
- // 获取代币信息
- const gethotTokens = async () => {
- const data = await walletStore.updateTokenVal()
- hotTokensList.value = data;
- selecctList.value = data[0];
- }
- const goToAddress = () => {
- router.push('addressManagement')
- }
- const selectPop = (type) => {
- showWallet.value = true;
- selectType.value = type;
- }
- watch(() => selectType.value, (val) => {
- if (val === 2) {
- estimateGasFee();
- }
- });
- const estimateGasFee = async () => {
- try {
- const from = walletStore.account;
- const to = walletAddress.value;
- const amount = unitNum.value;
- if (!from || !to || !amount) return;
- const isACC = selecctList.value.name === 'ACC';
- let tx;
- if (isACC) {
- // ① 直接转账
- tx = {
- from,
- to,
- value: web3.utils.toWei(amount.toString(), 'ether'),
- };
- } else {
- // ② ERC20 代币转账
- const contract = new web3.eth.Contract(pubData, selecctList.value.address);
- const value = web3.utils.toWei(amount.toString(), 'ether'); // 注意小数位
- tx = {
- from,
- to: selecctList.value.address,
- data: contract.methods.transfer(to, value).encodeABI(),
- };
- }
- const gasPrice = await web3.eth.getGasPrice(); // 单位 Wei
- const gasLimit = await web3.eth.estimateGas(tx); // 单位 Gas
- const feeWei = BigInt(gasPrice) * BigInt(gasLimit);
- gasFee.value = Number(web3.utils.fromWei(feeWei.toString(), 'ether')).toFixed(8);
- } catch (err) {
- console.error('预估矿工费失败:', err);
- gasFee.value = '0.0000';
- }
- };
- const changeList = (item) => {
- selecctList.value = item;
- showWallet.value = false;
- unitNum.value = ''
- }
- // 确认
- const confirm = async () => {
- showWallet.value = false;
- showPassWord.value = true;
- };
- // 密码取消
- const cancel = () => {
- passWord.value = '';
- showPassWord.value = false;
- }
- // 密码确认
- const popConfirm = () => {
- if(passWord.value != walletStore.accountPassword){
- showNotify({ type: 'warning', message: '请输入正确的密码' });
- }else{
- getData();
- }
- }
- // 请求链
- const getData = async () => {
- const loading = showLoadingToast({
- message: '转账中…',
- forbidClick: true,
- duration: 0,
- });
- try {
- let receipt;
- if (selecctList.value.name === 'ACC') {
- // ETH 转账
- receipt = await sendETH(
- walletStore.privateKey,
- walletAddress.value,
- unitNum.value
- );
- } else {
- // ERC‑20 代币转账
- receipt = await sendToken(
- walletStore.privateKey,
- selecctList.value.address,
- walletAddress.value,
- unitNum.value
- );
- }
- showToast({ message: '转账成功', type: 'success' });
- console.log('交易哈希:', receipt.transactionHash);
- router.push('/wallet');
- } catch (err) {
- console.error('⚠️ 转账失败:', err);
- const msg =
- err?.message?.replace('Returned error: ', '') ||
- '发生未知错误,请稍后重试';
- showToast({ message: `转账失败:${msg}`, type: 'fail' });
- } finally {
- showWallet.value = false;
- }
- }
- // ========= ETH 转账 =========
- const sendETH = async (privateKey, toAddress, amountInEther) => {
- const account = web3.eth.accounts.privateKeyToAccount(privateKey);
- const from = account.address;
- const nonce = await web3.eth.getTransactionCount(from, 'latest');
- const gasPrice = await web3.eth.getGasPrice();
- const tx = {
- from,
- to: toAddress,
- value: web3.utils.toWei(amountInEther.toString(), 'ether'),
- gas: 21000,
- gasPrice,
- nonce,
- chainId: await web3.eth.getChainId(),
- };
- const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
- return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
- };
- // ========= ERC‑20 代币转账 =========
- const sendToken = async (privateKey, tokenAddress, toAddress, amount) => {
- const account = web3.eth.accounts.privateKeyToAccount(privateKey);
- const from = account.address;
- const contract = new web3.eth.Contract(pubData, tokenAddress);
- const nonce = await web3.eth.getTransactionCount(from, 'latest');
- const gasPrice = await web3.eth.getGasPrice();
- const value = web3.utils.toWei(amount.toString(), 'ether');
- const txData = contract.methods.transfer(toAddress, value).encodeABI();
- const tx = {
- from,
- to: tokenAddress,
- data: txData,
- gas: 100000,
- gasPrice,
- nonce,
- chainId: await web3.eth.getChainId(),
- };
- const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
- return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
- };
- onMounted(async ()=>{
- gethotTokens();
- })
- </script>
- <style lang="less" scoped>
- .container{
- display: flex;
- flex-direction: column;
- height: calc(100vh - 44px);
- padding: 17px 17px 33px;
- box-sizing: border-box;
- .content{
- flex: 1;
- .card-box{
- margin-bottom: 25px;
- .card-title{
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 12px;
- color: #8D8D8D;
- margin-bottom: 6px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .card-input{
- background: #F2F2F2;
- border-radius: 8px;
- border: 1px solid #D8D8D8;
- display: flex;
- align-items: center;
- padding: 18px 17px;
- height: 56px;
- box-sizing: border-box;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 15px;
- color: #000000;
- .card-text{
- margin: 0 5px 0 8px;
- }
- .card-input-right{
- display: flex;
- align-items: center;
- flex-shrink: 0;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 15px;
- color: #000000;
- margin-left: 17px;
- .line{
- height: 25px;
- border: 1px solid #D8D8D8;
- width: 0;
- margin: 0 17px;
- }
- .all{
- font-weight: 500;
- color: #4765DD;
- }
- }
- .price{
- font-size: 12px;
- color: #8D8D8D;
- margin-top: 1px;
- }
- :deep(.van-cell) {
- background:#F2F2F2 !important;
- padding: 0 !important;
- }
- :deep(.van-field__control) {
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 15px;
- color: #000000;
- }
- }
- }
- }
- .footer-btn{
- height: 40px !important;
- line-height: 40px !important;
- background: linear-gradient( 90deg, #4765DD 0%, #40A4FB 100%) !important;
- border-radius:28px;
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 15px;
- padding: 9px 0;
- box-sizing: border-box;
- color: #FFFFFF;
- }
- .pop-content{
- display: flex;
- flex-direction: column;
- .pop-title{
- padding: 17px;
- border-bottom: 1px solid #F2F2F2;
- display: flex;
- align-items: center;
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 17px;
- color: #000000;
- .title{
- flex: 1;
- display: flex;
- justify-content: center;
- }
- }
- .list-ul{
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: auto;
- margin: 16px;
- .list-li{
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- margin-bottom: 16px;
- .list-li-lf{
- display: flex;
- align-items: center;
- font-size: 15px;
- color: #000000;
- }
- .list-li-ri{
- font-size: 15px;
- color: @font-color2;
- text-align: right;
- .list-li-ri-num{
- margin-top: 3px;
- font-weight: 400;
- font-size: 12px;
- color: #8D8D8D;
- }
- }
- }
- .list-li:last-child{
- margin-bottom: 0;
- }
- }
- .list-ul::-webkit-scrollbar{
- width: 0;
- }
- .pop-detail{
- padding: 17px 17px 0;
- .pop-detail-title{
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 17px;
- color: #000000;
- margin-bottom: 20px;
- text-align: center;
- }
- .pop-detail-cell{
- display: flex;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 15px;
- color: #4F4F4F;
- padding: 10px 0;
- border-bottom: 1px solid #F2F2F2;
- .cell-label{
- width: 76px;
- flex-shrink: 0;
- }
- .cell-text{
- word-break: break-word;
- }
- }
- .pop-detail-cell:last-child{
- border-bottom: 0;
- }
- .pop-btn{
- margin-top: 53px;
- .btn{
- height: 40px;
- line-height: 40px;
- border-radius: 50px;
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 15px;
- color: #FFFFFF;
- }
- }
- }
- }
- }
- .pop-content-password{
- padding: 27px 35px 25px 34px;
- .pop-title-password{
- font-family: PingFang SC, PingFang SC;
- font-weight: 500;
- font-size: 17px;
- color: #000000;
- text-align: center;
- }
- .pop-input{
- background: #F2F2F2;
- border-radius: 8px;
- height: 40px;
- margin: 21px 0 31px;
- }
- .pop-btn-password{
- display: flex;
- justify-content: center;
- .btn-password{
- width: 83px;
- height: 29px;
- line-height: 29px;
- padding: 5px 0 !important;
- border-radius: 6px;
- font-family: PingFang SC, PingFang SC;
- font-weight: 400;
- font-size: 15px;
- box-sizing:border-box;
- }
- .cancel{
- margin-right: 17px !important;
- border: 1px solid #D8D8D8;
- color: #000 !important;
- }
- .confirm{
- background: @theme-color1;
- color: #FFF;
- font-weight: 500;
- }
- }
- }
- :deep(.van-popup--center) {
- margin: 0 40px !important;
- width: auto !important;
- }
- </style>
|