This commit is contained in:
2026-01-22 18:31:30 +08:00
commit d703ac3574
46 changed files with 7751 additions and 0 deletions

View File

@@ -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;
}
}

351
src/js/README.md Normal file
View File

@@ -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 文件中使用 `<script type="module">` 引入主入口文件:
```html
<!-- index.html 和 basic-info.html -->
<script type="module" src="./src/js/main.js"></script>
```
**注意:**
- 需要删除旧的 `<script>` 标签(如 `config.js``script.js``basic-info.js`
- 确保所有模块文件都使用 `.js` 扩展名
- 现代浏览器都支持 ES6 模块
### 方法 2: 使用构建工具(可选)
如果需要更好的兼容性和性能优化,可以使用 Vite 或 Webpack
#### 使用 Vite
1. 安装 Vite
```bash
npm install -D vite
```
2. 创建 `vite.config.js`
```javascript
import { defineConfig } from 'vite';
export default defineConfig({
root: './',
build: {
outDir: 'dist',
rollupOptions: {
input: {
main: './index.html',
basicInfo: './basic-info.html'
}
}
}
});
```
3.`package.json` 中添加脚本:
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
```
4. 启动开发服务器:
```bash
npm run dev
```
## 📦 模块说明
### 配置模块 (config/)
所有应用配置的集中管理位置。
**使用示例:**
```javascript
import { API_CONFIG, LOAN_CONFIG, DEBUG_CONFIG } from './config/index.js';
// 使用 API 配置
const url = API_CONFIG.BASE_URL + API_CONFIG.ENDPOINTS.SEND_SMS;
// 使用借款配置
const maxAmount = LOAN_CONFIG.AMOUNT.MAX;
// 使用调试配置
if (DEBUG_CONFIG.ENABLED) {
console.log('Debug mode is on');
}
```
### 核心模块 (core/)
提供核心功能,如 API 请求、缓存管理等。
**使用示例:**
```javascript
import { ApiClient, UserCache, FormIdGenerator } from './core/index.js';
// API 请求
const response = await ApiClient.post('/api/endpoint', { data: 'value' });
// 用户缓存
UserCache.saveUserSession({ customerid: '123', mobile: '13800138000' });
const session = UserCache.getUserSession();
// 表单 ID
const formId = FormIdGenerator.getOrCreate();
```
### 工具函数模块 (utils/)
提供纯函数工具,无副作用。
**使用示例:**
```javascript
import { Validator, Formatter, debounce } from './utils/index.js';
// 验证
const phoneValidation = Validator.validatePhone('13800138000');
// 格式化
const maskedPhone = Formatter.maskPhone('13800138000'); // "138****8000"
// 防抖
const debouncedFn = debounce(() => console.log('Called'), 300);
```
### 业务服务层 (services/)
封装业务逻辑,与核心模块和工具函数协作。
**使用示例:**
```javascript
import { SMSService, AuthService, LoanService } from './services/index.js';
// 发送短信
const result = await SMSService.send('13800138000');
// 用户登录
const loginResult = await AuthService.registerOrLogin('13800138000', loanData);
// 计算还款计划
const plan = LoanService.calculateRepaymentPlan(50000, 12);
```
### UI 组件 (ui/)
可复用的 UI 组件类。
**使用示例:**
```javascript
import { Picker, Modal, CityPicker, showToast } from './ui/index.js';
// 选择器
const picker = new Picker({
triggerId: 'trigger',
modalId: 'modal',
// ... 其他配置
});
// 模态框
const modal = new Modal({
modalId: 'modal',
onConfirm: () => console.log('Confirmed')
});
// Toast 提示
showToast('操作成功', 2000);
```
### 页面逻辑 (pages/)
页面级别的逻辑封装。
**使用示例:**
```javascript
import { IndexPage, BasicInfoPage } from './pages/index.js';
// 主页面会自动初始化,无需手动调用
```
## 🔄 从旧代码迁移
### 步骤 1: 备份旧文件
```bash
mkdir old
mv config.js script.js basic-info.js old/
```
### 步骤 2: 修改 HTML 文件
**index.html 和 basic-info.html**
删除旧的 script 标签:
```html
<!-- 删除这些行 -->
<script src="config.js"></script>
<script src="script.js"></script>
<script src="basic-info.js"></script>
```
添加新的 module script
```html
<!-- 添加这一行 -->
<script type="module" src="./src/js/main.js"></script>
```
### 步骤 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
<script type="module" src="./src/js/main.js"></script>
```
### 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

View File

@@ -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,
};

176
src/js/config/app.config.js Normal file
View File

@@ -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,
};

49
src/js/config/index.js Normal file
View File

@@ -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,
};

141
src/js/core/api.js Normal file
View File

@@ -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<Object>} - 响应数据
*/
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<Object>} - 响应数据
*/
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<Object>} - 响应数据
*/
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 || '网络错误,请稍后重试'
};
}
}
}

View File

@@ -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<Object>} - 保存结果
*/
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<Object|null>} - 草稿数据,如果不存在或不是草稿状态则返回 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] 已清除草稿数据');
}
}

