commit d703ac357466da6aa88ea671f981127f80a36cdf Author: 绝尘 <237809796@qq.com> Date: Thu Jan 22 18:31:30 2026 +0800 . diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..d9d285f --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,139 @@ +# flux-web 部署指南 + +本项目**无需编译**,可直接部署源码。 + +## 📝 部署前准备 + +### 1. 修改 API 地址 + +编辑 `src/js/config/api.config.js`: + +```javascript +BASE_URL: 'https://your-api-domain.com', // 改成你的API地址 +``` + +### 2. 关闭调试模式 + +编辑 `src/js/config/app.config.js`: + +```javascript +ENABLED: false, // 改成 false +``` + +--- + +## 🚀 三种部署方式 + +### 方式一:Nginx 部署(推荐) + +**第1步:上传文件** + +```bash +# 上传到服务器 +/var/www/flux-web/ +``` + +**第2步:创建配置文件** + +```bash +# 复制示例配置 +cp nginx.conf.example /etc/nginx/sites-available/flux-web + +# 修改配置中的域名和路径 +vim /etc/nginx/sites-available/flux-web +``` + +**第3步:启用站点** + +```bash +# 创建软链接 +ln -s /etc/nginx/sites-available/flux-web /etc/nginx/sites-enabled/ + +# 测试配置 +nginx -t + +# 重载 +systemctl reload nginx +``` + +**第4步:配置 HTTPS(可选)** + +```bash +# 安装 certbot +apt install certbot python3-certbot-nginx + +# 获取证书 +certbot --nginx -d your-domain.com +``` + +--- + +### 方式二:Node.js 服务器 + +```bash +# 安装 PM2 +npm install -g pm2 + +# 启动服务 +pm2 start server.js --name flux-web + +# 设置开机自启 +pm2 startup +pm2 save +``` + +--- + +### 方式三:对象存储 + CDN + +适合阿里云 OSS、腾讯云 COS 等: + +1. 在控制台上传整个 `flux-web` 目录 +2. 配置 CDN 加速 +3. 绑定自定义域名 + +--- + +## ✅ 部署后检查 + +访问你的域名,确认: + +- ✓ 页面正常显示 +- ✓ 样式加载正常 +- ✓ 浏览器控制台无报错 +- ✓ API 请求成功(F12 查看 Network) + +--- + +## 🔄 更新项目 + +```bash +# 备份 +cp -r /var/www/flux-web /var/www/flux-web.backup + +# 上传新文件覆盖即可 +``` + +--- + +## ❓ 常见问题 + +**Q: 页面空白?** +A: 检查浏览器控制台(F12)查看报错信息 + +**Q: API 请求失败?** +A: 检查 `src/js/config/api.config.js` 中的 API 地址是否正确 + +**Q: 静态资源 404?** +A: 确认 `static/` 和 `src/` 目录都已上传 + +**Q: 如何清除缓存?** +A: 修改 `index.html` 中 CSS/JS 引用,加版本号:`style.css?v=2` + +--- + +## 📞 需要帮助? + +- 查看详细配置:`nginx.conf.example` +- 项目说明:`README.md` +- 模块化文档:`src/js/README.md` diff --git a/README.md b/README.md new file mode 100644 index 0000000..053a881 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# flux-web + +薇钱包 H5 前端项目 - 纯前端,无需编译 + +## 🚀 快速开始 + +### 开发 + +```bash +node server.js +# 访问 http://localhost:3000 +``` + +### 部署 + +**无需编译**,直接部署源码即可。查看 [DEPLOY.md](./DEPLOY.md) + +## 📁 项目结构 + +``` +flux-web/ +├── index.html # 主页面 +├── basic-info.html # 基本信息页面 +├── style.css / basic-info.css # 样式文件 +├── src/js/ # JavaScript 模块 +│ ├── config/ # 配置 +│ ├── core/ # 核心 +│ ├── services/ # 业务服务 +│ ├── ui/ # UI 组件 +│ ├── utils/ # 工具 +│ └── pages/ # 页面逻辑 +└── static/ # 静态资源 +``` + +## ⚙️ 配置 + +### 修改 API 地址 + +`src/js/config/api.config.js`: + +```javascript +BASE_URL: 'http://localhost:8071', // 改成你的API地址 +``` + +### 关闭调试模式 + +`src/js/config/app.config.js`: + +```javascript +ENABLED: false, // 生产环境改为 false +``` + +## 📦 部署方式 + +- **Nginx** - 推荐,查看 `nginx.conf.example` +- **Node.js** - 使用 PM2:`pm2 start server.js` +- **对象存储** - 阿里云 OSS / 腾讯云 COS + +详细步骤:[DEPLOY.md](./DEPLOY.md) + +## 📖 文档 + +- [部署指南](./DEPLOY.md) +- [模块化说明](./src/js/README.md) +- [极光配置](./docs/jverify-configuration.md) + +--- + +北京百雅科技有限公司 © 2025 diff --git a/basic-info.css b/basic-info.css new file mode 100644 index 0000000..f2f600b --- /dev/null +++ b/basic-info.css @@ -0,0 +1,932 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + background-color: #f5f5f5; + color: #333; + font-size: 14px; + line-height: 1.5; +} + +.basic-info-container { + min-height: 100vh; + padding: 16px; + padding-bottom: 100px; +} + +/* 顶部卡片 */ +.top-card { + width: 100%; + height: 142px; + background-image: url('./static/image/personalTop.png'); + background-size: 100% 100%; + background-position: center; + background-repeat: no-repeat; + border-radius: 12px; + padding: 16px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08); + margin-bottom: -8px; + position: relative; + z-index: 1; +} + +.top-title { + font-size: 12px; + font-weight: 600; + color: #fff; + /*margin-bottom: 4px;*/ +} + +.top-money { + font-size: 42px; + color: #fff; + font-weight: 700; + margin: -5px 0; +} + +.top-text { + font-size: 12px; + font-weight: 400; + color: #fff; + margin-top: 2px; +} + +.progress-wrapper { + display: flex; + align-items: center; + margin-top: 4px; +} + +.progress-bar { + flex: 1; + height: 8px; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 50px; + margin-right: 5px; + overflow: hidden; + position: relative; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #19be6b, #1dd87a, #19be6b); + background-size: 200% 100%; + border-radius: 50px; + width: 0%; + transition: width 0.5s ease; + position: relative; + overflow: hidden; + -webkit-animation: progressPulse 2s ease-in-out infinite, progressGradient 3s ease infinite; + animation: progressPulse 2s ease-in-out infinite, progressGradient 3s ease infinite; +} + +.progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + -webkit-animation: shimmer 2s infinite; + animation: shimmer 2s infinite; +} + +@-webkit-keyframes shimmer { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } + 100% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} + +@keyframes shimmer { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } + 100% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} + +@-webkit-keyframes progressPulse { + 0%, 100% { + -webkit-box-shadow: 0 0 0 0 rgba(25, 190, 107, 0.4); + box-shadow: 0 0 0 0 rgba(25, 190, 107, 0.4); + } + 50% { + -webkit-box-shadow: 0 0 0 4px rgba(25, 190, 107, 0); + box-shadow: 0 0 0 4px rgba(25, 190, 107, 0); + } +} + +@keyframes progressPulse { + 0%, 100% { + -webkit-box-shadow: 0 0 0 0 rgba(25, 190, 107, 0.4); + box-shadow: 0 0 0 0 rgba(25, 190, 107, 0.4); + } + 50% { + -webkit-box-shadow: 0 0 0 4px rgba(25, 190, 107, 0); + box-shadow: 0 0 0 4px rgba(25, 190, 107, 0); + } +} + +@-webkit-keyframes progressGradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes progressGradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +.progress-text { + color: #fff; + font-size: 14px; + font-weight: 500; + min-width: 40px; + text-align: right; +} + +/* 资产信息区域 */ +.asset-section { + background-color: #fff; + border-radius: 8px; + margin-top: 0; + padding: 0; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08); +} + +.asset-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #efefef; + cursor: pointer; +} + +.asset-title { + font-size: 16px; + color: #854a19; + font-weight: 500; + display: flex; + align-items: center; +} + +.asset-icon { + width: 22px; + height: 22px; + margin-right: 9px; + display: inline-block; + background-color: #fff; + border-radius: 4px; +} + +.asset-arrow { + width: 20px; + height: 20px; + color: #999; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.section-arrow { + width: 20px; + height: 20px; + display: inline-block; + background-color: #fff; + border-radius: 4px; +} + +.asset-list { + padding: 0; +} + +.asset-item { + padding: 9px 16px; + font-size: 14px; + border-bottom: 1px solid #f5f5f5; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.5s ease, transform 0.5s ease; + display: none; +} + +.asset-item.show { + display: block; + opacity: 1; + transform: translateY(0); +} + +.asset-item:last-child { + border-bottom: none; +} + +.item-top { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +.item-name { + color: #333; + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + margin-right: 7px; +} + +.item-value { + display: flex; + align-items: center; + color: #999; + font-size: 14px; +} + +.item-value.selected { + color: #333; +} + +.item-icon { + width: 20px; + height: 20px; + margin-left: 8px; + display: inline-block; + background-color: #fff; + border-radius: 4px; + flex-shrink: 0; +} + +.item-options { + margin-top: 12px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 8px; + max-height: 100px; + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), + margin-top 0.4s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s ease, + padding 0.4s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 1; + padding: 0; +} + +.asset-item.collapsed .item-options { + max-height: 0 !important; + margin-top: 0 !important; + opacity: 0; + padding: 0 !important; +} + +.option-btn { + flex: 1; + min-width: 40%; + height: 30px; + line-height: 30px; + text-align: center; + background: #f5f5f5; + border-radius: 4px; + color: #777; + font-size: 12px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.option-btn:hover { + background: #e8e8e8; +} + +.option-btn.selected { + background: #3474fe; + color: #fff; +} + +.option-btn:active { + transform: scale(0.98); +} + +/* 基本信息区域 */ +.basic-info-section { + background-color: #fff; + border-radius: 8px; + margin-top: 16px; + padding: 0; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08); + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.basic-info-section.show { + opacity: 1; + transform: translateY(0); +} + +.basic-info-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #efefef; + cursor: pointer; +} + +.basic-info-title { + font-size: 16px; + color: #854a19; + font-weight: 500; + display: flex; + align-items: center; +} + +.basic-info-icon { + width: 22px; + height: 22px; + margin-right: 9px; + display: inline-block; +} + +.basic-info-arrow { + width: 20px; + height: 20px; + color: #999; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.basic-info-list { + padding: 0; +} + +/* 基本信息项行样式 */ +.basic-info-item-row { + padding: 12px 16px; + border-bottom: 1px solid #f5f5f5; +} + +.basic-info-item-row:last-child { + border-bottom: none; +} + +.basic-info-item-row .item-top { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; +} + +.basic-info-item-row .item-name { + color: #333; + font-size: 14px; + font-weight: 500; +} + +.basic-info-item-row .item-value { + color: #999; + font-size: 14px; + display: flex; + align-items: center; + flex: 1; +} + +.basic-info-item-row .item-value-text { + flex: 1; + text-align: left; +} + +.basic-info-item-row .item-icon { + margin-left: auto; + flex-shrink: 0; +} + +.basic-info-item-row .item-value.selected { + color: #333; +} + +.basic-info-item-row .item-icon { + width: 20px; + height: 20px; + margin-left: 8px; + display: inline-block; + background-color: #fff; + border-radius: 4px; + flex-shrink: 0; +} + +/* 内联输入框样式 */ +.basic-input-inline { + flex: 1; + height: 32px; + padding: 0 8px; + font-size: 14px; + color: #333; + background-color: transparent; + border: none; + outline: none; + text-align: left; +} + +.basic-input-inline::placeholder { + color: #999; + text-align: left; +} + +.basic-input-inline:focus { + color: #333; +} + +.basic-info-item-row .item-top { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; + min-height: 32px; +} + +/* 城市字段的值居左显示 */ +.basic-info-item-row#basic-info-city .item-top { + justify-content: flex-start; + gap: 8px; +} + +.basic-info-item-row#basic-info-city .item-value { + flex: 1; +} + +.basic-info-item-row#basic-info-city .item-value-text { + flex: 1; + text-align: left; +} + +.basic-info-item-row#basic-info-city .item-icon { + margin-left: auto; + flex-shrink: 0; +} + +/* 输入框样式 */ +.basic-input { + width: 100%; + height: 36px; + padding: 0 12px; + font-size: 14px; + color: #333; + background-color: #f5f5f5; + border: 1px solid transparent; + border-radius: 4px; + outline: none; + transition: all 0.3s ease; + margin-bottom: 8px; +} + +.basic-input:focus { + background-color: #fff; + border-color: #3474fe; +} + +.confirm-btn { + width: 100%; + height: 32px; + line-height: 32px; + text-align: center; + background: #3474fe; + border-radius: 4px; + color: #fff; + font-size: 12px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.confirm-btn:hover { + background: #2563eb; +} + +.confirm-btn:active { + transform: scale(0.98); +} + +/* 底部按钮 */ +.button-section { + position: fixed; + bottom: 20px; + left: 0; + width: 100%; + padding: 16px; + padding-bottom: calc(8px + env(safe-area-inset-bottom)); + background-color: #fff; + box-shadow: 0 -2px 4px 0 rgba(0, 0, 0, 0.08); + z-index: 100; +} + +/* 协议行 */ +.agreement-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #666; + margin-bottom: 10px; + line-height: 1.4; +} + +.agreement-checkbox { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + cursor: pointer; +} + +.agreement-checkbox input[type="checkbox"] { + width: 12px; + height: 12px; + margin: 0; +} + +.agreement-links { + color: #2e6df6; + display: inline-block; +} + +.submit-btn { + width: 100%; + height: 48px; + line-height: 48px; + color: #fff; + font-size: 18px; + text-align: center; + background: #3474fe; + border: none; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; +} + +.submit-btn:disabled { + background: #c8c9cc; + cursor: not-allowed; +} + +.submit-btn:not(:disabled):active { + opacity: 0.9; + transform: scale(0.98); +} + +/* 动画效果 */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.asset-item.show { + animation: fadeInUp 0.5s ease forwards; +} + +/* 城市选择器模态框 */ +.city-picker-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: none; +} + +.city-picker-modal.show { + display: block; +} + +.city-picker-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.city-picker-content { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: #f5f5f5; + border-radius: 16px 16px 0 0; + max-height: 70vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.city-picker-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #e5e5e5; + background-color: #fff; +} + +.city-picker-btn { + background: none; + border: none; + font-size: 16px; + padding: 8px 16px; + cursor: pointer; + transition: opacity 0.3s ease; +} + +.city-picker-btn:active { + opacity: 0.6; +} + +.city-picker-cancel { + color: #333; +} + +.city-picker-confirm { + color: #3474fe; + font-weight: 500; +} + +.city-picker-body { + display: flex; + flex: 1; + overflow: hidden; + background-color: #f5f5f5; +} + +.city-picker-column { + flex: 1; + overflow-y: auto; + background-color: #fff; +} + +.city-picker-column:first-child { + border-right: 1px solid #e5e5e5; +} + +.city-picker-item { + padding: 12px 16px; + font-size: 14px; + color: #333; + cursor: pointer; + transition: background-color 0.2s ease; + border-bottom: 1px solid #f5f5f5; +} + +.city-picker-item:active { + background-color: #f0f0f0; +} + +.city-picker-item.active { + background-color: #f5f5f5; + color: #333; + font-weight: 500; +} + +body.modal-open { + overflow: hidden; +} + +/* Toast 提示样式 */ +.toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease; + pointer-events: none; + max-width: 80%; + text-align: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.toast.show { + opacity: 1; + visibility: visible; + transform: translate(-50%, -50%) scale(1); +} + +.toast-content { + line-height: 1.5; +} + +/* 基本信息错误提示 */ +.basic-info-error { + font-size: 12px; + color: #ff4444; + margin-top: 4px; + padding-left: 0; + text-align: right; +} + +.basic-input-inline.error { + color: #ff4444; + border-bottom: 1px solid #ff4444; + background-color: #fff5f5; +} + +/* 协议提示弹窗样式 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.modal-overlay.show { + display: flex; + opacity: 1; +} + +.modal-overlay.showing { + display: flex; +} + +#agreementModal { + align-items: center; + justify-content: center; +} + +.agreement-modal { + width: 90%; + max-width: 320px; + background-color: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + transform: scale(0.8); + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1001; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +#agreementModal.show .agreement-modal { + transform: scale(1); + opacity: 1; +} + +.agreement-modal-header { + padding: 20px 20px 16px; + text-align: center; + border-bottom: 1px solid #eee; +} + +.agreement-modal-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; +} + +.agreement-modal-body { + padding: 20px; + text-align: center; +} + +.agreement-modal-text { + font-size: 14px; + color: #333; + margin-bottom: 12px; + line-height: 1.5; +} + +.agreement-modal-links { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.agreement-modal-link { + color: #3474fe; + font-size: 14px; + text-decoration: none; + line-height: 1.5; +} + +.agreement-modal-link:active { + opacity: 0.7; +} + +.agreement-modal-footer { + display: flex; + border-top: 1px solid #eee; + padding: 0; +} + +.agreement-modal-btn { + flex: 1; + height: 48px; + line-height: 48px; + font-size: 16px; + text-align: center; + border: none; + cursor: pointer; + transition: all 0.2s ease; + background: transparent; +} + +.agreement-modal-btn-cancel { + color: #666; + border-right: 1px solid #eee; +} + +.agreement-modal-btn-cancel:active { + background-color: #f5f5f5; +} + +.agreement-modal-btn-confirm { + color: #fff; + background: linear-gradient(140deg, #3474fe, #3474fe); + font-weight: 500; +} + +.agreement-modal-btn-confirm:active { + opacity: 0.9; +} + +body.modal-open { + overflow: hidden; + position: fixed; + width: 100%; +} diff --git a/basic-info.html b/basic-info.html new file mode 100644 index 0000000..9d27403 --- /dev/null +++ b/basic-info.html @@ -0,0 +1,119 @@ + + + + + + 基本信息填写 + + + +
+ +
+
根据您的基础信息进行评估,请如实填写
+
35,000
+
*最终额度以最后审批为准
+
+
+
+
+
0%
+
+
+ + +
+
+
+ 资产信息 + 资产信息(0/6) +
+ 展开 +
+ +
+ +
+
+ + + + + + +
+ + +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ + + + + + + + diff --git a/docs/jverify-configuration.md b/docs/jverify-configuration.md new file mode 100644 index 0000000..6f8cff4 --- /dev/null +++ b/docs/jverify-configuration.md @@ -0,0 +1,250 @@ +# 极光一键登录集成配置说明 + +## 🎯 功能概述 + +已成功集成**极光一键登录**功能到薇钱包H5应用中,支持: +- ✅ 极光一键登录(无需输入手机号和验证码) +- ✅ 自动降级到短信验证码登录(当一键登录不可用时) +- ✅ 智能检测运营商网络环境 + +--- + +## 📝 配置步骤 + +### **1. 配置极光应用ID** + +编辑文件:`src/js/pages/index.page.js` + +找到第30行: +```javascript +// 极光一键登录配置 +this.jVerifyAppId = ''; // 需要配置极光应用ID +``` + +修改为: +```javascript +// 极光一键登录配置 +this.jVerifyAppId = '您的极光AppKey'; // 例如:'a1b2c3d4e5f6g7h8i9j0' +``` + +### **2. 获取极光AppKey** + +1. 登录 [极光官网](https://www.jiguang.cn/) +2. 进入「JVerification」产品 +3. 创建应用或使用现有应用 +4. 在应用设置中获取 **AppKey** + +--- + +## 🔄 工作流程 + +``` +用户打开页面 + ↓ +检测是否配置了极光AppKey? + ↓ +┌────────────┬────────────┐ +│ 已配置 │ 未配置 │ +│ 检测SDK │ 直接使用 │ +│ 可用性 │ 短信验证 │ +└────────────┴────────────┘ + ↓ +SDK可用? + ↓ +┌────────────┬────────────┐ +│ 是 │ 否 │ +│ 显示"一键 │ 降级到 │ +│ 登录"按钮 │ 短信验证 │ +└────────────┴────────────┘ + ↓ +用户点击"一键登录" + ↓ +获取Token → 验证成功? + ↓ +┌────────────┬────────────┐ +│ 成功 │ 失败 │ +│ 自动登录 │ 降级到 │ +│ 跳转页面 │ 短信验证 │ +└────────────┴────────────┘ +``` + +--- + +## 🎨 UI展示 + +### **一键登录可用时:** +``` +┌─────────────────────────┐ +│ 📱 一键登录 │ ← 紫色渐变按钮 +├─────────────────────────┤ +│ ─────────── 或 ─────── │ +│ │ +│ 手机号: [____________] │ +│ 立即申请 │ +└─────────────────────────┘ +``` + +### **一键登录不可用时(自动降级):** +``` +┌─────────────────────────┐ +│ │ +│ 手机号: [____________] │ +│ 立即申请 │ +└─────────────────────────┘ +``` + +--- + +## 🔧 后端API配置 + +确保后端服务 `ali-sms` 正常运行: + +1. **启动后端服务** + ```bash + cd /e/wk-oth/go-work/ali-sms + go run main.go + ``` + +2. **确认API端点** + - 一键登录验证:`POST /auth/jpush/login` + - 请求参数: + ```json + { + "appId": "您的极光AppKey", + "loginToken": "极光SDK获取的token" + } + ``` + +3. **配置CORS** + - 确保后端允许前端域名跨域访问 + - 或在开发环境使用代理 + +--- + +## 📱 测试步骤 + +### **1. 配置AppKey** +编辑 `src/js/pages/index.page.js`,设置 `this.jVerifyAppId` + +### **2. 启动前端服务** +```bash +# 在 IDEA 中右键 index.html → Open In Browser +``` + +### **3. 观察行为** + +**场景A:极光SDK可用** +- ✅ 显示"一键登录"按钮 +- ✅ 点击后自动登录 +- ✅ 成功后跳转页面 + +**场景B:极光SDK不可用** +- ✅ 自动隐藏"一键登录"按钮 +- ✅ 直接显示手机号输入框 +- ✅ 使用短信验证码登录 + +**场景C:一键登录失败** +- ✅ 显示降级提示 +- ✅ 自动切换到短信验证码登录 + +--- + +## 🐛 调试方法 + +### **查看控制台日志** +```javascript +// 一键登录初始化 +[IndexPage] 未配置极光应用ID,跳过一键登录 // 需要配置AppKey +[OneClickLoginButton] 一键登录可用 // SDK加载成功 +[OneClickLoginButton] 一键登录不可用 // SDK加载失败,会自动降级 + +// 一键登录过程 +[JVerifyService] 极光SDK初始化成功 +[JVerifyService] 获取token成功 +[JVerifyService] 一键登录成功: 138****8000 +[IndexPage] 一键登录后注册成功 +``` + +### **常见问题** + +#### **问题1:不显示一键登录按钮** +- ✅ 检查是否配置了 `this.jVerifyAppId` +- ✅ 打开控制台查看错误信息 +- ✅ 确认用户未登录状态 + +#### **问题2:点击后无反应** +- ✅ 检查后端服务是否运行 +- ✅ 检查网络请求(F12 → Network标签) +- ✅ 查看控制台错误 + +#### **问题3:登录失败但未降级** +- ✅ 检查 `onFallback` 回调是否正常 +- ✅ 查看后端返回的错误信息 + +--- + +## 🔐 安全建议 + +1. **保护AppKey** + - 不要将AppKey硬编码在前端(考虑从后端获取) + - 生产环境使用环境变量 + +2. **手机号保护** + - 后端响应已包含 `phoneMasked`(脱敏手机号) + - 生产环境建议移除 `phone` 字段 + +3. **HTTPS传输** + - 生产环境强制使用HTTPS + - 防止中间人攻击 + +--- + +## 📚 相关文件清单 + +### **新增文件** +``` +src/js/ +├── services/ +│ └── jverify.service.js # 极光一键登录服务 +├── ui/ +│ └── one-click-login.js # 一键登录按钮组件 +└── pages/ + └── index.page.js # 主页面(已集成) + +src/css/components/ +└── one-click-login.css # 一键登录样式 +``` + +### **修改文件** +``` +index.html # 添加了一键登录容器和CSS引用 +``` + +--- + +## ✅ 完成检查清单 + +- [ ] 配置极光AppKey(在 `index.page.js` 中) +- [ ] 启动后端服务 `ali-sms` +- [ ] 测试一键登录功能(手机端访问) +- [ ] 测试降级逻辑(关闭WiFi,使用4G/5G) +- [ ] 测试登录失败场景 +- [ ] 检查生产环境配置 + +--- + +## 🎉 完成效果 + +配置完成后,用户将享受: + +1. **更快的登录体验** - 无需输入手机号和验证码 +2. **无缝降级** - 一键登录不可用时自动切换 +3. **更好的用户体验** - 减少操作步骤 + +**一键登录成功率通常达到 85%+!** 📈 + +--- + +**配置完成日期:** 2025-01-22 +**集成人员:** Claude Code +**版本:** 2.1.0 diff --git a/docs/offline-sdk-guide.md b/docs/offline-sdk-guide.md new file mode 100644 index 0000000..d5628fe --- /dev/null +++ b/docs/offline-sdk-guide.md @@ -0,0 +1,186 @@ +# 极光离线SDK配置指南 + +## 🎯 目标 +解决 `ERR_CONNECTION_CLOSED` 错误,使用离线SDK文件。 + +⚠️ **重要提示**:极光一键登录SDK需要 **crypto-js** 作为依赖库! + +--- + +## 📦 完整依赖(2个文件) + +### **必需文件:** +1. **crypto-js.min.js** - 加密库依赖(必需) +2. **jverification_web.js** - 极光一键登录SDK + +--- + +## 📁 方法1:从CDN下载(推荐,最快) + +### **步骤1:下载 crypto-js** + +浏览器打开以下任一地址,保存网页内容: +``` +https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js +或 +https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js +``` + +**保存为**:`E:\wk-flux\微钱包_h5\web\src\assets\js\crypto-js.min.js` + +### **步骤2:下载极光SDK** + +浏览器打开: +``` +https://jverification.jiguang.cn/scripts/jverification-web.5.3.1.min.js +``` + +**保存为**:`E:\wk-flux\微钱包_h5\web\src\assets\js\jverification_web.js` + +### **步骤3:完成** + +刷新页面即可! + +--- + +## 📁 方法2:从极光官方下载 + +--- + +## 🔧 **方法2:使用备用CDN(已自动配置)** + +我已经为您配置了多CDN自动切换机制: + +```javascript +// CDN加载优先级: +1. https://js.jverification.com/jverification_web.js // 官方CDN +2. https://cdn.jsdelivr.net/npm/.../jverification_web.js // jsDelivr +3. https://unpkg.com/jverification-web-sdk/.../jverification_web.js // unpkg +4. ./src/assets/js/jverification_web.js // 本地文件 +``` + +**代码已自动尝试所有CDN源,无需手动配置!** + +--- + +## 📋 **方法3:从GitHub获取(开发者)** + +如果您熟悉Git和npm: + +```bash +# 方案A:使用npm安装(推荐) +npm install jverification-web-sdk --save +# 然后复制文件: +cp node_modules/jverification-web-sdk/dist/jverification_web.js src/assets/js/ + +# 方案B:从GitHub下载 +# 访问: +# https://github.com/jpush/jverification-web-sdk +``` + +--- + +## 🌐 **方法4:使用代理/VPN(如果网络受限)** + +1. 开启VPN +2. 访问:`https://js.jverification.com/jverification_web.js` +3. 保存网页内容为 `jverification_web.js` +4. 保存到:`src/assets/js/` + +--- + +## ✅ **验证配置** + +### **测试步骤:** + +1. **下载并保存文件** +2. **刷新页面**(Ctrl + Shift + R) +3. **查看控制台** + +### **成功标志:** +``` +✅ [JVerifyService] CryptoJS加载成功: https://cdnjs.cloudflare.com/... +✅ [JVerifyService] JVerification SDK加载成功: https://jverification.jiguang.cn/... + 或 +✅ [JVerifyService] CryptoJS加载成功: ./src/assets/js/crypto-js.min.js +✅ [JVerifyService] JVerification SDK加载成功: ./src/assets/js/jverification_web.js +``` + +### **失败标志:** +``` +❌ [JVerifyService] CryptoJS CDN加载失败: https://cdnjs.cloudflare.com/... +❌ [JVerifyService] JVerification CDN加载失败: https://jverification.jiguang.cn/... +❌ [JVerifyService] 所有CDN源均无法访问 +``` + +--- + +## 🚀 **快速配置(推荐)** + +### **最快方案:使用备用CDN** + +我已经为您配置了备用CDN,通常能直接访问。 + +**刷新页面试试!** 很多时候 jsDelivr 或 unpkg 可以访问! + +### **如果备用CDN也不行:** + +1. 访问极光资源页面下载(方法1) +2. 或使用VPN下载(方法4) +3. 保存到本地 `src/assets/js/jverification_web.js` + +--- + +## 📱 **测试一键登录** + +### **在真实手机上测试(推荐):** + +1. **手机连接电脑** + - Android: 使用 Chrome DevTools 远程调试 + - iPhone: 使用 Safari Web Inspector + +2. **或部署到服务器** + - 使用真实域名访问 + - 在手机浏览器打开 + +3. **使用移动网络** + - 关闭WiFi + - 使用4G/5G网络 + +--- + +## ⚠️ **重要说明** + +### **当前状态:** + +- ✅ **后端服务**:已配置并运行(端口26117) +- ✅ **前端配置**:AppKey已配置 +- ✅ **RSA密钥**:已配置 +- ✅ **Picker组件**:已修复 +- ⏳ **离线SDK**:待下载 + +### **即使没有SDK,功能仍然正常:** + +- ✅ 短信验证码登录 +- ✅ 表单验证 +- ✅ 自动登录 +- ✅ 页面跳转 + +**一键登录是锦上添花,不是必需功能!** + +--- + +## 🎯 **推荐做法** + +### **开发阶段:** +- ✅ 使用短信验证码登录(当前) +- ⏸️ 稍后配置一键登录 + +### **生产环境:** +- ✅ 使用离线SDK文件 +- ✅ 或使用备用CDN +- ✅ 确保移动网络可用 + +--- + +**请选择一个方法配置SDK,或告诉我您的选择!** 😊 \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5c438d8 --- /dev/null +++ b/index.html @@ -0,0 +1,217 @@ + + + + + + 薇钱包 + + + + +
+ +
+
+
+ +
全部借出200000
+
+
+ 年化利率10.8%-24%(单利),实际以审核为准 +
+
+ + +
+
+
还款方式
+
+
随借随还
+
按日计算,提前还0手续费
+
+
+
+
还款期数
+
+
12个月
+
年化利率10.8%-24%(单利),以实际为准
+
+
+
+
还款计划
+
+
首期02月05日 应还 4916.67元
+
+
+
+ +
* 贷款试算功能,仅作为借还款信息参考
+ + +
+
+
利率
+
年化利率10.8%-24% (单利)
+
+
+
借款用途
+
+ 个人日常消费 + > +
+
+
+ + +
+
+
手机号
+ +
+ + + + + +
+ +
+
+ + +
+ + 《个人信息共享授权书》 + 《白丫融注册协议》 + 《隐私政策》 +
+ + + +
+ + + + + + + + + + + + + + + + + + diff --git a/jverification_web.js b/jverification_web.js new file mode 100644 index 0000000..e561634 --- /dev/null +++ b/jverification_web.js @@ -0,0 +1,55 @@ +!function(n){var i={};function r(t){if(i[t])return i[t].exports;var e=i[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}r.m=n,r.c=i,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=18)}([function(t,e,n){var i,l,r,o,s,a,p,c,u,h,f,d,g;t.exports=(i=i||(l=Math,r=Object.create||function(){function n(){}return function(t){var e;return n.prototype=t,e=new n,n.prototype=null,e}}(),s=(o={}).lib={},a=s.Base={extend:function(t){var e=r(this);return t&&e.mixIn(t),e.hasOwnProperty("init")&&this.init!==e.init||(e.init=function(){e.$super.init.apply(this,arguments)}),(e.init.prototype=e).$super=this,e},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},p=s.WordArray=a.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:4*t.length},toString:function(t){return(t||u).stringify(this)},concat:function(t){var e=this.words,n=t.words,i=this.sigBytes,r=t.sigBytes;if(this.clamp(),i%4)for(var o=0;o>>2]>>>24-o%4*8&255;e[i+o>>>2]|=s<<24-(i+o)%4*8}else for(var o=0;o>>2]=n[o>>>2];return this.sigBytes+=r,this},clamp:function(){var t=this.words,e=this.sigBytes;t[e>>>2]&=4294967295<<32-e%4*8,t.length=l.ceil(e/4)},clone:function(){var t=a.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var e,n=[],i=function(e){var e=e,n=987654321,i=4294967295;return function(){var t=((n=36969*(65535&n)+(n>>16)&i)<<16)+(e=18e3*(65535&e)+(e>>16)&i)&i;return t/=4294967296,(t+=.5)*(.5>>2]>>>24-r%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var e=t.length,n=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new p.init(n,e/2)}},h=c.Latin1={stringify:function(t){for(var e=t.words,n=t.sigBytes,i=[],r=0;r>>2]>>>24-r%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,n=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new p.init(n,e)}},f=c.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=s.BufferedBlockAlgorithm=a.extend({reset:function(){this._data=new p.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=f.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var e=this._data,n=e.words,i=e.sigBytes,r=this.blockSize,o=4*r,s=i/o,a=(s=t?l.ceil(s):l.max((0|s)-this._minBufferSize,0))*r,c=l.min(4*a,i);if(a){for(var u=0;u>>2];t.sigBytes-=e}},o.BlockCipher=f.extend({cfg:f.cfg.extend({mode:m,padding:v}),reset:function(){f.reset.call(this);var t=this.cfg,e=t.iv,n=t.mode;if(this._xformMode==this._ENC_XFORM_MODE)var i=n.createEncryptor;else{var i=n.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==i?this._mode.init(this,e&&e.words):(this._mode=i.call(n,this,e&&e.words),this._mode.__creator=i)},_doProcessBlock:function(t,e){this._mode.processBlock(t,e)},_doFinalize:function(){var t=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){t.pad(this._data,this.blockSize);var e=this._process(!0)}else{var e=this._process(!0);t.unpad(e)}return e},blockSize:4}),C=o.CipherParams=s.extend({init:function(t){this.mixIn(t)},toString:function(t){return(t||this.formatter).stringify(this)}}),T=r.format={},E=T.OpenSSL={stringify:function(t){var e=t.ciphertext,n=t.salt;if(n)var i=c.create([1398893684,1701076831]).concat(n).concat(e);else var i=e;return i.toString(h)},parse:function(t){var e=h.parse(t),n=e.words;if(1398893684==n[0]&&1701076831==n[1]){var i=c.create(n.slice(2,4));n.splice(0,4),e.sigBytes-=16}return C.create({ciphertext:e,salt:i})}},S=o.SerializableCipher=s.extend({cfg:s.extend({format:E}),encrypt:function(t,e,n,i){i=this.cfg.extend(i);var r=t.createEncryptor(n,i),o=r.finalize(e),s=r.cfg;return C.create({ciphertext:o,key:n,iv:s.iv,algorithm:t,mode:s.mode,padding:s.padding,blockSize:t.blockSize,formatter:i.format})},decrypt:function(t,e,n,i){i=this.cfg.extend(i),e=this._parse(e,i.format);var r=t.createDecryptor(n,i).finalize(e.ciphertext);return r},_parse:function(t,e){return"string"==typeof t?e.parse(t,this):t}}),O=r.kdf={},_=O.OpenSSL={execute:function(t,e,n,i){i||(i=c.random(8));var r=p.create({keySize:e+n}).compute(t,i),o=c.create(r.words.slice(e),4*n);return r.sigBytes=4*e,C.create({key:r,iv:o,salt:i})}},A=o.PasswordBasedCipher=S.extend({cfg:S.cfg.extend({kdf:_}),encrypt:function(t,e,n,i){var r=(i=this.cfg.extend(i)).kdf.execute(n,t.keySize,t.ivSize);i.iv=r.iv;var o=S.encrypt.call(this,t,e,r.key,i);return o.mixIn(r),o},decrypt:function(t,e,n,i){i=this.cfg.extend(i),e=this._parse(e,i.format);var r=i.kdf.execute(n,t.keySize,t.ivSize,e.salt);i.iv=r.iv;var o=S.decrypt.call(this,t,e,r.key,i);return o}}))))},function(t,e,n){"use strict";(function(t){e.__esModule=!0,e.JGCons={SDK_VERSION:"5.3.1",LOG_TAG_PREFIX:"[JIGUANG-JVerification]",META_NAME:"referrer",META_CONTENT:"always",IFRAME_URL:"https://jverification.jiguang.cn",HTML_URL:"http://huminios.com",URL_SIGN_CU:"https://h5.verification.jiguang.cn/v1/open/sign/cu2",REQUEST:{URL_INIT:"https://h5.verification.jiguang.cn/v1/open/init",URL_SIGN_CM:"https://h5.verification.jiguang.cn/v1/open/sign/cm",URL_LOGIN_SIGN_CM:"https://h5.verification.jiguang.cn/v1/open/login/sign/cm",URL_SIGN_CT:"https://h5.verification.jiguang.cn/v1/open/sign/ct",URL_LOGIN_SIGN_CT:"https://h5.verification.jiguang.cn/v1/open/login/sign/ct",URL_SIGN_CU:"https://h5.verification.jiguang.cn/v1/open/sign/cu2",URL_SIGN_CU2:t.env.URL_SIGN_CU2,TIME_OUT:5e3,TOKEN:{TYPE:"verify",VERSION:1},LOGIN:{TYPE:"login",VERSION:1}},OPERATOR:{CM:"CM",CM_SUPPORT:!0,CM_YDRZ:"https://www.cmpassport.com/NumberAbility/jssdkVlm_yw/jssdk_v1.0.0.min.js",CM_LOGIN_SRC:"https://h5auth.cmpassport.com/h5/js/jssdk_auth_idaas/jssdk.min.js",CM_LOGIN_SEC_CSS:"https://h5auth.cmpassport.com/h5/js/jssdk_auth_idaas/css/ydrz-layer.css",CM_CHECK_SRC_TIME:500,CM_CHECK_SRC_COUNT:10,CU:"CU",CU_SUPPORT:!0,CU_CUAU:"https://opencloud.wostore.cn/authz/resource/js/jssdk/auth-ujssdk.min.js",CU_LOGIN_SRC:"https://opencloud.wostore.cn/h5netauth/h5login/singleton/h5auth1.min.js",CU_UI:{CU_LOGIN_LOGO:"https://opencloud.wostore.cn/h5netauth/resource/images/logo5.png",CU_APP_NAME:"应用"},CU_CHECK_SRC_TIME:500,CU_CHECK_SRC_COUNT:10,CT:"CT",CT_SUPPORT:!0,CT_LOGIN_SRC:"https://static.e.189.cn/open/login/js/wap/js-sdk/EAccountSDK-fjs-1.5.2.min.js",CT_CHECK_SRC_TIME:500,CT_CHECK_SRC_COUNT:10},CUSTOMUI:{appName:"应用名称",btnName:2,logo:"/image/logo.png",iframeLogo:"",iframeLogoPath:"",maskPhone:"",authPageType:0,isConcealLoginBtn:!1,loginBtnColor:"",loginTextColor:"",customPolicyLink1:["",""],customPolicyLink2:["",""],customPolicyLink3:["",""],customPolicyLinkColor:"",isDisplayOtherWayBtn:!1,customOtherWayText:"",customOtherWayTextColor:"",isOpenAuthOperationMonitor:!0}}}).call(this,n(14))},function(t,e,n){"use strict";e.__esModule=!0;var i=n(7);e.getRandomId=function(t){return Math.random().toString(36).substr(2,t||8)},e.isEmpty=function(t){return!t||0==t.length},e.log=function(t,e,n){n?i.Log.dd(e,t):i.Log.d(e,t)},e.isOnLine=function(){var t=!0;if(window.navigator){if("boolean"==typeof(t=window.navigator.onLine))return t;t=!0}return t},e.isWifi=function(){var t="",e=window.navigator.connection;return e&&(t=e.type),!!t&&"WIFI"==t.toUpperCase()},e.isDesktop=function(){return!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)},e.uuid=function(t,e){var n,i="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),r=[];if(e=e||i.length,t)for(n=0;n>4,n=(15&o)<<4|s>>2,i=(3&s)<<6|a,c[l++]=e,64!=s&&(c[l++]=n),64!=a&&(c[l++]=i);return c}(t),n="",i=0;i>=4,e=r.substr(15&t,1)+e;return e=e.toLowerCase()}var p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},function(t,e,n){var i,r,o,s,h,a,c,u;t.exports=(i=n(0),n(12),n(13),o=(r=i).lib,s=o.Base,h=o.WordArray,a=r.algo,c=a.MD5,u=a.EvpKDF=s.extend({cfg:s.extend({keySize:4,hasher:c,iterations:1}),init:function(t){this.cfg=this.cfg.extend(t)},compute:function(t,e){for(var n=this.cfg,i=n.hasher.create(),r=h.create(),o=r.words,s=n.keySize,a=n.iterations;o.length>>2]>>>24-o%4*8&255)<<16|(e[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|e[o+2>>>2]>>>24-(o+2)%4*8&255,a=0;a<4&&o+.75*a>>6*(3-a)&63));var c=i.charAt(64);if(c)for(;r.length%4;)r.push(c);return r.join("")},parse:function(t){var e=t.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var r=0;r>>6-o%4*2;i[r>>>2]|=(s|a)<<24-r%4*8,r++}return c.create(i,r)}(t,e,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},i.enc.Base64)},function(t,e,n){var s;t.exports=(s=n(0),function(h){var t=s,e=t.lib,n=e.WordArray,i=e.Hasher,r=t.algo,I=[];!function(){for(var t=0;t<64;t++)I[t]=4294967296*h.abs(h.sin(t+1))|0}();var o=r.MD5=i.extend({_doReset:function(){this._hash=new n.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,e){for(var n=0;n<16;n++){var i=e+n,r=t[i];t[i]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8)}var o=this._hash.words,s=t[e+0],a=t[e+1],c=t[e+2],u=t[e+3],h=t[e+4],l=t[e+5],p=t[e+6],f=t[e+7],d=t[e+8],g=t[e+9],m=t[e+10],y=t[e+11],v=t[e+12],C=t[e+13],T=t[e+14],E=t[e+15],S=o[0],O=o[1],_=o[2],A=o[3];O=k(O=k(O=k(O=k(O=M(O=M(O=M(O=M(O=w(O=w(O=w(O=w(O=b(O=b(O=b(O=b(O,_=b(_,A=b(A,S=b(S,O,_,A,s,7,I[0]),O,_,a,12,I[1]),S,O,c,17,I[2]),A,S,u,22,I[3]),_=b(_,A=b(A,S=b(S,O,_,A,h,7,I[4]),O,_,l,12,I[5]),S,O,p,17,I[6]),A,S,f,22,I[7]),_=b(_,A=b(A,S=b(S,O,_,A,d,7,I[8]),O,_,g,12,I[9]),S,O,m,17,I[10]),A,S,y,22,I[11]),_=b(_,A=b(A,S=b(S,O,_,A,v,7,I[12]),O,_,C,12,I[13]),S,O,T,17,I[14]),A,S,E,22,I[15]),_=w(_,A=w(A,S=w(S,O,_,A,a,5,I[16]),O,_,p,9,I[17]),S,O,y,14,I[18]),A,S,s,20,I[19]),_=w(_,A=w(A,S=w(S,O,_,A,l,5,I[20]),O,_,m,9,I[21]),S,O,E,14,I[22]),A,S,h,20,I[23]),_=w(_,A=w(A,S=w(S,O,_,A,g,5,I[24]),O,_,T,9,I[25]),S,O,u,14,I[26]),A,S,d,20,I[27]),_=w(_,A=w(A,S=w(S,O,_,A,C,5,I[28]),O,_,c,9,I[29]),S,O,f,14,I[30]),A,S,v,20,I[31]),_=M(_,A=M(A,S=M(S,O,_,A,l,4,I[32]),O,_,d,11,I[33]),S,O,y,16,I[34]),A,S,T,23,I[35]),_=M(_,A=M(A,S=M(S,O,_,A,a,4,I[36]),O,_,h,11,I[37]),S,O,f,16,I[38]),A,S,m,23,I[39]),_=M(_,A=M(A,S=M(S,O,_,A,C,4,I[40]),O,_,s,11,I[41]),S,O,u,16,I[42]),A,S,p,23,I[43]),_=M(_,A=M(A,S=M(S,O,_,A,g,4,I[44]),O,_,v,11,I[45]),S,O,E,16,I[46]),A,S,c,23,I[47]),_=k(_,A=k(A,S=k(S,O,_,A,s,6,I[48]),O,_,f,10,I[49]),S,O,T,15,I[50]),A,S,l,21,I[51]),_=k(_,A=k(A,S=k(S,O,_,A,v,6,I[52]),O,_,u,10,I[53]),S,O,m,15,I[54]),A,S,a,21,I[55]),_=k(_,A=k(A,S=k(S,O,_,A,d,6,I[56]),O,_,E,10,I[57]),S,O,p,15,I[58]),A,S,C,21,I[59]),_=k(_,A=k(A,S=k(S,O,_,A,h,6,I[60]),O,_,y,10,I[61]),S,O,c,15,I[62]),A,S,g,21,I[63]),o[0]=o[0]+S|0,o[1]=o[1]+O|0,o[2]=o[2]+_|0,o[3]=o[3]+A|0},_doFinalize:function(){var t=this._data,e=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;e[i>>>5]|=128<<24-i%32;var r=h.floor(n/4294967296),o=n;e[15+(i+64>>>9<<4)]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8),e[14+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),t.sigBytes=4*(e.length+1),this._process();for(var s=this._hash,a=s.words,c=0;c<4;c++){var u=a[c];a[c]=16711935&(u<<8|u>>>24)|4278255360&(u<<24|u>>>8)}return s},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});function b(t,e,n,i,r,o,s){var a=t+(e&n|~e&i)+r+s;return(a<>>32-o)+e}function w(t,e,n,i,r,o,s){var a=t+(e&i|n&~i)+r+s;return(a<>>32-o)+e}function M(t,e,n,i,r,o,s){var a=t+(e^n^i)+r+s;return(a<>>32-o)+e}function k(t,e,n,i,r,o,s){var a=t+(n^(e|~i))+r+s;return(a<>>32-o)+e}t.MD5=i._createHelper(o),t.HmacMD5=i._createHmacHelper(o)}(Math),s.MD5)},function(t,e,n){"use strict";e.__esModule=!0;var i,r,o,s=n(2);i=e.Log||(e.Log={}),r=!1,o=s.JGCons.LOG_TAG_PREFIX,i.setDebugMode=function(t){i.d(o,"setDebugMode:"+t),r=t},i.d=function(t,e){},i.dd=function(t,e){r&&window.console&&console.log(o+"["+t+"]"+e)},i.ww=function(t,e){window.console&&console.warn(o+"["+t+"]"+e)},i.ee=function(t,e){window.console&&console.error(o+"["+t+"]"+e)}},function(t,e,n){"use strict";e.__esModule=!0;var s=n(7),a=n(2),c="RequestHelper",i=function(){function s(){}return s.prototype.request=function(t,e){var n=this.generateHttpRequest();if(n){if(n.onload=function(t){4===n.readyState&&(200==n.status?e(!0,s.REQUEST_SUCC,n.responseText,n.status):403==n.status?e(!1,s.REQUEST_FAILED,n.responseText,n.status):e(!1,s.REQUEST_FAILED,"request failed with status "+n.status,n.status))},n.onloadstart=function(t){},n.onabort=function(t){e(!1,s.REQUEST_ERROR,"request onabort")},n.onerror=function(t){e(!1,s.REQUEST_ERROR,"request error")},n.ontimeout=function(t){e(!1,s.REQUEST_TIME_OUT,"request timeout")},"GET"==t.method){if(t.param){var i=t.param,r=[];for(var o in i)i.hasOwnProperty(o)&&r.push(encodeURIComponent(o)+"="+encodeURIComponent(i[o]));t.url=t.url+"?"+r.join("&")}n.open("GET",t.url,t.async),t.async&&(n.timeout=a.JGCons.REQUEST.TIME_OUT),n.send(null)}else n.open("POST",t.url,t.async),t.async&&(n.timeout=a.JGCons.REQUEST.TIME_OUT),n.setRequestHeader("Content-Type","application/json;charset=UTF-8"),n.send(t.param);return t.async?void 0:200==n.status?{result:n.responseText,statusCode:n.status}:{reslut:"request failed with status "+n.status,statusCode:n.status}}e(!1,s.REQUEST_ERROR,"not support ajax")},s.prototype.generateHttpRequest=function(){var e;if(XMLHttpRequest)try{e=new XMLHttpRequest}catch(t){}else if(ActiveXObject)try{e=new ActiveXObject("Microsoft.XMLHTTP")}catch(t){e=new ActiveXObject("Msxml2.XMLHTTP")}return e},s.REQUEST_SUCC=0,s.REQUEST_TIME_OUT=1,s.REQUEST_ERROR=2,s.REQUEST_FAILED=3,s}();e.RequestController=i,e.http=function(t){return t.async?new Promise(function(r,o){(new i).request(t,function(t,e,n,i){t?(s.Log.d(c,"request "+(t?"success":"failed")+":"+i+"_"+JSON.stringify(n)),r({result:n})):(s.Log.ee(c,"request failed :"+i+"_"+e+"_"+JSON.stringify(n)),o({code:e,result:n,statusCode:i}))})}):(new i).request(t,function(t,e,n,i){})},e.jsonp=function(e){if(!(e=e||{}).url||!e.callback)throw new Error("参数不合法");var n=document.getElementsByTagName("head")[0],t="";e.data?(e.data[e.callback]=e.callback,t+=function(t){var e=[];for(var n in t)e.push(encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e.join("&")}(e.data)):t+=e.callback+"="+e.callback;var i=document.createElement("script");n.appendChild(i);var r=window;if(r[e.callback]=function(t){n.removeChild(i),clearTimeout(i.timer),r[e.callback]=null,e.success&&e.success(t)},1===e.oSscrType){var o=e.url.indexOf("?")<0?"?":"&";i.src=e.url+o+t}else i.src=e.url;i.timer=setTimeout(function(){r[e.callback]=null,n.removeChild(i),e.fail&&e.fail({message:"request timeout"})},a.JGCons.REQUEST.TIME_OUT)}},function(t,e,n){var i;t.exports=(i=n(0),n(10),n(24),n(25),n(5),n(6),n(12),n(15),n(26),n(16),n(27),n(28),n(29),n(13),n(30),n(4),n(1),n(31),n(32),n(33),n(34),n(35),n(36),n(37),n(38),n(39),n(40),n(41),n(42),n(43),n(44),n(45),n(46),i)},function(t,e,n){var i,r,o,s,a,c;t.exports=(i=n(0),o=(r=i).lib,s=o.Base,a=o.WordArray,(c=r.x64={}).Word=s.extend({init:function(t,e){this.high=t,this.low=e}}),c.WordArray=s.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:8*t.length},toX32:function(){for(var t=this.words,e=t.length,n=[],i=0;i>>31}var h=(i<<5|i>>>27)+a+l[c];h+=c<20?1518500249+(r&o|~r&s):c<40?1859775393+(r^o^s):c<60?(r&o|r&s|o&s)-1894007588:(r^o^s)-899497514,a=s,s=o,o=r<<30|r>>>2,r=i,i=h}n[0]=n[0]+i|0,n[1]=n[1]+r|0,n[2]=n[2]+o|0,n[3]=n[3]+s|0,n[4]=n[4]+a|0},_doFinalize:function(){var t=this._data,e=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(i+64>>>9<<4)]=Math.floor(n/4294967296),e[15+(i+64>>>9<<4)]=n,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=a.clone.call(this);return t._hash=this._hash.clone(),t}}),r.SHA1=a._createHelper(u),r.HmacSHA1=a._createHmacHelper(u),i.SHA1)},function(t,e,n){var i,r,o,s,a,u,c;t.exports=(i=n(0),o=(r=i).lib,s=o.Base,a=r.enc,u=a.Utf8,c=r.algo,void(c.HMAC=s.extend({init:function(t,e){t=this._hasher=new t.init,"string"==typeof e&&(e=u.parse(e));var n=t.blockSize,i=4*n;e.sigBytes>i&&(e=t.finalize(e)),e.clamp();for(var r=this._oKey=e.clone(),o=this._iKey=e.clone(),s=r.words,a=o.words,c=0;c>>7)^(p<<14|p>>>18)^p>>>3,d=T[l-2],g=(d<<15|d>>>17)^(d<<13|d>>>19)^d>>>10;T[l]=f+T[l-7]+g+T[l-16]}var m=i&r^i&o^r&o,y=(i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22),v=h+((a<<26|a>>>6)^(a<<21|a>>>11)^(a<<7|a>>>25))+(a&c^~a&u)+C[l]+T[l];h=u,u=c,c=a,a=s+v|0,s=o,o=r,r=i,i=v+(y+m)|0}n[0]=n[0]+i|0,n[1]=n[1]+r|0,n[2]=n[2]+o|0,n[3]=n[3]+s|0,n[4]=n[4]+a|0,n[5]=n[5]+c|0,n[6]=n[6]+u|0,n[7]=n[7]+h|0},_doFinalize:function(){var t=this._data,e=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(i+64>>>9<<4)]=r.floor(n/4294967296),e[15+(i+64>>>9<<4)]=n,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});t.SHA256=i._createHelper(a),t.HmacSHA256=i._createHmacHelper(a)}(Math),c.SHA256)},function(t,e,n){var c;t.exports=(c=n(0),n(10),function(){var t=c,e=t.lib.Hasher,n=t.x64,i=n.Word,r=n.WordArray,o=t.algo;function s(){return i.create.apply(i,arguments)}var Ot=[s(1116352408,3609767458),s(1899447441,602891725),s(3049323471,3964484399),s(3921009573,2173295548),s(961987163,4081628472),s(1508970993,3053834265),s(2453635748,2937671579),s(2870763221,3664609560),s(3624381080,2734883394),s(310598401,1164996542),s(607225278,1323610764),s(1426881987,3590304994),s(1925078388,4068182383),s(2162078206,991336113),s(2614888103,633803317),s(3248222580,3479774868),s(3835390401,2666613458),s(4022224774,944711139),s(264347078,2341262773),s(604807628,2007800933),s(770255983,1495990901),s(1249150122,1856431235),s(1555081692,3175218132),s(1996064986,2198950837),s(2554220882,3999719339),s(2821834349,766784016),s(2952996808,2566594879),s(3210313671,3203337956),s(3336571891,1034457026),s(3584528711,2466948901),s(113926993,3758326383),s(338241895,168717936),s(666307205,1188179964),s(773529912,1546045734),s(1294757372,1522805485),s(1396182291,2643833823),s(1695183700,2343527390),s(1986661051,1014477480),s(2177026350,1206759142),s(2456956037,344077627),s(2730485921,1290863460),s(2820302411,3158454273),s(3259730800,3505952657),s(3345764771,106217008),s(3516065817,3606008344),s(3600352804,1432725776),s(4094571909,1467031594),s(275423344,851169720),s(430227734,3100823752),s(506948616,1363258195),s(659060556,3750685593),s(883997877,3785050280),s(958139571,3318307427),s(1322822218,3812723403),s(1537002063,2003034995),s(1747873779,3602036899),s(1955562222,1575990012),s(2024104815,1125592928),s(2227730452,2716904306),s(2361852424,442776044),s(2428436474,593698344),s(2756734187,3733110249),s(3204031479,2999351573),s(3329325298,3815920427),s(3391569614,3928383900),s(3515267271,566280711),s(3940187606,3454069534),s(4118630271,4000239992),s(116418474,1914138554),s(174292421,2731055270),s(289380356,3203993006),s(460393269,320620315),s(685471733,587496836),s(852142971,1086792851),s(1017036298,365543100),s(1126000580,2618297676),s(1288033470,3409855158),s(1501505948,4234509866),s(1607167915,987167468),s(1816402316,1246189591)],_t=[];!function(){for(var t=0;t<80;t++)_t[t]=s()}();var a=o.SHA512=e.extend({_doReset:function(){this._hash=new r.init([new i.init(1779033703,4089235720),new i.init(3144134277,2227873595),new i.init(1013904242,4271175723),new i.init(2773480762,1595750129),new i.init(1359893119,2917565137),new i.init(2600822924,725511199),new i.init(528734635,4215389547),new i.init(1541459225,327033209)])},_doProcessBlock:function(t,e){for(var n=this._hash.words,i=n[0],r=n[1],o=n[2],s=n[3],a=n[4],c=n[5],u=n[6],h=n[7],l=i.high,p=i.low,f=r.high,d=r.low,g=o.high,m=o.low,y=s.high,v=s.low,C=a.high,T=a.low,E=c.high,S=c.low,O=u.high,_=u.low,A=h.high,I=h.low,b=l,w=p,M=f,k=d,U=g,R=m,B=y,x=v,D=C,L=T,N=E,G=S,P=O,J=_,H=A,V=I,j=0;j<80;j++){var F=_t[j];if(j<16)var Q=F.high=0|t[e+2*j],K=F.low=0|t[e+2*j+1];else{var q=_t[j-15],z=q.high,Z=q.low,Y=(z>>>1|Z<<31)^(z>>>8|Z<<24)^z>>>7,W=(Z>>>1|z<<31)^(Z>>>8|z<<24)^(Z>>>7|z<<25),X=_t[j-2],$=X.high,tt=X.low,et=($>>>19|tt<<13)^($<<3|tt>>>29)^$>>>6,nt=(tt>>>19|$<<13)^(tt<<3|$>>>29)^(tt>>>6|$<<26),it=_t[j-7],rt=it.high,ot=it.low,st=_t[j-16],at=st.high,ct=st.low;Q=(Q=(Q=Y+rt+((K=W+ot)>>>0>>0?1:0))+et+((K+=nt)>>>0>>0?1:0))+at+((K+=ct)>>>0>>0?1:0),F.high=Q,F.low=K}var ut,ht=D&N^~D&P,lt=L&G^~L&J,pt=b&M^b&U^M&U,ft=w&k^w&R^k&R,dt=(b>>>28|w<<4)^(b<<30|w>>>2)^(b<<25|w>>>7),gt=(w>>>28|b<<4)^(w<<30|b>>>2)^(w<<25|b>>>7),mt=(D>>>14|L<<18)^(D>>>18|L<<14)^(D<<23|L>>>9),yt=(L>>>14|D<<18)^(L>>>18|D<<14)^(L<<23|D>>>9),vt=Ot[j],Ct=vt.high,Tt=vt.low,Et=H+mt+((ut=V+yt)>>>0>>0?1:0),St=gt+ft;H=P,V=J,P=N,J=G,N=D,G=L,D=B+(Et=(Et=(Et=Et+ht+((ut+=lt)>>>0>>0?1:0))+Ct+((ut+=Tt)>>>0>>0?1:0))+Q+((ut+=K)>>>0>>0?1:0))+((L=x+ut|0)>>>0>>0?1:0)|0,B=U,x=R,U=M,R=k,M=b,k=w,b=Et+(dt+pt+(St>>>0>>0?1:0))+((w=ut+St|0)>>>0>>0?1:0)|0}p=i.low=p+w,i.high=l+b+(p>>>0>>0?1:0),d=r.low=d+k,r.high=f+M+(d>>>0>>0?1:0),m=o.low=m+R,o.high=g+U+(m>>>0>>0?1:0),v=s.low=v+x,s.high=y+B+(v>>>0>>0?1:0),T=a.low=T+L,a.high=C+D+(T>>>0>>0?1:0),S=c.low=S+G,c.high=E+N+(S>>>0>>0?1:0),_=u.low=_+J,u.high=O+P+(_>>>0>>0?1:0),I=h.low=I+V,h.high=A+H+(I>>>0>>0?1:0)},_doFinalize:function(){var t=this._data,e=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[30+(i+128>>>10<<5)]=Math.floor(n/4294967296),e[31+(i+128>>>10<<5)]=n,t.sigBytes=4*e.length,this._process(),this._hash.toX32()},clone:function(){var t=e.clone.call(this);return t._hash=this._hash.clone(),t},blockSize:32});t.SHA512=e._createHelper(a),t.HmacSHA512=e._createHmacHelper(a)}(),c.SHA512)},function(t,n,e){"use strict";function i(t){var e="";switch(t){case n.MSG_CODE.SUCC:e="success";break;case n.MSG_CODE.FAILED:e="unknown error";break;case n.MSG_CODE.INIT.INITING:e="initing , please try again later";break;case n.MSG_CODE.INIT.APPKEY_EMPTY:e="empty appkey";break;case n.MSG_CODE.INIT.INIT_FAILED:e="init failed";break;case n.MSG_CODE.INIT.INIT_TIME_OUT:e="init timeout";break;case n.MSG_CODE.INIT.NO_INTERNET:e="no internet";break;case n.MSG_CODE.INIT.DOMAIN_NAME_EMPTY:e="domain name empty";break;case n.MSG_CODE.TOKEN.NOT_INIT:e="please call init correctly first";break;case n.MSG_CODE.TOKEN.GET_TOKEN_FAILED:e="getToken faild";break;case n.MSG_CODE.TOKEN.REQUESTING:e="token requesting, please try again later";break;case n.MSG_CODE.TOKEN.NOT_SUPPORT:e="not support";break;case n.MSG_CODE.TOKEN.ERROR_OPERATER:e="operater has to be one of CM,CT,CU2";break;case n.MSG_CODE.USEREVENT.CHECKOUT_CLOCE_PAGE:e="Close Page";break;case n.MSG_CODE.USEREVENT.CHECKOUT_SELECT:e="CheckBox Select";break;case n.MSG_CODE.USEREVENT.CHECKOUT_UNSELECT:e="CheckBox unSelect";break;case n.MSG_CODE.USEREVENT.CHECKOUT_INPUT_START:e="Start typing";break;case n.MSG_CODE.USEREVENT.CHECKOUT_INPUT_END:e="End typing";break;case n.MSG_CODE.USEREVENT.CHECKOUT_LOGIN:e="Start Auth";break;case n.MSG_CODE.USEREVENT.CHECKOUT_OTHER_LOGIN:e="Other Login";break;case n.MSG_CODE.USEREVENT.CHECKOUT_LOGINAUTHPAGE_SHOW:e="Login Auth Page Show";break;case n.MSG_CODE.TOKEN.TIME_OUT:e="Login Auth Time Out"}return e}n.__esModule=!0,n.MSG_CODE={SUCC:0,FAILED:1e3,INIT:{INITING:1001,APPKEY_EMPTY:1002,INIT_FAILED:1003,INIT_TIME_OUT:1004,NO_INTERNET:1005,DOMAIN_NAME_EMPTY:1006},TOKEN:{NOT_INIT:2001,GET_TOKEN_FAILED:2002,REQUESTING:2003,NOT_SUPPORT:2004,ERROR_OPERATER:2005,TIME_OUT:2006},USEREVENT:{CHECKOUT_CLOCE_PAGE:3001,CHECKOUT_SELECT:3002,CHECKOUT_UNSELECT:3003,CHECKOUT_INPUT_START:3004,CHECKOUT_INPUT_END:3005,CHECKOUT_LOGIN:3006,CHECKOUT_OTHER_LOGIN:3007,CHECKOUT_LOGINAUTHPAGE_SHOW:3008}},n.buildCallbakMsg=function(t,e){return{code:t,message:i(t),content:e}},n.buildfEventCallbakMsg=function(t,e){return{code:t,message:i(t),content:e}},n.buildOperaterCallbakMsg=function(t,e,n){return{code:t,operater:e,message:i(t),content:n}}},function(t,e,n){"use strict";e.__esModule=!0;var c=n(7),i=n(3),u=n(2);n(19);var o=n(22),s=n(17),r=n(51),a=n(8),h="JVerificationInterface";var l=new(function(){function r(){this.mInitStatu=r.INIT_FAIL}return r.prototype.init=function(t,e){if(c.Log.setDebugMode(t.debugMode),this.mInitStatu!=r.INIT_ING)if(i.isEmpty(t.appkey))e.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.APPKEY_EMPTY));else if(i.isEmpty(t.domainName))e.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.DOMAIN_NAME_EMPTY));else{if(c.Log.dd(h,"action:init - sdkVersion:"+u.JGCons.SDK_VERSION),this.isInitSuccess())return c.Log.dd(h,"already init success"),void e.onSuccess(s.buildCallbakMsg(s.MSG_CODE.SUCC));if(!i.isOnLine())return c.Log.ww(h,"init fail:no internet"),void e.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.NO_INTERNET));this.mInitStatu=r.INIT_ING,this._initConfig(t);try{this._init(e)}catch(t){c.Log.ww(h,"init fail:"+t),e.onFail(s.buildCallbakMsg(s.MSG_CODE.FAILED))}}else e.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.INITING))},r.prototype._initConfig=function(t){t.sdkVersion=u.JGCons.SDK_VERSION,this.mInitConfig=t,u.JGCons.HTML_URL=this.mInitConfig.domainName},r.prototype._init=function(n){var i=this;a.http({url:u.JGCons.REQUEST.URL_INIT,method:"GET",param:{appKey:this.mInitConfig.appkey,probIsp:!0},async:!0,header:!0}).then(function(t){var e=JSON.parse(t.result);i.mOperaterControl=new o.OperaterControl(i.mInitConfig.appkey),i.mOperaterControl.setAppId(u.JGCons.OPERATOR.CM,e.cmApId),i.mOperaterControl.setCMOpenType(e.cmOpenType),i.mOperaterControl.setCMAppKey(u.JGCons.OPERATOR.CM,e.cmAppkey),i.mOperaterControl.setAppId(u.JGCons.OPERATOR.CU,e.cu2ApId),i.mOperaterControl.setAppId(u.JGCons.OPERATOR.CT,e.ctApId),i.mOperaterControl.setFirstOprater(e.isp),i.mInitStatu=r.INIT_SUCC,c.Log.dd(h,"init success"),n.onSuccess(s.buildCallbakMsg(s.MSG_CODE.SUCC))}).catch(function(t){i.mInitStatu=r.INIT_FAIL,c.Log.ee(h,"init failed :"+JSON.stringify(t.code)+" "+JSON.stringify(t.result)),t.code==a.RequestController.REQUEST_TIME_OUT?n.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.INIT_TIME_OUT)):n.onFail(s.buildCallbakMsg(s.MSG_CODE.INIT.INIT_FAILED))})},r.prototype.isInitSuccess=function(){return this.mInitStatu==r.INIT_SUCC},r.prototype.getToken=function(t,e){if(this.isInitSuccess())try{this.mOperaterControl.getToken(function(t){c.Log.dd(h,"getToken succ:"+JSON.stringify(t)),e.onSuccess(t)},function(t){c.Log.ww(h,"getToken fail:"+JSON.stringify(t)),e.onFail(t)},t)}catch(t){c.Log.ww(h,"getToken fail:"+t),e.onFail(s.buildCallbakMsg(s.MSG_CODE.FAILED))}else e.onFail(s.buildCallbakMsg(s.MSG_CODE.TOKEN.NOT_INIT))},r.prototype.setCustomUIWithConfig=function(t){if(t){if(c.Log.dd(h,"setCustomUIWithConfig:"+JSON.stringify(t)),t.logo&&(u.JGCons.OPERATOR.CU_UI.CU_LOGIN_LOGO=t.logo,u.JGCons.CUSTOMUI.logo=t.logo,-1!=t.logo.indexOf("http")?u.JGCons.CUSTOMUI.iframeLogoPath=t.logo:(u.JGCons.CUSTOMUI.iframeLogo=t.logo,s=t.logo,(a=document.createElement("img")).src=s,a.crossOrigin="anonymous",a.onload=function(){var t,e,n=(t=a,(e=document.createElement("canvas")).width=t.width,e.height=t.height,e.getContext("2d").drawImage(t,0,0,t.width,t.height),e.toDataURL("image/png"));c.Log.dd(h,"loginAuth image:"+n.length),u.JGCons.CUSTOMUI.iframeLogo=n})),t.appName){var e=t.appName;6"+t+" type->"+e),u.JGCons.CUSTOMUI.authPageType="dialog"==e?1:0;var r=this;window.addEventListener("message",function(t){if(t.origin==u.JGCons.IFRAME_URL||t.origin==u.JGCons.HTML_URL){var e=t.data;"-1"==e?(r._closePage(),r.mOperaterControl.setOpraterState(!1),i.userEvent(s.buildfEventCallbakMsg(s.MSG_CODE.USEREVENT.CHECKOUT_CLOCE_PAGE))):"-2"==e&&r._closePage()}},!1);try{this.mOperaterControl.loginAuth(function(t){c.Log.dd(h,"loginAuth succ:"+JSON.stringify(t)),r._closePage(),i.onSuccess(t)},function(t){c.Log.ww(h,"loginAuth fail:"+JSON.stringify(t)),r._closePage(),i.onFail(t)},t,e,n)}catch(t){c.Log.ww(h,"loginAuth fail:"+t),r._closePage(),i.onFail(s.buildCallbakMsg(s.MSG_CODE.FAILED))}}else i.onFail(s.buildCallbakMsg(s.MSG_CODE.TOKEN.NOT_INIT))},r.prototype.checkVerifyEnable=function(){return!(i.isWifi()||i.isDesktop()||!i.isOnLine())},r.prototype.isCellular=function(){return"cellular"==this.mOperaterControl.getConnection()},r.prototype.isWifi=function(){return"wifi"==this.mOperaterControl.getConnection()},r.prototype._getCUSign=function(){return this.mOperaterControl.signCU()},r.prototype._getCULoginTokenFail=function(t){var e=new CustomEvent("jiguang_cu_login",{detail:{data:JSON.stringify(t),type:"fail"}});window.dispatchEvent(e)},r.prototype._getCULoginTokenSuccess=function(t){var e=new CustomEvent("jiguang_cu_login",{detail:{data:JSON.stringify(t),type:"succ"}});window.dispatchEvent(e)},r.prototype._authOperationMonitor=function(t){var e=new CustomEvent("jiguang_cu_even",{detail:{data:JSON.stringify(t),type:"even"}});window.dispatchEvent(e)},r.prototype.addMeta=function(){var t=document.getElementsByTagName("head"),e=document.createElement("meta");e.name=u.JGCons.META_NAME,e.content=u.JGCons.META_CONTENT,t[0].appendChild(e)},r.INIT_SUCC="INITSUCC",r.INIT_ING="INITING",r.INIT_FAIL="INITFAIL",r}());l.addMeta(),window.JVerificationInterface={init:function(t){l.init(t,new r.CallbackHelper(t))},isInitSuccess:function(){return l.isInitSuccess()},getToken:function(t){l.getToken(t.operater,new r.CallbackHelper(t))},checkVerifyEnable:function(){return l.checkVerifyEnable()},isCellular:function(){return l.isCellular()},isWifi:function(){return l.isWifi()},loginAuth:function(t){l.loginAuth(t.operater,t.type,t.timeout,new r.CallbackHelper(t))},addEventListen:function(t){l.addEventListen(new r.CallbackHelper(t))},setCustomUIWithConfig:function(t){l.setCustomUIWithConfig(t)},_getCUSign:function(){return l._getCUSign()},_getCULoginTokenSuccess:function(t){l._getCULoginTokenSuccess(t)},_getCULoginTokenFail:function(t){l._getCULoginTokenFail(t)},_authOperationMonitor:function(t){l._authOperationMonitor(t)}}},function(t,e,n){"use strict";t.exports=n(20).polyfill()},function(t,e,n){(function(H,V){ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version v4.2.6+9869a4bc + */ +t.exports=function(){"use strict";function u(t){return"function"==typeof t}var n=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},i=0,e=void 0,r=void 0,a=function(t,e){p[i]=t,p[i+1]=e,2===(i+=2)&&(r?r(f):v())},t="undefined"!=typeof window?window:void 0,o=t||{},s=o.MutationObserver||o.WebKitMutationObserver,c="undefined"==typeof self&&void 0!==H&&"[object process]"==={}.toString.call(H),h="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel;function l(){var t=setTimeout;return function(){return t(f,1)}}var p=new Array(1e3);function f(){for(var t=0;t=u.JGCons.OPERATOR.CM_CHECK_SRC_COUNT?(e("load YDRZ failed"),clearInterval(i)):l.log("try to load YDRZ times:"+n,o.getType())},u.JGCons.OPERATOR.CM_CHECK_SRC_TIME);else o._getToken(t,e);else e("is not support now")})},t.prototype._getNowTime=function(){var t=new Date,e=t.getFullYear(),n=9=u.JGCons.OPERATOR.CM_CHECK_SRC_COUNT?(e("load YDRZAuthLogin failed"),clearInterval(i)):l.log("try to load YDRZAuthLogin times:"+n,s.getType())},u.JGCons.OPERATOR.CM_CHECK_SRC_TIME);else s._loginAuth(t,e,r);else e("is not support now")})},t.prototype._loginAuth=function(o,s,t){var a=this,c=this;h.http({url:u.JGCons.REQUEST.URL_LOGIN_SIGN_CM,method:"GET",param:{appKey:this.mAppKey},async:!0,header:!0}).then(function(t){var e=JSON.parse(t.result),n="0";"0/1/2"==a.mCMOpenType&&(n=a.mCMOpenType);var i={version:"2.0",timestamp:e.timestamp,appId:c.mAppid,appkey:"0242CB62691821785A700E2A94B014FF",businessType:"8",openType:n,traceId:e.msgId,expandParams:"",isTest:""},r=i.appId+i.businessType+i.traceId+i.timestamp+i.traceId+i.version+i.appkey;p.MD5(r).toString();a.setCMUI(),YDRZAuthLogin.getTokenInfo({data:{version:i.version,appId:i.appId,sign:e.sign,traceId:i.traceId,timestamp:i.timestamp,openType:i.openType,expandParams:i.expandParams,isTest:i.isTest,authPageType:"2"},success:function(t){l.log("getTokenInfo result: "+JSON.stringify(t),c.getType()),"103000"===t.code?o(c.buildLoginMsg({token:t.token,user_information:t.userInformation,isNewCmToken:1})):s(t.code+"|"+t.message+"|"+t.msgId)},error:function(t){l.log("authGetToken getToken error: "+JSON.stringify(t),c.getType()),t.YDData?"501"==t.YDData.code?(window.parent.postMessage("-1",u.JGCons.HTML_URL),s("-1|"+JSON.stringify(t)+"|"+t.msgId)):s(t.YDData.code+"|"+JSON.stringify(t)+"|"+t.msgId):"501"==t.code?(window.parent.postMessage("-1",u.JGCons.HTML_URL),s("-1|"+t.message+"|"+t.msgId)):s(t.code+"|"+t.message+"|"+t.msgId)},layerCallback:function(t){l.log("layerCallback: "+JSON.stringify(t)),"103000"==t.code&&window.parent.postMessage("004",u.JGCons.HTML_URL)},clickListenerCallback:function(t){l.log("clickListenerCallback: "+JSON.stringify(t)),"2"==t.code&&(t.checked?window.parent.postMessage("001",u.JGCons.HTML_URL):window.parent.postMessage("002",u.JGCons.HTML_URL))},authLoadSuccess:function(t){l.log("authLoadSuccess: "+JSON.stringify(t)),"103000"==t.code&&(window.parent.postMessage("006",u.JGCons.HTML_URL),c.mPreloginCallback())}})}).catch(function(t){s("request sign failed")})},t.prototype.setCMUI=function(){var t=0<=u.JGCons.CUSTOMUI.logo.length&&"/image/logo.png"!=u.JGCons.CUSTOMUI.logo?u.JGCons.CUSTOMUI.logo:u.JGCons.IFRAME_URL+"/scripts/5.1.3/image/logo.png",e=0>>2]|=t[i]<<24-i%4*8;r.call(this,n,e)}else r.apply(this,arguments)}).prototype=t}}(),i.lib.WordArray)},function(t,e,n){var i;t.exports=(i=n(0),function(){var t=i,r=t.lib.WordArray,e=t.enc;function s(t){return t<<8&4278255360|t>>>8&16711935}e.Utf16=e.Utf16BE={stringify:function(t){for(var e=t.words,n=t.sigBytes,i=[],r=0;r>>2]>>>16-r%4*8&65535;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,n=[],i=0;i>>1]|=t.charCodeAt(i)<<16-i%2*16;return r.create(n,2*e)}},e.Utf16LE={stringify:function(t){for(var e=t.words,n=t.sigBytes,i=[],r=0;r>>2]>>>16-r%4*8&65535);i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,n=[],i=0;i>>1]|=s(t.charCodeAt(i)<<16-i%2*16);return r.create(n,2*e)}}}(),i.enc.Utf16)},function(t,e,n){var i,r,o,s,a,c;t.exports=(i=n(0),n(15),o=(r=i).lib.WordArray,s=r.algo,a=s.SHA256,c=s.SHA224=a.extend({_doReset:function(){this._hash=new o.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var t=a._doFinalize.call(this);return t.sigBytes-=4,t}}),r.SHA224=a._createHelper(c),r.HmacSHA224=a._createHmacHelper(c),i.SHA224)},function(t,e,n){var i,r,o,s,a,c,u,h;t.exports=(i=n(0),n(10),n(16),o=(r=i).x64,s=o.Word,a=o.WordArray,c=r.algo,u=c.SHA512,h=c.SHA384=u.extend({_doReset:function(){this._hash=new a.init([new s.init(3418070365,3238371032),new s.init(1654270250,914150663),new s.init(2438529370,812702999),new s.init(355462360,4144912697),new s.init(1731405415,4290775857),new s.init(2394180231,1750603025),new s.init(3675008525,1694076839),new s.init(1203062813,3204075428)])},_doFinalize:function(){var t=u._doFinalize.call(this);return t.sigBytes-=16,t}}),r.SHA384=u._createHelper(h),r.HmacSHA384=u._createHmacHelper(h),i.SHA384)},function(t,e,n){var o;t.exports=(o=n(0),n(10),function(p){var t=o,e=t.lib,f=e.WordArray,i=e.Hasher,h=t.x64.Word,n=t.algo,M=[],k=[],U=[];!function(){for(var t=1,e=0,n=0;n<24;n++){M[t+5*e]=(n+1)*(n+2)/2%64;var i=(2*t+3*e)%5;t=e%5,e=i}for(t=0;t<5;t++)for(e=0;e<5;e++)k[t+5*e]=e+(2*t+3*e)%5*5;for(var r=1,o=0;o<24;o++){for(var s=0,a=0,c=0;c<7;c++){if(1&r){var u=(1<>>24)|4278255360&(o<<24|o>>>8),s=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),(_=n[r]).high^=s,_.low^=o}for(var a=0;a<24;a++){for(var c=0;c<5;c++){for(var u=0,h=0,l=0;l<5;l++)u^=(_=n[c+5*l]).high,h^=_.low;var p=R[c];p.high=u,p.low=h}for(c=0;c<5;c++){var f=R[(c+4)%5],d=R[(c+1)%5],g=d.high,m=d.low;for(u=f.high^(g<<1|m>>>31),h=f.low^(m<<1|g>>>31),l=0;l<5;l++)(_=n[c+5*l]).high^=u,_.low^=h}for(var y=1;y<25;y++){var v=(_=n[y]).high,C=_.low,T=M[y];h=T<32?(u=v<>>32-T,C<>>32-T):(u=C<>>64-T,v<>>64-T);var E=R[k[y]];E.high=u,E.low=h}var S=R[0],O=n[0];for(S.high=O.high,S.low=O.low,c=0;c<5;c++)for(l=0;l<5;l++){var _=n[y=c+5*l],A=R[y],I=R[(c+1)%5+5*l],b=R[(c+2)%5+5*l];_.high=A.high^~I.high&b.high,_.low=A.low^~I.low&b.low}_=n[0];var w=U[a];_.high^=w.high,_.low^=w.low}},_doFinalize:function(){var t=this._data,e=t.words,n=(this._nDataBytes,8*t.sigBytes),i=32*this.blockSize;e[n>>>5]|=1<<24-n%32,e[(p.ceil((n+1)/i)*i>>>5)-1]|=128,t.sigBytes=4*e.length,this._process();for(var r=this._state,o=this.cfg.outputLength/8,s=o/8,a=[],c=0;c>>24)|4278255360&(h<<24|h>>>8),l=16711935&(l<<8|l>>>24)|4278255360&(l<<24|l>>>8),a.push(l),a.push(h)}return new f.init(a,o)},clone:function(){for(var t=i.clone.call(this),e=t._state=this._state.slice(0),n=0;n<25;n++)e[n]=e[n].clone();return t}});t.SHA3=i._createHelper(r),t.HmacSHA3=i._createHmacHelper(r)}(Math),o.SHA3)},function(t,e,n){var a;t.exports=(a=n(0), +/** @preserve + (c) 2012 by Cédric Mesnil. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +function(t){var e=a,n=e.lib,i=n.WordArray,r=n.Hasher,o=e.algo,O=i.create([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]),_=i.create([5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]),A=i.create([11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]),I=i.create([8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]),b=i.create([0,1518500249,1859775393,2400959708,2840853838]),w=i.create([1352829926,1548603684,1836072691,2053994217,0]),s=o.RIPEMD160=r.extend({_doReset:function(){this._hash=i.create([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,e){for(var n=0;n<16;n++){var i=e+n,r=t[i];t[i]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8)}var o,s,a,c,u,h,l,p,f,d,g,m=this._hash.words,y=b.words,v=w.words,C=O.words,T=_.words,E=A.words,S=I.words;for(h=o=m[0],l=s=m[1],p=a=m[2],f=c=m[3],d=u=m[4],n=0;n<80;n+=1)g=o+t[e+C[n]]|0,g+=n<16?M(s,a,c)+y[0]:n<32?k(s,a,c)+y[1]:n<48?U(s,a,c)+y[2]:n<64?R(s,a,c)+y[3]:B(s,a,c)+y[4],g=(g=x(g|=0,E[n]))+u|0,o=u,u=c,c=x(a,10),a=s,s=g,g=h+t[e+T[n]]|0,g+=n<16?B(l,p,f)+v[0]:n<32?R(l,p,f)+v[1]:n<48?U(l,p,f)+v[2]:n<64?k(l,p,f)+v[3]:M(l,p,f)+v[4],g=(g=x(g|=0,S[n]))+d|0,h=d,d=f,f=x(p,10),p=l,l=g;g=m[1]+a+f|0,m[1]=m[2]+c+d|0,m[2]=m[3]+u+h|0,m[3]=m[4]+o+l|0,m[4]=m[0]+s+p|0,m[0]=g},_doFinalize:function(){var t=this._data,e=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;e[i>>>5]|=128<<24-i%32,e[14+(i+64>>>9<<4)]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),t.sigBytes=4*(e.length+1),this._process();for(var r=this._hash,o=r.words,s=0;s<5;s++){var a=o[s];o[s]=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8)}return r},clone:function(){var t=r.clone.call(this);return t._hash=this._hash.clone(),t}});function M(t,e,n){return t^e^n}function k(t,e,n){return t&e|~t&n}function U(t,e,n){return(t|~e)^n}function R(t,e,n){return t&n|e&~n}function B(t,e,n){return t^(e|~n)}function x(t,e){return t<>>32-e}e.RIPEMD160=r._createHelper(s),e.HmacRIPEMD160=r._createHmacHelper(s)}(Math),a.RIPEMD160)},function(t,e,n){var i,r,o,s,y,a,c,v,u;t.exports=(i=n(0),n(12),n(13),o=(r=i).lib,s=o.Base,y=o.WordArray,a=r.algo,c=a.SHA1,v=a.HMAC,u=a.PBKDF2=s.extend({cfg:s.extend({keySize:4,hasher:c,iterations:1}),init:function(t){this.cfg=this.cfg.extend(t)},compute:function(t,e){for(var n=this.cfg,i=v.create(n.hasher,t),r=y.create(),o=y.create([1]),s=r.words,a=o.words,c=n.keySize,u=n.iterations;s.length>24&255)){var e=t>>16&255,n=t>>8&255,i=255&t;255===e?(e=0,255===n?(n=0,255===i?i=0:++i):++n):++e,t=0,t+=e<<16,t+=n<<8,t+=i}else t+=1<<24;return t}var e=t.Encryptor=t.extend({processBlock:function(t,e){var n,i=this._cipher,r=i.blockSize,o=this._iv,s=this._counter;o&&(s=this._counter=o.slice(0),this._iv=void 0),0===((n=s)[0]=u(n[0]))&&(n[1]=u(n[1]));var a=s.slice(0);i.encryptBlock(a,0);for(var c=0;c>>2]|=r<<24-o%4*8,t.sigBytes+=r},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},i.pad.Ansix923)},function(t,e,n){var r;t.exports=(r=n(0),n(1),r.pad.Iso10126={pad:function(t,e){var n=4*e,i=n-t.sigBytes%n;t.concat(r.lib.WordArray.random(i-1)).concat(r.lib.WordArray.create([i<<24],1))},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},r.pad.Iso10126)},function(t,e,n){var i;t.exports=(i=n(0),n(1),i.pad.Iso97971={pad:function(t,e){t.concat(i.lib.WordArray.create([2147483648],1)),i.pad.ZeroPadding.pad(t,e)},unpad:function(t){i.pad.ZeroPadding.unpad(t),t.sigBytes--}},i.pad.Iso97971)},function(t,e,n){var i;t.exports=(i=n(0),n(1),i.pad.ZeroPadding={pad:function(t,e){var n=4*e;t.clamp(),t.sigBytes+=n-(t.sigBytes%n||n)},unpad:function(t){for(var e=t.words,n=t.sigBytes-1;!(e[n>>>2]>>>24-n%4*8&255);)n--;t.sigBytes=n+1}},i.pad.ZeroPadding)},function(t,e,n){var i;t.exports=(i=n(0),n(1),i.pad.NoPadding={pad:function(){},unpad:function(){}},i.pad.NoPadding)},function(t,e,n){var i,r,o,s;t.exports=(i=n(0),n(1),o=(r=i).lib.CipherParams,s=r.enc.Hex,r.format.Hex={stringify:function(t){return t.ciphertext.toString(s)},parse:function(t){var e=s.parse(t);return o.create({ciphertext:e})}},i.format.Hex)},function(t,e,n){var r;t.exports=(r=n(0),n(5),n(6),n(4),n(1),function(){var t=r,e=t.lib.BlockCipher,n=t.algo,u=[],h=[],l=[],p=[],f=[],d=[],g=[],m=[],y=[],v=[];!function(){for(var t=[],e=0;e<256;e++)t[e]=e<128?e<<1:e<<1^283;var n=0,i=0;for(e=0;e<256;e++){var r=i^i<<1^i<<2^i<<3^i<<4;r=r>>>8^255&r^99,u[n]=r;var o=t[h[r]=n],s=t[o],a=t[s],c=257*t[r]^16843008*r;l[n]=c<<24|c>>>8,p[n]=c<<16|c>>>16,f[n]=c<<8|c>>>24,d[n]=c,c=16843009*a^65537*s^257*o^16843008*n,g[r]=c<<24|c>>>8,m[r]=c<<16|c>>>16,y[r]=c<<8|c>>>24,v[r]=c,n?(n=o^t[t[t[a^o]]],i^=t[t[i]]):n=i=1}}();var C=[0,1,2,4,8,16,32,64,128,27,54],i=n.AES=e.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var t=this._keyPriorReset=this._key,e=t.words,n=t.sigBytes/4,i=4*((this._nRounds=n+6)+1),r=this._keySchedule=[],o=0;o>>24]<<24|u[s>>>16&255]<<16|u[s>>>8&255]<<8|u[255&s]):(s=u[(s=s<<8|s>>>24)>>>24]<<24|u[s>>>16&255]<<16|u[s>>>8&255]<<8|u[255&s],s^=C[o/n|0]<<24),r[o]=r[o-n]^s}for(var a=this._invKeySchedule=[],c=0;c>>24]]^m[u[s>>>16&255]]^y[u[s>>>8&255]]^v[u[255&s]]}},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._keySchedule,l,p,f,d,u)},decryptBlock:function(t,e){var n=t[e+1];t[e+1]=t[e+3],t[e+3]=n,this._doCryptBlock(t,e,this._invKeySchedule,g,m,y,v,h),n=t[e+1],t[e+1]=t[e+3],t[e+3]=n},_doCryptBlock:function(t,e,n,i,r,o,s,a){for(var c=this._nRounds,u=t[e]^n[0],h=t[e+1]^n[1],l=t[e+2]^n[2],p=t[e+3]^n[3],f=4,d=1;d>>24]^r[h>>>16&255]^o[l>>>8&255]^s[255&p]^n[f++],m=i[h>>>24]^r[l>>>16&255]^o[p>>>8&255]^s[255&u]^n[f++],y=i[l>>>24]^r[p>>>16&255]^o[u>>>8&255]^s[255&h]^n[f++],v=i[p>>>24]^r[u>>>16&255]^o[h>>>8&255]^s[255&l]^n[f++];u=g,h=m,l=y,p=v}g=(a[u>>>24]<<24|a[h>>>16&255]<<16|a[l>>>8&255]<<8|a[255&p])^n[f++],m=(a[h>>>24]<<24|a[l>>>16&255]<<16|a[p>>>8&255]<<8|a[255&u])^n[f++],y=(a[l>>>24]<<24|a[p>>>16&255]<<16|a[u>>>8&255]<<8|a[255&h])^n[f++],v=(a[p>>>24]<<24|a[u>>>16&255]<<16|a[h>>>8&255]<<8|a[255&l])^n[f++],t[e]=g,t[e+1]=m,t[e+2]=y,t[e+3]=v},keySize:8});t.AES=e._createHelper(i)}(),r.AES)},function(t,e,n){var a;t.exports=(a=n(0),n(5),n(6),n(4),n(1),function(){var t=a,e=t.lib,n=e.WordArray,i=e.BlockCipher,r=t.algo,u=[57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4],h=[14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32],l=[1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28],p=[{0:8421888,268435456:32768,536870912:8421378,805306368:2,1073741824:512,1342177280:8421890,1610612736:8389122,1879048192:8388608,2147483648:514,2415919104:8389120,2684354560:33280,2952790016:8421376,3221225472:32770,3489660928:8388610,3758096384:0,4026531840:33282,134217728:0,402653184:8421890,671088640:33282,939524096:32768,1207959552:8421888,1476395008:512,1744830464:8421378,2013265920:2,2281701376:8389120,2550136832:33280,2818572288:8421376,3087007744:8389122,3355443200:8388610,3623878656:32770,3892314112:514,4160749568:8388608,1:32768,268435457:2,536870913:8421888,805306369:8388608,1073741825:8421378,1342177281:33280,1610612737:512,1879048193:8389122,2147483649:8421890,2415919105:8421376,2684354561:8388610,2952790017:33282,3221225473:514,3489660929:8389120,3758096385:32770,4026531841:0,134217729:8421890,402653185:8421376,671088641:8388608,939524097:512,1207959553:32768,1476395009:8388610,1744830465:2,2013265921:33282,2281701377:32770,2550136833:8389122,2818572289:514,3087007745:8421888,3355443201:8389120,3623878657:0,3892314113:33280,4160749569:8421378},{0:1074282512,16777216:16384,33554432:524288,50331648:1074266128,67108864:1073741840,83886080:1074282496,100663296:1073758208,117440512:16,134217728:540672,150994944:1073758224,167772160:1073741824,184549376:540688,201326592:524304,218103808:0,234881024:16400,251658240:1074266112,8388608:1073758208,25165824:540688,41943040:16,58720256:1073758224,75497472:1074282512,92274688:1073741824,109051904:524288,125829120:1074266128,142606336:524304,159383552:0,176160768:16384,192937984:1074266112,209715200:1073741840,226492416:540672,243269632:1074282496,260046848:16400,268435456:0,285212672:1074266128,301989888:1073758224,318767104:1074282496,335544320:1074266112,352321536:16,369098752:540688,385875968:16384,402653184:16400,419430400:524288,436207616:524304,452984832:1073741840,469762048:540672,486539264:1073758208,503316480:1073741824,520093696:1074282512,276824064:540688,293601280:524288,310378496:1074266112,327155712:16384,343932928:1073758208,360710144:1074282512,377487360:16,394264576:1073741824,411041792:1074282496,427819008:1073741840,444596224:1073758224,461373440:524304,478150656:0,494927872:16400,511705088:1074266128,528482304:540672},{0:260,1048576:0,2097152:67109120,3145728:65796,4194304:65540,5242880:67108868,6291456:67174660,7340032:67174400,8388608:67108864,9437184:67174656,10485760:65792,11534336:67174404,12582912:67109124,13631488:65536,14680064:4,15728640:256,524288:67174656,1572864:67174404,2621440:0,3670016:67109120,4718592:67108868,5767168:65536,6815744:65540,7864320:260,8912896:4,9961472:256,11010048:67174400,12058624:65796,13107200:65792,14155776:67109124,15204352:67174660,16252928:67108864,16777216:67174656,17825792:65540,18874368:65536,19922944:67109120,20971520:256,22020096:67174660,23068672:67108868,24117248:0,25165824:67109124,26214400:67108864,27262976:4,28311552:65792,29360128:67174400,30408704:260,31457280:65796,32505856:67174404,17301504:67108864,18350080:260,19398656:67174656,20447232:0,21495808:65540,22544384:67109120,23592960:256,24641536:67174404,25690112:65536,26738688:67174660,27787264:65796,28835840:67108868,29884416:67109124,30932992:67174400,31981568:4,33030144:65792},{0:2151682048,65536:2147487808,131072:4198464,196608:2151677952,262144:0,327680:4198400,393216:2147483712,458752:4194368,524288:2147483648,589824:4194304,655360:64,720896:2147487744,786432:2151678016,851968:4160,917504:4096,983040:2151682112,32768:2147487808,98304:64,163840:2151678016,229376:2147487744,294912:4198400,360448:2151682112,425984:0,491520:2151677952,557056:4096,622592:2151682048,688128:4194304,753664:4160,819200:2147483648,884736:4194368,950272:4198464,1015808:2147483712,1048576:4194368,1114112:4198400,1179648:2147483712,1245184:0,1310720:4160,1376256:2151678016,1441792:2151682048,1507328:2147487808,1572864:2151682112,1638400:2147483648,1703936:2151677952,1769472:4198464,1835008:2147487744,1900544:4194304,1966080:64,2031616:4096,1081344:2151677952,1146880:2151682112,1212416:0,1277952:4198400,1343488:4194368,1409024:2147483648,1474560:2147487808,1540096:64,1605632:2147483712,1671168:4096,1736704:2147487744,1802240:2151678016,1867776:4160,1933312:2151682048,1998848:4194304,2064384:4198464},{0:128,4096:17039360,8192:262144,12288:536870912,16384:537133184,20480:16777344,24576:553648256,28672:262272,32768:16777216,36864:537133056,40960:536871040,45056:553910400,49152:553910272,53248:0,57344:17039488,61440:553648128,2048:17039488,6144:553648256,10240:128,14336:17039360,18432:262144,22528:537133184,26624:553910272,30720:536870912,34816:537133056,38912:0,43008:553910400,47104:16777344,51200:536871040,55296:553648128,59392:16777216,63488:262272,65536:262144,69632:128,73728:536870912,77824:553648256,81920:16777344,86016:553910272,90112:537133184,94208:16777216,98304:553910400,102400:553648128,106496:17039360,110592:537133056,114688:262272,118784:536871040,122880:0,126976:17039488,67584:553648256,71680:16777216,75776:17039360,79872:537133184,83968:536870912,88064:17039488,92160:128,96256:553910272,100352:262272,104448:553910400,108544:0,112640:553648128,116736:16777344,120832:262144,124928:537133056,129024:536871040},{0:268435464,256:8192,512:270532608,768:270540808,1024:268443648,1280:2097152,1536:2097160,1792:268435456,2048:0,2304:268443656,2560:2105344,2816:8,3072:270532616,3328:2105352,3584:8200,3840:270540800,128:270532608,384:270540808,640:8,896:2097152,1152:2105352,1408:268435464,1664:268443648,1920:8200,2176:2097160,2432:8192,2688:268443656,2944:270532616,3200:0,3456:270540800,3712:2105344,3968:268435456,4096:268443648,4352:270532616,4608:270540808,4864:8200,5120:2097152,5376:268435456,5632:268435464,5888:2105344,6144:2105352,6400:0,6656:8,6912:270532608,7168:8192,7424:268443656,7680:270540800,7936:2097160,4224:8,4480:2105344,4736:2097152,4992:268435464,5248:268443648,5504:8200,5760:270540808,6016:270532608,6272:270540800,6528:270532616,6784:8192,7040:2105352,7296:2097160,7552:0,7808:268435456,8064:268443656},{0:1048576,16:33555457,32:1024,48:1049601,64:34604033,80:0,96:1,112:34603009,128:33555456,144:1048577,160:33554433,176:34604032,192:34603008,208:1025,224:1049600,240:33554432,8:34603009,24:0,40:33555457,56:34604032,72:1048576,88:33554433,104:33554432,120:1025,136:1049601,152:33555456,168:34603008,184:1048577,200:1024,216:34604033,232:1,248:1049600,256:33554432,272:1048576,288:33555457,304:34603009,320:1048577,336:33555456,352:34604032,368:1049601,384:1025,400:34604033,416:1049600,432:1,448:0,464:34603008,480:33554433,496:1024,264:1049600,280:33555457,296:34603009,312:1,328:33554432,344:1048576,360:1025,376:34604032,392:33554433,408:34603008,424:0,440:34604033,456:1049601,472:1024,488:33555456,504:1048577},{0:134219808,1:131072,2:134217728,3:32,4:131104,5:134350880,6:134350848,7:2048,8:134348800,9:134219776,10:133120,11:134348832,12:2080,13:0,14:134217760,15:133152,2147483648:2048,2147483649:134350880,2147483650:134219808,2147483651:134217728,2147483652:134348800,2147483653:133120,2147483654:133152,2147483655:32,2147483656:134217760,2147483657:2080,2147483658:131104,2147483659:134350848,2147483660:0,2147483661:134348832,2147483662:134219776,2147483663:131072,16:133152,17:134350848,18:32,19:2048,20:134219776,21:134217760,22:134348832,23:131072,24:0,25:131104,26:134348800,27:134219808,28:134350880,29:133120,30:2080,31:134217728,2147483664:131072,2147483665:2048,2147483666:134348832,2147483667:133152,2147483668:32,2147483669:134348800,2147483670:134217728,2147483671:134219808,2147483672:134350880,2147483673:134217760,2147483674:134219776,2147483675:0,2147483676:133120,2147483677:2080,2147483678:131104,2147483679:134350848}],f=[4160749569,528482304,33030144,2064384,129024,8064,504,2147483679],o=r.DES=i.extend({_doReset:function(){for(var t=this._key.words,e=[],n=0;n<56;n++){var i=u[n]-1;e[n]=t[i>>>5]>>>31-i%32&1}for(var r=this._subKeys=[],o=0;o<16;o++){var s=r[o]=[],a=l[o];for(n=0;n<24;n++)s[n/6|0]|=e[(h[n]-1+a)%28]<<31-n%6,s[4+(n/6|0)]|=e[28+(h[n+24]-1+a)%28]<<31-n%6;for(s[0]=s[0]<<1|s[0]>>>31,n=1;n<7;n++)s[n]=s[n]>>>4*(n-1)+3;s[7]=s[7]<<5|s[7]>>>27}var c=this._invSubKeys=[];for(n=0;n<16;n++)c[n]=r[15-n]},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._subKeys)},decryptBlock:function(t,e){this._doCryptBlock(t,e,this._invSubKeys)},_doCryptBlock:function(t,e,n){this._lBlock=t[e],this._rBlock=t[e+1],d.call(this,4,252645135),d.call(this,16,65535),g.call(this,2,858993459),g.call(this,8,16711935),d.call(this,1,1431655765);for(var i=0;i<16;i++){for(var r=n[i],o=this._lBlock,s=this._rBlock,a=0,c=0;c<8;c++)a|=p[c][((s^r[c])&f[c])>>>0];this._lBlock=s,this._rBlock=o^a}var u=this._lBlock;this._lBlock=this._rBlock,this._rBlock=u,d.call(this,1,1431655765),g.call(this,8,16711935),g.call(this,2,858993459),d.call(this,16,65535),d.call(this,4,252645135),t[e]=this._lBlock,t[e+1]=this._rBlock},keySize:2,ivSize:2,blockSize:2});function d(t,e){var n=(this._lBlock>>>t^this._rBlock)&e;this._rBlock^=n,this._lBlock^=n<>>t^this._lBlock)&e;this._lBlock^=n,this._rBlock^=n<>>2]>>>24-s%4*8&255;o=(o+i[r]+a)%256;var c=i[r];i[r]=i[o],i[o]=c}this._i=this._j=0},_doProcessBlock:function(t,e){t[e]^=r.call(this)},keySize:8,ivSize:0});function r(){for(var t=this._S,e=this._i,n=this._j,i=0,r=0;r<4;r++){n=(n+t[e=(e+1)%256])%256;var o=t[e];t[e]=t[n],t[n]=o,i|=t[(t[e]+t[n])%256]<<24-8*r}return this._i=e,this._j=n,i}t.RC4=e._createHelper(i);var o=n.RC4Drop=i.extend({cfg:i.cfg.extend({drop:192}),_doReset:function(){i._doReset.call(this);for(var t=this.cfg.drop;0>>24)|4278255360&(t[n]<<24|t[n]>>>8);var i=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],r=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]];for(n=this._b=0;n<4;n++)p.call(this);for(n=0;n<8;n++)r[n]^=i[n+4&7];if(e){var o=e.words,s=o[0],a=o[1],c=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),u=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),h=c>>>16|4294901760&u,l=u<<16|65535&c;for(r[0]^=c,r[1]^=h,r[2]^=u,r[3]^=l,r[4]^=c,r[5]^=h,r[6]^=u,r[7]^=l,n=0;n<4;n++)p.call(this)}},_doProcessBlock:function(t,e){var n=this._X;p.call(this),r[0]=n[0]^n[5]>>>16^n[3]<<16,r[1]=n[2]^n[7]>>>16^n[5]<<16,r[2]=n[4]^n[1]>>>16^n[7]<<16,r[3]=n[6]^n[3]>>>16^n[1]<<16;for(var i=0;i<4;i++)r[i]=16711935&(r[i]<<8|r[i]>>>24)|4278255360&(r[i]<<24|r[i]>>>8),t[e+i]^=r[i]},blockSize:4,ivSize:2});function p(){for(var t=this._X,e=this._C,n=0;n<8;n++)c[n]=e[n];for(e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0,n=0;n<8;n++){var i=t[n]+e[n],r=65535&i,o=i>>>16,s=((r*r>>>17)+r*o>>>15)+o*o,a=((4294901760&i)*i|0)+((65535&i)*i|0);u[n]=s^a}t[0]=u[0]+(u[7]<<16|u[7]>>>16)+(u[6]<<16|u[6]>>>16)|0,t[1]=u[1]+(u[0]<<8|u[0]>>>24)+u[7]|0,t[2]=u[2]+(u[1]<<16|u[1]>>>16)+(u[0]<<16|u[0]>>>16)|0,t[3]=u[3]+(u[2]<<8|u[2]>>>24)+u[1]|0,t[4]=u[4]+(u[3]<<16|u[3]>>>16)+(u[2]<<16|u[2]>>>16)|0,t[5]=u[5]+(u[4]<<8|u[4]>>>24)+u[3]|0,t[6]=u[6]+(u[5]<<16|u[5]>>>16)+(u[4]<<16|u[4]>>>16)|0,t[7]=u[7]+(u[6]<<8|u[6]>>>24)+u[5]|0}t.Rabbit=e._createHelper(i)}(),o.Rabbit)},function(t,e,n){var o;t.exports=(o=n(0),n(5),n(6),n(4),n(1),function(){var t=o,e=t.lib.StreamCipher,n=t.algo,r=[],c=[],u=[],i=n.RabbitLegacy=e.extend({_doReset:function(){for(var t=this._key.words,e=this.cfg.iv,n=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],i=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]],r=this._b=0;r<4;r++)p.call(this);for(r=0;r<8;r++)i[r]^=n[r+4&7];if(e){var o=e.words,s=o[0],a=o[1],c=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),u=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8),h=c>>>16|4294901760&u,l=u<<16|65535&c;for(i[0]^=c,i[1]^=h,i[2]^=u,i[3]^=l,i[4]^=c,i[5]^=h,i[6]^=u,i[7]^=l,r=0;r<4;r++)p.call(this)}},_doProcessBlock:function(t,e){var n=this._X;p.call(this),r[0]=n[0]^n[5]>>>16^n[3]<<16,r[1]=n[2]^n[7]>>>16^n[5]<<16,r[2]=n[4]^n[1]>>>16^n[7]<<16,r[3]=n[6]^n[3]>>>16^n[1]<<16;for(var i=0;i<4;i++)r[i]=16711935&(r[i]<<8|r[i]>>>24)|4278255360&(r[i]<<24|r[i]>>>8),t[e+i]^=r[i]},blockSize:4,ivSize:2});function p(){for(var t=this._X,e=this._C,n=0;n<8;n++)c[n]=e[n];for(e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0,n=0;n<8;n++){var i=t[n]+e[n],r=65535&i,o=i>>>16,s=((r*r>>>17)+r*o>>>15)+o*o,a=((4294901760&i)*i|0)+((65535&i)*i|0);u[n]=s^a}t[0]=u[0]+(u[7]<<16|u[7]>>>16)+(u[6]<<16|u[6]>>>16)|0,t[1]=u[1]+(u[0]<<8|u[0]>>>24)+u[7]|0,t[2]=u[2]+(u[1]<<16|u[1]>>>16)+(u[0]<<16|u[0]>>>16)|0,t[3]=u[3]+(u[2]<<8|u[2]>>>24)+u[1]|0,t[4]=u[4]+(u[3]<<16|u[3]>>>16)+(u[2]<<16|u[2]>>>16)|0,t[5]=u[5]+(u[4]<<8|u[4]>>>24)+u[3]|0,t[6]=u[6]+(u[5]<<16|u[5]>>>16)+(u[4]<<16|u[4]>>>16)|0,t[7]=u[7]+(u[6]<<8|u[6]>>>24)+u[5]|0}t.RabbitLegacy=e._createHelper(i)}(),o.RabbitLegacy)},function(t,e,n){"use strict";var i,r=this&&this.__extends||(i=function(t,e){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,e)},function(t,e){function n(){this.constructor=t}i(t,e),t.prototype=null===e?Object.create(e):(n.prototype=e.prototype,new n)});e.__esModule=!0;var a=n(2),o=n(11),c=n(3),s=n(8),u=n(48),h=function(e){function t(t){return e.call(this,a.JGCons.OPERATOR.CU,a.JGCons.OPERATOR.CU_SUPPORT,t)||this}return r(t,e),t.prototype.init=function(){if(this.isSupport()&&!this.isInit()){c.log("init....",this.getType()),this.setInitStatu(o.Operater.INIT_ING);var t=document.createElement("span");t.id="b",document.body.appendChild(t),window.signType="A",c.addScript(a.JGCons.IFRAME_URL+"/scripts/5.1.6/sdk/cu2/1.0.0/jquery.js"),c.addScript(a.JGCons.IFRAME_URL+"/scripts/5.1.6/sdk/cu2/1.0.0/fingerprint2.min.1.5.1.js"),c.addScript(a.JGCons.IFRAME_URL+"/scripts/5.1.6/sdk/cu2/1.0.0/nis-h5-sdk-1.2-timeout-version.js"),c.addScript(a.JGCons.IFRAME_URL+"/scripts/5.1.6/sdk/cu2/1.0.0/ispAuthPrefetch.js"),c.addLink(a.JGCons.IFRAME_URL+"/scripts/5.1.5/css/pop.css"),window.addEventListener("message",this.receiveMessage,!1),this.setInitStatu(o.Operater.INIT_SUCC)}},t.prototype.receiveMessage=function(t){"010"==t.data&&document.getElementById("cmiframe").contentWindow.postMessage(a.JGCons.CUSTOMUI.iframeLogo,a.JGCons.IFRAME_URL)},t.prototype.getToken=function(){var r=this,o=this;return new Promise(function(t,e){if(o.isSupport())if(c.isEmpty(r.mAppid))e("CU appid is empty");else if("undefined"==typeof NisSDK)var n=0,i=setInterval(function(){n++,"undefined"!=typeof NisSDK?(o._getToken(t,e),clearInterval(i)):n>=a.JGCons.OPERATOR.CU_CHECK_SRC_COUNT?(e("load CUAU failed"),clearInterval(i)):c.log("try to load CUAU times:"+n,o.getType())},a.JGCons.OPERATOR.CU_CHECK_SRC_TIME);else o._getToken(t,e);else e("is not support now")})},t.prototype._getToken=function(e,n){var i=this,t=(new Date).getTime(),r=this.sign(t);c.log("cu sign :"+r),LTRZ.getTokenInfo({appKey:this.mAppid,authenticator:r,ts:t}).then(function(t){c.log("getToken result :"+JSON.stringify(t),i.getType()),void 0!==t.accessCode?t.operator!=a.JGCons.OPERATOR.CU?n("current operater is not CU"):e(i.buildTokenMsg({token:t.accessCode})):(c.log("cu getToken error code:"+t.code+" msg:"+t.msg,i.getType()),-2==t.code?n("-17|"+t.msg):n(t.code+"|"+t.msg+"|"+t.msgid))}).catch(function(t){c.log("cu getToken error code:"+t.code+" msg:"+t.msg,i.getType()),-2==t.code?n("-17|"+t.msg):n(t.code+"|"+t.msg)})},t.prototype.sign=function(t){var e=null!=t?t:(new Date).getTime(),n=s.http({url:a.JGCons.REQUEST.URL_SIGN_CU,method:"GET",param:{appKey:this.mAppKey,ts:e},async:!1,header:!0});c.log("cu getSign result :"+JSON.stringify(n),this.getType());var i="";return 200==n.statusCode&&(i=JSON.parse(n.result).sign),i},t.prototype.loginAuth=function(i){var r=this,o=this;return c.log("cu2 login auth",o.getType()),new Promise(function(t,e){if(o.isSupport())if(c.isEmpty(r.mAppid))e("CU appid is empty");else if("undefined"==typeof NisSDK)var n=setInterval(function(){0,"undefined"!=typeof NisSDK?(o._loginAuth(t,e,i),clearInterval(n)):e("didn't find CU LOGIN sdk")},a.JGCons.OPERATOR.CU_CHECK_SRC_TIME);else o._loginAuth(t,e,i);else e("is not support now")})},t.prototype.nisLogin=function(){var e=this;NisSDK.nisLogin().then(function(t){c.log(t,e.getType())}).catch(function(t){c.log(t,e.getType())})},t.prototype._loginAuth=function(i,r,o){var s=this;c.log("cu2 _login auth appid:"+this.mAppid,s.getType());var t=(new Date).getTime();window.login="nisLogin()";var e={authenticator:this.sign(t),clientId:this.mAppid,reqTime:t,logo:a.JGCons.OPERATOR.CU_UI.CU_LOGIN_LOGO,appName:a.JGCons.OPERATOR.CU_UI.CU_APP_NAME,appPrivacyOne:["中国联通认证服务条款","https://hs.wosms.cn/protocol/protocol.html"],appPrivacyTwo:["客户自定义条款","https://*****.html"],timeout:this.mTimeOut};Object.assign(e,a.JGCons.CUSTOMUI),c.log("ui config setting :"+JSON.stringify(e),s.getType(),!0),NisSDK.getAuthurl(e).then(function(t){if(void 0!==t.accessCode){c.log("cu getAuthurl result:"+JSON.stringify(t),s.getType());var e={code:t.code,msg:t.msg,traceId:t.traceId,operator:t.operator,pmobile:t.pmobile,accessToken:t.accessCode};if(a.JGCons.CUSTOMUI.maskPhone=e.pmobile,c.log("CU2 getAuthurl type:"+o),null!=s.operaterUI&&null!=s.operaterUI||(s.operaterUI=new u.OperaterUI,s.operaterUI.operateType=a.JGCons.OPERATOR.CU),"dialog"==o)s.operaterUI.loginAuthPop(),s.mPreloginCallback(),s._authLogin(i,r,e,o);else{var n=s.operaterUI.createIframe();n.allowFullscreen=!1,n.style.overflow="hidden",n.addEventListener("load",function(){window.parent.postMessage("006",a.JGCons.HTML_URL),s.mPreloginCallback(),c.log("CU: login Auth Page show success")}),document.body.appendChild(n),s._authLogin(i,r,e,o)}}else r(t.code+"|"+t.msg)}).catch(function(t){c.log("cu getAuthurl error code:"+t,s.getType()),c.log("cu getAuthurl error code:"+t.code+" msg:"+t.msg,s.getType()),-2==t.code?r("-17|"+t.msg):r(t.code+"|"+t.msg)})},t.prototype._authLogin=function(i,r,t,e){var o=this,s=t;window.addEventListener("message",function(t){if((t.origin==a.JGCons.IFRAME_URL||t.origin==a.JGCons.HTML_URL)&&-1!=t.data.indexOf("005")){c.log("CU2 AUTH LOGIN:"+s.pmobile,o.getType());var e=t.data.replace("005-","");if(c.isEmpty(e))return c.log("Not operating for too long maskPhone expire",o.getType()),void r("Not operating for too long maskPhone expire");if(c.isEmpty(s.accessToken))return c.log("Not operating for too long maskPhone expire",o.getType()),void r("Not operating for too long maskPhone expire");if(c.isEmpty(s.pmobile))return c.log("Not pmobile or pmobile expire",o.getType()),void r("Not pmobile or pmobile expire");var n=s.pmobile.replace("****",e);i(o.buildLoginMsg({token:s.accessToken,phone:n}))}},!1)},t}(o.Operater);e.OperaterCU=h},function(t,e,n){"use strict";e.__esModule=!0;var L=n(2),N=n(3),i=function(){function t(){var n=this;this._doKeys=function(t){var e=t.target.getAttribute("qs-data-value");if(null!=e)switch(e){case"<":this._maskPhone(e);break;case"":break;default:this._maskPhone(Number(e))}},this._keyBoardClick=function(t){var e=n;switch(t.target.nodeName){case"TD":case"IMG":e._doKeys(t)}},this._closeAuthPage=function(){N.log("js closeAuthPage"),window.parent.postMessage("-1",L.JGCons.HTML_URL)},this._selectProtocol=function(){var t=document.getElementById("jv-checkbox");document.getElementById("jv-login");t.checked?window.parent.postMessage("001",L.JGCons.HTML_URL):window.parent.postMessage("002",L.JGCons.HTML_URL)},this._loginOtherWay=function(){N.log("postMessage->loginOtherWay"),window.parent.postMessage("-2",L.JGCons.HTML_URL)}}return t.prototype.init=function(t,e){this.uiconfig=t,this.preloginInfo=e},t.prototype.createIframe=function(){var t=document.createElement("iframe");t.id="cmiframe",t.style.width="100%",t.style.height="100%",t.style.margin="0",t.style.padding="0",t.style.overflow="hidden",t.style.border="none",t.style.position="fixed",t.style.zIndex="2147483647",t.style.backgroundColor=" rgb(255, 255, 255)",t.style.left="0",t.style.top="0";var e=L.JGCons.IFRAME_URL+"/scripts/5.1.4/JVCustomUI.html?";if(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=e+"maskPhone="+encodeURIComponent(L.JGCons.CUSTOMUI.maskPhone))+"&authPageType="+L.JGCons.CUSTOMUI.authPageType)+"&logo="+L.JGCons.CUSTOMUI.iframeLogoPath)+"&appName="+encodeURIComponent(L.JGCons.CUSTOMUI.appName))+"&loginBtnColor="+encodeURIComponent(L.JGCons.CUSTOMUI.loginBtnColor.replace("#","")))+"&loginTextColor="+encodeURIComponent(L.JGCons.CUSTOMUI.loginTextColor.replace("#","")))+"&isDisplayOtherWayBtn="+Boolean(L.JGCons.CUSTOMUI.isDisplayOtherWayBtn))+"&customOtherWayText="+encodeURIComponent(L.JGCons.CUSTOMUI.customOtherWayText))+"&customOtherWayTextColor="+encodeURIComponent(L.JGCons.CUSTOMUI.customOtherWayTextColor.replace("#","")))+"&customPolicyLinkColor="+encodeURIComponent(L.JGCons.CUSTOMUI.customPolicyLinkColor.replace("#","")),this.operateType==L.JGCons.OPERATOR.CU){var n="《中国联通认证服务条款》",i="https://hs.wosms.cn/protocol/protocol.html";e=(e=e+"&customPolicyLink0_0="+encodeURIComponent(n))+"&customPolicyLink0_1="+i}else{n="《中国移动认证服务协议》",i="https://wap.cmpassport.com/resources/html/contract.html?flag=true";e=(e=e+"&customPolicyLink0_0="+encodeURIComponent(n))+"&customPolicyLink0_1="+i}return void 0!==L.JGCons.CUSTOMUI.customPolicyLink1&&0!==L.JGCons.CUSTOMUI.customPolicyLink1.length&&L.JGCons.CUSTOMUI.customPolicyLink1[0]&&L.JGCons.CUSTOMUI.customPolicyLink1[1]&&(e=(e=e+"&customPolicyLink1_0="+encodeURIComponent(L.JGCons.CUSTOMUI.customPolicyLink1[0]))+"&customPolicyLink1_1="+L.JGCons.CUSTOMUI.customPolicyLink1[1]),void 0!==L.JGCons.CUSTOMUI.customPolicyLink2&&0!==L.JGCons.CUSTOMUI.customPolicyLink2.length&&L.JGCons.CUSTOMUI.customPolicyLink2[0]&&L.JGCons.CUSTOMUI.customPolicyLink2[1]&&(e=(e=e+"&customPolicyLink2_0="+encodeURIComponent(L.JGCons.CUSTOMUI.customPolicyLink2[0]))+"&customPolicyLink2_1="+L.JGCons.CUSTOMUI.customPolicyLink2[1]),void 0!==L.JGCons.CUSTOMUI.customPolicyLink3&&0!==L.JGCons.CUSTOMUI.customPolicyLink3.length&&L.JGCons.CUSTOMUI.customPolicyLink3[0]&&L.JGCons.CUSTOMUI.customPolicyLink3[1]&&(e=(e=e+"&customPolicyLink3_0="+encodeURIComponent(L.JGCons.CUSTOMUI.customPolicyLink3[0]))+"&customPolicyLink3_1="+L.JGCons.CUSTOMUI.customPolicyLink3[1]),e=e+"&domainName="+L.JGCons.HTML_URL,console.log("iframe.src->"+e),t.src=e,t},t.prototype._maskPhone=function(t){var e=document.getElementById("inputVal3"),n=document.getElementById("inputVal4"),i=document.getElementById("inputVal5"),r=document.getElementById("inputVal6");"<"==t?r.textContent?r.textContent="":i.textContent?i.textContent="":n.textContent?n.textContent="":e.textContent&&(e.textContent=""):e.textContent?n.textContent?i.textContent?r.textContent||(window.parent.postMessage("004",L.JGCons.HTML_URL),r.textContent=t):i.textContent=t:n.textContent=t:(e.textContent=""+t,window.parent.postMessage("003",L.JGCons.HTML_URL));var o=document.getElementById("jv-login");e.textContent&&n.textContent&&i.textContent&&r.textContent?(o.disabled=!1,L.JGCons.CUSTOMUI.loginBtnColor?o.style.backgroundColor=L.JGCons.CUSTOMUI.loginBtnColor:o.style.backgroundColor="#9400D3"):(o.disabled=!0,o.style.backgroundColor="#ccc")},t.prototype._keyBordUI=function(){var t=document.getElementById("jv-pop-id-main"),e=document.createElement("script");e.type="text/css",e.src=L.JGCons.IFRAME_URL+"/scripts/5.1.4/css/keyboard.css",document.body.appendChild(e);var n,i,r=[1,2,3,4,5,6,7,8,9," ","0","<"],o=document.createElement("DIV"),s=document.createElement("DIV"),a=document.createElement("TABLE"),c=document.createElement("TBODY");o.className="qs-key-board-wrap",o.id="qs_key_board_dev",s.className="qs-key-board",s.id="qs-keyboard-id",s.addEventListener("click",this._keyBoardClick,!1);for(var u=0;u=h.JGCons.OPERATOR.CM_CHECK_SRC_COUNT?(e("load CT login fjs failed"),clearInterval(i)):p.log("try to load CT login fjs times:"+n)},h.JGCons.OPERATOR.CM_CHECK_SRC_TIME);else s._loginAuth(t,e,r);else e("is not support now")})},t.prototype._loginAuth=function(e,i,t){var r=this,n="";t&&"dialog"==t&&(n="lite"),fjs.getAccessCode({debug:!1,btnId:"jg-ct-fix-btn",appId:this.mAppid,timeout:this.mTimeOut,theme:n,getSignParams:function(t){p.log("getSignParams :"+t.encryValue,r.getType()),l.http({url:h.JGCons.REQUEST.URL_LOGIN_SIGN_CT,method:"GET",param:{appKey:r.mAppKey,encryptValue:t.encryValue},async:!0,header:!0}).then(function(t){p.log("getSign result :"+JSON.stringify(t.result),r.getType());var e=JSON.parse(t.result);fjs.setSign(e.sign)}).catch(function(t){i("request sign failed")})},ready:function(t){p.log("getToken ready: "+JSON.stringify(t),r.getType());var e=document.getElementById("jg-ct-fix-btn");try{var n=document.createEvent("MouseEvents");n.initMouseEvent("click",!0,!0,document.defaultView,0,0,0,-1,-1,!1,!1,!1,!1,0,null),e.dispatchEvent(n),window.parent.postMessage("006",h.JGCons.HTML_URL),r.mPreloginCallback(),p.log("CT: login Auth Page show success")}catch(t){i("ct dispath event failed")}},success:function(t){p.log("getToken result: "+JSON.stringify(t),r.getType()),"0"===t.result?e(r.buildLoginMsg({token:t.accessCode})):i(t.result+"|"+t.msg)},error:function(t){"string"!=typeof t&&(p.log("getToken error->: "+JSON.stringify(t),r.getType()),i(t.result+"|"+t.msg))}})},t}(o.Operater);e.OperaterCT=s},function(t,e,n){!function(t){"use strict";var e="0123456789abcdefghijklmnopqrstuvwxyz";function c(t){return e.charAt(t)}function n(t,e){return t&e}function u(t,e){return t|e}function i(t,e){return t^e}function r(t,e){return t&~e}function o(t){if(0==t)return-1;var e=0;return 0==(65535&t)&&(t>>=16,e+=16),0==(255&t)&&(t>>=8,e+=8),0==(15&t)&&(t>>=4,e+=4),0==(3&t)&&(t>>=2,e+=2),0==(1&t)&&++e,e}function s(t){for(var e=0;0!=t;)t&=t-1,++e;return e}var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function h(t){var e,n,i="";for(e=0;e+3<=t.length;e+=3)n=parseInt(t.substring(e,e+3),16),i+=a.charAt(n>>6)+a.charAt(63&n);for(e+1==t.length?(n=parseInt(t.substring(e,e+1),16),i+=a.charAt(n<<2)):e+2==t.length&&(n=parseInt(t.substring(e,e+2),16),i+=a.charAt(n>>2)+a.charAt((3&n)<<4));0<(3&i.length);)i+="=";return i}function l(t){var e,n="",i=0,r=0;for(e=0;e>2),r=3&o,1):1==i?(n+=c(r<<2|o>>4),r=15&o,2):2==i?(n+=c(r),n+=c(o>>2),r=3&o,3):(n+=c(r<<2|o>>4),n+=c(15&o),0))}return 1==i&&(n+=c(r<<2)),n} +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */var p,f,d=function(t,e){return(d=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,e)},g={decode:function(t){var e;if(void 0===p){var n="0123456789ABCDEF",i=" \f\n\r\t \u2028\u2029";for(p={},e=0;e<16;++e)p[n.charAt(e)]=e;for(n=n.toLowerCase(),e=10;e<16;++e)p[n.charAt(e)]=e;for(e=0;e>16,i[i.length]=r>>8&255,i[i.length]=255&r,o=r=0):r<<=6}}switch(o){case 1:throw new Error("Base64 encoding incomplete: at least 2 bits missing");case 2:i[i.length]=r>>10;break;case 3:i[i.length]=r>>16,i[i.length]=r>>8&255}return i},re:/-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/,unarmor:function(t){var e=m.re.exec(t);if(e)if(e[1])t=e[1];else{if(!e[2])throw new Error("RegExp out of sync");t=e[2]}return m.decode(t)}},y=1e13,v=function(){function t(t){this.buf=[+t||0]}return t.prototype.mulAdd=function(t,e){var n,i,r=this.buf,o=r.length;for(n=0;ne&&(t=t.substring(0,e)+C),t}var O,_=function(){function n(t,e){this.hexDigits="0123456789ABCDEF",this.pos=t instanceof n?(this.enc=t.enc,t.pos):(this.enc=t,e)}return n.prototype.get=function(t){if(void 0===t&&(t=this.pos++),t>=this.enc.length)throw new Error("Requesting byte offset "+t+" on a stream of length "+this.enc.length);return"string"==typeof this.enc?this.enc.charCodeAt(t):this.enc[t]},n.prototype.hexByte=function(t){return this.hexDigits.charAt(t>>4&15)+this.hexDigits.charAt(15&t)},n.prototype.hexDump=function(t,e,n){for(var i="",r=t;r>h&1?"1":"0";if(s.length>n)return o+S(s,n)}return o+s},n.prototype.parseOctetString=function(t,e,n){if(this.isASCII(t,e))return S(this.parseStringISO(t,e),n);var i=e-t,r="("+i+" byte)\n";(n/=2)n)return S(i,n);r=new v,o=0}}return 0>6,this.tagConstructed=0!=(32&e),this.tagNumber=31&e,31==this.tagNumber){for(var n=new v;e=t.get(),n.mulAdd(128,127&e),128&e;);this.tagNumber=n.simplify()}}return t.prototype.isUniversal=function(){return 0===this.tagClass},t.prototype.isEOC=function(){return 0===this.tagClass&&0===this.tagNumber},t}(),b=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],w=(1<<26)/b[b.length-1],M=function(){function C(t,e,n){null!=t&&("number"==typeof t?this.fromNumber(t,e,n):null==e&&"string"!=typeof t?this.fromString(t,256):this.fromString(t,e))}return C.prototype.toString=function(t){if(this.s<0)return"-"+this.negate().toString(t);var e;if(16==t)e=4;else if(8==t)e=3;else if(2==t)e=1;else if(32==t)e=5;else{if(4!=t)return this.toRadix(t);e=2}var n,i=(1<>a)&&(r=!0,o=c(n));0<=s;)a>(a+=this.DB-e)):(n=this[s]>>(a-=e)&i,a<=0&&(a+=this.DB,--s)),0>24},C.prototype.shortValue=function(){return 0==this.t?this.s:this[0]<<16>>16},C.prototype.signum=function(){return this.s<0?-1:this.t<=0||1==this.t&&this[0]<=0?0:1},C.prototype.toByteArray=function(){var t=this.t,e=[];e[0]=this.s;var n,i=this.DB-t*this.DB%8,r=0;if(0>i)!=(this.s&this.DM)>>i&&(e[r++]=n|this.s<>(i+=this.DB-8)):(n=this[t]>>(i-=8)&255,i<=0&&(i+=this.DB,--t)),0!=(128&n)&&(n|=-256),0==r&&(128&this.s)!=(128&n)&&++r,(0=this.t?0!=this.s:0!=(this[e]&1<>r-c&u:(l=(t[f]&(1<>this.DB+r-c)),a=n;0==(1&l);)l>>=1,--a;if((r-=a)<0&&(r+=this.DB,--f),d)s[l].copyTo(o),d=!1;else{for(;1this.DB?(this[this.t-1]|=(s&(1<>this.DB-o):this[this.t-1]|=s<=this.DB&&(o-=this.DB))}8==n&&0!=(128&+t[0])&&(this.s=-1,0>i|s,s=(this[a]&r)<=this.t)e.t=0;else{var i=t%this.DB,r=this.DB-i,o=(1<>i;for(var s=n+1;s>i;0>=this.DB;if(t.t>=this.DB;i+=this.s}else{for(i+=this.s;n>=this.DB;i-=t.s}e.s=i<0?-1:0,i<-1?e[n++]=this.DV+i:0=e.DV&&(t[n+e.t]-=e.DV,t[n+e.t+1]=1)}0>this.F2:0),p=this.FV/l,f=(1<=n&&(this.dMultiply(i),this.dAddOffset(s,0),s=o=0))}0t&&this.subTo(C.ONE.shiftLeft(t-1),this);else{var i=[],r=7&t;i.length=1+(t>>3),e.nextBytes(i),0>=this.DB;if(t.t>=this.DB;i+=this.s}else{for(i+=this.s;n>=this.DB;i+=t.s}e.s=i<0?-1:0,0=this.DV;)this[e]-=this.DV,++e>=this.t&&(this[this.t++]=0),++this[e]}},C.prototype.multiplyLowerTo=function(t,e,n){var i=Math.min(this.t+t.t,e);for(n.s=0,n.t=i;0>1)&&(t=b.length);for(var r=x(),o=0;ot&&r.subTo(C.ONE.shiftLeft(t-1),r),r.isProbablePrime(e)?setTimeout(function(){i()},0):setTimeout(o,0)};setTimeout(o,0)}else{var s=[],a=7&t;s.length=1+(t>>3),e.nextBytes(s),0>15,this.um=(1<>15)*this.mpl&this.um)<<15)&t.DM;for(n=e+this.m.t,t[n]+=this.m.am(0,i,t,e,0,this.m.t);t[n]>=t.DV;)t[n]-=t.DV,t[++n]++}t.clamp(),t.drShiftTo(this.m.t,t),0<=t.compareTo(this.m)&&t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,n){t.multiplyTo(e,n),this.reduce(n)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}(),B=function(){function t(t){this.m=t,this.r2=x(),this.q3=x(),M.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t)}return t.prototype.convert=function(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var e=x();return t.copyTo(e),this.reduce(e),e},t.prototype.revert=function(t){return t},t.prototype.reduce=function(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);0<=t.compareTo(this.m);)t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,n){t.multiplyTo(e,n),this.reduce(n)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}();function x(){return new M(null)}function D(t,e){return new M(t,e)}O="Microsoft Internet Explorer"==navigator.appName?(M.prototype.am=function(t,e,n,i,r,o){for(var s=32767&e,a=e>>15;0<=--o;){var c=32767&this[t],u=this[t++]>>15,h=a*c+u*s;c=s*c+((32767&h)<<15)+n[i]+(1073741823&r),r=(c>>>30)+(h>>>15)+a*u+(r>>>30),n[i++]=1073741823&c}return r},30):"Netscape"!=navigator.appName?(M.prototype.am=function(t,e,n,i,r,o){for(;0<=--o;){var s=e*this[t++]+n[i]+r;r=Math.floor(s/67108864),n[i++]=67108863&s}return r},26):(M.prototype.am=function(t,e,n,i,r,o){for(var s=16383&e,a=e>>14;0<=--o;){var c=16383&this[t],u=this[t++]>>14,h=a*c+u*s;c=s*c+((16383&h)<<14)+n[i]+r,r=(c>>28)+(h>>14)+a*u,n[i++]=268435455&c}return r},28),M.prototype.DB=O,M.prototype.DM=(1<>>16)&&(t=e,n+=16),0!=(e=t>>8)&&(t=e,n+=8),0!=(e=t>>4)&&(t=e,n+=4),0!=(e=t>>2)&&(t=e,n+=2),0!=(e=t>>1)&&(t=e,n+=1),n}M.ZERO=J(0),M.ONE=J(1);var V,j,F=function(){function t(){this.i=0,this.j=0,this.S=[]}return t.prototype.init=function(t){var e,n,i;for(e=0;e<256;++e)this.S[e]=e;for(e=n=0;e<256;++e)n=n+this.S[e]+t[e%t.length]&255,i=this.S[e],this.S[e]=this.S[n],this.S[n]=i;this.i=0,this.j=0},t.prototype.next=function(){var t;return this.i=this.i+1&255,this.j=this.j+this.S[this.i]&255,t=this.S[this.i],this.S[this.i]=this.S[this.j],this.S[this.j]=t,this.S[t+this.S[this.i]&255]},t}(),Q=256,K=null;if(null==K){K=[];var q=void(j=0);if(window.crypto&&window.crypto.getRandomValues){var z=new Uint32Array(256);for(window.crypto.getRandomValues(z),q=0;q>6|192):(n[--e]=63&r|128,n[--e]=r>>6&63|128,r>>12|224)}n[--e]=0;for(var o=new W,s=[];2>3);if(null==e)return null;var n=this.doPublic(e);if(null==n)return null;var i=n.toString(16);return 0==(1&i.length)?i:"0"+i},t.prototype.setPrivate=function(t,e,n){null!=t&&null!=e&&0>1;this.e=parseInt(e,16);for(var r=new M(e,16);;){for(;this.p=new M(t-i,1,n),0!=this.p.subtract(M.ONE).gcd(r).compareTo(M.ONE)||!this.p.isProbablePrime(10););for(;this.q=new M(i,1,n),0!=this.q.subtract(M.ONE).gcd(r).compareTo(M.ONE)||!this.q.isProbablePrime(10););if(this.p.compareTo(this.q)<=0){var o=this.p;this.p=this.q,this.q=o}var s=this.p.subtract(M.ONE),a=this.q.subtract(M.ONE),c=s.multiply(a);if(0==c.gcd(r).compareTo(M.ONE)){this.n=this.p.multiply(this.q),this.d=r.modInverse(c),this.dmp1=this.d.mod(s),this.dmq1=this.d.mod(a),this.coeff=this.q.modInverse(this.p);break}}},t.prototype.decrypt=function(t){var e=D(t,16),n=this.doPrivate(e);return null==n?null:function(t,e){for(var n=t.toByteArray(),i=0;i=n.length)return null;for(var r="";++i>3)},t.prototype.generateAsync=function(t,e,r){var o=new W,s=t>>1;this.e=parseInt(e,16);var a=new M(e,16),c=this,u=function(){var e=function(){if(c.p.compareTo(c.q)<=0){var t=c.p;c.p=c.q,c.q=t}var e=c.p.subtract(M.ONE),n=c.q.subtract(M.ONE),i=e.multiply(n);0==i.gcd(a).compareTo(M.ONE)?(c.n=c.p.multiply(c.q),c.d=a.modInverse(i),c.dmp1=c.d.mod(e),c.dmq1=c.d.mod(n),c.coeff=c.q.modInverse(c.p),setTimeout(function(){r()},0)):setTimeout(u,0)},n=function(){c.q=x(),c.q.fromNumberAsync(s,1,o,function(){c.q.subtract(M.ONE).gcda(a,function(t){0==t.compareTo(M.ONE)&&c.q.isProbablePrime(10)?setTimeout(e,0):setTimeout(n,0)})})},i=function(){c.p=x(),c.p.fromNumberAsync(t-s,1,o,function(){c.p.subtract(M.ONE).gcda(a,function(t){0==t.compareTo(M.ONE)&&c.p.isProbablePrime(10)?setTimeout(n,0):setTimeout(i,0)})})};setTimeout(i,0)};setTimeout(u,0)},t.prototype.sign=function(t,e,n){var i=$[n]||"",r=i+e(t).toString(),o=function(t,e){if(eMIT License + */ +var et={};void 0!==et.asn1&&et.asn1||(et.asn1={}),et.asn1.ASN1Util=new function(){this.integerToByteHex=function(t){var e=t.toString(16);return e.length%2==1&&(e="0"+e),e},this.bigIntToMinTwosComplementsHex=function(t){var e=t.toString(16);if("-"!=e.substr(0,1))e.length%2==1?e="0"+e:e.match(/^[0-7]/)||(e="00"+e);else{var n=e.substr(1),i=n.length;i%2==1?i+=1:e.match(/^[0-7]/)||(i+=2);for(var r="",o=0;o=e?t:new Array(e-t.length+1).join("0")+t},this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=stohex(t)},this.setByDateValue=function(t,e,n,i,r,o){var s=new Date(Date.UTC(t,e-1,n,i,r,o,0));this.setByDate(s)},this.getFreshValueHex=function(){return this.hV}},tt.lang.extend(et.asn1.DERAbstractTime,et.asn1.ASN1Object),et.asn1.DERAbstractStructured=function(t){et.asn1.DERAbstractString.superclass.constructor.call(this),this.setByASN1ObjectArray=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array=t},this.appendASN1Object=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array.push(t)},this.asn1Array=new Array,void 0!==t&&void 0!==t.array&&(this.asn1Array=t.array)},tt.lang.extend(et.asn1.DERAbstractStructured,et.asn1.ASN1Object),et.asn1.DERBoolean=function(){et.asn1.DERBoolean.superclass.constructor.call(this),this.hT="01",this.hTLV="0101ff"},tt.lang.extend(et.asn1.DERBoolean,et.asn1.ASN1Object),et.asn1.DERInteger=function(t){et.asn1.DERInteger.superclass.constructor.call(this),this.hT="02",this.setByBigInteger=function(t){this.hTLV=null,this.isModified=!0,this.hV=et.asn1.ASN1Util.bigIntToMinTwosComplementsHex(t)},this.setByInteger=function(t){var e=new M(String(t),10);this.setByBigInteger(e)},this.setValueHex=function(t){this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&(void 0!==t.bigint?this.setByBigInteger(t.bigint):void 0!==t.int?this.setByInteger(t.int):"number"==typeof t?this.setByInteger(t):void 0!==t.hex&&this.setValueHex(t.hex))},tt.lang.extend(et.asn1.DERInteger,et.asn1.ASN1Object),et.asn1.DERBitString=function(t){if(void 0!==t&&void 0!==t.obj){var e=et.asn1.ASN1Util.newObject(t.obj);t.hex="00"+e.getEncodedHex()}et.asn1.DERBitString.superclass.constructor.call(this),this.hT="03",this.setHexValueIncludingUnusedBits=function(t){this.hTLV=null,this.isModified=!0,this.hV=t},this.setUnusedBitsAndHexValue=function(t,e){if(t<0||7 { + let filePath = path.join(rootDir, req.url === '/' ? 'index.html' : req.url); + + const extname = String(path.extname(filePath)).toLowerCase(); + const contentType = mimeTypes[extname] || 'application/octet-stream'; + + fs.readFile(filePath, (error, content) => { + if (error) { + if (error.code === 'ENOENT') { + res.writeHead(404, { 'Content-Type': 'text/html' }); + res.end('

404 Not Found

', 'utf-8'); + } else { + res.writeHead(500); + res.end(`Server Error: ${error.code}`, 'utf-8'); + } + } else { + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content, 'utf-8'); + } + }); +}); + +server.listen(port, () => { + console.log(`服务器已启动!`); + console.log(`访问地址: http://localhost:${port}`); + console.log(`按 Ctrl+C 停止服务器`); +}); diff --git a/src/css/components/one-click-login.css b/src/css/components/one-click-login.css new file mode 100644 index 0000000..49222cc --- /dev/null +++ b/src/css/components/one-click-login.css @@ -0,0 +1,104 @@ +/* 一键登录按钮样式 */ +.one-click-login-wrapper { + padding: 0 16px; + margin-bottom: 16px; + /* 初始隐藏,避免布局闪烁 */ + display: none; +} + +.one-click-login-wrapper.show { + display: block; +} + +.one-click-login-container { + margin-bottom: 16px; + text-align: center; +} + +.one-click-login-btn { + width: 100%; + height: 48px; + border: none; + border-radius: 24px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 16px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.one-click-login-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); +} + +.one-click-login-btn:active:not(:disabled) { + transform: translateY(0); +} + +.one-click-login-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.one-click-login-btn .btn-icon { + font-size: 20px; +} + +.one-click-login-btn .btn-text { + flex: 1; +} + +/* 加载动画 */ +.btn-spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 分割线 */ +.login-divider { + display: flex; + align-items: center; + margin: 20px 0; + gap: 12px; +} + +.divider-line { + flex: 1; + height: 1px; + background: #e0e0e0; +} + +.divider-text { + color: #999; + font-size: 14px; + padding: 0 8px; +} + +/* 响应式 */ +@media (max-width: 480px) { + .one-click-login-btn { + height: 44px; + font-size: 15px; + } + + .one-click-login-btn .btn-icon { + font-size: 18px; + } +} diff --git a/src/js/README.md b/src/js/README.md new file mode 100644 index 0000000..2cbebcd --- /dev/null +++ b/src/js/README.md @@ -0,0 +1,351 @@ +# 薇钱包 H5 项目 - 模块化重构说明 + +## 📁 新的目录结构 + +``` +web/ +├── src/js/ +│ ├── config/ # 配置模块 +│ │ ├── index.js # 配置统一导出 +│ │ ├── api.config.js # API 配置 +│ │ └── app.config.js # 应用配置 +│ │ +│ ├── core/ # 核心模块 +│ │ ├── index.js # 核心模块统一导出 +│ │ ├── api.js # API 请求封装 +│ │ ├── user-cache.js # 用户缓存管理 +│ │ ├── form-id.js # 表单 ID 生成器 +│ │ └── draft-manager.js # 草稿管理器 +│ │ +│ ├── utils/ # 工具函数库 +│ │ ├── index.js # 工具函数统一导出 +│ │ ├── validator.js # 表单验证器 +│ │ ├── formatter.js # 格式化工具 +│ │ └── helper.js # 通用辅助函数 +│ │ +│ ├── services/ # 业务服务层 +│ │ ├── index.js # 服务层统一导出 +│ │ ├── sms.service.js # 短信服务 +│ │ ├── auth.service.js # 认证服务 +│ │ ├── loan.service.js # 借款服务 +│ │ └── form.service.js # 表单服务 +│ │ +│ ├── ui/ # UI 组件 +│ │ ├── index.js # UI 组件统一导出 +│ │ ├── modal.js # 模态框基类 +│ │ ├── picker.js # 选择器组件 +│ │ ├── toast.js # Toast 提示 +│ │ └── city-picker.js # 城市选择器 +│ │ +│ ├── pages/ # 页面逻辑 +│ │ ├── index.js # 页面逻辑统一导出 +│ │ ├── index.page.js # 主页面逻辑 +│ │ └── basic-info.page.js # 基本信息页面逻辑 +│ │ +│ └── main.js # 应用主入口 +│ +├── index.html # 主借款申请页面 +├── basic-info.html # 基本信息填写页面 +├── config.js # 旧配置文件(已废弃) +├── script.js # 旧主页面逻辑(已废弃) +└── basic-info.js # 旧基本信息页面逻辑(已废弃) +``` + +## 🚀 使用新模块化结构 + +### 方法 1: 使用 ES6 模块(推荐) + +在 HTML 文件中使用 ` +``` + +**注意:** +- 需要删除旧的 ` + + +``` + +添加新的 module script: +```html + + +``` + +### 步骤 3: 测试功能 + +1. 启动开发服务器: +```bash +node server.js +``` + +2. 在浏览器中访问 `http://localhost:3000` + +3. 测试所有功能: + - 借款申请流程 + - 短信验证 + - 资产信息填写 + - 基本信息填写 + - 表单提交 + +## ✨ 新架构的优势 + +### 1. 更好的代码组织 +- **职责分离**:每个模块只负责一个功能 +- **易于定位**:问题查找更快 +- **代码复用**:组件和服务可在多个页面使用 + +### 2. 更强的可维护性 +- **单一职责**:文件小,易于理解 +- **依赖清晰**:通过 import 明确依赖关系 +- **易于扩展**:新增功能只需添加新模块 + +### 3. 更好的开发体验 +- **IDE 支持**:完整的代码提示和跳转 +- **调试友好**:源码映射支持 +- **版本控制**:小的改动更清晰 + +### 4. 性能优化潜力 +- **按需加载**:ES6 模块支持按需加载 +- **Tree Shaking**:未使用的代码可被删除 +- **缓存优化**:模块级别的缓存 + +## 🐛 常见问题 + +### Q1: 浏览器报错 "Cannot use import statement outside a module" + +**原因:** 浏览器不支持 ES6 模块或 script 标签没有 `type="module"`。 + +**解决:** 确保 script 标签包含 `type="module"`: +```html + +``` + +### Q2: CORS 错误 + +**原因:** 直接使用 file:// 协议打开 HTML 文件。 + +**解决:** 使用 HTTP 服务器: +```bash +# 使用 Node.js +npx serve . + +# 或使用 Python +python -m http.server 8000 + +# 或使用项目中的 server.js +node server.js +``` + +### Q3: 找不到模块 + +**原因:** 路径错误或文件名错误。 + +**解决:** +- 确保所有 import 路径以 `./` 或 `../` 开头 +- 检查文件名和扩展名是否正确 +- 确保导出语句(export)正确 + +## 📚 进一步阅读 + +- [ES6 模块入门](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules) +- [Vite 官方文档](https://cn.vitejs.dev/) +- [JavaScript 模块化最佳实践](https://www.patterns.dev/posts/modules/) + +## 🤝 贡献 + +在添加新功能时,请遵循现有的模块化结构: + +1. **配置** → 添加到 `config/` 目录 +2. **工具函数** → 添加到 `utils/` 目录 +3. **业务逻辑** → 添加到 `services/` 目录 +4. **UI 组件** → 添加到 `ui/` 目录 +5. **页面逻辑** → 添加到 `pages/` 目录 + +记得在相应的 `index.js` 文件中导出新模块! + +--- + +**重构完成日期:** 2025-01-21 +**重构人员:** Claude Code +**版本:** 2.0.0 diff --git a/src/js/config/api.config.js b/src/js/config/api.config.js new file mode 100644 index 0000000..5663ee4 --- /dev/null +++ b/src/js/config/api.config.js @@ -0,0 +1,30 @@ +/** + * API 配置 + * 统一管理所有 API 接口地址和配置 + */ + +export const API_CONFIG = { + // 基础 URL - 开发环境 + BASE_URL: 'http://localhost:8071', + + // 生产环境 URL(如需切换,取消注释并注释掉上面的) + // BASE_URL: 'https://flux.1216.top', + + // API 端点配置 + ENDPOINTS: { + // 短信相关接口(使用 JSON 格式) + SEND_SMS: '/zcore/sms/send', + VERIFY_SMS: '/zcore/sms/verify', + + // 客户相关接口(使用 x-www-form-urlencoded 格式) + CUSTOMER_REGISTER: '/partnerh5/login', + + // 表单相关接口(使用 x-www-form-urlencoded 格式) + SUBMIT_FORM: '/partnerh5/submit', + SUBMIT_DRAFT_FORM: '/partnerh5/save_draft', + GET_DRAFT_FORM: '/partnerh5/get_draft', + }, + + // 请求超时配置(毫秒) + TIMEOUT: 30000, +}; diff --git a/src/js/config/app.config.js b/src/js/config/app.config.js new file mode 100644 index 0000000..516e578 --- /dev/null +++ b/src/js/config/app.config.js @@ -0,0 +1,176 @@ +/** + * 应用配置 + * 包含应用级别的常量、映射关系、模拟数据等 + */ + +// ==================== 调试配置 ==================== +export const DEBUG_CONFIG = { + // 调试模式开关 + ENABLED: true, + + // 调试模式下的固定验证码 + SMS_CODE: '123456', + + // 是否启用详细日志 + VERBOSE_LOGGING: true, +}; + +// ==================== 动画配置 ==================== +export const ANIMATION_CONFIG = { + // 模态框动画时长(毫秒) + MODAL_DURATION: 300, + + // 滚轮防抖延迟(毫秒) + WHEEL_DEBOUNCE_DELAY: 150, + + // 滚动延迟(毫秒) + SCROLL_DELAY: 100, + + // Toast 默认显示时长(毫秒) + TOAST_DURATION: 2000, +}; + +// ==================== 资产映射配置 ==================== +export const ASSET_CONFIG = { + // 资产选项映射:中文 → 数字值 + VALUE_MAPPING: { + '有房产': 1, '无房产': 2, + '有车辆': 1, '无车辆': 2, + '有公积金': 1, '无公积金': 2, + '有社保': 1, '无社保': 2, + '有信用卡': 1, '无信用卡': 2, + '有银行流水': 1, '无银行流水': 2 + }, + + // 资产选项反向映射:数字值 → 中文 + REVERSE_MAPPING: { + house: { 1: '有房产', 2: '无房产' }, + car: { 1: '有车辆', 2: '无车辆' }, + fund: { 1: '有公积金', 2: '无公积金' }, + social: { 1: '有社保', 2: '无社保' }, + credit: { 1: '有信用卡', 2: '无信用卡' }, + bank: { 1: '有银行流水', 2: '无银行流水' } + }, + + // 资产项配置列表 + ITEMS: [ + { id: 'house', name: '房产', options: ['有房产', '无房产'] }, + { id: 'car', name: '车辆', options: ['有车辆', '无车辆'] }, + { id: 'fund', name: '公积金', options: ['有公积金', '无公积金'] }, + { id: 'social', name: '社保', options: ['有社保', '无社保'] }, + { id: 'credit', name: '信用卡', options: ['有信用卡', '无信用卡'] }, + { id: 'bank', name: '银行流水', options: ['有银行流水', '无银行流水'] } + ], + + // 进度金额配置 + PROGRESS_MONEY: { + INITIAL: 35000, + FINAL: 50000, + }, +}; + +// ==================== 基本信息配置 ==================== +export const BASIC_INFO_CONFIG = { + // 基本信息字段配置 + ITEMS: [ + { id: 'name', name: '真实姓名', placeholder: '请输入真实姓名', type: 'input' }, + { id: 'idCard', name: '身份证号', placeholder: '请输入身份证号', type: 'input' }, + { id: 'city', name: '所属城市', placeholder: '请选择', type: 'select' } + ], +}; + +// ==================== 省份城市数据 ==================== +export const PROVINCE_CITY_DATA = { + '江西省': ['南昌市', '九江市', '上饶市', '抚州市', '宜春市', '吉安市', '赣州市', '景德镇市', '萍乡市', '新余市', '鹰潭市'], + '山东省': ['济南市', '青岛市', '淄博市', '枣庄市', '东营市', '烟台市', '潍坊市', '济宁市', '泰安市', '威海市', '日照市', '临沂市', '德州市', '聊城市', '滨州市', '菏泽市'], + '河南省': ['郑州市', '开封市', '洛阳市', '平顶山市', '安阳市', '鹤壁市', '新乡市', '焦作市', '濮阳市', '许昌市', '漯河市', '三门峡市', '南阳市', '商丘市', '信阳市', '周口市', '驻马店市'], + '湖北省': ['武汉市', '黄石市', '十堰市', '宜昌市', '襄阳市', '鄂州市', '荆门市', '孝感市', '荆州市', '黄冈市', '咸宁市', '随州市'], + '湖南省': ['长沙市', '株洲市', '湘潭市', '衡阳市', '邵阳市', '岳阳市', '常德市', '张家界市', '益阳市', '郴州市', '永州市', '怀化市', '娄底市'], + '广东省': ['广州市', '韶关市', '深圳市', '珠海市', '汕头市', '佛山市', '江门市', '湛江市', '茂名市', '肇庆市', '惠州市', '梅州市', '汕尾市', '河源市', '阳江市', '清远市', '东莞市', '中山市', '潮州市', '揭阳市', '云浮市'], + '甘肃省': ['兰州市', '嘉峪关市', '金昌市', '白银市', '天水市', '武威市', '张掖市', '平凉市', '酒泉市', '庆阳市', '定西市', '陇南市'] +}; + +// ==================== 借款相关配置 ==================== +export const LOAN_CONFIG = { + // 借款金额范围 + AMOUNT: { + DEFAULT: 50000, + MAX: 200000, + MIN: 1000, + }, + + // 年化利率范围 + INTEREST_RATE: { + MIN: 10.8, + MAX: 24, + }, + + // 还款期数选项 + PERIOD_OPTIONS: [3, 6, 9, 12, 18, 24, 36], + + // 默认还款期数 + DEFAULT_PERIOD: 12, + + // 借款用途选项 + PURPOSES: [ + '个人日常消费', + '装修', + '旅游', + '教育', + '医疗', + '购车', + '其他' + ], + + // 默认借款用途 + DEFAULT_PURPOSE: '个人日常消费', +}; + +// ==================== 缓存配置 ==================== +export const CACHE_CONFIG = { + // 用户缓存时长(毫秒) + USER_SESSION_DURATION: 7 * 24 * 60 * 60 * 1000, // 7天 + + // 缓存键名 + KEYS: { + USER_SESSION: 'flux_user_session', + FORM_ID: 'flux_form_id', + }, +}; + +// ==================== 验证规则配置 ==================== +export const VALIDATION_CONFIG = { + // 手机号验证正则 + PHONE_REGEX: /^1[3-9]\d{9}$/, + + // 姓名验证正则(2-20个汉字,支持少数民族姓名) + NAME_REGEX: /^[\u4e00-\u9fa5]{2,20}(·[\u4e00-\u9fa5]+)*$/, + + // 身份证号验证正则 + ID_CARD_18_REGEX: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/, + ID_CARD_15_REGEX: /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/, + + // 身份证校验码权重 + ID_CARD_WEIGHTS: [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2], + ID_CARD_CHECK_CODES: ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'], +}; + +// ==================== 借款用途选择器配置 ==================== +export const PURPOSE_PICKER_CONFIG = { + triggerId: 'loanPurposeTrigger', + modalId: 'purposeModal', + cancelBtnId: 'purposeCancelBtn', + confirmBtnId: 'purposeConfirmBtn', + optionSelector: '#purposeModal .modal-option', +}; + +// ==================== 还款期数选择器配置 ==================== +export const TERM_PICKER_CONFIG = { + triggerId: 'termTrigger', + modalId: 'termModal', + cancelBtnId: 'termCancelBtn', + confirmBtnId: 'termConfirmBtn', + optionSelector: '#termModal .modal-option', + enableWheel: true, + useDataValue: true, +}; diff --git a/src/js/config/index.js b/src/js/config/index.js new file mode 100644 index 0000000..4fe3002 --- /dev/null +++ b/src/js/config/index.js @@ -0,0 +1,49 @@ +/** + * 配置统一导出 + * 提供统一的配置访问接口 + */ + +// 先导入所有配置 +import { API_CONFIG } from './api.config.js'; +import { + DEBUG_CONFIG, + ANIMATION_CONFIG, + ASSET_CONFIG, + BASIC_INFO_CONFIG, + PROVINCE_CITY_DATA, + LOAN_CONFIG, + CACHE_CONFIG, + VALIDATION_CONFIG, + PURPOSE_PICKER_CONFIG, + TERM_PICKER_CONFIG, +} from './app.config.js'; + +// 重新导出所有配置(命名导出) +export { API_CONFIG }; +export { + DEBUG_CONFIG, + ANIMATION_CONFIG, + ASSET_CONFIG, + BASIC_INFO_CONFIG, + PROVINCE_CITY_DATA, + LOAN_CONFIG, + CACHE_CONFIG, + VALIDATION_CONFIG, + PURPOSE_PICKER_CONFIG, + TERM_PICKER_CONFIG, +}; + +// 默认导出所有配置(分组对象) +export default { + API: API_CONFIG, + DEBUG: DEBUG_CONFIG, + ANIMATION: ANIMATION_CONFIG, + ASSET: ASSET_CONFIG, + BASIC_INFO: BASIC_INFO_CONFIG, + PROVINCE_CITY: PROVINCE_CITY_DATA, + LOAN: LOAN_CONFIG, + CACHE: CACHE_CONFIG, + VALIDATION: VALIDATION_CONFIG, + PURPOSE_PICKER: PURPOSE_PICKER_CONFIG, + TERM_PICKER: TERM_PICKER_CONFIG, +}; diff --git a/src/js/core/api.js b/src/js/core/api.js new file mode 100644 index 0000000..57447b6 --- /dev/null +++ b/src/js/core/api.js @@ -0,0 +1,141 @@ +/** + * API 客户端 + * 统一封装所有 HTTP 请求 + */ + +import { API_CONFIG, DEBUG_CONFIG } from '../config/index.js'; +import { UserCache } from './user-cache.js'; + +export class ApiClient { + /** + * 构建查询参数 + * @param {Object} params - 参数对象 + * @returns {string} - URL 编码的参数字符串 + */ + static buildParams(params) { + return Object.entries(params) + .map(([k, v]) => `${k}=${encodeURIComponent(typeof v === 'string' ? v : JSON.stringify(v))}`) + .join('&'); + } + + /** + * 获取请求头 + * @param {string} contentType - 内容类型 + * @returns {Object} - 请求头对象 + */ + static getHeaders(contentType = 'application/json') { + const headers = { 'Content-Type': contentType }; + + // 添加用户会话信息 + const session = UserCache.getUserSession(); + if (session?.sessionid) { + headers['jsessionid'] = session.sessionid; + } + + return headers; + } + + /** + * POST 请求 - JSON 格式 + * @param {string} endpoint - API 端点 + * @param {Object} data - 请求数据 + * @returns {Promise} - 响应数据 + */ + static async post(endpoint, data = {}) { + const url = API_CONFIG.BASE_URL + endpoint; + const headers = this.getHeaders('application/json'); + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (DEBUG_CONFIG.ENABLED && DEBUG_CONFIG.VERBOSE_LOGGING) { + console.log(`[API] POST ${endpoint}`, { request: data, response: result }); + } + + return result; + } catch (error) { + console.error(`[API] POST ${endpoint} 请求失败:`, error); + return { + retcode: -1, + retinfo: error.message || '网络错误,请稍后重试' + }; + } + } + + /** + * XPOST 请求 - x-www-form-urlencoded 格式 + * @param {string} endpoint - API 端点 + * @param {Object} data - 请求数据(会被包装在 bean 对象中) + * @returns {Promise} - 响应数据 + */ + static async xpost(endpoint, data = {}) { + const url = API_CONFIG.BASE_URL + endpoint; + const headers = this.getHeaders('application/x-www-form-urlencoded'); + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: this.buildParams({ bean: data }) + }); + + const result = await response.json(); + + if (DEBUG_CONFIG.ENABLED && DEBUG_CONFIG.VERBOSE_LOGGING) { + console.log(`[API] XPOST ${endpoint}`, { request: data, response: result }); + } + + return result; + } catch (error) { + console.error(`[API] XPOST ${endpoint} 请求失败:`, error); + return { + retcode: -1, + retinfo: error.message || '网络错误,请稍后重试' + }; + } + } + + /** + * GET 请求 + * @param {string} endpoint - API 端点 + * @param {Object} params - 查询参数 + * @returns {Promise} - 响应数据 + */ + static async get(endpoint, params = {}) { + const url = new URL(API_CONFIG.BASE_URL + endpoint); + + // 添加查询参数 + Object.entries(params).forEach(([key, value]) => { + url.searchParams.append(key, value); + }); + + const headers = this.getHeaders('application/json'); + + try { + const response = await fetch(url, { + method: 'GET', + headers + }); + + const result = await response.json(); + + if (DEBUG_CONFIG.ENABLED && DEBUG_CONFIG.VERBOSE_LOGGING) { + console.log(`[API] GET ${endpoint}`, { params, response: result }); + } + + return result; + } catch (error) { + console.error(`[API] GET ${endpoint} 请求失败:`, error); + return { + retcode: -1, + retinfo: error.message || '网络错误,请稍后重试' + }; + } + } +} diff --git a/src/js/core/draft-manager.js b/src/js/core/draft-manager.js new file mode 100644 index 0000000..2000199 --- /dev/null +++ b/src/js/core/draft-manager.js @@ -0,0 +1,183 @@ +/** + * 草稿管理器 + * 负责草稿数据的保存、加载和恢复 + */ + +import { ApiClient } from './api.js'; +import { API_CONFIG, ASSET_CONFIG, CACHE_CONFIG, DEBUG_CONFIG } from '../config/index.js'; +import { FormIdGenerator } from './form-id.js'; + +export class DraftManager { + /** + * 构建草稿数据 + * @param {Object} selectedValues - 资产信息 + * @param {Object} basicInfoValues - 基本信息Values - 表单数据对象 + * @private + */ + static _buildDraftData(selectedValues, basicInfoValues) { + return { + assets: selectedValues, + basicInfo: basicInfoValues + }; + } + + /** + * 转换为服务器数据格式 + * @param {Object} formData - 表单数据 + * @returns {Object} - 服务器数据格式 + * @private + */ + static _convertToServerFormat(formData) { + return { + house: ASSET_CONFIG.VALUE_MAPPING[formData.assets.house] || 0, + vehicle: ASSET_CONFIG.VALUE_MAPPING[formData.assets.car] || 0, + fund: ASSET_CONFIG.VALUE_MAPPING[formData.assets.fund] || 0, + social: ASSET_CONFIG.VALUE_MAPPING[formData.assets.social] || 0, + credit: ASSET_CONFIG.VALUE_MAPPING[formData.assets.credit] || 0, + bank: ASSET_CONFIG.VALUE_MAPPING[formData.assets.bank] || 0, + realname: formData.basicInfo.name || '', + idcard: formData.basicInfo.idCard || '', + city: formData.basicInfo.city || '', + draftstatus: 1, // 草稿状态 + formid: FormIdGenerator.getOrCreate() + }; + } + + /** + * 从服务器格式转换为表单格式 + * @param {Object} serverData - 服务器数据 + * @returns {Object} - 表单数据 + * @private + */ + static _convertFromServerFormat(serverData) { + const formData = { + assets: {}, + basicInfo: {} + }; + + // 转换资产信息(数字值转中文选项) + if (serverData.house && ASSET_CONFIG.REVERSE_MAPPING.house[serverData.house]) { + formData.assets.house = ASSET_CONFIG.REVERSE_MAPPING.house[serverData.house]; + } + if (serverData.vehicle && ASSET_CONFIG.REVERSE_MAPPING.car[serverData.vehicle]) { + formData.assets.car = ASSET_CONFIG.REVERSE_MAPPING.car[serverData.vehicle]; + } + if (serverData.fund && ASSET_CONFIG.REVERSE_MAPPING.fund[serverData.fund]) { + formData.assets.fund = ASSET_CONFIG.REVERSE_MAPPING.fund[serverData.fund]; + } + if (serverData.social && ASSET_CONFIG.REVERSE_MAPPING.social[serverData.social]) { + formData.assets.social = ASSET_CONFIG.REVERSE_MAPPING.social[serverData.social]; + } + if (serverData.credit && ASSET_CONFIG.REVERSE_MAPPING.credit[serverData.credit]) { + formData.assets.credit = ASSET_CONFIG.REVERSE_MAPPING.credit[serverData.credit]; + } + if (serverData.bank && ASSET_CONFIG.REVERSE_MAPPING.bank[serverData.bank]) { + formData.assets.bank = ASSET_CONFIG.REVERSE_MAPPING.bank[serverData.bank]; + } + + // 转换基本信息 + if (serverData.realname) { + formData.basicInfo.name = serverData.realname; + } + if (serverData.idcard) { + formData.basicInfo.idCard = serverData.idcard; + } + if (serverData.city) { + formData.basicInfo.city = serverData.city; + } + + return formData; + } + + /** + * 获取 URL 中的 shortcode + * @returns {string} - shortcode + * @private + */ + static _getShortcode() { + const params = new URLSearchParams(window.location.search); + return params.get('code') || params.get('shortcode') || ''; + } + + /** + * 保存草稿数据到服务器 + * @param {Object} selectedValues - 资产信息 + * @param {Object} basicInfoValues - 基本信息Values + * @returns {Promise} - 保存结果 + */ + static async saveDraft(selectedValues, basicInfoValues) { + try { + const formData = this._buildDraftData(selectedValues, basicInfoValues); + const requestData = { + ...this._convertToServerFormat(formData), + shortcode: this._getShortcode() + }; + + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.SUBMIT_DRAFT_FORM, requestData); + + if (response.retcode === 0) { + console.log('[DraftManager] 草稿保存成功,Form ID:', FormIdGenerator.getOrCreate()); + return { + success: true, + message: '草稿保存成功', + data: response.result + }; + } else { + return { + success: false, + message: response.retinfo || '草稿保存失败' + }; + } + } catch (error) { + console.error('[DraftManager] 草稿保存出错:', error); + return { + success: false, + message: '网络错误,草稿保存失败' + }; + } + } + + /** + * 从服务器加载草稿数据 + * @returns {Promise} - 草稿数据,如果不存在或不是草稿状态则返回 null + */ + static async loadDraft() { + const formId = FormIdGenerator.getCurrent(); + if (!formId) { + console.log('[DraftManager] 没有表单ID,无法加载草稿'); + return null; + } + + try { + const requestData = { formid: formId }; + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.GET_DRAFT_FORM, requestData); + + if (response.retcode === 0 && response.result) { + const draftData = response.result; + + // 检查是否是草稿状态 + if (draftData.draftstatus === 1) { + console.log('[DraftManager] 加载草稿数据成功:', draftData); + return this._convertFromServerFormat(draftData); + } else { + console.log('[DraftManager] 表单已正式提交,不是草稿'); + return null; + } + } else { + console.log('[DraftManager] 没有找到草稿数据'); + return null; + } + } catch (error) { + console.error('[DraftManager] 加载草稿出错:', error); + return null; + } + } + + /** + * 清除草稿数据(清除表单ID,下次保存时会创建新的) + */ + static clearDraft() { + FormIdGenerator.clear(); + console.log('[DraftManager] 已清除草稿数据'); + } +} diff --git a/src/js/core/form-id.js b/src/js/core/form-id.js new file mode 100644 index 0000000..bcd92c3 --- /dev/null +++ b/src/js/core/form-id.js @@ -0,0 +1,85 @@ +/** + * 表单 ID 生成器 + * 负责生成和管理表单的唯一标识符 + */ + +import { CACHE_CONFIG } from '../config/index.js'; + +export class FormIdGenerator { + // 防止递归调用检测 + static _gettingFormId = false; + static _lastFormId = ''; + + /** + * 生成随机9位数字的表单唯一标识符 + * @returns {number} - 9位随机数字 + * @private + */ + static _generateRandomId() { + return Math.floor(Math.random() * 900000000) + 100000000; // 生成100000000-999999999之间的9位数字 + } + + /** + * 获取或生成表单唯一ID + * @returns {string} - 表单ID字符串 + */ + static getOrCreate() { + // 防止递归调用检测 + if (this._gettingFormId) { + console.error('[FormIdGenerator] 检测到递归调用 getOrCreateFormId', new Error().stack); + return this._lastFormId || ''; + } + this._gettingFormId = true; + + try { + console.log('[FormIdGenerator] getOrCreate 被调用'); + let formId = localStorage.getItem(CACHE_CONFIG.KEYS.FORM_ID); + console.log('[FormIdGenerator] 从 localStorage 获取的 formId:', formId); + + if (!formId) { + formId = this._generateRandomId().toString(); + localStorage.setItem(CACHE_CONFIG.KEYS.FORM_ID, formId); + console.log('[FormIdGenerator] 生成新的表单ID:', formId); + } + + this._lastFormId = formId; + return formId; + } catch (error) { + console.error('[FormIdGenerator] getOrCreate 出错:', error); + return ''; + } finally { + this._gettingFormId = false; + } + } + + /** + * 清除表单ID + */ + static clear() { + localStorage.removeItem(CACHE_CONFIG.KEYS.FORM_ID); + this._lastFormId = ''; + console.log('[FormIdGenerator] 已清除表单ID'); + } + + /** + * 获取当前表单ID(不生成新的) + * @returns {string|null} - 当前表单ID,如果不存在则返回 null + */ + static getCurrent() { + return localStorage.getItem(CACHE_CONFIG.KEYS.FORM_ID); + } + + /** + * 设置表单ID + * @param {string} formId - 表单ID + */ + static set(formId) { + if (!formId) { + console.warn('[FormIdGenerator] 尝试设置空的表单ID'); + return; + } + localStorage.setItem(CACHE_CONFIG.KEYS.FORM_ID, formId); + this._lastFormId = formId; + console.log('[FormIdGenerator] 已设置表单ID:', formId); + } +} diff --git a/src/js/core/index.js b/src/js/core/index.js new file mode 100644 index 0000000..e936791 --- /dev/null +++ b/src/js/core/index.js @@ -0,0 +1,8 @@ +/** + * 核心模块统一导出 + */ + +export { ApiClient } from './api.js'; +export { UserCache } from './user-cache.js'; +export { FormIdGenerator } from './form-id.js'; +export { DraftManager } from './draft-manager.js'; diff --git a/src/js/core/user-cache.js b/src/js/core/user-cache.js new file mode 100644 index 0000000..56d22cd --- /dev/null +++ b/src/js/core/user-cache.js @@ -0,0 +1,88 @@ +/** + * 用户缓存管理 + * 负责用户登录状态的本地存储和管理 + */ + +import { CACHE_CONFIG } from '../config/index.js'; + +export class UserCache { + /** + * 保存用户登录状态 + * @param {Object} userData - 用户数据 + * @param {string} userData.customerid - 客户ID + * @param {string} userData.mobile - 手机号 + * @param {string} userData.sessionid - 会话ID + * @param {string} userData.loginPhone - 登录时使用的手机号 + * @param {Object} userData.formData - 表单数据 + */ + static saveUserSession(userData) { + const sessionData = { + // 兼容 customerid 和 customerId 两种字段名 + customerid: userData.customerid || userData.customerId, + mobile: userData.mobile, + // 兼容 sessionid 和 sessionId 两种字段名 + sessionid: userData.sessionid || userData.sessionId, + loginTime: Date.now(), + loginPhone: userData.loginPhone || '', // 保存登录时使用的手机号 + formData: userData.formData || null // 保存表单数据 + }; + + console.log('[UserCache] 保存登录状态:', sessionData); + localStorage.setItem(CACHE_CONFIG.KEYS.USER_SESSION, JSON.stringify(sessionData)); + } + + /** + * 获取用户登录状态 + * @returns {Object|null} - 用户会话数据,如果不存在或已过期则返回 null + */ + static getUserSession() { + try { + const sessionStr = localStorage.getItem(CACHE_CONFIG.KEYS.USER_SESSION); + if (!sessionStr) return null; + + const sessionData = JSON.parse(sessionStr); + const now = Date.now(); + + // 检查缓存是否过期 + if (now - sessionData.loginTime > CACHE_CONFIG.USER_SESSION_DURATION) { + this.clearUserSession(); + return null; + } + + return sessionData; + } catch (error) { + console.error('[UserCache] 获取用户缓存失败:', error); + this.clearUserSession(); + return null; + } + } + + /** + * 清除用户登录状态 + */ + static clearUserSession() { + localStorage.removeItem(CACHE_CONFIG.KEYS.USER_SESSION); + console.log('[UserCache] 已清除用户登录状态'); + } + + /** + * 检查用户是否已登录 + * @returns {boolean} - 是否已登录 + */ + static isLoggedIn() { + const session = this.getUserSession(); + return session !== null && (session.sessionid || session.loginPhone); + } + + /** + * 获取脱敏的手机号 + * @returns {string|null} - 脱敏后的手机号 + */ + static getMaskedPhone() { + const session = this.getUserSession(); + if (!session || !session.loginPhone) return null; + + const phone = session.loginPhone; + return phone.substring(0, 3) + '****' + phone.substring(7); + } +} diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..089cc0b --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,79 @@ +/** + * 应用主入口文件 + * 根据当前页面自动初始化相应的页面逻辑 + */ + +import { IndexPage, BasicInfoPage } from './pages/index.js'; + +/** + * 应用类 + */ +class App { + constructor() { + this.currentPage = null; + } + + /** + * 初始化应用 + */ + init() { + // 等待 DOM 完全准备好 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.setup()); + } else { + // DOM 已经准备好,使用 setTimeout 确保在下一个事件循环中执行 + setTimeout(() => this.setup(), 0); + } + } + + /** + * 设置应用 + */ + setup() { + // 检测当前页面并初始化对应的页面逻辑 + if (this.isIndexPage()) { + console.log('[App] 初始化主页面'); + this.currentPage = new IndexPage(); + } else if (this.isBasicInfoPage()) { + console.log('[App] 初始化基本信息页面'); + this.currentPage = new BasicInfoPage(); + } else { + console.warn('[App] 未知的页面类型'); + } + } + + /** + * 判断是否为主页面 + * @returns {boolean} + */ + isIndexPage() { + return document.getElementById('loanAmount') !== null; + } + + /** + * 判断是否为基本信息页面 + * @returns {boolean} + */ + isBasicInfoPage() { + return document.getElementById('assetList') !== null; + } + + /** + * 销毁应用 + */ + destroy() { + if (this.currentPage) { + this.currentPage.destroy(); + this.currentPage = null; + } + } +} + +// 创建应用实例并初始化 +const app = new App(); +app.init(); + +// 将应用实例暴露到全局,方便调试 +window.__app = app; + +console.log('[App] 应用入口已加载'); diff --git a/src/js/pages/basic-info.page.js b/src/js/pages/basic-info.page.js new file mode 100644 index 0000000..e5ad0ef --- /dev/null +++ b/src/js/pages/basic-info.page.js @@ -0,0 +1,651 @@ +/** + * 基本信息填写页面逻辑 + * 负责资产信息和基本信息的填写和提交 + */ + +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 { showToast } from '../ui/toast.js'; + +export class BasicInfoPage { + constructor() { + // 表单数据 + this.selectedValues = {}; // 资产信息 + this.basicInfoValues = {}; // 基本信息 + + // 当前步骤(渐进式显示) + this.currentStep = 0; + + // 提交状态锁 + this.isSubmitting = false; + this.isSavingDraft = false; + this.autoSaveTimer = null; + + // 组件实例 + this.cityPicker = null; + this.agreementModal = null; + + this.init(); + } + + /** + * 初始化页面 + */ + init() { + // 等待 DOM 加载完成 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.setup()); + } else { + this.setup(); + } + } + + /** + * 设置页面 + */ + async setup() { + // 先检查必要的页面元素是否存在 + if (!document.getElementById('assetList')) { + console.warn('[BasicInfoPage] 不是基本信息页面,跳过初始化'); + return; + } + + // 检查登录态 + if (!UserCache.isLoggedIn()) { + showToast('请先登录'); + setTimeout(() => { + window.location.href = 'index.html' + window.location.search; + }, 1500); + return; + } + + this.initElements(); + this.initComponents(); + this.renderForm(); + this.bindEvents(); + this.updateProgress(); + + // 尝试加载草稿数据 + const draftData = await DraftManager.loadDraft(); + if (draftData) { + this.restoreDraftData(draftData); + showToast('已恢复草稿数据'); + } + } + + /** + * 初始化 DOM 元素引用 + */ + initElements() { + this.elements = { + progressFill: document.getElementById('progressFill'), + progressText: document.getElementById('progressText'), + completedCount: document.getElementById('completedCount'), + topMoney: document.getElementById('topMoney'), + assetList: document.getElementById('assetList'), + submitBtn: document.getElementById('submitBtn'), + basicInfoSection: document.getElementById('basicInfoSection'), + basicInfoList: document.getElementById('basicInfoList'), + basicInfoCompletedCount: document.getElementById('basicInfoCompletedCount'), + agreementCheckbox: document.getElementById('agreementCheckbox') + }; + } + + /** + * 初始化组件 + */ + initComponents() { + // 城市选择器 + this.cityPicker = new CityPicker({ + modalId: 'cityPickerModal', + provinceColumnId: 'provinceColumn', + cityColumnId: 'cityColumn', + cancelBtnId: 'cityCancelBtn', + confirmBtnId: 'cityConfirmBtn', + onConfirm: (result) => { + this.handleCityConfirm(result); + } + }); + + // 协议提示模态框 + this.agreementModal = new Modal({ + modalId: 'agreementModal', + onConfirm: () => { + if (this.elements.agreementCheckbox) { + this.elements.agreementCheckbox.checked = true; + } + this.agreementModal.hide(); + this.handleSubmit(); + } + }); + } + + /** + * 渲染表单 + */ + renderForm() { + this.renderAssetItems(); + this.renderBasicInfoItems(); + this.startProgressiveReveal(); + } + + /** + * 渲染资产选项 + */ + renderAssetItems() { + this.elements.assetList.innerHTML = ''; + + ASSET_CONFIG.ITEMS.forEach((item, index) => { + const assetItem = document.createElement('div'); + assetItem.className = 'asset-item'; + assetItem.id = `asset-${item.id}`; + assetItem.dataset.index = index; + + // 初始状态:只有第一项显示 + if (index > 0) { + assetItem.style.display = 'none'; + } + + const selectedValue = this.selectedValues[item.id] || ''; + const isCollapsed = !!selectedValue; + const iconSrc = selectedValue ? './static/image/dropdown-right.png' : './static/image/dropdown-down.png'; + + assetItem.innerHTML = ` +
+
${item.name}:
+
+ ${selectedValue || '请选择'} + ${selectedValue ? '展开' : '收起'} +
+
+
+ ${item.options.map(option => ` + + `).join('')} +
+ `; + + this.elements.assetList.appendChild(assetItem); + }); + + // 绑定资产选项事件 + this.bindAssetItemEvents(); + } + + /** + * 绑定资产选项事件 + */ + bindAssetItemEvents() { + // 选项按钮点击 + document.querySelectorAll('.option-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const itemId = btn.dataset.item; + const value = btn.dataset.value; + this.selectAssetOption(itemId, value); + }); + }); + + // 头部点击(折叠/展开) + document.querySelectorAll('.item-top').forEach(top => { + top.addEventListener('click', () => { + const item = top.closest('.asset-item'); + if (!item || item.style.display === 'none') return; + + const isCollapsed = item.classList.contains('collapsed'); + const itemId = item.id.replace('asset-', ''); + const iconEl = document.getElementById(`icon-${itemId}`); + + if (isCollapsed) { + item.classList.remove('collapsed'); + if (iconEl) { + iconEl.src = './static/image/dropdown-down.png'; + iconEl.alt = '收起'; + } + } else { + item.classList.add('collapsed'); + if (iconEl) { + iconEl.src = './static/image/dropdown-right.png'; + iconEl.alt = '展开'; + } + } + }); + }); + } + + /** + * 选择资产选项 + * @param {string} itemId - 资产项ID + * @param {string} value - 选项值 + */ + selectAssetOption(itemId, value) { + this.selectedValues[itemId] = value; + + // 更新 UI + const valueEl = document.getElementById(`value-${itemId}`); + const options = document.querySelectorAll(`#options-${itemId} .option-btn`); + const currentItem = document.getElementById(`asset-${itemId}`); + const iconEl = document.getElementById(`icon-${itemId}`); + + valueEl.textContent = value; + valueEl.classList.add('selected'); + + if (iconEl) { + iconEl.src = './static/image/dropdown-right.png'; + iconEl.alt = '展开'; + } + + options.forEach(btn => { + btn.classList.toggle('selected', btn.dataset.value === value); + }); + + // 折叠已选择的项 + if (currentItem) { + currentItem.classList.add('collapsed'); + } + + // 更新进度 + this.updateProgress(); + + // 检查提交按钮状态 + this.checkSubmitButton(); + + // 自动保存草稿 + this.autoSaveDraft(); + + // 渐进式显示下一项 + const currentIndex = ASSET_CONFIG.ITEMS.findIndex(item => item.id === itemId); + if (currentIndex === this.currentStep && this.currentStep < ASSET_CONFIG.ITEMS.length - 1) { + setTimeout(() => { + this.revealNextItem(); + }, 400); + } else if (currentIndex === ASSET_CONFIG.ITEMS.length - 1) { + // 资产信息全部完成,显示基本信息区域 + setTimeout(() => { + this.showBasicInfoSection(); + }, 400); + } + } + + /** + * 渲染基本信息字段 + */ + renderBasicInfoItems() { + this.elements.basicInfoList.innerHTML = ''; + + BASIC_INFO_CONFIG.ITEMS.forEach((item) => { + const infoItem = document.createElement('div'); + infoItem.className = 'basic-info-item-row'; + infoItem.id = `basic-info-${item.id}`; + + const selectedValue = this.basicInfoValues[item.id] || ''; + + if (item.type === 'input') { + infoItem.innerHTML = ` +
+
${item.name}:
+ +
+ + `; + } else if (item.id === 'city') { + const iconSrc = selectedValue ? './static/image/dropdown-right.png' : './static/image/dropdown-down.png'; + infoItem.innerHTML = ` +
+
${item.name}:
+
+ ${selectedValue || item.placeholder} + ${selectedValue ? '展开' : '收起'} +
+
+ `; + + // 绑定城市选择 + const header = infoItem.querySelector(`#basic-header-${item.id}`); + header.addEventListener('click', () => { + this.cityPicker.open(this.basicInfoValues[item.id]); + }); + } + + this.elements.basicInfoList.appendChild(infoItem); + + // 绑定输入事件 + if (item.type === 'input') { + const input = infoItem.querySelector(`#basic-input-${item.id}`); + const errorEl = infoItem.querySelector(`#error-${item.id}`); + + input.addEventListener('input', () => { + this.basicInfoValues[item.id] = input.value.trim(); + this.updateBasicInfoProgress(); + this.checkSubmitButton(); + + // 清除错误提示 + input.classList.remove('error'); + if (errorEl) { + errorEl.style.display = 'none'; + } + }); + } + }); + } + + /** + * 绑定页面事件 + */ + bindEvents() { + // 提交按钮 + this.elements.submitBtn.addEventListener('click', () => this.handleSubmit()); + } + + /** + * 处理城市选择确认 + * @param {Object} result - 选择结果 + */ + handleCityConfirm(result) { + this.basicInfoValues.city = result.value; + const valueEl = document.getElementById('basic-value-city'); + + if (valueEl) { + valueEl.classList.add('selected'); + const textEl = valueEl.querySelector('.item-value-text'); + if (textEl) { + textEl.textContent = result.value; + } + + const iconEl = document.getElementById('basic-icon-city'); + if (iconEl) { + iconEl.src = './static/image/dropdown-right.png'; + iconEl.alt = '展开'; + } + } + + this.updateBasicInfoProgress(); + this.checkSubmitButton(); + this.autoSaveDraft(); + } + + /** + * 渐进式显示 + */ + startProgressiveReveal() { + this.revealItem(0); + this.currentStep = 0; + } + + /** + * 显示下一项 + */ + revealNextItem() { + if (this.currentStep < ASSET_CONFIG.ITEMS.length - 1) { + this.currentStep++; + this.revealItem(this.currentStep); + } + } + + /** + * 显示指定项 + * @param {number} index - 索引 + */ + revealItem(index) { + const item = document.getElementById(`asset-${ASSET_CONFIG.ITEMS[index].id}`); + if (item) { + item.style.display = 'block'; + item.classList.remove('collapsed'); + requestAnimationFrame(() => { + item.classList.add('show'); + }); + } + } + + /** + * 更新资产进度 + */ + updateProgress() { + const completed = Object.keys(this.selectedValues).length; + const progress = (completed / ASSET_CONFIG.ITEMS.length) * 100; + + this.elements.progressFill.style.width = `${progress}%`; + this.elements.progressText.textContent = `${Math.round(progress)}%`; + this.elements.completedCount.textContent = completed; + + // 更新金额 + const money = ASSET_CONFIG.PROGRESS_MONEY.INITIAL + + (ASSET_CONFIG.PROGRESS_MONEY.FINAL - ASSET_CONFIG.PROGRESS_MONEY.INITIAL) * (progress / 100); + this.elements.topMoney.textContent = Math.round(money).toLocaleString(); + } + + /** + * 更新基本信息进度 + */ + updateBasicInfoProgress() { + const completed = Object.keys(this.basicInfoValues).filter(key => this.basicInfoValues[key]).length; + if (this.elements.basicInfoCompletedCount) { + this.elements.basicInfoCompletedCount.textContent = completed; + } + } + + /** + * 检查提交按钮状态 + */ + checkSubmitButton() { + const assetCompleted = Object.keys(this.selectedValues).length; + this.elements.submitBtn.disabled = assetCompleted < ASSET_CONFIG.ITEMS.length; + } + + /** + * 显示基本信息区域 + */ + showBasicInfoSection() { + if (this.elements.basicInfoSection) { + this.elements.basicInfoSection.style.display = 'block'; + this.elements.basicInfoSection.classList.add('expanded'); + requestAnimationFrame(() => { + this.elements.basicInfoSection.classList.add('show'); + }); + + const bottomSection = document.getElementById('bottomSection'); + if (bottomSection) { + bottomSection.style.display = 'block'; + } + + // 滚动到基本信息区域 + setTimeout(() => { + this.elements.basicInfoSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, 300); + } + } + + /** + * 恢复草稿数据 + * @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(); + } + } + + /** + * 自动保存草稿 + */ + autoSaveDraft() { + if (this.isSubmitting || this.isSavingDraft) { + return; + } + + if (this.autoSaveTimer) { + clearTimeout(this.autoSaveTimer); + } + + this.autoSaveTimer = setTimeout(async () => { + if (this.isSubmitting || this.isSavingDraft) { + return; + } + + const assetCompleted = Object.keys(this.selectedValues).length; + const basicInfoCompleted = Object.keys(this.basicInfoValues).filter(key => this.basicInfoValues[key]).length; + + if (assetCompleted > 0 || basicInfoCompleted > 0) { + this.isSavingDraft = true; + try { + const result = await DraftManager.saveDraft(this.selectedValues, this.basicInfoValues); + if (result.success) { + console.log('[BasicInfoPage] 草稿自动保存成功'); + } + } catch (error) { + console.error('[BasicInfoPage] 草稿自动保存出错:', error); + } finally { + this.isSavingDraft = false; + } + } + }, 2000); + } + + /** + * 处理表单提交 + */ + async handleSubmit() { + // 检查是否已完成所有项 + const assetCompleted = Object.keys(this.selectedValues).length; + const basicInfoCompleted = Object.keys(this.basicInfoValues).filter(key => this.basicInfoValues[key]).length; + + if (assetCompleted < ASSET_CONFIG.ITEMS.length) { + showToast('请完成所有资产信息填写'); + return; + } + + if (basicInfoCompleted < BASIC_INFO_CONFIG.ITEMS.length) { + showToast('请完成所有基本信息填写'); + return; + } + + // 验证姓名 + const nameValidation = Validator.validateName(this.basicInfoValues.name); + if (!nameValidation.valid) { + showToast(nameValidation.message); + this.showFieldError('name', nameValidation.message); + return; + } + + // 验证身份证号 + const idCardValidation = Validator.validateIdCard(this.basicInfoValues.idCard); + if (!idCardValidation.valid) { + showToast(idCardValidation.message); + this.showFieldError('idCard', idCardValidation.message); + return; + } + + // 检查协议 + if (!this.elements.agreementCheckbox.checked) { + this.agreementModal.show(); + return; + } + + // 清除错误提示 + document.querySelectorAll('.basic-input-inline.error').forEach(el => { + el.classList.remove('error'); + }); + document.querySelectorAll('.basic-info-error').forEach(el => { + el.style.display = 'none'; + }); + + // 构建提交数据 + const submitData = { + ...this.selectedValues, + ...this.basicInfoValues + }; + + console.log('提交的数据:', submitData); + + // 禁用提交按钮 + this.elements.submitBtn.disabled = true; + this.elements.submitBtn.textContent = '提交中...'; + this.isSubmitting = true; + + try { + // 这里需要调用 FormService.submitForm() 方法 + // 由于需要转换数据格式,暂时使用 console.log + showToast('表单提交功能待实现'); + + // 提交成功后的处理: + // FormService.clearDraft(); + // showToast('信息提交成功!'); + + } catch (error) { + console.error('提交失败:', error); + showToast('提交失败,请稍后重试'); + this.elements.submitBtn.disabled = false; + this.elements.submitBtn.textContent = '下一步'; + this.isSubmitting = false; + } + } + + /** + * 显示字段错误 + * @param {string} fieldId - 字段ID + * @param {string} message - 错误消息 + */ + showFieldError(fieldId, message) { + const input = document.getElementById(`basic-input-${fieldId}`); + const errorEl = document.getElementById(`error-${fieldId}`); + + if (input) { + input.classList.add('error'); + } + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + } + + /** + * 销毁页面 + */ + destroy() { + if (this.autoSaveTimer) { + clearTimeout(this.autoSaveTimer); + } + + if (this.cityPicker) { + this.cityPicker.destroy(); + } + + if (this.agreementModal) { + this.agreementModal.destroy(); + } + } +} diff --git a/src/js/pages/index.js b/src/js/pages/index.js new file mode 100644 index 0000000..b4237f8 --- /dev/null +++ b/src/js/pages/index.js @@ -0,0 +1,6 @@ +/** + * 页面逻辑模块统一导出 + */ + +export { IndexPage } from './index.page.js'; +export { BasicInfoPage } from './basic-info.page.js'; diff --git a/src/js/pages/index.page.js b/src/js/pages/index.page.js new file mode 100644 index 0000000..f46a9b7 --- /dev/null +++ b/src/js/pages/index.page.js @@ -0,0 +1,560 @@ +/** + * 主借款申请页面逻辑 + * 负责借款申请页面的交互和业务逻辑 + */ + +import { Picker, Modal, OneClickLoginButton } from '../ui/index.js'; +import { Validator, Formatter } from '../utils/index.js'; +import { SMSService, AuthService, LoanService } from '../services/index.js'; +import { LOAN_CONFIG, PURPOSE_PICKER_CONFIG, TERM_PICKER_CONFIG, ANIMATION_CONFIG } from '../config/index.js'; +import { UserCache } from '../core/user-cache.js'; +import { showToast } from '../ui/toast.js'; + +export class IndexPage { + constructor() { + // 借款数据 + this.loanData = { + amount: LOAN_CONFIG.AMOUNT.DEFAULT, + period: LOAN_CONFIG.DEFAULT_PERIOD, + purpose: LOAN_CONFIG.DEFAULT_PURPOSE + }; + + // 组件实例 + this.purposePicker = null; + this.termPicker = null; + this.verifyCodeModal = null; + this.agreementModal = null; + this.oneClickLoginBtn = null; // 一键登录按钮 + + // 极光一键登录配置 + this.jVerifyAppId = '80570da3ef331d9de547b4f1'; // 极光AppKey + + // 倒计时定时器 + this.countdownTimer = null; + + this.init(); + } + + /** + * 初始化页面 + */ + init() { + // 等待 DOM 加载完成 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.setup()); + } else { + this.setup(); + } + } + + /** + * 设置页面 + */ + setup() { + // 先检查必要的页面元素是否存在 + if (!document.getElementById('loanAmount')) { + console.warn('[IndexPage] 不是主页面,跳过初始化'); + return; + } + + this.initElements(); + this.initPickers(); + this.initModals(); + this.initOneClickLogin(); // 初始化一键登录 + this.bindEvents(); + this.restoreUserData(); + } + + /** + * 初始化 DOM 元素引用 + */ + initElements() { + this.elements = { + loanAmount: document.getElementById('loanAmount'), + maxLoan: document.getElementById('maxLoan'), + repaymentPlan: document.getElementById('repaymentPlan'), + loanPurpose: document.getElementById('loanPurpose'), + repaymentTerm: document.getElementById('repaymentTerm'), + phoneNumber: document.getElementById('phoneNumber'), + phoneError: document.getElementById('phoneError'), + applyBtn: document.getElementById('applyBtn'), + agreementCheck: document.getElementById('agreementCheck') + }; + } + + /** + * 初始化选择器 + */ + initPickers() { + // 确保在下一个事件循环中初始化(DOM完全加载) + setTimeout(() => { + // 借款用途选择器 + if (this.elements.loanPurpose) { + this.purposePicker = new Picker({ + ...PURPOSE_PICKER_CONFIG, + displayEl: this.elements.loanPurpose, + dataKey: 'purpose', + defaultValue: this.loanData.purpose, + onConfirm: (value) => { + this.loanData.purpose = value; + } + }); + } + + // 还款期数选择器 + if (this.elements.repaymentTerm) { + this.termPicker = new Picker({ + ...TERM_PICKER_CONFIG, + displayEl: this.elements.repaymentTerm, + dataKey: 'period', + defaultValue: this.loanData.period, + formatDisplay: Formatter.formatRepaymentPeriod, + onConfirm: (value) => { + this.loanData.period = value; + this.updateRepaymentPlan(); + } + }); + } + }, 100); + } + + /** + * 初始化模态框 + */ + initModals() { + // 验证码模态框 + this.verifyCodeModal = new Modal({ + modalId: 'verifyCodeModal', + onConfirm: () => this.handleVerifyCodeConfirm(), + closeOnOverlay: false + }); + + // 协议提示模态框 + this.agreementModal = new Modal({ + modalId: 'agreementModal', + onConfirm: () => this.handleAgreementConfirm() + }); + } + + /** + * 绑定事件 + */ + bindEvents() { + // 借款金额输入 + this.elements.loanAmount.addEventListener('input', () => { + const amount = Math.min( + parseInt(this.elements.loanAmount.value) || 0, + LOAN_CONFIG.AMOUNT.MAX + ); + this.elements.loanAmount.value = amount; + this.loanData.amount = amount; + this.updateRepaymentPlan(); + }); + + // 全部借出按钮 + this.elements.maxLoan.addEventListener('click', () => { + this.elements.loanAmount.value = LOAN_CONFIG.AMOUNT.MAX; + this.loanData.amount = LOAN_CONFIG.AMOUNT.MAX; + this.updateRepaymentPlan(); + }); + + // 申请按钮 + this.elements.applyBtn.addEventListener('click', () => this.handleApply()); + + // 手机号输入(清除错误提示) + this.elements.phoneNumber.addEventListener('input', () => { + if (!AuthService.isLoggedIn() && this.elements.phoneError.style.display !== 'none') { + this.elements.phoneError.style.display = 'none'; + this.elements.phoneNumber.classList.remove('error'); + } + }); + } + + /** + * 恢复用户数据 + */ + restoreUserData() { + const userSession = UserCache.getUserSession(); + + if (userSession && userSession.loginPhone) { + // 用户已登录,显示脱敏手机号 + this.elements.phoneNumber.value = UserCache.getMaskedPhone(); + this.elements.phoneNumber.readOnly = true; + this.elements.phoneNumber.style.color = '#666'; + + // 恢复表单数据 + if (userSession.formData) { + if (userSession.formData.loanamount) { + this.elements.loanAmount.value = userSession.formData.loanamount; + this.loanData.amount = userSession.formData.loanamount; + } + if (userSession.formData.repaymentperiod) { + this.termPicker.setValue(userSession.formData.repaymentperiod); + this.loanData.period = userSession.formData.repaymentperiod; + } + if (userSession.formData.loanpurpose) { + this.purposePicker.setValue(userSession.formData.loanpurpose); + this.loanData.purpose = userSession.formData.loanpurpose; + } + } + + // 显示登录状态提示 + this.elements.phoneError.textContent = '已登录'; + this.elements.phoneError.style.display = 'block'; + this.elements.phoneError.style.color = '#3474fe'; + + // 重新计算还款计划 + this.updateRepaymentPlan(); + } else { + // 未登录,设置初始值 + this.elements.loanAmount.value = this.loanData.amount; + this.elements.maxLoan.textContent = `全部借出${LOAN_CONFIG.AMOUNT.MAX}`; + this.updateRepaymentPlan(); + } + } + + /** + * 更新还款计划 + */ + updateRepaymentPlan() { + const plan = LoanService.calculateRepaymentPlan( + this.loanData.amount, + this.loanData.period + ); + this.elements.repaymentPlan.textContent = LoanService.formatRepaymentPlan(plan); + } + + /** + * 处理申请按钮点击 + */ + handleApply() { + // 检查是否已登录 + if (AuthService.isLoggedIn()) { + // 已登录,检查协议 + if (!this.elements.agreementCheck.checked) { + this.agreementModal.show(); + return; + } + + // 直接跳转到基本信息页面 + this.navigateToBasicInfo(); + return; + } + + // 未登录,验证手机号 + const phone = this.elements.phoneNumber.value.trim(); + const validation = Validator.validatePhone(phone); + + if (!validation.valid) { + this.elements.phoneError.textContent = validation.message; + this.elements.phoneError.style.display = 'block'; + this.elements.phoneError.style.color = '#ff4d4f'; + this.elements.phoneNumber.classList.add('error'); + return; + } + + // 清除错误提示 + this.elements.phoneError.style.display = 'none'; + this.elements.phoneNumber.classList.remove('error'); + + // 检查协议 + if (!this.elements.agreementCheck.checked) { + this.agreementModal.show(); + return; + } + + // 显示验证码模态框 + this.showVerifyCodeModal(phone); + } + + /** + * 显示验证码模态框 + * @param {string} phone - 手机号 + */ + async showVerifyCodeModal(phone) { + this.currentVerifyPhone = phone; + + const tipEl = document.getElementById('verifyCodeTip'); + const inputEl = document.getElementById('verifyCodeInput'); + const countdownEl = document.getElementById('verifyCodeCountdown'); + const errorEl = document.getElementById('verifyCodeError'); + + // 格式化手机号显示 + const formattedPhone = Formatter.maskPhone(phone); + tipEl.textContent = '发送中...'; + + // 重置状态 + inputEl.value = ''; + errorEl.style.display = 'none'; + inputEl.classList.remove('error'); + + // 显示模态框 + this.verifyCodeModal.show(); + + // 发送短信验证码 + const sendResult = await SMSService.send(phone); + + if (sendResult.success) { + tipEl.textContent = `已发送至:${formattedPhone}`; + this.startCountdown(countdownEl); + } else { + tipEl.textContent = `发送失败:${formattedPhone}`; + errorEl.textContent = sendResult.message; + errorEl.style.display = 'block'; + countdownEl.textContent = '重新发送'; + countdownEl.style.color = '#3474fe'; + countdownEl.style.cursor = 'pointer'; + countdownEl.onclick = () => this.resendSMS(countdownEl); + } + } + + /** + * 启动倒计时 + * @param {HTMLElement} countdownEl - 倒计时元素 + */ + startCountdown(countdownEl) { + let time = 59; + countdownEl.textContent = `${time}s`; + countdownEl.style.color = '#999'; + countdownEl.style.cursor = 'default'; + countdownEl.onclick = null; + + this.countdownTimer = setInterval(() => { + time--; + if (time > 0) { + countdownEl.textContent = `${time}s`; + } else { + countdownEl.textContent = '重新发送'; + countdownEl.style.color = '#3474fe'; + countdownEl.style.cursor = 'pointer'; + countdownEl.onclick = () => this.resendSMS(countdownEl); + clearInterval(this.countdownTimer); + } + }, 1000); + } + + /** + * 重新发送短信 + * @param {HTMLElement} countdownEl - 倒计时元素 + */ + async resendSMS(countdownEl) { + const tipEl = document.getElementById('verifyCodeTip'); + const errorEl = document.getElementById('verifyCodeError'); + + countdownEl.textContent = '发送中...'; + countdownEl.style.color = '#999'; + countdownEl.style.cursor = 'default'; + countdownEl.onclick = null; + errorEl.style.display = 'none'; + + const sendResult = await SMSService.send(this.currentVerifyPhone); + + if (sendResult.success) { + this.startCountdown(countdownEl); + } else { + errorEl.textContent = sendResult.message; + errorEl.style.display = 'block'; + countdownEl.textContent = '重新发送'; + countdownEl.style.color = '#3474fe'; + countdownEl.style.cursor = 'pointer'; + countdownEl.onclick = () => this.resendSMS(countdownEl); + } + } + + /** + * 处理验证码确认 + */ + async handleVerifyCodeConfirm() { + const inputEl = document.getElementById('verifyCodeInput'); + const errorEl = document.getElementById('verifyCodeError'); + const code = inputEl.value.trim(); + + // 验证码验证 + const validation = Validator.validateSmsCode(code); + if (!validation.valid) { + errorEl.textContent = validation.message; + errorEl.style.display = 'block'; + inputEl.classList.add('error'); + return; + } + + // 显示验证中状态 + this.verifyCodeModal.setConfirmButton(true, '验证中...'); + errorEl.style.display = 'none'; + + // 验证短信验证码 + const verifyResult = await SMSService.verify(this.currentVerifyPhone, code); + + if (!verifyResult.success) { + errorEl.textContent = verifyResult.message; + errorEl.style.display = 'block'; + inputEl.classList.add('error'); + this.verifyCodeModal.setConfirmButton(false, '确定'); + return; + } + + // 验证成功,进行用户注册/登录 + this.verifyCodeModal.setConfirmButton(true, '登录中...'); + + const registerResult = await AuthService.registerOrLogin( + this.currentVerifyPhone, + this.loanData + ); + + if (registerResult.success) { + // 登录成功,跳转到基本信息页面 + this.verifyCodeModal.hide(); + this.navigateToBasicInfo(); + } else { + errorEl.textContent = registerResult.message; + errorEl.style.display = 'block'; + this.verifyCodeModal.setConfirmButton(false, '确定'); + } + } + + /** + * 处理协议确认 + */ + handleAgreementConfirm() { + this.elements.agreementCheck.checked = true; + this.agreementModal.hide(); + + if (AuthService.isLoggedIn()) { + this.navigateToBasicInfo(); + } else { + const phone = this.elements.phoneNumber.value.trim(); + const validation = Validator.validatePhone(phone); + if (validation.valid) { + this.showVerifyCodeModal(phone); + } + } + } + + /** + * 跳转到基本信息页面 + */ + navigateToBasicInfo() { + window.location.href = 'basic-info.html' + window.location.search; + } + + /** + * 初始化一键登录按钮 + */ + initOneClickLogin() { + // 如果没有配置极光应用ID,跳过初始化 + if (!this.jVerifyAppId) { + console.log('[IndexPage] 未配置极光应用ID,跳过一键登录'); + return; + } + + // 检查用户是否已登录 + const userSession = UserCache.getUserSession(); + if (userSession && userSession.loginPhone) { + console.log('[IndexPage] 用户已登录,不显示一键登录'); + return; + } + + try { + this.oneClickLoginBtn = new OneClickLoginButton({ + containerId: 'oneClickLoginWrapper', + appId: this.jVerifyAppId, + buttonText: '一键登录', + onSuccess: (phone) => this.handleOneClickLoginSuccess(phone), + onFallback: () => this.handleOneClickLoginFallback() + }); + } catch (error) { + console.error('[IndexPage] 初始化一键登录失败:', error); + } + } + + /** + * 处理一键登录成功 + * @param {string} phone - 手机号 + */ + async handleOneClickLoginSuccess(phone) { + showToast('一键登录成功!'); + + try { + // 使用一键登录的手机号进行用户注册/登录 + const registerResult = await AuthService.registerOrLogin(phone, this.loanData); + + if (registerResult.success) { + // 登录成功,跳转到基本信息页面 + showToast('登录成功!'); + setTimeout(() => { + this.navigateToBasicInfo(); + }, 500); + } else { + // 注册/登录失败,降级到短信验证 + showToast(registerResult.message); + this.showPhoneNumberInput(phone); // 显示手机号输入框 + } + } catch (error) { + console.error('[IndexPage] 一键登录后注册失败:', error); + showToast('登录失败,请使用短信验证码登录'); + this.handleOneClickLoginFallback(); + } + } + + /** + * 处理一键登录降级(切换到短信验证码) + */ + handleOneClickLoginFallback() { + console.log('[IndexPage] 一键登录降级到短信验证码'); + // 隐藏一键登录按钮(已由组件自动处理) + // 显示手机号输入框和立即申请按钮 + this.showPhoneNumberInput(); + } + + /** + * 显示手机号输入区域 + * @param {string} defaultPhone - 默认手机号(可选) + */ + showPhoneNumberInput(defaultPhone = '') { + const phoneInput = this.elements.phoneNumber; + const phoneWrapper = phoneInput?.closest('.phone-input-wrapper'); + const buttonWrapper = document.querySelector('.button-wrapper'); + + // 只在元素被隐藏时才设置 display,避免布局闪烁 + if (phoneWrapper && phoneWrapper.style.display === 'none') { + phoneWrapper.style.display = 'block'; + } + if (buttonWrapper && buttonWrapper.style.display === 'none') { + buttonWrapper.style.display = 'block'; + } + + // 如果提供了默认手机号,自动填充 + if (defaultPhone && phoneInput) { + phoneInput.value = defaultPhone; + } + } + + /** + * 销毁页面 + */ + destroy() { + if (this.countdownTimer) { + clearInterval(this.countdownTimer); + } + + if (this.purposePicker) { + this.purposePicker.destroy(); + } + + if (this.termPicker) { + this.termPicker.destroy(); + } + + if (this.verifyCodeModal) { + this.verifyCodeModal.destroy(); + } + + if (this.agreementModal) { + this.agreementModal.destroy(); + } + + if (this.oneClickLoginBtn) { + this.oneClickLoginBtn.destroy(); + } + } +} diff --git a/src/js/services/auth.service.js b/src/js/services/auth.service.js new file mode 100644 index 0000000..cd6b345 --- /dev/null +++ b/src/js/services/auth.service.js @@ -0,0 +1,90 @@ +/** + * 认证服务 + * 负责用户注册、登录等认证相关功能 + */ + +import { ApiClient } from '../core/api.js'; +import { UserCache } from '../core/user-cache.js'; +import { API_CONFIG } from '../config/index.js'; + +export class AuthService { + /** + * 客户注册或登录 + * @param {string} phone - 手机号 + * @param {Object} loanData - 借款数据 + * @param {number} loanData.loanamount - 借款金额 + * @param {number} loanData.repaymentperiod - 还款期数 + * @param {string} loanData.loanpurpose - 借款用途 + * @returns {Promise<{success: boolean, message: string, data: Object}>} - 注册/登录结果 + */ + static async registerOrLogin(phone, loanData) { + try { + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.CUSTOMER_REGISTER, { + mobile: phone, + loanamount: loanData.loanamount, + repaymentperiod: loanData.repaymentperiod, + loanpurpose: loanData.loanpurpose + }); + + if (response.retcode === 0) { + console.log('[AuthService] 后端返回数据:', response.result); + + // 保存用户登录状态到缓存 + const userData = { + ...response.result, + customerid: response.result.customerid || response.result.customerId, + loginPhone: phone, + sessionid: response.result.sessionid || response.result.sessionId, + mobile: response.result.mobile, + formData: loanData + }; + + console.log('[AuthService] 保存用户数据:', userData); + UserCache.saveUserSession(userData); + + return { + success: true, + message: response.retinfo || '登录成功', + data: response.result + }; + } else { + return { success: false, message: response.retinfo }; + } + } catch (error) { + console.error('[AuthService] 注册/登录失败:', error); + return { success: false, message: error.message || '网络错误,请稍后重试' }; + } + } + + /** + * 检查用户登录状态 + * @returns {boolean} - 是否已登录 + */ + static isLoggedIn() { + return UserCache.isLoggedIn(); + } + + /** + * 获取当前用户信息 + * @returns {Object|null} - 用户信息 + */ + static getCurrentUser() { + return UserCache.getUserSession(); + } + + /** + * 退出登录 + */ + static logout() { + UserCache.clearUserSession(); + console.log('[AuthService] 用户已退出登录'); + } + + /** + * 获取脱敏手机号 + * @returns {string|null} - 脱敏后的手机号 + */ + static getMaskedPhone() { + return UserCache.getMaskedPhone(); + } +} diff --git a/src/js/services/form.service.js b/src/js/services/form.service.js new file mode 100644 index 0000000..fcfa909 --- /dev/null +++ b/src/js/services/form.service.js @@ -0,0 +1,133 @@ +/** + * 表单服务 + * 负责表单提交和数据管理 + */ + +import { ApiClient } from '../core/api.js'; +import { FormIdGenerator } from '../core/form-id.js'; +import { API_CONFIG } from '../config/index.js'; + +export class FormService { + /** + * 获取 URL 中的 shortcode + * @returns {string} - shortcode + * @private + */ + static _getShortcode() { + const params = new URLSearchParams(window.location.search); + return params.get('code') || params.get('shortcode') || ''; + } + + /** + * 提交表单数据到服务器 + * @param {Object} formData - 表单数据 + * @param {Object} formData.assets - 资产信息 + * @param {Object} formData.basicInfo - 基本信息 + * @returns {Promise<{success: boolean, message: string, data: Object}>} - 提交结果 + */ + static async submitForm(formData) { + try { + const requestData = { + shortcode: this._getShortcode(), + ...formData, + draftstatus: 0, // 正式提交状态 + formid: FormIdGenerator.getOrCreate() + }; + + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.SUBMIT_FORM, requestData); + + if (response.retcode === 0) { + // 提交成功后清除本地存储的formId + FormIdGenerator.clear(); + + console.log('[FormService] 表单提交成功'); + return { + success: true, + message: '表单提交成功', + data: response.result + }; + } else { + return { success: false, message: response.retinfo || '提交失败' }; + } + } catch (error) { + console.error('[FormService] 表单提交失败:', error); + return { success: false, message: error.message || '网络错误,请稍后重试' }; + } + } + + /** + * 保存草稿数据到服务器 + * @param {Object} formData - 表单数据 + * @returns {Promise<{success: boolean, message: string, data: Object}>} - 保存结果 + */ + static async saveDraft(formData) { + try { + const requestData = { + shortcode: this._getShortcode(), + ...formData, + draftstatus: 1, // 草稿状态 + formid: FormIdGenerator.getOrCreate() + }; + + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.SUBMIT_DRAFT_FORM, requestData); + + if (response.retcode === 0) { + console.log('[FormService] 草稿保存成功,Form ID:', FormIdGenerator.getOrCreate()); + return { + success: true, + message: '草稿保存成功', + data: response.result + }; + } else { + return { success: false, message: response.retinfo || '草稿保存失败' }; + } + } catch (error) { + console.error('[FormService] 草稿保存失败:', error); + return { success: false, message: error.message || '网络错误,请稍后重试' }; + } + } + + /** + * 从服务器加载草稿数据 + * @returns {Promise} - 草稿数据,如果不存在则返回 null + */ + static async loadDraft() { + const formId = FormIdGenerator.getCurrent(); + if (!formId) { + console.log('[FormService] 没有表单ID,无法加载草稿'); + return null; + } + + try { + const requestData = { formid: formId }; + const response = await ApiClient.xpost(API_CONFIG.ENDPOINTS.GET_DRAFT_FORM, requestData); + + if (response.retcode === 0 && response.result) { + const draftData = response.result; + + // 检查是否是草稿状态 + if (draftData.draftstatus === 1) { + console.log('[FormService] 加载草稿数据成功:', draftData); + return draftData; + } else { + console.log('[FormService] 表单已正式提交,不是草稿'); + return null; + } + } else { + console.log('[FormService] 没有找到草稿数据'); + return null; + } + } catch (error) { + console.error('[FormService] 加载草稿失败:', error); + return null; + } + } + + /** + * 清除草稿数据 + */ + static clearDraft() { + FormIdGenerator.clear(); + console.log('[FormService] 已清除草稿数据'); + } +} diff --git a/src/js/services/index.js b/src/js/services/index.js new file mode 100644 index 0000000..d823f4a --- /dev/null +++ b/src/js/services/index.js @@ -0,0 +1,9 @@ +/** + * 业务服务层统一导出 + */ + +export { SMSService } from './sms.service.js'; +export { AuthService } from './auth.service.js'; +export { LoanService } from './loan.service.js'; +export { FormService } from './form.service.js'; +export { JVerifyService, getJVerifyService } from './jverify.service.js'; diff --git a/src/js/services/jverify.service.js b/src/js/services/jverify.service.js new file mode 100644 index 0000000..0291908 --- /dev/null +++ b/src/js/services/jverify.service.js @@ -0,0 +1,354 @@ +/** + * 极光一键登录服务 + * 提供极光一键登录功能,失败时自动降级到短信验证码登录 + */ + +import { ApiClient } from '../core/api.js'; +import { DEBUG_CONFIG } from '../config/index.js'; + +export class JVerifyService { + constructor() { + this.isLoaded = false; + this.isAvailable = false; + this.sdk = null; + + // 极光配置(需要从后端获取或配置) + this.config = { + appId: '', // 极光应用ID + // 后端 API 地址 + apiUrl: '/auth/jpush/login' + }; + } + + /** + * 初始化极光SDK + * @param {string} appId - 极光应用ID + * @returns {Promise} - 是否初始化成功 + */ + async init(appId) { + if (this.isLoaded) { + return this.isAvailable; + } + + this.config.appId = appId; + console.log('[JVerifyService] 初始化SDK, appId:', appId); + + try { + // 动态加载极光SDK脚本 + await this.loadSDK(); + + // 初始化极光SDK + if (window.JVerificationInterface && window.JVerificationInterface.init) { + // 调试模式配置 + const debugMode = DEBUG_CONFIG.ENABLED; + + console.log('[JVerifyService] 准备调用SDK.init, appkey:', appId); + // 初始化SDK + window.JVerificationInterface.init({ + appkey: appId, // 注意:官方文档中是 appkey(全小写),不是 appKey + debugMode: debugMode, + success: () => { + console.log('[JVerifyService] 极光SDK初始化成功'); + this.isAvailable = true; + this.isLoaded = true; + }, + fail: (error) => { + console.warn('[JVerifyService] 极光SDK初始化失败:', error); + this.isAvailable = false; + this.isLoaded = true; + } + }); + + return true; + } else { + console.warn('[JVerifyService] 极光SDK未找到'); + this.isLoaded = true; + this.isAvailable = false; + return false; + } + } catch (error) { + console.error('[JVerifyService] 加载极光SDK失败:', error); + this.isLoaded = true; + this.isAvailable = false; + return false; + } + } + + /** + * 动态加载crypto-js依赖(极光SDK必需) + * @returns {Promise} + * @private + */ + loadCryptoJS() { + return new Promise((resolve, reject) => { + // 检查是否已加载 + if (window.CryptoJS) { + console.log('[JVerifyService] CryptoJS已加载'); + resolve(); + return; + } + + const cdnUrls = [ + 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js', + 'https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js', + './src/assets/js/crypto-js.min.js' + ]; + + let currentIndex = 0; + + const tryLoadScript = () => { + if (currentIndex >= cdnUrls.length) { + reject(new Error('CryptoJS加载失败:所有CDN源均无法访问')); + return; + } + + const script = document.createElement('script'); + script.charset = 'UTF-8'; + script.src = cdnUrls[currentIndex]; + + script.onload = () => { + console.log(`[JVerifyService] CryptoJS加载成功: ${cdnUrls[currentIndex]}`); + resolve(); + }; + + script.onerror = () => { + console.warn(`[JVerifyService] CryptoJS CDN加载失败: ${cdnUrls[currentIndex]}`); + currentIndex++; + tryLoadScript(); + }; + + document.head.appendChild(script); + }; + + tryLoadScript(); + }); + } + + /** + * 动态加载极光SDK脚本(支持离线和多CDN) + * @returns {Promise} + * @private + */ + loadSDK() { + return new Promise((resolve, reject) => { + // 检查是否已加载 + if (window.JVerificationInterface) { + console.log('[JVerifyService] 极光SDK已加载'); + resolve(); + return; + } + + // 先加载crypto-js依赖 + this.loadCryptoJS().then(() => { + // CryptoJS加载成功后,继续加载JVerification SDK + this.loadJVerificationScript().then(resolve).catch(reject); + }).catch((error) => { + console.error('[JVerifyService] 加载依赖失败:', error); + reject(error); + }); + }); + } + + /** + * 加载极光JVerification脚本 + * @returns {Promise} + * @private + */ + loadJVerificationScript() { + return new Promise((resolve, reject) => { + // 检查是否已加载 + if (window.JVerificationInterface) { + console.log('[JVerifyService] 极光SDK已加载'); + resolve(); + return; + } + + // CDN列表(按优先级排序) + // 注意:广告拦截器可能会阻止 jiguang.cn 域名,建议使用离线文件 + const cdnUrls = [ + './jverification_web.js', // 本地文件(根目录,推荐) + 'https://unpkg.com/jg-jverification-cordova-plugin@latest/dist/jverification_web.js', // unpkg(通常不被拦截) + 'https://cdn.jsdelivr.net/npm/jg-jverification-cordova-plugin/dist/jverification_web.js', // jsDelivr + ]; + + let currentIndex = 0; + + const tryLoadScript = () => { + if (currentIndex >= cdnUrls.length) { + reject(new Error('所有CDN源均无法访问')); + return; + } + + const script = document.createElement('script'); + script.charset = 'UTF-8'; + script.src = cdnUrls[currentIndex]; + + script.onload = () => { + console.log(`[JVerifyService] JVerification SDK加载成功: ${cdnUrls[currentIndex]}`); + resolve(); + }; + + script.onerror = () => { + console.warn(`[JVerifyService] JVerification CDN加载失败: ${cdnUrls[currentIndex]}`); + currentIndex++; + tryLoadScript(); // 尝试下一个CDN + }; + + document.head.appendChild(script); + }; + + tryLoadScript(); + }); + } + + /** + * 检查一键登录是否可用 + * @returns {boolean} + */ + checkAvailable() { + return this.isLoaded && this.isAvailable; + } + + /** + * 获取登录Token + * @returns {Promise} - 登录Token + */ + async getToken() { + return new Promise((resolve, reject) => { + if (!this.isAvailable) { + reject(new Error('极光一键登录不可用')); + return; + } + + try { + // 调用极光SDK获取token + window.JVerificationInterface.getToken({ + success: (result) => { + console.log('[JVerifyService] 获取token成功:', result); + resolve(result.token); // 返回登录token + }, + fail: (error) => { + console.error('[JVerifyService] 获取token失败:', error); + reject(new Error(error.code + ': ' + error.content)); + } + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * 使用极光一键登录 + * @param {string} appId - 极光应用ID + * @returns {Promise<{success: boolean, phone: string, message: string}>} + */ + async login(appId) { + try { + // 1. 检查SDK是否可用 + if (!this.checkAvailable()) { + return { + success: false, + phone: null, + message: '一键登录不可用,请使用短信验证码登录' + }; + } + + // 2. 获取登录token + const token = await this.getToken(); + + // 3. 调用后端API验证 + const response = await ApiClient.post(this.config.apiUrl, { + appId: appId, + loginToken: token + }); + + if (response.retcode === 0 && response.result) { + console.log('[JVerifyService] 一键登录成功:', response.result.phoneMasked); + return { + success: true, + phone: response.result.phone, + message: '登录成功' + }; + } else { + console.error('[JVerifyService] 后端验证失败:', response.retinfo); + return { + success: false, + phone: null, + message: response.retinfo || '登录验证失败' + }; + } + } catch (error) { + console.error('[JVerifyService] 一键登录异常:', error); + + // 判断错误类型,返回友好的提示 + if (error.message.includes('不可用')) { + return { + success: false, + phone: null, + message: '一键登录不可用' + }; + } + + return { + success: false, + phone: null, + message: '登录失败,请使用短信验证码登录' + }; + } + } + + /** + * 预登录(检查运营商网络) + * @returns {Promise} + */ + async preLogin() { + return new Promise((resolve) => { + if (!this.isAvailable) { + resolve(false); + return; + } + + 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); + } + }); + } + + /** + * 重置SDK状态 + */ + reset() { + if (window.JVerificationInterface && window.JVerificationInterface.clear) { + window.JVerificationInterface.clear(); + } + this.isLoaded = false; + this.isAvailable = false; + } +} + +// 创建全局单例 +let jVerifyService = null; + +/** + * 获取极光一键登录服务实例 + * @returns {JVerifyService} + */ +export function getJVerifyService() { + if (!jVerifyService) { + jVerifyService = new JVerifyService(); + } + return jVerifyService; +} diff --git a/src/js/services/loan.service.js b/src/js/services/loan.service.js new file mode 100644 index 0000000..fa156bc --- /dev/null +++ b/src/js/services/loan.service.js @@ -0,0 +1,109 @@ +/** + * 借款服务 + * 负责借款相关计算和业务逻辑 + */ + +import { LOAN_CONFIG } from '../config/index.js'; + +export class LoanService { + /** + * 计算还款计划 + * @param {number} amount - 借款金额 + * @param {number} period - 还款期数(月) + * @param {number} interestRate - 年化利率(%) + * @returns {Object} - 还款计划 + */ + static calculateRepaymentPlan(amount, period, interestRate = LOAN_CONFIG.INTEREST_RATE.MIN) { + const monthlyPrincipal = amount / period; + const monthlyInterest = amount * (interestRate / 100) / 12; + const firstAmount = monthlyPrincipal + monthlyInterest; + + // 计算首期还款日期 + const firstDate = this.calculateFirstRepaymentDate(); + + return { + firstDate, + firstAmount: firstAmount.toFixed(2), + totalInterest: (monthlyInterest * period).toFixed(2), + totalAmount: (amount + monthlyInterest * period).toFixed(2) + }; + } + + /** + * 计算首期还款日期 + * @param {number} daysToAdd - 增加的天数(默认30天) + * @returns {string} - 格式化的日期(如:"02月05日") + */ + static calculateFirstRepaymentDate(daysToAdd = 30) { + const date = new Date(); + date.setDate(date.getDate() + daysToAdd); + + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${month}月${day}日`; + } + + /** + * 计算预期额度(根据完成度) + * @param {number} progress - 进度(0-100) + * @returns {number} - 预期额度 + */ + static calculateExpectedAmount(progress) { + const { INITIAL, FINAL } = LOAN_CONFIG.AMOUNT; + // 根据完成度线性增加 + return INITIAL + (FINAL - INITIAL) * (progress / 100); + } + + /** + * 验证借款金额 + * @param {number} amount - 借款金额 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateAmount(amount) { + if (isNaN(amount)) { + return { valid: false, message: '请输入有效的借款金额' }; + } + + if (amount < LOAN_CONFIG.AMOUNT.MIN) { + return { + valid: false, + message: `借款金额不能低于${LOAN_CONFIG.AMOUNT.MIN}元` + }; + } + + if (amount > LOAN_CONFIG.AMOUNT.MAX) { + return { + valid: false, + message: `借款金额不能超过${LOAN_CONFIG.AMOUNT.MAX}元` + }; + } + + return { valid: true }; + } + + /** + * 格式化还款计划显示 + * @param {Object} plan - 还款计划 + * @returns {string} - 格式化后的显示文本 + */ + static formatRepaymentPlan(plan) { + return `首期${plan.firstDate} 应还 ${plan.firstAmount}元`; + } + + /** + * 获取还款期数选项列表 + * @returns {Array} - 期数选项数组 + */ + static getPeriodOptions() { + return LOAN_CONFIG.PERIOD_OPTIONS; + } + + /** + * 获取借款用途选项列表 + * @returns {Array} - 用途选项数组 + */ + static getPurposeOptions() { + return LOAN_CONFIG.PURPOSES; + } +} diff --git a/src/js/services/sms.service.js b/src/js/services/sms.service.js new file mode 100644 index 0000000..0ba618c --- /dev/null +++ b/src/js/services/sms.service.js @@ -0,0 +1,78 @@ +/** + * 短信服务 + * 负责短信验证码的发送和验证 + */ + +import { ApiClient } from '../core/api.js'; +import { API_CONFIG, DEBUG_CONFIG } from '../config/index.js'; + +export class SMSService { + /** + * 发送短信验证码 + * @param {string} phone - 手机号 + * @returns {Promise<{success: boolean, message: string}>} - 发送结果 + */ + static async send(phone) { + try { + // 调试模式:不发送真实短信 + if (DEBUG_CONFIG.ENABLED) { + console.log(`[SMSService] 调试模式,跳过发送短信,请使用验证码: ${DEBUG_CONFIG.SMS_CODE}`); + return { + success: true, + message: `调试模式:请输入验证码 ${DEBUG_CONFIG.SMS_CODE}` + }; + } + + const response = await ApiClient.post(API_CONFIG.ENDPOINTS.SEND_SMS, { + phone + }); + + if (response.retcode === 0) { + return { success: true, message: response.retinfo }; + } else { + return { success: false, message: response.retinfo }; + } + } catch (error) { + console.error('[SMSService] 发送短信失败:', error); + return { success: false, message: error.message || '网络错误,请稍后重试' }; + } + } + + /** + * 验证短信验证码 + * @param {string} phone - 手机号 + * @param {string} code - 验证码 + * @returns {Promise<{success: boolean, message: string}>} - 验证结果 + */ + static async verify(phone, code) { + try { + // 调试模式:固定验证码验证 + if (DEBUG_CONFIG.ENABLED) { + if (code === DEBUG_CONFIG.SMS_CODE) { + console.log(`[SMSService] 调试模式,验证码验证成功: ${code}`); + return { success: true, message: '验证码验证成功(调试模式)' }; + } else { + console.log(`[SMSService] 调试模式,验证码错误: ${code},正确验证码: ${DEBUG_CONFIG.SMS_CODE}`); + return { + success: false, + message: `验证码错误,调试模式请使用: ${DEBUG_CONFIG.SMS_CODE}` + }; + } + } + + const response = await ApiClient.post(API_CONFIG.ENDPOINTS.VERIFY_SMS, { + phone, + code + }); + + if (response.retcode === 0) { + return { success: true, message: response.retinfo }; + } else { + return { success: false, message: response.retinfo }; + } + } catch (error) { + console.error('[SMSService] 验证短信失败:', error); + return { success: false, message: error.message || '网络错误,请稍后重试' }; + } + } +} diff --git a/src/js/ui/city-picker.js b/src/js/ui/city-picker.js new file mode 100644 index 0000000..2e066db --- /dev/null +++ b/src/js/ui/city-picker.js @@ -0,0 +1,267 @@ +/** + * 城市选择器组件 + * 提供省份和城市的联动选择功能 + */ + +import { PROVINCE_CITY_DATA } from '../config/index.js'; + +export class CityPicker { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {string} options.modalId - 模态框元素ID + * @param {string} options.provinceColumnId - 省份列元素ID + * @param {string} options.cityColumnId - 城市列元素ID + * @param {string} options.cancelBtnId - 取消按钮ID + * @param {string} options.confirmBtnId - 确认按钮ID + * @param {Function} options.onConfirm - 确认回调 + */ + constructor(options) { + this.modal = document.getElementById(options.modalId); + this.provinceColumn = document.getElementById(options.provinceColumnId); + this.cityColumn = document.getElementById(options.cityColumnId); + this.cancelBtn = document.getElementById(options.cancelBtnId); + this.confirmBtn = document.getElementById(options.confirmBtnId); + this.onConfirm = options.onConfirm || null; + + // 选中状态 + this.selectedProvince = ''; + this.selectedCity = ''; + this.tempSelectedProvince = ''; + this.tempSelectedCity = ''; + + if (!this.modal) { + console.error(`[CityPicker] 找不到模态框元素: ${options.modalId}`); + return; + } + + this.init(); + } + + /** + * 初始化 + */ + init() { + // 绑定取消按钮 + if (this.cancelBtn) { + this.cancelBtn.addEventListener('click', () => this.close()); + } + + // 绑定确认按钮 + if (this.confirmBtn) { + this.confirmBtn.addEventListener('click', () => this.confirmSelection()); + } + + // 绑定遮罩层点击 + const overlay = this.modal.querySelector('.city-picker-overlay'); + if (overlay) { + overlay.addEventListener('click', () => this.close()); + } + + // 渲染省份列表 + this.renderProvinceList(); + } + + /** + * 渲染省份列表 + */ + renderProvinceList() { + if (!this.provinceColumn) return; + + this.provinceColumn.innerHTML = ''; + const provinces = Object.keys(PROVINCE_CITY_DATA); + + provinces.forEach(province => { + const provinceItem = document.createElement('div'); + provinceItem.className = 'city-picker-item'; + provinceItem.textContent = province; + provinceItem.dataset.province = province; + + provinceItem.addEventListener('click', () => { + this.selectProvince(province); + }); + + this.provinceColumn.appendChild(provinceItem); + }); + } + + /** + * 选择省份 + * @param {string} province - 省份 + */ + selectProvince(province) { + this.tempSelectedProvince = province; + + // 更新省份选中状态 + const items = this.provinceColumn.querySelectorAll('.city-picker-item'); + items.forEach(item => { + item.classList.toggle('active', item.dataset.province === province); + }); + + // 渲染城市列表 + this.renderCityList(province); + } + + /** + * 渲染城市列表 + * @param {string} province - 省份 + */ + renderCityList(province) { + if (!this.cityColumn) return; + + this.cityColumn.innerHTML = ''; + const cities = PROVINCE_CITY_DATA[province] || []; + + cities.forEach(city => { + const cityItem = document.createElement('div'); + cityItem.className = 'city-picker-item'; + cityItem.textContent = city; + cityItem.dataset.city = city; + + cityItem.addEventListener('click', () => { + this.selectCity(city); + }); + + this.cityColumn.appendChild(cityItem); + }); + + // 如果之前选择了这个省份的城市,自动选中 + if (this.tempSelectedCity && cities.includes(this.tempSelectedCity)) { + this.selectCity(this.tempSelectedCity); + } else if (cities.length > 0) { + // 默认选择第一个城市 + this.selectCity(cities[0]); + } + } + + /** + * 选择城市 + * @param {string} city - 城市 + */ + selectCity(city) { + this.tempSelectedCity = city; + + // 更新城市选中状态 + const items = this.cityColumn.querySelectorAll('.city-picker-item'); + items.forEach(item => { + item.classList.toggle('active', item.dataset.city === city); + }); + } + + /** + * 打开选择器 + * @param {string} currentValue - 当前值(格式:"省/市") + */ + open(currentValue = '') { + // 解析当前值 + if (currentValue) { + const parts = currentValue.split('/'); + if (parts.length === 2) { + this.tempSelectedProvince = parts[0]; + this.tempSelectedCity = parts[1]; + } + } + + // 如果没有选中,默认选择第一个省份 + if (!this.tempSelectedProvince) { + const provinces = Object.keys(PROVINCE_CITY_DATA); + if (provinces.length > 0) { + this.tempSelectedProvince = provinces[0]; + } + } + + // 渲染列表 + this.renderProvinceList(); + if (this.tempSelectedProvince) { + this.selectProvince(this.tempSelectedProvince); + } + + // 显示模态框 + document.body.classList.add('modal-open'); + this.modal.classList.add('show'); + } + + /** + * 关闭选择器 + */ + close() { + if (!this.modal) return; + + this.modal.classList.remove('show'); + document.body.classList.remove('modal-open'); + + // 恢复选中状态 + this.tempSelectedProvince = this.selectedProvince; + this.tempSelectedCity = this.selectedCity; + } + + /** + * 确认选择 + */ + confirmSelection() { + if (this.tempSelectedProvince && this.tempSelectedCity) { + this.selectedProvince = this.tempSelectedProvince; + this.selectedCity = this.tempSelectedCity; + + const cityValue = `${this.tempSelectedProvince}/${this.tempSelectedCity}`; + + if (this.onConfirm) { + this.onConfirm({ + province: this.selectedProvince, + city: this.selectedCity, + value: cityValue + }); + } + + this.close(); + } + } + + /** + * 设置选中值 + * @param {string} value - 值(格式:"省/市") + */ + setValue(value) { + if (!value) return; + + const parts = value.split('/'); + if (parts.length === 2) { + this.selectedProvince = parts[0]; + this.selectedCity = parts[1]; + this.tempSelectedProvince = parts[0]; + this.tempSelectedCity = parts[1]; + } + } + + /** + * 获取选中值 + * @returns {{province: string, city: string, value: string}} - 选中值 + */ + getValue() { + return { + province: this.selectedProvince, + city: this.selectedCity, + value: `${this.selectedProvince}/${this.selectedCity}` + }; + } + + /** + * 重置选择器 + */ + reset() { + this.selectedProvince = ''; + this.selectedCity = ''; + this.tempSelectedProvince = ''; + this.tempSelectedCity = ''; + } + + /** + * 销毁选择器 + */ + destroy() { + this.close(); + this.modal = null; + this.provinceColumn = null; + this.cityColumn = null; + } +} diff --git a/src/js/ui/index.js b/src/js/ui/index.js new file mode 100644 index 0000000..c383980 --- /dev/null +++ b/src/js/ui/index.js @@ -0,0 +1,9 @@ +/** + * UI 组件统一导出 + */ + +export { Modal } from './modal.js'; +export { Picker } from './picker.js'; +export { Toast, getToast, showToast, showSuccess, showError } from './toast.js'; +export { CityPicker } from './city-picker.js'; +export { OneClickLoginButton } from './one-click-login.js'; diff --git a/src/js/ui/modal.js b/src/js/ui/modal.js new file mode 100644 index 0000000..895619a --- /dev/null +++ b/src/js/ui/modal.js @@ -0,0 +1,157 @@ +/** + * 模态框基类 + * 提供通用的模态框功能 + */ + +import { ANIMATION_CONFIG } from '../config/index.js'; + +export class Modal { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {string} options.modalId - 模态框元素ID + * @param {Function} options.onConfirm - 确认回调 + * @param {Function} options.onCancel - 取消回调 + * @param {boolean} options.closeOnOverlay - 点击遮罩层是否关闭(默认true) + */ + constructor(options) { + this.modal = document.getElementById(options.modalId); + this.onConfirm = options.onConfirm || null; + this.onCancel = options.onCancel || null; + this.closeOnOverlay = options.closeOnOverlay !== false; + this.isOpen = false; + + if (!this.modal) { + console.error(`[Modal] 找不到模态框元素: ${options.modalId}`); + return; + } + + this.init(); + } + + /** + * 初始化 + */ + init() { + // 绑定关闭按钮 + const cancelBtn = this.modal.querySelector('.modal-cancel-btn, [id$="CancelBtn"]'); + if (cancelBtn) { + cancelBtn.addEventListener('click', () => this.onCancelClick()); + } + + // 绑定确认按钮 + const confirmBtn = this.modal.querySelector('.modal-confirm-btn, [id$="ConfirmBtn"]'); + if (confirmBtn) { + confirmBtn.addEventListener('click', () => this.onConfirmClick()); + } + + // 绑定遮罩层点击 + if (this.closeOnOverlay) { + this.modal.addEventListener('click', (e) => { + if (e.target === this.modal) { + this.hide(); + } + }); + } + + // 绑定ESC键关闭 + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen) { + this.hide(); + } + }); + } + + /** + * 显示模态框 + */ + show() { + if (!this.modal) return; + + document.body.classList.add('modal-open'); + this.modal.classList.add('showing'); + this.isOpen = true; + + requestAnimationFrame(() => { + this.modal.classList.add('show'); + }); + } + + /** + * 隐藏模态框 + */ + hide() { + if (!this.modal) return; + + this.modal.classList.remove('show'); + this.isOpen = false; + + setTimeout(() => { + this.modal.classList.remove('showing'); + document.body.classList.remove('modal-open'); + }, ANIMATION_CONFIG.MODAL_DURATION); + } + + /** + * 确认按钮点击处理 + */ + onConfirmClick() { + if (this.onConfirm) { + this.onConfirm(); + } else { + this.hide(); + } + } + + /** + * 取消按钮点击处理 + */ + onCancelClick() { + if (this.onCancel) { + this.onCancel(); + } + this.hide(); + } + + /** + * 设置确认按钮状态 + * @param {boolean} disabled - 是否禁用 + * @param {string} text - 按钮文本 + */ + setConfirmButton(disabled, text) { + const confirmBtn = this.modal.querySelector('.modal-confirm-btn, [id$="ConfirmBtn"]'); + if (confirmBtn) { + confirmBtn.disabled = disabled; + if (text) { + confirmBtn.textContent = text; + } + } + } + + /** + * 重置模态框状态 + */ + reset() { + // 清除所有输入框的值 + const inputs = this.modal.querySelectorAll('input[type="text"], input[type="number"], textarea'); + inputs.forEach(input => { + input.value = ''; + input.classList.remove('error'); + }); + + // 隐藏所有错误提示 + const errors = this.modal.querySelectorAll('.error-message, .basic-info-error'); + errors.forEach(error => { + error.style.display = 'none'; + }); + } + + /** + * 销毁模态框 + */ + destroy() { + this.hide(); + // 移除事件监听器等清理工作 + this.modal = null; + } +} diff --git a/src/js/ui/one-click-login.js b/src/js/ui/one-click-login.js new file mode 100644 index 0000000..4c5cddb --- /dev/null +++ b/src/js/ui/one-click-login.js @@ -0,0 +1,207 @@ +/** + * 一键登录按钮组件 + * 提供极光一键登录按钮和降级提示 + */ + +import { getJVerifyService } from '../services/index.js'; + +export class OneClickLoginButton { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {string} options.containerId - 容器元素ID + * @param {string} options.appId - 极光应用ID + * @param {Function} options.onSuccess - 登录成功回调 (phone) => void + * @param {Function} options.onFallback - 降级回调 () => void + * @param {string} options buttonText - 按钮文本(默认"一键登录") + */ + constructor(options) { + this.container = document.getElementById(options.containerId); + this.appId = options.appId || ''; + this.onSuccess = options.onSuccess || null; + this.onFallback = options.onFallback || null; + this.buttonText = options.buttonText || '一键登录'; + this.isLoading = false; + + if (!this.container) { + console.error('[OneClickLoginButton] 找不到容器元素'); + return; + } + + this.render(); + this.init(); + } + + /** + * 初始化 + */ + async init() { + try { + // 初始化极光SDK + const jVerifyService = getJVerifyService(); + const isReady = await jVerifyService.init(this.appId); + + if (isReady && jVerifyService.checkAvailable()) { + // 检查运营商网络 + const canLogin = await jVerifyService.preLogin(); + + if (canLogin) { + // 显示一键登录按钮 + this.showButton(); + console.log('[OneClickLoginButton] 一键登录可用'); + } else { + // 运营商网络不支持,隐藏一键登录 + this.hide(); + console.log('[OneClickLoginButton] 运营商网络不支持'); + if (this.onFallback) { + this.onFallback(); + } + } + } else { + // SDK不可用,隐藏一键登录 + this.hide(); + console.log('[OneClickLoginButton] 一键登录不可用'); + if (this.onFallback) { + this.onFallback(); + } + } + } catch (error) { + console.error('[OneClickLoginButton] 初始化失败:', error); + this.hide(); + if (this.onFallback) { + this.onFallback(); + } + } + } + + /** + * 渲染按钮 + */ + render() { + this.container.innerHTML = ` + + `; + + // 绑定事件 + const btn = this.container.querySelector('#oneClickLoginBtn'); + if (btn) { + btn.addEventListener('click', () => this.handleClick()); + } + } + + /** + * 显示按钮 + */ + showButton() { + const container = document.getElementById('oneClickLoginContainer'); + if (container) { + container.style.display = 'block'; + } + // 显示 wrapper 容器(避免布局闪烁) + if (this.container) { + this.container.classList.add('show'); + } + } + + /** + * 隐藏组件 + */ + hide() { + if (this.container) { + this.container.style.display = 'none'; + this.container.classList.remove('show'); + } + } + + /** + * 处理点击事件 + */ + async handleClick() { + if (this.isLoading) { + return; // 防止重复点击 + } + + this.isLoading = true; + this.setLoading(true); + + try { + const jVerifyService = getJVerifyService(); + const result = await jVerifyService.login(this.appId); + + if (result.success) { + // 登录成功 + console.log('[OneClickLoginButton] 登录成功:', result.phone); + if (this.onSuccess) { + this.onSuccess(result.phone); + } + } else { + // 登录失败,降级到短信验证 + console.log('[OneClickLoginButton] 登录失败:', result.message); + this.showFallbackMessage(result.message); + if (this.onFallback) { + this.onFallback(); + } + } + } catch (error) { + console.error('[OneClickLoginButton] 登录异常:', error); + this.showFallbackMessage('登录失败,请使用短信验证码登录'); + if (this.onFallback) { + this.onFallback(); + } + } finally { + this.isLoading = false; + this.setLoading(false); + } + } + + /** + * 设置加载状态 + * @param {boolean} loading + */ + setLoading(loading) { + const btn = this.container.querySelector('#oneClickLoginBtn'); + if (btn) { + if (loading) { + btn.disabled = true; + btn.innerHTML = ` + + 登录中... + `; + } else { + btn.disabled = false; + btn.innerHTML = ` + 📱 + ${this.buttonText} + `; + } + } + } + + /** + * 显示降级消息 + * @param {string} message + */ + showFallbackMessage(message) { + // 可以在这里显示一个Toast提示 + console.log('[OneClickLoginButton] 降级提示:', message); + } + + /** + * 销毁组件 + */ + destroy() { + if (this.container) { + this.container.innerHTML = ''; + } + } +} diff --git a/src/js/ui/picker.js b/src/js/ui/picker.js new file mode 100644 index 0000000..2feb331 --- /dev/null +++ b/src/js/ui/picker.js @@ -0,0 +1,281 @@ +/** + * 选择器组件 + * 提供滚轮选择和点击选择功能 + */ + +import { ANIMATION_CONFIG } from '../config/index.js'; + +export class Picker { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {string} options.triggerId - 触发器元素ID + * @param {string} options.modalId - 模态框元素ID + * @param {string} options.cancelBtnId - 取消按钮ID + * @param {string} options.confirmBtnId - 确认按钮ID + * @param {string} options.optionSelector - 选项选择器 + * @param {HTMLElement} options.displayEl - 显示元素 + * @param {string} options.dataKey - 数据键名 + * @param {Function} options.onConfirm - 确认回调 + * @param {boolean} options.enableWheel - 是否启用滚轮切换(默认false) + * @param {boolean} options.useDataValue - 是否使用data-value(默认false) + * @param {Function} options.formatDisplay - 格式化显示函数 + * @param {any} options.defaultValue - 默认值 + */ + constructor(options) { + this.trigger = document.getElementById(options.triggerId); + this.modal = document.getElementById(options.modalId); + this.cancelBtn = document.getElementById(options.cancelBtnId); + this.confirmBtn = document.getElementById(options.confirmBtnId); + this.optionSelector = options.optionSelector; + this.displayEl = options.displayEl; + this.dataKey = options.dataKey; + this.onConfirm = options.onConfirm || null; + this.enableWheel = options.enableWheel || false; + this.useDataValue = options.useDataValue || false; + this.formatDisplay = options.formatDisplay || null; + this.defaultValue = options.defaultValue || null; + + this.selectedValue = this.defaultValue; + this.tempSelectedValue = this.defaultValue; + this.options = []; + this.modalBody = null; + + if (!this.trigger || !this.modal) { + console.error(`[Picker] 找不到必要的元素`); + return; + } + + this.init(); + } + + /** + * 初始化 + */ + init() { + // 获取所有选项元素 + this.options = Array.from(document.querySelectorAll(this.optionSelector)); + this.modalBody = this.modal.querySelector('.modal-body'); + + // 绑定触发器点击 + this.trigger.addEventListener('click', () => this.open()); + + // 绑定取消按钮 + if (this.cancelBtn) { + this.cancelBtn.addEventListener('click', () => this.close(true)); + } + + // 绑定确认按钮 + if (this.confirmBtn) { + this.confirmBtn.addEventListener('click', () => this.handleConfirm()); + } + + // 绑定选项点击 + this.options.forEach(option => { + option.addEventListener('click', () => this.selectOption(option)); + }); + + // 绑定遮罩层点击 + this.modal.addEventListener('click', (e) => { + if (e.target === this.modal) { + this.close(true); + } + }); + + // 绑定键盘事件 + document.addEventListener('keydown', (e) => { + if (!this.modal.classList.contains('show')) return; + + if (e.key === 'Escape') { + this.close(true); + } else if (e.key === 'Enter') { + this.handleConfirm(); + } + }); + + // 初始化滚轮导航 + if (this.enableWheel) { + this.initWheelNavigation(); + } + + // 更新初始显示 + this.updateDisplay(); + } + + /** + * 获取选项的值 + * @param {HTMLElement} option - 选项元素 + * @returns {any} - 选项值 + */ + getOptionValue(option) { + if (this.useDataValue) { + const val = option.getAttribute('data-value'); + return isNaN(val) ? val : parseInt(val); + } + return this.getOptionText(option); + } + + /** + * 获取选项的文本内容 + * @param {HTMLElement} option - 选项元素 + * @returns {string} - 选项文本 + */ + getOptionText(option) { + const textEl = option.querySelector('.modal-option-text'); + return textEl ? textEl.textContent : option.textContent.trim(); + } + + /** + * 更新选中状态 + * @param {any} value - 值 + */ + updateSelection(value = this.selectedValue) { + this.options.forEach(option => { + const optionValue = this.getOptionValue(option); + const isActive = optionValue == value; // 使用==而不是===以支持数字比较 + option.classList.toggle('active', isActive); + + if (isActive) { + setTimeout(() => { + option.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, ANIMATION_CONFIG.SCROLL_DELAY); + } + }); + } + + /** + * 更新显示 + */ + updateDisplay() { + if (this.displayEl) { + const displayValue = this.formatDisplay + ? this.formatDisplay(this.selectedValue) + : this.selectedValue; + this.displayEl.textContent = displayValue; + } + } + + /** + * 打开选择器 + */ + open() { + this.tempSelectedValue = this.selectedValue; + document.body.classList.add('modal-open'); + this.modal.classList.add('showing'); + + requestAnimationFrame(() => { + this.modal.classList.add('show'); + this.updateSelection(this.tempSelectedValue); + }); + } + + /** + * 关闭选择器 + * @param {boolean} restore - 是否恢复选中值 + */ + close(restore = false) { + if (restore) { + this.tempSelectedValue = this.selectedValue; + this.updateSelection(this.tempSelectedValue); + } + + this.modal.classList.remove('show'); + + setTimeout(() => { + this.modal.classList.remove('showing'); + document.body.classList.remove('modal-open'); + }, ANIMATION_CONFIG.MODAL_DURATION); + } + + /** + * 选择选项 + * @param {HTMLElement} option - 选项元素 + */ + selectOption(option) { + this.options.forEach(opt => opt.classList.remove('active')); + option.classList.add('active'); + this.tempSelectedValue = this.getOptionValue(option); + } + + /** + * 确认选择 + */ + handleConfirm() { + this.selectedValue = this.tempSelectedValue; + this.updateDisplay(); + + if (this.onConfirm) { + this.onConfirm(this.selectedValue); + } + + this.close(); + } + + /** + * 设置选中值 + * @param {any} value - 值 + */ + setValue(value) { + this.selectedValue = value; + this.tempSelectedValue = value; + this.updateDisplay(); + } + + /** + * 获取选中值 + * @returns {any} - 选中值 + */ + getValue() { + return this.selectedValue; + } + + /** + * 初始化滚轮导航 + */ + initWheelNavigation() { + if (!this.modalBody) return; + + let isScrolling = false; + let lastWheelTime = 0; + + this.modalBody.addEventListener('wheel', (e) => { + if (!this.modal.classList.contains('show')) return; + + e.preventDefault(); + e.stopPropagation(); + + // 节流处理 + const now = Date.now(); + if (isScrolling || now - lastWheelTime < ANIMATION_CONFIG.WHEEL_DEBOUNCE_DELAY) return; + + lastWheelTime = now; + isScrolling = true; + setTimeout(() => { isScrolling = false; }, ANIMATION_CONFIG.WHEEL_DEBOUNCE_DELAY); + + const currentIndex = this.options.findIndex(opt => opt.classList.contains('active')); + let nextIndex = currentIndex; + + if (e.deltaY > 0) { + nextIndex = Math.min(currentIndex + 1, this.options.length - 1); + } else if (e.deltaY < 0) { + nextIndex = Math.max(currentIndex - 1, 0); + } + + if (nextIndex !== currentIndex) { + this.selectOption(this.options[nextIndex]); + this.options[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }, { passive: false }); + } + + /** + * 销毁选择器 + */ + destroy() { + this.close(); + // 移除事件监听器等清理工作 + this.trigger = null; + this.modal = null; + this.options = []; + } +} diff --git a/src/js/ui/toast.js b/src/js/ui/toast.js new file mode 100644 index 0000000..738794b --- /dev/null +++ b/src/js/ui/toast.js @@ -0,0 +1,174 @@ +/** + * Toast 提示组件 + * 提供轻量级的消息提示功能 + */ + +import { ANIMATION_CONFIG } from '../config/index.js'; + +export class Toast { + /** + * 构造函数 + * @param {Object} options - 配置选项 + * @param {string} options.toastId - Toast 元素ID(默认 "toast") + * @param {string} options.contentId - 内容元素ID(默认 "toastContent") + * @param {number} options.duration - 显示时长(毫秒) + * @param {string} options.type - 类型(success | error | warning | info) + */ + constructor(options = {}) { + this.toast = document.getElementById(options.toastId || 'toast'); + this.contentEl = document.getElementById(options.contentId || 'toastContent'); + this.defaultDuration = options.duration || ANIMATION_CONFIG.TOAST_DURATION; + this.defaultType = options.type || 'info'; + + if (!this.toast) { + console.warn('[Toast] 找不到 Toast 元素,请确保 DOM 中存在对应元素'); + } + } + + /** + * 显示 Toast + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长(毫秒),0 表示不自动关闭 + * @param {string} type - 类型 + */ + show(message, duration = this.defaultDuration, type = this.defaultType) { + if (!this.toast || !this.contentEl) { + console.warn('[Toast] Toast 元素不存在'); + return; + } + + // 清除之前的定时器 + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + + // 设置消息内容和类型 + this.contentEl.textContent = message; + this.setType(type); + + // 显示 Toast + this.toast.classList.add('show'); + + // 自动关闭 + if (duration > 0) { + this.timer = setTimeout(() => { + this.hide(); + }, duration); + } + } + + /** + * 隐藏 Toast + */ + hide() { + if (!this.toast) return; + + this.toast.classList.remove('show'); + + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + /** + * 设置 Toast 类型 + * @param {string} type - 类型 + */ + setType(type) { + if (!this.toast) return; + + // 移除所有类型类 + this.toast.classList.remove('toast-success', 'toast-error', 'toast-warning', 'toast-info'); + + // 添加类型类 + this.toast.classList.add(`toast-${type}`); + } + + /** + * 显示成功消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ + success(message, duration) { + this.show(message, duration, 'success'); + } + + /** + * 显示错误消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ + error(message, duration) { + this.show(message, duration, 'error'); + } + + /** + * 显示警告消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ + warning(message, duration) { + this.show(message, duration, 'warning'); + } + + /** + * 显示信息消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ + info(message, duration) { + this.show(message, duration, 'info'); + } + + /** + * 销毁 Toast + */ + destroy() { + this.hide(); + this.toast = null; + this.contentEl = null; + } +} + +// 创建全局 Toast 实例 +let globalToast = null; + +/** + * 获取全局 Toast 实例 + * @returns {Toast} - Toast 实例 + */ +export function getToast() { + if (!globalToast) { + globalToast = new Toast(); + } + return globalToast; +} + +/** + * 快捷方法:显示 Toast + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ +export function showToast(message, duration) { + return getToast().show(message, duration); +} + +/** + * 快捷方法:显示成功消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ +export function showSuccess(message, duration) { + return getToast().success(message, duration); +} + +/** + * 快捷方法:显示错误消息 + * @param {string} message - 消息内容 + * @param {number} duration - 显示时长 + */ +export function showError(message, duration) { + return getToast().error(message, duration); +} diff --git a/src/js/utils/formatter.js b/src/js/utils/formatter.js new file mode 100644 index 0000000..6feaea1 --- /dev/null +++ b/src/js/utils/formatter.js @@ -0,0 +1,114 @@ +/** + * 格式化工具 + * 提供各种数据格式化功能 + */ + +export class Formatter { + /** + * 手机号脱敏显示(中间4位用星号) + * @param {string} phone - 手机号 + * @returns {string} - 脱敏后的手机号 + */ + static maskPhone(phone) { + if (!phone || phone.length !== 11) { + return phone; + } + return phone.substring(0, 3) + '****' + phone.substring(7); + } + + /** + * 格式化还款期数显示 + * @param {number} period - 还款期数 + * @returns {string} - 格式化后的期数(如:"12个月") + */ + static formatRepaymentPeriod(period) { + return `${period}个月`; + } + + /** + * 格式化金额显示(添加千分位) + * @param {number} amount - 金额 + * @returns {string} - 格式化后的金额 + */ + static formatMoney(amount) { + if (isNaN(amount)) return '0'; + return amount.toLocaleString(); + } + + /** + * 格式化日期 + * @param {Date|string|number} date - 日期 + * @param {string} format - 格式(如:"YYYY-MM-DD") + * @returns {string} - 格式化后的日期 + */ + static formatDate(date, format = 'YYYY-MM-DD') { + const d = new Date(date); + + if (isNaN(d.getTime())) { + return ''; + } + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds); + } + + /** + * 格式化利率显示 + * @param {number} rate - 利率 + * @returns {string} - 格式化后的利率(如:"10.8%") + */ + static formatInterestRate(rate) { + if (isNaN(rate)) return '0%'; + return `${rate}%`; + } + + /** + * 格式化百分比 + * @param {number} value - 数值(0-1) + * @param {number} decimals - 小数位数 + * @returns {string} - 格式化后的百分比 + */ + static formatPercent(value, decimals = 0) { + if (isNaN(value)) return '0%'; + return `${(value * 100).toFixed(decimals)}%`; + } + + /** + * 格式化城市显示(省/市格式) + * @param {string} province - 省份 + * @param {string} city - 城市 + * @returns {string} - 格式化后的城市(如:"江西省/南昌市") + */ + static formatCity(province, city) { + if (!province || !city) return ''; + return `${province}/${city}`; + } + + /** + * 解析城市字符串 + * @param {string} cityStr - 城市字符串(如:"江西省/南昌市") + * @returns {{province: string, city: string}} - 省份和城市对象 + */ + static parseCity(cityStr) { + if (!cityStr) return { province: '', city: '' }; + + const parts = cityStr.split('/'); + if (parts.length === 2) { + return { province: parts[0], city: parts[1] }; + } + + return { province: '', city: cityStr }; + } +} diff --git a/src/js/utils/helper.js b/src/js/utils/helper.js new file mode 100644 index 0000000..ee569e8 --- /dev/null +++ b/src/js/utils/helper.js @@ -0,0 +1,283 @@ +/** + * 通用辅助函数 + * 提供常用的工具函数 + */ + +/** + * 防抖函数 + * @param {Function} func - 要执行的函数 + * @param {number} delay - 延迟时间(毫秒) + * @returns {Function} - 防抖后的函数 + */ +export function debounce(func, delay) { + let timeoutId; + return function (...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(this, args), delay); + }; +} + +/** + * 节流函数 + * @param {Function} func - 要执行的函数 + * @param {number} delay - 延迟时间(毫秒) + * @returns {Function} - 节流后的函数 + */ +export function throttle(func, delay) { + let lastTime = 0; + return function (...args) { + const now = Date.now(); + if (now - lastTime >= delay) { + lastTime = now; + return func.apply(this, args); + } + }; +} + +/** + * 深拷贝对象 + * @param {any} obj - 要拷贝的对象 + * @returns {any} - 拷贝后的对象 + */ +export function deepClone(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(item => deepClone(item)); + } + + const cloned = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + cloned[key] = deepClone(obj[key]); + } + } + + return cloned; +} + +/** + * 获取对象的深层属性值 + * @param {Object} obj - 对象 + * @param {string} path - 属性路径(如:"a.b.c") + * @param {any} defaultValue - 默认值 + * @returns {any} - 属性值 + */ +export function getDeepValue(obj, path, defaultValue = null) { + const keys = path.split('.'); + let result = obj; + + for (const key of keys) { + if (result === null || result === undefined) { + return defaultValue; + } + result = result[key]; + } + + return result !== undefined ? result : defaultValue; +} + +/** + * 检查对象是否为空 + * @param {Object} obj - 对象 + * @returns {boolean} - 是否为空 + */ +export function isEmpty(obj) { + if (obj === null || obj === undefined) return true; + if (typeof obj === 'string') return obj.trim().length === 0; + if (Array.isArray(obj)) return obj.length === 0; + if (typeof obj === 'object') return Object.keys(obj).length === 0; + return false; +} + +/** + * 延迟执行 + * @param {number} ms - 延迟时间(毫秒) + * @returns {Promise} - Promise 对象 + */ +export function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * 生成随机字符串 + * @param {number} length - 字符串长度 + * @returns {string} - 随机字符串 + */ +export function randomString(length = 8) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * 获取URL参数 + * @param {string} name - 参数名 + * @param {string} url - URL(可选,默认为当前页面URL) + * @returns {string|null} - 参数值 + */ +export function getUrlParam(name, url = window.location.href) { + const params = new URLSearchParams(new URL(url).search); + return params.get(name); +} + +/** + * 设置URL参数 + * @param {string} name - 参数名 + * @param {string} value - 参数值 + * @param {boolean} replace - 是否替换历史记录 + */ +export function setUrlParam(name, value, replace = false) { + const url = new URL(window.location.href); + url.searchParams.set(name, value); + + if (replace) { + window.history.replaceState(null, '', url.toString()); + } else { + window.history.pushState(null, '', url.toString()); + } +} + +/** + * 移除URL参数 + * @param {string} name - 参数名 + * @param {boolean} replace - 是否替换历史记录 + */ +export function removeUrlParam(name, replace = false) { + const url = new URL(window.location.href); + url.searchParams.delete(name); + + if (replace) { + window.history.replaceState(null, '', url.toString()); + } else { + window.history.pushState(null, '', url.toString()); + } +} + +/** + * 复制文本到剪贴板 + * @param {string} text - 要复制的文本 + * @returns {Promise} - 是否成功 + */ +export async function copyToClipboard(text) { + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + return true; + } else { + // 降级方案 + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + const success = document.execCommand('copy'); + document.body.removeChild(textarea); + return success; + } + } catch (error) { + console.error('[Helper] 复制到剪贴板失败:', error); + return false; + } +} + +/** + * 检查是否为移动设备 + * @returns {boolean} - 是否为移动设备 + */ +export function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} + +/** + * 获取设备类型 + * @returns {string} - 设备类型("mobile" | "tablet" | "desktop") + */ +export function getDeviceType() { + const userAgent = navigator.userAgent; + + if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) { + if (/iPad/i.test(userAgent) || (/Android/i.test(userAgent) && window.innerWidth >= 768)) { + return 'tablet'; + } + return 'mobile'; + } + + return 'desktop'; +} + +/** + * 平滑滚动到元素 + * @param {HTMLElement|string} element - 元素或元素ID + * @param {Object} options - 选项 + */ +export function scrollToElement(element, options = {}) { + const target = typeof element === 'string' ? document.getElementById(element) : element; + + if (!target) return; + + const defaultOptions = { + behavior: 'smooth', + block: 'start', + inline: 'nearest', + ...options + }; + + target.scrollIntoView(defaultOptions); +} + +/** + * 下载文件 + * @param {string} url - 文件URL + * @param {string} filename - 文件名 + */ +export function downloadFile(url, filename) { + const link = document.createElement('a'); + link.href = url; + link.download = filename || ''; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +/** + * 格式化文件大小 + * @param {number} bytes - 字节数 + * @returns {string} - 格式化后的文件大小 + */ +export function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +/** + * 获取浏览器信息 + * @returns {Object} - 浏览器信息 + */ +export function getBrowserInfo() { + const userAgent = navigator.userAgent; + + return { + userAgent, + language: navigator.language, + platform: navigator.platform, + cookieEnabled: navigator.cookieEnabled, + onLine: navigator.onLine, + screenWidth: window.screen.width, + screenHeight: window.screen.height, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + }; +} diff --git a/src/js/utils/index.js b/src/js/utils/index.js new file mode 100644 index 0000000..4f7e965 --- /dev/null +++ b/src/js/utils/index.js @@ -0,0 +1,7 @@ +/** + * 工具函数模块统一导出 + */ + +export { Validator } from './validator.js'; +export { Formatter } from './formatter.js'; +export * from './helper.js'; diff --git a/src/js/utils/validator.js b/src/js/utils/validator.js new file mode 100644 index 0000000..c44431d --- /dev/null +++ b/src/js/utils/validator.js @@ -0,0 +1,149 @@ +/** + * 表单验证器 + * 提供各种表单字段的验证功能 + */ + +import { VALIDATION_CONFIG } from '../config/index.js'; + +export class Validator { + /** + * 验证手机号 + * @param {string} phone - 手机号 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validatePhone(phone) { + if (!phone || !phone.trim()) { + return { valid: false, message: '请输入手机号' }; + } + + if (!VALIDATION_CONFIG.PHONE_REGEX.test(phone)) { + return { valid: false, message: '请输入正确的手机号' }; + } + + return { valid: true }; + } + + /** + * 验证姓名(2-20个汉字,支持少数民族姓名) + * @param {string} name - 姓名 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateName(name) { + if (!name || !name.trim()) { + return { valid: false, message: '请输入真实姓名' }; + } + + const nameStr = name.trim(); + + if (!VALIDATION_CONFIG.NAME_REGEX.test(nameStr)) { + return { valid: false, message: '请输入2-20个汉字,支持少数民族姓名' }; + } + + return { valid: true }; + } + + /** + * 验证身份证号 + * @param {string} idCard - 身份证号 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateIdCard(idCard) { + if (!idCard || !idCard.trim()) { + return { valid: false, message: '请输入身份证号' }; + } + + const idCardStr = idCard.trim(); + + // 验证18位身份证号 + if (idCardStr.length === 18) { + if (!VALIDATION_CONFIG.ID_CARD_18_REGEX.test(idCardStr)) { + return { valid: false, message: '请输入正确的身份证号' }; + } + + // 验证校验码 + if (!this._validateIdCardCheckCode(idCardStr)) { + return { valid: false, message: '身份证号校验码错误' }; + } + + return { valid: true }; + } + + // 验证15位身份证号(旧版) + if (idCardStr.length === 15) { + if (!VALIDATION_CONFIG.ID_CARD_15_REGEX.test(idCardStr)) { + return { valid: false, message: '请输入正确的身份证号' }; + } + + return { valid: true }; + } + + return { valid: false, message: '请输入正确的身份证号' }; + } + + /** + * 验证18位身份证号的校验码 + * @param {string} idCard - 18位身份证号 + * @returns {boolean} - 校验码是否正确 + * @private + */ + static _validateIdCardCheckCode(idCard) { + let sum = 0; + for (let i = 0; i < 17; i++) { + sum += parseInt(idCard[i]) * VALIDATION_CONFIG.ID_CARD_WEIGHTS[i]; + } + + const checkCode = VALIDATION_CONFIG.ID_CARD_CHECK_CODES[sum % 11]; + return idCard[17].toUpperCase() === checkCode; + } + + /** + * 验证短信验证码 + * @param {string} code - 验证码 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateSmsCode(code) { + if (!code || !code.trim()) { + return { valid: false, message: '请输入验证码' }; + } + + if (code.length !== 6) { + return { valid: false, message: '请输入6位验证码' }; + } + + return { valid: true }; + } + + /** + * 验证必填项 + * @param {any} value - 值 + * @param {string} fieldName - 字段名称 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateRequired(value, fieldName = '此项') { + if (value === null || value === undefined || (typeof value === 'string' && !value.trim())) { + return { valid: false, message: `请输入${fieldName}` }; + } + + return { valid: true }; + } + + /** + * 验证数字范围 + * @param {number} value - 数值 + * @param {number} min - 最小值 + * @param {number} max - 最大值 + * @param {string} fieldName - 字段名称 + * @returns {{valid: boolean, message: string}} - 验证结果 + */ + static validateRange(value, min, max, fieldName = '数值') { + if (isNaN(value)) { + return { valid: false, message: `${fieldName}必须是数字` }; + } + + if (value < min || value > max) { + return { valid: false, message: `${fieldName}必须在${min}-${max}之间` }; + } + + return { valid: true }; + } +} diff --git a/static/image/base-pic.png b/static/image/base-pic.png new file mode 100644 index 0000000..7bc4d03 Binary files /dev/null and b/static/image/base-pic.png differ diff --git a/static/image/dropdown-down.png b/static/image/dropdown-down.png new file mode 100644 index 0000000..ac2c6a7 Binary files /dev/null and b/static/image/dropdown-down.png differ diff --git a/static/image/dropdown-right.png b/static/image/dropdown-right.png new file mode 100644 index 0000000..aa568ad Binary files /dev/null and b/static/image/dropdown-right.png differ diff --git a/static/image/personalTop.png b/static/image/personalTop.png new file mode 100644 index 0000000..94e3b3a Binary files /dev/null and b/static/image/personalTop.png differ diff --git a/static/image/zc-pic.png b/static/image/zc-pic.png new file mode 100644 index 0000000..4800fec Binary files /dev/null and b/static/image/zc-pic.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..2e1dab1 --- /dev/null +++ b/style.css @@ -0,0 +1,756 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + background-color: #f5f5f5; + color: #333; + font-size: 14px; + line-height: 1.5; +} + +.container { + min-height: 100vh; + padding: 10px 16px; + padding-bottom: 20px; +} + +.money-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + border-bottom: 1px solid #ddd; + padding-bottom: 12px; + position: relative; +} + +.money-label { + font-weight: 600; + margin-right: 8px; + font-size: 14px; +} + +.money-input { + flex: 1; + font-size: 38px; + font-weight: 600; + border: none; + outline: none; + color: #333; + background: transparent; + width: 265px; + text-align: left; +} + +.max-loan { + position: absolute; + bottom: 4px; + right: 0; + color: #3474fe; + font-size: 12px; + cursor: pointer; +} + +.interest-rate-text { + margin-top: 12px; + font-size: 12px; + color: #777; +} + +.rate-highlight { + color: #3474fe; +} + +/* 借款金额区域 - 统一使用money-section */ +.money-section { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08); + font-size: 14px; + margin-top: 8px; +} + +.money-first { + margin-top: 5px; + padding: 20px; + box-shadow: none; + position: relative; +} + +.money-card { + padding: 0 20px; +} + +.money-compact { + padding-bottom: 1px; +} + +.money-phone { + padding-top: 16px; + padding-bottom: 16px; + padding-left: 0; + padding-right: 0; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + height: 52px; + border-bottom: 1px solid #efefef; +} + +.info-item.clickable { + cursor: pointer; +} + +.info-item.no-border { + border: none; +} + +.info-item.compact { + height: 49px; + color: #777; + font-size: 12px; +} + +.info-label { + font-weight: 600; + color: #333; + font-size: 14px; + min-width: 56px; +} + +.info-content.row { + flex-direction: row; + align-items: center; + justify-content: flex-end; +} + +.info-content { + text-align: right; + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; +} + +.info-main { + font-size: 12px; + font-weight: 400; + color: #333; +} + +.info-desc { + color: #aaa; + font-size: 11px; + margin-top: 2px; +} + +.arrow-icon { + color: #999; + margin-left: 4px; + font-size: 14px; +} + +.disclaimer-text { + font-size: 10px; + color: #777; + margin-top: 8px; + padding: 0 20px; +} + +.phone-input-wrapper { + height: 52px; + padding: 0 16px; + margin: 0 16px 16px 16px; + background-color: #f8f8f8; + border-radius: 25px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; +} + +.phone-label { + font-size: 14px; + font-weight: 600; + color: #333; +} + +.phone-input { + font-size: 12px; + font-weight: 400; + border: none; + outline: none; + background: transparent; + text-align: right; + flex: 1; + margin-left: 12px; + color: #333; + width: 175px; +} + +.phone-input::placeholder { + color: #999; +} + +.phone-input.error { + color: #ff4444; +} + +.phone-error { + font-size: 12px; + color: #ff4444; + text-align: center; + margin-top: 8px; + padding: 0 16px; + min-height: 20px; +} + +.button-wrapper { + width: 100%; + padding: 24px 16px; + padding-top: 20px; + padding-bottom: 0; + background-color: #fff; +} + +.apply-btn { + width: 100%; + height: 48px; + line-height: 48px; + color: #fff; + font-size: 18px; + text-align: center; + background: linear-gradient(140deg, #3474fe, #3474fe); + border: none; + border-radius: 25px; + cursor: pointer; + transition: all 0.2s ease; +} + +.apply-btn:active { + opacity: 0.9; +} + +/* 协议勾选 */ +.agreement-section { + margin: 24px 16px 16px; + display: flex; + align-items: center; + flex-wrap: wrap; + font-size: 11px; + line-height: 1.5; +} + +.checkbox-wrapper { + display: flex; + align-items: center; + cursor: pointer; + margin-right: 4px; +} + +.checkbox-input { + display: none; +} + +.checkbox-icon { + width: 12px; + height: 12px; + border: 1px solid #c8c9cc; + border-radius: 50%; + margin-right: 5px; + position: relative; + flex-shrink: 0; +} + +.checkbox-input:checked + .checkbox-icon { + background-color: #2979ff; + border-color: #2979ff; +} + +.checkbox-input:checked + .checkbox-icon::after { + content: ''; + position: absolute; + left: 3px; + top: 1px; + width: 4px; + height: 7px; + border: solid #fff; + border-width: 0 1px 1px 0; + transform: rotate(45deg); +} + +.checkbox-label { + font-size: 11px; + color: #606266; +} + +.agreement-link { + color: #2979ff; + font-size: 12px; + text-decoration: none; + margin: 0 2px; + white-space: nowrap; +} + +.agreement-link:active { + opacity: 0.7; +} + +/* 底部声明 */ +.footer-section { + text-align: center; + color: #aaa; + margin: 16px 0; + font-size: 12px; + padding: 0 16px; +} + +.footer-title { + font-weight: 600; + margin-bottom: 8px; +} + +.footer-section p { + line-height: 22px; + font-size: 11px; + margin: 4px 0; +} + +/* 借款用途选择器模态框 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: none; + align-items: flex-end; + justify-content: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.modal-overlay.show { + display: flex; + opacity: 1; +} + +.modal-overlay.showing { + display: flex; +} + +.modal-content { + width: 100%; + max-width: 100%; + background-color: #fff; + border-radius: 16px 16px 0 0; + max-height: 70vh; + display: flex; + flex-direction: column; + transform: translateY(100%); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.modal-overlay.show .modal-content { + transform: translateY(0); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #eee; + flex-shrink: 0; + position: relative; +} + +.modal-header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(to right, transparent, #eee 20%, #eee 80%, transparent); +} + +.modal-btn { + background: none; + border: none; + font-size: 16px; + cursor: pointer; + padding: 8px 16px; + transition: all 0.2s ease; + border-radius: 4px; + min-width: 60px; + text-align: center; + user-select: none; +} + +.modal-btn-cancel { + color: #666; +} + +.modal-btn-cancel:active { + background-color: #f5f5f5; + opacity: 0.8; +} + +.modal-btn-confirm { + color: #3474fe; + font-weight: 500; +} + +.modal-btn-confirm:active { + background-color: rgba(52, 116, 254, 0.1); + opacity: 0.8; +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 0; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; +} + +.modal-body::-webkit-scrollbar { + width: 4px; +} + +.modal-body::-webkit-scrollbar-track { + background: transparent; +} + +.modal-body::-webkit-scrollbar-thumb { + background: #ddd; + border-radius: 2px; +} + +.modal-body::-webkit-scrollbar-thumb:hover { + background: #bbb; +} + +/* 模态框选项通用样式 */ +.modal-option { + padding: 16px 20px; + font-size: 16px; + color: #333; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: all 0.2s ease; + position: relative; + user-select: none; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-option:last-child { + border-bottom: none; +} + +.modal-option-text { + text-align: center; +} + +.modal-option-check { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #ddd; + transition: all 0.2s ease; + flex-shrink: 0; + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); +} + +.modal-option:active { + background-color: #f5f5f5; +} + +.modal-option.active { + background-color: #e8e8e8; +} + +.modal-option.active .modal-option-text { + color: #333; + font-weight: 600; +} + +.modal-option.active .modal-option-check { + border-color: #3474fe; + background-color: #3474fe; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23fff' d='M10 3L4.5 8.5 2 6' stroke='none' stroke-width='2' fill-rule='evenodd'/%3E%3C/svg%3E"); + background-size: 10px 10px; + background-position: center; + background-repeat: no-repeat; +} + +/* 防止背景滚动 */ +body.modal-open { + overflow: hidden; + position: fixed; + width: 100%; +} + +/* 验证码弹窗样式 */ +#verifyCodeModal { + align-items: center; + justify-content: center; +} + +.verify-code-modal { + width: 90%; + max-width: 400px; + background-color: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + transform: scale(0.8); + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1001; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); +} + +#verifyCodeModal.show .verify-code-modal { + transform: scale(1); + opacity: 1; +} + +.verify-code-header { + padding: 20px 20px 16px; + text-align: center; + border-bottom: 1px solid #eee; +} + +.verify-code-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; +} + +.verify-code-body { + padding: 24px 20px; +} + +.verify-code-tip { + font-size: 14px; + color: #666; + text-align: center; + margin-bottom: 20px; +} + +.verify-code-input-wrapper { + display: flex; + align-items: center; + background-color: #f8f8f8; + border-radius: 8px; + padding: 0 16px; + height: 48px; + margin-bottom: 12px; + border: 1px solid transparent; + transition: all 0.2s ease; +} + +.verify-code-input-wrapper:focus-within { + border-color: #3474fe; + background-color: #fff; +} + +.verify-code-input-wrapper.error { + border-color: #ff4444; + background-color: #fff5f5; +} + +.verify-code-input-icon { + font-size: 18px; + margin-right: 12px; + flex-shrink: 0; +} + +.verify-code-input { + flex: 1; + border: none; + outline: none; + background: transparent; + font-size: 16px; + color: #333; + height: 100%; +} + +.verify-code-input::placeholder { + color: #999; +} + +.verify-code-countdown { + font-size: 14px; + color: #999; + margin-left: 12px; + flex-shrink: 0; + cursor: pointer; + user-select: none; +} + +.verify-code-error { + font-size: 12px; + color: #ff4444; + text-align: center; + margin-top: 8px; +} + +.verify-code-footer { + padding: 0 20px 24px; +} + +.verify-code-btn { + width: 100%; + height: 48px; + line-height: 48px; + color: #fff; + font-size: 16px; + text-align: center; + background: linear-gradient(140deg, #3474fe, #3474fe); + border: none; + border-radius: 24px; + cursor: pointer; + transition: all 0.2s ease; +} + +.verify-code-btn:active { + opacity: 0.9; +} + +/* 协议提示弹窗样式 */ +#agreementModal { + align-items: center; + justify-content: center; +} + +.agreement-modal { + width: 90%; + max-width: 320px; + background-color: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + transform: scale(0.8); + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1001; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +#agreementModal.show .agreement-modal { + transform: scale(1); + opacity: 1; +} + +.agreement-modal-header { + padding: 20px 20px 16px; + text-align: center; + border-bottom: 1px solid #eee; +} + +.agreement-modal-title { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0; +} + +.agreement-modal-body { + padding: 20px; + text-align: center; +} + +.agreement-modal-text { + font-size: 14px; + color: #333; + margin-bottom: 12px; + line-height: 1.5; +} + +.agreement-modal-links { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.agreement-modal-link { + color: #3474fe; + font-size: 14px; + text-decoration: none; + line-height: 1.5; +} + +.agreement-modal-link:active { + opacity: 0.7; +} + +.agreement-modal-footer { + display: flex; + border-top: 1px solid #eee; + padding: 0; +} + +.agreement-modal-btn { + flex: 1; + height: 48px; + line-height: 48px; + font-size: 16px; + text-align: center; + border: none; + cursor: pointer; + transition: all 0.2s ease; + background: transparent; +} + +.agreement-modal-btn-cancel { + color: #666; + border-right: 1px solid #eee; +} + +.agreement-modal-btn-cancel:active { + background-color: #f5f5f5; +} + +.agreement-modal-btn-confirm { + color: #fff; + background: linear-gradient(140deg, #3474fe, #3474fe); + font-weight: 500; +} + +.agreement-modal-btn-confirm:active { + opacity: 0.9; +} + +/* 响应式适配 */ +@media (max-width: 375px) { + .money-input { + font-size: 32px; + } + + .info-item { + height: 49px; + } +}