diff --git a/basic-info.css b/basic-info.css index d0900ac..b748f8e 100644 --- a/basic-info.css +++ b/basic-info.css @@ -1075,3 +1075,239 @@ body.modal-open { .back-home-btn:active { 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); +} diff --git a/src/js/config/api.config.js b/src/js/config/api.config.js index 1e68620..d29f5df 100644 --- a/src/js/config/api.config.js +++ b/src/js/config/api.config.js @@ -27,6 +27,9 @@ export const API_CONFIG = { SUBMIT_DRAFT_FORM: '/partnerh5/save_draft', GET_DRAFT_FORM: '/partnerh5/get_draft', + // 授权状态查询接口 + CHECK_AUTH_STATUS: '/partnerh5/check_auth_status', + // 区域数据接口 AREA_LIST: '/partnerh5/area_list', }, diff --git a/src/js/pages/basic-info.page.js b/src/js/pages/basic-info.page.js index a8174aa..74ad084 100644 --- a/src/js/pages/basic-info.page.js +++ b/src/js/pages/basic-info.page.js @@ -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 { showToast } from '../ui/toast.js'; import { AreaService } from '../services/area.service.js'; +import { AuthFlowService, AUTH_STATUS } from '../services/index.js'; export class BasicInfoPage { constructor() { @@ -655,85 +656,242 @@ export class BasicInfoPage { } /** - * 显示提交成功页面 - * @param {Object} data - 返回数据 { formdataid, h5Urls } + * 显示提交成功页面(授权流程) + * @param {Object} data - 返回数据 { formdataid, h5Urls, redirectUrl } */ showSubmitSuccessDialog(data) { - const h5Urls = data.h5Urls; - const urlEntries = Object.entries(h5Urls); + const { formdataid, h5Urls, redirectUrl } = data; - // 隐藏表单内容 - document.getElementById('assetList').style.display = 'none'; - document.getElementById('basicInfoSection').style.display = 'none'; - this.elements.submitBtn.style.display = 'none'; + // 隐藏表单相关的所有内容 + const topCard = document.querySelector('.top-card'); + const assetSection = document.querySelector('.asset-section'); + const basicInfoSection = document.getElementById('basicInfoSection'); + const bottomSection = document.getElementById('bottomSection'); - // 创建成功提示区域 - const successContainer = document.createElement('div'); - successContainer.className = 'submit-success-container'; - successContainer.innerHTML = ` -
-
-

信息提交成功

-

表单ID:${data.formdataid}

-
+ if (topCard) topCard.style.display = 'none'; + if (assetSection) assetSection.style.display = 'none'; + if (basicInfoSection) basicInfoSection.style.display = 'none'; + if (bottomSection) bottomSection.style.display = 'none'; - ${urlEntries.length > 0 ? ` -
- ${urlEntries.map(([name, url], index) => ` -
-
- ${name} - 请在下方完成申请流程 -
- + // 创建授权流程容器 + const authContainer = document.createElement('div'); + authContainer.className = 'auth-flow-container'; + authContainer.id = 'authFlowContainer'; + + // 如果没有 h5Urls,显示成功提示 + if (!h5Urls || h5Urls.length === 0) { + authContainer.innerHTML = ` +
+
+
信息提交成功
+
您的申请已提交成功!
+ ${redirectUrl ? ` +
+ 5 秒后自动跳转... +
+ + ` : ` + + `} +
+ `; + + const mainContainer = document.querySelector('.container') || document.body; + mainContainer.insertBefore(authContainer, mainContainer.firstChild); + + // 如果有 redirectUrl,启动倒计时 + if (redirectUrl) { + this.startFinalCountdown(redirectUrl); + } + return; + } + + // 有 h5Urls,显示授权流程界面 + authContainer.innerHTML = ` +
+
+ 授权进度 + (1/${h5Urls.length}) +
+
+ ${h5Urls.map((item, index) => ` +
+ + ${item.apiname} + 等待中
`).join('')} - - ${urlEntries.length > 1 ? ` -
- ${urlEntries.map(([name, url], index) => ` - - `).join('')} -
- ` : ''}
- ` : ` -
-

您的申请已提交成功!

-

我们会尽快处理您的申请。

+
+
+
+ ${h5Urls[0].apiname} + 请在下方完成授权
- `} - - `; - // 插入到页面顶部 const mainContainer = document.querySelector('.container') || document.body; - mainContainer.insertBefore(successContainer, mainContainer.firstChild); + mainContainer.insertBefore(authContainer, mainContainer.firstChild); - // 绑定 Tab 切换事件 - if (urlEntries.length > 1) { - const tabs = successContainer.querySelectorAll('.product-tab'); - const iframes = successContainer.querySelectorAll('.iframe-wrapper'); + // 启动授权流程 + this.runAuthFlow(formdataid, h5Urls, redirectUrl); + } - 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 状态 - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); + await AuthFlowService.startAuthFlow(formdataid, h5Urls, { + // 开始处理某个产品 + onStart: (item, index) => { + console.log('[BasicInfoPage] 开始处理:', item.apiname); + + // 更新进度显示 + if (currentIndexEl) { + currentIndexEl.textContent = index + 1; + } + if (currentProductNameEl) { + currentProductNameEl.textContent = item.apiname; + } - // 更新 iframe 显示 - iframes.forEach(iframe => iframe.classList.remove('active')); - iframes[index].classList.add('active'); + // 更新进度列表状态 + 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 = ` +
+
全部授权完成
+
您的申请已全部提交成功!
+ ${redirectUrl ? ` +
+ 5 秒后自动跳转... +
+ + ` : ` + + `} + `; + + 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); + } + ); } /** diff --git a/src/js/services/auth-flow.service.js b/src/js/services/auth-flow.service.js new file mode 100644 index 0000000..656a949 --- /dev/null +++ b/src/js/services/auth-flow.service.js @@ -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} - 状态信息 { 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} - { 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} + */ + 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 }; diff --git a/src/js/services/index.js b/src/js/services/index.js index d823f4a..d3af52f 100644 --- a/src/js/services/index.js +++ b/src/js/services/index.js @@ -7,3 +7,4 @@ export { AuthService } from './auth.service.js'; export { LoanService } from './loan.service.js'; export { FormService } from './form.service.js'; export { JVerifyService, getJVerifyService } from './jverify.service.js'; +export { AuthFlowService, AUTH_STATUS } from './auth-flow.service.js'; diff --git a/src/js/ui/one-click-login.js b/src/js/ui/one-click-login.js index 97291d1..101799c 100644 --- a/src/js/ui/one-click-login.js +++ b/src/js/ui/one-click-login.js @@ -4,34 +4,15 @@ */ import { getJVerifyService } from '../services/index.js'; +import { showToast } from './toast.js'; +import { API_CONFIG } from '../config/api.config.js'; -/** - * 简单的 Toast 提示工具 - * @param {string} message - 提示消息 - * @param {number} duration - 持续时间(毫秒) - */ -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); -} +// 极光一键登录错误码 +const JVERIFY_ERROR_CODES = { + AUTH_FAILED: '2002', // 运营商授权失败 + TIMEOUT: '2006', // 登录超时 + NOT_SUPPORTED: 'not support' // 当前环境不支持 +}; export class OneClickLoginButton { /** @@ -178,7 +159,7 @@ export class OneClickLoginButton { this.setButtonState('verifying'); // 调用后端验证 - const response = await fetch('/zcore/jpush/login', { + const response = await fetch(API_CONFIG.ENDPOINTS.JPUSH_LOGIN, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -206,11 +187,11 @@ export class OneClickLoginButton { // 判断错误类型,提供友好提示 let message = '登录失败,请使用短信验证码登录'; - if (error.message && error.message.includes('2002')) { + if (error.message && error.message.includes(JVERIFY_ERROR_CODES.AUTH_FAILED)) { message = '运营商授权失败,请使用短信验证码登录'; - } else if (error.message && error.message.includes('2006')) { + } else if (error.message && error.message.includes(JVERIFY_ERROR_CODES.TIMEOUT)) { message = '登录超时,请重试或使用短信验证码登录'; - } else if (error.message && error.message.includes('not support')) { + } else if (error.message && error.message.includes(JVERIFY_ERROR_CODES.NOT_SUPPORTED)) { message = '当前环境不支持一键登录,请使用短信验证码登录'; }