diff --git a/.gitignore b/.gitignore index a3bddf7..3b55449 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ *.iml /tmp -.claude \ No newline at end of file +.claude/ +*-test.html \ No newline at end of file diff --git a/basic-info.css b/basic-info.css index f2f600b..d0900ac 100644 --- a/basic-info.css +++ b/basic-info.css @@ -738,9 +738,9 @@ body { } .city-picker-item.active { - background-color: #f5f5f5; - color: #333; - font-weight: 500; + background-color: #e8f0ff; + color: #3474fe; + font-weight: 600; } body.modal-open { @@ -930,3 +930,148 @@ body.modal-open { position: fixed; width: 100%; } + +/* ==================== 提交成功页面样式 ==================== */ +.submit-success-container { + max-width: 800px; + margin: 20px auto; + padding: 30px; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.success-header { + text-align: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; +} + +.success-icon { + font-size: 48px; + margin-bottom: 10px; +} + +.success-header h2 { + font-size: 24px; + color: #333; + margin: 10px 0; +} + +.formdata-id { + font-size: 14px; + color: #999; +} + +.iframe-container { + position: relative; + margin-top: 20px; +} + +.iframe-wrapper { + display: none; +} + +.iframe-wrapper.active { + display: block; +} + +.iframe-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #f8f9fa; + border-radius: 8px 8px 0 0; + border: 1px solid #e9ecef; +} + +.product-name { + font-weight: 600; + color: #333; + font-size: 16px; +} + +.iframe-hint { + font-size: 13px; + color: #999; +} + +.product-iframe { + width: 100%; + height: 600px; + border: 1px solid #e9ecef; + border-top: none; + border-radius: 0 0 8px 8px; + background: #fff; +} + +.product-tabs { + display: flex; + gap: 10px; + margin-bottom: 15px; + border-bottom: 2px solid #e9ecef; +} + +.product-tab { + flex: 1; + padding: 12px 20px; + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + font-size: 15px; + color: #666; + cursor: pointer; + transition: all 0.3s ease; +} + +.product-tab:hover { + color: #3474fe; + background: #f8f9fa; +} + +.product-tab.active { + color: #3474fe; + border-bottom-color: #3474fe; + font-weight: 600; +} + +.no-urls-message { + text-align: center; + padding: 40px 20px; + color: #666; + font-size: 16px; +} + +.no-urls-message p { + margin: 10px 0; +} + +.success-footer { + margin-top: 30px; + text-align: center; +} + +.back-home-btn { + padding: 12px 40px; + background: linear-gradient(140deg, #3474fe, #3474fe); + color: #fff; + border: none; + border-radius: 24px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.back-home-btn:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(52, 116, 254, 0.3); +} + +.back-home-btn:active { + transform: translateY(0); +} diff --git a/docs/jverify-configuration.md b/docs/jverify-configuration.md index 6f8cff4..cf37398 100644 --- a/docs/jverify-configuration.md +++ b/docs/jverify-configuration.md @@ -97,16 +97,10 @@ SDK可用? ## 🔧 后端API配置 -确保后端服务 `ali-sms` 正常运行: +后端服务部署在服务器上(非本地开发环境) -1. **启动后端服务** - ```bash - cd /e/wk-oth/go-work/ali-sms - go run main.go - ``` - -2. **确认API端点** - - 一键登录验证:`POST /auth/jpush/login` +1. **API端点** + - 一键登录验证:`POST /zcore/jpush/login` - 请求参数: ```json { @@ -115,9 +109,13 @@ SDK可用? } ``` -3. **配置CORS** - - 确保后端允许前端域名跨域访问 - - 或在开发环境使用代理 +2. **本地开发配置** + - 如需本地调试前端,请在 `src/js/config/index.js` 中配置 API 代理 + - 或直接修改 `src/js/services/jverify.service.js` 中的 `apiUrl` 指向服务器地址 + +3. **确保后端服务运行** + - 服务器上的后端服务需要正常运行并可访问 + - 确保CORS配置正确,允许前端域名访问 --- diff --git a/src/css/components/one-click-login.css b/src/css/components/one-click-login.css index 49222cc..dd4791a 100644 --- a/src/css/components/one-click-login.css +++ b/src/css/components/one-click-login.css @@ -102,3 +102,26 @@ font-size: 18px; } } + +/* Toast 提示样式 */ +.one-click-toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 9999; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + max-width: 80%; + text-align: center; +} + +.one-click-toast.show { + opacity: 1; +} diff --git a/src/js/config/api.config.js b/src/js/config/api.config.js index 5663ee4..1e68620 100644 --- a/src/js/config/api.config.js +++ b/src/js/config/api.config.js @@ -16,6 +16,9 @@ export const API_CONFIG = { SEND_SMS: '/zcore/sms/send', VERIFY_SMS: '/zcore/sms/verify', + // 极光一键登录接口(使用 JSON 格式) + JPUSH_LOGIN: '/zcore/jpush/login', + // 客户相关接口(使用 x-www-form-urlencoded 格式) CUSTOMER_REGISTER: '/partnerh5/login', @@ -23,6 +26,9 @@ export const API_CONFIG = { SUBMIT_FORM: '/partnerh5/submit', SUBMIT_DRAFT_FORM: '/partnerh5/save_draft', GET_DRAFT_FORM: '/partnerh5/get_draft', + + // 区域数据接口 + AREA_LIST: '/partnerh5/area_list', }, // 请求超时配置(毫秒) diff --git a/src/js/config/app.config.js b/src/js/config/app.config.js index 516e578..88ad133 100644 --- a/src/js/config/app.config.js +++ b/src/js/config/app.config.js @@ -136,6 +136,12 @@ export const CACHE_CONFIG = { USER_SESSION: 'flux_user_session', FORM_ID: 'flux_form_id', }, + + // 测试模式配置 + TEST_MODE: { + ENABLED: true, // 是否启用测试模式 + DEFAULT_SHORTCODE: 'sRh907', // 测试模式下默认的短链代码 + }, }; // ==================== 验证规则配置 ==================== diff --git a/src/js/core/draft-manager.js b/src/js/core/draft-manager.js index 2000199..0e7d645 100644 --- a/src/js/core/draft-manager.js +++ b/src/js/core/draft-manager.js @@ -95,8 +95,21 @@ export class DraftManager { * @private */ static _getShortcode() { + // 测试模式下使用默认 shortcode + if (CACHE_CONFIG.TEST_MODE.ENABLED) { + console.log('[DraftManager] 测试模式,使用默认 shortcode:', CACHE_CONFIG.TEST_MODE.DEFAULT_SHORTCODE); + return CACHE_CONFIG.TEST_MODE.DEFAULT_SHORTCODE; + } + const params = new URLSearchParams(window.location.search); - return params.get('code') || params.get('shortcode') || ''; + const shortcode = params.get('code') || params.get('shortcode') || ''; + + if (!shortcode) { + console.warn('[DraftManager] URL 中未找到 code 或 shortcode 参数'); + console.warn('[DraftManager] 当前 URL:', window.location.href); + } + + return shortcode; } /** @@ -107,22 +120,29 @@ export class DraftManager { */ static async saveDraft(selectedValues, basicInfoValues) { try { + const formId = FormIdGenerator.getOrCreate(); + console.log('[DraftManager] 保存草稿,formId:', formId); + const formData = this._buildDraftData(selectedValues, basicInfoValues); const requestData = { ...this._convertToServerFormat(formData), shortcode: this._getShortcode() }; + console.log('[DraftManager] 保存草稿请求数据:', requestData); + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.SUBMIT_DRAFT_FORM, requestData); + console.log('[DraftManager] 保存草稿响应:', response); if (response.retcode === 0) { - console.log('[DraftManager] 草稿保存成功,Form ID:', FormIdGenerator.getOrCreate()); + console.log('[DraftManager] 草稿保存成功,Form ID:', formId); return { success: true, message: '草稿保存成功', data: response.result }; } else { + console.error('[DraftManager] 草稿保存失败,retcode:', response.retcode, 'retinfo:', response.retinfo); return { success: false, message: response.retinfo || '草稿保存失败' @@ -143,6 +163,8 @@ export class DraftManager { */ static async loadDraft() { const formId = FormIdGenerator.getCurrent(); + console.log('[DraftManager] 开始加载草稿,formId:', formId); + if (!formId) { console.log('[DraftManager] 没有表单ID,无法加载草稿'); return null; @@ -150,10 +172,15 @@ export class DraftManager { try { const requestData = { formid: formId }; + console.log('[DraftManager] 请求参数:', requestData); + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.GET_DRAFT_FORM, requestData); + console.log('[DraftManager] 响应结果:', response); if (response.retcode === 0 && response.result) { const draftData = response.result; + console.log('[DraftManager] 草稿数据:', draftData); + console.log('[DraftManager] 草稿状态 draftstatus:', draftData.draftstatus); // 检查是否是草稿状态 if (draftData.draftstatus === 1) { @@ -164,7 +191,7 @@ export class DraftManager { return null; } } else { - console.log('[DraftManager] 没有找到草稿数据'); + console.log('[DraftManager] 没有找到草稿数据,retcode:', response.retcode, 'retinfo:', response.retinfo); return null; } } catch (error) { @@ -180,4 +207,50 @@ export class DraftManager { FormIdGenerator.clear(); console.log('[DraftManager] 已清除草稿数据'); } + + /** + * 提交表单 + * @param {Object} selectedValues - 资产信息 + * @param {Object} basicInfoValues - 基本信息Values - 表单数据对象 + * @returns {Promise} - 提交结果 + */ + static async submitForm(selectedValues, basicInfoValues) { + try { + const formId = FormIdGenerator.getOrCreate(); + console.log('[DraftManager] 提交表单,formId:', formId); + + const formData = this._buildDraftData(selectedValues, basicInfoValues); + const requestData = { + ...this._convertToServerFormat(formData), + shortcode: this._getShortcode(), + draftstatus: 0 // 正式提交 + }; + + console.log('[DraftManager] 提交表单请求数据:', requestData); + + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.SUBMIT_FORM, requestData); + console.log('[DraftManager] 提交表单响应:', response); + + if (response.retcode === 0) { + console.log('[DraftManager] 表单提交成功,Form ID:', formId); + return { + success: true, + message: '提交成功', + data: response.result + }; + } else { + console.error('[DraftManager] 表单提交失败,retcode:', response.retcode, 'retinfo:', response.retinfo); + return { + success: false, + message: response.retinfo || '提交失败' + }; + } + } catch (error) { + console.error('[DraftManager] 表单提交出错:', error); + return { + success: false, + message: '网络错误,提交失败' + }; + } + } } diff --git a/src/js/pages/basic-info.page.js b/src/js/pages/basic-info.page.js index e5ad0ef..a8174aa 100644 --- a/src/js/pages/basic-info.page.js +++ b/src/js/pages/basic-info.page.js @@ -6,8 +6,9 @@ import { CityPicker, Modal } from '../ui/index.js'; import { Validator, Formatter } from '../utils/index.js'; import { DraftManager, FormIdGenerator, UserCache } from '../core/index.js'; -import { ASSET_CONFIG, BASIC_INFO_CONFIG, PROVINCE_CITY_DATA } 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 { AreaService } from '../services/area.service.js'; export class BasicInfoPage { constructor() { @@ -23,6 +24,9 @@ export class BasicInfoPage { this.isSavingDraft = false; this.autoSaveTimer = null; + // 身份证自动填充标记 + this.lastFilledAreaCode = null; + // 组件实例 this.cityPicker = null; this.agreementModal = null; @@ -66,13 +70,6 @@ export class BasicInfoPage { this.renderForm(); this.bindEvents(); this.updateProgress(); - - // 尝试加载草稿数据 - const draftData = await DraftManager.loadDraft(); - if (draftData) { - this.restoreDraftData(draftData); - showToast('已恢复草稿数据'); - } } /** @@ -225,6 +222,10 @@ export class BasicInfoPage { * @param {string} value - 选项值 */ selectAssetOption(itemId, value) { + console.log('[BasicInfoPage] 选择资产选项:', itemId, '=', value); + + // 检查是否是新选择(之前没有值或值改变) + const isNewSelection = !this.selectedValues[itemId]; this.selectedValues[itemId] = value; // 更新 UI @@ -259,13 +260,17 @@ export class BasicInfoPage { // 自动保存草稿 this.autoSaveDraft(); - // 渐进式显示下一项 + // 渐进式显示:计算当前已完成的最大索引 const currentIndex = ASSET_CONFIG.ITEMS.findIndex(item => item.id === itemId); + const completedCount = Object.keys(this.selectedValues).length; + console.log('[BasicInfoPage] 当前选项索引:', currentIndex, '已完成数量:', completedCount, 'currentStep:', this.currentStep); + + // 如果刚完成的是当前 step 的项,显示下一项 if (currentIndex === this.currentStep && this.currentStep < ASSET_CONFIG.ITEMS.length - 1) { setTimeout(() => { this.revealNextItem(); }, 400); - } else if (currentIndex === ASSET_CONFIG.ITEMS.length - 1) { + } else if (completedCount === ASSET_CONFIG.ITEMS.length) { // 资产信息全部完成,显示基本信息区域 setTimeout(() => { this.showBasicInfoSection(); @@ -324,7 +329,7 @@ export class BasicInfoPage { const input = infoItem.querySelector(`#basic-input-${item.id}`); const errorEl = infoItem.querySelector(`#error-${item.id}`); - input.addEventListener('input', () => { + input.addEventListener('input', async () => { this.basicInfoValues[item.id] = input.value.trim(); this.updateBasicInfoProgress(); this.checkSubmitButton(); @@ -334,6 +339,24 @@ export class BasicInfoPage { if (errorEl) { errorEl.style.display = 'none'; } + + // 身份证输入到6位时,自动填充地区 + if (item.id === 'idCard' && input.value.trim().length >= 6) { + const areaCode = Validator.extractAreaCode(input.value); + console.log('[BasicInfoPage] 身份证输入,提取地区代码:', areaCode); + + if (areaCode && areaCode !== this.lastFilledAreaCode) { + console.log('[BasicInfoPage] 查询地区信息...'); + const areaInfo = await AreaService.getAreaByCode(areaCode); + console.log('[BasicInfoPage] 查询结果:', areaInfo); + + if (areaInfo) { + this.handleCityConfirm(areaInfo); + this.lastFilledAreaCode = areaCode; + console.log('[BasicInfoPage] 地区自动填充成功'); + } + } + } }); } }); @@ -353,8 +376,9 @@ export class BasicInfoPage { */ handleCityConfirm(result) { this.basicInfoValues.city = result.value; - const valueEl = document.getElementById('basic-value-city'); + // 更新显示 + const valueEl = document.getElementById('basic-value-city'); if (valueEl) { valueEl.classList.add('selected'); const textEl = valueEl.querySelector('.item-value-text'); @@ -369,9 +393,24 @@ export class BasicInfoPage { } } + // 同步更新城市选择器的选中状态 + if (this.cityPicker && result.provinceCode && result.cityCode) { + this.cityPicker.setValue(result.value); + this.cityPicker.selectedProvince = result.province; + this.cityPicker.selectedCity = result.city; + this.cityPicker.selectedProvinceCode = result.provinceCode; + this.cityPicker.selectedCityCode = result.cityCode; + this.cityPicker.tempSelectedProvince = result.province; + this.cityPicker.tempSelectedCity = result.city; + this.cityPicker.tempSelectedProvinceCode = result.provinceCode; + this.cityPicker.tempSelectedCityCode = result.cityCode; + } + this.updateBasicInfoProgress(); this.checkSubmitButton(); this.autoSaveDraft(); + + console.log('[BasicInfoPage] 城市已更新并同步选择器:', result); } /** @@ -386,8 +425,10 @@ export class BasicInfoPage { * 显示下一项 */ revealNextItem() { + console.log('[BasicInfoPage] revealNextItem 被调用,当前 currentStep:', this.currentStep); if (this.currentStep < ASSET_CONFIG.ITEMS.length - 1) { this.currentStep++; + console.log('[BasicInfoPage] currentStep 更新为:', this.currentStep, '准备显示第', this.currentStep, '项'); this.revealItem(this.currentStep); } } @@ -397,13 +438,20 @@ export class BasicInfoPage { * @param {number} index - 索引 */ revealItem(index) { - const item = document.getElementById(`asset-${ASSET_CONFIG.ITEMS[index].id}`); + const itemId = ASSET_CONFIG.ITEMS[index].id; + const item = document.getElementById(`asset-${itemId}`); + console.log('[BasicInfoPage] revealItem 被调用,index:', index, 'itemId:', itemId, '找到元素:', !!item); + if (item) { item.style.display = 'block'; item.classList.remove('collapsed'); + console.log('[BasicInfoPage] 元素已设置为 display:block'); requestAnimationFrame(() => { item.classList.add('show'); + console.log('[BasicInfoPage] 元素已添加 .show 类'); }); + } else { + console.error('[BasicInfoPage] 未找到元素: asset-' + itemId); } } @@ -465,40 +513,6 @@ export class BasicInfoPage { } } - /** - * 恢复草稿数据 - * @param {Object} draftData - 草稿数据 - */ - restoreDraftData(draftData) { - // 恢复资产信息 - Object.keys(draftData.assets).forEach(itemId => { - if (draftData.assets[itemId]) { - this.selectedValues[itemId] = draftData.assets[itemId]; - } - }); - - // 恢复基本信息 - Object.keys(draftData.basicInfo).forEach(itemId => { - if (draftData.basicInfo[itemId]) { - this.basicInfoValues[itemId] = draftData.basicInfo[itemId]; - } - }); - - // 重新渲染表单 - this.renderForm(); - - // 更新进度 - this.updateProgress(); - this.updateBasicInfoProgress(); - this.checkSubmitButton(); - - // 显示基本信息区域(如果资产信息已全部完成) - const assetCompleted = Object.keys(this.selectedValues).length; - if (assetCompleted >= ASSET_CONFIG.ITEMS.length) { - this.showBasicInfoSection(); - } - } - /** * 自动保存草稿 */ @@ -525,6 +539,16 @@ export class BasicInfoPage { const result = await DraftManager.saveDraft(this.selectedValues, this.basicInfoValues); if (result.success) { console.log('[BasicInfoPage] 草稿自动保存成功'); + } else { + // 保存失败,显示提示 + console.warn('[BasicInfoPage] 草稿保存失败:', result.message); + if (result.message.includes('短链无效')) { + console.error('[BasicInfoPage] 错误:URL 中缺少 shortcode 参数'); + console.error('[BasicInfoPage] 当前 URL:', window.location.href); + console.error('[BasicInfoPage] 解决方案:'); + console.error(' 1. 在 URL 中添加 ?code=your_shortcode'); + console.error(' 2. 或启用测试模式(CACHE_CONFIG.TEST_MODE.ENABLED = true)'); + } } } catch (error) { console.error('[BasicInfoPage] 草稿自动保存出错:', error); @@ -597,23 +621,121 @@ export class BasicInfoPage { this.isSubmitting = true; try { - // 这里需要调用 FormService.submitForm() 方法 - // 由于需要转换数据格式,暂时使用 console.log - showToast('表单提交功能待实现'); + // 调用提交 API + const response = await DraftManager.submitForm(this.selectedValues, this.basicInfoValues); + console.log('[BasicInfoPage] 提交响应:', response); - // 提交成功后的处理: - // FormService.clearDraft(); - // showToast('信息提交成功!'); + if (response.success) { + // 清除草稿 + DraftManager.clearDraft(); + + // 提交成功,显示结果 + if (response.data && response.data.h5Urls) { + // 如果有返回的 H5 URL,显示跳转选项 + this.showSubmitSuccessDialog(response.data); + } else { + // 普通成功提示 + showToast('信息提交成功!'); + // 延迟跳转或刷新 + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } else { + throw new Error(response.message || '提交失败'); + } } catch (error) { console.error('提交失败:', error); - showToast('提交失败,请稍后重试'); + showToast(error.message || '提交失败,请稍后重试'); this.elements.submitBtn.disabled = false; this.elements.submitBtn.textContent = '下一步'; this.isSubmitting = false; } } + /** + * 显示提交成功页面 + * @param {Object} data - 返回数据 { formdataid, h5Urls } + */ + showSubmitSuccessDialog(data) { + const h5Urls = data.h5Urls; + const urlEntries = Object.entries(h5Urls); + + // 隐藏表单内容 + document.getElementById('assetList').style.display = 'none'; + document.getElementById('basicInfoSection').style.display = 'none'; + this.elements.submitBtn.style.display = 'none'; + + // 创建成功提示区域 + const successContainer = document.createElement('div'); + successContainer.className = 'submit-success-container'; + successContainer.innerHTML = ` +
+
+

信息提交成功

+

表单ID:${data.formdataid}

+
+ + ${urlEntries.length > 0 ? ` +
+ ${urlEntries.map(([name, url], index) => ` +
+
+ ${name} + 请在下方完成申请流程 +
+ +
+ `).join('')} + + ${urlEntries.length > 1 ? ` +
+ ${urlEntries.map(([name, url], index) => ` + + `).join('')} +
+ ` : ''} +
+ ` : ` +
+

您的申请已提交成功!

+

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

+
+ `} + + + `; + + // 插入到页面顶部 + const mainContainer = document.querySelector('.container') || document.body; + mainContainer.insertBefore(successContainer, mainContainer.firstChild); + + // 绑定 Tab 切换事件 + if (urlEntries.length > 1) { + const tabs = successContainer.querySelectorAll('.product-tab'); + const iframes = successContainer.querySelectorAll('.iframe-wrapper'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const index = parseInt(tab.dataset.index); + + // 更新 Tab 状态 + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // 更新 iframe 显示 + iframes.forEach(iframe => iframe.classList.remove('active')); + iframes[index].classList.add('active'); + }); + }); + } + } + /** * 显示字段错误 * @param {string} fieldId - 字段ID diff --git a/src/js/services/area.service.js b/src/js/services/area.service.js new file mode 100644 index 0000000..fa35650 --- /dev/null +++ b/src/js/services/area.service.js @@ -0,0 +1,136 @@ +/** + * 区域数据服务 + * 提供省市区数据查询功能 + */ + +import { API_CONFIG } from '../config/api.config.js'; +import { ApiClient } from '../core/api.js'; + +const CACHE_KEY_PREFIX = 'area_cache_'; +const CACHE_DURATION = 10 * 60 * 1000; // 10分钟 + +/** + * 区域服务 + */ +export class AreaService { + /** + * 获取区域列表 + * @param {string} provincecode - 省份代码(可选) + * @returns {Promise} 区域列表 [{code, name}] + */ + static async getAreaList(provincecode) { + // 检查缓存 + const cacheKey = `${CACHE_KEY_PREFIX}${provincecode || 'all'}`; + const cached = localStorage.getItem(cacheKey); + + if (cached) { + const { data, timestamp } = JSON.parse(cached); + if (Date.now() - timestamp < CACHE_DURATION) { + return data; + } + } + + try { + // 构建请求参数 + const params = {}; + if (provincecode) { + params.provincecode = provincecode; + } + + // 使用 ApiClient 发送请求 + const result = await ApiClient.get(API_CONFIG.ENDPOINTS.AREA_LIST, params); + + if (result.retcode !== 0) { + throw new Error(result.retmsg || '获取区域数据失败'); + } + + const areaList = result.result || []; + + // 保存到缓存 + localStorage.setItem(cacheKey, JSON.stringify({ + data: areaList, + timestamp: Date.now() + })); + + return areaList; + } catch (error) { + console.error('[AreaService] 获取区域数据失败:', error); + throw error; + } + } + + /** + * 获取所有省份 + * @returns {Promise} 省份列表 + */ + static async getProvinces() { + return this.getAreaList(); + } + + /** + * 获取指定省份的市和区 + * @param {string} provincecode - 省份代码(如:13) + * @returns {Promise} 市区列表 + */ + static async getCities(provincecode) { + if (!provincecode) { + throw new Error('省份代码不能为空'); + } + return this.getAreaList(provincecode); + } + + /** + * 清除缓存 + */ + static clearCache() { + Object.keys(localStorage) + .filter(key => key.startsWith(CACHE_KEY_PREFIX)) + .forEach(key => localStorage.removeItem(key)); + } + + /** + * 根据地区代码(身份证前6位)查询地区信息 + * @param {string} areaCode - 地区代码(6位) + * @returns {Promise} - 地区信息 {province, city, district, value},如果未找到则返回 null + */ + static async getAreaByCode(areaCode) { + if (!areaCode || areaCode.length < 6) { + return null; + } + + try { + const provincecode = areaCode.substring(0, 2); + + // 并行查询省列表和该省的市区列表 + const [provinces, areas] = await Promise.all([ + this.getProvinces(), + this.getAreaList(provincecode) + ]); + + // 查找省、市、区 + const province = provinces.find(p => p.code === provincecode); + const city = areas.find(a => a.code === areaCode.substring(0, 4)); + const district = areas.find(a => a.code === areaCode); + + // 至少要有省份才返回 + if (province) { + return { + province: province.name, + city: city ? city.name : '', + district: district ? district.name : '', + provinceCode: province.code, + cityCode: city ? city.code : '', + districtCode: district ? district.code : '', + value: city ? `${province.name}/${city.name}` : province.name + }; + } + + return null; + } catch (error) { + console.error('[AreaService] 根据代码查询地区失败:', error); + return null; + } + } +} + +export default AreaService; diff --git a/src/js/services/jverify.service.js b/src/js/services/jverify.service.js index 0291908..1c32e3f 100644 --- a/src/js/services/jverify.service.js +++ b/src/js/services/jverify.service.js @@ -16,7 +16,7 @@ export class JVerifyService { this.config = { appId: '', // 极光应用ID // 后端 API 地址 - apiUrl: '/auth/jpush/login' + apiUrl: '/zcore/jpush/login' }; } @@ -41,14 +41,23 @@ export class JVerifyService { if (window.JVerificationInterface && window.JVerificationInterface.init) { // 调试模式配置 const debugMode = DEBUG_CONFIG.ENABLED; + // 获取当前页面完整URL(必需参数) + const domain = window.location.origin; - console.log('[JVerifyService] 准备调用SDK.init, appkey:', appId); + console.log('[JVerifyService] 准备调用SDK.init, appkey:', appId, 'domainName:', domain); // 初始化SDK window.JVerificationInterface.init({ appkey: appId, // 注意:官方文档中是 appkey(全小写),不是 appKey + domainName: domain, // 当前页面完整URL(必需参数,包含协议) debugMode: debugMode, success: () => { console.log('[JVerifyService] 极光SDK初始化成功'); + + // 延迟设置UI样式(等待SDK完全加载) + setTimeout(() => { + this.setupCustomUI(); + }, 500); + this.isAvailable = true; this.isLoaded = true; }, @@ -225,7 +234,8 @@ export class JVerifyService { window.JVerificationInterface.getToken({ success: (result) => { console.log('[JVerifyService] 获取token成功:', result); - resolve(result.token); // 返回登录token + // 根据官方文档,token 在 content 字段中 + resolve(result.content); }, fail: (error) => { console.error('[JVerifyService] 获取token失败:', error); @@ -238,6 +248,122 @@ export class JVerifyService { }); } + /** + * 一键登录(使用官方 loginAuth 方法,提供完整UI体验) + * @param {Object} options - 配置选项 + * @param {string} options.operater - 优先运营商(CM/CU/CT) + * @param {string} options.type - 登录模式(full: 全屏, dialog: 弹窗) + * @param {number} options.timeout - 超时时间(毫秒,默认9000) + * @returns {Promise<{token: string, operater: string}>} + */ + async loginAuth(options = {}) { + const { + operater = 'CM', // 默认优先移动 + type = 'full', // 默认全屏模式 + timeout = 9000 // 默认9秒超时 + } = options; + + return new Promise((resolve, reject) => { + if (!this.isAvailable) { + reject(new Error('极光一键登录不可用')); + return; + } + + try { + console.log('[JVerifyService] 调用一键登录 loginAuth, operater:', operater, 'type:', type); + + // 使用官方 loginAuth 方法 + window.JVerificationInterface.loginAuth({ + operater: operater, + type: type, + timeout: timeout, + success: (result) => { + console.log('[JVerifyService] 一键登录成功:', result); + resolve({ + token: result.content, // 登录token + operater: result.operater // 运营商 + }); + }, + fail: (error) => { + console.error('[JVerifyService] 一键登录失败:', error); + reject(new Error(error.code + ': ' + error.message)); + } + }); + } catch (error) { + console.error('[JVerifyService] 一键登录异常:', error); + reject(error); + } + }); + } + + /** + * 设置一键登录UI样式 + * @param {Object} uiConfig - UI配置 + * @param {string} uiConfig.logo - Logo图片URL + * @param {string} uiConfig.appName - 应用名称 + * @param {string} uiConfig.loginBtnColor - 登录按钮颜色 + * @param {string} uiConfig.loginTextColor - 登录按钮文字颜色 + */ + setCustomUI(uiConfig = {}) { + // 检查SDK是否可用 + if (!window.JVerificationInterface) { + console.warn('[JVerifyService] JVerificationInterface 不存在'); + return false; + } + + // 检查方法是否存在 + if (!window.JVerificationInterface.setCustomUIWithConfig) { + console.warn('[JVerifyService] setCustomUIWithConfig 方法不存在,可能SDK版本不支持UI定制'); + console.log('[JVerifyService] 可用方法:', Object.keys(window.JVerificationInterface).filter(k => typeof window.JVerificationInterface[k] === 'function')); + return false; + } + + try { + window.JVerificationInterface.setCustomUIWithConfig(uiConfig); + console.log('[JVerifyService] UI设置成功:', uiConfig); + return true; + } catch (error) { + console.error('[JVerifyService] UI设置失败:', error); + return false; + } + } + + /** + * 自动设置官方UI样式(在SDK初始化成功后调用) + * @private + */ + setupCustomUI() { + // 根据当前页面主题设置UI + const uiConfig = { + // Logo设置(尺寸建议:弹窗模式 60x60) + logo: 'https://via.placeholder.com/60x60/667eea/ffffff?text=薇', // 替换为实际logo + + // 应用名称(最多15个字符) + appName: '薇钱包', + + // 登录按钮颜色(与自定义按钮保持一致) + loginBtnColor: '#667eea', + + // 登录按钮文字颜色 + loginTextColor: '#ffffff', + + // 协议链接颜色 + customPolicyLinkColor: '#667eea', + + // 是否显示其他登录方式按钮(电信不支持) + isDisplayOtherWayBtn: false, + + // 其他登录方式按钮文字颜色 + customOtherWayTextColor: '#666666' + }; + + const success = this.setCustomUI(uiConfig); + + if (!success) { + console.log('[JVerifyService] UI定制不可用,将使用运营商默认样式(不影响功能)'); + } + } + /** * 使用极光一键登录 * @param {string} appId - 极光应用ID @@ -300,31 +426,44 @@ export class JVerifyService { /** * 预登录(检查运营商网络) + * 使用官方推荐的 checkVerifyEnable 和 isCellular 方法 * @returns {Promise} */ async preLogin() { - return new Promise((resolve) => { - if (!this.isAvailable) { - resolve(false); - return; + if (!this.isAvailable) { + return false; + } + + try { + // 使用官方 API 检查网络环境 + const isVerifyEnabled = window.JVerificationInterface.checkVerifyEnable(); + if (!isVerifyEnabled) { + console.log('[JVerifyService] 当前网络环境不支持认证'); + return false; } - try { - window.JVerificationInterface.checkLogin({ - success: () => { - console.log('[JVerifyService] 运营商网络检查通过'); - resolve(true); - }, - fail: (error) => { - console.warn('[JVerifyService] 运营商网络检查失败:', error); - resolve(false); - } - }); - } catch (error) { - console.error('[JVerifyService] 预登录检查异常:', error); - resolve(false); + const isCellular = window.JVerificationInterface.isCellular(); + + // WiFi检测说明: + // - 移动端WiFi:官方不支持一键登录(需要蜂窝网络) + // - 手机热点:电脑通过WiFi连接热点,实际底层是蜂窝网络 + // - 为了测试方便,这里做宽松检测,允许尝试 + const isWifi = window.JVerificationInterface.isWifi(); + + if (isWifi && !isCellular) { + // 纯WiFi环境(非热点),不支持 + console.log('[JVerifyService] 当前网络环境是WiFi网络,不支持一键登录'); + return false; } - }); + + // 如果是蜂窝网络,或者无法确定网络类型,允许尝试 + console.log('[JVerifyService] 运营商网络检查通过'); + return true; + } catch (error) { + console.error('[JVerifyService] 预登录检查异常:', error); + // 检查失败时也允许尝试,让运营商API来判断 + return true; + } } /** diff --git a/src/js/ui/city-picker.js b/src/js/ui/city-picker.js index 2e066db..8743270 100644 --- a/src/js/ui/city-picker.js +++ b/src/js/ui/city-picker.js @@ -3,7 +3,7 @@ * 提供省份和城市的联动选择功能 */ -import { PROVINCE_CITY_DATA } from '../config/index.js'; +import { AreaService } from '../services/area.service.js'; export class CityPicker { /** @@ -27,8 +27,16 @@ export class CityPicker { // 选中状态 this.selectedProvince = ''; this.selectedCity = ''; + this.selectedProvinceCode = ''; + this.selectedCityCode = ''; this.tempSelectedProvince = ''; this.tempSelectedCity = ''; + this.tempSelectedProvinceCode = ''; + this.tempSelectedCityCode = ''; + + // 数据缓存 + this.provinces = []; + this.cities = []; if (!this.modal) { console.error(`[CityPicker] 找不到模态框元素: ${options.modalId}`); @@ -58,8 +66,20 @@ export class CityPicker { overlay.addEventListener('click', () => this.close()); } - // 渲染省份列表 - this.renderProvinceList(); + // 预加载省份数据 + this.loadProvinces(); + } + + /** + * 加载省份数据 + */ + async loadProvinces() { + try { + this.provinces = await AreaService.getProvinces(); + } catch (error) { + console.error('[CityPicker] 加载省份数据失败:', error); + this.provinces = []; + } } /** @@ -69,16 +89,16 @@ export class CityPicker { if (!this.provinceColumn) return; this.provinceColumn.innerHTML = ''; - const provinces = Object.keys(PROVINCE_CITY_DATA); - provinces.forEach(province => { + this.provinces.forEach(province => { const provinceItem = document.createElement('div'); provinceItem.className = 'city-picker-item'; - provinceItem.textContent = province; - provinceItem.dataset.province = province; + provinceItem.textContent = province.name; + provinceItem.dataset.province = province.name; + provinceItem.dataset.provinceCode = province.code; provinceItem.addEventListener('click', () => { - this.selectProvince(province); + this.selectProvince(province.name, province.code); }); this.provinceColumn.appendChild(provinceItem); @@ -87,64 +107,90 @@ export class CityPicker { /** * 选择省份 - * @param {string} province - 省份 + * @param {string} provinceName - 省份名称 + * @param {string} provinceCode - 省份代码 */ - selectProvince(province) { - this.tempSelectedProvince = province; + async selectProvince(provinceName, provinceCode) { + this.tempSelectedProvince = provinceName; + this.tempSelectedProvinceCode = provinceCode; // 更新省份选中状态 const items = this.provinceColumn.querySelectorAll('.city-picker-item'); items.forEach(item => { - item.classList.toggle('active', item.dataset.province === province); + item.classList.toggle('active', item.dataset.provinceCode === provinceCode); }); - // 渲染城市列表 - this.renderCityList(province); + // 加载并渲染城市列表 + await this.loadCities(provinceCode); + } + + /** + * 加载城市数据 + * @param {string} provinceCode - 省份代码 + */ + async loadCities(provinceCode) { + try { + this.cities = await AreaService.getCities(provinceCode); + this.renderCityList(); + } catch (error) { + console.error('[CityPicker] 加载城市数据失败:', error); + this.cities = []; + } } /** * 渲染城市列表 - * @param {string} province - 省份 */ - renderCityList(province) { + renderCityList() { if (!this.cityColumn) return; this.cityColumn.innerHTML = ''; - const cities = PROVINCE_CITY_DATA[province] || []; + + // 只显示市(4位),不显示区(6位) + const cities = this.cities.filter(area => area.code.length === 4); cities.forEach(city => { const cityItem = document.createElement('div'); cityItem.className = 'city-picker-item'; - cityItem.textContent = city; - cityItem.dataset.city = city; + cityItem.textContent = city.name; + cityItem.dataset.city = city.name; + cityItem.dataset.cityCode = city.code; cityItem.addEventListener('click', () => { - this.selectCity(city); + this.selectCity(city.name, city.code); }); this.cityColumn.appendChild(cityItem); }); - // 如果之前选择了这个省份的城市,自动选中 - if (this.tempSelectedCity && cities.includes(this.tempSelectedCity)) { - this.selectCity(this.tempSelectedCity); + // 如果之前选择了这个省的城市,自动选中 + if (this.tempSelectedCityCode) { + const existingCity = cities.find(c => c.code === this.tempSelectedCityCode); + if (existingCity) { + this.selectCity(existingCity.name, existingCity.code); + } else if (cities.length > 0) { + // 默认选择第一个 + this.selectCity(cities[0].name, cities[0].code); + } } else if (cities.length > 0) { - // 默认选择第一个城市 - this.selectCity(cities[0]); + // 默认选择第一个 + this.selectCity(cities[0].name, cities[0].code); } } /** * 选择城市 - * @param {string} city - 城市 + * @param {string} cityName - 城市名称 + * @param {string} cityCode - 城市代码 */ - selectCity(city) { - this.tempSelectedCity = city; + selectCity(cityName, cityCode) { + this.tempSelectedCity = cityName; + this.tempSelectedCityCode = cityCode; // 更新城市选中状态 const items = this.cityColumn.querySelectorAll('.city-picker-item'); items.forEach(item => { - item.classList.toggle('active', item.dataset.city === city); + item.classList.toggle('active', item.dataset.cityCode === cityCode); }); } @@ -152,28 +198,49 @@ export class CityPicker { * 打开选择器 * @param {string} currentValue - 当前值(格式:"省/市") */ - open(currentValue = '') { - // 解析当前值 + async open(currentValue = '') { + // 解析当前值,并查找对应的代码 if (currentValue) { const parts = currentValue.split('/'); if (parts.length === 2) { - this.tempSelectedProvince = parts[0]; - this.tempSelectedCity = parts[1]; + const provinceName = parts[0]; + const cityName = parts[1]; + + // 查找省份代码 + if (this.provinces.length === 0) { + await this.loadProvinces(); + } + const province = this.provinces.find(p => p.name === provinceName); + if (province) { + this.tempSelectedProvince = provinceName; + this.tempSelectedProvinceCode = province.code; + + // 加载城市数据并查找城市代码 + await this.loadCities(province.code); + const city = this.cities.find(c => c.name === cityName && c.code.length === 4); + if (city) { + this.tempSelectedCity = cityName; + this.tempSelectedCityCode = city.code; + } + } } } + // 等待省份数据加载完成 + if (this.provinces.length === 0) { + await this.loadProvinces(); + } + // 如果没有选中,默认选择第一个省份 - if (!this.tempSelectedProvince) { - const provinces = Object.keys(PROVINCE_CITY_DATA); - if (provinces.length > 0) { - this.tempSelectedProvince = provinces[0]; - } + if (!this.tempSelectedProvince && this.provinces.length > 0) { + this.tempSelectedProvince = this.provinces[0].name; + this.tempSelectedProvinceCode = this.provinces[0].code; } // 渲染列表 this.renderProvinceList(); - if (this.tempSelectedProvince) { - this.selectProvince(this.tempSelectedProvince); + if (this.tempSelectedProvinceCode) { + await this.selectProvince(this.tempSelectedProvince, this.tempSelectedProvinceCode); } // 显示模态框 @@ -193,6 +260,8 @@ export class CityPicker { // 恢复选中状态 this.tempSelectedProvince = this.selectedProvince; this.tempSelectedCity = this.selectedCity; + this.tempSelectedProvinceCode = this.selectedProvinceCode; + this.tempSelectedCityCode = this.selectedCityCode; } /** @@ -202,6 +271,8 @@ export class CityPicker { if (this.tempSelectedProvince && this.tempSelectedCity) { this.selectedProvince = this.tempSelectedProvince; this.selectedCity = this.tempSelectedCity; + this.selectedProvinceCode = this.tempSelectedProvinceCode; + this.selectedCityCode = this.tempSelectedCityCode; const cityValue = `${this.tempSelectedProvince}/${this.tempSelectedCity}`; @@ -209,6 +280,8 @@ export class CityPicker { this.onConfirm({ province: this.selectedProvince, city: this.selectedCity, + provinceCode: this.selectedProvinceCode, + cityCode: this.selectedCityCode, value: cityValue }); } @@ -230,6 +303,7 @@ export class CityPicker { this.selectedCity = parts[1]; this.tempSelectedProvince = parts[0]; this.tempSelectedCity = parts[1]; + // 代码需要通过查找获取,这里暂时留空 } } @@ -241,6 +315,8 @@ export class CityPicker { return { province: this.selectedProvince, city: this.selectedCity, + provinceCode: this.selectedProvinceCode, + cityCode: this.selectedCityCode, value: `${this.selectedProvince}/${this.selectedCity}` }; } @@ -251,8 +327,12 @@ export class CityPicker { reset() { this.selectedProvince = ''; this.selectedCity = ''; + this.selectedProvinceCode = ''; + this.selectedCityCode = ''; this.tempSelectedProvince = ''; this.tempSelectedCity = ''; + this.tempSelectedProvinceCode = ''; + this.tempSelectedCityCode = ''; } /** diff --git a/src/js/ui/one-click-login.js b/src/js/ui/one-click-login.js index 4c5cddb..97291d1 100644 --- a/src/js/ui/one-click-login.js +++ b/src/js/ui/one-click-login.js @@ -5,6 +5,34 @@ import { getJVerifyService } from '../services/index.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); +} + export class OneClickLoginButton { /** * 构造函数 @@ -124,7 +152,7 @@ export class OneClickLoginButton { } /** - * 处理点击事件 + * 处理点击事件 - 使用极光官方 loginAuth 方法 */ async handleClick() { if (this.isLoading) { @@ -136,31 +164,84 @@ export class OneClickLoginButton { try { const jVerifyService = getJVerifyService(); - const result = await jVerifyService.login(this.appId); - if (result.success) { + // 使用官方 loginAuth 方法,提供完整的一键登录体验 + const loginResult = await jVerifyService.loginAuth({ + operater: 'CM', // 优先中国移动 + type: 'dialog', // 弹窗模式(更适合H5) + timeout: 9000 // 9秒超时 + }); + + console.log('[OneClickLoginButton] 获取授权token成功:', loginResult); + + // 显示验证中状态 + this.setButtonState('verifying'); + + // 调用后端验证 + const response = await fetch('/zcore/jpush/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + appId: this.appId, + loginToken: loginResult.token + }) + }); + + const data = await response.json(); + + if (data.retcode === 0 && data.result && data.result.phone) { // 登录成功 - console.log('[OneClickLoginButton] 登录成功:', result.phone); + console.log('[OneClickLoginButton] 登录成功:', data.result.phoneMasked); if (this.onSuccess) { - this.onSuccess(result.phone); + this.onSuccess(data.result.phone); } } else { - // 登录失败,降级到短信验证 - console.log('[OneClickLoginButton] 登录失败:', result.message); - this.showFallbackMessage(result.message); - if (this.onFallback) { - this.onFallback(); - } + throw new Error(data.retinfo || '后端验证失败'); } } catch (error) { console.error('[OneClickLoginButton] 登录异常:', error); - this.showFallbackMessage('登录失败,请使用短信验证码登录'); + + // 判断错误类型,提供友好提示 + let message = '登录失败,请使用短信验证码登录'; + + if (error.message && error.message.includes('2002')) { + message = '运营商授权失败,请使用短信验证码登录'; + } else if (error.message && error.message.includes('2006')) { + message = '登录超时,请重试或使用短信验证码登录'; + } else if (error.message && error.message.includes('not support')) { + message = '当前环境不支持一键登录,请使用短信验证码登录'; + } + + this.showFallbackMessage(message); + showToast(message, 3000); + + // 降级到短信验证 if (this.onFallback) { this.onFallback(); } } finally { this.isLoading = false; - this.setLoading(false); + if (!this.container.querySelector('.one-click-login-btn').classList.contains('verifying')) { + this.setLoading(false); + } + } + } + + /** + * 设置按钮状态 + * @param {string} state - 状态:loading, verifying + */ + setButtonState(state) { + const btn = this.container.querySelector('#oneClickLoginBtn'); + if (!btn) return; + + if (state === 'verifying') { + btn.innerHTML = ` + + 验证中... + `; } } diff --git a/src/js/utils/validator.js b/src/js/utils/validator.js index c44431d..dca1f2a 100644 --- a/src/js/utils/validator.js +++ b/src/js/utils/validator.js @@ -96,6 +96,26 @@ export class Validator { return idCard[17].toUpperCase() === checkCode; } + /** + * 从身份证号提取地区代码 + * @param {string} idCard - 身份证号 + * @returns {string|null} - 地区代码(前6位),如果无效则返回 null + */ + static extractAreaCode(idCard) { + if (!idCard) { + return null; + } + + const idCardStr = idCard.trim(); + + // 身份证号前6位是地区代码 + if (idCardStr.length >= 6) { + return idCardStr.substring(0, 6); + } + + return null; + } + /** * 验证短信验证码 * @param {string} code - 验证码