feat(recharge): USDT 通道改为内嵌二维码模式,替代跳转 pay.g7g7.top

- 接入 qrcodejs2 生成 TRC20 地址二维码
- 充值弹窗内嵌:地址/金额/QR/倒计时,一键复制
- 每 3 秒轮询 /epay_order_status,到账自动关闭 + 刷新余额
- 后端返回 qrcode 字段时走内嵌;若无则回退跳转式(兼容)
This commit is contained in:
testadmin
2026-04-16 23:23:00 +08:00
parent d1f69e4bb9
commit b912dcef5e
3 changed files with 277 additions and 3 deletions
+7
View File
@@ -11,6 +11,7 @@
"axios": "^0.18.0",
"babel-preset-es2015": "^6.24.1",
"px2rem-loader": "^0.1.9",
"qrcodejs2": "^0.0.2",
"swiper": "^4.5.0",
"vue": "^2.5.2",
"vue-js-cookie": "^2.1.0",
@@ -11619,6 +11620,12 @@
"teleport": ">=0.2.0"
}
},
"node_modules/qrcodejs2": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/qrcodejs2/-/qrcodejs2-0.0.2.tgz",
"integrity": "sha512-+Y4HA+cb6qUzdgvI3KML8GYpMFwB24dFwzMkS/yXq6hwtUGNUnZQdUnksrV1XGMc2mid5ROw5SAuY9XhI3ValA==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+1
View File
@@ -13,6 +13,7 @@
"axios": "^0.18.0",
"babel-preset-es2015": "^6.24.1",
"px2rem-loader": "^0.1.9",
"qrcodejs2": "^0.0.2",
"swiper": "^4.5.0",
"vue": "^2.5.2",
"vue-js-cookie": "^2.1.0",
+268 -2
View File
@@ -37,11 +37,52 @@
</div>
<!-- 其他渠道 iframe -->
<iframe v-else class="page" :src="webconfig.PayUrl+'?uid='+userInfo.uid+'&api_token='+userInfo.api_token"></iframe>
<!-- USDT 内嵌二维码支付弹窗 -->
<div class="qr-mask" v-if="qr.visible" @click.self="closeQr">
<div class="qr-box">
<div class="qr-close" @click="closeQr">×</div>
<div class="qr-title">USDT (TRC20) {{Language.recharge_online2 || 'Thanh toán'}}</div>
<div class="qr-status" :class="qr.statusClass">{{qr.statusText}}</div>
<div class="qr-code-wrap" ref="qrcodeEl"></div>
<div class="qr-field">
<div class="qr-field-label">{{Language.pay_amount_usdt || 'Số tiền USDT (chính xác)'}}</div>
<div class="qr-field-value amount">
<span>{{qr.data.usdt_amount}} USDT</span>
<button class="qr-copy" @click="copyText(qr.data.usdt_amount, 'amount')">
{{qr.copied==='amount' ? '' : 'Copy'}}
</button>
</div>
</div>
<div class="qr-field">
<div class="qr-field-label">{{Language.pay_wallet_address || 'Địa chỉ ví (TRC20)'}}</div>
<div class="qr-field-value addr">
<span>{{qr.data.usdt_address}}</span>
<button class="qr-copy" @click="copyText(qr.data.usdt_address, 'addr')">
{{qr.copied==='addr' ? '' : 'Copy'}}
</button>
</div>
</div>
<div class="qr-meta">
<span> {{qr.data.vnd_amount}} VND</span>
<span class="qr-countdown"> {{qr.countdownText}}</span>
</div>
<div class="qr-tip">
{{Language.pay_usdt_warning || '⚠ Vui lòng chuyển chính xác số tiền. Sai số sẽ không thể xác định đơn hàng!'}}
</div>
</div>
</div>
</div>
</template>
<script>
import './recharge.css'
import QRCode from 'qrcodejs2'
import {encrypt,decrypt} from '../../common/js/secret'
import { mapState } from 'vuex'
export default {
@@ -51,12 +92,26 @@ export default {
alinAmount: '',
epayAmount: '',
submitting: false,
ApiUrl: JSON.parse(localStorage.getItem('ApiUrl'))
ApiUrl: JSON.parse(localStorage.getItem('ApiUrl')),
qr: {
visible: false,
data: { usdt_address: '', usdt_amount: '', vnd_amount: '', expires_at: 0, order_sn: '' },
countdownText: '',
statusText: '',
statusClass: 'waiting',
copied: '',
},
_qrInstance: null,
_pollTimer: null,
_countdownTimer: null,
}
},
mounted: function() {
},
beforeDestroy(){
this.stopQrTimers();
},
methods:{
offPop(){
this.$store.dispatch('updatemainPop',{type:'',ishow:false});
@@ -126,7 +181,10 @@ export default {
this.submitting = false;
if(response.data.Success==1){
const data = JSON.parse(decrypt(response.data.Data));
if(data.status == 1 && data.url){
if(data.status == 1 && data.qrcode){
this.openQr(data.qrcode);
} else if(data.status == 1 && data.url){
// 后端暂未升级时回退到跳转式
window.open(data.url, '_blank');
} else {
this.$message({type:'error', message: data.message || 'Nạp thất bại'});
@@ -140,6 +198,95 @@ export default {
this.$message({type:'error', message:'Yêu cầu thất bại'});
});
},
openQr(qrcode){
this.qr.data = qrcode;
this.qr.visible = true;
this.qr.statusText = this.Language.pay_waiting || 'Đang chờ thanh toán...';
this.qr.statusClass = 'waiting';
this.qr.copied = '';
this.$nextTick(() => {
this.renderQr(qrcode.usdt_address);
});
this.startCountdown();
this.startPolling();
},
renderQr(text){
const el = this.$refs.qrcodeEl;
if(!el) return;
el.innerHTML = '';
this._qrInstance = new QRCode(el, {
text: text,
width: 180,
height: 180,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M,
});
},
startCountdown(){
const tick = () => {
const left = Math.max(0, this.qr.data.expires_at - Math.floor(Date.now()/1000));
const m = Math.floor(left/60);
const s = left % 60;
this.qr.countdownText = (m<10?'0':'')+m+':'+(s<10?'0':'')+s;
if(left <= 0){
this.qr.statusText = this.Language.pay_expired || 'Đơn hàng đã hết hạn';
this.qr.statusClass = 'expired';
this.stopQrTimers();
}
};
tick();
this._countdownTimer = setInterval(tick, 1000);
},
startPolling(){
const poll = () => {
if(!this.qr.visible || !this.qr.data.order_sn) return;
const encryptstring = encrypt(JSON.stringify({
user_id: this.userInfo.uid,
order_sn: this.qr.data.order_sn,
api_token: this.userInfo.api_token,
})).toString();
this.$axios.post(this.ApiUrl+'/epay_order_status', {encryptData:encryptstring}).then(res=>{
if(res.data.Success!=1) return;
const d = JSON.parse(decrypt(res.data.Data));
if(d.status == 1){
this.qr.statusText = this.Language.pay_success || 'Thanh toán thành công!';
this.qr.statusClass = 'success';
this.stopQrTimers();
this.updateMoney && this.updateMoney();
setTimeout(() => this.closeQr(), 2500);
} else if(d.status == 2){
this.qr.statusText = this.Language.pay_expired || 'Đơn hàng đã hết hạn';
this.qr.statusClass = 'expired';
this.stopQrTimers();
}
}).catch(()=>{});
};
this._pollTimer = setInterval(poll, 3000);
setTimeout(poll, 1000); // 首次提前 1s 探测
},
copyText(text, tag){
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch(e) {}
document.body.removeChild(ta);
this.qr.copied = tag;
setTimeout(() => { if(this.qr.copied === tag) this.qr.copied = ''; }, 1500);
},
closeQr(){
this.qr.visible = false;
this.stopQrTimers();
if(this.$refs.qrcodeEl) this.$refs.qrcodeEl.innerHTML = '';
this._qrInstance = null;
},
stopQrTimers(){
if(this._pollTimer){ clearInterval(this._pollTimer); this._pollTimer = null; }
if(this._countdownTimer){ clearInterval(this._countdownTimer); this._countdownTimer = null; }
},
},
computed: {
...mapState(['userInfo','webconfig','Language'])
@@ -286,4 +433,123 @@ export default {
color: #ffeb3b;
}
/* ============ 内嵌 USDT 二维码弹窗 ============ */
.qr-mask{
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.72);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.qr-box{
width: 360px;
background: #1a1206;
border: 1px solid #907545;
border-radius: 10px;
padding: 20px 22px;
box-shadow: 0 0 20px rgba(229,185,50,0.15);
color: #e5b932;
position: relative;
box-sizing: border-box;
}
.qr-close{
position: absolute;
right: 10px; top: 6px;
color: #aaa;
font-size: 22px;
cursor: pointer;
line-height: 1;
padding: 2px 8px;
}
.qr-close:hover{ color: #e5b932; }
.qr-title{
text-align: center;
font-size: 15px;
font-weight: bold;
margin-bottom: 6px;
}
.qr-status{
text-align: center;
font-size: 12px;
padding: 4px 0;
margin-bottom: 10px;
border-radius: 4px;
}
.qr-status.waiting{ color: #e5b932; }
.qr-status.success{ color: #7cd992; }
.qr-status.expired{ color: #e74c3c; }
.qr-code-wrap{
width: 180px;
height: 180px;
margin: 0 auto 14px;
background: #fff;
padding: 8px;
border-radius: 6px;
box-sizing: content-box;
}
.qr-code-wrap img, .qr-code-wrap canvas{
display: block;
width: 180px !important;
height: 180px !important;
}
.qr-field{
margin-bottom: 10px;
}
.qr-field-label{
color: #bbb;
font-size: 11px;
margin-bottom: 4px;
}
.qr-field-value{
display: flex;
align-items: center;
gap: 8px;
background: rgba(0,0,0,0.35);
border: 1px solid #5a4323;
border-radius: 4px;
padding: 6px 8px;
font-size: 12px;
word-break: break-all;
}
.qr-field-value.amount{
color: #fff;
font-weight: bold;
font-size: 14px;
}
.qr-field-value.addr{
color: #e5b932;
font-family: 'Courier New', monospace;
font-size: 11px;
}
.qr-field-value > span{ flex: 1; }
.qr-copy{
flex-shrink: 0;
background: #c5971e;
border: none;
color: #161107;
font-size: 11px;
padding: 3px 10px;
border-radius: 3px;
cursor: pointer;
}
.qr-copy:hover{ background: #e5b932; }
.qr-meta{
display: flex;
justify-content: space-between;
color: #bbb;
font-size: 12px;
margin: 12px 0 6px;
}
.qr-countdown{ color: #e5b932; font-weight: bold; }
.qr-tip{
color: #e74c3c;
font-size: 11px;
text-align: center;
line-height: 1.5;
margin-top: 8px;
opacity: 0.85;
}
</style>