85
src/js/core/form-id.js Normal file
View File

@@ -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);
}
}

8
src/js/core/index.js Normal file
View File

@@ -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';

88
src/js/core/user-cache.js Normal file
View File

@@ -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);
}
}

79
src/js/main.js Normal file
View File

@@ -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] 应用入口已加载');

View File

@@ -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 = `
<div class="item-top">
<div class="item-name">${item.name}:</div>
<div class="item-value ${selectedValue ? 'selected' : ''}" id="value-${item.id}">
${selectedValue || '请选择'}
<img class="item-icon" src="${iconSrc}" alt="${selectedValue ? '展开' : '收起'}" id="icon-${item.id}">
</div>
</div>
<div class="item-options" id="options-${item.id}">
${item.options.map(option => `
<button class="option-btn ${selectedValue === option ? 'selected' : ''}"
data-item="${item.id}"
data-value="${option}">
${option}
</button>
`).join('')}
</div>
`;
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 = `
<div class="item-top">
<div class="item-name">${item.name}:</div>
<input type="text"
class="basic-input-inline"
id="basic-input-${item.id}"
placeholder="${item.placeholder}"
value="${selectedValue}">
</div>
<div class="basic-info-error" id="error-${item.id}" style="display: none;"></div>
`;
} else if (item.id === 'city') {
const iconSrc = selectedValue ? './static/image/dropdown-right.png' : './static/image/dropdown-down.png';
infoItem.innerHTML = `
<div class="item-top item-top-clickable" id="basic-header-${item.id}">
<div class="item-name">${item.name}:</div>
<div class="item-value ${selectedValue ? 'selected' : ''}" id="basic-value-${item.id}">
<span class="item-value-text">${selectedValue || item.placeholder}</span>
<img class="item-icon" src="${iconSrc}" alt="${selectedValue ? '展开' : '收起'}" id="basic-icon-${item.id}">
</div>
</div>
`;
// 绑定城市选择
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();
}
}
}

6
src/js/pages/index.js Normal file
View File

@@ -0,0 +1,6 @@
/**
* 页面逻辑模块统一导出
*/
export { IndexPage } from './index.page.js';
export { BasicInfoPage } from './basic-info.page.js';

560
src/js/pages/index.page.js Normal file
View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<Object|null>} - 草稿数据,如果不存在则返回 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] 已清除草稿数据');
}
}

9
src/js/services/index.js Normal file
View File

@@ -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';

View File

@@ -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<boolean>} - 是否初始化成功
*/
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<void>}
* @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<void>}
* @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<void>}
* @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<string>} - 登录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<boolean>}
*/
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;
}

View File

@@ -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<number>} - 期数选项数组
*/
static getPeriodOptions() {
return LOAN_CONFIG.PERIOD_OPTIONS;
}
/**
* 获取借款用途选项列表
* @returns {Array<string>} - 用途选项数组
*/
static getPurposeOptions() {
return LOAN_CONFIG.PURPOSES;
}
}

View File

@@ -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 || '网络错误,请稍后重试' };
}
}
}

267
src/js/ui/city-picker.js Normal file
View File

@@ -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;
}
}

9
src/js/ui/index.js Normal file
View File

@@ -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';

157
src/js/ui/modal.js Normal file
View File

@@ -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;
}
}

View File

@@ -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 = `
<div class="one-click-login-container" id="oneClickLoginContainer" style="display: none;">
<button class="one-click-login-btn" id="oneClickLoginBtn">
<span class="btn-icon">📱</span>
<span class="btn-text">${this.buttonText}</span>
</button>
<div class="login-divider">
<span class="divider-line"></span>
<span class="divider-text">或</span>
<span class="divider-line"></span>
</div>
</div>
`;
// 绑定事件
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 = `
<span class="btn-spinner"></span>
<span class="btn-text">登录中...</span>
`;
} else {
btn.disabled = false;
btn.innerHTML = `
<span class="btn-icon">📱</span>
<span class="btn-text">${this.buttonText}</span>
`;
}
}
}
/**
* 显示降级消息
* @param {string} message
*/
showFallbackMessage(message) {
// 可以在这里显示一个Toast提示
console.log('[OneClickLoginButton] 降级提示:', message);
}
/**
* 销毁组件
*/
destroy() {
if (this.container) {
this.container.innerHTML = '';
}
}
}

281
src/js/ui/picker.js Normal file
View File

@@ -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 = [];
}
}

174
src/js/ui/toast.js Normal file
View File

@@ -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);
}

114
src/js/utils/formatter.js Normal file
View File

@@ -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 };
}
}

283
src/js/utils/helper.js Normal file
View File

@@ -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<boolean>} - 是否成功
*/
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,
};
}

7
src/js/utils/index.js Normal file
View File

@@ -0,0 +1,7 @@
/**
* 工具函数模块统一导出
*/
export { Validator } from './validator.js';
export { Formatter } from './formatter.js';
export * from './helper.js';

149
src/js/utils/validator.js Normal file
View File

@@ -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 };
}
}