完善:多步 H5 授权流程
This commit is contained in:
236
basic-info.css
236
basic-info.css
@@ -1075,3 +1075,239 @@ body.modal-open {
|
|||||||
.back-home-btn:active {
|
.back-home-btn:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 授权流程样式 ==================== */
|
||||||
|
.auth-flow-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 授权进度区域 */
|
||||||
|
.auth-progress-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.active {
|
||||||
|
background: #e8f0ff;
|
||||||
|
border: 1px solid #3474fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.completed {
|
||||||
|
background: #f0fff4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.completed .auth-progress-icon {
|
||||||
|
color: #19be6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.completed .auth-progress-status {
|
||||||
|
color: #19be6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.failed {
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.failed .auth-progress-icon {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.failed .auth-progress-status {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.timeout {
|
||||||
|
background: #fffbe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.timeout .auth-progress-icon {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.timeout .auth-progress-status {
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.active .auth-progress-icon {
|
||||||
|
color: #3474fe;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-progress-item.active .auth-progress-status {
|
||||||
|
color: #3474fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iframe 区域 */
|
||||||
|
.auth-iframe-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-iframe-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: linear-gradient(135deg, #3474fe 0%, #5b8def 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-iframe-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-iframe-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 350px);
|
||||||
|
min-height: 400px;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 授权完成区域 */
|
||||||
|
.auth-complete-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-complete-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
line-height: 72px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
background: linear-gradient(135deg, #19be6b 0%, #47d88a 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(25, 190, 107, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-complete-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-complete-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-countdown {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-countdown #countdownSeconds {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3474fe;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-redirect-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 48px;
|
||||||
|
background: linear-gradient(135deg, #3474fe 0%, #5b8def 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(52, 116, 254, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-redirect-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(52, 116, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-redirect-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export const API_CONFIG = {
|
|||||||
SUBMIT_DRAFT_FORM: '/partnerh5/save_draft',
|
SUBMIT_DRAFT_FORM: '/partnerh5/save_draft',
|
||||||
GET_DRAFT_FORM: '/partnerh5/get_draft',
|
GET_DRAFT_FORM: '/partnerh5/get_draft',
|
||||||
|
|
||||||
|
// 授权状态查询接口
|
||||||
|
CHECK_AUTH_STATUS: '/partnerh5/check_auth_status',
|
||||||
|
|
||||||
// 区域数据接口
|
// 区域数据接口
|
||||||
AREA_LIST: '/partnerh5/area_list',
|
AREA_LIST: '/partnerh5/area_list',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DraftManager, FormIdGenerator, UserCache } from '../core/index.js';
|
|||||||
import { ASSET_CONFIG, BASIC_INFO_CONFIG, PROVINCE_CITY_DATA, CACHE_CONFIG } from '../config/index.js';
|
import { ASSET_CONFIG, BASIC_INFO_CONFIG, PROVINCE_CITY_DATA, CACHE_CONFIG } from '../config/index.js';
|
||||||
import { showToast } from '../ui/toast.js';
|
import { showToast } from '../ui/toast.js';
|
||||||
import { AreaService } from '../services/area.service.js';
|
import { AreaService } from '../services/area.service.js';
|
||||||
|
import { AuthFlowService, AUTH_STATUS } from '../services/index.js';
|
||||||
|
|
||||||
export class BasicInfoPage {
|
export class BasicInfoPage {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -655,85 +656,242 @@ export class BasicInfoPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示提交成功页面
|
* 显示提交成功页面(授权流程)
|
||||||
* @param {Object} data - 返回数据 { formdataid, h5Urls }
|
* @param {Object} data - 返回数据 { formdataid, h5Urls, redirectUrl }
|
||||||
*/
|
*/
|
||||||
showSubmitSuccessDialog(data) {
|
showSubmitSuccessDialog(data) {
|
||||||
const h5Urls = data.h5Urls;
|
const { formdataid, h5Urls, redirectUrl } = data;
|
||||||
const urlEntries = Object.entries(h5Urls);
|
|
||||||
|
|
||||||
// 隐藏表单内容
|
// 隐藏表单相关的所有内容
|
||||||
document.getElementById('assetList').style.display = 'none';
|
const topCard = document.querySelector('.top-card');
|
||||||
document.getElementById('basicInfoSection').style.display = 'none';
|
const assetSection = document.querySelector('.asset-section');
|
||||||
this.elements.submitBtn.style.display = 'none';
|
const basicInfoSection = document.getElementById('basicInfoSection');
|
||||||
|
const bottomSection = document.getElementById('bottomSection');
|
||||||
|
|
||||||
// 创建成功提示区域
|
if (topCard) topCard.style.display = 'none';
|
||||||
const successContainer = document.createElement('div');
|
if (assetSection) assetSection.style.display = 'none';
|
||||||
successContainer.className = 'submit-success-container';
|
if (basicInfoSection) basicInfoSection.style.display = 'none';
|
||||||
successContainer.innerHTML = `
|
if (bottomSection) bottomSection.style.display = 'none';
|
||||||
<div class="success-header">
|
|
||||||
<div class="success-icon">✅</div>
|
|
||||||
<h2>信息提交成功</h2>
|
|
||||||
<p class="formdata-id">表单ID:${data.formdataid}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${urlEntries.length > 0 ? `
|
// 创建授权流程容器
|
||||||
<div class="iframe-container">
|
const authContainer = document.createElement('div');
|
||||||
${urlEntries.map(([name, url], index) => `
|
authContainer.className = 'auth-flow-container';
|
||||||
<div class="iframe-wrapper ${index === 0 ? 'active' : ''}" data-index="${index}">
|
authContainer.id = 'authFlowContainer';
|
||||||
<div class="iframe-header">
|
|
||||||
<span class="product-name">${name}</span>
|
// 如果没有 h5Urls,显示成功提示
|
||||||
<span class="iframe-hint">请在下方完成申请流程</span>
|
if (!h5Urls || h5Urls.length === 0) {
|
||||||
</div>
|
authContainer.innerHTML = `
|
||||||
<iframe src="${url}" class="product-iframe" frameborder="0"></iframe>
|
<div class="auth-complete-section">
|
||||||
|
<div class="auth-complete-icon">✓</div>
|
||||||
|
<div class="auth-complete-title">信息提交成功</div>
|
||||||
|
<div class="auth-complete-desc">您的申请已提交成功!</div>
|
||||||
|
${redirectUrl ? `
|
||||||
|
<div class="auth-countdown">
|
||||||
|
<span id="countdownSeconds">5</span> 秒后自动跳转...
|
||||||
|
</div>
|
||||||
|
<button class="auth-redirect-btn" id="redirectNowBtn">立即跳转</button>
|
||||||
|
` : `
|
||||||
|
<button class="auth-redirect-btn" onclick="window.location.reload()">返回首页</button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mainContainer = document.querySelector('.container') || document.body;
|
||||||
|
mainContainer.insertBefore(authContainer, mainContainer.firstChild);
|
||||||
|
|
||||||
|
// 如果有 redirectUrl,启动倒计时
|
||||||
|
if (redirectUrl) {
|
||||||
|
this.startFinalCountdown(redirectUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有 h5Urls,显示授权流程界面
|
||||||
|
authContainer.innerHTML = `
|
||||||
|
<div class="auth-progress-section">
|
||||||
|
<div class="auth-progress-header">
|
||||||
|
<span class="auth-progress-title">授权进度</span>
|
||||||
|
<span class="auth-progress-count">(<span id="currentIndex">1</span>/${h5Urls.length})</span>
|
||||||
|
</div>
|
||||||
|
<div class="auth-progress-list" id="authProgressList">
|
||||||
|
${h5Urls.map((item, index) => `
|
||||||
|
<div class="auth-progress-item" data-apicode="${item.apicode}" data-index="${index}">
|
||||||
|
<span class="auth-progress-icon" id="icon-${item.apicode}">○</span>
|
||||||
|
<span class="auth-progress-name">${item.apiname}</span>
|
||||||
|
<span class="auth-progress-status" id="status-${item.apicode}">等待中</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
|
|
||||||
${urlEntries.length > 1 ? `
|
|
||||||
<div class="product-tabs">
|
|
||||||
${urlEntries.map(([name, url], index) => `
|
|
||||||
<button class="product-tab ${index === 0 ? 'active' : ''}" data-index="${index}">
|
|
||||||
${name}
|
|
||||||
</button>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
` : `
|
</div>
|
||||||
<div class="no-urls-message">
|
<div class="auth-iframe-section">
|
||||||
<p>您的申请已提交成功!</p>
|
<div class="auth-iframe-header">
|
||||||
<p>我们会尽快处理您的申请。</p>
|
<span class="auth-iframe-title" id="currentProductName">${h5Urls[0].apiname}</span>
|
||||||
|
<span class="auth-iframe-hint">请在下方完成授权</span>
|
||||||
</div>
|
</div>
|
||||||
`}
|
<iframe src="" class="auth-iframe" id="authIframe" frameborder="0"></iframe>
|
||||||
|
|
||||||
<div class="success-footer">
|
|
||||||
<button onclick="window.location.reload()" class="back-home-btn">返回首页</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 插入到页面顶部
|
|
||||||
const mainContainer = document.querySelector('.container') || document.body;
|
const mainContainer = document.querySelector('.container') || document.body;
|
||||||
mainContainer.insertBefore(successContainer, mainContainer.firstChild);
|
mainContainer.insertBefore(authContainer, mainContainer.firstChild);
|
||||||
|
|
||||||
// 绑定 Tab 切换事件
|
// 启动授权流程
|
||||||
if (urlEntries.length > 1) {
|
this.runAuthFlow(formdataid, h5Urls, redirectUrl);
|
||||||
const tabs = successContainer.querySelectorAll('.product-tab');
|
}
|
||||||
const iframes = successContainer.querySelectorAll('.iframe-wrapper');
|
|
||||||
|
|
||||||
tabs.forEach(tab => {
|
/**
|
||||||
tab.addEventListener('click', () => {
|
* 执行授权流程
|
||||||
const index = parseInt(tab.dataset.index);
|
* @param {number} formdataid - 表单数据ID
|
||||||
|
* @param {Array} h5Urls - H5 URL 列表
|
||||||
|
* @param {string} redirectUrl - 最终跳转 URL
|
||||||
|
*/
|
||||||
|
async runAuthFlow(formdataid, h5Urls, redirectUrl) {
|
||||||
|
const iframe = document.getElementById('authIframe');
|
||||||
|
const currentIndexEl = document.getElementById('currentIndex');
|
||||||
|
const currentProductNameEl = document.getElementById('currentProductName');
|
||||||
|
|
||||||
// 更新 Tab 状态
|
await AuthFlowService.startAuthFlow(formdataid, h5Urls, {
|
||||||
tabs.forEach(t => t.classList.remove('active'));
|
// 开始处理某个产品
|
||||||
tab.classList.add('active');
|
onStart: (item, index) => {
|
||||||
|
console.log('[BasicInfoPage] 开始处理:', item.apiname);
|
||||||
|
|
||||||
// 更新 iframe 显示
|
// 更新进度显示
|
||||||
iframes.forEach(iframe => iframe.classList.remove('active'));
|
if (currentIndexEl) {
|
||||||
iframes[index].classList.add('active');
|
currentIndexEl.textContent = index + 1;
|
||||||
|
}
|
||||||
|
if (currentProductNameEl) {
|
||||||
|
currentProductNameEl.textContent = item.apiname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度列表状态
|
||||||
|
const iconEl = document.getElementById(`icon-${item.apicode}`);
|
||||||
|
const statusEl = document.getElementById(`status-${item.apicode}`);
|
||||||
|
if (iconEl) iconEl.textContent = '●';
|
||||||
|
if (statusEl) statusEl.textContent = '授权中...';
|
||||||
|
|
||||||
|
// 高亮当前项
|
||||||
|
document.querySelectorAll('.auth-progress-item').forEach(el => {
|
||||||
|
el.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
const currentItem = document.querySelector(`.auth-progress-item[data-apicode="${item.apicode}"]`);
|
||||||
|
if (currentItem) {
|
||||||
|
currentItem.classList.add('active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// iframe 切换
|
||||||
|
onIframeChange: (url) => {
|
||||||
|
console.log('[BasicInfoPage] iframe 切换到:', url);
|
||||||
|
if (iframe && url) {
|
||||||
|
iframe.src = url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 状态更新
|
||||||
|
onProgress: (item, index, status) => {
|
||||||
|
console.log('[BasicInfoPage] 状态更新:', item.apiname, status);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 单个完成
|
||||||
|
onComplete: (item, index, result) => {
|
||||||
|
console.log('[BasicInfoPage] 完成:', item.apiname, result);
|
||||||
|
|
||||||
|
const iconEl = document.getElementById(`icon-${item.apicode}`);
|
||||||
|
const statusEl = document.getElementById(`status-${item.apicode}`);
|
||||||
|
const progressItem = document.querySelector(`.auth-progress-item[data-apicode="${item.apicode}"]`);
|
||||||
|
|
||||||
|
if (result.timeout) {
|
||||||
|
// 超时
|
||||||
|
if (iconEl) iconEl.textContent = '○';
|
||||||
|
if (statusEl) statusEl.textContent = '已超时';
|
||||||
|
if (progressItem) progressItem.classList.add('timeout');
|
||||||
|
} else if (result.status === AUTH_STATUS.APPLY_FAIL) {
|
||||||
|
// 失败
|
||||||
|
if (iconEl) iconEl.textContent = '✗';
|
||||||
|
if (statusEl) statusEl.textContent = '失败';
|
||||||
|
if (progressItem) progressItem.classList.add('failed');
|
||||||
|
} else {
|
||||||
|
// 成功
|
||||||
|
if (iconEl) iconEl.textContent = '✓';
|
||||||
|
if (statusEl) statusEl.textContent = '已完成';
|
||||||
|
if (progressItem) progressItem.classList.add('completed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 全部完成
|
||||||
|
onAllComplete: () => {
|
||||||
|
console.log('[BasicInfoPage] 全部授权完成');
|
||||||
|
this.showAuthComplete(redirectUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示授权完成界面
|
||||||
|
* @param {string} redirectUrl - 跳转 URL
|
||||||
|
*/
|
||||||
|
showAuthComplete(redirectUrl) {
|
||||||
|
const container = document.getElementById('authFlowContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 隐藏 iframe 区域,显示完成提示
|
||||||
|
const iframeSection = container.querySelector('.auth-iframe-section');
|
||||||
|
if (iframeSection) {
|
||||||
|
iframeSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建完成提示
|
||||||
|
const completeSection = document.createElement('div');
|
||||||
|
completeSection.className = 'auth-complete-section';
|
||||||
|
completeSection.innerHTML = `
|
||||||
|
<div class="auth-complete-icon">✓</div>
|
||||||
|
<div class="auth-complete-title">全部授权完成</div>
|
||||||
|
<div class="auth-complete-desc">您的申请已全部提交成功!</div>
|
||||||
|
${redirectUrl ? `
|
||||||
|
<div class="auth-countdown">
|
||||||
|
<span id="countdownSeconds">5</span> 秒后自动跳转...
|
||||||
|
</div>
|
||||||
|
<button class="auth-redirect-btn" id="redirectNowBtn">立即跳转</button>
|
||||||
|
` : `
|
||||||
|
<button class="auth-redirect-btn" onclick="window.location.reload()">返回首页</button>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(completeSection);
|
||||||
|
|
||||||
|
// 启动倒计时
|
||||||
|
if (redirectUrl) {
|
||||||
|
this.startFinalCountdown(redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动最终倒计时
|
||||||
|
* @param {string} redirectUrl - 跳转 URL
|
||||||
|
*/
|
||||||
|
startFinalCountdown(redirectUrl) {
|
||||||
|
const countdownEl = document.getElementById('countdownSeconds');
|
||||||
|
const redirectBtn = document.getElementById('redirectNowBtn');
|
||||||
|
|
||||||
|
// 立即跳转按钮
|
||||||
|
if (redirectBtn) {
|
||||||
|
redirectBtn.addEventListener('click', () => {
|
||||||
|
AuthFlowService.redirect(redirectUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动倒计时
|
||||||
|
AuthFlowService.startCountdown(
|
||||||
|
AuthFlowService.COUNTDOWN_SECONDS,
|
||||||
|
(remaining) => {
|
||||||
|
if (countdownEl) {
|
||||||
|
countdownEl.textContent = remaining;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
AuthFlowService.redirect(redirectUrl);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
220
src/js/services/auth-flow.service.js
Normal file
220
src/js/services/auth-flow.service.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* 授权流程服务
|
||||||
|
* 处理多个 H5 授权页面的逐个加载、轮询检测、超时控制和最终跳转
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApiClient } from '../core/api.js';
|
||||||
|
import { API_CONFIG } from '../config/index.js';
|
||||||
|
|
||||||
|
// 授权状态常量
|
||||||
|
const AUTH_STATUS = {
|
||||||
|
WAITING: 1, // 等待授权
|
||||||
|
CALLBACK: 2, // 已回调
|
||||||
|
APPLY_OK: 3, // 进件成功
|
||||||
|
APPLY_FAIL: 4 // 进件失败
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AuthFlowService {
|
||||||
|
// 配置常量
|
||||||
|
static POLL_INTERVAL = 3000; // 轮询间隔:3秒
|
||||||
|
static POLL_TIMEOUT = 10 * 60 * 1000; // 超时时间:10分钟
|
||||||
|
static COUNTDOWN_SECONDS = 5; // 倒计时:5秒
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查单个 API 的授权状态
|
||||||
|
* @param {number} formdataid - 表单数据ID
|
||||||
|
* @param {string} apicode - API编码
|
||||||
|
* @returns {Promise<Object>} - 状态信息 { apicode, apiname, status, h5url }
|
||||||
|
*/
|
||||||
|
static async checkAuthStatus(formdataid, apicode) {
|
||||||
|
try {
|
||||||
|
const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.CHECK_AUTH_STATUS, {
|
||||||
|
formdataid,
|
||||||
|
apicode
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.retcode === 0 && response.result) {
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('[AuthFlowService] 检查授权状态失败:', response.retinfo);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AuthFlowService] 检查授权状态出错:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待授权完成(轮询)
|
||||||
|
* @param {number} formdataid - 表单数据ID
|
||||||
|
* @param {string} apicode - API编码
|
||||||
|
* @param {Function} onStatusChange - 状态变化回调
|
||||||
|
* @returns {Promise<Object>} - { success: boolean, status: number, timeout: boolean }
|
||||||
|
*/
|
||||||
|
static async waitForAuth(formdataid, apicode, onStatusChange) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 检查超时
|
||||||
|
if (Date.now() - startTime > this.POLL_TIMEOUT) {
|
||||||
|
console.log('[AuthFlowService] 轮询超时:', apicode);
|
||||||
|
return { success: false, status: AUTH_STATUS.WAITING, timeout: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询状态
|
||||||
|
const result = await this.checkAuthStatus(formdataid, apicode);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// 通知状态变化
|
||||||
|
if (onStatusChange) {
|
||||||
|
onStatusChange(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态 >= 2 表示已回调/成功/失败,可以进入下一个
|
||||||
|
if (result.status >= AUTH_STATUS.CALLBACK) {
|
||||||
|
return {
|
||||||
|
success: result.status === AUTH_STATUS.CALLBACK || result.status === AUTH_STATUS.APPLY_OK,
|
||||||
|
status: result.status,
|
||||||
|
timeout: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待下次轮询
|
||||||
|
await this.sleep(this.POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行授权流程
|
||||||
|
* @param {number} formdataid - 表单数据ID
|
||||||
|
* @param {Array} h5Urls - H5 URL 列表 [{ apicode, apiname, h5url }]
|
||||||
|
* @param {Object} callbacks - 回调函数集合
|
||||||
|
* - onStart(item, index) - 开始处理某个产品
|
||||||
|
* - onProgress(item, index, status) - 状态更新
|
||||||
|
* - onComplete(item, index, result) - 单个产品完成
|
||||||
|
* - onAllComplete() - 全部完成
|
||||||
|
* - onIframeChange(url) - iframe 需要切换 URL
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async startAuthFlow(formdataid, h5Urls, callbacks = {}) {
|
||||||
|
const { onStart, onProgress, onComplete, onAllComplete, onIframeChange } = callbacks;
|
||||||
|
|
||||||
|
console.log('[AuthFlowService] 开始授权流程, formdataid:', formdataid, 'h5Urls:', h5Urls);
|
||||||
|
|
||||||
|
for (let i = 0; i < h5Urls.length; i++) {
|
||||||
|
const item = h5Urls[i];
|
||||||
|
console.log('[AuthFlowService] 处理第', i + 1, '个产品:', item.apiname);
|
||||||
|
|
||||||
|
// 通知开始处理
|
||||||
|
if (onStart) {
|
||||||
|
onStart(item, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换 iframe
|
||||||
|
if (onIframeChange && item.h5url) {
|
||||||
|
onIframeChange(item.h5url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询等待授权完成
|
||||||
|
const result = await this.waitForAuth(formdataid, item.apicode, (status) => {
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress(item, i, status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[AuthFlowService] 产品授权结果:', item.apiname, result);
|
||||||
|
|
||||||
|
// 通知单个完成
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(item, i, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部完成
|
||||||
|
console.log('[AuthFlowService] 全部授权流程完成');
|
||||||
|
if (onAllComplete) {
|
||||||
|
onAllComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始倒计时
|
||||||
|
* @param {number} seconds - 倒计时秒数
|
||||||
|
* @param {Function} onTick - 每秒回调,参数为剩余秒数
|
||||||
|
* @param {Function} onComplete - 倒计时完成回调
|
||||||
|
* @returns {Function} - 取消函数
|
||||||
|
*/
|
||||||
|
static startCountdown(seconds, onTick, onComplete) {
|
||||||
|
let remaining = seconds;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (onTick) {
|
||||||
|
onTick(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining--;
|
||||||
|
setTimeout(tick, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// 返回取消函数
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到目标页面
|
||||||
|
* @param {string} url - 目标 URL
|
||||||
|
*/
|
||||||
|
static redirect(url) {
|
||||||
|
if (url) {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助函数:延时
|
||||||
|
* @param {number} ms - 毫秒
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
static sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
* @param {number} status - 状态码
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static getStatusText(status) {
|
||||||
|
switch (status) {
|
||||||
|
case AUTH_STATUS.WAITING:
|
||||||
|
return '等待授权';
|
||||||
|
case AUTH_STATUS.CALLBACK:
|
||||||
|
return '已完成';
|
||||||
|
case AUTH_STATUS.APPLY_OK:
|
||||||
|
return '授权成功';
|
||||||
|
case AUTH_STATUS.APPLY_FAIL:
|
||||||
|
return '授权失败';
|
||||||
|
default:
|
||||||
|
return '未知状态';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出状态常量
|
||||||
|
export { AUTH_STATUS };
|
||||||
@@ -7,3 +7,4 @@ export { AuthService } from './auth.service.js';
|
|||||||
export { LoanService } from './loan.service.js';
|
export { LoanService } from './loan.service.js';
|
||||||
export { FormService } from './form.service.js';
|
export { FormService } from './form.service.js';
|
||||||
export { JVerifyService, getJVerifyService } from './jverify.service.js';
|
export { JVerifyService, getJVerifyService } from './jverify.service.js';
|
||||||
|
export { AuthFlowService, AUTH_STATUS } from './auth-flow.service.js';
|
||||||
|
|||||||
@@ -4,34 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getJVerifyService } from '../services/index.js';
|
import { getJVerifyService } from '../services/index.js';
|
||||||
|
import { showToast } from './toast.js';
|
||||||
|
import { API_CONFIG } from '../config/api.config.js';
|
||||||
|
|
||||||
/**
|
// 极光一键登录错误码
|
||||||
* 简单的 Toast 提示工具
|
const JVERIFY_ERROR_CODES = {
|
||||||
* @param {string} message - 提示消息
|
AUTH_FAILED: '2002', // 运营商授权失败
|
||||||
* @param {number} duration - 持续时间(毫秒)
|
TIMEOUT: '2006', // 登录超时
|
||||||
*/
|
NOT_SUPPORTED: 'not support' // 当前环境不支持
|
||||||
function showToast(message, duration = 2000) {
|
};
|
||||||
// 移除现有的 toast
|
|
||||||
const existingToast = document.querySelector('.one-click-toast');
|
|
||||||
if (existingToast) {
|
|
||||||
existingToast.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的 toast
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = 'one-click-toast';
|
|
||||||
toast.textContent = message;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
|
|
||||||
// 触发动画
|
|
||||||
setTimeout(() => toast.classList.add('show'), 10);
|
|
||||||
|
|
||||||
// 自动移除
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('show');
|
|
||||||
setTimeout(() => toast.remove(), 300);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OneClickLoginButton {
|
export class OneClickLoginButton {
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +159,7 @@ export class OneClickLoginButton {
|
|||||||
this.setButtonState('verifying');
|
this.setButtonState('verifying');
|
||||||
|
|
||||||
// 调用后端验证
|
// 调用后端验证
|
||||||
const response = await fetch('/zcore/jpush/login', {
|
const response = await fetch(API_CONFIG.ENDPOINTS.JPUSH_LOGIN, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -206,11 +187,11 @@ export class OneClickLoginButton {
|
|||||||
// 判断错误类型,提供友好提示
|
// 判断错误类型,提供友好提示
|
||||||
let message = '登录失败,请使用短信验证码登录';
|
let message = '登录失败,请使用短信验证码登录';
|
||||||
|
|
||||||
if (error.message && error.message.includes('2002')) {
|
if (error.message && error.message.includes(JVERIFY_ERROR_CODES.AUTH_FAILED)) {
|
||||||
message = '运营商授权失败,请使用短信验证码登录';
|
message = '运营商授权失败,请使用短信验证码登录';
|
||||||
} else if (error.message && error.message.includes('2006')) {
|
} else if (error.message && error.message.includes(JVERIFY_ERROR_CODES.TIMEOUT)) {
|
||||||
message = '登录超时,请重试或使用短信验证码登录';
|
message = '登录超时,请重试或使用短信验证码登录';
|
||||||
} else if (error.message && error.message.includes('not support')) {
|
} else if (error.message && error.message.includes(JVERIFY_ERROR_CODES.NOT_SUPPORTED)) {
|
||||||
message = '当前环境不支持一键登录,请使用短信验证码登录';
|
message = '当前环境不支持一键登录,请使用短信验证码登录';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user