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
JavaScript
/**
* 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
};