diff --git a/.cursor/cursorrules-mobile.md b/.cursor/cursorrules-mobile.md index f2bbb71e..fbab21de 100644 --- a/.cursor/cursorrules-mobile.md +++ b/.cursor/cursorrules-mobile.md @@ -358,6 +358,51 @@ export default { ## 5. 🎨 图标使用规范 (SVG Data URI 方案) +### 🚨 强制性图标使用规范(2025-08-06 新增) + +**⚠️ 重要说明**: 移动端项目已全面禁用 uview-plus 的 u-icon 组件,强制使用 iconfont 图标系统! + +#### 📋 强制要求 + +1. **🚫 禁止使用 u-icon 组件** + ```vue + + + + + ``` + +2. **✅ 必须使用 iconfont 图标** + ```vue + + + + + ``` + +3. **🔄 动态图标类名** + ```vue + + + ``` + +#### 🎯 替换映射表 + +| u-icon name | iconfont class | 说明 | +|-------------|----------------|------| +| `search` | `icon-search` | 搜索图标 | +| `account` | `icon-account-circle` | 用户账户图标 | +| `close` | `icon-close` | 关闭图标 | +| `email` | `icon-email` | 邮件图标 | +| `arrow-right` | `icon-arrow-right` | 右箭头图标 | +| `arrow-down` | `icon-arrow-down` | 下箭头图标 | +| `home` | `icon-home` | 首页图标 | +| `setting` | `icon-setting` | 设置图标 | + ### 背景说明 由于 uview-plus 的 u-icon 组件在某些环境下存在显示异常(显示为横线),项目采用 **SVG Data URI** 方案实现图标系统,确保跨平台兼容性和显示稳定性。 @@ -369,6 +414,7 @@ export default { - ✅ **加载稳定**: 无 404 错误,无网络请求,性能优异 - ✅ **样式灵活**: 通过 CSS filter 实现颜色自定义 - ✅ **维护简单**: 基于 Material Design,图标清晰美观 +- ✅ **显示稳定**: 彻底解决 u-icon 组件显示异常问题 ### 图标配置文件 @@ -1784,3 +1830,152 @@ npm run lint:fix **📝 维护**: 移动端开发规范文档 **🔄 更新**: 2025-01-03 (新增登录状态管理规范) **📁 文件**: .cursor/cursorrules-mobile.md + +# uview-plus 图标组件规范(2025-07-01 新增) +- 移动端UI开发中,图标组件统一使用 (uview-plus 3.x 官方标准),禁止使用 或 。 +- 推荐通过 easycom 自动全局注册,无需手动 import。 +- 标准用法示例: + ```vue + + + ``` +- 图标名称请参考 uview-plus 官方文档:https://v3.uviewui.com/components/icon.html + +## uview-plus 图标字体配置规范(2025-08-06 更新) + +### 📦 版本升级经验总结 + +**升级记录**: uview-plus 3.4.45 → 3.4.73 (+28个版本) +**升级时间**: 2025-08-06 +**配置优化**: 采用官方推荐的字体加载机制 + +### 🔧 官方推荐配置方案 + +#### 方案1: 自托管字体配置(官方推荐 ⭐) + +**参考文档**: https://uview-plus.jiangruyi.com/components/icon.html#自定义默认字体图标自托管资源 + +```javascript +// main.js - 官方推荐配置方式 +app.use(uviewPlus, () => { + return { + options: { + config: { + // 🔧 自托管字体图标资源地址,避免CDN依赖问题 + iconUrl: '/static/fonts/uview-plus-official.ttf' + } + } + } +}); +``` + +#### 方案2: 手动加载字体(官方兼容性方案) + +**参考文档**: https://uview-plus.jiangruyi.com/components/icon.html#新版本全局加载兼容性 + +```javascript +// App.vue - 如果在小程序等场景出现图标无法全局加载 +import { fontUtil } from 'uview-plus'; + +export default { + onLaunch: function () { + // 官方推荐:手动加载uview-plus字体图标 + fontUtil.loadFont(); + } +}; +``` + +#### 方案3: CSS备用方案(平台兼容) + +```css +/* App.vue - 其他平台如抖音/QQ/百度小程序的备用方案 */ +@font-face { + font-family: 'uicon-iconfont'; + src: url('/static/fonts/uview-plus-official.ttf') format('truetype'); +} +``` + +### 🏆 最佳实践配置 + +```javascript +// main.js - 完整配置示例 +import { createSSRApp } from 'vue'; +import App from './App.vue'; +import uviewPlus from 'uview-plus'; +import 'uview-plus/index.scss'; +import '@/static/fonts/iconfont.css'; // 自定义SVG图标库 + +export function createApp() { + const app = createSSRApp(App); + + // 🔧 官方推荐:uview-plus配置 + app.use(uviewPlus, () => { + return { + options: { + config: { + iconUrl: '/static/fonts/uview-plus-official.ttf' + } + } + } + }); + + return { app }; +} +``` + +```javascript +// App.vue - 兼容性配置 +import { fontUtil } from 'uview-plus'; + +export default { + onLaunch: function () { + // 🔧 官方推荐:手动加载字体图标 + try { + fontUtil.loadFont(); + console.log('✅ 手动加载uview-plus字体成功'); + } catch (error) { + console.error('❌ 手动加载uview-plus字体失败:', error); + } + } +}; +``` + +#### 混合图标使用策略 +- **uview-plus图标**: 使用 `` 系统通用图标 +- **项目iconfont**: 使用 `` 业务专用图标 +- **优先级**: 项目iconfont > uview-plus图标 > CDN字体 + +### ⚠️ 重要注意事项 + +#### 升级前检查 +1. **字体文件**: 确保本地字体文件存在且可访问 +2. **网络环境**: 测试CDN地址访问性 +3. **版本兼容**: 检查字体版本与组件版本匹配 + +#### 最佳实践 +1. **优先级**: 项目iconfont > 本地字体 > CDN字体 +2. **测试覆盖**: 在不同网络环境下测试图标显示 +3. **回退机制**: 准备备用图标方案 +4. **文档记录**: 及时更新字体配置变更 + +### 📚 相关文件位置 +- **字体配置**: `mobile/src/main.js` +- **测试页面**: `mobile/src/pages/demo/uview-icon-test/index.vue` +- **字体文件**: `mobile/src/static/fonts/` +- **项目图标**: `mobile/src/static/fonts/iconfont.css` + +### 🧪 测试验证 +```bash +# 启动开发服务器 +npm run dev:h5 + +# 访问测试页面 +http://localhost:3000/#/pages/demo/uview-icon-test/index +``` + +### 📖 参考文档 +- uview-plus官方文档: https://uview-plus.jiangruyi.com/ +- 字体图标组件: https://uview-plus.jiangruyi.com/components/icon.html +- 自托管配置: https://uview-plus.jiangruyi.com/components/icon.html#自定义默认字体图标自托管资源 +- 兼容性配置: https://uview-plus.jiangruyi.com/components/icon.html#新版本全局加载兼容性 +- 项目图标文档: `docs/开发文档/API/u-icon替换映射表.md` diff --git a/mobile/.gitignore b/mobile/.gitignore index 7ce6e504..2561bb4f 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -1,21 +1,23 @@ -# Logs -logs -*.log +.DS_Store +node_modules/ +unpackage/ +dist/ + +# local env files +.env.local +.env.*.local + +# Log files npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -.DS_Store -dist -*.local # Editor directories and files +.project .idea +.vscode *.suo *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw* diff --git a/mobile/.npmrc b/mobile/.npmrc new file mode 100644 index 00000000..bf2e7648 --- /dev/null +++ b/mobile/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/mobile/PROJECT_OVERVIEW.md b/mobile/PROJECT_OVERVIEW.md deleted file mode 100644 index 1e925914..00000000 --- a/mobile/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,249 +0,0 @@ -# 智慧校园 Vue3 应用 - -基于 Vue3 + uniapp 开发的现代化智慧校园移动应用,采用现代简约设计风格,支持多平台部署。 - -## 🎯 项目特色 - -### 🎨 设计系统 - -- **现代简约风格**: 清新简洁的界面设计 -- **统一色彩方案**: 主题色 RGB(93, 135, 255) -- **组件化架构**: 高度复用的 Vue 组件 -- **响应式布局**: 适配多种屏幕尺寸 - -### 📱 核心功能 - -#### 首页 - -- 个性化欢迎横幅(用户信息 + 天气) -- 快速统计仪表板 -- 常用服务快捷入口 -- 待办任务管理 -- 校园动态资讯 -- 快捷功能工具栏 - -#### 办公中心 - -- 智能搜索功能 -- 分类服务导航: - - 🔥 热门服务 - - 🎓 学习教务 - - 🏠 生活服务 - - 🏢 校园资源 - - 📋 办事服务 - - 🔍 信息查询 - - ⚡ 快捷工具 - -#### 我的页面 - -- 个人信息展示 -- 学习状态统计 -- 功能菜单分组: - - 我的服务 - - 校园生活 - - 应用设置 - - 帮助与反馈 - -### 🎭 交互体验 - -#### 动画效果 - -- **页面转场**: 流畅的页面切换动画 -- **组件动画**: 卡片、列表项的进入动画 -- **微交互**: 点击反馈、悬停效果 -- **加载动画**: 优雅的加载状态提示 - -#### 手势操作 - -- **滑动切换**: 左右滑动切换页面 -- **长按菜单**: 长按显示上下文菜单 -- **双击操作**: 双击触发特殊功能 -- **触觉反馈**: 操作时的震动反馈 - -### ♿ 无障碍访问 - -#### 键盘导航 - -- **Tab 导航**: 支持键盘 Tab 键导航 -- **方向键**: 网格布局中的方向键导航 -- **快捷键**: Enter/Space 激活,Escape 取消 - -#### 屏幕阅读器支持 - -- **ARIA 标签**: 完整的语义化标记 -- **实时区域**: 动态内容的屏幕阅读器通知 -- **焦点管理**: 智能的焦点流转控制 - -#### 辅助功能 - -- **高对比度模式**: 视觉障碍用户友好 -- **大字体模式**: 可调节的字体大小 -- **屏幕阅读器优化**: 减少干扰的简化模式 - -### 📐 响应式设计 - -#### 断点设置 - -- **Mobile**: ≤ 375px -- **Tablet**: ≤ 768px -- **Desktop**: > 768px - -#### 适配特性 - -- **弹性网格**: 自适应列数的网格布局 -- **安全区域**: 支持刘海屏等特殊设备 -- **方向适配**: 横竖屏切换优化 -- **触摸友好**: 合适的触摸目标尺寸 - -## 🛠 技术架构 - -### 核心技术栈 - -- **Vue 3**: 组合式 API + TypeScript 支持 -- **uniapp**: 跨平台开发框架 -- **Sass**: CSS 预处理器 -- **Vite**: 现代化构建工具 - -### 组件库 - -``` -src/components/ -├── NavBar.vue # 导航栏组件 -├── Card.vue # 卡片组件 -├── ServiceItem.vue # 服务项组件 -├── StatCard.vue # 统计卡片组件 -└── GridLayout.vue # 网格布局组件 -``` - -### 工具库 - -``` -src/utils/ -├── animations.js # 动画工具 -├── gestures.js # 手势识别 -├── accessibility.js # 无障碍访问 -├── device.js # 设备检测 -├── request.js # 网络请求 -└── sse.js # 服务端推送 -``` - -### 页面结构 - -``` -src/pages/ -├── index/ # 首页 -├── workbench/ # 办公中心 -└── profile/ # 我的页面 -``` - -## 🎨 设计规范 - -### 色彩系统 - -```scss ---primary-color: rgb(93, 135, 255); // 主品牌色 ---primary-light: rgba(93, 135, 255, 0.1); ---primary-dark: rgb(73, 115, 235); ---text-primary: #333333; ---text-secondary: #666666; ---bg-color: #f8f9fa; -``` - -### 间距系统 - -```scss ---spacing-sm: 8px; // 小间距 ---spacing-base: 12px; // 基础间距 ---spacing-lg: 16px; // 大间距 ---spacing-xl: 20px; // 超大间距 -``` - -### 圆角系统 - -```scss ---border-radius-sm: 6px; ---border-radius-base: 8px; ---border-radius-lg: 12px; ---border-radius-xl: 16px; -``` - -## 🚀 部署与运行 - -### 开发环境 - -```bash -# 安装依赖 -npm install - -# 启动开发服务器 (H5) -npm run dev:h5 - -# 启动微信小程序开发 -npm run dev:mp-weixin - -# 启动 App 开发 -npm run dev:app -``` - -### 生产构建 - -```bash -# H5 构建 -npm run build:h5 - -# 微信小程序构建 -npm run build:mp-weixin - -# App 构建 -npm run build:app -``` - -## 📊 性能优化 - -### 代码优化 - -- **组件懒加载**: 路由级别的代码分割 -- **图片优化**: WebP 格式 + 响应式图片 -- **缓存策略**: 智能的资源缓存机制 - -### 用户体验 - -- **预加载**: 关键资源的预加载 -- **离线支持**: Service Worker 离线缓存 -- **快速响应**: 乐观更新 + 错误回退 - -## 🔧 配置说明 - -### 主题定制 - -在 `src/uni.scss` 中修改 CSS 变量即可快速定制主题色彩。 - -### 功能开关 - -在各页面组件的 data 中可以配置功能模块的显示/隐藏。 - -### 无障碍设置 - -用户的无障碍偏好会自动保存到本地存储,下次启动时自动恢复。 - -## 📱 支持平台 - -- ✅ H5 (Web) -- ✅ 微信小程序 -- ✅ 支付宝小程序 -- ✅ iOS App -- ✅ Android App -- ✅ 鸿蒙 App - -## 🏆 项目亮点 - -1. **现代化设计**: 采用最新的 Material Design 3 设计理念 -2. **卓越性能**: 60fps 流畅动画,秒级启动时间 -3. **全面无障碍**: WCAG 2.1 AA 级无障碍访问标准 -4. **智能交互**: 手势操作 + 语音控制支持 -5. **跨平台兼容**: 一套代码,多端部署 -6. **可维护架构**: 模块化设计,易于扩展和维护 - ---- - -_�� 让校园生活更便捷、更智能、更美好!_ diff --git a/mobile/README.md b/mobile/README.md index 5c7f3277..34ad6930 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -1,177 +1,19 @@ -# 智慧校园 UniApp 项目 - -基于 UniApp + Vue3 + uView-plus 的跨平台智慧校园应用。 - -## 技术栈 - -- **框架**: UniApp + Vue3 -- **UI 组件库**: uView-plus -- **代码规范**: ESLint -- **样式**: SCSS -- **不使用**: TypeScript - -## 支持平台 - -- 微信小程序 -- H5 -- iOS App -- Android App -- 其他小程序平台 - -## 项目结构 +# mobile2 +## Project setup ``` -src/ -├── pages/ # 页面目录 -│ ├── index/ # 首页 -│ ├── workbench/ # 办公中心 -│ └── profile/ # 我的 -├── utils/ # 工具函数 -│ ├── request.js # API请求工具 -│ ├── sse.js # SSE实时数据工具 -│ └── device.js # 设备兼容性工具 -├── static/ # 静态资源 -├── App.vue # 应用入口 -├── main.js # 项目入口 -├── pages.json # 页面配置 -└── uni.scss # 全局样式 +pnpm install ``` -## 核心功能 - -### 1. 底部导航 - -- 首页:展示校园服务快捷入口 -- 办公中心:办公应用和功能 -- 我的:个人信息和设置 - -### 2. 数据管理 - -- RESTful API 接口 -- 智能缓存策略(支持过期时间) -- SSE 实时数据推送(H5 环境使用原生 EventSource,小程序环境使用轮询) - -### 3. 设备兼容性 - -- 低端设备检测和性能优化 -- 网络环境自适应(2G/3G/4G/5G/WiFi) -- 刘海屏、折叠屏适配 -- 安全区域处理 - -## 开发命令 - -```bash -# 安装依赖 -npm install - -# H5开发 -npm run dev:h5 - -# 微信小程序开发 -npm run dev:mp-weixin - -# Android开发 -npm run dev:app-android - -# iOS开发 -npm run dev:app-ios - -# 代码检查 -npm run lint - -# 代码自动修复 -npm run lint:fix +### Compiles and hot-reloads for development +``` +pnpm run serve ``` -## 构建命令 - -```bash -# H5构建 -npm run build:h5 - -# 微信小程序构建 -npm run build:mp-weixin - -# App构建 -npm run build:app +### Compiles and minifies for production +``` +pnpm run build ``` -## 开发规范 - -### 代码风格 - -- 使用 2 个空格缩进 -- 使用单引号 -- 不使用分号 -- 组件名使用 PascalCase -- 文件名使用 kebab-case - -### 目录规范 - -- 页面组件放在 `pages/` 目录 -- 通用组件放在 `components/` 目录 -- 工具函数放在 `utils/` 目录 -- 静态资源放在 `static/` 目录 - -## API 使用示例 - -```javascript -import { api, storage } from '@/utils/request'; - -// 发起请求 -const data = await api.getUserInfo(); - -// 设置缓存(30分钟过期) -storage.set('userInfo', data, 30 * 60); - -// 获取缓存 -const cachedData = storage.get('userInfo'); -``` - -## SSE 实时数据示例 - -```javascript -import { messageManager } from '@/utils/sse'; - -// 连接实时消息 -messageManager.connect('userId123'); - -// 订阅消息 -messageManager.subscribe('notification', (data) => { - console.log('收到通知:', data); -}); - -// 断开连接 -messageManager.disconnect(); -``` - -## 设备适配示例 - -```javascript -import { deviceUtils, adaptUtils } from '@/utils/device'; - -// 检测设备类型 -const deviceType = deviceUtils.getDeviceType(); - -// 获取安全区域样式 -const safeAreaStyle = adaptUtils.getSafeAreaStyle(); - -// 判断是否为低端设备 -const isLowEnd = deviceUtils.isLowEndDevice(); -``` - -## 注意事项 - -1. **图标资源**: 当前底部导航使用文字显示,实际开发时需要替换为对应的图标文件 -2. **API 地址**: 请在 `utils/request.js` 中配置正确的后端 API 地址 -3. **SSE 服务**: SSE 功能需要后端支持,在小程序环境会自动降级为轮询 -4. **权限配置**: 使用定位、相机等原生功能时需要在 `manifest.json` 中配置相应权限 - -## 下一步开发 - -1. 根据设计原型完善页面 UI -2. 接入后端 API 接口 -3. 添加具体业务功能模块 -4. 配置推送、定位等原生能力 -5. 添加图标资源文件 -6. 完善错误处理和用户反馈 +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/mobile/babel.config.js b/mobile/babel.config.js new file mode 100644 index 00000000..4c1134bc --- /dev/null +++ b/mobile/babel.config.js @@ -0,0 +1,81 @@ +const webpack = require('webpack') +const plugins = [] + +if (process.env.UNI_OPT_TREESHAKINGNG) { + plugins.push(require('@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js')) +} + +if ( + ( + process.env.UNI_PLATFORM === 'app-plus' && + process.env.UNI_USING_V8 + ) || + ( + process.env.UNI_PLATFORM === 'h5' && + process.env.UNI_H5_BROWSER === 'builtin' + ) +) { + const path = require('path') + + const isWin = /^win/.test(process.platform) + + const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path) + + const input = normalizePath(process.env.UNI_INPUT_DIR) + try { + plugins.push([ + require('@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console'), + { + file (file) { + file = normalizePath(file) + if (file.indexOf(input) === 0) { + return path.relative(input, file) + } + return false + } + } + ]) + } catch (e) { } +} + +process.UNI_LIBRARIES = process.UNI_LIBRARIES || ['@dcloudio/uni-ui'] +process.UNI_LIBRARIES.forEach(libraryName => { + plugins.push([ + 'import', + { + 'libraryName': libraryName, + 'customName': (name) => { + return `${libraryName}/lib/${name}/${name}` + } + } + ]) +}) + +if (process.env.UNI_PLATFORM !== 'h5') { + plugins.push('@babel/plugin-transform-runtime') +} + +const config = { + presets: [ + [ + '@vue/app', + { + modules: webpack.version[0] > 4 ? 'auto' : 'commonjs', + useBuiltIns: process.env.UNI_PLATFORM === 'h5' ? 'usage' : 'entry' + } + ] + ], + plugins +} + +const UNI_H5_TEST = '**/@dcloudio/uni-h5/dist/index.umd.min.js' +if (process.env.NODE_ENV === 'production') { + config.overrides = [{ + test: UNI_H5_TEST, + compact: true, + }] +} else { + config.ignore = [UNI_H5_TEST] +} + +module.exports = config diff --git a/mobile/docs/图标管理系统使用指南.md b/mobile/docs/图标管理系统使用指南.md deleted file mode 100644 index cc070017..00000000 --- a/mobile/docs/图标管理系统使用指南.md +++ /dev/null @@ -1,348 +0,0 @@ -# 📚 图标管理系统使用指南 - -## 🎯 概述 - -项目采用了 **双图标系统**: -- **iconfont 自定义图标** (推荐) - 基于 SVG data URI 的轻量级图标系统 -- **uview-plus 图标** (辅助) - UI库自带图标,部分图标存在显示问题 - -## 📁 文件结构 - -``` -mobile/src/static/fonts/ -├── iconfont.css # 主图标文件(SVG data URI) -├── iconfont.ttf # 字体文件(可选) -└── iconfont.woff # 字体文件(可选) - -mobile/src/ -├── main.js # 引入图标CSS -├── pages/iconfont-demo/ # 图标演示页面 -└── pages/uview-icon-test/ # uview图标问题测试页面 -``` - -## 🚀 添加新图标 - 超级简单! - -### 方法一:SVG Data URI(推荐) - -**优势**: -- ✅ 无需下载字体文件 -- ✅ 支持彩色图标 -- ✅ 完美适配所有平台 -- ✅ 加载速度最快 - -**操作步骤**: - -#### 1. 从 iconfont.cn 下载 SVG -1. 访问 [iconfont.cn](https://www.iconfont.cn/) -2. 搜索需要的图标 -3. 下载 SVG 格式 - -#### 2. 转换为 data URI -使用在线工具或以下代码: - -```javascript -// SVG 转 data URI 工具函数 -function svgToDataUri(svgContent) { - const encoded = encodeURIComponent(svgContent) - .replace(/'/g, '%27') - .replace(/"/g, '%22'); - return `data:image/svg+xml;charset=utf8,${encoded}`; -} - -// 示例 -const svgContent = ` - -`; -``` - -#### 3. 添加到 CSS 文件 -在 `mobile/src/static/fonts/iconfont.css` 中添加: - -```css -/* 新增图标示例 */ -.icon-add-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z'/%3E%3C/svg%3E"); -} -``` - -#### 4. 在页面中使用 -```vue - -``` - -### 方法二:iconfont.cn 项目同步(适合大量图标) - -#### 1. 创建 iconfont 项目 -1. 在 iconfont.cn 注册账号 -2. 创建新项目 -3. 添加需要的图标到项目 - -#### 2. 下载代码 -1. 点击"下载至本地" -2. 选择 "Font class" 或 "Unicode" 方式 -3. 下载解压得到文件 - -#### 3. 替换项目文件 -```bash -# 将下载的文件复制到项目 -cp iconfont.css mobile/src/static/fonts/ -cp iconfont.ttf mobile/src/static/fonts/ -cp iconfont.woff mobile/src/static/fonts/ -``` - -#### 4. 更新字体引用(如需要) -在 `uni.scss` 或相关文件中更新字体路径: - -```scss -@font-face { - font-family: 'iconfont'; - src: url('./static/fonts/iconfont.woff') format('woff'), - url('./static/fonts/iconfont.ttf') format('truetype'); -} -``` - -## 🎨 使用规范 - -### 基础用法 -```vue - - - -``` - -### 响应式尺寸 -```vue - - - -``` - -## 📋 当前可用图标列表 - -### 基础图标 -- `icon-user` - 用户 -- `icon-lock` - 锁定 -- `icon-eye` - 显示 -- `icon-eye-close` - 隐藏 -- `icon-home` - 首页 -- `icon-setting` - 设置 - -### 操作图标 -- `icon-add` - 添加 -- `icon-close` - 关闭 -- `icon-check` - 确认 -- `icon-search` - 搜索 -- `icon-arrow-right` - 右箭头 - -### 功能图标 -- `icon-message` - 消息 -- `icon-phone` - 电话 -- `icon-mail` - 邮件 -- `icon-login` - 登录 - -## 🔧 管理工具 - -### 在线 SVG 编辑器 -- [SVG Editor](https://svg-edit.github.io/svgedit/) -- [Boxy SVG](https://boxy-svg.com/) - -### Data URI 转换工具 -```javascript -// 工具函数:SVG 转 data URI -function createSvgDataUri(svgPath, viewBox = "0 0 24 24") { - const svg = ``; - return `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`; -} - -// 使用示例 -const newIconCSS = ` -.icon-new-feature { - background-image: ${createSvgDataUri("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10...")}; -} -`; -``` - -### 批量添加脚本 -```javascript -// 批量生成图标CSS的脚本 -const icons = [ - { name: 'star', path: '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' }, - { name: 'heart', path: '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' } -]; - -function generateIconCSS(icons) { - return icons.map(icon => ` -.icon-${icon.name} { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='${icon.path}'/%3E%3C/svg%3E"); -}`).join('\n'); -} -``` - -## 🎯 最佳实践 - -### 1. 命名规范 -```css -/* ✅ 推荐命名 */ -.icon-user /* 功能性图标 */ -.icon-user-add /* 带操作的图标 */ -.icon-arrow-right /* 方向性图标 */ - -/* ❌ 避免命名 */ -.icon-1 /* 无意义编号 */ -.icon-temp /* 临时命名 */ -``` - -### 2. 颜色管理 -```scss -// 使用CSS变量管理图标颜色 -:root { - --icon-primary: #1890ff; - --icon-success: #52c41a; - --icon-warning: #faad14; - --icon-error: #ff4d4f; -} - -.icon-primary { color: var(--icon-primary); } -.icon-success { color: var(--icon-success); } -.icon-warning { color: var(--icon-warning); } -.icon-error { color: var(--icon-error); } -``` - -### 3. 尺寸规范 -```scss -// 标准尺寸系统 -.icon-xs { width: 12px; height: 12px; } // 超小 -.icon-sm { width: 16px; height: 16px; } // 小 -.icon-md { width: 24px; height: 24px; } // 中(默认) -.icon-lg { width: 32px; height: 32px; } // 大 -.icon-xl { width: 48px; height: 48px; } // 超大 -``` - -## 🔄 迁移指南 - -### 从 uview 图标迁移到 iconfont - -```vue - - - - - -``` - -### 批量替换工具 -```bash -# 使用正则表达式批量替换 -# 将 { - if (!document.querySelector('#iconfont-css')) { - const link = document.createElement('link'); - link.id = 'iconfont-css'; - link.rel = 'stylesheet'; - link.href = './static/fonts/iconfont.css'; - document.head.appendChild(link); - } -}; -``` - -### 2. CDN 优化 -```css -/* 使用CDN加载字体文件 */ -@font-face { - font-family: 'iconfont'; - src: url('https://cdn.example.com/fonts/iconfont.woff') format('woff'); -} -``` - -## 🛠️ 开发工具 - -### VS Code 插件推荐 -- **SVG Viewer** - 预览SVG文件 -- **CSS Peek** - 快速查看CSS定义 -- **IconFont Helper** - iconfont图标提示 - -### 图标搜索工具 -- [Iconify](https://iconify.design/) - 100,000+ 开源图标 -- [Feather Icons](https://feathericons.com/) - 简洁线性图标 -- [Heroicons](https://heroicons.com/) - Tailwind CSS图标 - -## ❓ 常见问题 - -### Q: 图标不显示怎么办? -A: 检查以下几点: -1. CSS文件是否正确引入 -2. 类名是否拼写正确 -3. 字体文件路径是否正确 -4. 浏览器是否支持data URI - -### Q: 如何修改图标颜色? -A: 使用 `color` 或 `fill` 属性: -```css -.my-icon { - color: #1890ff; /* 对于SVG data URI */ - fill: #1890ff; /* 对于字体图标 */ -} -``` - -### Q: 图标模糊怎么解决? -A: 确保图标尺寸为偶数像素: -```css -.icon-clear { - width: 24px; /* ✅ 偶数 */ - height: 24px; /* ✅ 偶数 */ -} -``` - -## 📞 技术支持 - -- **项目位置**: `mobile/src/static/fonts/` -- **演示页面**: `/pages/iconfont-demo/index` -- **测试页面**: `/pages/uview-icon-test/index` -- **文档更新**: 2025-01-07 - ---- - -**✨ 总结**: iconfont 图标系统让您轻松管理图标,添加新图标只需要复制粘贴几行CSS代码!推荐使用 SVG data URI 方式,兼容性最好,性能最优。 \ No newline at end of file diff --git a/mobile/eslint.config.cjs b/mobile/eslint.config.cjs deleted file mode 100644 index 83368d38..00000000 --- a/mobile/eslint.config.cjs +++ /dev/null @@ -1,33 +0,0 @@ -const js = require('@eslint/js'); -const vue = require('eslint-plugin-vue'); - -module.exports = [ - js.configs.recommended, - ...vue.configs['flat/essential'], - { - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - globals: { - uni: 'readonly', - wx: 'readonly', - plus: 'readonly', - console: 'readonly', - process: 'readonly', - EventSource: 'readonly', - setTimeout: 'readonly', - setInterval: 'readonly', - clearInterval: 'readonly', - }, - }, - rules: { - 'vue/multi-word-component-names': 'off', - 'no-console': 'warn', - 'no-debugger': 'warn', - 'no-unused-vars': 'warn', - // indent: ['error', 2], - quotes: ['error', 'single'], - // semi: ['error', 'never'], - }, - }, -]; diff --git a/mobile/index.html b/mobile/index.html deleted file mode 100644 index fb81ddc6..00000000 --- a/mobile/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - -
- - - diff --git a/mobile/package.json b/mobile/package.json index ff729bc1..eedf7efa 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,80 +1,117 @@ { - "name": "uni-preset-vue", - "version": "0.0.0", + "name": "mobile2", + "version": "0.1.0", + "private": true, "scripts": { - "dev:custom": "uni -p", - "dev:h5": "uni", - "dev:h5:ssr": "uni --ssr", - "dev:mp-alipay": "uni -p mp-alipay", - "dev:mp-baidu": "uni -p mp-baidu", - "dev:mp-jd": "uni -p mp-jd", - "dev:mp-kuaishou": "uni -p mp-kuaishou", - "dev:mp-lark": "uni -p mp-lark", - "dev:mp-qq": "uni -p mp-qq", - "dev:mp-toutiao": "uni -p mp-toutiao", - "dev:mp-harmony": "uni -p mp-harmony", - "dev:mp-weixin": "uni -p mp-weixin", - "dev:mp-xhs": "uni -p mp-xhs", - "dev:quickapp-webview": "uni -p quickapp-webview", - "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", - "dev:quickapp-webview-union": "uni -p quickapp-webview-union", - "build:custom": "uni build -p", - "build:h5": "uni build", - "build:h5:ssr": "uni build --ssr", - "build:mp-alipay": "uni build -p mp-alipay", - "build:mp-baidu": "uni build -p mp-baidu", - "build:mp-jd": "uni build -p mp-jd", - "build:mp-kuaishou": "uni build -p mp-kuaishou", - "build:mp-lark": "uni build -p mp-lark", - "build:mp-qq": "uni build -p mp-qq", - "build:mp-toutiao": "uni build -p mp-toutiao", - "build:mp-harmony": "uni build -p mp-harmony", - "build:mp-weixin": "uni build -p mp-weixin", - "build:mp-xhs": "uni build -p mp-xhs", - "build:quickapp-webview": "uni build -p quickapp-webview", - "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", - "build:quickapp-webview-union": "uni build -p quickapp-webview-union", - "lint": "eslint src/**/*.{js,vue}", - "lint:fix": "eslint src/**/*.{js,vue} --fix" + "serve": "npm run dev:h5", + "build": "npm run build:h5", + "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build", + "build:custom": "cross-env NODE_ENV=production uniapp-cli custom", + "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build", + "build:mp-360": "cross-env NODE_ENV=production UNI_PLATFORM=mp-360 vue-cli-service uni-build", + "build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build", + "build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build", + "build:mp-harmony": "cross-env NODE_ENV=production UNI_PLATFORM=mp-harmony vue-cli-service uni-build", + "build:mp-jd": "cross-env NODE_ENV=production UNI_PLATFORM=mp-jd vue-cli-service uni-build", + "build:mp-kuaishou": "cross-env NODE_ENV=production UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build", + "build:mp-lark": "cross-env NODE_ENV=production UNI_PLATFORM=mp-lark vue-cli-service uni-build", + "build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build", + "build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build", + "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build", + "build:mp-xhs": "cross-env NODE_ENV=production UNI_PLATFORM=mp-xhs vue-cli-service uni-build", + "build:quickapp-native": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-native vue-cli-service uni-build", + "build:quickapp-webview": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview vue-cli-service uni-build", + "build:quickapp-webview-huawei": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build", + "build:quickapp-webview-union": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build", + "dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch", + "dev:custom": "cross-env NODE_ENV=development uniapp-cli custom", + "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve", + "dev:mp-360": "cross-env NODE_ENV=development UNI_PLATFORM=mp-360 vue-cli-service uni-build --watch", + "dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch", + "dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch", + "dev:mp-harmony": "cross-env NODE_ENV=development UNI_PLATFORM=mp-harmony vue-cli-service uni-build --watch", + "dev:mp-jd": "cross-env NODE_ENV=development UNI_PLATFORM=mp-jd vue-cli-service uni-build --watch", + "dev:mp-kuaishou": "cross-env NODE_ENV=development UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build --watch", + "dev:mp-lark": "cross-env NODE_ENV=development UNI_PLATFORM=mp-lark vue-cli-service uni-build --watch", + "dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch", + "dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch", + "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch", + "dev:mp-xhs": "cross-env NODE_ENV=development UNI_PLATFORM=mp-xhs vue-cli-service uni-build --watch", + "dev:quickapp-native": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-native vue-cli-service uni-build --watch", + "dev:quickapp-webview": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview vue-cli-service uni-build --watch", + "dev:quickapp-webview-huawei": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build --watch", + "dev:quickapp-webview-union": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build --watch", + "info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js", + "serve:quickapp-native": "node node_modules/@dcloudio/uni-quickapp-native/bin/serve.js", + "test:android": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=android jest -i", + "test:h5": "cross-env UNI_PLATFORM=h5 jest -i", + "test:ios": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=ios jest -i", + "test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest -i", + "test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i" }, "dependencies": { - "@dcloudio/uni-app": "3.0.0-4060620250520001", - "@dcloudio/uni-app-harmony": "3.0.0-4060620250520001", - "@dcloudio/uni-app-plus": "3.0.0-4060620250520001", - "@dcloudio/uni-components": "3.0.0-4060620250520001", - "@dcloudio/uni-h5": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-jd": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-lark": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-qq": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001", - "@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001", - "@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001", - "big-integer": "^1.6.52", - "buffer": "^6.0.3", + "@dcloudio/uni-app": "^2.0.2-4070520250711001", + "@dcloudio/uni-app-plus": "^2.0.2-4070520250711001", + "@dcloudio/uni-h5": "^2.0.2-4070520250711001", + "@dcloudio/uni-i18n": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-360": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-alipay": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-baidu": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-harmony": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-jd": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-kuaishou": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-lark": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-qq": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-toutiao": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-vue": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-weixin": "^2.0.2-4070520250711001", + "@dcloudio/uni-mp-xhs": "^2.0.2-4070520250711001", + "@dcloudio/uni-quickapp-native": "^2.0.2-4070520250711001", + "@dcloudio/uni-quickapp-webview": "^2.0.2-4070520250711001", + "@dcloudio/uni-stacktracey": "^2.0.2-4070520250711001", + "@dcloudio/uni-stat": "^2.0.2-4070520250711001", + "@vue/shared": "^3.0.0", + "core-js": "^3.8.3", "crypto-js": "^4.2.0", + "flyio": "^0.6.2", "jsencrypt": "^3.3.2", - "node-rsa": "^1.1.1", - "uview-plus": "^3.4.45", - "vite-plugin-node-polyfills": "^0.23.0", - "vue": "^3.4.21", - "vue-i18n": "^9.1.9" + "uview-ui": "2.0.38", + "vue": ">= 2.6.14 < 2.7", + "vue-class-component": "^6.3.2", + "vue-property-decorator": "^8.0.0", + "vuex": "^3.2.0" }, "devDependencies": { - "@dcloudio/types": "^3.4.8", - "@dcloudio/uni-automator": "3.0.0-4060620250520001", - "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001", - "@dcloudio/uni-stacktracey": "3.0.0-4060620250520001", - "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", - "@eslint/js": "^9.30.1", - "@vue/runtime-core": "^3.4.21", - "eslint": "^9.30.1", - "eslint-plugin-vue": "^10.3.0", - "sass": "^1.89.2", - "vite": "5.2.8" + "@babel/plugin-syntax-typescript": "^7.2.0", + "@dcloudio/types": "^3.3.2", + "@dcloudio/uni-automator": "^2.0.2-4070520250711001", + "@dcloudio/uni-cli-i18n": "^2.0.2-4070520250711001", + "@dcloudio/uni-cli-shared": "^2.0.2-4070520250711001", + "@dcloudio/uni-helper-json": "*", + "@dcloudio/uni-migration": "^2.0.2-4070520250711001", + "@dcloudio/uni-template-compiler": "^2.0.2-4070520250711001", + "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.2-4070520250711001", + "@dcloudio/vue-cli-plugin-uni": "^2.0.2-4070520250711001", + "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.2-4070520250711001", + "@dcloudio/webpack-uni-mp-loader": "^2.0.2-4070520250711001", + "@dcloudio/webpack-uni-pages-loader": "^2.0.2-4070520250711001", + "@vue/cli-plugin-babel": "~5.0.0", + "@vue/cli-plugin-typescript": "~5.0.8", + "@vue/cli-service": "~5.0.0", + "babel-plugin-import": "^1.11.0", + "cross-env": "^7.0.2", + "jest": "^25.4.0", + "postcss-comment": "^2.0.0", + "sass": "^1.90.0", + "sass-loader": "^10.5.2", + "typescript": "~4.5.5", + "vue-template-compiler": ">= 2.6.14 < 2.7" + }, + "browserslist": [ + "Android >= 4.4", + "ios >= 9" + ], + "uni-app": { + "scripts": {} } } diff --git a/mobile/postcss.config.js b/mobile/postcss.config.js new file mode 100644 index 00000000..c4a965e9 --- /dev/null +++ b/mobile/postcss.config.js @@ -0,0 +1,27 @@ +const path = require('path') +const webpack = require('webpack') +const config = { + parser: require('postcss-comment'), + plugins: [ + require('postcss-import')({ + resolve (id, basedir, importOptions) { + if (id.startsWith('~@/')) { + return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3)) + } else if (id.startsWith('@/')) { + return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2)) + } else if (id.startsWith('/') && !id.startsWith('//')) { + return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1)) + } + return id + } + }), + require('autoprefixer')({ + remove: process.env.UNI_PLATFORM !== 'h5' + }), + require('@dcloudio/vue-cli-plugin-uni/packages/postcss') + ] +} +if (webpack.version[0] > 4) { + delete config.parser +} +module.exports = config diff --git a/mobile/public/index.html b/mobile/public/index.html new file mode 100644 index 00000000..a528055d --- /dev/null +++ b/mobile/public/index.html @@ -0,0 +1,25 @@ + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + + +
+ + + + \ No newline at end of file diff --git a/mobile/shims-uni.d.ts b/mobile/shims-uni.d.ts index ed4adcfc..b24295a4 100644 --- a/mobile/shims-uni.d.ts +++ b/mobile/shims-uni.d.ts @@ -1,10 +1,11 @@ /// -import 'vue' - -declare module '@vue/runtime-core' { +import Vue from 'vue' +declare module "vue/types/options" { type Hooks = App.AppInstance & Page.PageInstance; - - interface ComponentCustomOptions extends Hooks { - + interface ComponentOptions extends Hooks { + /** + * 组件类型 + */ + mpType?: string; } } diff --git a/mobile/shims-vue.d.ts b/mobile/shims-vue.d.ts new file mode 100644 index 00000000..eb40980e --- /dev/null +++ b/mobile/shims-vue.d.ts @@ -0,0 +1,4 @@ +declare module "*.vue" { + import Vue from 'vue' + export default Vue +} diff --git a/mobile/src/App.vue b/mobile/src/App.vue index e0a7f47d..150c63c3 100644 --- a/mobile/src/App.vue +++ b/mobile/src/App.vue @@ -1,34 +1,23 @@ - - diff --git a/mobile/src/common/config.js b/mobile/src/common/config.js new file mode 100644 index 00000000..3ebedeca --- /dev/null +++ b/mobile/src/common/config.js @@ -0,0 +1,72 @@ +// 移动端配置管理 - 统一管理密钥和请求配置 +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, +}; + +// API请求配置 +export 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, +}; + +/** + * 更新加密配置 + * @param {Object} config 新的配置对象 + */ +export const updateEncryptConfig = (config) => { + Object.assign(ENCRYPT_CONFIG, config); + console.log('🔧 加密配置已更新:', ENCRYPT_CONFIG); +}; + +/** + * 更新请求配置 + * @param {Object} config 新的配置对象 + */ +export const updateRequestConfig = (config) => { + Object.assign(REQUEST_CONFIG, config); + console.log('🔧 请求配置已更新:', REQUEST_CONFIG); +}; + +/** + * 获取当前配置摘要 + * @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, + baseURL: REQUEST_CONFIG.baseURL, + timeout: REQUEST_CONFIG.timeout, + }; +}; diff --git a/mobile/src/utils/crypto.js b/mobile/src/common/crypto.js similarity index 84% rename from mobile/src/utils/crypto.js rename to mobile/src/common/crypto.js index 4d2782f9..f727d22c 100644 --- a/mobile/src/utils/crypto.js +++ b/mobile/src/common/crypto.js @@ -1,24 +1,7 @@ -// 移动端加密解密工具 - 与web端保持一致 +// 移动端加密解密工具 - 与mobile工程保持一致 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, -}; +import { ENCRYPT_CONFIG, updateEncryptConfig } from './config.js'; /** * 随机生成32位的字符串 @@ -210,15 +193,6 @@ export const decryptWithAES = (data, key) => { 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} 验证结果 @@ -287,5 +261,5 @@ export const getConfigSummary = () => { }; }; -// 导出配置以便外部修改 -export { ENCRYPT_CONFIG as config }; +// 导出配置更新方法 +export { updateEncryptConfig }; diff --git a/mobile/src/utils/request.js b/mobile/src/common/request.js similarity index 66% rename from mobile/src/utils/request.js rename to mobile/src/common/request.js index f3692053..c3a16600 100644 --- a/mobile/src/utils/request.js +++ b/mobile/src/common/request.js @@ -1,3 +1,4 @@ +// 移动端统一请求工具 - 与mobile工程保持一致 import { generateAesKey, encryptBase64, @@ -6,63 +7,77 @@ import { decryptWithAes, encrypt, decrypt, - ENCRYPT_CONFIG, - updateEncryptConfig, - validateKeyPair, - getConfigSummary, } from './crypto.js'; +import { ENCRYPT_CONFIG, REQUEST_CONFIG } from './config.js'; +import { sessionCache, userStorage } from './storage.js'; -// API请求配置 - 可配置 -const REQUEST_CONFIG = { - // 基础URL配置 - baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com', +/** + * 获取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 ''; + } +} - // 请求超时时间 - timeout: 300000, // 5分钟,适合大数据量导出 +/** + * 处理401未授权响应 + */ +function handleUnauthorized(message = '登录已失效,请重新登录') { + console.log('🔒 处理401未授权响应:', message); - // 客户端ID - clientId: 'e5cd7e4891bf95d1d19206ce24a7b32e', + // 清除登录信息 + try { + uni.removeStorageSync('access_token'); + uni.removeStorageSync('userInfo'); + uni.removeStorageSync('isLoggedIn'); + console.log('📤 已清除本地登录信息'); + } catch (e) { + console.error('❌ 清除登录信息失败:', e); + } - // 默认referer - referer: 'http://localhost/login?redirect=/index', + // 显示提示信息 + uni.showToast({ + title: message, + icon: 'none', + duration: 2000, + }); - // 是否启用加密 - encryptEnabled: true, + // 延迟跳转,确保Toast显示 + setTimeout(() => { + // 获取当前页面路径 + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + const currentPath = '/' + currentPage.route; - // 防重复提交间隔时间(ms) - repeatSubmitInterval: 500, -}; - -// 会话缓存工具 -const sessionCache = { - set: (key, value) => { - try { - uni.setStorageSync(`session_${key}`, JSON.stringify(value)); - } catch (e) { - console.error('设置会话缓存失败:', e); + // 如果当前不是登录页,则跳转到登录页 + 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); +} - 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 @@ -286,66 +301,6 @@ export const request = (options) => { }); }; -// 获取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方法和加密拦截器 @@ -401,164 +356,6 @@ export const api = { }), }; -// 用户信息缓存工具 -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) => { // 显示加载提示 @@ -595,21 +392,3 @@ export const download = (url, params, fileName) => { }); }); }; - -// 配置更新方法 -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 }; diff --git a/mobile/src/common/storage.js b/mobile/src/common/storage.js new file mode 100644 index 00000000..8529a80c --- /dev/null +++ b/mobile/src/common/storage.js @@ -0,0 +1,267 @@ +// 用户存储管理工具 - 与mobile工程保持一致 + +// 会话缓存工具 +export 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 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); + } + }, +}; + +// Token测试工具 +export const tokenTest = { + // 测试Token存储 + 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信息 + 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; + } + }, + + // 清除所有登录信息 + 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; + } + }, +}; diff --git a/mobile/src/utils/workflow-api.js b/mobile/src/common/workflow-api.js similarity index 79% rename from mobile/src/utils/workflow-api.js rename to mobile/src/common/workflow-api.js index 7b8607da..d682b5dd 100644 --- a/mobile/src/utils/workflow-api.js +++ b/mobile/src/common/workflow-api.js @@ -50,6 +50,41 @@ export const workflowApi = { method: 'GET', data: params, }), + // 获取全部待办任务(对应web端allTaskWaiting.vue) + pageByAllTaskWait: (params) => + request({ + url: '/workflow/task/pageByAllTaskWait', + method: 'GET', + data: params, + }), + // 获取全部已办任务(对应web端allTaskWaiting.vue) + pageByAllTaskFinish: (params) => + request({ + url: '/workflow/task/pageByAllTaskFinish', + method: 'GET', + data: params, + }), + // 获取我的已办任务(对应web端taskFinish.vue) + pageByTaskFinish: (params) => + request({ + url: '/workflow/task/pageByTaskFinish', + method: 'GET', + data: params, + }), + // 获取我的抄送任务(对应web端taskCopyList.vue) + pageByTaskCopy: (params) => + request({ + url: '/workflow/task/pageByTaskCopy', + method: 'GET', + data: params, + }), + // 修改办理人 + updateAssignee: (taskIds, assigneeId) => + request({ + url: '/workflow/task/updateAssignee', + method: 'POST', + data: { taskIds, assigneeId }, + }), // 启动流程实例 startProcessInstance: (data) => request({ diff --git a/mobile/src/components/Card.vue b/mobile/src/components/Card.vue index 8eaefbb6..48a57174 100644 --- a/mobile/src/components/Card.vue +++ b/mobile/src/components/Card.vue @@ -45,36 +45,36 @@ export default { diff --git a/mobile/src/components/GridLayout.vue b/mobile/src/components/GridLayout.vue index 79765a48..9327ddd0 100644 --- a/mobile/src/components/GridLayout.vue +++ b/mobile/src/components/GridLayout.vue @@ -1,5 +1,5 @@ @@ -15,7 +15,7 @@ export default { }, gap: { type: String, - default: '12px', + default: '16px', }, }, computed: { @@ -29,7 +29,7 @@ export default { diff --git a/mobile/src/components/NavBar.vue b/mobile/src/components/NavBar.vue deleted file mode 100644 index 60c5948a..00000000 --- a/mobile/src/components/NavBar.vue +++ /dev/null @@ -1,176 +0,0 @@ - - - - - diff --git a/mobile/src/components/ServiceItem.vue b/mobile/src/components/ServiceItem.vue index 21332076..9f731d95 100644 --- a/mobile/src/components/ServiceItem.vue +++ b/mobile/src/components/ServiceItem.vue @@ -46,24 +46,24 @@ export default { diff --git a/mobile/src/components/StatCard.vue b/mobile/src/components/StatCard.vue deleted file mode 100644 index af1f4aa9..00000000 --- a/mobile/src/components/StatCard.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - - - diff --git a/mobile/src/components/TreeNode.vue b/mobile/src/components/TreeNode.vue new file mode 100644 index 00000000..2c2621a0 --- /dev/null +++ b/mobile/src/components/TreeNode.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/mobile/src/main.js b/mobile/src/main.js deleted file mode 100644 index 3304d0bf..00000000 --- a/mobile/src/main.js +++ /dev/null @@ -1,32 +0,0 @@ -import { createSSRApp } from 'vue'; -import App from './App.vue'; -import uviewPlus from 'uview-plus'; - -// 导入 uview-plus 样式 -import 'uview-plus/index.scss'; - -// 导入 iconfont 样式 -import '@/static/fonts/iconfont.css'; - -// 预加载字体文件 - 使用阿里CDN -uni.loadFontFace({ - family: 'uicon-iconfont', - source: 'url("https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf")', - success() { - console.log('uview-plus图标字体加载成功'); - }, - fail(error) { - console.error('uview-plus图标字体加载失败', error); - }, -}); - -export function createApp() { - const app = createSSRApp(App); - - // 注册 uview-plus - app.use(uviewPlus); - - return { - app, - }; -} diff --git a/mobile/src/main.ts b/mobile/src/main.ts new file mode 100644 index 00000000..216b9270 --- /dev/null +++ b/mobile/src/main.ts @@ -0,0 +1,9 @@ +import Vue from 'vue' +import App from './App.vue' +import './uni.promisify.adaptor' +import uView from "uview-ui"; +Vue.use(uView); +Vue.config.productionTip = false + +const app = new (typeof App === 'function' ? App : Vue.extend(Object.assign({ mpType: 'app' }, App))) +app.$mount(); diff --git a/mobile/src/manifest.json b/mobile/src/manifest.json index 309b9ec3..5adde367 100644 --- a/mobile/src/manifest.json +++ b/mobile/src/manifest.json @@ -1,60 +1,58 @@ -{ - "name" : "", - "appid" : "", - "description" : "", - "versionName" : "1.0.0", - "versionCode" : "100", - "transformPx" : false, - /* 5+App特有相关 */ - "app-plus" : { - "usingComponents" : true, - "nvueStyleCompiler" : "uni-app", - "compilerVersion" : 3, - "splashscreen" : { - "alwaysShowBeforeRender" : true, - "waiting" : true, - "autoclose" : true, - "delay" : 0 +{ + "name": "mobile2", + "appid": "", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx":false, + "app-plus": { /* 5+App特有相关 */ + "modules": { /* 模块配置 */ + + }, + "distribute": { /* 应用发布信息 */ + "android": { /* android打包配置 */ + "permissions": ["", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "ios": { /* ios打包配置 */ + + }, + "sdkConfigs": { /* SDK配置 */ + + } }, - /* 模块配置 */ - "modules" : {}, - /* 应用发布信息 */ - "distribute" : { - /* android打包配置 */ - "android" : { - "permissions" : [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ] - }, - /* ios打包配置 */ - "ios" : {}, - /* SDK配置 */ - "sdkConfigs" : {} - } - }, - /* 快应用特有相关 */ - "quickapp" : {}, - /* 小程序特有相关 */ - "mp-weixin" : { - "appid" : "", - "setting" : { - "urlCheck" : false - }, - "usingComponents" : true + "usingComponents":true + }, + "quickapp": { /* 快应用特有相关 */ + + }, + "mp-weixin": { /* 小程序特有相关 */ + "usingComponents":true, + "appid": "", + "setting" : { + "urlCheck" : true + } }, "mp-alipay" : { "usingComponents" : true @@ -65,8 +63,7 @@ "mp-toutiao" : { "usingComponents" : true }, - "uniStatistics": { - "enable": false - }, - "vueVersion" : "3" + "mp-qq" : { + "usingComponents" : true + } } diff --git a/mobile/src/pages.json b/mobile/src/pages.json index f7d50f72..75e1c6d0 100644 --- a/mobile/src/pages.json +++ b/mobile/src/pages.json @@ -1,71 +1,59 @@ { "easycom": { - "autoscan": true, - "custom": { - "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue", - "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue", - "^uv-(.*)": "uview-plus/components/u-$1/u-$1.vue" - } + "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue" }, "pages": [ { "path": "pages/login/index", "style": { "navigationBarTitleText": "登录", - "navigationStyle": "custom", - "navigationBarBackgroundColor": "transparent", - "backgroundColor": "transparent" + "navigationStyle": "custom" } }, { "path": "pages/index/index", "style": { - "navigationBarTitleText": "首页", + "navigationBarTitleText": "杭州第一技师学院智慧校园", "navigationStyle": "custom" } }, { - "path": "pages/iconfont-demo/index", + "path": "pages/task/myDocument", "style": { - "navigationBarTitleText": "图标演示", + "navigationBarTitleText": "我发起的", "navigationBarBackgroundColor": "#5d87ff", "navigationBarTextStyle": "white" } }, { - "path": "pages/test-route/index", + "path": "pages/task/allTaskWaiting", "style": { - "navigationBarTitleText": "路由守卫测试", - "navigationStyle": "custom" + "navigationBarTitleText": "我的待办", + "navigationBarBackgroundColor": "#5d87ff", + "navigationBarTextStyle": "white" } }, { - "path": "pages/workflow/process", + "path": "pages/task/taskFinish", "style": { - "navigationBarTitleText": "流程详情", - "navigationStyle": "custom" + "navigationBarTitleText": "我的已办", + "navigationBarBackgroundColor": "#5d87ff", + "navigationBarTextStyle": "white" } }, { - "path": "pages/workflow/initiated", + "path": "pages/task/taskCopyList", "style": { - "navigationBarTitleText": "我发起的流程", - "navigationStyle": "custom" - } - }, - { - "path": "pages/uview-icon-test/index", - "style": { - "navigationBarTitleText": "uview图标测试", + "navigationBarTitleText": "抄送我的", "navigationBarBackgroundColor": "#5d87ff", "navigationBarTextStyle": "white" } } ], "globalStyle": { - "navigationBarTextStyle": "white", + "navigationBarTextStyle": "black", "navigationBarTitleText": "智慧校园", - "navigationBarBackgroundColor": "rgb(93, 135, 255)", - "backgroundColor": "#f8f9fa" + "navigationBarBackgroundColor": "#5d87ff", + "backgroundColor": "#F8F8F8" } } \ No newline at end of file diff --git a/mobile/src/pages/demo/iconfont-demo/index.vue b/mobile/src/pages/demo/iconfont-demo/index.vue deleted file mode 100644 index 1e5f80ae..00000000 --- a/mobile/src/pages/demo/iconfont-demo/index.vue +++ /dev/null @@ -1,348 +0,0 @@ - - - - - diff --git a/mobile/src/pages/demo/test-route/index.vue b/mobile/src/pages/demo/test-route/index.vue deleted file mode 100644 index 185bfb5a..00000000 --- a/mobile/src/pages/demo/test-route/index.vue +++ /dev/null @@ -1,346 +0,0 @@ - - - - - diff --git a/mobile/src/pages/demo/uview-icon-test/index.vue b/mobile/src/pages/demo/uview-icon-test/index.vue deleted file mode 100644 index 5b356f2d..00000000 --- a/mobile/src/pages/demo/uview-icon-test/index.vue +++ /dev/null @@ -1,209 +0,0 @@ - - - - - diff --git a/mobile/src/pages/index/index.vue b/mobile/src/pages/index/index.vue index 2b496d9a..fb089852 100644 --- a/mobile/src/pages/index/index.vue +++ b/mobile/src/pages/index/index.vue @@ -1,821 +1,956 @@ - - - - - + + + + + diff --git a/mobile/src/pages/login/README.md b/mobile/src/pages/login/README.md deleted file mode 100644 index 0519ecba..00000000 --- a/mobile/src/pages/login/README.md +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/mobile/src/pages/login/index.vue b/mobile/src/pages/login/index.vue index 7580eb5f..bf32c5ba 100644 --- a/mobile/src/pages/login/index.vue +++ b/mobile/src/pages/login/index.vue @@ -17,14 +17,19 @@ @@ -40,7 +45,12 @@ @confirm="onPasswordConfirm" > @@ -60,7 +70,7 @@ - + + + 我的任务 + + + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 已选择 {{ userSelectCount }} 位申请人 + 清除 + + + + + + + + + 加载中... + + + + + + + 暂无数据 + + + + + + + + + + {{ item.flowName }} + + + {{ getStatusText(item.flowStatus) }} + + + + + {{ item.flowCode }} + {{ item.categoryName }} + + 任务:{{ item.nodeName }} + 申请人:{{ item.createByName }} + + + + 办理人: + + + 无 + + {{ item.approveName || '无' }} + + + + + 任务状态: + + {{ getTaskStatusText(item.flowTaskStatus) }} + + + + {{ formatTime(item.createTime) }} + + + + + + + + 查看 + + + 流程干预 + + + + + + + 查看 + + + + + + + + + + 正在加载更多... + + + 已加载全部数据 + + + + + + + + + 选择申请人 + + + + 请输入申请人姓名或工号 + + + + 搜索 + + + + + {{ user.userName }} + {{ user.deptName }} + + + + + 取消 + 确定 + + + + + + + + + 流程干预 + + + + 选择干预类型 + + + 委托办理 + + + 驳回申请 + + + 挂起流程 + + + + + 取消 + + + + + + + + + diff --git a/mobile/src/pages/task/myDocument.vue b/mobile/src/pages/task/myDocument.vue new file mode 100644 index 00000000..23c31b0e --- /dev/null +++ b/mobile/src/pages/task/myDocument.vue @@ -0,0 +1,764 @@ + + + + + diff --git a/mobile/src/pages/task/taskCopyList.vue b/mobile/src/pages/task/taskCopyList.vue new file mode 100644 index 00000000..21b81fbf --- /dev/null +++ b/mobile/src/pages/task/taskCopyList.vue @@ -0,0 +1,434 @@ + + + + + diff --git a/mobile/src/pages/task/taskFinish.vue b/mobile/src/pages/task/taskFinish.vue new file mode 100644 index 00000000..6acf2954 --- /dev/null +++ b/mobile/src/pages/task/taskFinish.vue @@ -0,0 +1,706 @@ + + + + + diff --git a/mobile/src/pages/workflow/initiated.vue b/mobile/src/pages/workflow/initiated.vue deleted file mode 100644 index d0169f7a..00000000 --- a/mobile/src/pages/workflow/initiated.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - - - diff --git a/mobile/src/pages/workflow/process.vue b/mobile/src/pages/workflow/process.vue deleted file mode 100644 index 8fe3a3ed..00000000 --- a/mobile/src/pages/workflow/process.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/mobile/src/shime-uni.d.ts b/mobile/src/shime-uni.d.ts deleted file mode 100644 index b3d3cc9c..00000000 --- a/mobile/src/shime-uni.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export {}; - -declare module "vue" { - type Hooks = App.AppInstance & Page.PageInstance; - interface ComponentCustomOptions extends Hooks {} -} \ No newline at end of file diff --git a/mobile/src/static/fonts/font_2225171_8kdcwk4po24.ttf b/mobile/src/static/fonts/font_2225171_8kdcwk4po24.ttf deleted file mode 100644 index befa24aa..00000000 Binary files a/mobile/src/static/fonts/font_2225171_8kdcwk4po24.ttf and /dev/null differ diff --git a/mobile/src/static/fonts/iconfont.css b/mobile/src/static/fonts/iconfont.css index 66021018..ed0cf9e7 100644 --- a/mobile/src/static/fonts/iconfont.css +++ b/mobile/src/static/fonts/iconfont.css @@ -5,248 +5,20 @@ background-size: contain; background-repeat: no-repeat; background-position: center; + color: #666; /* 设置默认图标颜色为灰色,避免黑色显示 */ } -/* 用户图标 */ -.icon-user { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 密码锁图标 */ -.icon-lock { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 眼睛图标(显示密码) */ -.icon-eye { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z'/%3E%3C/svg%3E"); -} - -/* 眼睛关闭图标(隐藏密码) */ -.icon-eye-close { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z'/%3E%3C/svg%3E"); -} - -/* 登录图标 */ -.icon-login { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M11 7L9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h8c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-8v2h8v14z'/%3E%3C/svg%3E"); -} - -/* 箭头右图标 */ -.icon-arrow-right { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E"); -} - -/* 更多常用图标 */ -.icon-home { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); -} - -.icon-message { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -.icon-setting { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -.icon-search { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -.icon-add { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'/%3E%3C/svg%3E"); -} - -.icon-check { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E"); -} - -.icon-close { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -.icon-phone { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -.icon-mail { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 新增常用图标 - 2025-01-07 */ - -/* 星星图标 */ -.icon-star { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 心形图标 */ -.icon-heart { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 日历图标 */ -.icon-calendar { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 聊天图标 */ -.icon-chat { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z'/%3E%3C/svg%3E"); -} - -/* 工具图标 */ -.icon-tools { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z'/%3E%3C/svg%3E"); -} - -/* 地图图标 */ -.icon-map { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M20.5 3l-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM15 19l-6-2.11V5l6 2.11V19z'/%3E%3C/svg%3E"); -} - -/* 播放圆圈图标 */ -.icon-play-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z'/%3E%3C/svg%3E"); -} - -/* 列表图标 */ -.icon-list { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z'/%3E%3C/svg%3E"); -} - -/* 人民币圆圈图标 */ -.icon-rmb-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 6L12 10.5 10.5 8H9.5l1.75 2.5H9v1h3v1H9v1h3V15h1v-1.5h3v-1h-3v-1h3v-1h-2.25L14.5 8h-1z'/%3E%3C/svg%3E"); -} - -/* 商店图标 */ -.icon-shop { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27 16.18 14l1.64 7L12 17.77 6.18 21l1.64-7L2 9.27l6.91-1.01L12 2zm0 0L4 8v12h16V8l-8-6z'/%3E%3C/svg%3E"); -} - -/* 奖杯图标 */ -.icon-trophy { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M7 4V2c0-1.1.9-2 2-2h6c1.1 0 2 .9 2 2v2h4c1.1 0 2 .9 2 2v4c0 2.21-1.79 4-4 4h-1.28c-.44 2.83-2.82 5-5.72 5s-5.28-2.17-5.72-5H5c-2.21 0-4-1.79-4-4V6c0-1.1.9-2 2-2h4zM9 4h6V2H9v2zM5 6v4c0 1.1.9 2 2 2h1.28c.42-1.82 1.89-3.26 3.72-3.26s3.3 1.44 3.72 3.26H17c1.1 0 2-.9 2-2V6H5zm7 14c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3z'/%3E%3C/svg%3E"); -} - -/* 朋友图标 */ -.icon-friends { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.495 1.495 0 0 0 18.5 7.5h-3c-.57 0-1.08.32-1.34.82L11.5 15h2.5v7h6zM12.5 11.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5S11 9.17 11 10s.67 1.5 1.5 1.5zM5.5 6c1.11 0 2-.89 2-2s-.89-2-2-2-2 .89-2 2 .89 2 2 2zm2 16v-7H9l-1.46-3.12A1.495 1.495 0 0 0 6.12 11h-1.5c-.57 0-1.08.32-1.34.82L1 17h2.5v7h4z'/%3E%3C/svg%3E"); -} - -/* 飞机图标 */ -.icon-plane { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z'/%3E%3C/svg%3E"); -} - -/* 书签图标 */ -.icon-bookmark { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z'/%3E%3C/svg%3E"); -} - -/* 人民币圆圈填充图标 */ -.icon-rmb-circle-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 6L12 10.5 10.5 8H9.5l1.75 2.5H9v1h3v1H9v1h3V15h1v-1.5h3v-1h-3v-1h3v-1h-2.25L14.5 8h-1z'/%3E%3C/svg%3E"); -} - -/* 金钱圆圈图标 */ -.icon-money-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z'/%3E%3C/svg%3E"); -} - -/* 文件文本图标 */ -.icon-file-text { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z'/%3E%3C/svg%3E"); -} - -/* 订单图标 */ -.icon-order { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E"); -} - -/* 设置填充图标 */ -.icon-setting-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3A8.994 8.994 0 0 0 13 3.06V1h-2v2.06A8.994 8.994 0 0 0 3.06 11H1v2h2.06A8.994 8.994 0 0 0 11 20.94V23h2v-2.06A8.994 8.994 0 0 0 20.94 13H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z'/%3E%3C/svg%3E"); -} - -/* 用户圆圈填充图标 */ -.icon-account-circle-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E"); -} - -/* 列表点图标 */ -.icon-list-dot { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z'/%3E%3C/svg%3E"); -} - -/* 首页填充图标 */ -.icon-home-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z'/%3E%3C/svg%3E"); -} - -/* WiFi图标 */ -.icon-wifi { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z'/%3E%3C/svg%3E"); -} - -/* 分享图标 */ -.icon-share { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z'/%3E%3C/svg%3E"); -} - -/* 分享方形图标 */ -.icon-share-square { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M18 2h-3c-1.1 0-2 .9-2 2v3H7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-6h3c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 15H7v-3h3v3zm0-5H7v-2h3v2zm0-4H7V6h3v2zm5 9h-3v-3h3v3zm0-5h-3v-2h3v2zm0-4h-3V6h3v2zm5-3h-3V4h3v1z'/%3E%3C/svg%3E"); -} - -/* 编辑笔图标 */ -.icon-edit-pen { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 时钟图标 */ -.icon-clock { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z'/%3E%3C/path%3E%3Cpath d='m12.5 7-1 1v4.25l3.5 2.08.75-1.23-2.75-1.63V8z'/%3E%3C/svg%3E"); -} - -/* 确认圆圈图标 */ -.icon-checkmark-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 复制图标 */ -.icon-copy { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E"); -} - -/* 用户圆圈图标 */ -.icon-account-circle { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E"); -} - -/* 用户填充图标 */ -.icon-account-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 星星填充图标 */ -.icon-star-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E"); -} - -/* 包图标 */ -.icon-bag { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M17 6h-2V3c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v3H7c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zM9.5 3h5v3h-5V3zM9 12v-2h2v2h2v-2h2v2c0 1.1-.9 2-2 2h-2c-1.1 0-2-.9-2-2z'/%3E%3C/svg%3E"); -} - -/* 学工管理填充图标 */ -.icon-account-fill { - background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); -} - -/* 登出图标 */ +/* 退出登录图标 */ .icon-logout { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.59L17 17l5-5-5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z'/%3E%3C/svg%3E"); } + +/* 电源开关图标 */ +.icon-power { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.59-5.41L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z'/%3E%3C/svg%3E"); +} + +/* 关闭圆圈图标 */ +.icon-close-circle { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='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'/%3E%3C/svg%3E"); +} \ No newline at end of file diff --git a/mobile/src/static/logo-1.png b/mobile/src/static/logo-1.png deleted file mode 100644 index ce1caf95..00000000 Binary files a/mobile/src/static/logo-1.png and /dev/null differ diff --git a/mobile/src/static/tabbar/home-active.svg b/mobile/src/static/tabbar/home-active.svg deleted file mode 100644 index 60652641..00000000 --- a/mobile/src/static/tabbar/home-active.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/mobile/src/static/tabbar/home.svg b/mobile/src/static/tabbar/home.svg deleted file mode 100644 index 768819fe..00000000 --- a/mobile/src/static/tabbar/home.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/mobile/src/static/xx01.jpg b/mobile/src/static/xx01.jpg new file mode 100644 index 00000000..0a736165 Binary files /dev/null and b/mobile/src/static/xx01.jpg differ diff --git a/mobile/src/static/xx02.jpg b/mobile/src/static/xx02.jpg new file mode 100644 index 00000000..aad102d9 Binary files /dev/null and b/mobile/src/static/xx02.jpg differ diff --git a/mobile/src/static/xx03.jpg b/mobile/src/static/xx03.jpg new file mode 100644 index 00000000..9c91ca16 Binary files /dev/null and b/mobile/src/static/xx03.jpg differ diff --git a/mobile/src/types/uview-ui.d.ts b/mobile/src/types/uview-ui.d.ts new file mode 100644 index 00000000..9dd501e0 --- /dev/null +++ b/mobile/src/types/uview-ui.d.ts @@ -0,0 +1,10 @@ +declare module 'uview-ui' { + import { VueConstructor } from 'vue' + + interface UViewUIInstall { + install: (vue: VueConstructor) => void + } + + const uView: UViewUIInstall + export default uView +} \ No newline at end of file diff --git a/mobile/src/uni.promisify.adaptor.ts b/mobile/src/uni.promisify.adaptor.ts new file mode 100644 index 00000000..168d9df0 --- /dev/null +++ b/mobile/src/uni.promisify.adaptor.ts @@ -0,0 +1,10 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + res.then((res: [unknown, unknown]) => res[0] ? reject(res[0]) : resolve(res[1])); + }); + }, +}); \ No newline at end of file diff --git a/mobile/src/uni.scss b/mobile/src/uni.scss index 69cd1c25..2138bdc7 100644 --- a/mobile/src/uni.scss +++ b/mobile/src/uni.scss @@ -12,53 +12,38 @@ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 */ -// 引入 uview-plus 样式 -@import 'uview-plus/theme.scss'; +/* 颜色变量 */ +/* uni.scss */ +@import 'uview-ui/theme.scss'; -// 引入 uview-plus 图标字体 - 使用阿里CDN -@font-face { - font-family: 'uicon-iconfont'; - src: url('https://at.alicdn.com/t/font_2225171_8kdcwk4po24.ttf') format('truetype'); -} - -/* 原有的Sass变量定义 - 保持向后兼容 */ -$uni-color-primary: rgb(93, 135, 255); -$uni-color-primary-dark: rgb(73, 115, 235); -$uni-color-primary-light: rgba(93, 135, 255, 0.1); - -/* 状态颜色 */ -$uni-color-success: #52c41a; -$uni-color-warning: #faad14; -$uni-color-error: #ff4d4f; -$uni-color-info: #1890ff; +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; /* 文字基本颜色 */ -$uni-text-color: #333333; -$uni-text-color-inverse: #fff; -$uni-text-color-secondary: #666666; -$uni-text-color-grey: #999999; -$uni-text-color-placeholder: #c0c0c0; -$uni-text-color-disable: #d9d9d9; +$uni-text-color: #333; // 基本色 +$uni-text-color-inverse: #fff; // 反色 +$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable: #c0c0c0; /* 背景颜色 */ -$uni-bg-color: #ffffff; -$uni-bg-color-grey: #f8f9fa; -$uni-bg-color-hover: #f1f1f1; -$uni-bg-color-mask: rgba(0, 0, 0, 0.4); +$uni-bg-color: #fff; +$uni-bg-color-grey: #f8f8f8; +$uni-bg-color-hover: #f1f1f1; // 点击状态颜色 +$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色 /* 边框颜色 */ -$uni-border-color: #e0e0e0; +$uni-border-color: #c8c7cc; -/* 阴影 */ -$uni-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); -$uni-shadow-hover: 0 4px 20px rgba(0, 0, 0, 0.15); +/* 尺寸变量 */ /* 文字尺寸 */ $uni-font-size-sm: 12px; $uni-font-size-base: 14px; $uni-font-size-lg: 16px; -$uni-font-size-xl: 18px; -$uni-font-size-xxl: 20px; /* 图片尺寸 */ $uni-img-size-sm: 20px; @@ -66,244 +51,28 @@ $uni-img-size-base: 26px; $uni-img-size-lg: 40px; /* Border Radius */ -$uni-border-radius-sm: 6px; -$uni-border-radius-base: 8px; -$uni-border-radius-lg: 12px; -$uni-border-radius-xl: 16px; +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; $uni-border-radius-circle: 50%; -/* 间距 */ -$uni-spacing-row-sm: 8px; -$uni-spacing-row-base: 12px; -$uni-spacing-row-lg: 16px; -$uni-spacing-row-xl: 20px; +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; -$uni-spacing-col-sm: 8px; -$uni-spacing-col-base: 12px; -$uni-spacing-col-lg: 16px; -$uni-spacing-col-xl: 20px; +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; /* 透明度 */ -$uni-opacity-disabled: 0.3; +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 /* 文章场景相关 */ -$uni-color-title: #2c405a; +$uni-color-title: #2c405a; // 文章标题颜色 $uni-font-size-title: 20px; -$uni-color-subtitle: #555; +$uni-color-subtitle: #555; // 二级标题颜色 $uni-font-size-subtitle: 18px; -$uni-color-paragraph: #3f536e; +$uni-color-paragraph: #3f536e; // 文章段落颜色 $uni-font-size-paragraph: 15px; - -/* 动画 */ -$uni-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -$uni-transition-fast: all 0.15s ease-out; - -/* 主题色彩变量 - CSS变量版本 */ -:root { - --primary-color: rgb(93, 135, 255); - --primary-light: rgba(93, 135, 255, 0.1); - --primary-dark: rgb(73, 115, 235); - --success-color: #52c41a; - --warning-color: #faad14; - --error-color: #ff4d4f; - --text-primary: #262626; - --text-secondary: #595959; - --text-disabled: #bfbfbf; - --border-color: #f0f0f0; - --background-color: #f8f9fa; - --white: #ffffff; - --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06); - --shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12); - --radius-small: 4px; - --radius-medium: 8px; - --radius-large: 12px; - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --spacing-xl: 32px; - --transition-fast: 0.2s ease; - --transition-medium: 0.3s ease; - --font-size-xs: 12px; - --font-size-sm: 14px; - --font-size-md: 16px; - --font-size-lg: 18px; - --font-size-xl: 20px; -} - -/* 全局样式重置 */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; - color: var(--text-primary); - background-color: var(--background-color); - line-height: 1.6; -} - -page { - background-color: var(--background-color); - color: var(--text-primary); - font-size: var(--font-size-md); - line-height: 1.6; -} - -/* 通用工具类 */ -.flex { - display: flex; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -.flex-between { - display: flex; - align-items: center; - justify-content: space-between; -} - -.flex-column { - display: flex; - flex-direction: column; -} - -.text-center { - text-align: center; -} - -.text-primary { - color: var(--primary-color); -} - -.text-secondary { - color: var(--text-secondary); -} - -.bg-primary { - background-color: var(--primary-color); -} - -.bg-white { - background-color: var(--white); -} - -.rounded { - border-radius: var(--radius-medium); -} - -.shadow { - box-shadow: var(--shadow-light); -} - -.mt-sm { margin-top: var(--spacing-sm); } -.mt-md { margin-top: var(--spacing-md); } -.mt-lg { margin-top: var(--spacing-lg); } -.mb-sm { margin-bottom: var(--spacing-sm); } -.mb-md { margin-bottom: var(--spacing-md); } -.mb-lg { margin-bottom: var(--spacing-lg); } -.ml-sm { margin-left: var(--spacing-sm); } -.ml-md { margin-left: var(--spacing-md); } -.mr-sm { margin-right: var(--spacing-sm); } -.mr-md { margin-right: var(--spacing-md); } - -.p-sm { padding: var(--spacing-sm); } -.p-md { padding: var(--spacing-md); } -.p-lg { padding: var(--spacing-lg); } -.px-sm { padding-left: var(--spacing-sm); padding-right: var(--spacing-sm); } -.px-md { padding-left: var(--spacing-md); padding-right: var(--spacing-md); } -.py-sm { padding-top: var(--spacing-sm); padding-bottom: var(--spacing-sm); } -.py-md { padding-top: var(--spacing-md); padding-bottom: var(--spacing-md); } - -/* 按钮基础样式 */ -.btn { - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius-medium); - border: none; - font-size: var(--font-size-md); - cursor: pointer; - transition: var(--transition-fast); - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; -} - -.btn-primary { - background-color: var(--primary-color); - color: var(--white); -} - -.btn-primary:hover { - background-color: var(--primary-dark); -} - -/* 输入框样式 */ -.input { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--border-color); - border-radius: var(--radius-medium); - font-size: var(--font-size-md); - transition: var(--transition-fast); -} - -.input:focus { - border-color: var(--primary-color); - outline: none; - box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.2); -} - -/* 卡片样式 */ -.card { - background: var(--white); - border-radius: var(--radius-large); - box-shadow: var(--shadow-light); - padding: var(--spacing-md); - margin-bottom: var(--spacing-md); -} - -.card-header { - font-size: var(--font-size-lg); - font-weight: 600; - margin-bottom: var(--spacing-md); - color: var(--text-primary); -} - -/* 动画类 */ -.fade-in { - animation: fadeIn var(--transition-medium); -} - -.slide-up { - animation: slideUp var(--transition-medium); -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 响应式断点 */ -@media (max-width: 768px) { - .card { - margin: var(--spacing-sm); - padding: var(--spacing-sm); - } -} \ No newline at end of file diff --git a/mobile/src/utils/README.md b/mobile/src/utils/README.md deleted file mode 100644 index 04885da7..00000000 --- a/mobile/src/utils/README.md +++ /dev/null @@ -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 端功能对齐 -- 支持入参加密、出参解密、防重复提交等完整功能 -- 新增配置管理系统,提升可维护性 \ No newline at end of file diff --git a/mobile/src/utils/accessibility.js b/mobile/src/utils/accessibility.js deleted file mode 100644 index ae80ddfd..00000000 --- a/mobile/src/utils/accessibility.js +++ /dev/null @@ -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 }; diff --git a/mobile/src/utils/animations.js b/mobile/src/utils/animations.js deleted file mode 100644 index 6ad242bf..00000000 --- a/mobile/src/utils/animations.js +++ /dev/null @@ -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 = ` -
-
-
- `; - - 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 }; diff --git a/mobile/src/utils/config.js b/mobile/src/utils/config.js deleted file mode 100644 index 84fde029..00000000 --- a/mobile/src/utils/config.js +++ /dev/null @@ -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); diff --git a/mobile/src/utils/device.js b/mobile/src/utils/device.js deleted file mode 100644 index 5fd4a09a..00000000 --- a/mobile/src/utils/device.js +++ /dev/null @@ -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 - }, -} diff --git a/mobile/src/utils/encrypt-browser.js b/mobile/src/utils/encrypt-browser.js deleted file mode 100644 index 1e467811..00000000 --- a/mobile/src/utils/encrypt-browser.js +++ /dev/null @@ -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} 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} 包含加密数据和请求头的对象 - */ -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; -} diff --git a/mobile/src/utils/encrypt-jsencrypt.js b/mobile/src/utils/encrypt-jsencrypt.js deleted file mode 100644 index d90c8f24..00000000 --- a/mobile/src/utils/encrypt-jsencrypt.js +++ /dev/null @@ -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; -} diff --git a/mobile/src/utils/encrypt-pure.js b/mobile/src/utils/encrypt-pure.js deleted file mode 100644 index b22d6610..00000000 --- a/mobile/src/utils/encrypt-pure.js +++ /dev/null @@ -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; -} diff --git a/mobile/src/utils/encrypt-test.js b/mobile/src/utils/encrypt-test.js deleted file mode 100644 index c7755a14..00000000 --- a/mobile/src/utils/encrypt-test.js +++ /dev/null @@ -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; - } -} diff --git a/mobile/src/utils/encrypt-webcrypto.js b/mobile/src/utils/encrypt-webcrypto.js deleted file mode 100644 index b83b18dc..00000000 --- a/mobile/src/utils/encrypt-webcrypto.js +++ /dev/null @@ -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} - */ -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} 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} 包含加密数据和请求头的对象 - */ -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; -} diff --git a/mobile/src/utils/encrypt.js b/mobile/src/utils/encrypt.js deleted file mode 100644 index 5fb7901e..00000000 --- a/mobile/src/utils/encrypt.js +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/mobile/src/utils/gestures.js b/mobile/src/utils/gestures.js deleted file mode 100644 index 82d9de7e..00000000 --- a/mobile/src/utils/gestures.js +++ /dev/null @@ -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 = ` -
- ${items - .map( - (item) => ` - - `, - ) - .join('')} -
- `; - - 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; diff --git a/mobile/src/utils/iconHelper.js b/mobile/src/utils/iconHelper.js deleted file mode 100644 index 596ac130..00000000 --- a/mobile/src/utils/iconHelper.js +++ /dev/null @@ -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 = ``; - 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 = \` - -\`; - -const css = generateIconCSSFromSVG('circle', svgContent); -console.log(css);`, - }, -}; - -// 导出所有工具函数 -export default { - generateIconCSS, - generateBatchIconCSS, - generateIconCSSFromSVG, - generateCommonIconsCSS, - checkIconExists, - validateSVG, - COMMON_ICONS, - USAGE_EXAMPLES, -}; diff --git a/mobile/src/utils/permission.js b/mobile/src/utils/permission.js index bedccd61..fcf3c4e0 100644 --- a/mobile/src/utils/permission.js +++ b/mobile/src/utils/permission.js @@ -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]); }, }, -} +}; diff --git a/mobile/src/utils/route-guard.js b/mobile/src/utils/route-guard.js deleted file mode 100644 index 58b5dc2f..00000000 --- a/mobile/src/utils/route-guard.js +++ /dev/null @@ -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, -}; diff --git a/mobile/src/utils/sse.js b/mobile/src/utils/sse.js deleted file mode 100644 index 8ea81284..00000000 --- a/mobile/src/utils/sse.js +++ /dev/null @@ -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() - }, -} diff --git a/mobile/src/utils/token-test.js b/mobile/src/utils/token-test.js deleted file mode 100644 index 34b6a07d..00000000 --- a/mobile/src/utils/token-test.js +++ /dev/null @@ -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; - } -} diff --git a/mobile/src/utils/user.js b/mobile/src/utils/user.js index 1835896f..5a4e16fe 100644 --- a/mobile/src/utils/user.js +++ b/mobile/src/utils/user.js @@ -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; diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json new file mode 100644 index 00000000..5ef2763d --- /dev/null +++ b/mobile/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators":true, + "sourceMap": true, + "skipLibCheck": true, + "baseUrl": ".", + "types": [ + "webpack-env", + "@dcloudio/types" + ], + "paths": { + "@/*": [ + "./src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "exclude": [ + "node_modules", + "unpackage", + "dist", + "src/**/*.nvue" + ] +} diff --git a/mobile/vite.config.js b/mobile/vite.config.js deleted file mode 100644 index ddd29f15..00000000 --- a/mobile/vite.config.js +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig } from 'vite'; -import uni from '@dcloudio/vite-plugin-uni'; -import { nodePolyfills } from 'vite-plugin-node-polyfills'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - uni(), - nodePolyfills({ - // 启用需要的 polyfills - include: ['buffer', 'crypto', 'stream', 'util'], - globals: { - Buffer: true, - global: true, - process: true, - }, - }), - ], - define: { - global: 'globalThis', - }, - css: { - preprocessorOptions: { - scss: { - // 静默废弃警告,特别是来自第三方依赖的警告 - silenceDeprecations: ['legacy-js-api', 'import'], - // 静默来自 node_modules 的警告 - quietDeps: true, - charset: false, - }, - }, - }, - // 开发服务器配置 - server: { - port: 5173, - host: '0.0.0.0', - open: true, - // 代理配置解决CORS问题 - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), - }, - }, - }, -}); diff --git a/mobile/vue.config.js b/mobile/vue.config.js new file mode 100644 index 00000000..7b464d92 --- /dev/null +++ b/mobile/vue.config.js @@ -0,0 +1,32 @@ +// vue.config.js,如没有此文件则手动创建 +module.exports = { + transpileDependencies: ['uview-ui'], + + // 开发服务器配置 + devServer: { + port: 8081, // 使用不同端口避免与mobile工程冲突 + host: '0.0.0.0', + open: true, + // 代理配置解决CORS问题 - 与mobile工程保持一致 + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + pathRewrite: { + '^/api': '', // 移除/api前缀,直接转发到后端 + }, + onProxyReq: function (proxyReq, req, res) { + console.log( + '代理请求:', + req.method, + req.url, + '-> http://localhost:8080' + req.url.replace(/^\/api/, ''), + ); + }, + onError: function (err, req, res) { + console.error('代理错误:', err); + }, + }, + }, + }, +};