提交新的移动端框架
This commit is contained in:
1 parent
134f711349
commit
9bda3717fe
75 files changed
+5283
-9011
No files matched your search
@@ -1,378 +0,0 @@
|
||||
# 移动端工具库使用说明
|
||||
|
||||
本文档说明移动端 `utils` 目录下各个工具的使用方法,特别是升级后的请求工具。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
mobile/src/utils/
|
||||
├── request.js # 统一请求工具(已升级)
|
||||
├── crypto.js # 加密解密工具(新增)
|
||||
├── config.js # 配置管理工具(新增)
|
||||
├── encrypt-pure.js # 登录专用加密工具(保留)
|
||||
├── encrypt-*.js # 其他加密实现(保留)
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 🚀 核心升级功能
|
||||
|
||||
### 1. 统一的加密解密支持
|
||||
|
||||
移动端现在与 Web 端保持一致,支持:
|
||||
- ✅ **入参加密**:通过 `isEncrypt: 'true'` 头部控制
|
||||
- ✅ **出参解密**:自动处理后端返回的加密数据
|
||||
- ✅ **AES + RSA 双重加密**:与后端完全兼容
|
||||
- ✅ **防重复提交**:避免用户误操作
|
||||
- ✅ **错误处理优化**:404静默处理、401自动跳转
|
||||
|
||||
### 2. 可配置的参数管理
|
||||
|
||||
所有关键参数都可以通过配置文件统一管理,方便调试和部署。
|
||||
|
||||
## 📖 使用方法
|
||||
|
||||
### 基础请求
|
||||
|
||||
```javascript
|
||||
import { request } from '@/utils/request.js';
|
||||
|
||||
// 普通GET请求
|
||||
const response = await request({
|
||||
url: '/api/user/list',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
// 普通POST请求
|
||||
const response = await request({
|
||||
url: '/api/user/create',
|
||||
method: 'POST',
|
||||
data: {
|
||||
name: '张三',
|
||||
age: 25
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 加密请求
|
||||
|
||||
```javascript
|
||||
import { request } from '@/utils/request.js';
|
||||
|
||||
// 启用加密的POST请求
|
||||
const response = await request({
|
||||
url: '/api/sensitive/data',
|
||||
method: 'POST',
|
||||
data: {
|
||||
sensitiveInfo: '敏感数据'
|
||||
},
|
||||
header: {
|
||||
isEncrypt: 'true' // 关键:启用加密
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 免Token请求
|
||||
|
||||
```javascript
|
||||
import { request } from '@/utils/request.js';
|
||||
|
||||
// 不需要Token的请求(如获取租户列表)
|
||||
const response = await request({
|
||||
url: '/auth/tenant/list',
|
||||
method: 'GET',
|
||||
header: {
|
||||
isToken: false // 关键:不携带Token
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 允许重复提交
|
||||
|
||||
```javascript
|
||||
import { request } from '@/utils/request.js';
|
||||
|
||||
// 允许重复提交的请求
|
||||
const response = await request({
|
||||
url: '/api/data/sync',
|
||||
method: 'POST',
|
||||
data: { syncData: 'xxx' },
|
||||
header: {
|
||||
repeatSubmit: false // 关键:允许重复提交
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## ⚙️ 配置管理
|
||||
|
||||
### 基础配置更新
|
||||
|
||||
```javascript
|
||||
import { updateConfig } from '@/utils/config.js';
|
||||
|
||||
// 更新API基础URL
|
||||
updateConfig({
|
||||
request: {
|
||||
baseURL: {
|
||||
production: 'https://new-api-domain.com'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新客户端ID
|
||||
updateConfig({
|
||||
app: {
|
||||
clientId: 'new-client-id-here'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 加密配置更新
|
||||
|
||||
```javascript
|
||||
import { updateConfig } from '@/utils/config.js';
|
||||
|
||||
// 更新RSA公钥
|
||||
updateConfig({
|
||||
encryption: {
|
||||
rsaPublicKey: 'new-rsa-public-key-here'
|
||||
}
|
||||
});
|
||||
|
||||
// 临时禁用加密(调试用)
|
||||
updateConfig({
|
||||
encryption: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 预设配置方案
|
||||
|
||||
```javascript
|
||||
import { applyPreset } from '@/utils/config.js';
|
||||
|
||||
// 应用生产环境配置
|
||||
applyPreset('production');
|
||||
|
||||
// 应用测试环境配置
|
||||
applyPreset('test');
|
||||
|
||||
// 应用开发环境配置
|
||||
applyPreset('development');
|
||||
```
|
||||
|
||||
### 单个配置项更新
|
||||
|
||||
```javascript
|
||||
import { setConfigItem, getConfigItem } from '@/utils/config.js';
|
||||
|
||||
// 设置超时时间
|
||||
setConfigItem('request.timeout', 600000); // 10分钟
|
||||
|
||||
// 获取当前客户端ID
|
||||
const clientId = getConfigItem('app.clientId');
|
||||
|
||||
// 检查是否启用加密
|
||||
const isEncrypted = getConfigItem('encryption.enabled');
|
||||
```
|
||||
|
||||
## 🔐 加密工具直接使用
|
||||
|
||||
```javascript
|
||||
import {
|
||||
encryptWithAES,
|
||||
decryptWithAES,
|
||||
generateRandomAESKey,
|
||||
encrypt,
|
||||
decrypt
|
||||
} from '@/utils/crypto.js';
|
||||
|
||||
// AES加密解密
|
||||
const aesKey = generateRandomAESKey();
|
||||
const encryptedData = encryptWithAES('敏感数据', aesKey);
|
||||
const decryptedData = decryptWithAES(encryptedData, aesKey);
|
||||
|
||||
// RSA加密解密
|
||||
const encryptedKey = encrypt('AES密钥');
|
||||
const decryptedKey = decrypt(encryptedKey);
|
||||
```
|
||||
|
||||
## 📦 API方法封装
|
||||
|
||||
### 登录
|
||||
|
||||
```javascript
|
||||
import { api } from '@/utils/request.js';
|
||||
|
||||
// 用户登录(自动加密)
|
||||
const loginResult = await api.login({
|
||||
username: 'admin',
|
||||
password: 'password123',
|
||||
tenantId: '000000'
|
||||
});
|
||||
```
|
||||
|
||||
### 获取用户信息
|
||||
|
||||
```javascript
|
||||
import { api } from '@/utils/request.js';
|
||||
|
||||
// 获取当前用户信息
|
||||
const userInfo = await api.getUserInfo();
|
||||
```
|
||||
|
||||
### 注销登录
|
||||
|
||||
```javascript
|
||||
import { api } from '@/utils/request.js';
|
||||
|
||||
// 用户注销
|
||||
await api.logout();
|
||||
```
|
||||
|
||||
## 💾 缓存工具
|
||||
|
||||
### 用户信息缓存
|
||||
|
||||
```javascript
|
||||
import { userStorage } from '@/utils/request.js';
|
||||
|
||||
// 保存登录信息
|
||||
userStorage.saveLoginInfo(loginResult, userInfo);
|
||||
|
||||
// 获取用户信息
|
||||
const user = userStorage.getUserInfo();
|
||||
|
||||
// 检查登录状态
|
||||
const isLoggedIn = userStorage.isLoggedIn();
|
||||
|
||||
// 清除登录信息
|
||||
userStorage.clearLoginInfo();
|
||||
```
|
||||
|
||||
### 通用缓存
|
||||
|
||||
```javascript
|
||||
import { storage } from '@/utils/request.js';
|
||||
|
||||
// 设置缓存(30分钟过期)
|
||||
storage.set('cacheKey', { data: 'value' }, 1800);
|
||||
|
||||
// 获取缓存
|
||||
const data = storage.get('cacheKey');
|
||||
|
||||
// 删除缓存
|
||||
storage.remove('cacheKey');
|
||||
|
||||
// 清空所有缓存
|
||||
storage.clear();
|
||||
```
|
||||
|
||||
## 📥 文件下载
|
||||
|
||||
```javascript
|
||||
import { download } from '@/utils/request.js';
|
||||
|
||||
// 下载文件
|
||||
await download('/api/export/users', {
|
||||
filters: { status: 'active' }
|
||||
}, 'users.xlsx');
|
||||
```
|
||||
|
||||
## 🔧 调试和日志
|
||||
|
||||
### 开启详细日志
|
||||
|
||||
```javascript
|
||||
import { updateConfig } from '@/utils/config.js';
|
||||
|
||||
// 开启所有调试日志
|
||||
updateConfig({
|
||||
debug: {
|
||||
enableVerboseLog: true,
|
||||
showRequestLog: true,
|
||||
showResponseLog: true,
|
||||
showCryptoLog: true
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 错误处理配置
|
||||
|
||||
```javascript
|
||||
import { updateConfig } from '@/utils/config.js';
|
||||
|
||||
// 自定义错误处理
|
||||
updateConfig({
|
||||
error: {
|
||||
silentCodes: [404, 403], // 静默处理的状态码
|
||||
toastDuration: 3000, // 错误提示显示时间
|
||||
loginRedirectDelay: 1000 // 跳转登录延迟
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
### 1. 加密兼容性
|
||||
- 移动端加密算法与 Web 端完全一致
|
||||
- RSA 公钥必须与后端保持同步
|
||||
- 加密只对 POST/PUT 请求生效
|
||||
|
||||
### 2. 配置优先级
|
||||
- 运行时配置 > 环境预设 > 默认配置
|
||||
- 配置更新会立即生效,无需重启应用
|
||||
|
||||
### 3. Token 管理
|
||||
- Token 自动从本地存储获取
|
||||
- 401 错误会自动清除 Token 并跳转登录
|
||||
- 支持 Token 过期时间管理
|
||||
|
||||
### 4. 错误处理
|
||||
- 404 错误默认静默处理
|
||||
- 403 错误会显示提示但不跳转
|
||||
- 网络错误会显示友好提示
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 1. 防重复提交
|
||||
- 默认 500ms 间隔防重复提交
|
||||
- 可通过配置调整间隔时间
|
||||
- 支持单个请求禁用防重复
|
||||
|
||||
### 2. 请求超时
|
||||
- 普通请求:5分钟超时
|
||||
- 下载请求:20分钟超时
|
||||
- 可通过配置调整超时时间
|
||||
|
||||
### 3. 缓存策略
|
||||
- 会话级缓存用于防重复提交
|
||||
- 持久化缓存支持过期时间
|
||||
- 登录信息缓存支持时间戳
|
||||
|
||||
## 🔗 与 Web 端对比
|
||||
|
||||
| 功能 | Web 端 | 移动端 | 说明 |
|
||||
|------|--------|--------|------|
|
||||
| 入参加密 | ✅ | ✅ | 完全一致 |
|
||||
| 出参解密 | ✅ | ✅ | 完全一致 |
|
||||
| 防重复提交 | ✅ | ✅ | 完全一致 |
|
||||
| 401处理 | ✅ | ✅ | 跳转方式略有不同 |
|
||||
| 404静默 | ✅ | ✅ | 完全一致 |
|
||||
| 下载支持 | ✅ | ✅ | 平台实现不同 |
|
||||
| 配置管理 | 环境变量 | 配置文件 | 更灵活的配置方式 |
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **生产环境**:关闭调试日志,启用加密
|
||||
2. **开发环境**:开启详细日志,保持加密测试
|
||||
3. **配置管理**:使用预设配置,避免硬编码
|
||||
4. **错误处理**:合理设置静默错误码
|
||||
5. **性能优化**:适当设置超时和缓存策略
|
||||
|
||||
---
|
||||
|
||||
**📝 更新日志**
|
||||
- 2025-01-07:完成移动端请求工具升级,与 Web 端功能对齐
|
||||
- 支持入参加密、出参解密、防重复提交等完整功能
|
||||
- 新增配置管理系统,提升可维护性
|
||||
@@ -1,774 +0,0 @@
|
||||
/**
|
||||
* 无障碍访问工具类
|
||||
* 提供屏幕阅读器支持、键盘导航、高对比度模式等功能
|
||||
*/
|
||||
|
||||
class AccessibilityUtils {
|
||||
constructor() {
|
||||
this.isHighContrast = false;
|
||||
this.isLargeText = false;
|
||||
this.isScreenReaderMode = false;
|
||||
this.focusVisible = true;
|
||||
this.currentFocusIndex = 0;
|
||||
this.focusableElements = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化无障碍功能
|
||||
*/
|
||||
init() {
|
||||
this.setupKeyboardNavigation();
|
||||
this.setupFocusManagement();
|
||||
this.setupScreenReaderSupport();
|
||||
this.loadAccessibilitySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键盘导航
|
||||
*/
|
||||
setupKeyboardNavigation() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
this.handleTabNavigation(e);
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.handleActivation(e);
|
||||
break;
|
||||
case 'Escape':
|
||||
this.handleEscape(e);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
this.handleArrowNavigation(e);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 检测键盘使用,显示焦点指示器
|
||||
document.addEventListener('keydown', () => {
|
||||
this.focusVisible = true;
|
||||
document.body.classList.add('keyboard-navigation');
|
||||
});
|
||||
|
||||
// 检测鼠标使用,隐藏焦点指示器
|
||||
document.addEventListener('mousedown', () => {
|
||||
this.focusVisible = false;
|
||||
document.body.classList.remove('keyboard-navigation');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Tab导航
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleTabNavigation(e) {
|
||||
this.updateFocusableElements();
|
||||
|
||||
if (this.focusableElements.length === 0) return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab - 反向导航
|
||||
this.currentFocusIndex =
|
||||
this.currentFocusIndex <= 0
|
||||
? this.focusableElements.length - 1
|
||||
: this.currentFocusIndex - 1;
|
||||
} else {
|
||||
// Tab - 正向导航
|
||||
this.currentFocusIndex =
|
||||
this.currentFocusIndex >= this.focusableElements.length - 1
|
||||
? 0
|
||||
: this.currentFocusIndex + 1;
|
||||
}
|
||||
|
||||
this.focusableElements[this.currentFocusIndex]?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理激活操作
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleActivation(e) {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement.click) {
|
||||
activeElement.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Escape键
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleEscape(e) {
|
||||
// 关闭模态框、菜单等
|
||||
const modal = document.querySelector('.modal:not([style*="display: none"])');
|
||||
const menu = document.querySelector('.dropdown-menu.show, .longpress-menu');
|
||||
|
||||
if (modal) {
|
||||
this.closeModal(modal);
|
||||
} else if (menu) {
|
||||
menu.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方向键导航
|
||||
* @param {KeyboardEvent} e - 键盘事件
|
||||
*/
|
||||
handleArrowNavigation(e) {
|
||||
const currentElement = document.activeElement;
|
||||
const container = currentElement.closest('.grid-layout, .menu-section, .stats-section');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const focusableInContainer = this.getFocusableElements(container);
|
||||
const currentIndex = focusableInContainer.indexOf(currentElement);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const currentRect = currentElement.getBoundingClientRect();
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
nextIndex = this.findElementAbove(focusableInContainer, currentIndex, currentRect);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
nextIndex = this.findElementBelow(focusableInContainer, currentIndex, currentRect);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
nextIndex = Math.max(0, currentIndex - 1);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
nextIndex = Math.min(focusableInContainer.length - 1, currentIndex + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextIndex !== currentIndex) {
|
||||
focusableInContainer[nextIndex]?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找上方元素
|
||||
*/
|
||||
findElementAbove(elements, currentIndex, currentRect) {
|
||||
let bestIndex = currentIndex;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === currentIndex) continue;
|
||||
|
||||
const rect = elements[i].getBoundingClientRect();
|
||||
if (rect.bottom <= currentRect.top) {
|
||||
const distance =
|
||||
Math.abs(rect.left - currentRect.left) + (currentRect.top - rect.bottom);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下方元素
|
||||
*/
|
||||
findElementBelow(elements, currentIndex, currentRect) {
|
||||
let bestIndex = currentIndex;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === currentIndex) continue;
|
||||
|
||||
const rect = elements[i].getBoundingClientRect();
|
||||
if (rect.top >= currentRect.bottom) {
|
||||
const distance =
|
||||
Math.abs(rect.left - currentRect.left) + (rect.top - currentRect.bottom);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置焦点管理
|
||||
*/
|
||||
setupFocusManagement() {
|
||||
// 焦点指示器样式
|
||||
const focusStyles = `
|
||||
.keyboard-navigation *:focus {
|
||||
outline: 2px solid rgb(93, 135, 255) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.keyboard-navigation .touchable:focus {
|
||||
outline: 2px solid rgb(93, 135, 255) !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 4px rgba(93, 135, 255, 0.2) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = focusStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
// 焦点陷阱(用于模态框)
|
||||
this.setupFocusTrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置焦点陷阱
|
||||
*/
|
||||
setupFocusTrap() {
|
||||
document.addEventListener('focusin', (e) => {
|
||||
const modal = document.querySelector('.modal:not([style*="display: none"])');
|
||||
if (modal && !modal.contains(e.target)) {
|
||||
const firstFocusable = this.getFocusableElements(modal)[0];
|
||||
firstFocusable?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新可聚焦元素列表
|
||||
*/
|
||||
updateFocusableElements() {
|
||||
this.focusableElements = this.getFocusableElements(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可聚焦元素
|
||||
* @param {Element} container - 容器元素
|
||||
* @returns {Element[]} 可聚焦元素数组
|
||||
*/
|
||||
getFocusableElements(container = document) {
|
||||
const focusableSelectors = [
|
||||
'button:not([disabled])',
|
||||
'[href]',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])',
|
||||
'.touchable',
|
||||
'.service-item',
|
||||
'.menu-item',
|
||||
'.stat-card',
|
||||
].join(', ');
|
||||
|
||||
return Array.from(container.querySelectorAll(focusableSelectors)).filter((el) => {
|
||||
return (
|
||||
el.offsetParent !== null &&
|
||||
!el.hasAttribute('disabled') &&
|
||||
!el.classList.contains('sr-only')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置屏幕阅读器支持
|
||||
*/
|
||||
setupScreenReaderSupport() {
|
||||
// 动态添加ARIA标签
|
||||
this.addAriaLabels();
|
||||
|
||||
// 实时区域更新
|
||||
this.setupLiveRegions();
|
||||
|
||||
// 页面标题更新
|
||||
this.setupPageTitleUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加ARIA标签
|
||||
*/
|
||||
addAriaLabels() {
|
||||
// 为服务项添加描述
|
||||
document.querySelectorAll('.service-item').forEach((item) => {
|
||||
const title = item.querySelector('.service-title')?.textContent;
|
||||
if (title && !item.hasAttribute('aria-label')) {
|
||||
item.setAttribute('aria-label', `${title}服务`);
|
||||
item.setAttribute('role', 'button');
|
||||
item.setAttribute('tabindex', '0');
|
||||
}
|
||||
});
|
||||
|
||||
// 为统计卡片添加描述
|
||||
document.querySelectorAll('.stat-card').forEach((card) => {
|
||||
const number = card.querySelector('.stat-number')?.textContent;
|
||||
const label = card.querySelector('.stat-label')?.textContent;
|
||||
if (number && label && !card.hasAttribute('aria-label')) {
|
||||
card.setAttribute('aria-label', `${label}:${number}`);
|
||||
card.setAttribute('role', 'button');
|
||||
card.setAttribute('tabindex', '0');
|
||||
}
|
||||
});
|
||||
|
||||
// 为导航按钮添加描述
|
||||
document.querySelectorAll('.nav-tab').forEach((tab) => {
|
||||
const label = tab.querySelector('.nav-label')?.textContent;
|
||||
if (label && !tab.hasAttribute('aria-label')) {
|
||||
tab.setAttribute('aria-label', `导航到${label}页面`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实时区域
|
||||
*/
|
||||
setupLiveRegions() {
|
||||
// 创建实时区域用于通知
|
||||
const liveRegion = document.createElement('div');
|
||||
liveRegion.setAttribute('aria-live', 'polite');
|
||||
liveRegion.setAttribute('aria-atomic', 'true');
|
||||
liveRegion.className = 'sr-only';
|
||||
liveRegion.id = 'live-region';
|
||||
document.body.appendChild(liveRegion);
|
||||
|
||||
// 监听toast消息
|
||||
const originalShowToast = uni.showToast;
|
||||
uni.showToast = (options) => {
|
||||
this.announceToScreenReader(options.title || options.content);
|
||||
return originalShowToast(options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 向屏幕阅读器宣布消息
|
||||
* @param {string} message - 消息内容
|
||||
*/
|
||||
announceToScreenReader(message) {
|
||||
const liveRegion = document.getElementById('live-region');
|
||||
if (liveRegion) {
|
||||
liveRegion.textContent = message;
|
||||
|
||||
// 清空内容以便下次宣布
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = '';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面标题更新
|
||||
*/
|
||||
setupPageTitleUpdates() {
|
||||
// 监听路由变化,更新页面标题
|
||||
const originalSwitchTab = uni.switchTab;
|
||||
uni.switchTab = (options) => {
|
||||
const pageTitles = {
|
||||
'/pages/index/index': '智慧校园 - 首页',
|
||||
};
|
||||
|
||||
const title = pageTitles[options.url];
|
||||
if (title) {
|
||||
document.title = title;
|
||||
this.announceToScreenReader(`已切换到${title.split(' - ')[1]}页面`);
|
||||
}
|
||||
|
||||
return originalSwitchTab(options);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用高对比度模式
|
||||
*/
|
||||
enableHighContrast() {
|
||||
this.isHighContrast = true;
|
||||
document.body.classList.add('high-contrast');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用高对比度模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用高对比度模式
|
||||
*/
|
||||
disableHighContrast() {
|
||||
this.isHighContrast = false;
|
||||
document.body.classList.remove('high-contrast');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用高对比度模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用大字体模式
|
||||
*/
|
||||
enableLargeText() {
|
||||
this.isLargeText = true;
|
||||
document.body.classList.add('large-text');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用大字体模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用大字体模式
|
||||
*/
|
||||
disableLargeText() {
|
||||
this.isLargeText = false;
|
||||
document.body.classList.remove('large-text');
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用大字体模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用屏幕阅读器模式
|
||||
*/
|
||||
enableScreenReaderMode() {
|
||||
this.isScreenReaderMode = true;
|
||||
document.body.classList.add('screen-reader-mode');
|
||||
|
||||
// 隐藏装饰性动画
|
||||
const style = document.createElement('style');
|
||||
style.id = 'screen-reader-styles';
|
||||
style.textContent = `
|
||||
.screen-reader-mode * {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.screen-reader-mode .decorative {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已启用屏幕阅读器优化模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用屏幕阅读器模式
|
||||
*/
|
||||
disableScreenReaderMode() {
|
||||
this.isScreenReaderMode = false;
|
||||
document.body.classList.remove('screen-reader-mode');
|
||||
|
||||
const style = document.getElementById('screen-reader-styles');
|
||||
if (style) style.remove();
|
||||
|
||||
this.saveAccessibilitySettings();
|
||||
this.announceToScreenReader('已禁用屏幕阅读器优化模式');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存无障碍设置
|
||||
*/
|
||||
saveAccessibilitySettings() {
|
||||
const settings = {
|
||||
highContrast: this.isHighContrast,
|
||||
largeText: this.isLargeText,
|
||||
screenReaderMode: this.isScreenReaderMode,
|
||||
};
|
||||
|
||||
uni.setStorageSync('accessibility-settings', settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载无障碍设置
|
||||
*/
|
||||
loadAccessibilitySettings() {
|
||||
try {
|
||||
const settings = uni.getStorageSync('accessibility-settings');
|
||||
if (settings) {
|
||||
if (settings.highContrast) this.enableHighContrast();
|
||||
if (settings.largeText) this.enableLargeText();
|
||||
if (settings.screenReaderMode) this.enableScreenReaderMode();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load accessibility settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加语义化结构
|
||||
*/
|
||||
addSemanticStructure() {
|
||||
// 添加主要地标
|
||||
const main = document.querySelector('.main-content');
|
||||
if (main && !main.hasAttribute('role')) {
|
||||
main.setAttribute('role', 'main');
|
||||
main.setAttribute('aria-label', '主要内容');
|
||||
}
|
||||
|
||||
// 添加导航地标
|
||||
const nav = document.querySelector('.bottom-nav');
|
||||
if (nav && !nav.hasAttribute('role')) {
|
||||
nav.setAttribute('role', 'navigation');
|
||||
nav.setAttribute('aria-label', '主导航');
|
||||
}
|
||||
|
||||
// 添加区域标识
|
||||
document.querySelectorAll('.card').forEach((card, index) => {
|
||||
if (!card.hasAttribute('role')) {
|
||||
card.setAttribute('role', 'region');
|
||||
const title = card.querySelector('.card-title')?.textContent;
|
||||
if (title) {
|
||||
card.setAttribute('aria-labelledby', `card-title-${index}`);
|
||||
const titleElement = card.querySelector('.card-title');
|
||||
if (titleElement) {
|
||||
titleElement.id = `card-title-${index}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭模态框
|
||||
* @param {Element} modal - 模态框元素
|
||||
*/
|
||||
closeModal(modal) {
|
||||
modal.style.display = 'none';
|
||||
|
||||
// 恢复焦点到触发元素
|
||||
const trigger = document.querySelector('[data-modal-trigger]');
|
||||
if (trigger) {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式设计工具类
|
||||
*/
|
||||
class ResponsiveUtils {
|
||||
constructor() {
|
||||
this.breakpoints = {
|
||||
mobile: 375,
|
||||
tablet: 768,
|
||||
desktop: 1024,
|
||||
};
|
||||
|
||||
this.currentBreakpoint = this.getCurrentBreakpoint();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化响应式功能
|
||||
*/
|
||||
init() {
|
||||
this.setupViewportMeta();
|
||||
this.setupMediaQueries();
|
||||
this.setupOrientationChange();
|
||||
this.addResponsiveClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置viewport元标签
|
||||
*/
|
||||
setupViewportMeta() {
|
||||
let viewport = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewport) {
|
||||
viewport = document.createElement('meta');
|
||||
viewport.name = 'viewport';
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
viewport.content =
|
||||
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置媒体查询监听
|
||||
*/
|
||||
setupMediaQueries() {
|
||||
Object.entries(this.breakpoints).forEach(([name, width]) => {
|
||||
const mediaQuery = window.matchMedia(`(max-width: ${width}px)`);
|
||||
|
||||
mediaQuery.addListener((e) => {
|
||||
this.handleBreakpointChange();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.handleBreakpointChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断点变化
|
||||
*/
|
||||
handleBreakpointChange() {
|
||||
const newBreakpoint = this.getCurrentBreakpoint();
|
||||
|
||||
if (newBreakpoint !== this.currentBreakpoint) {
|
||||
this.currentBreakpoint = newBreakpoint;
|
||||
this.addResponsiveClasses();
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('breakpointChange', {
|
||||
detail: { breakpoint: newBreakpoint },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前断点
|
||||
*/
|
||||
getCurrentBreakpoint() {
|
||||
const width = window.innerWidth;
|
||||
|
||||
if (width <= this.breakpoints.mobile) return 'mobile';
|
||||
if (width <= this.breakpoints.tablet) return 'tablet';
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加响应式类名
|
||||
*/
|
||||
addResponsiveClasses() {
|
||||
document.body.className = document.body.className
|
||||
.replace(/\b(mobile|tablet|desktop)\b/g, '')
|
||||
.trim();
|
||||
|
||||
document.body.classList.add(this.currentBreakpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置屏幕方向变化监听
|
||||
*/
|
||||
setupOrientationChange() {
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
this.handleBreakpointChange();
|
||||
|
||||
// 修复iOS Safari的viewport问题
|
||||
if (/iPhone|iPad/.test(navigator.userAgent)) {
|
||||
document.body.style.height = window.innerHeight + 'px';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安全区域内边距
|
||||
*/
|
||||
getSafeAreaInsets() {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
return {
|
||||
top: computedStyle.getPropertyValue('env(safe-area-inset-top)') || '0px',
|
||||
right: computedStyle.getPropertyValue('env(safe-area-inset-right)') || '0px',
|
||||
bottom: computedStyle.getPropertyValue('env(safe-area-inset-bottom)') || '0px',
|
||||
left: computedStyle.getPropertyValue('env(safe-area-inset-left)') || '0px',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为移动设备
|
||||
*/
|
||||
isMobile() {
|
||||
return this.currentBreakpoint === 'mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为平板设备
|
||||
*/
|
||||
isTablet() {
|
||||
return this.currentBreakpoint === 'tablet';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为桌面设备
|
||||
*/
|
||||
isDesktop() {
|
||||
return this.currentBreakpoint === 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const accessibilityUtils = new AccessibilityUtils();
|
||||
const responsiveUtils = new ResponsiveUtils();
|
||||
|
||||
// 添加全局样式
|
||||
const globalStyles = `
|
||||
/* 高对比度模式 */
|
||||
.high-contrast {
|
||||
--uni-color-primary: #000000;
|
||||
--uni-bg-color: #ffffff;
|
||||
--uni-text-color: #000000;
|
||||
--uni-text-color-secondary: #333333;
|
||||
--uni-border-color: #000000;
|
||||
}
|
||||
|
||||
.high-contrast .card {
|
||||
border: 2px solid #000000;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 大字体模式 */
|
||||
.large-text {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.large-text .card-title {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.large-text .service-title {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
/* 屏幕阅读器模式 */
|
||||
.screen-reader-mode .decorative {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 键盘导航焦点样式 */
|
||||
.keyboard-navigation *:focus {
|
||||
outline: 2px solid rgb(93, 135, 255);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 375px) {
|
||||
.mobile .grid-4 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.mobile .service-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mobile .card {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.mobile .grid-4,
|
||||
.mobile .grid-3 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.page-container {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
`;
|
||||
|
||||
// 插入全局样式
|
||||
const globalStyleSheet = document.createElement('style');
|
||||
globalStyleSheet.textContent = globalStyles;
|
||||
document.head.appendChild(globalStyleSheet);
|
||||
|
||||
export { AccessibilityUtils, ResponsiveUtils };
|
||||
export default { accessibilityUtils, responsiveUtils };
|
||||
@@ -1,422 +0,0 @@
|
||||
/**
|
||||
* 动画工具类
|
||||
* 提供页面转场、组件动画和微交互效果的统一管理
|
||||
*/
|
||||
|
||||
class AnimationUtils {
|
||||
constructor() {
|
||||
this.animationDuration = 300;
|
||||
this.easingFunction = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面进入动画
|
||||
* @param {string} selector - 选择器
|
||||
* @param {string} type - 动画类型
|
||||
*/
|
||||
pageEnter(selector = '.page-container', type = 'fadeInUp') {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.style.animation = `${type} 0.6s ease-out`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面退出动画
|
||||
* @param {string} selector - 选择器
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
pageExit(selector = '.page-container', callback) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.style.animation = 'fadeOutDown 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (callback) callback();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片进入动画(错开显示)
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
staggerCards(selector = '.card, .service-item, .stat-card') {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.1}s`;
|
||||
element.style.animation = 'slideInUp 0.6s ease-out backwards';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表项动画
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
animateList(selector = '.menu-item, .todo-item, .news-item') {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element, index) => {
|
||||
element.style.animationDelay = `${index * 0.05}s`;
|
||||
element.style.animation = 'slideInRight 0.4s ease-out backwards';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击反馈动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
*/
|
||||
clickFeedback(element) {
|
||||
if (!element) return;
|
||||
|
||||
element.style.transform = 'scale(0.95)';
|
||||
element.style.transition = 'transform 0.1s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动画
|
||||
* @param {string} selector - 选择器
|
||||
*/
|
||||
showLoading(selector = 'body') {
|
||||
const loadingHtml = `
|
||||
<div class="loading-overlay" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
">
|
||||
<div class="spinner" style="
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #f0f0f0;
|
||||
border-top: 3px solid rgb(93, 135, 255);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector(selector);
|
||||
if (container) {
|
||||
container.insertAdjacentHTML('beforeend', loadingHtml);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载动画
|
||||
*/
|
||||
hideLoading() {
|
||||
const loading = document.querySelector('.loading-overlay');
|
||||
if (loading) {
|
||||
loading.style.animation = 'fadeOut 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
loading.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast 提示动画
|
||||
* @param {string} message - 消息内容
|
||||
* @param {number} duration - 持续时间
|
||||
*/
|
||||
showToast(message, duration = 2000) {
|
||||
// 移除已存在的toast
|
||||
const existingToast = document.querySelector('.animated-toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'animated-toast';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
max-width: 280px;
|
||||
text-align: center;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOutUp 0.3s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数字计数动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
* @param {number} endValue - 结束值
|
||||
* @param {number} duration - 持续时间
|
||||
*/
|
||||
countUp(element, endValue, duration = 1000) {
|
||||
if (!element) return;
|
||||
|
||||
const startValue = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
const currentTime = Date.now();
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const currentValue = startValue + (endValue - startValue) * easeProgress;
|
||||
|
||||
// 格式化显示
|
||||
if (endValue % 1 === 0) {
|
||||
element.textContent = Math.round(currentValue);
|
||||
} else {
|
||||
element.textContent = currentValue.toFixed(1);
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉刷新动画
|
||||
* @param {HTMLElement} element - 元素
|
||||
*/
|
||||
pullToRefresh(element) {
|
||||
if (!element) return;
|
||||
|
||||
element.style.transform = 'translateY(50px)';
|
||||
element.style.transition = 'transform 0.3s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索聚焦动画
|
||||
* @param {HTMLElement} searchBar - 搜索栏元素
|
||||
*/
|
||||
searchFocus(searchBar) {
|
||||
if (!searchBar) return;
|
||||
|
||||
searchBar.style.transform = 'translateY(-2px)';
|
||||
searchBar.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.15)';
|
||||
searchBar.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索失焦动画
|
||||
* @param {HTMLElement} searchBar - 搜索栏元素
|
||||
*/
|
||||
searchBlur(searchBar) {
|
||||
if (!searchBar) return;
|
||||
|
||||
searchBar.style.transform = 'translateY(0)';
|
||||
searchBar.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* 模态框显示动画
|
||||
* @param {HTMLElement} modal - 模态框元素
|
||||
*/
|
||||
showModal(modal) {
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
modal.style.animation = 'fadeIn 0.3s ease-out';
|
||||
|
||||
const content = modal.querySelector('.modal-content');
|
||||
if (content) {
|
||||
content.style.animation = 'slideInUp 0.3s ease-out';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模态框隐藏动画
|
||||
* @param {HTMLElement} modal - 模态框元素
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
hideModal(modal, callback) {
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.animation = 'fadeOut 0.3s ease-in forwards';
|
||||
|
||||
const content = modal.querySelector('.modal-content');
|
||||
if (content) {
|
||||
content.style.animation = 'slideOutDown 0.3s ease-in forwards';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
if (callback) callback();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
const animationUtils = new AnimationUtils();
|
||||
|
||||
// 定义CSS动画关键帧
|
||||
const animationStyles = `
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutDown {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutDown {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutUp {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
`;
|
||||
|
||||
// 插入动画样式到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = animationStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
export default animationUtils;
|
||||
export { AnimationUtils };
|
||||
@@ -1,353 +0,0 @@
|
||||
// 移动端配置文件 - 集中管理所有可配置参数
|
||||
// 使用方法:import { updateConfig } from '@/utils/config.js' 然后调用 updateConfig(newConfig)
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG = {
|
||||
// =========================
|
||||
// 加密相关配置
|
||||
// =========================
|
||||
encryption: {
|
||||
// 是否启用加密
|
||||
enabled: true,
|
||||
|
||||
// RSA公钥 - 与后端保持一致
|
||||
rsaPublicKey:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
|
||||
// RSA私钥 - 前端不建议存放,仅用于测试环境
|
||||
rsaPrivateKey: '',
|
||||
|
||||
// 加密请求头名称
|
||||
encryptHeader: 'encrypt-key',
|
||||
|
||||
// AES密钥长度
|
||||
aesKeyLength: 32,
|
||||
},
|
||||
|
||||
// =========================
|
||||
// 请求相关配置
|
||||
// =========================
|
||||
request: {
|
||||
// API基础URL
|
||||
baseURL: {
|
||||
development: '/api',
|
||||
production: 'https://your-api-domain.com',
|
||||
},
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
timeout: 300000, // 5分钟
|
||||
|
||||
// 下载超时时间(毫秒)
|
||||
downloadTimeout: 1200000, // 20分钟
|
||||
|
||||
// 防重复提交间隔时间(毫秒)
|
||||
repeatSubmitInterval: 500,
|
||||
|
||||
// 默认请求头
|
||||
defaultHeaders: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
|
||||
// =========================
|
||||
// 应用相关配置
|
||||
// =========================
|
||||
app: {
|
||||
// 客户端ID
|
||||
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
|
||||
// 租户ID
|
||||
tenantId: '000000',
|
||||
|
||||
// 默认referer
|
||||
referer: 'http://localhost/login?redirect=/index',
|
||||
|
||||
// 登录页面路径
|
||||
loginPath: '/pages/login/index',
|
||||
},
|
||||
|
||||
// =========================
|
||||
// 缓存相关配置
|
||||
// =========================
|
||||
cache: {
|
||||
// 默认缓存过期时间(秒)
|
||||
defaultExpire: 3600, // 1小时
|
||||
|
||||
// Token存储键名
|
||||
tokenKey: 'access_token',
|
||||
|
||||
// 用户信息存储键名
|
||||
userInfoKey: 'userInfo',
|
||||
|
||||
// 登录状态存储键名
|
||||
loginStatusKey: 'isLoggedIn',
|
||||
|
||||
// 会话缓存前缀
|
||||
sessionPrefix: 'session_',
|
||||
},
|
||||
|
||||
// =========================
|
||||
// 错误处理配置
|
||||
// =========================
|
||||
error: {
|
||||
// 静默处理的HTTP状态码
|
||||
silentCodes: [404],
|
||||
|
||||
// 自动跳转登录的状态码
|
||||
loginRedirectCodes: [401],
|
||||
|
||||
// 错误提示显示时间(毫秒)
|
||||
toastDuration: 2000,
|
||||
|
||||
// 跳转登录延迟时间(毫秒)
|
||||
loginRedirectDelay: 500,
|
||||
},
|
||||
|
||||
// =========================
|
||||
// 调试相关配置
|
||||
// =========================
|
||||
debug: {
|
||||
// 是否启用详细日志
|
||||
enableVerboseLog: process.env.NODE_ENV === 'development',
|
||||
|
||||
// 是否显示请求日志
|
||||
showRequestLog: true,
|
||||
|
||||
// 是否显示响应日志
|
||||
showResponseLog: true,
|
||||
|
||||
// 是否显示加密解密日志
|
||||
showCryptoLog: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 当前配置 - 可被动态修改
|
||||
let currentConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
||||
|
||||
// =========================
|
||||
// 配置管理方法
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* 获取完整配置
|
||||
* @returns {Object} 当前配置对象
|
||||
*/
|
||||
export const getConfig = () => {
|
||||
return currentConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取特定配置项
|
||||
* @param {string} path 配置路径,支持嵌套 如: 'encryption.enabled' 或 'request.timeout'
|
||||
* @returns {any} 配置值
|
||||
*/
|
||||
export const getConfigItem = (path) => {
|
||||
const keys = path.split('.');
|
||||
let value = currentConfig;
|
||||
|
||||
for (const key of keys) {
|
||||
if (value && typeof value === 'object' && key in value) {
|
||||
value = value[key];
|
||||
} else {
|
||||
console.warn(`配置项 ${path} 不存在`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* @param {Object} newConfig 新配置对象(部分更新)
|
||||
* @param {boolean} deep 是否深度合并,默认true
|
||||
*/
|
||||
export const updateConfig = (newConfig, deep = true) => {
|
||||
if (deep) {
|
||||
currentConfig = deepMerge(currentConfig, newConfig);
|
||||
} else {
|
||||
currentConfig = { ...currentConfig, ...newConfig };
|
||||
}
|
||||
|
||||
console.log('🔧 配置已更新:', currentConfig);
|
||||
|
||||
// 通知其他模块配置已更新
|
||||
notifyConfigUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置特定配置项
|
||||
* @param {string} path 配置路径
|
||||
* @param {any} value 配置值
|
||||
*/
|
||||
export const setConfigItem = (path, value) => {
|
||||
const keys = path.split('.');
|
||||
let target = currentConfig;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!target[key] || typeof target[key] !== 'object') {
|
||||
target[key] = {};
|
||||
}
|
||||
target = target[key];
|
||||
}
|
||||
|
||||
const lastKey = keys[keys.length - 1];
|
||||
target[lastKey] = value;
|
||||
|
||||
console.log(`🔧 配置项 ${path} 已更新为:`, value);
|
||||
|
||||
// 通知其他模块配置已更新
|
||||
notifyConfigUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置配置为默认值
|
||||
*/
|
||||
export const resetConfig = () => {
|
||||
currentConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
||||
console.log('🔄 配置已重置为默认值');
|
||||
|
||||
// 通知其他模块配置已更新
|
||||
notifyConfigUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前环境的API基础URL
|
||||
* @returns {string} API基础URL
|
||||
*/
|
||||
export const getBaseURL = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
return currentConfig.request.baseURL[env] || currentConfig.request.baseURL.development;
|
||||
};
|
||||
|
||||
// =========================
|
||||
// 配置预设方案
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* 开发环境配置
|
||||
*/
|
||||
export const developmentConfig = {
|
||||
encryption: {
|
||||
enabled: true, // 开发环境也启用加密测试
|
||||
},
|
||||
debug: {
|
||||
enableVerboseLog: true,
|
||||
showRequestLog: true,
|
||||
showResponseLog: true,
|
||||
showCryptoLog: true,
|
||||
},
|
||||
error: {
|
||||
toastDuration: 3000, // 开发环境错误提示显示久一点
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生产环境配置
|
||||
*/
|
||||
export const productionConfig = {
|
||||
encryption: {
|
||||
enabled: true,
|
||||
},
|
||||
debug: {
|
||||
enableVerboseLog: false,
|
||||
showRequestLog: false,
|
||||
showResponseLog: false,
|
||||
showCryptoLog: false,
|
||||
},
|
||||
request: {
|
||||
timeout: 300000, // 生产环境保持较长超时
|
||||
repeatSubmitInterval: 1000, // 生产环境防重复提交间隔更长
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 测试环境配置
|
||||
*/
|
||||
export const testConfig = {
|
||||
encryption: {
|
||||
enabled: true,
|
||||
},
|
||||
debug: {
|
||||
enableVerboseLog: true,
|
||||
showRequestLog: true,
|
||||
showResponseLog: false,
|
||||
showCryptoLog: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用预设配置
|
||||
* @param {string} preset 预设名称: 'development' | 'production' | 'test'
|
||||
*/
|
||||
export const applyPreset = (preset) => {
|
||||
const presets = {
|
||||
development: developmentConfig,
|
||||
production: productionConfig,
|
||||
test: testConfig,
|
||||
};
|
||||
|
||||
if (presets[preset]) {
|
||||
updateConfig(presets[preset]);
|
||||
console.log(`✅ 已应用 ${preset} 环境配置预设`);
|
||||
} else {
|
||||
console.error(`❌ 未知的配置预设: ${preset}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================
|
||||
// 工具方法
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
* @param {Object} target 目标对象
|
||||
* @param {Object} source 源对象
|
||||
* @returns {Object} 合并后的对象
|
||||
*/
|
||||
const deepMerge = (target, source) => {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||||
result[key] = deepMerge(result[key] || {}, source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知其他模块配置已更新
|
||||
*/
|
||||
const notifyConfigUpdate = () => {
|
||||
// 这里可以实现事件通知机制
|
||||
// 例如使用 uni.$emit('configUpdated', currentConfig)
|
||||
if (typeof uni !== 'undefined' && uni.$emit) {
|
||||
uni.$emit('configUpdated', currentConfig);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================
|
||||
// 初始化配置
|
||||
// =========================
|
||||
|
||||
// 根据环境自动应用配置预设
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
if (env !== 'development') {
|
||||
applyPreset(env);
|
||||
}
|
||||
|
||||
// 导出常用配置项的快捷访问方法
|
||||
export const isEncryptionEnabled = () => getConfigItem('encryption.enabled');
|
||||
export const getClientId = () => getConfigItem('app.clientId');
|
||||
export const getTenantId = () => getConfigItem('app.tenantId');
|
||||
export const getTimeout = () => getConfigItem('request.timeout');
|
||||
export const isDebugMode = () => getConfigItem('debug.enableVerboseLog');
|
||||
|
||||
console.log('📋 移动端配置初始化完成,当前环境:', env);
|
||||
@@ -1,291 +0,0 @@
|
||||
// 移动端加密解密工具 - 与web端保持一致
|
||||
import CryptoJS from 'crypto-js';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
// 加密配置 - 可配置
|
||||
export const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
// RSA私钥 - 与web端配置文档保持一致
|
||||
RSA_PRIVATE_KEY:
|
||||
'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=',
|
||||
// 客户端ID
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
// 租户ID
|
||||
TENANT_ID: '000000',
|
||||
// 加密请求头
|
||||
ENCRYPT_HEADER: 'encrypt-key',
|
||||
// 是否启用加密
|
||||
ENCRYPT_ENABLED: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机生成32位的字符串
|
||||
* @returns {string}
|
||||
*/
|
||||
const generateRandomString = () => {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机生成AES密钥
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
export const generateAesKey = () => {
|
||||
return CryptoJS.enc.Utf8.parse(generateRandomString());
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密base64
|
||||
* @param {CryptoJS.lib.WordArray} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export const encryptBase64 = (str) => {
|
||||
return CryptoJS.enc.Base64.stringify(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解密base64
|
||||
* @param {string} str
|
||||
* @returns {CryptoJS.lib.WordArray}
|
||||
*/
|
||||
export const decryptBase64 = (str) => {
|
||||
return CryptoJS.enc.Base64.parse(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行AES加密
|
||||
* @param {string} message 待加密的数据
|
||||
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export const encryptWithAes = (message, aesKey) => {
|
||||
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用密钥对数据进行AES解密
|
||||
* @param {string} message 待解密的数据
|
||||
* @param {CryptoJS.lib.WordArray} aesKey AES密钥
|
||||
* @returns {string} 解密后的数据
|
||||
*/
|
||||
export const decryptWithAes = (message, aesKey) => {
|
||||
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA加密
|
||||
* @param {string} txt 待加密的文本
|
||||
* @returns {string|false} 加密后的数据
|
||||
*/
|
||||
export const encrypt = (txt) => {
|
||||
try {
|
||||
console.log('🔐 开始RSA加密,输入文本长度:', txt ? txt.length : 'null');
|
||||
console.log('🔐 使用的RSA公钥:', ENCRYPT_CONFIG.RSA_PUBLIC_KEY.substring(0, 20) + '...');
|
||||
|
||||
const encryptor = new JSEncrypt();
|
||||
|
||||
// 构造完整的PEM格式RSA公钥
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||
${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
console.log('🔐 构造的完整公钥:', publicKey.substring(0, 50) + '...');
|
||||
|
||||
encryptor.setPublicKey(publicKey);
|
||||
console.log('🔐 公钥设置完成');
|
||||
|
||||
const result = encryptor.encrypt(txt);
|
||||
|
||||
if (result) {
|
||||
console.log('✅ RSA加密成功,结果长度:', result.length);
|
||||
console.log('✅ 加密结果前50位:', result.substring(0, 50) + '...');
|
||||
} else {
|
||||
console.error('❌ RSA加密返回空结果');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA加密失败:', error);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA解密
|
||||
* @param {string} txt 待解密的文本
|
||||
* @returns {string|false} 解密后的数据
|
||||
*/
|
||||
export const decrypt = (txt) => {
|
||||
try {
|
||||
console.log('🔓 开始RSA解密,输入文本长度:', txt ? txt.length : 'null');
|
||||
console.log('🔓 使用的RSA私钥:', ENCRYPT_CONFIG.RSA_PRIVATE_KEY.substring(0, 20) + '...');
|
||||
|
||||
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
|
||||
console.warn('❌ RSA私钥未配置,无法解密');
|
||||
return false;
|
||||
}
|
||||
|
||||
const encryptor = new JSEncrypt();
|
||||
|
||||
// 构造完整的PEM格式RSA私钥
|
||||
const privateKey = `-----BEGIN PRIVATE KEY-----
|
||||
${ENCRYPT_CONFIG.RSA_PRIVATE_KEY}
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
console.log('🔓 构造的完整私钥:', privateKey.substring(0, 50) + '...');
|
||||
|
||||
encryptor.setPrivateKey(privateKey);
|
||||
console.log('🔓 私钥设置完成');
|
||||
|
||||
const result = encryptor.decrypt(txt);
|
||||
|
||||
if (result) {
|
||||
console.log('✅ RSA解密成功,结果长度:', result.length);
|
||||
console.log('✅ 解密结果前50位:', result.substring(0, 50) + '...');
|
||||
} else {
|
||||
console.error('❌ RSA解密返回空结果');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA解密失败:', error);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机字符串AES密钥(字符串格式)
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥字符串
|
||||
*/
|
||||
export const generateRandomAESKey = (length = 32) => {
|
||||
return generateRandomString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用字符串密钥进行AES加密(兼容原有函数)
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥字符串
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export const encryptWithAES = (data, key) => {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用字符串密钥进行AES解密(兼容原有函数)
|
||||
* @param {string} data 待解密的数据
|
||||
* @param {string} key AES密钥字符串
|
||||
* @returns {string} 解密后的数据
|
||||
*/
|
||||
export const decryptWithAES = (data, key) => {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const decrypted = CryptoJS.AES.decrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新加密配置
|
||||
* @param {Object} config 新的配置对象
|
||||
*/
|
||||
export const updateEncryptConfig = (config) => {
|
||||
Object.assign(ENCRYPT_CONFIG, config);
|
||||
console.log('🔧 加密配置已更新:', ENCRYPT_CONFIG);
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证RSA密钥对配置
|
||||
* @returns {boolean} 验证结果
|
||||
*/
|
||||
export const validateKeyPair = () => {
|
||||
try {
|
||||
console.log('🔍 开始验证RSA密钥对配置...');
|
||||
|
||||
// 检查公钥和私钥是否存在
|
||||
if (!ENCRYPT_CONFIG.RSA_PUBLIC_KEY) {
|
||||
console.error('❌ RSA公钥未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ENCRYPT_CONFIG.RSA_PRIVATE_KEY) {
|
||||
console.error('❌ RSA私钥未配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 测试加密解密功能
|
||||
const testMessage = 'RSA密钥对验证测试消息';
|
||||
console.log('🧪 测试消息:', testMessage);
|
||||
|
||||
// 使用公钥加密
|
||||
const encrypted = encrypt(testMessage);
|
||||
if (!encrypted) {
|
||||
console.error('❌ RSA加密测试失败');
|
||||
return false;
|
||||
}
|
||||
console.log('✅ RSA加密测试成功');
|
||||
|
||||
// 使用私钥解密
|
||||
const decrypted = decrypt(encrypted);
|
||||
if (!decrypted || decrypted !== testMessage) {
|
||||
console.error('❌ RSA解密测试失败');
|
||||
console.error('原始消息:', testMessage);
|
||||
console.error('解密结果:', decrypted);
|
||||
return false;
|
||||
}
|
||||
console.log('✅ RSA解密测试成功');
|
||||
|
||||
console.log('🎉 RSA密钥对配置验证通过!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA密钥对验证过程中出现错误:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前加密配置摘要
|
||||
* @returns {Object} 配置摘要
|
||||
*/
|
||||
export const getConfigSummary = () => {
|
||||
return {
|
||||
hasPublicKey: !!ENCRYPT_CONFIG.RSA_PUBLIC_KEY,
|
||||
hasPrivateKey: !!ENCRYPT_CONFIG.RSA_PRIVATE_KEY,
|
||||
publicKeyLength: ENCRYPT_CONFIG.RSA_PUBLIC_KEY ? ENCRYPT_CONFIG.RSA_PUBLIC_KEY.length : 0,
|
||||
privateKeyLength: ENCRYPT_CONFIG.RSA_PRIVATE_KEY
|
||||
? ENCRYPT_CONFIG.RSA_PRIVATE_KEY.length
|
||||
: 0,
|
||||
clientId: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
tenantId: ENCRYPT_CONFIG.TENANT_ID,
|
||||
encryptHeader: ENCRYPT_CONFIG.ENCRYPT_HEADER,
|
||||
encryptEnabled: ENCRYPT_CONFIG.ENCRYPT_ENABLED,
|
||||
};
|
||||
};
|
||||
|
||||
// 导出配置以便外部修改
|
||||
export { ENCRYPT_CONFIG as config };
|
||||
@@ -1,204 +0,0 @@
|
||||
// 设备信息和兼容性工具
|
||||
export const deviceUtils = {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
try {
|
||||
return uni.getSystemInfoSync()
|
||||
} catch (e) {
|
||||
console.error('获取系统信息失败:', e)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
// 判断是否为低端设备
|
||||
isLowEndDevice() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
// 判断条件:内存小于4GB 或 CPU性能较差
|
||||
if (systemInfo.memSize && systemInfo.memSize < 4) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Android 设备版本较低
|
||||
if (systemInfo.platform === 'android' && systemInfo.system) {
|
||||
const version = parseFloat(systemInfo.system.replace('Android ', ''))
|
||||
if (version < 7.0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// iOS 设备版本较低
|
||||
if (systemInfo.platform === 'ios' && systemInfo.system) {
|
||||
const version = parseFloat(systemInfo.system.replace('iOS ', ''))
|
||||
if (version < 12.0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
// 获取屏幕适配信息
|
||||
getScreenInfo() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
return {
|
||||
screenWidth: systemInfo.screenWidth,
|
||||
screenHeight: systemInfo.screenHeight,
|
||||
statusBarHeight: systemInfo.statusBarHeight,
|
||||
safeAreaInsets: systemInfo.safeAreaInsets,
|
||||
// 判断是否为刘海屏
|
||||
hasNotch:
|
||||
systemInfo.safeAreaInsets &&
|
||||
systemInfo.safeAreaInsets.top > systemInfo.statusBarHeight,
|
||||
// 判断是否为横屏
|
||||
isLandscape: systemInfo.screenWidth > systemInfo.screenHeight,
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设备类型
|
||||
getDeviceType() {
|
||||
const systemInfo = this.getSystemInfo()
|
||||
|
||||
if (systemInfo.uniPlatform === 'mp-weixin') {
|
||||
return 'wechat-miniprogram'
|
||||
} else if (systemInfo.uniPlatform === 'h5') {
|
||||
return 'h5'
|
||||
} else if (systemInfo.uniPlatform === 'app-plus') {
|
||||
return systemInfo.platform === 'ios' ? 'ios-app' : 'android-app'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
},
|
||||
}
|
||||
|
||||
// 网络状态管理
|
||||
export const networkUtils = {
|
||||
// 获取网络状态
|
||||
getNetworkType() {
|
||||
return new Promise((resolve) => {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
resolve(res.networkType)
|
||||
},
|
||||
fail: () => {
|
||||
resolve('unknown')
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 监听网络状态变化
|
||||
onNetworkStatusChange(callback) {
|
||||
uni.onNetworkStatusChange(callback)
|
||||
},
|
||||
|
||||
// 判断是否为弱网环境
|
||||
async isWeakNetwork() {
|
||||
const networkType = await this.getNetworkType()
|
||||
return ['2g', '3g', 'slow'].includes(networkType)
|
||||
},
|
||||
|
||||
// 根据网络状况调整请求策略
|
||||
async getRequestConfig() {
|
||||
const isWeak = await this.isWeakNetwork()
|
||||
|
||||
if (isWeak) {
|
||||
return {
|
||||
timeout: 30000, // 增加超时时间
|
||||
retryTimes: 3, // 增加重试次数
|
||||
useCache: true, // 优先使用缓存
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timeout: 10000,
|
||||
retryTimes: 1,
|
||||
useCache: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 性能优化工具
|
||||
export const performanceUtils = {
|
||||
// 图片懒加载配置
|
||||
getImageLoadConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 低端设备减少同时加载的图片数量
|
||||
concurrent: isLowEnd ? 2 : 5,
|
||||
// 低端设备使用较小的图片
|
||||
quality: isLowEnd ? 'low' : 'high',
|
||||
// 预加载距离
|
||||
threshold: isLowEnd ? 100 : 300,
|
||||
}
|
||||
},
|
||||
|
||||
// 动画性能配置
|
||||
getAnimationConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 低端设备禁用复杂动画
|
||||
enableComplexAnimation: !isLowEnd,
|
||||
// 动画持续时间
|
||||
duration: isLowEnd ? 200 : 300,
|
||||
// 动画缓动函数
|
||||
easing: isLowEnd ? 'linear' : 'ease-out',
|
||||
}
|
||||
},
|
||||
|
||||
// 列表滚动优化
|
||||
getScrollConfig() {
|
||||
const isLowEnd = deviceUtils.isLowEndDevice()
|
||||
|
||||
return {
|
||||
// 虚拟滚动阈值
|
||||
virtualScrollThreshold: isLowEnd ? 50 : 100,
|
||||
// 缓冲区大小
|
||||
bufferSize: isLowEnd ? 5 : 10,
|
||||
// 是否开启上拉加载节流
|
||||
enableThrottle: isLowEnd,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 适配工具
|
||||
export const adaptUtils = {
|
||||
// 获取安全区域样式
|
||||
getSafeAreaStyle() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
|
||||
return {
|
||||
paddingTop: `${screenInfo.safeAreaInsets?.top || screenInfo.statusBarHeight || 0}px`,
|
||||
paddingBottom: `${screenInfo.safeAreaInsets?.bottom || 0}px`,
|
||||
paddingLeft: `${screenInfo.safeAreaInsets?.left || 0}px`,
|
||||
paddingRight: `${screenInfo.safeAreaInsets?.right || 0}px`,
|
||||
}
|
||||
},
|
||||
|
||||
// 获取底部安全区域高度
|
||||
getBottomSafeHeight() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
return screenInfo.safeAreaInsets?.bottom || 0
|
||||
},
|
||||
|
||||
// 获取顶部安全区域高度
|
||||
getTopSafeHeight() {
|
||||
const screenInfo = deviceUtils.getScreenInfo()
|
||||
return screenInfo.safeAreaInsets?.top || screenInfo.statusBarHeight || 0
|
||||
},
|
||||
|
||||
// rpx转px
|
||||
rpxToPx(rpx) {
|
||||
const systemInfo = deviceUtils.getSystemInfo()
|
||||
return (rpx * systemInfo.screenWidth) / 750
|
||||
},
|
||||
|
||||
// px转rpx
|
||||
pxToRpx(px) {
|
||||
const systemInfo = deviceUtils.getSystemInfo()
|
||||
return (px * 750) / systemInfo.screenWidth
|
||||
},
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 浏览器兼容版本
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {Promise<string|null>} RSA加密后的AES密钥
|
||||
*/
|
||||
export async function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
// 将Base64公钥转换为ArrayBuffer
|
||||
const publicKeyData = Uint8Array.from(atob(ENCRYPT_CONFIG.RSA_PUBLIC_KEY), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
// 导入RSA公钥
|
||||
const publicKey = await crypto.subtle.importKey(
|
||||
'spki',
|
||||
publicKeyData,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-1',
|
||||
},
|
||||
false,
|
||||
['encrypt'],
|
||||
);
|
||||
|
||||
// 将AES密钥转为Base64
|
||||
const aesKeyBase64 = btoa(aesKey);
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
},
|
||||
publicKey,
|
||||
data,
|
||||
);
|
||||
|
||||
// 转换为Base64
|
||||
const encryptedArray = new Uint8Array(encrypted);
|
||||
return btoa(String.fromCharCode.apply(null, encryptedArray));
|
||||
} catch (error) {
|
||||
console.error('RSA加密失败:', error.message);
|
||||
console.log('🔄 回退到简化加密方案...');
|
||||
|
||||
// 回退方案:使用AES密钥的Base64编码(临时方案)
|
||||
try {
|
||||
return btoa(aesKey);
|
||||
} catch (fallbackError) {
|
||||
console.error('回退加密也失败:', fallbackError.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Promise<Object>} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export async function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 使用JSEncrypt
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {string|null} RSA加密后的AES密钥
|
||||
*/
|
||||
export function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
console.log('🔧 开始RSA加密,AES密钥:', aesKey);
|
||||
|
||||
// 构造完整的PEM格式RSA公钥
|
||||
const pemKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqEfJl9KxCi6s3Fpjs234+uCvG
|
||||
SIDnR585F0jlTmWxbv0yYWe6xQDfnmhpJdwuG7nHEHfxIxLI/Yq+4fejDAMCAwEA
|
||||
AQ==
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
console.log('🔧 RSA公钥PEM格式已构造');
|
||||
|
||||
// 创建JSEncrypt实例
|
||||
const encrypt = new JSEncrypt();
|
||||
|
||||
// 设置公钥
|
||||
const keySetResult = encrypt.setPublicKey(pemKey);
|
||||
console.log('🔧 公钥设置结果:', keySetResult);
|
||||
|
||||
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
||||
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
||||
console.log('🔧 AES密钥Base64:', aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encrypted = encrypt.encrypt(aesKeyBase64);
|
||||
|
||||
console.log('🔧 RSA加密结果:', encrypted);
|
||||
|
||||
if (!encrypted) {
|
||||
throw new Error('JSEncrypt RSA加密返回null,可能是公钥格式错误或加密失败');
|
||||
}
|
||||
|
||||
console.log('✅ RSA加密成功,结果长度:', encrypted.length);
|
||||
return encrypted;
|
||||
} catch (error) {
|
||||
console.error('❌ RSA加密失败:', error.message);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Object} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
import bigInt from 'big-integer';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64解码
|
||||
*/
|
||||
function base64Decode(str) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
let result = '';
|
||||
let buffer = 0;
|
||||
let bits = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charAt(i);
|
||||
if (char === '=') break;
|
||||
|
||||
const index = chars.indexOf(char);
|
||||
if (index === -1) continue;
|
||||
|
||||
buffer = (buffer << 6) | index;
|
||||
bits += 6;
|
||||
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
result += String.fromCharCode((buffer >> bits) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析RSA公钥
|
||||
*/
|
||||
function parseRSAPublicKey(keyStr) {
|
||||
try {
|
||||
console.log('🔧 开始解析RSA公钥...');
|
||||
|
||||
// Base64解码
|
||||
const binaryStr = base64Decode(keyStr);
|
||||
const bytes = [];
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes.push(binaryStr.charCodeAt(i));
|
||||
}
|
||||
|
||||
console.log('🔧 公钥字节长度:', bytes.length);
|
||||
|
||||
// 简化的ASN.1解析 - 跳过前面的头部直接获取n和e
|
||||
// 对于我们的固定公钥,直接使用已知的n和e值
|
||||
// 这个是从公钥解析得出的正确n和e值
|
||||
const n = bigInt(
|
||||
'8907294147844816284558538478551088035165434911351789542173700276555680401825462782219485742265212984242385862425017144718658441075282309582555957891823523',
|
||||
);
|
||||
const e = bigInt('65537'); // 标准的RSA指数
|
||||
|
||||
console.log('✅ RSA公钥解析成功');
|
||||
console.log('🔧 n值长度:', n.toString().length);
|
||||
console.log('🔧 e值:', e.toString());
|
||||
|
||||
return { n, e };
|
||||
} catch (error) {
|
||||
console.error('❌ RSA公钥解析失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PKCS#1 v1.5 填充
|
||||
*/
|
||||
function pkcs1Padding(data, keyLength) {
|
||||
const dataBytes = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
dataBytes.push(data.charCodeAt(i));
|
||||
}
|
||||
|
||||
const paddingLength = keyLength - 3 - dataBytes.length;
|
||||
if (paddingLength < 8) {
|
||||
throw new Error('数据太长,无法进行PKCS#1填充');
|
||||
}
|
||||
|
||||
const padded = [0x00, 0x02];
|
||||
|
||||
// 添加随机填充字节
|
||||
for (let i = 0; i < paddingLength; i++) {
|
||||
let randomByte;
|
||||
do {
|
||||
randomByte = Math.floor(Math.random() * 256);
|
||||
} while (randomByte === 0);
|
||||
padded.push(randomByte);
|
||||
}
|
||||
|
||||
padded.push(0x00);
|
||||
padded.push(...dataBytes);
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯JavaScript RSA加密
|
||||
*/
|
||||
function rsaEncrypt(data, publicKey) {
|
||||
try {
|
||||
console.log('🔧 开始纯JavaScript RSA加密...');
|
||||
console.log('🔧 待加密数据:', data);
|
||||
|
||||
const keyLength = 64; // 512位密钥 = 64字节
|
||||
|
||||
// PKCS#1填充
|
||||
const paddedBytes = pkcs1Padding(data, keyLength);
|
||||
console.log('🔧 填充后字节长度:', paddedBytes.length);
|
||||
|
||||
// 转换为大整数
|
||||
let message = bigInt(0);
|
||||
for (let i = 0; i < paddedBytes.length; i++) {
|
||||
message = message.multiply(256).add(paddedBytes[i]);
|
||||
}
|
||||
|
||||
console.log('🔧 消息大整数长度:', message.toString().length);
|
||||
|
||||
// RSA加密: c = m^e mod n
|
||||
const encrypted = message.modPow(publicKey.e, publicKey.n);
|
||||
console.log('🔧 加密后大整数长度:', encrypted.toString().length);
|
||||
|
||||
// 转换为字节数组
|
||||
const encryptedBytes = [];
|
||||
let temp = encrypted;
|
||||
for (let i = 0; i < keyLength; i++) {
|
||||
encryptedBytes.unshift(temp.mod(256).toJSNumber());
|
||||
temp = temp.divide(256);
|
||||
}
|
||||
|
||||
// 转换为Base64
|
||||
let binaryStr = '';
|
||||
for (let i = 0; i < encryptedBytes.length; i++) {
|
||||
binaryStr += String.fromCharCode(encryptedBytes[i]);
|
||||
}
|
||||
|
||||
const base64 = btoa(binaryStr);
|
||||
console.log('✅ RSA加密成功,Base64长度:', base64.length);
|
||||
|
||||
return base64;
|
||||
} catch (error) {
|
||||
console.error('❌ 纯JavaScript RSA加密失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 使用纯JavaScript实现
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {string|null} RSA加密后的AES密钥
|
||||
*/
|
||||
export function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
console.log('🔧 开始纯JavaScript RSA加密,AES密钥:', aesKey);
|
||||
|
||||
// 解析RSA公钥
|
||||
const publicKey = parseRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
|
||||
if (!publicKey) {
|
||||
throw new Error('RSA公钥解析失败');
|
||||
}
|
||||
|
||||
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
||||
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
||||
console.log('🔧 AES密钥Base64:', aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encrypted = rsaEncrypt(aesKeyBase64, publicKey);
|
||||
|
||||
if (!encrypted) {
|
||||
throw new Error('纯JavaScript RSA加密失败');
|
||||
}
|
||||
|
||||
console.log('✅ 纯JavaScript RSA加密成功,结果长度:', encrypted.length);
|
||||
return encrypted;
|
||||
} catch (error) {
|
||||
console.error('❌ 纯JavaScript RSA加密失败:', error.message);
|
||||
console.error('❌ 错误堆栈:', error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Object} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// 加密功能测试工具
|
||||
import {
|
||||
encryptLoginData,
|
||||
generateRandomAESKey,
|
||||
encryptWithAES,
|
||||
encryptAESKeyWithRSA,
|
||||
} from './encrypt-pure.js';
|
||||
|
||||
/**
|
||||
* 测试RSA加密功能
|
||||
*/
|
||||
export async function testRSAEncryption() {
|
||||
console.log('🧪 开始测试RSA加密功能...');
|
||||
|
||||
try {
|
||||
// 测试生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('✅ AES密钥生成成功:', aesKey);
|
||||
|
||||
// 测试AES加密
|
||||
const testData = 'test data';
|
||||
const aesEncrypted = encryptWithAES(testData, aesKey);
|
||||
console.log('✅ AES加密成功:', aesEncrypted.substring(0, 50) + '...');
|
||||
|
||||
// 测试RSA加密 - 这里是关键
|
||||
const rsaEncrypted = encryptAESKeyWithRSA(aesKey);
|
||||
if (rsaEncrypted) {
|
||||
console.log('✅ RSA加密成功:', rsaEncrypted.substring(0, 50) + '...');
|
||||
console.log('✅ RSA加密结果长度:', rsaEncrypted.length);
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ RSA加密失败: 返回null');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ RSA加密测试失败:', error);
|
||||
console.error('❌ 错误详情:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试完整登录加密流程
|
||||
*/
|
||||
export async function testLoginEncryption() {
|
||||
console.log('🧪 开始测试完整登录加密流程...');
|
||||
|
||||
try {
|
||||
const loginData = {
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
tenantId: '000000',
|
||||
};
|
||||
|
||||
const result = encryptLoginData(loginData);
|
||||
|
||||
// 如果返回Promise,需要await
|
||||
const actualResult = result.then ? await result : result;
|
||||
|
||||
if (actualResult.success) {
|
||||
console.log('✅ 登录加密成功');
|
||||
console.log('✅ 加密数据长度:', actualResult.encryptedData.length);
|
||||
console.log('✅ 请求头:', JSON.stringify(actualResult.headers, null, 2));
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ 登录加密失败:', actualResult.error);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 登录加密测试失败:', error);
|
||||
console.error('❌ 错误详情:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行所有测试
|
||||
*/
|
||||
export async function runAllTests() {
|
||||
console.log('🧪 开始运行所有加密测试...');
|
||||
|
||||
try {
|
||||
const rsaTest = await testRSAEncryption();
|
||||
const loginTest = await testLoginEncryption();
|
||||
|
||||
if (rsaTest && loginTest) {
|
||||
console.log('🎉 所有加密测试通过!');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ 存在测试失败!');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 测试执行失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
|
||||
RSA_PUBLIC_KEY:
|
||||
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000',
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64字符串转ArrayBuffer
|
||||
* @param {string} base64
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
function base64ToArrayBuffer(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* ArrayBuffer转Base64字符串
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns {string}
|
||||
*/
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入RSA公钥
|
||||
* @param {string} publicKeyBase64 Base64格式的公钥
|
||||
* @returns {Promise<CryptoKey>}
|
||||
*/
|
||||
async function importRSAPublicKey(publicKeyBase64) {
|
||||
try {
|
||||
const keyBuffer = base64ToArrayBuffer(publicKeyBase64);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'spki',
|
||||
keyBuffer,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-1',
|
||||
},
|
||||
false,
|
||||
['encrypt'],
|
||||
);
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
console.error('导入RSA公钥失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥 - 使用Web Crypto API
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {Promise<string|null>} RSA加密后的AES密钥
|
||||
*/
|
||||
export async function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
// 检查Web Crypto API支持
|
||||
if (!crypto || !crypto.subtle) {
|
||||
throw new Error('当前环境不支持Web Crypto API');
|
||||
}
|
||||
|
||||
// 导入RSA公钥
|
||||
const publicKey = await importRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
|
||||
|
||||
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
|
||||
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
|
||||
|
||||
// 将字符串转为ArrayBuffer
|
||||
const dataBuffer = new TextEncoder().encode(aesKeyBase64);
|
||||
|
||||
// RSA加密
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
},
|
||||
publicKey,
|
||||
dataBuffer,
|
||||
);
|
||||
|
||||
// 转为Base64字符串
|
||||
const encryptedBase64 = arrayBufferToBase64(encryptedBuffer);
|
||||
|
||||
console.log('✅ Web Crypto RSA加密成功,结果长度:', encryptedBase64.length);
|
||||
return encryptedBase64;
|
||||
} catch (error) {
|
||||
console.error('Web Crypto RSA加密失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Promise<Object>} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export async function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥 - 异步操作
|
||||
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import CryptoJS from 'crypto-js';
|
||||
import NodeRSA from 'node-rsa';
|
||||
|
||||
// 配置信息
|
||||
const ENCRYPT_CONFIG = {
|
||||
// RSA公钥 - 与后端保持一致
|
||||
RSA_PUBLIC_KEY: 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7h96MCAwEAAQ==',
|
||||
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
TENANT_ID: '000000'
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机AES密钥
|
||||
* @param {number} length 密钥长度,默认32位
|
||||
* @returns {string} AES密钥
|
||||
*/
|
||||
export function generateRandomAESKey(length = 32) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密数据
|
||||
* @param {string} data 待加密的数据
|
||||
* @param {string} key AES密钥
|
||||
* @returns {string} 加密后的数据
|
||||
*/
|
||||
export function encryptWithAES(data, key) {
|
||||
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密AES密钥
|
||||
* @param {string} aesKey AES密钥
|
||||
* @returns {string|null} RSA加密后的AES密钥
|
||||
*/
|
||||
export function encryptAESKeyWithRSA(aesKey) {
|
||||
try {
|
||||
const pemKey = `-----BEGIN PUBLIC KEY-----\n${ENCRYPT_CONFIG.RSA_PUBLIC_KEY}\n-----END PUBLIC KEY-----`;
|
||||
const rsaKey = new NodeRSA(pemKey);
|
||||
rsaKey.setOptions({ encryptionScheme: 'pkcs1' });
|
||||
|
||||
// 将AES密钥转为Base64
|
||||
const aesKeyBase64 = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(aesKey));
|
||||
|
||||
// RSA加密
|
||||
return rsaKey.encrypt(aesKeyBase64, 'base64');
|
||||
} catch (error) {
|
||||
console.error('RSA加密失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录数据加密处理
|
||||
* @param {Object} loginData 登录数据对象
|
||||
* @returns {Object} 包含加密数据和请求头的对象
|
||||
*/
|
||||
export function encryptLoginData(loginData) {
|
||||
try {
|
||||
// 1. 生成AES密钥
|
||||
const aesKey = generateRandomAESKey(32);
|
||||
console.log('🔑 生成AES密钥:', aesKey);
|
||||
|
||||
// 2. 准备完整的登录数据
|
||||
const fullLoginData = {
|
||||
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
|
||||
username: loginData.username,
|
||||
password: loginData.password,
|
||||
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
|
||||
grantType: loginData.grantType || 'password',
|
||||
};
|
||||
|
||||
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
|
||||
|
||||
// 3. AES加密登录数据
|
||||
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
|
||||
console.log('🔐 AES加密后数据长度:', encryptedData.length);
|
||||
|
||||
// 4. RSA加密AES密钥
|
||||
const encryptedKey = encryptAESKeyWithRSA(aesKey);
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
|
||||
|
||||
// 5. 返回加密结果和请求头
|
||||
return {
|
||||
success: true,
|
||||
encryptedData: encryptedData,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
clientid: ENCRYPT_CONFIG.CLIENT_ID,
|
||||
'encrypt-key': encryptedKey,
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
isToken: 'false',
|
||||
isEncrypt: 'true',
|
||||
repeatSubmit: 'false',
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录数据加密失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密配置
|
||||
*/
|
||||
export function getEncryptConfig() {
|
||||
return ENCRYPT_CONFIG;
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
/**
|
||||
* 手势识别工具类
|
||||
* 支持滑动、长按、双击、捏合缩放等手势操作
|
||||
*/
|
||||
|
||||
class GestureUtils {
|
||||
constructor() {
|
||||
this.touchStartTime = 0;
|
||||
this.touchStartPos = { x: 0, y: 0 };
|
||||
this.touchMovePos = { x: 0, y: 0 };
|
||||
this.lastTouchTime = 0;
|
||||
this.tapCount = 0;
|
||||
this.longPressTimer = null;
|
||||
this.longPressDelay = 500; // 长按延迟时间
|
||||
this.doubleTapDelay = 300; // 双击延迟时间
|
||||
this.swipeThreshold = 50; // 滑动阈值
|
||||
this.gestureCallbacks = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化手势监听
|
||||
* @param {HTMLElement} element - 监听元素
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
init(element, options = {}) {
|
||||
if (!element) return;
|
||||
|
||||
const config = {
|
||||
enableSwipe: true,
|
||||
enableLongPress: true,
|
||||
enableDoubleTap: true,
|
||||
enablePinch: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
// 存储元素和配置
|
||||
this.element = element;
|
||||
this.config = config;
|
||||
|
||||
// 绑定事件监听器
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定触摸事件
|
||||
*/
|
||||
bindEvents() {
|
||||
if (!this.element) return;
|
||||
|
||||
// 阻止默认的触摸行为
|
||||
this.element.style.touchAction = 'manipulation';
|
||||
|
||||
// 绑定事件
|
||||
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
this.element.addEventListener('touchcancel', this.handleTouchCancel.bind(this), {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
// 绑定鼠标事件(用于桌面端测试)
|
||||
this.element.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
this.element.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
this.element.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册手势回调
|
||||
* @param {string} gesture - 手势类型
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
on(gesture, callback) {
|
||||
if (!this.gestureCallbacks.has(gesture)) {
|
||||
this.gestureCallbacks.set(gesture, []);
|
||||
}
|
||||
this.gestureCallbacks.get(gesture).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发手势回调
|
||||
* @param {string} gesture - 手势类型
|
||||
* @param {Object} data - 手势数据
|
||||
*/
|
||||
trigger(gesture, data = {}) {
|
||||
const callbacks = this.gestureCallbacks.get(gesture);
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸开始
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchStart(e) {
|
||||
const touch = e.touches[0];
|
||||
this.touchStartTime = Date.now();
|
||||
this.touchStartPos = { x: touch.clientX, y: touch.clientY };
|
||||
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
|
||||
|
||||
// 长按检测
|
||||
if (this.config.enableLongPress) {
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
this.trigger('longpress', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
|
||||
// 触觉反馈
|
||||
if (uni && uni.vibrateShort) {
|
||||
uni.vibrateShort();
|
||||
}
|
||||
}, this.longPressDelay);
|
||||
}
|
||||
|
||||
// 双击检测
|
||||
if (this.config.enableDoubleTap) {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - this.lastTouchTime < this.doubleTapDelay) {
|
||||
this.tapCount++;
|
||||
} else {
|
||||
this.tapCount = 1;
|
||||
}
|
||||
this.lastTouchTime = currentTime;
|
||||
}
|
||||
|
||||
// 阻止默认行为(可选)
|
||||
if (this.config.preventDefault !== false) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸移动
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchMove(e) {
|
||||
const touch = e.touches[0];
|
||||
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
|
||||
|
||||
// 如果移动了,取消长按
|
||||
if (this.longPressTimer) {
|
||||
const distance = this.getDistance(this.touchStartPos, this.touchMovePos);
|
||||
if (distance > 10) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 滑动检测
|
||||
if (this.config.enableSwipe) {
|
||||
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
|
||||
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
|
||||
|
||||
this.trigger('swipemove', {
|
||||
deltaX,
|
||||
deltaY,
|
||||
startPos: this.touchStartPos,
|
||||
currentPos: this.touchMovePos,
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
|
||||
// 捏合缩放检测
|
||||
if (this.config.enablePinch && e.touches.length === 2) {
|
||||
this.handlePinch(e);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸结束
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchEnd(e) {
|
||||
const touchEndTime = Date.now();
|
||||
const touchDuration = touchEndTime - this.touchStartTime;
|
||||
|
||||
// 清除长按定时器
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
|
||||
// 滑动检测
|
||||
if (this.config.enableSwipe) {
|
||||
this.detectSwipe();
|
||||
}
|
||||
|
||||
// 点击和双击检测
|
||||
if (this.config.enableDoubleTap && touchDuration < 300) {
|
||||
setTimeout(() => {
|
||||
if (this.tapCount === 1) {
|
||||
this.trigger('tap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
} else if (this.tapCount === 2) {
|
||||
this.trigger('doubletap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
|
||||
// 触觉反馈
|
||||
if (uni && uni.vibrateShort) {
|
||||
uni.vibrateShort();
|
||||
}
|
||||
}
|
||||
this.tapCount = 0;
|
||||
}, this.doubleTapDelay);
|
||||
} else if (touchDuration < 300) {
|
||||
this.trigger('tap', {
|
||||
position: this.touchStartPos,
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸取消
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handleTouchCancel(e) {
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
this.tapCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测滑动方向和距离
|
||||
*/
|
||||
detectSwipe() {
|
||||
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
|
||||
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
if (distance < this.swipeThreshold) return;
|
||||
|
||||
const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
|
||||
let direction = '';
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// 水平滑动
|
||||
direction = deltaX > 0 ? 'right' : 'left';
|
||||
} else {
|
||||
// 垂直滑动
|
||||
direction = deltaY > 0 ? 'down' : 'up';
|
||||
}
|
||||
|
||||
this.trigger('swipe', {
|
||||
direction,
|
||||
distance,
|
||||
angle,
|
||||
deltaX,
|
||||
deltaY,
|
||||
startPos: this.touchStartPos,
|
||||
endPos: this.touchMovePos,
|
||||
velocity: distance / (Date.now() - this.touchStartTime),
|
||||
});
|
||||
|
||||
// 触发特定方向的滑动事件
|
||||
this.trigger(`swipe${direction}`, {
|
||||
distance,
|
||||
velocity: distance / (Date.now() - this.touchStartTime),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理捏合缩放
|
||||
* @param {TouchEvent} e - 触摸事件
|
||||
*/
|
||||
handlePinch(e) {
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
|
||||
const currentDistance = this.getDistance(
|
||||
{ x: touch1.clientX, y: touch1.clientY },
|
||||
{ x: touch2.clientX, y: touch2.clientY },
|
||||
);
|
||||
|
||||
if (!this.initialPinchDistance) {
|
||||
this.initialPinchDistance = currentDistance;
|
||||
return;
|
||||
}
|
||||
|
||||
const scale = currentDistance / this.initialPinchDistance;
|
||||
|
||||
this.trigger('pinch', {
|
||||
scale,
|
||||
center: {
|
||||
x: (touch1.clientX + touch2.clientX) / 2,
|
||||
y: (touch1.clientY + touch2.clientY) / 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点距离
|
||||
* @param {Object} point1 - 点1
|
||||
* @param {Object} point2 - 点2
|
||||
* @returns {number} 距离
|
||||
*/
|
||||
getDistance(point1, point2) {
|
||||
const deltaX = point2.x - point1.x;
|
||||
const deltaY = point2.y - point1.y;
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标事件处理(用于桌面端测试)
|
||||
*/
|
||||
handleMouseDown(e) {
|
||||
this.isMouseDown = true;
|
||||
this.handleTouchStart({
|
||||
touches: [{ clientX: e.clientX, clientY: e.clientY }],
|
||||
target: e.target,
|
||||
preventDefault: () => e.preventDefault(),
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
if (!this.isMouseDown) return;
|
||||
this.handleTouchMove({
|
||||
touches: [{ clientX: e.clientX, clientY: e.clientY }],
|
||||
target: e.target,
|
||||
preventDefault: () => e.preventDefault(),
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
if (!this.isMouseDown) return;
|
||||
this.isMouseDown = false;
|
||||
this.handleTouchEnd({
|
||||
target: e.target,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁手势监听
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.element) return;
|
||||
|
||||
// 移除事件监听器
|
||||
this.element.removeEventListener('touchstart', this.handleTouchStart);
|
||||
this.element.removeEventListener('touchmove', this.handleTouchMove);
|
||||
this.element.removeEventListener('touchend', this.handleTouchEnd);
|
||||
this.element.removeEventListener('touchcancel', this.handleTouchCancel);
|
||||
this.element.removeEventListener('mousedown', this.handleMouseDown);
|
||||
this.element.removeEventListener('mousemove', this.handleMouseMove);
|
||||
this.element.removeEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
// 清除定时器
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
}
|
||||
|
||||
// 清除回调
|
||||
this.gestureCallbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面滑动切换功能
|
||||
*/
|
||||
class PageSwipeGesture {
|
||||
constructor() {
|
||||
this.currentPageIndex = 0;
|
||||
this.pages = ['home'];
|
||||
this.isAnimating = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
const gesture = new GestureUtils();
|
||||
gesture.init(document.body, {
|
||||
enableSwipe: true,
|
||||
enableLongPress: false,
|
||||
enableDoubleTap: false,
|
||||
});
|
||||
|
||||
// 只有一个页面,禁用滑动切换
|
||||
return gesture;
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
// 只有一个页面,不执行切换
|
||||
return;
|
||||
}
|
||||
|
||||
prevPage() {
|
||||
// 只有一个页面,不执行切换
|
||||
return;
|
||||
}
|
||||
|
||||
switchPage(pageName) {
|
||||
this.isAnimating = true;
|
||||
|
||||
const pageUrls = {
|
||||
home: '/pages/index/index',
|
||||
};
|
||||
|
||||
if (pageUrls[pageName]) {
|
||||
uni.navigateTo({
|
||||
url: pageUrls[pageName],
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
this.isAnimating = false;
|
||||
}, 300);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 长按菜单功能
|
||||
*/
|
||||
class LongPressMenu {
|
||||
constructor() {
|
||||
this.menu = null;
|
||||
this.isShowing = false;
|
||||
}
|
||||
|
||||
show(position, items = []) {
|
||||
if (this.isShowing) return;
|
||||
|
||||
this.isShowing = true;
|
||||
|
||||
// 创建菜单HTML
|
||||
const menuHtml = `
|
||||
<div class="longpress-menu" style="
|
||||
position: fixed;
|
||||
left: ${position.x}px;
|
||||
top: ${position.y}px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
">
|
||||
${items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="menu-item" style="
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
" data-action="${item.action}">
|
||||
${item.icon} ${item.text}
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', menuHtml);
|
||||
this.menu = document.querySelector('.longpress-menu');
|
||||
|
||||
// 绑定点击事件
|
||||
this.menu.addEventListener('click', (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
if (action && items.find((item) => item.action === action)) {
|
||||
const item = items.find((item) => item.action === action);
|
||||
if (item.callback) item.callback();
|
||||
}
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// 点击其他地方隐藏菜单
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.hideOnClickOutside.bind(this));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.isShowing || !this.menu) return;
|
||||
|
||||
this.menu.style.animation = 'fadeOut 0.2s ease-in forwards';
|
||||
setTimeout(() => {
|
||||
if (this.menu && this.menu.parentNode) {
|
||||
this.menu.remove();
|
||||
}
|
||||
this.isShowing = false;
|
||||
}, 200);
|
||||
|
||||
document.removeEventListener('click', this.hideOnClickOutside);
|
||||
}
|
||||
|
||||
hideOnClickOutside(e) {
|
||||
if (this.menu && !this.menu.contains(e.target)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出工具类
|
||||
export { GestureUtils, PageSwipeGesture, LongPressMenu };
|
||||
export default GestureUtils;
|
||||
@@ -1,192 +0,0 @@
|
||||
/**
|
||||
* iconfont 图标管理工具
|
||||
* 帮助开发者快速添加和管理自定义图标
|
||||
*/
|
||||
|
||||
/**
|
||||
* SVG Path 转换为 iconfont CSS 代码
|
||||
* @param {string} iconName - 图标名称 (如: star, heart)
|
||||
* @param {string} svgPath - SVG path 数据
|
||||
* @param {string} viewBox - SVG viewBox, 默认 "0 0 24 24"
|
||||
* @returns {string} CSS 代码
|
||||
*/
|
||||
export function generateIconCSS(iconName, svgPath, viewBox = '0 0 24 24') {
|
||||
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${viewBox}' fill='currentColor'><path d='${svgPath}'/></svg>`;
|
||||
const encodedSvg = encodeURIComponent(svg).replace(/'/g, '%27').replace(/"/g, '%22');
|
||||
|
||||
return `/* ${iconName}图标 */
|
||||
.icon-${iconName} {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成图标 CSS
|
||||
* @param {Array} icons - 图标数组 [{name, path, viewBox}]
|
||||
* @returns {string} 完整的 CSS 代码
|
||||
*/
|
||||
export function generateBatchIconCSS(icons) {
|
||||
return icons.map((icon) => generateIconCSS(icon.name, icon.path, icon.viewBox)).join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从完整SVG生成CSS
|
||||
* @param {string} iconName - 图标名称
|
||||
* @param {string} svgContent - 完整的SVG内容
|
||||
* @returns {string} CSS 代码
|
||||
*/
|
||||
export function generateIconCSSFromSVG(iconName, svgContent) {
|
||||
const encodedSvg = encodeURIComponent(svgContent).replace(/'/g, '%27').replace(/"/g, '%22');
|
||||
|
||||
return `/* ${iconName}图标 */
|
||||
.icon-${iconName} {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的常用图标路径
|
||||
* 来源:Material Design Icons
|
||||
*/
|
||||
export const COMMON_ICONS = {
|
||||
// 基础图标
|
||||
user: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
|
||||
lock: 'M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z',
|
||||
home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
|
||||
setting:
|
||||
'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z',
|
||||
|
||||
// 操作图标
|
||||
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
||||
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
|
||||
edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
|
||||
delete: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
|
||||
search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z',
|
||||
check: 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z',
|
||||
|
||||
// 状态图标
|
||||
success:
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
|
||||
warning: 'M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z',
|
||||
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
|
||||
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
|
||||
|
||||
// 文件图标
|
||||
download: 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z',
|
||||
upload: 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z',
|
||||
|
||||
// 导航图标
|
||||
arrowLeft: 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z',
|
||||
arrowRight: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z',
|
||||
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z',
|
||||
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z',
|
||||
|
||||
// 社交图标
|
||||
star: 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
|
||||
heart: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
|
||||
message:
|
||||
'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z',
|
||||
phone: 'M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z',
|
||||
mail: 'M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z',
|
||||
};
|
||||
|
||||
/**
|
||||
* 快速生成常用图标的CSS
|
||||
* @param {string[]} iconNames - 图标名称数组
|
||||
* @returns {string} CSS 代码
|
||||
*/
|
||||
export function generateCommonIconsCSS(iconNames) {
|
||||
const selectedIcons = iconNames
|
||||
.map((name) => ({
|
||||
name,
|
||||
path: COMMON_ICONS[name],
|
||||
}))
|
||||
.filter((icon) => icon.path);
|
||||
|
||||
return generateBatchIconCSS(selectedIcons);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查图标名称是否已存在
|
||||
* @param {string} iconName - 图标名称
|
||||
* @param {string} cssContent - 当前CSS文件内容
|
||||
* @returns {boolean} 是否已存在
|
||||
*/
|
||||
export function checkIconExists(iconName, cssContent) {
|
||||
return cssContent.includes(`.icon-${iconName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证SVG内容
|
||||
* @param {string} svgContent - SVG内容
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export function validateSVG(svgContent) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
|
||||
return !doc.querySelector('parsererror');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例
|
||||
*/
|
||||
export const USAGE_EXAMPLES = {
|
||||
// 单个图标生成
|
||||
singleIcon: {
|
||||
code: `import { generateIconCSS } from '@/utils/iconHelper';
|
||||
|
||||
const css = generateIconCSS(
|
||||
'calendar',
|
||||
'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z'
|
||||
);
|
||||
console.log(css);`,
|
||||
output: `/* calendar图标 */
|
||||
.icon-calendar {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,...");
|
||||
}`,
|
||||
},
|
||||
|
||||
// 批量图标生成
|
||||
batchIcons: {
|
||||
code: `import { generateCommonIconsCSS } from '@/utils/iconHelper';
|
||||
|
||||
const css = generateCommonIconsCSS(['star', 'heart', 'message']);
|
||||
console.log(css);`,
|
||||
output: `/* star图标 */
|
||||
.icon-star { ... }
|
||||
|
||||
/* heart图标 */
|
||||
.icon-heart { ... }
|
||||
|
||||
/* message图标 */
|
||||
.icon-message { ... }`,
|
||||
},
|
||||
|
||||
// 从SVG生成
|
||||
fromSVG: {
|
||||
code: `import { generateIconCSSFromSVG } from '@/utils/iconHelper';
|
||||
|
||||
const svgContent = \`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
|
||||
</svg>\`;
|
||||
|
||||
const css = generateIconCSSFromSVG('circle', svgContent);
|
||||
console.log(css);`,
|
||||
},
|
||||
};
|
||||
|
||||
// 导出所有工具函数
|
||||
export default {
|
||||
generateIconCSS,
|
||||
generateBatchIconCSS,
|
||||
generateIconCSSFromSVG,
|
||||
generateCommonIconsCSS,
|
||||
checkIconExists,
|
||||
validateSVG,
|
||||
COMMON_ICONS,
|
||||
USAGE_EXAMPLES,
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 移动端权限校验工具
|
||||
* 参考web端的permission.ts实现
|
||||
* 参考web端的permission.ts实现,适配mobile2工程
|
||||
*/
|
||||
|
||||
import { userManager } from '@/utils/user'
|
||||
import { userStorage } from '@/common/storage';
|
||||
|
||||
/**
|
||||
* 字符权限校验
|
||||
@@ -12,28 +12,28 @@ import { userManager } from '@/utils/user'
|
||||
*/
|
||||
export const checkPermi = (value) => {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const userInfo = userManager.getCurrentUser()
|
||||
const userInfo = userStorage.getUserInfo();
|
||||
if (!userInfo || !userInfo.permissions) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const permissions = userInfo.permissions
|
||||
const permissionDatas = value
|
||||
const all_permission = '*:*:*'
|
||||
const permissions = userInfo.permissions;
|
||||
const permissionDatas = value;
|
||||
const all_permission = '*:*:*';
|
||||
|
||||
const hasPermission = permissions.some((permission) => {
|
||||
return all_permission === permission || permissionDatas.includes(permission)
|
||||
})
|
||||
return all_permission === permission || permissionDatas.includes(permission);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
} else {
|
||||
console.error('need roles! Like checkPermi="[\'system:user:add\',\'system:user:edit\']"')
|
||||
return false
|
||||
console.error("need roles! Like checkPermi=\"['system:user:add','system:user:edit']\"");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 角色权限校验
|
||||
@@ -42,28 +42,28 @@ export const checkPermi = (value) => {
|
||||
*/
|
||||
export const checkRole = (value) => {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const userInfo = userManager.getCurrentUser()
|
||||
const userInfo = userStorage.getUserInfo();
|
||||
if (!userInfo || !userInfo.roles) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const roles = userInfo.roles
|
||||
const permissionRoles = value
|
||||
const super_admin = 'admin'
|
||||
const roles = userInfo.roles;
|
||||
const permissionRoles = value;
|
||||
const super_admin = 'admin';
|
||||
|
||||
const hasRole = roles.some((role) => {
|
||||
return super_admin === role || permissionRoles.includes(role)
|
||||
})
|
||||
return super_admin === role || permissionRoles.includes(role);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
} else {
|
||||
console.error('need roles! Like checkRole="[\'admin\',\'editor\']"')
|
||||
return false
|
||||
console.error("need roles! Like checkRole=\"['admin','editor']\"");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 权限指令校验函数
|
||||
@@ -72,18 +72,18 @@ export const checkRole = (value) => {
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const hasPermission = (permissions) => {
|
||||
if (!permissions) return true
|
||||
if (!permissions) return true;
|
||||
|
||||
// 如果传入的是字符串,转换为数组
|
||||
const perms = Array.isArray(permissions) ? permissions : [permissions]
|
||||
return checkPermi(perms)
|
||||
}
|
||||
const perms = Array.isArray(permissions) ? permissions : [permissions];
|
||||
return checkPermi(perms);
|
||||
};
|
||||
|
||||
/**
|
||||
* Vue过滤器:权限校验
|
||||
* 在模板中使用:v-if="hasPermi(['system:user:add'])"
|
||||
*/
|
||||
export const hasPermi = hasPermission
|
||||
export const hasPermi = hasPermission;
|
||||
|
||||
/**
|
||||
* 用户角色校验
|
||||
@@ -91,12 +91,12 @@ export const hasPermi = hasPermission
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const hasRole = (roles) => {
|
||||
if (!roles) return true
|
||||
if (!roles) return true;
|
||||
|
||||
// 如果传入的是字符串,转换为数组
|
||||
const roleList = Array.isArray(roles) ? roles : [roles]
|
||||
return checkRole(roleList)
|
||||
}
|
||||
const roleList = Array.isArray(roles) ? roles : [roles];
|
||||
return checkRole(roleList);
|
||||
};
|
||||
|
||||
/**
|
||||
* 权限控制混入
|
||||
@@ -106,17 +106,17 @@ export const permissionMixin = {
|
||||
methods: {
|
||||
// 权限校验方法
|
||||
hasPermi(permissions) {
|
||||
return hasPermission(permissions)
|
||||
return hasPermission(permissions);
|
||||
},
|
||||
|
||||
// 角色校验方法
|
||||
hasRole(roles) {
|
||||
return hasRole(roles)
|
||||
return hasRole(roles);
|
||||
},
|
||||
|
||||
// 检查单个权限
|
||||
checkPermission(permission) {
|
||||
return hasPermission([permission])
|
||||
return hasPermission([permission]);
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -1,615 +0,0 @@
|
||||
import {
|
||||
generateAesKey,
|
||||
encryptBase64,
|
||||
decryptBase64,
|
||||
encryptWithAes,
|
||||
decryptWithAes,
|
||||
encrypt,
|
||||
decrypt,
|
||||
ENCRYPT_CONFIG,
|
||||
updateEncryptConfig,
|
||||
validateKeyPair,
|
||||
getConfigSummary,
|
||||
} from './crypto.js';
|
||||
|
||||
// API请求配置 - 可配置
|
||||
const REQUEST_CONFIG = {
|
||||
// 基础URL配置
|
||||
baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com',
|
||||
|
||||
// 请求超时时间
|
||||
timeout: 300000, // 5分钟,适合大数据量导出
|
||||
|
||||
// 客户端ID
|
||||
clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e',
|
||||
|
||||
// 默认referer
|
||||
referer: 'http://localhost/login?redirect=/index',
|
||||
|
||||
// 是否启用加密
|
||||
encryptEnabled: true,
|
||||
|
||||
// 防重复提交间隔时间(ms)
|
||||
repeatSubmitInterval: 500,
|
||||
};
|
||||
|
||||
// 会话缓存工具
|
||||
const sessionCache = {
|
||||
set: (key, value) => {
|
||||
try {
|
||||
uni.setStorageSync(`session_${key}`, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.error('设置会话缓存失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
get: (key) => {
|
||||
try {
|
||||
const data = uni.getStorageSync(`session_${key}`);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (e) {
|
||||
console.error('获取会话缓存失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key) => {
|
||||
try {
|
||||
uni.removeStorageSync(`session_${key}`);
|
||||
} catch (e) {
|
||||
console.error('删除会话缓存失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 统一请求方法
|
||||
export const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 获取token
|
||||
const token = getToken();
|
||||
|
||||
// 处理请求配置
|
||||
const config = {
|
||||
url: REQUEST_CONFIG.baseURL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || REQUEST_CONFIG.timeout,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: token,
|
||||
clientid: options.clientId || REQUEST_CONFIG.clientId,
|
||||
// 注意:在H5环境中,浏览器会自动设置referer,手动设置会报错
|
||||
// referer: options.referer || REQUEST_CONFIG.referer,
|
||||
...options.header,
|
||||
},
|
||||
};
|
||||
|
||||
// 检查特殊header配置
|
||||
const isToken = options.header?.isToken === false;
|
||||
const isRepeatSubmit = options.header?.repeatSubmit === false;
|
||||
const isEncrypt = options.header?.isEncrypt === 'true';
|
||||
|
||||
// 如果不需要token,移除authorization
|
||||
if (isToken) {
|
||||
delete config.header.authorization;
|
||||
}
|
||||
|
||||
// 防重复提交检查
|
||||
if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'PUT')) {
|
||||
const requestObj = {
|
||||
url: config.url,
|
||||
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||
time: new Date().getTime(),
|
||||
};
|
||||
|
||||
const sessionObj = sessionCache.get('sessionObj');
|
||||
if (sessionObj) {
|
||||
const s_url = sessionObj.url;
|
||||
const s_data = sessionObj.data;
|
||||
const s_time = sessionObj.time;
|
||||
const interval = REQUEST_CONFIG.repeatSubmitInterval;
|
||||
|
||||
if (
|
||||
s_data === requestObj.data &&
|
||||
requestObj.time - s_time < interval &&
|
||||
s_url === requestObj.url
|
||||
) {
|
||||
const message = '数据正在处理,请勿重复提交';
|
||||
console.warn(`[${s_url}]: ` + message);
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(new Error(message));
|
||||
}
|
||||
}
|
||||
sessionCache.set('sessionObj', requestObj);
|
||||
}
|
||||
|
||||
// 加密处理
|
||||
if (
|
||||
REQUEST_CONFIG.encryptEnabled &&
|
||||
isEncrypt &&
|
||||
(config.method === 'POST' || config.method === 'PUT')
|
||||
) {
|
||||
try {
|
||||
console.log('🔐 开始请求加密处理...');
|
||||
|
||||
// 生成AES密钥
|
||||
const aesKey = generateAesKey();
|
||||
|
||||
// 使用RSA加密AES密钥
|
||||
const encryptedKey = encrypt(encryptBase64(aesKey));
|
||||
if (!encryptedKey) {
|
||||
throw new Error('RSA加密AES密钥失败');
|
||||
}
|
||||
|
||||
// 设置加密头
|
||||
config.header[ENCRYPT_CONFIG.ENCRYPT_HEADER] = encryptedKey;
|
||||
|
||||
// 加密请求数据
|
||||
const dataToEncrypt =
|
||||
typeof config.data === 'object' ? JSON.stringify(config.data) : config.data;
|
||||
config.data = encryptWithAes(dataToEncrypt, aesKey);
|
||||
|
||||
console.log('✅ 请求数据加密完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 请求加密失败:', error);
|
||||
uni.showToast({
|
||||
title: '请求加密失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// FormData处理
|
||||
if (config.data instanceof globalThis.FormData) {
|
||||
delete config.header['Content-Type'];
|
||||
}
|
||||
|
||||
console.log('📤 发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
hasToken: !!token,
|
||||
isEncrypt: isEncrypt,
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
uni.request({
|
||||
...config,
|
||||
success: (res) => {
|
||||
console.log('📥 收到响应:', {
|
||||
statusCode: res.statusCode,
|
||||
url: config.url,
|
||||
hasEncryptHeader: !!res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER],
|
||||
});
|
||||
|
||||
// 解密处理
|
||||
if (REQUEST_CONFIG.encryptEnabled && res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER]) {
|
||||
try {
|
||||
console.log('🔓 开始响应解密处理...');
|
||||
|
||||
// 获取加密的AES密钥
|
||||
const keyStr = res.header[ENCRYPT_CONFIG.ENCRYPT_HEADER];
|
||||
|
||||
// RSA解密获取AES密钥
|
||||
const base64Str = decrypt(keyStr);
|
||||
if (!base64Str) {
|
||||
throw new Error('RSA解密AES密钥失败');
|
||||
}
|
||||
|
||||
// Base64解码获取AES密钥
|
||||
const aesKey = decryptBase64(base64Str);
|
||||
|
||||
// AES解密响应数据
|
||||
const decryptedData = decryptWithAes(res.data, aesKey);
|
||||
|
||||
// 解析JSON
|
||||
res.data = JSON.parse(decryptedData);
|
||||
|
||||
console.log('✅ 响应数据解密完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 响应解密失败:', error);
|
||||
uni.showToast({
|
||||
title: '响应解密失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
if (res.statusCode === 200) {
|
||||
// 检查业务状态码
|
||||
if (res.data && res.data.code !== undefined) {
|
||||
if (res.data.code === 200) {
|
||||
resolve(res.data);
|
||||
} else if (res.data.code === 401) {
|
||||
// 401未授权,跳转到登录页
|
||||
console.log('🔒 Token失效,自动跳转到登录页');
|
||||
handleUnauthorized(res.data.msg || '登录已失效,请重新登录');
|
||||
reject(res.data);
|
||||
} else {
|
||||
// 其他业务错误
|
||||
const errorMsg = res.data.msg || '请求失败';
|
||||
if (res.data.code !== 404) {
|
||||
// 404错误静默处理
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
reject(res.data);
|
||||
}
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// HTTP 401未授权
|
||||
console.log('🔒 HTTP 401未授权,自动跳转到登录页');
|
||||
handleUnauthorized('登录已失效,请重新登录');
|
||||
reject(res);
|
||||
} else if (res.statusCode === 404) {
|
||||
// 404错误静默处理
|
||||
console.log('404错误被静默处理,不显示用户提示');
|
||||
reject(res);
|
||||
} else {
|
||||
// 其他HTTP状态码错误
|
||||
const errorMsg = `请求失败: ${res.statusCode}`;
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('❌ 网络请求失败:', error);
|
||||
let message = '网络请求失败';
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时,请检查网络连接';
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败,请检查网络';
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
});
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取token
|
||||
function getToken() {
|
||||
try {
|
||||
const token = uni.getStorageSync('access_token');
|
||||
console.log(
|
||||
'🔍 获取Token:',
|
||||
token ? `Bearer ${token.substring(0, 20)}...` : '❌ Token为空',
|
||||
);
|
||||
return token ? `Bearer ${token}` : '';
|
||||
} catch (e) {
|
||||
console.error('❌ 获取Token失败:', e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 处理401未授权响应
|
||||
function handleUnauthorized(message = '登录已失效,请重新登录') {
|
||||
console.log('🔒 处理401未授权响应:', message);
|
||||
|
||||
// 清除登录信息
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
console.log('📤 已清除本地登录信息');
|
||||
} catch (e) {
|
||||
console.error('❌ 清除登录信息失败:', e);
|
||||
}
|
||||
|
||||
// 显示提示信息
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 延迟跳转,确保Toast显示
|
||||
setTimeout(() => {
|
||||
// 获取当前页面路径
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPath = '/' + currentPage.route;
|
||||
|
||||
// 如果当前不是登录页,则跳转到登录页
|
||||
if (currentPath !== '/pages/login/index') {
|
||||
console.log('🔄 从', currentPath, '跳转到登录页');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
fail: (error) => {
|
||||
console.error('❌ 跳转登录页失败:', error);
|
||||
// 如果reLaunch失败,尝试其他方式
|
||||
uni.redirectTo({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// API方法封装
|
||||
export const api = {
|
||||
// 用户登录 - 使用统一的request方法和加密拦截器
|
||||
login: (data) => {
|
||||
console.log('🔐 开始登录流程...');
|
||||
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
tenantId: data.tenantId || '000000',
|
||||
clientId: data.clientId || REQUEST_CONFIG.clientId,
|
||||
grantType: data.grantType || 'password',
|
||||
},
|
||||
header: {
|
||||
isToken: false, // 登录接口不需要token
|
||||
isEncrypt: 'true', // 启用加密
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
const token = getToken();
|
||||
console.log(
|
||||
'🎫 获取用户信息,当前token:',
|
||||
token ? token.substring(0, 20) + '...' : '无token',
|
||||
);
|
||||
|
||||
return request({
|
||||
url: '/system/user/getInfo',
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
|
||||
// 获取租户列表
|
||||
getTenantList: (isToken = false) =>
|
||||
request({
|
||||
url: '/auth/tenant/list',
|
||||
method: 'GET',
|
||||
header: {
|
||||
isToken: isToken,
|
||||
},
|
||||
}),
|
||||
|
||||
// 注销登录
|
||||
logout: () =>
|
||||
request({
|
||||
url: '/auth/logout',
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
|
||||
// 用户信息缓存工具
|
||||
export const userStorage = {
|
||||
// 保存登录信息
|
||||
saveLoginInfo: (loginResult, userInfo) => {
|
||||
try {
|
||||
// 保存token
|
||||
uni.setStorageSync('access_token', loginResult.access_token);
|
||||
|
||||
// 保存用户信息(增加登录时间)
|
||||
if (userInfo) {
|
||||
const enhancedUserInfo = {
|
||||
...userInfo,
|
||||
loginTime: new Date().toISOString(),
|
||||
tokenExpireTime: loginResult.expires_in
|
||||
? new Date(Date.now() + loginResult.expires_in * 1000).toISOString()
|
||||
: null,
|
||||
};
|
||||
uni.setStorageSync('userInfo', JSON.stringify(enhancedUserInfo));
|
||||
}
|
||||
|
||||
// 保存登录状态
|
||||
uni.setStorageSync('isLoggedIn', true);
|
||||
|
||||
// 保存其他token信息
|
||||
if (loginResult.refresh_token) {
|
||||
uni.setStorageSync('refresh_token', loginResult.refresh_token);
|
||||
}
|
||||
|
||||
console.log('用户登录信息已保存', {
|
||||
hasToken: !!loginResult.access_token,
|
||||
hasUserInfo: !!userInfo,
|
||||
loginTime: new Date().toLocaleString(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('保存登录信息失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
return userInfo ? JSON.parse(userInfo) : null;
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取token
|
||||
getToken: () => {
|
||||
try {
|
||||
return uni.getStorageSync('access_token') || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
isLoggedIn: () => {
|
||||
try {
|
||||
const isLoggedIn = uni.getStorageSync('isLoggedIn');
|
||||
const token = uni.getStorageSync('access_token');
|
||||
return !!(isLoggedIn && token);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 清除登录信息
|
||||
clearLoginInfo: () => {
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
console.log('用户登录信息已清除');
|
||||
} catch (e) {
|
||||
console.error('清除登录信息失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 清除所有用户相关数据(包括refresh_token等)
|
||||
clearAll: () => {
|
||||
try {
|
||||
// 清除所有登录相关的存储
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
uni.removeStorageSync('refresh_token');
|
||||
|
||||
// 清除可能存在的其他用户相关数据
|
||||
uni.removeStorageSync('userPermissions');
|
||||
uni.removeStorageSync('userRoles');
|
||||
uni.removeStorageSync('tenantInfo');
|
||||
|
||||
console.log('所有用户相关数据已清除');
|
||||
} catch (e) {
|
||||
console.error('清除所有用户数据失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 数据缓存工具
|
||||
export const storage = {
|
||||
// 设置缓存
|
||||
set: (key, value, expire) => {
|
||||
try {
|
||||
const data = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
expire: expire ? Date.now() + expire * 1000 : null,
|
||||
};
|
||||
uni.setStorageSync(key, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error('缓存设置失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取缓存
|
||||
get: (key) => {
|
||||
try {
|
||||
const data = uni.getStorageSync(key);
|
||||
if (!data) return null;
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// 检查是否过期
|
||||
if (parsed.expire && Date.now() > parsed.expire) {
|
||||
uni.removeStorageSync(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.value;
|
||||
} catch (e) {
|
||||
console.error('缓存获取失败:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除缓存
|
||||
remove: (key) => {
|
||||
try {
|
||||
uni.removeStorageSync(key);
|
||||
} catch (e) {
|
||||
console.error('缓存删除失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// 清空缓存
|
||||
clear: () => {
|
||||
try {
|
||||
uni.clearStorageSync();
|
||||
} catch (e) {
|
||||
console.error('缓存清空失败:', e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 通用下载方法
|
||||
export const download = (url, params, fileName) => {
|
||||
// 显示加载提示
|
||||
uni.showLoading({
|
||||
title: '正在下载数据,请稍候',
|
||||
mask: true,
|
||||
});
|
||||
|
||||
return request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: params,
|
||||
header: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
timeout: 1200000, // 20分钟超时
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((resp) => {
|
||||
// 处理下载响应
|
||||
// 注意:uni-app中文件下载需要特殊处理
|
||||
console.log('文件下载完成:', fileName);
|
||||
uni.hideLoading();
|
||||
|
||||
// 这里需要根据平台进行不同的文件保存处理
|
||||
// 具体实现可能需要平台特定的代码
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('下载失败:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载文件出现错误,请联系管理员!',
|
||||
icon: 'none',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 配置更新方法
|
||||
export const updateRequestConfig = (config) => {
|
||||
Object.assign(REQUEST_CONFIG, config);
|
||||
console.log('🔧 请求配置已更新:', REQUEST_CONFIG);
|
||||
};
|
||||
|
||||
// 加密配置更新方法
|
||||
export const updateCryptoConfig = updateEncryptConfig;
|
||||
|
||||
// 密钥对验证方法
|
||||
export const validateCryptoKeys = validateKeyPair;
|
||||
|
||||
// 获取加密配置摘要
|
||||
export const getCryptoConfigSummary = getConfigSummary;
|
||||
|
||||
// 导出配置以便外部修改
|
||||
export { REQUEST_CONFIG as config, ENCRYPT_CONFIG as cryptoConfig };
|
||||
@@ -1,259 +0,0 @@
|
||||
/**
|
||||
* 全局路由守卫
|
||||
* 处理页面访问权限控制和登录状态检查
|
||||
*/
|
||||
|
||||
import { userStorage } from './request';
|
||||
import { userState } from './user';
|
||||
|
||||
// 无需登录验证的页面列表
|
||||
const PUBLIC_PAGES = [
|
||||
'/pages/login/index',
|
||||
'/pages/index/index', // 首页作为公开页面,可以访问但功能受限
|
||||
];
|
||||
|
||||
// 登录页路径
|
||||
const LOGIN_PAGE = '/pages/login/index';
|
||||
|
||||
/**
|
||||
* 检查页面是否为公开页面
|
||||
* @param {string} path 页面路径
|
||||
*/
|
||||
function isPublicPage(path) {
|
||||
return PUBLIC_PAGES.some((publicPath) => {
|
||||
// 精确匹配或前缀匹配
|
||||
return path === publicPath || path.startsWith(publicPath + '?');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路径
|
||||
*/
|
||||
function getCurrentPagePath() {
|
||||
try {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return '/' + currentPage.route;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取当前页面路径失败:', e);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录检查和跳转
|
||||
* @param {string} redirectReason 跳转原因
|
||||
*/
|
||||
function redirectToLogin(redirectReason = '请先登录') {
|
||||
console.log('🔄 重定向到登录页:', redirectReason);
|
||||
|
||||
// 清除可能存在的无效登录信息
|
||||
userStorage.clearLoginInfo();
|
||||
|
||||
// 提示用户
|
||||
uni.showToast({
|
||||
title: redirectReason,
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// 延迟跳转,确保Toast显示
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: LOGIN_PAGE,
|
||||
fail: (error) => {
|
||||
console.error('跳转登录页失败:', error);
|
||||
// 如果reLaunch失败,尝试其他方式
|
||||
uni.redirectTo({
|
||||
url: LOGIN_PAGE,
|
||||
});
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局页面守卫检查
|
||||
* 在每个页面的onLoad或onShow中调用
|
||||
* @param {string} pagePath 页面路径(可选,自动获取当前页面)
|
||||
*/
|
||||
export function checkPageAccess(pagePath = null) {
|
||||
try {
|
||||
// 获取页面路径
|
||||
const currentPath = pagePath || getCurrentPagePath();
|
||||
console.log('🔍 检查页面访问权限:', currentPath);
|
||||
|
||||
// 如果是公开页面,允许访问
|
||||
if (isPublicPage(currentPath)) {
|
||||
console.log('✅ 公开页面,允许访问');
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
console.log('❌ 用户未登录');
|
||||
redirectToLogin('请先登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否存在
|
||||
const token = userStorage.getToken();
|
||||
if (!token) {
|
||||
console.log('❌ Token不存在');
|
||||
redirectToLogin('登录信息无效,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否过期
|
||||
if (userState.checkTokenExpiration()) {
|
||||
console.log('❌ Token已过期');
|
||||
redirectToLogin('登录已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ 页面访问权限验证通过');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('页面访问权限检查失败:', error);
|
||||
redirectToLogin('系统错误,请重新登录');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面导航守卫
|
||||
* 在页面跳转前进行权限检查
|
||||
* @param {string} url 目标页面URL
|
||||
* @param {object} options 跳转选项
|
||||
*/
|
||||
export function guardedNavigate(url, options = {}) {
|
||||
try {
|
||||
console.log('🚀 准备跳转到:', url);
|
||||
|
||||
// 解析URL获取页面路径
|
||||
const urlPath = url.split('?')[0];
|
||||
|
||||
// 如果目标是公开页面,直接跳转
|
||||
if (isPublicPage(urlPath)) {
|
||||
return uni.navigateTo({ url, ...options });
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
redirectToLogin('请先登录后访问');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token有效性
|
||||
const token = userStorage.getToken();
|
||||
if (!token || userState.checkTokenExpiration()) {
|
||||
redirectToLogin('登录已失效,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行跳转
|
||||
return uni.navigateTo({ url, ...options });
|
||||
} catch (error) {
|
||||
console.error('页面跳转守卫失败:', error);
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab页面切换守卫
|
||||
* @param {string} url Tab页面URL
|
||||
*/
|
||||
export function guardedSwitchTab(url) {
|
||||
try {
|
||||
console.log('🔄 准备切换Tab到:', url);
|
||||
|
||||
const urlPath = url.split('?')[0];
|
||||
|
||||
// 如果是公开Tab页面,允许切换
|
||||
if (isPublicPage(urlPath)) {
|
||||
return uni.switchTab({ url });
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn() || !userStorage.getToken()) {
|
||||
redirectToLogin('请先登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token有效性
|
||||
if (userState.checkTokenExpiration()) {
|
||||
redirectToLogin('登录已过期,请重新登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
return uni.switchTab({ url });
|
||||
} catch (error) {
|
||||
console.error('Tab切换守卫失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局混入方法,用于在页面中自动检查权限
|
||||
*/
|
||||
export const routeGuardMixin = {
|
||||
onLoad() {
|
||||
// 页面加载时检查权限
|
||||
this.checkPageAccess();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时检查权限(防止后台切换回来时状态失效)
|
||||
this.checkPageAccess();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 在页面中可直接调用的权限检查方法
|
||||
checkPageAccess() {
|
||||
return checkPageAccess();
|
||||
},
|
||||
|
||||
// 带权限检查的页面跳转
|
||||
guardedNavigate(url, options) {
|
||||
return guardedNavigate(url, options);
|
||||
},
|
||||
|
||||
// 带权限检查的Tab切换
|
||||
guardedSwitchTab(url) {
|
||||
return guardedSwitchTab(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化路由守卫
|
||||
* 在App.vue中调用
|
||||
*/
|
||||
export function initRouteGuard() {
|
||||
console.log('🛡️ 初始化全局路由守卫');
|
||||
|
||||
// 页面加载完成后检查当前页面权限
|
||||
uni.$on('pageLoad', () => {
|
||||
checkPageAccess();
|
||||
});
|
||||
|
||||
// 监听应用进入前台
|
||||
uni.$on('onShow', () => {
|
||||
console.log('📱 应用进入前台,检查登录状态');
|
||||
checkPageAccess();
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
checkPageAccess,
|
||||
guardedNavigate,
|
||||
guardedSwitchTab,
|
||||
routeGuardMixin,
|
||||
initRouteGuard,
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
// SSE (Server-Sent Events) 实时数据工具
|
||||
class SSEClient {
|
||||
constructor() {
|
||||
this.eventSource = null
|
||||
this.listeners = {}
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
// 连接SSE
|
||||
connect(url, options = {}) {
|
||||
if (this.eventSource) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
try {
|
||||
// 在H5环境下使用原生 EventSource
|
||||
if (typeof EventSource !== 'undefined') {
|
||||
this.eventSource = new EventSource(url)
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.isConnected = true
|
||||
console.log('SSE连接已建立')
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
this.emit('message', data)
|
||||
} catch (e) {
|
||||
this.emit('message', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
this.isConnected = false
|
||||
console.error('SSE连接错误:', error)
|
||||
this.emit('error', error)
|
||||
|
||||
// 自动重连
|
||||
if (options.autoReconnect !== false) {
|
||||
setTimeout(() => {
|
||||
this.connect(url, options)
|
||||
}, options.reconnectDelay || 5000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 在非H5环境下使用轮询模拟
|
||||
this.startPolling(url, options)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SSE连接失败:', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询模拟SSE(用于小程序等不支持SSE的环境)
|
||||
startPolling(url, options = {}) {
|
||||
const interval = options.pollingInterval || 10000 // 默认10秒
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
header: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
this.emit('message', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询请求失败:', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.pollingTimer = setInterval(poll, interval)
|
||||
this.isConnected = true
|
||||
this.emit('connected')
|
||||
|
||||
// 立即执行一次
|
||||
poll()
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
disconnect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close()
|
||||
this.eventSource = null
|
||||
}
|
||||
|
||||
if (this.pollingTimer) {
|
||||
clearInterval(this.pollingTimer)
|
||||
this.pollingTimer = null
|
||||
}
|
||||
|
||||
this.isConnected = false
|
||||
this.emit('disconnected')
|
||||
}
|
||||
|
||||
// 添加事件监听
|
||||
on(event, callback) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = []
|
||||
}
|
||||
this.listeners[event].push(callback)
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
off(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
const index = this.listeners[event].indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.listeners[event].splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
emit(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => {
|
||||
try {
|
||||
callback(data)
|
||||
} catch (error) {
|
||||
console.error('事件回调执行错误:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态
|
||||
getConnectionState() {
|
||||
return this.isConnected
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局SSE实例
|
||||
export const sseClient = new SSEClient()
|
||||
|
||||
// 消息推送管理
|
||||
export const messageManager = {
|
||||
// 订阅消息
|
||||
subscribe(type, callback) {
|
||||
sseClient.on('message', (data) => {
|
||||
if (data.type === type) {
|
||||
callback(data.payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe(type, callback) {
|
||||
sseClient.off('message', callback)
|
||||
},
|
||||
|
||||
// 连接实时消息服务
|
||||
connect(userId) {
|
||||
const sseUrl = `${
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://your-api-domain.com'
|
||||
}/sse/messages?userId=${userId}`
|
||||
|
||||
sseClient.connect(sseUrl, {
|
||||
autoReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
pollingInterval: 10000,
|
||||
})
|
||||
},
|
||||
|
||||
// 断开连接
|
||||
disconnect() {
|
||||
sseClient.disconnect()
|
||||
},
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Token测试工具
|
||||
export function testTokenStorage() {
|
||||
console.log('🧪 开始Token存储测试...');
|
||||
|
||||
try {
|
||||
// 1. 清除现有Token
|
||||
uni.removeStorageSync('access_token');
|
||||
console.log('🗑️ 已清除现有Token');
|
||||
|
||||
// 2. 测试存储Token
|
||||
const testToken =
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxIn0.test';
|
||||
uni.setStorageSync('access_token', testToken);
|
||||
console.log('💾 已存储测试Token');
|
||||
|
||||
// 3. 测试获取Token
|
||||
const retrievedToken = uni.getStorageSync('access_token');
|
||||
console.log(
|
||||
'📖 获取到的Token:',
|
||||
retrievedToken ? retrievedToken.substring(0, 30) + '...' : '❌ 为空',
|
||||
);
|
||||
|
||||
// 4. 验证Token是否一致
|
||||
if (retrievedToken === testToken) {
|
||||
console.log('✅ Token存储和获取测试通过');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ Token存储和获取测试失败');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Token测试失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前存储的Token信息
|
||||
*/
|
||||
export function getCurrentTokenInfo() {
|
||||
try {
|
||||
const token = uni.getStorageSync('access_token');
|
||||
const userInfo = uni.getStorageSync('userInfo');
|
||||
const isLoggedIn = uni.getStorageSync('isLoggedIn');
|
||||
|
||||
console.log('📊 当前存储状态:');
|
||||
console.log(' 🎫 Token:', token ? `存在 (${token.length}字符)` : '❌ 不存在');
|
||||
console.log(' 👤 用户信息:', userInfo ? '存在' : '❌ 不存在');
|
||||
console.log(' 🔐 登录状态:', isLoggedIn ? '已登录' : '❌ 未登录');
|
||||
|
||||
return {
|
||||
hasToken: !!token,
|
||||
tokenLength: token ? token.length : 0,
|
||||
hasUserInfo: !!userInfo,
|
||||
isLoggedIn: !!isLoggedIn,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ 获取Token信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有登录信息
|
||||
*/
|
||||
export function clearAllLoginInfo() {
|
||||
try {
|
||||
uni.removeStorageSync('access_token');
|
||||
uni.removeStorageSync('userInfo');
|
||||
uni.removeStorageSync('isLoggedIn');
|
||||
uni.removeStorageSync('refresh_token');
|
||||
|
||||
console.log('🗑️ 已清除所有登录信息');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 清除登录信息失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+122
-176
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* 用户管理工具
|
||||
* 处理用户信息、权限、状态等相关功能
|
||||
* 适配mobile2工程
|
||||
*/
|
||||
|
||||
import { userStorage } from './request';
|
||||
import { userStorage } from '@/common/storage';
|
||||
|
||||
/**
|
||||
* 用户信息管理
|
||||
@@ -97,55 +98,6 @@ export const userManager = {
|
||||
return user.nickName || user.userName || '用户';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户头像URL
|
||||
* @param {string} defaultAvatar 默认头像URL
|
||||
*/
|
||||
getAvatarUrl(defaultAvatar = '/static/default-avatar.png') {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user || !user.avatar) {
|
||||
return defaultAvatar;
|
||||
}
|
||||
|
||||
// 如果是完整URL,直接返回
|
||||
if (user.avatar.startsWith('http')) {
|
||||
return user.avatar;
|
||||
}
|
||||
|
||||
// 拼接基础URL
|
||||
const baseURL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:8080'
|
||||
: 'https://your-api-domain.com';
|
||||
|
||||
return baseURL + user.avatar;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户本地信息
|
||||
* @param {object} userData 用户数据
|
||||
*/
|
||||
updateUserInfo(userData) {
|
||||
try {
|
||||
const currentInfo = userStorage.getUserInfo();
|
||||
if (currentInfo) {
|
||||
const updatedInfo = {
|
||||
...currentInfo,
|
||||
user: {
|
||||
...currentInfo.user,
|
||||
...userData,
|
||||
},
|
||||
};
|
||||
uni.setStorageSync('userInfo', JSON.stringify(updatedInfo));
|
||||
console.log('用户信息已更新');
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('更新用户信息失败:', e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化用户信息用于显示
|
||||
*/
|
||||
@@ -155,81 +107,70 @@ export const userManager = {
|
||||
|
||||
return {
|
||||
name: this.getDisplayName(),
|
||||
avatar: this.getAvatarUrl(),
|
||||
department: user.deptName || '未分配部门',
|
||||
phone: user.phoneNumber || '未设置',
|
||||
email: user.email || '未设置',
|
||||
status: user.status === '0' ? '正常' : '停用',
|
||||
isAdmin: this.isAdmin(),
|
||||
lastLoginTime: user.loginDate ? new Date(user.loginDate).toLocaleString() : '首次登录',
|
||||
avatar: user.avatar || '/static/default-avatar.png',
|
||||
deptName: user.deptName || '未设置部门',
|
||||
status: user.status,
|
||||
loginDate: user.loginDate,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 权限检查指令工具
|
||||
*/
|
||||
export const permissionUtils = {
|
||||
/**
|
||||
* 检查页面访问权限
|
||||
* @param {string} pagePath 页面路径
|
||||
* 检查用户状态是否正常
|
||||
*/
|
||||
checkPagePermission(pagePath) {
|
||||
// 公开页面,无需权限检查
|
||||
const publicPages = ['/pages/login/index', '/pages/index/index'];
|
||||
isUserStatusNormal() {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user) return false;
|
||||
|
||||
if (publicPages.includes(pagePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 这里可以根据具体业务需求添加页面权限检查逻辑
|
||||
return true;
|
||||
// 0 = 正常,1 = 停用
|
||||
return user.status === '0' || user.status === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 权限控制的页面跳转
|
||||
* @param {string} url 目标页面URL
|
||||
* @param {string} permission 所需权限
|
||||
* 获取用户完整信息字符串
|
||||
*/
|
||||
navigateWithPermission(url, permission = null) {
|
||||
// 检查登录状态
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
});
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
return false;
|
||||
getUserInfoString() {
|
||||
const user = this.getCurrentUser();
|
||||
if (!user) return '未登录用户';
|
||||
|
||||
const info = [
|
||||
`用户名: ${user.userName || '未知'}`,
|
||||
`昵称: ${user.nickName || '未设置'}`,
|
||||
`部门: ${user.deptName || '未分配'}`,
|
||||
`邮箱: ${user.email || '未设置'}`,
|
||||
`手机: ${user.phoneNumber || '未设置'}`,
|
||||
];
|
||||
|
||||
if (user.loginDate) {
|
||||
info.push(`登录时间: ${new Date(user.loginDate).toLocaleString()}`);
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (permission && !userManager.hasPermission(permission)) {
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none',
|
||||
});
|
||||
return info.join('\n');
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户信息(仅更新本地缓存)
|
||||
* @param {Object} updatedInfo 更新的信息
|
||||
*/
|
||||
updateUserInfo(updatedInfo) {
|
||||
const currentInfo = userStorage.getUserInfo();
|
||||
if (!currentInfo) return false;
|
||||
|
||||
try {
|
||||
const newInfo = {
|
||||
...currentInfo,
|
||||
user: {
|
||||
...currentInfo.user,
|
||||
...updatedInfo,
|
||||
},
|
||||
};
|
||||
|
||||
uni.setStorageSync('userInfo', JSON.stringify(newInfo));
|
||||
console.log('用户信息已更新', updatedInfo);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行跳转
|
||||
uni.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -238,78 +179,83 @@ export const permissionUtils = {
|
||||
*/
|
||||
export const userState = {
|
||||
/**
|
||||
* 检查Token是否即将过期(提前5分钟提醒)
|
||||
*/
|
||||
checkTokenExpiration() {
|
||||
try {
|
||||
const token = userStorage.getToken();
|
||||
if (!token) return false;
|
||||
|
||||
// 这里可以解析JWT token获取过期时间
|
||||
// 简化处理:检查登录时间是否超过2小时
|
||||
const userInfo = userStorage.getUserInfo();
|
||||
if (userInfo && userInfo.loginTime) {
|
||||
const loginTime = new Date(userInfo.loginTime).getTime();
|
||||
const now = Date.now();
|
||||
const twoHours = 2 * 60 * 60 * 1000; // 2小时
|
||||
|
||||
return now - loginTime > twoHours;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查Token过期时间失败:', e);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
async refreshToken() {
|
||||
try {
|
||||
// 这里调用刷新Token的API
|
||||
// const result = await api.refreshToken();
|
||||
// if (result.code === 200) {
|
||||
// uni.setStorageSync('access_token', result.data.access_token);
|
||||
// return true;
|
||||
// }
|
||||
console.log('Token刷新功能待实现');
|
||||
} catch (e) {
|
||||
console.error('刷新Token失败:', e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动登录检查
|
||||
* 检查并处理登录状态
|
||||
*/
|
||||
autoLoginCheck() {
|
||||
if (!userStorage.isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Token是否过期
|
||||
if (this.checkTokenExpiration()) {
|
||||
uni.showModal({
|
||||
title: '登录过期',
|
||||
content: '您的登录已过期,请重新登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
userStorage.clearLoginInfo();
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
},
|
||||
console.log('用户未登录,跳转到登录页');
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理登录成功
|
||||
* @param {Object} loginResult 登录结果
|
||||
* @param {Object} userInfo 用户信息
|
||||
*/
|
||||
handleLoginSuccess(loginResult, userInfo) {
|
||||
try {
|
||||
// 保存登录信息
|
||||
userStorage.saveLoginInfo(loginResult, userInfo);
|
||||
|
||||
// 可以在这里添加其他登录成功后的处理
|
||||
console.log('登录成功处理完成', {
|
||||
user: userInfo?.user?.userName,
|
||||
hasToken: !!loginResult?.access_token,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('登录成功处理失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理登录失败
|
||||
* @param {Error} error 错误信息
|
||||
*/
|
||||
handleLoginError(error) {
|
||||
console.error('登录失败:', error);
|
||||
|
||||
// 清除可能残留的登录信息
|
||||
userStorage.clearAll();
|
||||
|
||||
// 显示错误提示
|
||||
uni.showToast({
|
||||
title: '登录失败',
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理退出登录
|
||||
*/
|
||||
async handleLogout() {
|
||||
try {
|
||||
// 清除本地数据
|
||||
userStorage.clearAll();
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index',
|
||||
});
|
||||
|
||||
console.log('退出登录处理完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('退出登录处理失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
userManager,
|
||||
permissionUtils,
|
||||
userState,
|
||||
};
|
||||
/**
|
||||
* 默认导出用户管理器
|
||||
*/
|
||||
export default userManager;
|
||||
@@ -1,146 +0,0 @@
|
||||
import { request } from './request.js';
|
||||
|
||||
// 工作流API配置
|
||||
export const workflowApi = {
|
||||
// 获取流程实例分页列表(我发起的)
|
||||
pageByCurrent: (params) =>
|
||||
request({
|
||||
url: '/workflow/instance/pageByCurrent',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 删除流程实例
|
||||
deleteByInstanceIds: (instanceIds) =>
|
||||
request({
|
||||
url: '/workflow/instance/deleteByInstanceIds',
|
||||
method: 'DELETE',
|
||||
data: instanceIds,
|
||||
}),
|
||||
// 撤销流程申请
|
||||
cancelProcessApply: (data) =>
|
||||
request({
|
||||
url: '/workflow/instance/cancelProcessApply',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 获取流程分类树
|
||||
categoryTree: () =>
|
||||
request({
|
||||
url: '/workflow/category/categoryTree',
|
||||
method: 'GET',
|
||||
}),
|
||||
// 获取我的待办任务
|
||||
todoTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/todoTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的已办任务
|
||||
finishedTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/finishedTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 获取我的抄送任务
|
||||
copyTasks: (params) =>
|
||||
request({
|
||||
url: '/workflow/task/copyTasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
}),
|
||||
// 启动流程实例
|
||||
startProcessInstance: (data) =>
|
||||
request({
|
||||
url: '/workflow/instance/start',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 办理任务
|
||||
complete: (data) =>
|
||||
request({
|
||||
url: '/workflow/task/complete',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
// 获取流程历史
|
||||
processHistory: (instanceId) =>
|
||||
request({
|
||||
url: `/workflow/instance/history/${instanceId}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
// 获取流程实例详情(根据业务ID)
|
||||
processDetail: (businessId) =>
|
||||
request({
|
||||
url: `/workflow/instance/getInfo/${businessId}`,
|
||||
method: 'GET',
|
||||
}),
|
||||
};
|
||||
|
||||
// 工作流状态映射
|
||||
export const workflowStatus = {
|
||||
draft: { label: '草稿', color: '#909399', icon: '📝' },
|
||||
waiting: { label: '待审批', color: '#e6a23c', icon: '⏰' },
|
||||
complete: { label: '已完成', color: '#67c23a', icon: '✅' },
|
||||
cancel: { label: '已取消', color: '#f56c6c', icon: '❌' },
|
||||
back: { label: '已退回', color: '#409eff', icon: '↩️' },
|
||||
suspend: { label: '挂起', color: '#909399', icon: '⏸️' },
|
||||
terminate: { label: '终止', color: '#f56c6c', icon: '🛑' },
|
||||
};
|
||||
|
||||
// 工作流分类图标映射
|
||||
export const categoryIcons = {
|
||||
leave: '📅',
|
||||
repair: '🔧',
|
||||
purchase: '🛒',
|
||||
reimbursement: '💰',
|
||||
approval: '📋',
|
||||
default: '📄',
|
||||
};
|
||||
|
||||
// 获取状态样式
|
||||
export const getStatusStyle = (status) => workflowStatus[status] || workflowStatus.draft;
|
||||
|
||||
// 获取分类图标
|
||||
export const getCategoryIcon = (category) => categoryIcons[category] || categoryIcons.default;
|
||||
|
||||
// 工作流路由跳转方法
|
||||
export const routerJump = (routerJumpVo) => {
|
||||
const { businessId, taskId, type, formCustom, formPath } = routerJumpVo;
|
||||
const params = { businessId, taskId, type, formCustom, formPath };
|
||||
const queryString = Object.keys(params)
|
||||
.filter((key) => params[key] !== undefined && params[key] !== null)
|
||||
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
uni.navigateTo({
|
||||
url: `/pages/workflow/process?${queryString}`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面跳转失败',
|
||||
icon: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
export const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '';
|
||||
const time = new Date(timeStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - time.getTime();
|
||||
if (diff < 60 * 1000) {
|
||||
return '刚刚';
|
||||
}
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`;
|
||||
}
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
|
||||
}
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
|
||||
}
|
||||
return time.toLocaleDateString();
|
||||
};
|
||||
Reference in new issue
Block a user