feat: 充值页新增 CCpay 银行QR 第三通道
- recharge2.vue: 新增 Ngân hàng QR tab + 内嵌银行转账弹窗 + 轮询到账
This commit is contained in:
@@ -12,6 +12,9 @@
|
|||||||
<div class="pay-tab" :class="{active: payChannel=='epay'}" @click="payChannel='epay'">
|
<div class="pay-tab" :class="{active: payChannel=='epay'}" @click="payChannel='epay'">
|
||||||
USDT (TRC20)
|
USDT (TRC20)
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pay-tab" :class="{active: payChannel=='ccpay'}" @click="payChannel='ccpay'">
|
||||||
|
{{Language.recharge_bank2 || 'Ngân hàng QR'}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alin-form" v-if="payChannel=='alin'">
|
<div class="alin-form" v-if="payChannel=='alin'">
|
||||||
<div class="alin-item">
|
<div class="alin-item">
|
||||||
@@ -34,6 +37,15 @@
|
|||||||
{{submitting ? '...' : (Language.withdraw_page_confirm || '确认充值')}}
|
{{submitting ? '...' : (Language.withdraw_page_confirm || '确认充值')}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="alin-form" v-if="payChannel=='ccpay'">
|
||||||
|
<div class="alin-item">
|
||||||
|
<label>{{Language.withdraw_page_withdrawal_amount || '充值金额'}} (VND):</label>
|
||||||
|
<input class="input" type="number" v-model="ccpayAmount" placeholder="Nhập số tiền VND (vd: 1,000,000)">
|
||||||
|
</div>
|
||||||
|
<button class="btn alin-submit" @click="submitCcpay" :disabled="submitting">
|
||||||
|
{{submitting ? '...' : (Language.withdraw_page_confirm || '确认充值')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 其他渠道 iframe -->
|
<!-- 其他渠道 iframe -->
|
||||||
<iframe v-else class="page" :src="webconfig.PayUrl+'?uid='+userInfo.uid+'&api_token='+userInfo.api_token"></iframe>
|
<iframe v-else class="page" :src="webconfig.PayUrl+'?uid='+userInfo.uid+'&api_token='+userInfo.api_token"></iframe>
|
||||||
@@ -77,6 +89,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- CCpay 银行转账内嵌弹窗 -->
|
||||||
|
<div class="qr-mask" v-if="bank.visible" @click.self="closeBank">
|
||||||
|
<div class="qr-box">
|
||||||
|
<div class="qr-close" @click="closeBank">×</div>
|
||||||
|
<div class="qr-title">{{Language.recharge_bank2 || 'Ngân hàng QR'}} {{Language.recharge_online2 || 'Thanh toán'}}</div>
|
||||||
|
<div class="qr-status" :class="bank.statusClass">{{bank.statusText}}</div>
|
||||||
|
|
||||||
|
<div class="qr-code-wrap" ref="bankQrcodeEl"></div>
|
||||||
|
|
||||||
|
<div class="qr-field">
|
||||||
|
<div class="qr-field-label">Số tài khoản</div>
|
||||||
|
<div class="qr-field-value amount">
|
||||||
|
<span>{{bank.data.bank_account}}</span>
|
||||||
|
<button class="qr-copy" @click="copyText(bank.data.bank_account, 'bank_acc')">
|
||||||
|
{{qr.copied==='bank_acc' ? '✓' : 'Copy'}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-field">
|
||||||
|
<div class="qr-field-label">Tên tài khoản — Ngân hàng</div>
|
||||||
|
<div class="qr-field-value addr">
|
||||||
|
<span>{{bank.data.bank_name}} — {{bank.data.bank_short}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-meta">
|
||||||
|
<span>{{Number(bank.data.amount).toLocaleString()}} VND</span>
|
||||||
|
<span class="qr-countdown">⏱ {{bank.countdownText}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-tip">
|
||||||
|
{{Language.pay_bank_warning || '⚠ Vui lòng chuyển khoản chính xác số tiền và nội dung. Sai thông tin sẽ không thể xác nhận đơn hàng!'}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,6 +139,7 @@ export default {
|
|||||||
payChannel: 'alin',
|
payChannel: 'alin',
|
||||||
alinAmount: '',
|
alinAmount: '',
|
||||||
epayAmount: '',
|
epayAmount: '',
|
||||||
|
ccpayAmount: '',
|
||||||
submitting: false,
|
submitting: false,
|
||||||
ApiUrl: JSON.parse(localStorage.getItem('ApiUrl')),
|
ApiUrl: JSON.parse(localStorage.getItem('ApiUrl')),
|
||||||
qr: {
|
qr: {
|
||||||
@@ -104,6 +153,16 @@ export default {
|
|||||||
_qrInstance: null,
|
_qrInstance: null,
|
||||||
_pollTimer: null,
|
_pollTimer: null,
|
||||||
_countdownTimer: null,
|
_countdownTimer: null,
|
||||||
|
bank: {
|
||||||
|
visible: false,
|
||||||
|
data: { bank_account: '', bank_name: '', bank_short: '', qr_data: '', amount: 0, order_sn: '', expires_at: 0, timeout_sec: 600 },
|
||||||
|
countdownText: '',
|
||||||
|
statusText: '',
|
||||||
|
statusClass: 'waiting',
|
||||||
|
},
|
||||||
|
_bankQrInstance: null,
|
||||||
|
_bankPollTimer: null,
|
||||||
|
_bankCountdownTimer: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -111,6 +170,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy(){
|
beforeDestroy(){
|
||||||
this.stopQrTimers();
|
this.stopQrTimers();
|
||||||
|
this.stopBankTimers();
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
offPop(){
|
offPop(){
|
||||||
@@ -287,6 +347,123 @@ export default {
|
|||||||
if(this._pollTimer){ clearInterval(this._pollTimer); this._pollTimer = null; }
|
if(this._pollTimer){ clearInterval(this._pollTimer); this._pollTimer = null; }
|
||||||
if(this._countdownTimer){ clearInterval(this._countdownTimer); this._countdownTimer = null; }
|
if(this._countdownTimer){ clearInterval(this._countdownTimer); this._countdownTimer = null; }
|
||||||
},
|
},
|
||||||
|
submitCcpay(){
|
||||||
|
if(!this.ccpayAmount || this.ccpayAmount <= 0){
|
||||||
|
this.$message({type:'warning', message:'Vui lòng nhập số tiền!'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.ccpayAmount < 1000000){
|
||||||
|
this.$message({type:'warning', message:'Số tiền tối thiểu là 1,000,000'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.submitting) return;
|
||||||
|
this.submitting = true;
|
||||||
|
var encryptstring = encrypt(JSON.stringify({
|
||||||
|
user_id: this.userInfo.uid,
|
||||||
|
price: this.ccpayAmount,
|
||||||
|
client: 1,
|
||||||
|
username: this.userInfo.nickname,
|
||||||
|
api_token: this.userInfo.api_token,
|
||||||
|
})).toString();
|
||||||
|
this.$axios.post(this.ApiUrl+'/ccpay_online_order', {encryptData:encryptstring}).then(
|
||||||
|
response=>{
|
||||||
|
this.submitting = false;
|
||||||
|
if(response.data.Success==1){
|
||||||
|
const data = JSON.parse(decrypt(response.data.Data));
|
||||||
|
if(data.status == 1 && data.qrcode){
|
||||||
|
this.openBank(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'});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message({type:'error', message: response.data.Msg || 'Nạp thất bại'});
|
||||||
|
}
|
||||||
|
}).catch(error=>{
|
||||||
|
this.submitting = false;
|
||||||
|
console.log(error);
|
||||||
|
this.$message({type:'error', message:'Yêu cầu thất bại'});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openBank(qrcode){
|
||||||
|
this.bank.data = qrcode;
|
||||||
|
this.bank.visible = true;
|
||||||
|
this.bank.statusText = this.Language.pay_waiting || 'Đang chờ thanh toán...';
|
||||||
|
this.bank.statusClass = 'waiting';
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if(qrcode.qr_data){
|
||||||
|
this.renderBankQr(qrcode.qr_data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.startBankCountdown();
|
||||||
|
this.startBankPolling();
|
||||||
|
},
|
||||||
|
renderBankQr(text){
|
||||||
|
const el = this.$refs.bankQrcodeEl;
|
||||||
|
if(!el) return;
|
||||||
|
el.innerHTML = '';
|
||||||
|
this._bankQrInstance = new QRCode(el, {
|
||||||
|
text: text,
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
colorDark: '#000000',
|
||||||
|
colorLight: '#ffffff',
|
||||||
|
correctLevel: QRCode.CorrectLevel.M,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
startBankCountdown(){
|
||||||
|
const tick = () => {
|
||||||
|
const left = Math.max(0, this.bank.data.expires_at - Math.floor(Date.now()/1000));
|
||||||
|
const m = Math.floor(left/60);
|
||||||
|
const s = left % 60;
|
||||||
|
this.bank.countdownText = (m<10?'0':'')+m+':'+(s<10?'0':'')+s;
|
||||||
|
if(left <= 0){
|
||||||
|
this.bank.statusText = this.Language.pay_expired || 'Đơn hàng đã hết hạn';
|
||||||
|
this.bank.statusClass = 'expired';
|
||||||
|
this.stopBankTimers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
this._bankCountdownTimer = setInterval(tick, 1000);
|
||||||
|
},
|
||||||
|
startBankPolling(){
|
||||||
|
const poll = () => {
|
||||||
|
if(!this.bank.visible || !this.bank.data.order_sn) return;
|
||||||
|
const encryptstring = encrypt(JSON.stringify({
|
||||||
|
user_id: this.userInfo.uid,
|
||||||
|
order_sn: this.bank.data.order_sn,
|
||||||
|
api_token: this.userInfo.api_token,
|
||||||
|
})).toString();
|
||||||
|
this.$axios.post(this.ApiUrl+'/ccpay_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.bank.statusText = this.Language.pay_success || 'Thanh toán thành công!';
|
||||||
|
this.bank.statusClass = 'success';
|
||||||
|
this.stopBankTimers();
|
||||||
|
this.updateMoney && this.updateMoney();
|
||||||
|
setTimeout(() => this.closeBank(), 2500);
|
||||||
|
} else if(d.status == 2){
|
||||||
|
this.bank.statusText = this.Language.pay_expired || 'Đơn hàng đã hết hạn';
|
||||||
|
this.bank.statusClass = 'expired';
|
||||||
|
this.stopBankTimers();
|
||||||
|
}
|
||||||
|
}).catch(()=>{});
|
||||||
|
};
|
||||||
|
this._bankPollTimer = setInterval(poll, 3000);
|
||||||
|
setTimeout(poll, 1000);
|
||||||
|
},
|
||||||
|
closeBank(){
|
||||||
|
this.bank.visible = false;
|
||||||
|
this.stopBankTimers();
|
||||||
|
if(this.$refs.bankQrcodeEl) this.$refs.bankQrcodeEl.innerHTML = '';
|
||||||
|
this._bankQrInstance = null;
|
||||||
|
},
|
||||||
|
stopBankTimers(){
|
||||||
|
if(this._bankPollTimer){ clearInterval(this._bankPollTimer); this._bankPollTimer = null; }
|
||||||
|
if(this._bankCountdownTimer){ clearInterval(this._bankCountdownTimer); this._bankCountdownTimer = null; }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['userInfo','webconfig','Language'])
|
...mapState(['userInfo','webconfig','Language'])
|
||||||
|
|||||||
Reference in New Issue
Block a user