mirror of
https://gitee.com/bootx/dax-pay-h5
synced 2026-08-07 21:55:48 +08:00
feat(h5): 新增支付宝代运营授权落地页,路由独立到 isv-auth 命名空间
This commit is contained in:
@@ -84,6 +84,15 @@ const routeModuleList: Array<RouteRecordRaw> = [
|
||||
title: '支付宝认证',
|
||||
},
|
||||
},
|
||||
// 支付宝代运营授权落地页(移动独占:授权回跳换 app_auth_token)
|
||||
{
|
||||
path: RoutePath.ISV_AUTH_ALIPAY,
|
||||
name: 'AlipayIsvAuth',
|
||||
component: () => import('@/mobile/views/isv-auth/alipay/index.vue'),
|
||||
meta: {
|
||||
title: '支付宝代运营授权',
|
||||
},
|
||||
},
|
||||
// 微信认证落地页(跨端:与 PC 端同 path 各指各 view,微信公众号 OAuth 重定向回调取 code)
|
||||
{
|
||||
path: RoutePath.AUTH_WECHAT,
|
||||
|
||||
221
src/mobile/views/isv-auth/alipay/index.vue
Normal file
221
src/mobile/views/isv-auth/alipay/index.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import { showDialog } from 'vant'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { isvAuthCallback } from '@/shared/api/isv-auth'
|
||||
import alipayLogo from '@/shared/assets/icons/channel/alipay.svg'
|
||||
|
||||
defineOptions({ name: 'AlipayIsvAuthPage' })
|
||||
|
||||
declare const AlipayJSBridge: {
|
||||
call: (name: string) => void
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(true)
|
||||
const failed = ref(false)
|
||||
const failMsg = ref('')
|
||||
|
||||
/**
|
||||
* Base64 解码 state(通道商户号)
|
||||
*/
|
||||
function base64Decode(str: string): string {
|
||||
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
|
||||
return decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
|
||||
.join(''),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记失败
|
||||
*/
|
||||
function markFailed(msg: string) {
|
||||
failed.value = true
|
||||
failMsg.value = msg
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭支付宝内嵌 WebView
|
||||
*/
|
||||
function handleClose() {
|
||||
try {
|
||||
if (typeof AlipayJSBridge !== 'undefined') {
|
||||
AlipayJSBridge.call('closeWebview')
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// 非支付宝环境忽略
|
||||
}
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const appAuthCode = route.query.app_auth_code as string | undefined
|
||||
const state = route.query.state as string | undefined
|
||||
|
||||
if (!appAuthCode) {
|
||||
markFailed(t('isvAuth.alipay.authCodeMissing'))
|
||||
return
|
||||
}
|
||||
if (!state) {
|
||||
markFailed(t('isvAuth.alipay.stateMissing'))
|
||||
return
|
||||
}
|
||||
|
||||
let channelMchNo = ''
|
||||
try {
|
||||
channelMchNo = base64Decode(state)
|
||||
}
|
||||
catch {
|
||||
markFailed(t('isvAuth.alipay.stateInvalid'))
|
||||
return
|
||||
}
|
||||
if (!channelMchNo) {
|
||||
markFailed(t('isvAuth.alipay.stateInvalid'))
|
||||
return
|
||||
}
|
||||
|
||||
isvAuthCallback({
|
||||
channelMchNo,
|
||||
code: appAuthCode,
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = false
|
||||
showDialog({
|
||||
message: t('isvAuth.alipay.successMsg'),
|
||||
confirmButtonText: t('isvAuth.alipay.close'),
|
||||
}).then(() => {
|
||||
handleClose()
|
||||
})
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
markFailed(err?.message || t('isvAuth.alipay.authFail'))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading-box">
|
||||
<div class="logo-wrapper">
|
||||
<img class="channel-logo" :src="alipayLogo" alt="Alipay" width="64" height="64">
|
||||
</div>
|
||||
<van-loading vertical color="#1677ff" size="32px">
|
||||
<span class="loading-text">{{ t('isvAuth.alipay.loading') }}</span>
|
||||
</van-loading>
|
||||
<div class="footer-tip">
|
||||
<span>{{ t('isvAuth.alipay.secureTip') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 失败 -->
|
||||
<div v-else-if="failed" class="result-box">
|
||||
<div class="status-icon">
|
||||
<svg viewBox="0 0 1024 1024" width="64" height="64" aria-hidden="true">
|
||||
<path fill="#ee0a24" d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m193.5 561.7-41.8 41.8L512 515.8 360.3 667.5l-41.8-41.8L470.2 474 318.5 322.3l41.8-41.8L512 432.2l151.7-151.7 41.8 41.8L553.8 474l151.7 151.7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="result-title">
|
||||
{{ t('isvAuth.alipay.failTitle') }}
|
||||
</h3>
|
||||
<p class="fail-msg">
|
||||
{{ failMsg }}
|
||||
</p>
|
||||
<div class="action-buttons">
|
||||
<van-button plain round block class="close-btn" @click="handleClose">
|
||||
{{ t('isvAuth.alipay.close') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.auth-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.loading-box,
|
||||
.result-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
padding: 40px 24px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
.logo-wrapper {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.channel-logo {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.footer-tip {
|
||||
margin-top: 32px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.result-box {
|
||||
.status-icon {
|
||||
margin-bottom: 16px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
margin: 0 0 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.fail-msg {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
|
||||
.close-btn {
|
||||
color: #666;
|
||||
border-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/shared/api/isv-auth.ts
Normal file
24
src/shared/api/isv-auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RequestEnum } from '@/shared/enums/httpEnum'
|
||||
import { http } from '@/shared/utils/http/axios'
|
||||
|
||||
/**
|
||||
* 服务商(ISV)代运营授权: 授权码换取 app_auth_token
|
||||
*/
|
||||
export function isvAuthCallback(param: IsvAuthParam): Promise<void> {
|
||||
return http.request<void>({
|
||||
url: '/unipay/assist/alipay/isv/auth/callback',
|
||||
method: RequestEnum.POST,
|
||||
data: param,
|
||||
}, {
|
||||
// 由页面自行处理错误展示
|
||||
isShowMessage: false,
|
||||
})
|
||||
}
|
||||
|
||||
/** 代运营授权回调参数 */
|
||||
export interface IsvAuthParam {
|
||||
/** 通道商户号 */
|
||||
channelMchNo: string
|
||||
/** 应用授权码 */
|
||||
code: string
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import common from './common.json'
|
||||
import error from './error.json'
|
||||
import home from './home.json'
|
||||
import http from './http.json'
|
||||
import isvAuth from './isv-auth.json'
|
||||
import protocol from './protocol.json'
|
||||
import statement from './statement.json'
|
||||
|
||||
@@ -12,6 +13,7 @@ export default {
|
||||
common,
|
||||
http,
|
||||
auth,
|
||||
isvAuth,
|
||||
cashier,
|
||||
codePay,
|
||||
protocol,
|
||||
|
||||
13
src/shared/locales/en-US/isv-auth.json
Normal file
13
src/shared/locales/en-US/isv-auth.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"alipay": {
|
||||
"loading": "Completing agent authorization...",
|
||||
"secureTip": "Alipay agent authorization",
|
||||
"successMsg": "Authorization successful",
|
||||
"failTitle": "Authorization failed",
|
||||
"close": "Close",
|
||||
"authCodeMissing": "Failed to get authorization code",
|
||||
"stateMissing": "Failed to get channel merchant info",
|
||||
"stateInvalid": "Invalid channel merchant info",
|
||||
"authFail": "Authorization failed, please try again later"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import common from './common.json'
|
||||
import error from './error.json'
|
||||
import home from './home.json'
|
||||
import http from './http.json'
|
||||
import isvAuth from './isv-auth.json'
|
||||
import protocol from './protocol.json'
|
||||
import statement from './statement.json'
|
||||
|
||||
@@ -12,6 +13,7 @@ export default {
|
||||
common,
|
||||
http,
|
||||
auth,
|
||||
isvAuth,
|
||||
cashier,
|
||||
codePay,
|
||||
protocol,
|
||||
|
||||
13
src/shared/locales/zh-CN/isv-auth.json
Normal file
13
src/shared/locales/zh-CN/isv-auth.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"alipay": {
|
||||
"loading": "正在完成代运营授权...",
|
||||
"secureTip": "支付宝代运营授权",
|
||||
"successMsg": "授权成功",
|
||||
"failTitle": "授权失败",
|
||||
"close": "关闭页面",
|
||||
"authCodeMissing": "授权码获取失败",
|
||||
"stateMissing": "通道商户信息获取失败",
|
||||
"stateInvalid": "通道商户信息无效",
|
||||
"authFail": "授权失败,请稍后重试"
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ export const EXCLUSIVE_ROUTES: ExclusiveRoute[] = [
|
||||
{ path: RoutePath.CODE_PAY, name: 'CodePay', device: 'mobile' },
|
||||
// 支付宝认证落地页:仅移动端实现(支付宝 App 内 JSAPI)
|
||||
{ path: RoutePath.AUTH_ALIPAY, name: 'AlipayAuth', device: 'mobile' },
|
||||
// 支付宝代运营授权落地页:仅移动端实现(支付宝授权回跳)
|
||||
{ path: RoutePath.ISV_AUTH_ALIPAY, name: 'AlipayIsvAuth', device: 'mobile' },
|
||||
// 商户对账单:仅 PC 端实现
|
||||
{ path: RoutePath.MERCHANT_STATEMENT, name: 'MerchantStatement', device: 'pc' },
|
||||
]
|
||||
|
||||
@@ -25,6 +25,8 @@ export const RoutePath = {
|
||||
PROTOCOL: '/protocol/:protocolType',
|
||||
/** 支付宝认证落地页(移动独占:JSAPI 取码) */
|
||||
AUTH_ALIPAY: '/auth/alipay/:aliAppId/:queryCode',
|
||||
/** 支付宝代运营授权落地页(移动独占:支付宝授权回跳换 app_auth_token) */
|
||||
ISV_AUTH_ALIPAY: '/isv-auth/alipay',
|
||||
/** 微信认证落地页(跨端:公众号 OAuth 重定向回调,PC 微信内置浏览器也可访问) */
|
||||
AUTH_WECHAT: '/auth/wechat/:authToken',
|
||||
} as const
|
||||
|
||||
Reference in New Issue
Block a user