UNPKG

betaflow-sdk

Version:

BetaFlow SDK for web applications - A powerful tool for managing beta testing campaigns and user feedback

1,399 lines (1,262 loc) 58.4 kB
/** * BetaFlow SDK v1.0.0 * A powerful tool for managing beta testing campaigns and user feedback * * @author BetaFlow Team * @license MIT * @repository https://github.com/saudm/betaflow-sdk */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.BetaFlow = {})); })(this, (function (exports) { 'use strict'; /** * BetaFlow SDK - A powerful tool for managing beta testing campaigns and user feedback * @version 1.3.0 * @author BetaFlow Team * * v1.3.0 新特性: * - 动态表单:根据campaignId从后端获取表单配置 * - 丰富字段类型:支持多种输入类型和验证规则 * - 自定义头部:支持表单头部样式定制 * * 使用说明: * * 1. 基本初始化: * const betaflow = new BetaFlow({ * campaignId: 'your-campaign-id', * apiKey: 'your-api-key', * debug: true // 开启调试模式 * }); * * 2. 调试和诊断: * // 运行完整诊断 * const diagnosis = await betaflow.diagnose(); * console.log('诊断结果:', diagnosis); * * // 开启调试日志 * const betaflow = new BetaFlow({ debug: true, ... }); * * 3. 错误处理: * betaflow.config.onFormError = (error) => { * console.error('表单提交错误:', error.code, error.message); * }; * * 4. 常见错误代码: * - FETCH_FAILED: 网络请求失败,检查API端点和网络连接 * - CAMPAIGN_NOT_FOUND: 项目不存在,检查campaignId * - UNAUTHORIZED: API认证失败,检查apiKey * - TIMEOUT: 请求超时,检查网络连接 * * 5. 故障排除: * 如果遇到 "TypeError: Failed to fetch" 错误: * - 确保API服务器正在运行 * - 检查API端点URL是否正确 * - 确认没有CORS问题 * - 运行 betaflow.diagnose() 获取详细诊断信息 */ // BetaFlow SDK 主类 class BetaFlow { constructor(config) { this.config = { campaignId: config.campaignId, apiKey: config.apiKey, apiEndpoint: config.apiEndpoint || (typeof window !== 'undefined' ? window.location.origin + '/api' : 'https://betaflow.fulitimes.com/api'), language: config.language || 'zh-CN', theme: config.theme || 'light', position: config.position || 'bottom-right', debug: config.debug || false, ...config }; // 验证必要的配置参数 this.validateConfig(); // 只在浏览器环境中初始化UI if (typeof window !== 'undefined') { this.init(); } } init() { this.createStyles(); if (this.config.autoShow !== false) { this.createFloatingButton(); } this.bindEvents(); this.log('BetaFlow SDK 初始化完成'); } log(message, data = null) { if (this.config.debug) { console.log('[BetaFlow SDK]', message, data); } } logError(message, error = null) { if (this.config.debug) { console.error('[BetaFlow SDK Error]', message, error); } } logWarning(message, data = null) { if (this.config.debug) { console.warn('[BetaFlow SDK Warning]', message, data); } } validateConfig() { const requiredFields = ['campaignId', 'apiKey']; const missingFields = []; requiredFields.forEach(field => { if (!this.config[field]) { missingFields.push(field); } }); if (missingFields.length > 0) { const errorMsg = `缺少必要的配置参数: ${missingFields.join(', ')}`; this.logError('配置验证失败', errorMsg); throw new Error(`[BetaFlow SDK] ${errorMsg}`); } // 验证API端点格式 if (this.config.apiEndpoint && !this.isValidUrl(this.config.apiEndpoint)) { this.logWarning('API端点格式可能不正确', this.config.apiEndpoint); } this.log('配置验证通过', { apiEndpoint: this.config.apiEndpoint, campaignId: this.config.campaignId, hasApiKey: !!this.config.apiKey }); } isValidUrl(string) { try { new URL(string); return true; } catch (_) { return false; } } /** * 诊断SDK配置和连接状态 * @returns {Promise<Object>} 诊断结果 */ async diagnose() { const results = { timestamp: new Date().toISOString(), config: { apiEndpoint: this.config.apiEndpoint, campaignId: this.config.campaignId, hasApiKey: !!this.config.apiKey, debug: this.config.debug }, tests: [], summary: { passed: 0, failed: 0, warnings: 0 } }; // 测试1: 配置验证 try { this.validateConfig(); results.tests.push({ name: '配置验证', status: 'passed', message: '所有必要配置参数都已提供' }); results.summary.passed++; } catch (error) { results.tests.push({ name: '配置验证', status: 'failed', message: error.message, suggestion: '请检查 campaignId 等必要参数是否已正确配置' }); results.summary.failed++; } // 测试2: API端点格式验证 if (this.isValidUrl(this.config.apiEndpoint)) { results.tests.push({ name: 'API端点格式', status: 'passed', message: 'API端点格式正确' }); results.summary.passed++; } else { results.tests.push({ name: 'API端点格式', status: 'failed', message: 'API端点格式不正确', suggestion: '请确保API端点是完整的URL格式,如: http://localhost:3000/api' }); results.summary.failed++; } // 测试3: 网络连接 if (typeof navigator !== 'undefined' && !navigator.onLine) { results.tests.push({ name: '网络连接', status: 'failed', message: '网络连接不可用', suggestion: '请检查网络连接' }); results.summary.failed++; } else { results.tests.push({ name: '网络连接', status: 'passed', message: '网络连接正常' }); results.summary.passed++; } // 测试4: API连接测试 try { const campaignInfo = await this.getCampaignInfo(); if (campaignInfo.success) { results.tests.push({ name: 'API连接测试', status: 'passed', message: '成功连接到API并获取项目信息', data: { campaignName: campaignInfo.data?.name, campaignStatus: campaignInfo.data?.status } }); results.summary.passed++; } else { results.tests.push({ name: 'API连接测试', status: 'failed', message: campaignInfo.message, errorCode: campaignInfo.errorCode, suggestion: this.getSuggestionForError(campaignInfo.errorCode) }); results.summary.failed++; } } catch (error) { results.tests.push({ name: 'API连接测试', status: 'failed', message: error.message, suggestion: '请检查API端点和网络连接' }); results.summary.failed++; } // 生成总结 if (results.summary.failed === 0) { results.overall = 'healthy'; results.message = 'SDK配置正常,可以正常使用'; } else if (results.summary.failed > results.summary.passed) { results.overall = 'critical'; results.message = '存在严重配置问题,需要修复后才能正常使用'; } else { results.overall = 'warning'; results.message = '存在一些问题,但基本功能可能仍然可用'; } // 输出诊断结果 console.group('[BetaFlow SDK] 诊断结果'); console.log('整体状态:', results.overall); console.log('总结:', results.message); console.log('通过测试:', results.summary.passed); console.log('失败测试:', results.summary.failed); console.log('详细结果:', results.tests); console.groupEnd(); return results; } getSuggestionForError(errorCode) { const suggestions = { 'CAMPAIGN_NOT_FOUND': '请检查 campaignId 是否正确,确保项目存在', 'UNAUTHORIZED': '请检查 apiKey 是否正确配置', 'FORBIDDEN': '请检查API权限配置', 'FETCH_FAILED': '请检查API端点是否正确,服务器是否运行,是否存在CORS问题', 'TIMEOUT': '请检查网络连接速度和服务器响应时间', 'SERVER_ERROR': '服务器内部错误,请联系管理员或稍后重试', 'NETWORK_OFFLINE': '请检查网络连接' }; return suggestions[errorCode] || '请检查配置和网络连接'; } createStyles() { if (typeof document === 'undefined') return; const style = document.createElement('style'); style.textContent = ` .betaflow-container { position: fixed; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .betaflow-button { background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); color: white; border: none; border-radius: 50px; padding: 12px 20px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,123,255,0.3); transition: all 0.3s ease; font-size: 14px; font-weight: 500; white-space: nowrap; } .betaflow-button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,123,255,0.4); background: linear-gradient(135deg, #0056b3 0%, #004085 100%); } .betaflow-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10001; backdrop-filter: blur(4px); } .betaflow-form { background: white; padding: 30px; border-radius: 12px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 40px rgba(0,0,0,0.1); animation: betaflow-fadeIn 0.3s ease; } @keyframes betaflow-fadeIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } .betaflow-form h2 { margin: 0 0 20px 0; color: #333; font-size: 24px; font-weight: 600; } .betaflow-form-header { text-align: center; margin-bottom: 20px; padding: 20px; border-radius: 8px; } .betaflow-form-icon { margin-bottom: 15px; } .betaflow-form-icon img { max-width: 64px; max-height: 64px; border-radius: 8px; } .betaflow-form-title { margin: 0 0 10px 0; color: inherit; font-size: 24px; font-weight: 600; } .betaflow-form-subtitle { margin: 0 0 10px 0; color: inherit; font-size: 18px; font-weight: 400; opacity: 0.8; } .betaflow-form-description { margin: 0; color: inherit; font-size: 14px; line-height: 1.5; opacity: 0.7; } .betaflow-form label { display: block; margin-bottom: 5px; color: #555; font-weight: 500; } .betaflow-form .form-field { margin-bottom: 15px; } .betaflow-form .field-description { display: block; margin-top: 5px; font-size: 12px; color: #666; line-height: 1.4; } .betaflow-form input, .betaflow-form textarea, .betaflow-form select { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; transition: border-color 0.3s ease; box-sizing: border-box; } .betaflow-form input:focus, .betaflow-form textarea:focus, .betaflow-form select:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 3px rgba(0,123,255,0.1); } .betaflow-form .radio-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px; } .betaflow-form .radio-option, .betaflow-form .checkbox-option { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; } .betaflow-form .radio-option input[type="radio"], .betaflow-form .checkbox-option input[type="checkbox"] { width: auto; margin: 0; margin-bottom: 0; } .betaflow-form .button-group { display: flex; gap: 10px; margin-top: 20px; } .betaflow-form button { flex: 1; padding: 12px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s ease; } .betaflow-form .submit-btn { background: #007bff; color: white; } .betaflow-form .submit-btn:hover { background: #0056b3; } .betaflow-form .submit-btn:disabled { background: #ccc; cursor: not-allowed; } .betaflow-form .cancel-btn { background: #6c757d; color: white; } .betaflow-form .cancel-btn:hover { background: #545b62; } .betaflow-loading { display: inline-block; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top-color: transparent; animation: betaflow-spin 1s ease-in-out infinite; } @keyframes betaflow-spin { to { transform: rotate(360deg); } } .betaflow-success { text-align: center; padding: 40px 20px; } .betaflow-success .icon { font-size: 48px; color: #28a745; margin-bottom: 20px; } .betaflow-error { color: #dc3545; font-size: 14px; margin-top: 10px; } `; document.head.appendChild(style); } createFloatingButton() { if (typeof document === 'undefined') return; const container = document.createElement('div'); container.className = 'betaflow-container'; container.style.cssText = this.getPositionStyles(); const button = document.createElement('button'); button.className = 'betaflow-button'; button.textContent = this.config.buttonText || '申请 Beta 测试'; button.onclick = () => { this.showApplicationForm().catch(error => { this.log('显示申请表单失败', error); // 如果获取项目信息失败,仍然显示默认表单 this.showFallbackForm(); }); }; container.appendChild(button); document.body.appendChild(container); this.floatingButton = container; } getPositionStyles() { const positions = { 'bottom-right': 'bottom: 20px; right: 20px;', 'bottom-left': 'bottom: 20px; left: 20px;', 'top-right': 'top: 20px; right: 20px;', 'top-left': 'top: 20px; left: 20px;', 'bottom-center': 'bottom: 20px; left: 50%; transform: translateX(-50%);' }; return positions[this.config.position] || positions['bottom-right']; } async showApplicationForm() { if (typeof document === 'undefined') return; // 如果已经显示,则不重复显示 if (document.querySelector('.betaflow-modal')) return; this.log('显示申请表单'); // 获取项目信息以动态设置表单标题、字段和头部 let formTitle = this.config.formTitle; let formFields = null; let formHeader = null; let campaignName = null; let campaignInfo = null; if (this.config.campaignId) { try { campaignInfo = await this.getCampaignInfo(); this.log('getCampaignInfo返回', campaignInfo); if (campaignInfo.success && campaignInfo.data) { // 优先使用后端返回的项目名称作为标题 campaignName = campaignInfo.data.name; if (campaignName) { formTitle = campaignName; } // 获取表单字段配置 - 适配后端数据结构 if (campaignInfo.data.config && campaignInfo.data.config.formFields) { formFields = campaignInfo.data.config.formFields; } else if (campaignInfo.data.formFields) { formFields = campaignInfo.data.formFields; } // 获取表单头部配置 - 适配后端数据结构 if (campaignInfo.data.config && campaignInfo.data.config.formHeader) { formHeader = campaignInfo.data.config.formHeader; } else if (campaignInfo.data.formHeader) { formHeader = campaignInfo.data.formHeader; } this.log('获取到的表单配置', { formFields: formFields, formHeader: formHeader, campaignName: campaignName }); } else { // 获取项目信息失败,记录详细错误 this.logError('获取项目信息失败', { success: campaignInfo.success, message: campaignInfo.message, errorCode: campaignInfo.errorCode, httpStatus: campaignInfo.httpStatus }); // 如果是严重错误(如配置错误),显示警告 if (campaignInfo.errorCode === 'CAMPAIGN_NOT_FOUND' || campaignInfo.errorCode === 'UNAUTHORIZED' || campaignInfo.errorCode === 'FETCH_FAILED') { this.logWarning('项目配置可能有问题,将使用默认表单', { errorCode: campaignInfo.errorCode, message: campaignInfo.message }); } } } catch (error) { this.logError('获取项目信息异常', error); } } // 如果仍然没有标题,使用默认值 if (!formTitle) { formTitle = '申请 Beta 测试'; } // 生成表单头部HTML const formHeaderHtml = this.generateFormHeader(formHeader, formTitle); // 生成表单字段HTML(只有formFields有效才用,否则用默认) let formFieldsHtml = ''; if (formFields && Array.isArray(formFields) && formFields.length > 0) { formFieldsHtml = this.generateFormFields(formFields); } else { this.logWarning('未获取到后端表单字段,使用默认字段'); formFieldsHtml = this.getDefaultFormFields(); } const modal = document.createElement('div'); modal.className = 'betaflow-modal'; modal.innerHTML = ` <div class="betaflow-form"> ${formHeaderHtml} <form id="betaflow-application-form"> ${formFieldsHtml} <div class="button-group"> <button type="submit" class="submit-btn"> <span class="btn-text">提交申请</span> <span class="betaflow-loading" style="display: none;"></span> </button> <button type="button" class="cancel-btn" onclick="this.closest('.betaflow-modal').remove()">取消</button> </div> <div class="betaflow-error" style="display: none;"></div> </form> </div> `; document.body.appendChild(modal); // 绑定表单提交事件 const form = modal.querySelector('#betaflow-application-form'); form.onsubmit = (e) => this.handleFormSubmit(e, modal); // 触发回调 if (this.config.onFormShow) { this.config.onFormShow(); } } hideApplicationForm() { if (typeof document === 'undefined') return; const modal = document.querySelector('.betaflow-modal'); if (modal) { modal.remove(); this.log('隐藏申请表单'); } } /** * 生成表单头部HTML * @param {Object} formHeader - 表单头部配置 * @param {string} fallbackTitle - 备用标题 * @returns {string} 表单头部HTML字符串 */ generateFormHeader(formHeader, fallbackTitle) { if (!formHeader) { // 如果没有头部配置,使用简单的标题 return `<h2>${fallbackTitle}</h2>`; } const { title, subtitle, description, iconUrl, backgroundColor, textColor, className } = formHeader; // 构建头部样式 const headerStyles = []; if (backgroundColor) { headerStyles.push(`background-color: ${backgroundColor}`); } if (textColor) { headerStyles.push(`color: ${textColor}`); } const styleAttr = headerStyles.length > 0 ? ` style="${headerStyles.join('; ')}"` : ''; const classAttr = className ? ` class="betaflow-form-header ${className}"` : ' class="betaflow-form-header"'; // 构建头部内容 let headerContent = ''; // 图标 if (iconUrl) { headerContent += `<div class="betaflow-form-icon"><img src="${iconUrl}" alt="Icon" /></div>`; } // 标题 const displayTitle = title || fallbackTitle; if (displayTitle) { headerContent += `<h2 class="betaflow-form-title">${displayTitle}</h2>`; } // 副标题 if (subtitle) { headerContent += `<h3 class="betaflow-form-subtitle">${subtitle}</h3>`; } // 描述 if (description) { headerContent += `<p class="betaflow-form-description">${description}</p>`; } return `<div${classAttr}${styleAttr}>${headerContent}</div>`; } /** * 生成表单字段HTML * @param {Array} formFields - 表单字段配置数组 * @returns {string} 表单字段HTML字符串 */ generateFormFields(formFields) { // 如果没有配置表单字段,使用默认字段 if (!formFields || !Array.isArray(formFields) || formFields.length === 0) { return this.getDefaultFormFields(); } // 根据配置生成表单字段 return formFields.map(field => { const required = field.required ? ' *' : ''; const requiredAttr = field.required ? ' required' : ''; const placeholder = field.placeholder || `请输入${field.label}`; const defaultValue = field.defaultValue ? ` value="${field.defaultValue}"` : ''; const fieldName = field.name || field.label; // 构建验证属性 let validationAttrs = ''; if (field.validation) { if (field.validation.minLength) { validationAttrs += ` minlength="${field.validation.minLength}"`; } if (field.validation.maxLength) { validationAttrs += ` maxlength="${field.validation.maxLength}"`; } if (field.validation.pattern) { validationAttrs += ` pattern="${field.validation.pattern}"`; } } // 构建描述HTML const descriptionHtml = field.description ? `<small class="field-description">${field.description}</small>` : ''; switch (field.type) { case 'text': case 'email': case 'tel': case 'url': return ` <div class="form-field"> <label>${field.label}${required}</label> <input type="${field.type}" name="${fieldName}"${requiredAttr}${validationAttrs}${defaultValue} placeholder="${placeholder}"> ${descriptionHtml} </div>`; case 'number': return ` <div class="form-field"> <label>${field.label}${required}</label> <input type="number" name="${fieldName}"${requiredAttr}${validationAttrs}${defaultValue} placeholder="${placeholder}"> ${descriptionHtml} </div>`; case 'date': return ` <div class="form-field"> <label>${field.label}${required}</label> <input type="date" name="${fieldName}"${requiredAttr}${defaultValue}> ${descriptionHtml} </div>`; case 'textarea': const rows = field.rows || 3; return ` <div class="form-field"> <label>${field.label}${required}</label> <textarea name="${fieldName}" rows="${rows}"${requiredAttr}${validationAttrs} placeholder="${placeholder}">${field.defaultValue || ''}</textarea> ${descriptionHtml} </div>`; case 'select': const options = field.options || []; const optionsHtml = options.map(option => { const value = option.value || option; const label = option.label || option; const selected = field.defaultValue === value ? ' selected' : ''; return `<option value="${value}"${selected}>${label}</option>`; }).join(''); return ` <div class="form-field"> <label>${field.label}${required}</label> <select name="${fieldName}"${requiredAttr}> <option value="">请选择${field.label}</option> ${optionsHtml} </select> ${descriptionHtml} </div>`; case 'radio': const radioOptions = field.options || []; const radioHtml = radioOptions.map((option, index) => { const value = option.value || option; const label = option.label || option; const checked = field.defaultValue === value ? ' checked' : ''; return `<label class="radio-option"> <input type="radio" name="${fieldName}" value="${value}"${requiredAttr}${checked}> ${label} </label>`; }).join(''); return ` <div class="form-field"> <label>${field.label}${required}</label> <div class="radio-group"> ${radioHtml} </div> ${descriptionHtml} </div>`; case 'checkbox': const checked = field.defaultValue ? ' checked' : ''; return ` <div class="form-field"> <label class="checkbox-option"> <input type="checkbox" name="${fieldName}" value="1"${requiredAttr}${checked}> ${field.label}${required} </label> ${descriptionHtml} </div>`; default: // 默认作为文本输入处理 return ` <div class="form-field"> <label>${field.label}${required}</label> <input type="text" name="${fieldName}"${requiredAttr}${validationAttrs}${defaultValue} placeholder="${placeholder}"> ${descriptionHtml} </div>`; } }).join(''); } /** * 获取默认表单字段 * @returns {string} 默认表单字段HTML */ getDefaultFormFields() { return ` <div> <label>姓名 *</label> <input type="text" name="name" required placeholder="请输入您的姓名"> </div> <div> <label>邮箱 *</label> <input type="email" name="email" required placeholder="请输入您的邮箱地址"> </div> <div> <label>公司/组织</label> <input type="text" name="company" placeholder="请输入您的公司或组织名称"> </div> <div> <label>职位</label> <input type="text" name="position" placeholder="请输入您的职位"> </div> <div> <label>申请理由</label> <textarea name="reason" rows="3" placeholder="请简要说明您申请 Beta 测试的理由"></textarea> </div>`; } showFallbackForm() { if (typeof document === 'undefined') return; // 如果已经显示,则不重复显示 if (document.querySelector('.betaflow-modal')) return; this.log('显示备用申请表单'); // 使用默认表单字段 const formFieldsHtml = this.getDefaultFormFields(); const formTitle = this.config.formTitle || '申请 Beta 测试'; const modal = document.createElement('div'); modal.className = 'betaflow-modal'; modal.innerHTML = ` <div class="betaflow-form"> <h2>${formTitle}</h2> <form id="betaflow-application-form"> ${formFieldsHtml} <div class="button-group"> <button type="submit" class="submit-btn"> <span class="btn-text">提交申请</span> <span class="betaflow-loading" style="display: none;"></span> </button> <button type="button" class="cancel-btn" onclick="this.closest('.betaflow-modal').remove()">取消</button> </div> <div class="betaflow-error" style="display: none;"></div> </form> </div> `; document.body.appendChild(modal); // 绑定表单提交事件 const form = modal.querySelector('#betaflow-application-form'); form.onsubmit = (e) => this.handleFormSubmit(e, modal); // 触发回调 if (this.config.onFormShow) { this.config.onFormShow(); } } showFloatingButton() { if (this.floatingButton) { this.floatingButton.style.display = 'block'; } else { this.createFloatingButton(); } } hideFloatingButton() { if (this.floatingButton) { this.floatingButton.style.display = 'none'; } } async handleFormSubmit(event, modal) { event.preventDefault(); const form = event.target; const submitBtn = form.querySelector('.submit-btn'); const btnText = submitBtn.querySelector('.btn-text'); const loading = submitBtn.querySelector('.betaflow-loading'); const errorDiv = form.querySelector('.betaflow-error'); // 显示加载状态 submitBtn.disabled = true; btnText.style.display = 'none'; loading.style.display = 'inline-block'; errorDiv.style.display = 'none'; const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); this.log('提交表单数据', data); // 触发提交回调 if (this.config.onFormSubmit) { this.config.onFormSubmit(data); } try { const response = await this.submitApplication(data); if (response.success) { this.log('申请提交成功', response); this.showSuccessMessage(modal); // 触发成功回调 if (this.config.onFormSuccess) { this.config.onFormSuccess(response); } } else { throw new Error(response.message || '提交失败'); } } catch (error) { this.logError('申请提交失败', error); // 根据错误类型显示不同的错误信息 let errorMessage = error.message || '提交失败,请稍后重试'; let showDetails = false; if (error.code) { switch (error.code) { case 'FETCH_FAILED': errorMessage = '网络连接失败,请检查网络设置后重试'; showDetails = true; break; case 'TIMEOUT': errorMessage = '请求超时,请检查网络连接后重试'; break; case 'UNAUTHORIZED': errorMessage = 'API认证失败,请联系管理员检查配置'; showDetails = true; break; case 'FORBIDDEN': errorMessage = '访问被拒绝,请联系管理员'; break; case 'INVALID_REQUEST': errorMessage = '请求参数错误,请检查填写的信息'; break; case 'CONFLICT': errorMessage = '申请已存在,请勿重复提交'; break; case 'SERVER_ERROR': errorMessage = '服务器错误,请稍后重试'; break; case 'CAMPAIGN_NOT_FOUND': errorMessage = '项目不存在,请联系管理员检查配置'; showDetails = true; break; } } // 显示错误信息 if (showDetails && this.config.debug) { errorDiv.innerHTML = ` <div style="margin-bottom: 10px;">${errorMessage}</div> <details style="font-size: 12px; color: #666;"> <summary style="cursor: pointer;">查看详细信息</summary> <div style="margin-top: 5px; padding: 5px; background: #f5f5f5; border-radius: 3px;"> <div><strong>错误代码:</strong> ${error.code || 'UNKNOWN'}</div> <div><strong>API端点:</strong> ${this.config.apiEndpoint}</div> <div><strong>项目ID:</strong> ${this.config.campaignId}</div> ${error.httpStatus ? `<div><strong>HTTP状态:</strong> ${error.httpStatus}</div>` : ''} ${error.originalError ? `<div><strong>原始错误:</strong> ${error.originalError}</div>` : ''} </div> </details> `; } else { errorDiv.textContent = errorMessage; } errorDiv.style.display = 'block'; // 恢复按钮状态 submitBtn.disabled = false; btnText.style.display = 'inline'; loading.style.display = 'none'; // 触发错误回调 if (this.config.onFormError) { this.config.onFormError(error); } } } async submitApplication(data) { const url = `${this.config.apiEndpoint}/applications`; // 从表单数据中提取姓名和邮箱 // 优先使用表单中的email字段作为applicantEmail const applicantEmail = data.email || data['applicant-email'] || data.applicantEmail || ''; // 如果没有name字段,使用默认值或从其他字段推断 const applicantName = data.name || data['applicant-name'] || data.applicantName || 'Beta测试用户'; // 构建formData对象,排除已提取的字段 const formData = {...data}; delete formData.name; delete formData['applicant-name']; delete formData.applicantName; delete formData.email; delete formData['applicant-email']; delete formData.applicantEmail; const requestData = { campaignId: this.config.campaignId, applicantName, applicantEmail, formData, source: 'sdk', userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Node.js', timestamp: new Date().toISOString() }; this.log('提交申请', { url, dataKeys: Object.keys(requestData), formDataKeys: Object.keys(formData), applicantName, applicantEmail }); try { // 检查网络连接 if (typeof navigator !== 'undefined' && !navigator.onLine) { const errorMsg = '网络连接不可用,请检查网络设置'; this.logError('网络检查失败', errorMsg); throw new Error(errorMsg); } const headers = { 'Content-Type': 'application/json', ...(this.config.apiKey && {'Authorization': `Bearer ${this.config.apiKey}`}) }; this.log('发送申请请求', {url, headers: Object.keys(headers)}); const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(requestData), timeout: 15000 // 15秒超时 }); this.log('收到申请响应', { status: response.status, statusText: response.statusText, ok: response.ok }); const result = await response.json(); if (response.ok && result.success) { this.log('申请提交成功', result); return result; } else { let errorMessage = result.error || result.message || '提交失败'; let errorCode = 'SUBMIT_FAILED'; if (response.status === 400) { errorMessage = `请求参数错误: ${errorMessage}`; errorCode = 'INVALID_REQUEST'; } else if (response.status === 401) { errorMessage = 'API认证失败,请检查 apiKey 配置'; errorCode = 'UNAUTHORIZED'; } else if (response.status === 403) { errorMessage = '访问被拒绝,请检查权限配置'; errorCode = 'FORBIDDEN'; } else if (response.status === 409) { errorMessage = '申请已存在或冲突'; errorCode = 'CONFLICT'; } else if (response.status >= 500) { errorMessage = '服务器内部错误,请稍后重试'; errorCode = 'SERVER_ERROR'; } this.logError('申请提交失败', { status: response.status, message: errorMessage, response: result }); const error = new Error(errorMessage); error.code = errorCode; error.httpStatus = response.status; error.response = result; throw error; } } catch (error) { if (error.code) { // 已经是我们处理过的错误,直接抛出 throw error; } let errorMessage = error.message; let errorCode = 'NETWORK_ERROR'; if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { errorMessage = `网络请求失败,请检查:\n1. API端点是否正确: ${this.config.apiEndpoint}\n2. 服务器是否运行\n3. 是否存在CORS问题\n4. 网络连接是否正常`; errorCode = 'FETCH_FAILED'; } else if (error.name === 'AbortError') { errorMessage = '请求超时,请检查网络连接或服务器响应速度'; errorCode = 'TIMEOUT'; } else if (error.message.includes('JSON')) { errorMessage = '服务器响应格式错误,请检查API端点配置'; errorCode = 'INVALID_RESPONSE'; } this.logError('提交申请失败', { error: error.message, name: error.name, stack: error.stack, config: { apiEndpoint: this.config.apiEndpoint, campaignId: this.config.campaignId, hasApiKey: !!this.config.apiKey } }); const enhancedError = new Error(errorMessage); enhancedError.code = errorCode; enhancedError.originalError = error.message; throw enhancedError; } } async getCampaignInfo() { const url = `${this.config.apiEndpoint}/campaigns/${this.config.campaignId}`; this.log('获取项目信息', {url, campaignId: this.config.campaignId}); try { // 检查网络连接 if (typeof navigator !== 'undefined' && !navigator.onLine) { const errorMsg = '网络连接不可用,请检查网络设置'; this.logError('网络检查失败', errorMsg); return { success: false, message: errorMsg, errorCode: 'NETWORK_OFFLINE' }; } const headers = { 'Content-Type': 'application/json', ...(this.config.apiKey && {'Authorization': `Bearer ${this.config.apiKey}`}) }; this.log('发送请求', {url, headers: Object.keys(headers)}); const response = await fetch(url, { method: 'GET', headers, timeout: 10000 // 10秒超时 }); this.log('收到响应', { status: response.status, statusText: response.statusText, ok: response.ok, url: response.url }); if (!response.ok) { let errorMessage = `HTTP ${response.status}: ${response.statusText}`; let errorCode = 'HTTP_ERROR'; if (response.status === 404) { errorMessage = `项目不存在 (ID: ${this.config.campaignId}),请检查 campaignId 配置`; errorCode = 'CAMPAIGN_NOT_FOUND'; } else if (response.status === 401) { errorMessage = 'API认证失败,请检查 apiKey 配置'; errorCode = 'UNAUTHORIZED'; } else if (response.status === 403) { errorMessage = '访问被拒绝,请检查权限配置'; errorCode = 'FORBIDDEN'; } else if (response.status >= 500) { errorMessage = '服务器内部错误,请稍后重试'; errorCode = 'SERVER_ERROR'; } this.logError('API请求失败', {status: response.status, message: errorMessage}); return { success: false, message: errorMessage, errorCode, httpStatus: response.status }; } const result = await response.json(); this.log('解析响应成功', result); // 直接返回API的原始响应,避免双重嵌套 return result; } catch (error) { let errorMessage = error.message; let errorCode = 'NETWORK_ERROR'; if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { errorMessage = `网络请求失败,请检查:\n1. API端点是否正确: ${this.config.apiEndpoint}\n2. 服务器是否运行\n3. 是否存在CORS问题\n4. 网络连接是否正常`; errorCode = 'FETCH_FAILED'; } else if (error.name === 'AbortError') { errorMessage = '请求超时,请检查网络连接或服务器响应速度'; errorCode = 'TIMEOUT'; } else if (error.message.includes('JSON')) { errorMessage = '服务器响应格式错误,请检查API端点配置'; errorCode = 'INVALID_RESPONSE'; } this.logError('获取项目信息失败', { error: error.message, name: error.name, stack: error.stack, config: { apiEndpoint: this.config.apiEndpoint, campaignId: this.config.campaignId, hasApiKey: !!this.config.apiKey } }); return { success: false, message: errorMessage, errorCode, originalError: error.message }; } } /** * 验证用户申请状态 * @param {Object} request - 验证请求参数 * @param {string} [request.email] - 用户邮箱 * @param {string} [request.phone] - 用户手机号 * @returns {Promise<Object>} 验证结果 */ async verifyUserApplication(request) { this.log('验证用户申请状态', request); // 参数验证 if (!request || (!request.email && !request.phone)) { return { success: false, message: '请提供邮箱或手机号', errorCode: 'INVALID_PARAMS' }; } // 验证API Key是否存在 if (!this.config.apiKey) { return { success: false, message: 'API Key 未配置,请在初始化时提供 apiKey', errorCode: 'MISSING_API_KEY' }; } try { // 构建查询参数 const queryParams = new URLSearchParams({ campaignId: this.config.campaignId }); if (request.email) { queryParams.append('email', request.email); } if (request.phone) { queryParams.append('phone', request.phone); } const response = await fetch(`${this.config.apiEndpoint}/applications/verify?${queryParams}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.apiKey}` } }); const result = await response.json(); this.log('用户申请状态验证结果', result); if (response.ok && result.success) { // 根据申请状态返回相应的错误码和消息 if (!result.data || !result.data.application) { return { success: false, message: '该用户未申请公测', status: 'NOT_APPLIED', errorCode: 'USER_NOT_APPLIED' }; } const application = result.data.application; const status = application.status; if (status === 'APPROVED') { return { success: true, message: '用户已通过审核', status: 'APPROVED', applicationData: application }; } else if (status === 'PENDING') { return { success: false, message: '该用户申请待审核', status: 'PENDING', errorCode: 'USER_NOT_APPROVED', applicationData: application }; } else if (status === 'REJECTED') { return { success: false, message: '该用户申请已被拒绝', status: 'REJECTED', errorCode: 'USER_NOT_APPROVED', applicationData: application }; } else { return { success: false, message: '用户申请状态未知', status: status, errorCode: 'USER_NOT_APPROVED', applicationData: application };