提交新的移动端框架

This commit is contained in:
zlwu committed 2025-08-07 13:19:12 +08:00
1 parent 134f711349
commit 9bda3717fe
75 files changed
+5283 -9011

No files matched your search

+195
View File
@@ -358,6 +358,51 @@ export default {
## 5. 🎨 图标使用规范 (SVG Data URI 方案)
### 🚨 强制性图标使用规范(2025-08-06 新增)
**⚠️ 重要说明**: 移动端项目已全面禁用 uview-plus 的 u-icon 组件,强制使用 iconfont 图标系统!
#### 📋 强制要求
1. **🚫 禁止使用 u-icon 组件**
```vue
<!-- ❌ 禁止这样写 -->
<u-icon name="search" size="20" color="#666"></u-icon>
<u-icon name="account" size="24"></u-icon>
<u-icon name="close" @click="handleClose"></u-icon>
```
2. **✅ 必须使用 iconfont 图标**
```vue
<!-- ✅ 正确写法 -->
<text class="iconfont icon-search" style="font-size: 20px; color: #666;"></text>
<text class="iconfont icon-account-circle" style="font-size: 24px;"></text>
<text class="iconfont icon-close" @click="handleClose"></text>
```
3. **🔄 动态图标类名**
```vue
<!-- ✅ 动态切换图标 -->
<text
:class="'iconfont ' + (expanded ? 'icon-arrow-down' : 'icon-arrow-right')"
style="font-size: 16px; color: #999;"
@click="toggleExpand"
></text>
```
#### 🎯 替换映射表
| 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开发中,图标组件统一使用 <u-icon />(uview-plus 3.x 官方标准),禁止使用 <up-icon /> 或 <uv-icon />。
- 推荐通过 easycom 自动全局注册,无需手动 import。
- 标准用法示例:
```vue
<u-icon name="account" size="22" color="#b0b8c9" />
<u-icon name="lock-open" size="22" color="#b0b8c9" />
```
- 图标名称请参考 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图标**: 使用 `<u-icon name="home" />` 系统通用图标
- **项目iconfont**: 使用 `<text class="iconfont icon-user"></text>` 业务专用图标
- **优先级**: 项目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`
+13 -11
View File
@@ -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?
*.sw*
+1
View File
@@ -0,0 +1 @@
shamefully-hoist=true
-249
View File
@@ -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. **可维护架构**: 模块化设计,易于扩展和维护
---
_�� 让校园生活更便捷、更智能、更美好!_
+11 -169
View File
@@ -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/).
+81
View File
@@ -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
@@ -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 = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'>
<path d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z'/>
</svg>`;
```
#### 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
<template>
<text class="iconfont icon-add-circle" style="font-size: 24px; color: #1890ff;"></text>
</template>
```
### 方法二: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
<template>
<!-- 方式1:直接使用class -->
<text class="iconfont icon-user"></text>
<!-- 方式2:自定义样式 -->
<text class="iconfont icon-setting custom-icon"></text>
<!-- 方式3:动态样式 -->
<text
class="iconfont icon-home"
:style="{
fontSize: '24px',
color: iconColor
}"
></text>
</template>
<style>
.custom-icon {
font-size: 20px;
color: #52c41a;
}
</style>
```
### 响应式尺寸
```vue
<template>
<!-- 小图标 -->
<text class="iconfont icon-user icon-sm"></text>
<!-- 中等图标 -->
<text class="iconfont icon-user icon-md"></text>
<!-- 大图标 -->
<text class="iconfont icon-user icon-lg"></text>
</template>
<style>
.icon-sm { width: 16px; height: 16px; font-size: 16px; }
.icon-md { width: 24px; height: 24px; font-size: 24px; }
.icon-lg { width: 32px; height: 32px; font-size: 32px; }
</style>
```
## 📋 当前可用图标列表
### 基础图标
- `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 = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${viewBox}' fill='currentColor'><path d='${svgPath}'/></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
<!-- 迁移前 -->
<u-icon name="logout" size="20" color="#fff"></u-icon>
<!-- 迁移后 -->
<text class="iconfont icon-close" style="font-size: 20px; color: #fff;"></text>
```
### 批量替换工具
```bash
# 使用正则表达式批量替换
# 将 <u-icon name="logout" 替换为 <text class="iconfont icon-close"
find ./src -name "*.vue" -exec sed -i 's/<u-icon name="logout"/<text class="iconfont icon-close"/g' {} \;
```
## 🚀 性能优化
### 1. 按需加载
```javascript
// 动态导入图标CSS(大型应用)
const loadIconFont = async () => {
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 方式,兼容性最好,性能最优。
-33
View File
@@ -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'],
},
},
];
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+107 -70
View File
@@ -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": {}
}
}
+27
View File
@@ -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
+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
</head>
<body>
<noscript>
<strong>Please enable JavaScript to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+7 -6
View File
@@ -1,10 +1,11 @@
/// <reference types='@dcloudio/types' />
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<V extends Vue> extends Hooks {
/**
* 组件类型
*/
mpType?: string;
}
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}
+13 -24
View File
@@ -1,34 +1,23 @@
<script>
import { initRouteGuard } from '@/utils/route-guard';
export default {
onLaunch: function () {
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
mpType: 'app',
onLaunch() {
console.log('App Launch');
// 初始化全局路由守卫
initRouteGuard();
},
onShow: function () {
onShow() {
console.log('App Show');
},
onHide: function () {
onHide() {
console.log('App Hide');
},
};
});
</script>
<style>
/* 引入 uview-plus 全局样式 */
@import 'uview-plus/index.scss';
<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
@import 'uview-ui/index.scss';
/* 每个页面公共css */
page {
background-color: #f8f8f8;
}
/* 全局字体设置 */
* {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 引入自定义图标系统 */
@import '@/static/fonts/iconfont.css';
</style>
+72
View File
@@ -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,
};
};
@@ -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 };
@@ -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 };
+267
View File
@@ -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;
}
},
};
@@ -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({
+16 -16
View File
@@ -45,36 +45,36 @@ export default {
<style lang="scss" scoped>
.card {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
box-shadow: $uni-shadow;
transition: $uni-transition;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
margin: $uni-spacing-col-lg;
margin: 16px 0;
&:hover {
box-shadow: $uni-shadow-hover;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
}
.card-header {
padding: $uni-spacing-col-lg $uni-spacing-row-xl;
border-bottom: 1px solid $uni-border-color;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-title {
font-size: $uni-font-size-lg;
font-size: 16px;
font-weight: 600;
color: $uni-text-color;
color: #333333;
margin: 0;
}
.card-content {
padding: $uni-spacing-col-xl;
padding: 20px;
&.no-padding {
padding: 0;
@@ -82,22 +82,22 @@ export default {
}
.card-footer {
padding: $uni-spacing-col-lg $uni-spacing-row-xl;
border-top: 1px solid $uni-border-color;
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
background: rgba(0, 0, 0, 0.02);
}
/* 响应式设计 */
@media (max-width: 375px) {
.card {
margin: $uni-spacing-col-base;
margin: 12px 0;
}
.card-header,
.card-content,
.card-footer {
padding-left: $uni-spacing-row-lg;
padding-right: $uni-spacing-row-lg;
padding-left: 16px;
padding-right: 16px;
}
}
</style>
+7 -6
View File
@@ -1,5 +1,5 @@
<template>
<view class="grid-layout" :class="gridClass">
<view class="grid-layout" :class="gridClass" :style="{ '--grid-gap': gap }">
<slot></slot>
</view>
</template>
@@ -15,7 +15,7 @@ export default {
},
gap: {
type: String,
default: '12px',
default: '16px',
},
},
computed: {
@@ -29,7 +29,7 @@ export default {
<style lang="scss" scoped>
.grid-layout {
display: grid;
gap: v-bind(gap);
gap: 16px !important;
width: 100%;
}
@@ -49,15 +49,15 @@ export default {
@media (max-width: 375px) {
.grid-4 {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
gap: 12px;
}
.grid-3 {
gap: 8px;
gap: 12px;
}
.grid-2 {
gap: 8px;
gap: 12px;
}
}
@@ -65,6 +65,7 @@ export default {
.grid-4,
.grid-3 {
grid-template-columns: 1fr 1fr;
gap: 10px;
}
}
</style>
-176
View File
@@ -1,176 +0,0 @@
<template>
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="navbar-title">{{ title }}</text>
<view class="navbar-actions">
<slot name="actions">
<view
v-if="showRefresh"
class="btn-icon refresh-btn touchable"
@click="handleRefresh"
:aria-label="refreshLabel"
>
<text class="icon">🔄</text>
</view>
<view
v-if="showSearch"
class="btn-icon touchable"
@click="handleSearch"
:aria-label="searchLabel"
>
<text class="icon">🔍</text>
</view>
<view
v-if="showSettings"
class="btn-icon touchable"
@click="handleSettings"
:aria-label="settingsLabel"
>
<text class="icon">⚙️</text>
</view>
</slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'NavBar',
props: {
title: {
type: String,
default: '智慧校园',
},
showRefresh: {
type: Boolean,
default: false,
},
showSearch: {
type: Boolean,
default: false,
},
showSettings: {
type: Boolean,
default: false,
},
refreshLabel: {
type: String,
default: '刷新',
},
searchLabel: {
type: String,
default: '搜索',
},
settingsLabel: {
type: String,
default: '设置',
},
},
data() {
return {
statusBarHeight: 20,
};
},
mounted() {
// 获取状态栏高度
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 20;
},
methods: {
handleRefresh() {
this.$emit('refresh');
// 触觉反馈
uni.vibrateShort();
},
handleSearch() {
this.$emit('search');
uni.vibrateShort();
},
handleSettings() {
this.$emit('settings');
uni.vibrateShort();
},
},
};
</script>
<style lang="scss" scoped>
.navbar {
background: linear-gradient(135deg, $uni-color-primary, $uni-color-primary-dark);
color: $uni-text-color-inverse;
padding-bottom: 16px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: $uni-shadow;
}
.navbar-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 $uni-spacing-row-lg;
min-height: 44px;
}
.navbar-title {
font-size: $uni-font-size-xl;
font-weight: 600;
color: $uni-text-color-inverse;
}
.navbar-actions {
display: flex;
gap: $uni-spacing-row-base;
}
.btn-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: $uni-border-radius-circle;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: $uni-transition;
&:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.icon {
font-size: 18px;
color: $uni-text-color-inverse;
}
}
/* 动画效果 */
.refresh-btn {
animation: none;
&:active {
animation: spin 0.6s ease-in-out;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 无障碍支持 */
.btn-icon[aria-label] {
position: relative;
&:focus {
outline: 2px solid rgba(255, 255, 255, 0.5);
outline-offset: 2px;
}
}
</style>
+90 -80
View File
@@ -46,24 +46,24 @@ export default {
<style lang="scss" scoped>
.service-item {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
padding: $uni-spacing-col-xl;
background: #ffffff;
border-radius: 12px;
padding: 9px 16px;
text-align: center;
box-shadow: $uni-shadow;
transition: $uni-transition;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid transparent;
position: relative;
&:hover {
transform: translateY(-3px);
box-shadow: $uni-shadow-hover;
border-color: $uni-color-primary;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: #007aff;
}
&:focus {
outline: 2px solid $uni-color-primary;
outline: 2px solid #007aff;
outline-offset: 2px;
}
}
@@ -71,133 +71,143 @@ export default {
.service-icon {
width: 48px;
height: 48px;
background: $uni-color-primary-light;
border-radius: $uni-border-radius-circle;
background: rgba(0, 122, 255, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto $uni-spacing-col-base;
color: $uni-color-primary;
transition: $uni-transition;
margin: 0 auto 12px;
color: #007aff;
transition: all 0.3s ease;
.icon {
font-size: 24px;
}
// 支持u-icon组件
// 支持u-icon组件 - uview-plus图标
:deep(.u-icon) {
font-size: 24px !important;
color: inherit !important;
width: 24px;
height: 24px;
line-height: 1;
}
// 支持uview内置图标
:deep(.uview-icon) {
font-size: 24px !important;
color: inherit !important;
}
// 支持iconfont图标
.iconfont {
// 支持普通文本图标
.text-icon {
width: 24px;
height: 24px;
font-size: 24px;
color: inherit;
filter: brightness(0) saturate(100%) invert(49%) sepia(98%) saturate(3332%)
hue-rotate(196deg) brightness(104%) contrast(101%);
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.service-item:hover .service-icon {
background: $uni-color-primary;
color: $uni-text-color-inverse;
background: rgba(0, 122, 255, 0.2);
transform: scale(1.1);
.iconfont {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg)
brightness(100%) contrast(100%);
}
}
.service-title {
font-size: $uni-font-size-base;
font-size: 14px;
font-weight: 500;
color: $uni-text-color;
color: #333333;
display: block;
margin-top: 8px;
word-break: break-all;
line-height: 1.4;
}
.service-badge {
position: absolute;
top: -4px;
right: -4px;
background: #ff4757;
border-radius: $uni-border-radius-lg;
top: -6px;
right: -6px;
background: #ff3b30;
border-radius: 12px;
min-width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.badge-text {
color: $uni-text-color-inverse;
font-size: 11px;
font-weight: 500;
padding: 0 6px;
}
padding: 0 6px;
box-shadow: 0 2px 4px rgba(255, 59, 48, 0.3);
}
/* 网格布局支持 */
.grid-2 .service-item {
flex: 1;
}
.grid-3 .service-item {
flex: 1;
}
.grid-4 .service-item {
flex: 1;
}
/* 动画效果 */
.service-item {
animation: fadeInUp 0.6s ease-out backwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
.badge-text {
color: #ffffff;
font-size: 11px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
/* 响应式设计 */
@media (max-width: 375px) {
.service-item {
padding: $uni-spacing-col-lg;
padding: 20px 12px;
}
.service-icon {
width: 40px;
height: 40px;
margin-bottom: 8px;
.icon {
font-size: 20px;
}
// 响应式u-icon支持
.icon,
:deep(.u-icon) {
font-size: 20px !important;
}
// 响应式iconfont支持
.iconfont {
width: 20px;
height: 20px;
font-size: 20px;
}
}
.service-title {
font-size: $uni-font-size-sm;
font-size: 13px;
}
}
/* 小屏幕设备适配 */
@media (max-width: 320px) {
.service-item {
padding: 16px 8px;
}
.service-icon {
width: 36px;
height: 36px;
.icon,
:deep(.u-icon) {
font-size: 18px !important;
}
}
.service-title {
font-size: 12px;
margin-top: 6px;
}
}
/* 活跃状态动画 */
.service-item:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
/* 无障碍访问增强 */
@media (prefers-reduced-motion: reduce) {
.service-item,
.service-icon {
transition: none;
}
.service-item:hover {
transform: none;
}
}
</style>
-174
View File
@@ -1,174 +0,0 @@
<template>
<view class="stat-card touchable" @click="handleClick">
<text class="stat-number">{{ number }}</text>
<text class="stat-label">{{ label }}</text>
<view v-if="trend" class="stat-trend" :class="trendClass">
<text class="trend-icon">{{ trendIcon }}</text>
<text class="trend-text">{{ trend }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'StatCard',
props: {
number: {
type: [String, Number],
required: true,
},
label: {
type: String,
required: true,
},
trend: {
type: String,
default: '',
},
trendType: {
type: String,
default: 'neutral', // 'up', 'down', 'neutral'
validator: (value) => ['up', 'down', 'neutral'].includes(value),
},
},
computed: {
trendClass() {
return `trend-${this.trendType}`;
},
trendIcon() {
switch (this.trendType) {
case 'up':
return '↗️';
case 'down':
return '↘️';
default:
return '➖';
}
},
},
methods: {
handleClick() {
this.$emit('click', { number: this.number, label: this.label });
uni.vibrateShort();
},
},
};
</script>
<style lang="scss" scoped>
.stat-card {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
padding: $uni-spacing-col-xl;
text-align: center;
box-shadow: $uni-shadow;
transition: $uni-transition;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
&:hover {
transform: translateY(-2px);
box-shadow: $uni-shadow-hover;
}
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: $uni-color-primary;
margin-bottom: $uni-spacing-col-sm;
line-height: 1.2;
}
.stat-label {
font-size: $uni-font-size-sm;
color: $uni-text-color-secondary;
line-height: 1.4;
margin: 0;
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
margin-top: $uni-spacing-col-sm;
font-size: $uni-font-size-sm;
&.trend-up {
color: $uni-color-success;
}
&.trend-down {
color: $uni-color-error;
}
&.trend-neutral {
color: $uni-text-color-grey;
}
}
.trend-icon {
font-size: 12px;
}
.trend-text {
font-size: 11px;
font-weight: 500;
}
/* 动画效果 */
.stat-card {
animation: bounceIn 0.6s ease-out backwards;
}
@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);
}
}
/* 数字计数动画 */
.stat-number {
animation: countUp 1s ease-out;
}
@keyframes countUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.stat-card {
padding: $uni-spacing-col-lg;
}
.stat-number {
font-size: 20px;
}
.stat-label {
font-size: 11px;
}
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<template>
<view class="tree-node">
<view class="tree-item" :class="{ active: selectedId === node.id }" @click="handleSelect">
<view class="tree-item-content">
<view class="tree-item-left">
<u-icon
v-if="node.children && node.children.length > 0"
:name="expanded ? 'arrow-down' : 'arrow-right'"
size="16"
color="#666"
@click.stop="toggleExpand"
style="margin-right: 8px"
></u-icon>
<text class="tree-item-text">{{ node.label }}</text>
</view>
</view>
</view>
<!-- 子节点 -->
<view v-if="expanded && node.children && node.children.length > 0" class="tree-children">
<tree-node
v-for="child in node.children"
:key="child.id"
:node="child"
:selected-id="selectedId"
@select="handleChildSelect"
></tree-node>
</view>
</view>
</template>
<script>
export default {
name: 'TreeNode',
props: {
node: {
type: Object,
required: true,
},
selectedId: {
type: String,
default: '',
},
},
data() {
return {
expanded: false,
};
},
methods: {
handleSelect() {
this.$emit('select', this.node);
},
handleChildSelect(node) {
this.$emit('select', node);
},
toggleExpand() {
this.expanded = !this.expanded;
},
},
};
</script>
<style scoped>
.tree-node {
width: 100%;
}
.tree-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.tree-item:active {
background-color: #f5f5f5;
}
.tree-item.active {
background-color: #e6f7ff;
border-left: 3px solid #007aff;
}
.tree-item-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.tree-item-left {
display: flex;
align-items: center;
flex: 1;
}
.tree-item-text {
font-size: 14px;
color: #333;
flex: 1;
}
.tree-children {
padding-left: 20px;
background-color: #fafafa;
}
</style>
-32
View File
@@ -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,
};
}
+9
View File
@@ -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();
+57 -60
View File
@@ -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": ["<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": { /* ios打包配置 */
},
"sdkConfigs": { /* SDK配置 */
}
},
/* 模块配置 */
"modules" : {},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* 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
}
}
+18 -30
View File
@@ -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"
}
}
@@ -1,348 +0,0 @@
<template>
<view class="iconfont-demo">
<view class="demo-header">
<text class="demo-title">iconfont 图标库演示</text>
<text class="demo-subtitle">基于iconfont.cn的图标字体</text>
</view>
<view class="demo-content">
<!-- 基础图标 -->
<view class="icon-section">
<text class="section-title">基础图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-user"></text>
<text class="icon-name">用户</text>
<text class="icon-code">icon-user</text>
</view>
<view class="icon-item">
<text class="iconfont icon-lock"></text>
<text class="icon-name">锁定</text>
<text class="icon-code">icon-lock</text>
</view>
<view class="icon-item">
<text class="iconfont icon-eye"></text>
<text class="icon-name">眼睛</text>
<text class="icon-code">icon-eye</text>
</view>
<view class="icon-item">
<text class="iconfont icon-eye-close"></text>
<text class="icon-name">关闭眼睛</text>
<text class="icon-code">icon-eye-close</text>
</view>
</view>
</view>
<!-- 操作图标 -->
<view class="icon-section">
<text class="section-title">操作图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-add"></text>
<text class="icon-name">添加</text>
<text class="icon-code">icon-add</text>
</view>
<view class="icon-item">
<text class="iconfont icon-edit"></text>
<text class="icon-name">编辑</text>
<text class="icon-code">icon-edit</text>
</view>
<view class="icon-item">
<text class="iconfont icon-delete"></text>
<text class="icon-name">删除</text>
<text class="icon-code">icon-delete</text>
</view>
<view class="icon-item">
<text class="iconfont icon-search"></text>
<text class="icon-name">搜索</text>
<text class="icon-code">icon-search</text>
</view>
<view class="icon-item">
<text class="iconfont icon-refresh"></text>
<text class="icon-name">刷新</text>
<text class="icon-code">icon-refresh</text>
</view>
<view class="icon-item">
<text class="iconfont icon-close"></text>
<text class="icon-name">关闭</text>
<text class="icon-code">icon-close</text>
</view>
</view>
</view>
<!-- 文件图标 -->
<view class="icon-section">
<text class="section-title">文件图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-download"></text>
<text class="icon-name">下载</text>
<text class="icon-code">icon-download</text>
</view>
<view class="icon-item">
<text class="iconfont icon-upload"></text>
<text class="icon-name">上传</text>
<text class="icon-code">icon-upload</text>
</view>
</view>
</view>
<!-- 状态图标 -->
<view class="icon-section">
<text class="section-title">状态图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-success" style="color: #52c41a"></text>
<text class="icon-name">成功</text>
<text class="icon-code">icon-success</text>
</view>
<view class="icon-item">
<text class="iconfont icon-warning" style="color: #faad14"></text>
<text class="icon-name">警告</text>
<text class="icon-code">icon-warning</text>
</view>
<view class="icon-item">
<text class="iconfont icon-info" style="color: #1890ff"></text>
<text class="icon-name">信息</text>
<text class="icon-code">icon-info</text>
</view>
<view class="icon-item">
<text class="iconfont icon-check" style="color: #52c41a"></text>
<text class="icon-name">确认</text>
<text class="icon-code">icon-check</text>
</view>
</view>
</view>
<!-- 社交图标 -->
<view class="icon-section">
<text class="section-title">社交图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-star" style="color: #faad14"></text>
<text class="icon-name">星星</text>
<text class="icon-code">icon-star</text>
</view>
<view class="icon-item">
<text class="iconfont icon-heart" style="color: #ff4d4f"></text>
<text class="icon-name">喜欢</text>
<text class="icon-code">icon-heart</text>
</view>
<view class="icon-item">
<text class="iconfont icon-message"></text>
<text class="icon-name">消息</text>
<text class="icon-code">icon-message</text>
</view>
<view class="icon-item">
<text class="iconfont icon-phone"></text>
<text class="icon-name">电话</text>
<text class="icon-code">icon-phone</text>
</view>
<view class="icon-item">
<text class="iconfont icon-mail"></text>
<text class="icon-name">邮件</text>
<text class="icon-code">icon-mail</text>
</view>
</view>
</view>
<!-- 导航图标 -->
<view class="icon-section">
<text class="section-title">导航图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-home"></text>
<text class="icon-name">首页</text>
<text class="icon-code">icon-home</text>
</view>
<view class="icon-item">
<text class="iconfont icon-message"></text>
<text class="icon-name">消息</text>
<text class="icon-code">icon-message</text>
</view>
<view class="icon-item">
<text class="iconfont icon-setting"></text>
<text class="icon-name">设置</text>
<text class="icon-code">icon-setting</text>
</view>
<view class="icon-item">
<text class="iconfont icon-arrow-right"></text>
<text class="icon-name">箭头右</text>
<text class="icon-code">icon-arrow-right</text>
</view>
</view>
</view>
<!-- 联系图标 -->
<view class="icon-section">
<text class="section-title">联系图标</text>
<view class="icon-grid">
<view class="icon-item">
<text class="iconfont icon-phone"></text>
<text class="icon-name">电话</text>
<text class="icon-code">icon-phone</text>
</view>
<view class="icon-item">
<text class="iconfont icon-mail"></text>
<text class="icon-name">邮箱</text>
<text class="icon-code">icon-mail</text>
</view>
<view class="icon-item">
<text class="iconfont icon-check"></text>
<text class="icon-name">确认</text>
<text class="icon-code">icon-check</text>
</view>
</view>
</view>
<!-- 使用说明 -->
<view class="usage-section">
<text class="section-title">使用方法</text>
<view class="usage-code">
<text class="code-line">
&lt;text class="iconfont icon-user"&gt;&lt;/text&gt;
</text>
<text class="code-line">
&lt;text class="iconfont icon-lock"&gt;&lt;/text&gt;
</text>
<text class="code-line">/* 自定义样式 */</text>
<text class="code-line">.custom-icon {</text>
<text class="code-line">font-size: 24px;</text>
<text class="code-line">color: #5d87ff;</text>
<text class="code-line">}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'IconFontDemo',
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.iconfont-demo {
min-height: 100vh;
background: #f8f9fa;
padding: 20px;
}
.demo-header {
text-align: center;
margin-bottom: 32px;
}
.demo-title {
display: block;
font-size: 24px;
font-weight: bold;
color: #222;
margin-bottom: 8px;
}
.demo-subtitle {
display: block;
font-size: 14px;
color: #666;
}
.demo-content {
max-width: 800px;
margin: 0 auto;
}
.icon-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
display: block;
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid #5d87ff;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border: 1px solid #e9ecef;
border-radius: 8px;
background: #fafbfc;
transition: all 0.2s ease;
}
.icon-item:hover {
border-color: #5d87ff;
background: #f0f4ff;
}
.icon-item .iconfont {
width: 32px;
height: 32px;
margin-bottom: 8px;
filter: brightness(0) saturate(100%) invert(38%) sepia(87%) saturate(6400%) hue-rotate(225deg)
brightness(104%) contrast(104%);
}
.icon-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.icon-code {
font-size: 12px;
color: #666;
font-family: 'Consolas', 'Monaco', monospace;
background: #f1f3f4;
padding: 2px 6px;
border-radius: 4px;
}
.usage-section {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.usage-code {
background: #2d3748;
border-radius: 8px;
padding: 16px;
font-family: 'Consolas', 'Monaco', monospace;
}
.code-line {
display: block;
color: #e2e8f0;
font-size: 14px;
line-height: 1.6;
margin-bottom: 4px;
}
.code-line:last-child {
margin-bottom: 0;
}
</style>
-346
View File
@@ -1,346 +0,0 @@
<template>
<view class="test-page">
<NavBar title="路由守卫测试" />
<view class="content">
<view class="test-section">
<text class="section-title">🛡️ 路由守卫测试页面</text>
<text class="section-desc">如果你能看到这个页面,说明已经通过了登录验证</text>
</view>
<view class="test-info">
<view class="info-item">
<text class="info-label">当前时间:</text>
<text class="info-value">{{ currentTime }}</text>
</view>
<view class="info-item">
<text class="info-label">登录状态:</text>
<text class="info-value" :class="isLoggedIn ? 'status-ok' : 'status-error'">
{{ isLoggedIn ? '✅ 已登录' : '❌ 未登录' }}
</text>
</view>
<view class="info-item">
<text class="info-label">Token状态:</text>
<text class="info-value" :class="hasToken ? 'status-ok' : 'status-error'">
{{ hasToken ? '✅ 有效' : '❌ 无效' }}
</text>
</view>
<view class="info-item">
<text class="info-label">用户信息:</text>
<text class="info-value">{{ userName || '未获取' }}</text>
</view>
</view>
<view class="test-buttons">
<button class="test-btn" @click="testRedirectToLogin">测试跳转登录页</button>
<button class="test-btn" @click="testClearToken">清除Token测试</button>
<button class="test-btn" @click="testNavigateWithGuard">带守卫的页面跳转</button>
<button class="test-btn" @click="refreshPageInfo">刷新页面信息</button>
</view>
<view class="log-section">
<text class="section-title">📋 测试日志</text>
<scroll-view class="log-content" scroll-y>
<view
v-for="(log, index) in logs"
:key="index"
class="log-item"
:class="log.type"
>
<text class="log-time">{{ log.time }}</text>
<text class="log-text">{{ log.message }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import NavBar from '@/components/NavBar.vue';
import { userStorage } from '@/utils/request';
import { userManager, userState } from '@/utils/user';
import { checkPageAccess, guardedNavigate } from '@/utils/route-guard';
export default {
name: 'TestRoutePage',
components: {
NavBar,
},
data() {
return {
currentTime: '',
isLoggedIn: false,
hasToken: false,
userName: '',
logs: [],
};
},
onLoad() {
this.addLog('info', '页面加载开始');
// 检查页面访问权限
if (!checkPageAccess()) {
this.addLog('error', '页面访问权限检查失败,应该会自动跳转到登录页');
return;
}
this.addLog('success', '页面访问权限检查通过');
this.refreshPageInfo();
},
onShow() {
this.addLog('info', '页面显示事件触发');
// 页面显示时再次检查权限
if (!checkPageAccess()) {
this.addLog('error', '页面显示时权限检查失败');
return;
}
this.addLog('success', '页面显示时权限检查通过');
this.updateCurrentTime();
},
onUnload() {
this.addLog('info', '页面卸载');
},
methods: {
// 刷新页面信息
refreshPageInfo() {
this.updateCurrentTime();
this.checkLoginStatus();
this.checkTokenStatus();
this.loadUserInfo();
this.addLog('info', '页面信息已刷新');
},
// 更新当前时间
updateCurrentTime() {
this.currentTime = new Date().toLocaleString();
},
// 检查登录状态
checkLoginStatus() {
this.isLoggedIn = userStorage.isLoggedIn();
this.addLog('info', `登录状态检查: ${this.isLoggedIn ? '已登录' : '未登录'}`);
},
// 检查Token状态
checkTokenStatus() {
const token = userStorage.getToken();
this.hasToken = !!token;
const isExpired = userState.checkTokenExpiration();
this.addLog('info', `Token检查: ${this.hasToken ? '存在' : '不存在'}`);
if (this.hasToken) {
this.addLog('info', `Token过期检查: ${isExpired ? '已过期' : '未过期'}`);
}
},
// 加载用户信息
loadUserInfo() {
const user = userManager.getCurrentUser();
this.userName = user ? user.nickName || user.userName : '';
this.addLog('info', `用户信息: ${this.userName || '未获取到'}`);
},
// 测试跳转到登录页
testRedirectToLogin() {
this.addLog('warning', '手动跳转到登录页测试');
uni.reLaunch({
url: '/pages/login/index',
});
},
// 测试清除Token
testClearToken() {
this.addLog('warning', '清除Token进行测试');
userStorage.clearLoginInfo();
this.addLog('error', 'Token已清除,页面应该会自动跳转到登录页');
// 模拟页面重新检查权限
setTimeout(() => {
if (!checkPageAccess()) {
this.addLog('success', '权限检查失败,正在跳转登录页');
}
}, 1000);
},
// 测试带守卫的页面跳转
testNavigateWithGuard() {
this.addLog('info', '测试带权限检查的页面跳转');
const result = guardedNavigate('/pages/profile/index');
if (result) {
this.addLog('success', '跳转成功');
} else {
this.addLog('error', '跳转失败或被拦截');
}
},
// 添加日志
addLog(type, message) {
const log = {
type,
time: new Date().toLocaleTimeString(),
message,
};
this.logs.unshift(log);
// 限制日志数量
if (this.logs.length > 20) {
this.logs.pop();
}
console.log(`[${type.toUpperCase()}] ${message}`);
},
},
};
</script>
<style lang="scss" scoped>
.test-page {
min-height: 100vh;
background-color: $uni-bg-color-grey;
}
.content {
padding: 16px;
}
.test-section {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
padding: 20px;
margin-bottom: 16px;
text-align: center;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: $uni-text-color;
display: block;
margin-bottom: 8px;
}
.section-desc {
font-size: 14px;
color: $uni-text-color-grey;
line-height: 1.5;
}
.test-info {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
padding: 16px;
margin-bottom: 16px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 14px;
color: $uni-text-color-grey;
}
.info-value {
font-size: 14px;
color: $uni-text-color;
font-weight: 500;
&.status-ok {
color: #52c41a;
}
&.status-error {
color: #ff4d4f;
}
}
.test-buttons {
margin-bottom: 16px;
}
.test-btn {
width: 100%;
background: $uni-color-primary;
color: $uni-text-color-inverse;
border: none;
border-radius: $uni-border-radius-base;
padding: 12px;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
&:active {
background: $uni-color-primary-dark;
}
}
.log-section {
background: $uni-bg-color;
border-radius: $uni-border-radius-lg;
padding: 16px;
}
.log-content {
max-height: 300px;
margin-top: 12px;
}
.log-item {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: $uni-border-radius-base;
border-left: 3px solid;
&.info {
background: #f0f9ff;
border-left-color: #0ea5e9;
}
&.success {
background: #f0f9f4;
border-left-color: #22c55e;
}
&.warning {
background: #fffbeb;
border-left-color: #f59e0b;
}
&.error {
background: #fef2f2;
border-left-color: #ef4444;
}
}
.log-time {
font-size: 12px;
color: $uni-text-color-grey;
margin-right: 8px;
}
.log-text {
font-size: 13px;
color: $uni-text-color;
line-height: 1.4;
}
</style>
@@ -1,209 +0,0 @@
<template>
<view class="test-page">
<view class="test-header">
<text class="test-title">uview-plus 图标问题分析与解决</text>
</view>
<!-- 问题演示 -->
<view class="test-section">
<text class="section-title">❌ 问题演示 - uview-plus图标异常</text>
<view class="test-items">
<view class="test-item">
<u-icon name="logout" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">logout图标 (可能显示文字)</text>
</view>
<view class="test-item">
<u-icon name="power" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">power图标 (可能显示文字)</text>
</view>
<view class="test-item">
<u-icon name="exit" size="24" color="#ff4d4f"></u-icon>
<text class="test-desc">exit图标 (可能显示文字)</text>
</view>
</view>
</view>
<!-- 正常工作的uview图标 -->
<view class="test-section">
<text class="section-title">✅ 正常工作 - uview-plus图标</text>
<view class="test-items">
<view class="test-item">
<u-icon name="account" size="24" color="#52c41a"></u-icon>
<text class="test-desc">account图标 (正常)</text>
</view>
<view class="test-item">
<u-icon name="home" size="24" color="#52c41a"></u-icon>
<text class="test-desc">home图标 (正常)</text>
</view>
<view class="test-item">
<u-icon name="setting" size="24" color="#52c41a"></u-icon>
<text class="test-desc">setting图标 (正常)</text>
</view>
</view>
</view>
<!-- 解决方案 - 使用iconfont -->
<view class="test-section">
<text class="section-title">🎯 解决方案 - 使用iconfont自定义图标</text>
<view class="test-items">
<view class="test-item">
<text class="iconfont icon-close solution-icon"></text>
<text class="test-desc">关闭图标 (退出登录)</text>
</view>
<view class="test-item">
<text class="iconfont icon-user solution-icon"></text>
<text class="test-desc">用户图标</text>
</view>
<view class="test-item">
<text class="iconfont icon-setting solution-icon"></text>
<text class="test-desc">设置图标</text>
</view>
</view>
</view>
<!-- Unicode字符方案 -->
<view class="test-section">
<text class="section-title">🔤 备选方案 - Unicode字符</text>
<view class="test-items">
<view class="test-item">
<text class="unicode-icon">⏻</text>
<text class="test-desc">电源符号 (退出登录)</text>
</view>
<view class="test-item">
<text class="unicode-icon">❌</text>
<text class="test-desc">关闭符号</text>
</view>
<view class="test-item">
<text class="unicode-icon">⚙️</text>
<text class="test-desc">设置符号</text>
</view>
</view>
</view>
<!-- 解决方案说明 -->
<view class="solution-section">
<text class="section-title">💡 解决方案说明</text>
<view class="solution-content">
<text class="solution-text">
1. **根本原因**: uview-plus某些图标名称在当前版本字体文件中不存在
</text>
<text class="solution-text">
2. **推荐方案**: 使用项目中的iconfont自定义图标系统
</text>
<text class="solution-text">
3. **备选方案**: 使用Unicode字符(⏻ ❌ ⚙️),兼容性最好
</text>
<text class="solution-text">
4. **长期方案**: 更新uview-plus字体文件或使用其他图标库
</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UviewIconTest',
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.test-page {
min-height: 100vh;
background: #f8f9fa;
padding: 20px;
}
.test-header {
text-align: center;
margin-bottom: 32px;
}
.test-title {
display: block;
font-size: 20px;
font-weight: bold;
color: #333;
}
.test-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.test-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.test-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fafbfc;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.test-desc {
font-size: 14px;
color: #666;
}
.solution-icon {
font-size: 24px;
color: #1890ff;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.unicode-icon {
font-size: 24px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.solution-section {
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 12px;
padding: 20px;
}
.solution-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.solution-text {
display: block;
font-size: 14px;
color: #333;
line-height: 1.6;
}
</style>
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
+32 -40
View File
@@ -17,14 +17,19 @@
<view class="input-wrapper">
<u-input
v-model="username"
placeholder="账号"
placeholder="教工号/学号"
type="text"
border="true"
:custom-style="inputStyle"
placeholder-style="color: #999; font-size: 16px"
>
<template #prefix>
<text class="iconfont icon-user input-prefix-icon"></text>
<u-icon
name="account"
size="20"
color="#b0b8c9"
class="input-prefix-icon"
/>
</template>
</u-input>
</view>
@@ -40,7 +45,12 @@
@confirm="onPasswordConfirm"
>
<template #prefix>
<text class="iconfont icon-lock input-prefix-icon"></text>
<u-icon
name="lock"
size="20"
color="#b0b8c9"
class="input-prefix-icon"
/>
</template>
</u-input>
</view>
@@ -60,7 +70,7 @@
</view>
</u-button>
</view>
<!-- 测试按钮 -->
<!-- 测试按钮(可选) -->
<!-- <view style="margin-top: 16px; display: flex; flex-direction: column; gap: 8px">
<u-button text="测试加密功能" @click="testEncryption" type="info" size="small" />
<u-button text="测试Token存储" @click="testToken" type="warning" size="small" />
@@ -79,20 +89,20 @@
</template>
<script>
import { api, userStorage } from '@/utils/request';
import { runAllTests } from '@/utils/encrypt-test';
import { testTokenStorage, getCurrentTokenInfo, clearAllLoginInfo } from '@/utils/token-test';
import { api } from '@/common/request';
import { userStorage, tokenTest } from '@/common/storage';
import { validateKeyPair } from '@/common/crypto';
export default {
name: 'LoginPage',
data() {
return {
username: 'admin',
password: 'admin123',
username: '',
password: '',
loading: false,
tenantList: [],
tenantId: '000000',
// uview-plus 输入框样式
// uview-ui 输入框样式
inputStyle: {
height: '44px',
borderRadius: '8px',
@@ -102,7 +112,7 @@ export default {
color: '#222',
padding: '0 12px 0 12px',
},
// uview-plus 按钮样式
// uview-ui 按钮样式
buttonStyle: {
width: '100%',
height: '48px',
@@ -255,17 +265,6 @@ export default {
}
},
/**
* 表单输入处理
*/
onUsernameInput(value) {
this.username = value;
},
onPasswordInput(value) {
this.password = value;
},
/**
* 键盘回车事件
*/
@@ -281,7 +280,7 @@ export default {
try {
uni.showLoading({ title: '测试中...' });
const result = await runAllTests();
const result = validateKeyPair();
uni.hideLoading();
@@ -315,8 +314,8 @@ export default {
testToken() {
console.log('🧪 开始Token存储测试...');
const result = testTokenStorage();
getCurrentTokenInfo();
const result = tokenTest.testTokenStorage();
tokenTest.getCurrentTokenInfo();
uni.showToast({
title: result ? 'Token测试通过' : 'Token测试失败',
@@ -329,7 +328,7 @@ export default {
* 检查当前Token状态
*/
checkTokenStatus() {
const info = getCurrentTokenInfo();
const info = tokenTest.getCurrentTokenInfo();
const message = info
? `Token: ${info.hasToken ? '存在' : '无'} | 登录: ${info.isLoggedIn ? '是' : '否'}`
: 'Token检查失败';
@@ -345,7 +344,7 @@ export default {
* 清除登录信息
*/
clearLogin() {
clearAllLoginInfo();
tokenTest.clearAllLoginInfo();
uni.showToast({
title: '已清除登录信息',
icon: 'success',
@@ -369,12 +368,12 @@ export default {
align-items: center;
}
.login-card {
width: 90vw;
max-width: 350px;
width: 80vw;
max-width: 320px;
background: rgba(255, 255, 255, 0.85);
border-radius: 24px;
box-shadow: 0 8px 32px 0 rgba(93, 135, 255, 0.15);
padding: 36px 20px 24px 20px;
padding: 32px 24px 20px 24px;
display: flex;
flex-direction: column;
align-items: center;
@@ -418,25 +417,18 @@ export default {
gap: 16px;
}
/* uview-plus 输入框容器 */
/* uview-ui 输入框容器 */
.input-wrapper {
position: relative;
width: 100%;
}
/* uview-plus 输入框前缀图标 */
/* uview-ui 输入框前缀图标 */
.input-prefix-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 8px;
filter: brightness(0) saturate(100%) invert(67%) sepia(8%) saturate(1179%) hue-rotate(195deg)
brightness(95%) contrast(88%);
}
/* uview-plus 按钮内容布局 */
/* uview-ui 按钮内容布局 */
.login-btn-content {
display: flex;
align-items: center;
+865
View File
@@ -0,0 +1,865 @@
<template>
<view class="all-task-waiting-container">
<!-- 头部标题和操作 -->
<view class="header">
<text class="title">我的任务</text>
<view class="header-actions">
<u-icon name="search" size="20" color="#666" @click="toggleSearch"></u-icon>
<u-icon
name="account"
size="20"
color="#666"
style="margin-left: 16px"
@click="openUserSelect"
></u-icon>
</view>
</view>
<!-- 标签切换 -->
<view class="tabs-section">
<u-tabs
v-model="currentTab"
:list="tabList"
@change="changeTab"
active-color="#007aff"
inactive-color="#606266"
></u-tabs>
</view>
<!-- 搜索栏 -->
<view v-if="showSearch" class="search-section">
<view class="search-input">
<u-input
v-model="queryParams.nodeName"
placeholder="请输入任务名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-input">
<u-input
v-model="queryParams.flowName"
placeholder="请输入流程定义名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-actions">
<u-button type="primary" size="small" @click="handleQuery">搜索</u-button>
<u-button size="small" @click="resetQuery">重置</u-button>
</view>
</view>
<!-- 申请人筛选显示 -->
<view v-if="userSelectCount > 0" class="user-filter-section">
<view class="filter-info">
<text class="filter-text">已选择 {{ userSelectCount }} 位申请人</text>
<u-button type="error" size="mini" @click="clearUserFilter">清除</u-button>
</view>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<u-loading-icon></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 空数据 -->
<view v-else-if="taskList.length === 0" class="empty">
<view class="empty-content">
<u-icon name="file-text" size="80" color="#ddd"></u-icon>
<text class="empty-text">暂无数据</text>
</view>
</view>
<!-- 任务列表 -->
<view v-else class="task-list">
<view
v-for="item in taskList"
:key="item.id"
class="task-item"
@click="handleView(item)"
>
<!-- 任务信息 -->
<view class="task-header">
<view class="task-title-row">
<text class="task-name">{{ item.flowName }}</text>
<view
class="task-status"
:style="{
backgroundColor: getStatusStyle(item.flowStatus).color,
color: 'white',
}"
>
<text class="status-text">
{{ getStatusText(item.flowStatus) }}
</text>
</view>
</view>
<view class="task-info">
<text class="task-code">{{ item.flowCode }}</text>
<text class="task-category">{{ item.categoryName }}</text>
</view>
<text class="task-node-name">任务:{{ item.nodeName }}</text>
<text class="task-applicant">申请人:{{ item.createByName }}</text>
<!-- 办理人信息 -->
<view class="assignee-info">
<text class="assignee-label">办理人:</text>
<view v-if="currentTab === 0" class="assignee-tags">
<template v-if="item.assigneeNames">
<text
v-for="(name, index) in item.assigneeNames.split(',')"
:key="index"
class="assignee-tag"
>
{{ name }}
</text>
</template>
<text v-else class="assignee-tag no-assignee">无</text>
</view>
<text v-else class="assignee-name">{{ item.approveName || '无' }}</text>
</view>
<!-- 任务状态(已办时显示) -->
<view v-if="currentTab === 1" class="task-status-info">
<text class="task-status-label">任务状态:</text>
<text class="task-status-value">
{{ getTaskStatusText(item.flowTaskStatus) }}
</text>
</view>
<text class="task-time">{{ formatTime(item.createTime) }}</text>
</view>
<!-- 操作按钮 -->
<view class="task-actions" @click.stop>
<!-- 待办任务操作 -->
<view v-if="currentTab === 0" class="action-row">
<u-button type="primary" size="mini" @click="handleView(item)">
查看
</u-button>
<u-button
type="warning"
size="mini"
@click="handleMeddle(item)"
style="margin-left: 8px"
>
流程干预
</u-button>
</view>
<!-- 已办任务操作 -->
<view v-else class="action-row">
<u-button type="info" size="mini" @click="handleView(item)">
查看
</u-button>
</view>
</view>
</view>
</view>
<!-- 分页加载 -->
<view v-if="taskList.length > 0" class="pagination">
<u-loadmore v-if="hasMore" status="loading" @loadmore="loadMore">
<text>正在加载更多...</text>
</u-loadmore>
<view v-else class="load-end">
<text>已加载全部数据</text>
</view>
</view>
</view>
<!-- 申请人选择弹窗 -->
<u-popup v-model="showUserSelector" mode="center" width="80%" border-radius="12">
<view class="user-selector-popup">
<view class="popup-header">
<text class="popup-title">选择申请人</text>
<u-icon name="close" size="20" color="#666" @click="closeUserSelect"></u-icon>
</view>
<view class="popup-content">
<text class="popup-tip">请输入申请人姓名或工号</text>
<view class="search-user">
<u-input
v-model="userSearchKeyword"
placeholder="搜索申请人"
clearable
></u-input>
<u-button
type="primary"
size="small"
@click="searchUsers"
style="margin-top: 12px"
>
搜索
</u-button>
</view>
<view v-if="searchUsers.length > 0" class="user-results">
<view
v-for="user in searchUsers"
:key="user.userId"
class="user-item"
:class="{ selected: selectedUserIds.includes(user.userId) }"
@click="toggleUserSelect(user)"
>
<text class="user-name">{{ user.userName }}</text>
<text class="user-dept">{{ user.deptName }}</text>
</view>
</view>
</view>
<view class="popup-actions">
<u-button @click="closeUserSelect">取消</u-button>
<u-button type="primary" @click="confirmUserSelect">确定</u-button>
</view>
</view>
</u-popup>
<!-- 流程干预弹窗 -->
<u-popup v-model="showMeddlePopup" mode="center" width="80%" border-radius="12">
<view class="meddle-popup">
<view class="popup-header">
<text class="popup-title">流程干预</text>
<u-icon name="close" size="20" color="#666" @click="closeMeddlePopup"></u-icon>
</view>
<view class="popup-content">
<text class="popup-tip">选择干预类型</text>
<view class="meddle-options">
<u-button type="warning" @click="handleMeddleAction('delegate')">
委托办理
</u-button>
<u-button
type="error"
@click="handleMeddleAction('reject')"
style="margin-top: 12px"
>
驳回申请
</u-button>
<u-button
type="info"
@click="handleMeddleAction('suspend')"
style="margin-top: 12px"
>
挂起流程
</u-button>
</view>
</view>
<view class="popup-actions">
<u-button @click="closeMeddlePopup">取消</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { workflowApi, getStatusStyle, formatTime, routerJump } from '@/common/workflow-api.js';
export default {
data() {
return {
loading: false,
showSearch: false,
showUserSelector: false,
showMeddlePopup: false,
currentTab: 0,
tabList: [{ name: '待办任务' }, { name: '已办任务' }],
taskList: [],
total: 0,
hasMore: true,
// 用户筛选相关
userSelectCount: 0,
selectedUserIds: [],
userSearchKeyword: '',
searchUsers: [],
// 流程干预相关
currentMeddleTask: null,
queryParams: {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
createByIds: [],
},
};
},
onLoad() {
this.getWaitingList();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
onPullDownRefresh() {
this.refreshData();
},
methods: {
async getWaitingList() {
if (this.loading) return;
this.loading = true;
try {
const res = await workflowApi.pageByAllTaskWait(this.queryParams);
if (res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.taskList = res.rows || [];
} else {
this.taskList.push(...(res.rows || []));
}
this.total = res.total;
this.hasMore = this.taskList.length < this.total;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
console.error('加载待办任务失败:', e);
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
async getFinishList() {
if (this.loading) return;
this.loading = true;
try {
const res = await workflowApi.pageByAllTaskFinish(this.queryParams);
if (res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.taskList = res.rows || [];
} else {
this.taskList.push(...(res.rows || []));
}
this.total = res.total;
this.hasMore = this.taskList.length < this.total;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
console.error('加载已办任务失败:', e);
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
changeTab(index) {
this.currentTab = index;
this.queryParams.pageNum = 1;
this.hasMore = true;
this.taskList = [];
if (index === 0) {
this.getWaitingList();
} else {
this.getFinishList();
}
},
async loadMore() {
if (!this.hasMore || this.loading) return;
this.queryParams.pageNum++;
if (this.currentTab === 0) {
await this.getWaitingList();
} else {
await this.getFinishList();
}
},
async refreshData() {
this.queryParams.pageNum = 1;
this.hasMore = true;
if (this.currentTab === 0) {
await this.getWaitingList();
} else {
await this.getFinishList();
}
},
toggleSearch() {
this.showSearch = !this.showSearch;
},
handleQuery() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.refreshData();
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
createByIds: [],
};
this.userSelectCount = 0;
this.selectedUserIds = [];
this.hasMore = true;
this.refreshData();
},
handleView(item) {
this.routerJump({
businessId: item.businessId,
taskId: item.id,
type: 'view',
formCustom: item.formCustom,
formPath: item.formPath,
instanceId: item.instanceId,
});
},
handleMeddle(item) {
this.currentMeddleTask = item;
this.showMeddlePopup = true;
},
// 用户选择相关方法
openUserSelect() {
this.showUserSelector = true;
},
closeUserSelect() {
this.showUserSelector = false;
this.userSearchKeyword = '';
this.searchUsers = [];
},
async searchUsers() {
if (!this.userSearchKeyword.trim()) {
uni.showToast({ title: '请输入搜索关键词', icon: 'none' });
return;
}
// 这里应该调用用户搜索API,暂时模拟数据
this.searchUsers = [
{ userId: '1', userName: '张三', deptName: '技术部' },
{ userId: '2', userName: '李四', deptName: '财务部' },
{ userId: '3', userName: '王五', deptName: '人事部' },
];
},
toggleUserSelect(user) {
const index = this.selectedUserIds.findIndex((id) => id === user.userId);
if (index > -1) {
this.selectedUserIds.splice(index, 1);
} else {
this.selectedUserIds.push(user.userId);
}
},
confirmUserSelect() {
this.userSelectCount = this.selectedUserIds.length;
this.queryParams.createByIds = [...this.selectedUserIds];
this.closeUserSelect();
this.handleQuery();
},
clearUserFilter() {
this.userSelectCount = 0;
this.selectedUserIds = [];
this.queryParams.createByIds = [];
this.handleQuery();
},
// 流程干预相关方法
closeMeddlePopup() {
this.showMeddlePopup = false;
this.currentMeddleTask = null;
},
handleMeddleAction(action) {
const actionMap = {
delegate: '委托办理',
reject: '驳回申请',
suspend: '挂起流程',
};
uni.showToast({
title: `${actionMap[action]}功能开发中`,
icon: 'none',
});
this.closeMeddlePopup();
},
getStatusText(status) {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getTaskStatusText(status) {
const statusMap = {
agree: '同意',
reject: '驳回',
back: '退回',
delegate: '委托',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getStatusStyle(status) {
return getStatusStyle(status);
},
formatTime(timeStr) {
return formatTime(timeStr);
},
routerJump(params) {
routerJump(params);
},
},
};
</script>
<style scoped>
.all-task-waiting-container {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
.tabs-section {
background: #fff;
border-bottom: 1px solid #eee;
}
.search-section {
background: #fff;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.search-input {
margin-bottom: 12px;
}
.search-actions {
display: flex;
gap: 12px;
}
.user-filter-section {
background: #fff;
padding: 12px 20px;
border-bottom: 1px solid #eee;
}
.filter-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-text {
color: #007aff;
font-size: 14px;
}
.content {
flex: 1;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.loading-text {
margin-top: 8px;
color: #999;
font-size: 14px;
}
.empty {
padding: 40px 20px;
}
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 14px;
color: #999;
margin-top: 12px;
}
.task-list {
padding: 16px 20px;
}
.task-item {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s;
}
.task-item:active {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.task-header {
margin-bottom: 12px;
}
.task-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.task-name {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.task-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.task-info {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.task-code {
font-size: 13px;
color: #666;
}
.task-category {
font-size: 13px;
color: #666;
}
.task-node-name,
.task-applicant {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.assignee-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.assignee-label {
font-size: 13px;
color: #666;
margin-right: 4px;
}
.assignee-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.assignee-tag {
background: #e6f7ff;
color: #1890ff;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.assignee-tag.no-assignee {
background: #f5f5f5;
color: #999;
}
.assignee-name {
font-size: 13px;
color: #666;
}
.task-status-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.task-status-label {
font-size: 13px;
color: #666;
margin-right: 4px;
}
.task-status-value {
font-size: 13px;
color: #333;
font-weight: 500;
}
.task-time {
font-size: 12px;
color: #999;
}
.task-actions {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
}
.action-row {
display: flex;
gap: 8px;
}
.pagination {
padding: 16px 20px;
}
.load-end {
text-align: center;
padding: 16px;
}
.load-end text {
font-size: 14px;
color: #999;
}
/* 弹窗样式 */
.user-selector-popup,
.meddle-popup {
padding: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.popup-title {
font-size: 18px;
font-weight: 500;
color: #333;
}
.popup-content {
margin-bottom: 16px;
}
.popup-tip {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.search-user {
margin-bottom: 16px;
}
.user-results {
max-height: 200px;
overflow-y: auto;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.2s;
}
.user-item.selected {
border-color: #007aff;
background: #f0f8ff;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.user-dept {
font-size: 12px;
color: #666;
}
.meddle-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.popup-actions {
display: flex;
justify-content: space-between;
gap: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
}
</style>
+764
View File
@@ -0,0 +1,764 @@
<template>
<view class="my-document-container">
<!-- 头部标题和操作 -->
<view class="header">
<text class="title">我发起的流程</text>
<view class="header-actions">
<u-icon name="list" size="20" color="#666" @click="toggleCategory"></u-icon>
</view>
</view>
<!-- 搜索栏 -->
<view v-if="showSearch" class="search-section">
<view class="search-input">
<u-input
v-model="queryParams.flowCode"
placeholder="请输入流程定义编码"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-actions">
<u-button type="primary" size="small" @click="handleQuery">搜索</u-button>
<u-button size="small" @click="resetQuery">重置</u-button>
</view>
</view>
<!-- 当前选中的分类显示 -->
<view v-if="selectedCategory && selectedCategory.id" class="selected-category">
<text class="category-label">当前分类:{{ getCategoryPath(selectedCategory) }}</text>
<text class="category-clear" @click="clearCategory">清除</text>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<u-loading-icon></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 空数据 -->
<view v-else-if="processInstanceList.length === 0" class="empty">
<view class="empty-content">
<u-icon name="file-text" size="80" color="#ddd"></u-icon>
<text class="empty-text">暂无数据</text>
</view>
</view>
<!-- 流程列表 -->
<view v-else class="process-list">
<view
v-for="item in processInstanceList"
:key="item.id"
class="process-item"
@click="handleView(item)"
>
<!-- 流程信息 -->
<view class="process-header">
<view class="process-title-row">
<text class="process-name">{{ item.flowName }}</text>
<u-tag
:text="getStatusText(item.flowStatus)"
:type="getStatusType(item.flowStatus)"
plain
size="mini"
/>
</view>
<text class="process-time">{{ formatTime(item.createTime) }}</text>
</view>
<!-- 操作按钮 - 方案三:简洁卡片式 -->
<view class="process-actions-card" @click.stop>
<view class="action-card-content">
<view class="status-info">
<!-- 状态信息可以在这里添加 -->
</view>
<view class="quick-actions">
<view class="quick-btn view-btn" @click="handleView(item)">
<u-icon name="eye" size="18" color="#007aff"></u-icon>
</view>
<view
v-if="canEdit(item)"
class="quick-btn edit-btn"
@click="handleEdit(item)"
>
<u-icon name="edit-pen" size="18" color="#52c41a"></u-icon>
</view>
<view
v-if="item.flowStatus === 'waiting'"
class="quick-btn cancel-btn"
@click="handleCancel(item)"
>
<u-icon name="undo" size="18" color="#faad14"></u-icon>
</view>
<view
v-if="canDelete(item)"
class="quick-btn delete-btn"
@click="handleDelete(item)"
>
<u-icon name="close" size="18" color="#ff4d4f"></u-icon>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 分页加载 -->
<view v-if="processInstanceList.length > 0 && !loading" class="pagination">
<u-loadmore v-if="hasMore" status="loading" @loadmore="loadMore">
<text>正在加载更多...</text>
</u-loadmore>
<view v-else class="load-end">
<text>已加载全部数据</text>
</view>
</view>
</view>
<!-- 分类弹窗 -->
<u-popup
:show="showCategory"
mode="right"
width="380px"
:safe-area-inset-bottom="false"
:safe-area-inset-top="false"
@close="showCategory = false"
>
<view class="category-popup">
<!-- 弹窗头部 -->
<view class="popup-header">
<text class="popup-title">选择分类</text>
<u-icon
name="close"
size="24"
color="#666"
@click="showCategory = false"
></u-icon>
</view>
<!-- 分类树 -->
<scroll-view scroll-y class="category-tree">
<view class="tree-content">
<!-- 全部选项 -->
<view
class="tree-item"
:class="{ active: !selectedCategory || !selectedCategory.id }"
@click="selectCategory({ id: '', label: '全部' })"
>
<text class="tree-item-text">全部</text>
</view>
<!-- 如果没有数据,显示提示 -->
<view
v-if="categoryTree.length === 0"
style="padding: 20px; text-align: center"
>
<text style="color: #999">暂无分类数据</text>
</view>
<!-- 渲染树形结构 -->
<tree-node
v-for="item in categoryTree"
:key="item.id"
:node="item"
:selected-id="selectedCategory && selectedCategory.id"
@select="selectCategory"
></tree-node>
</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import { workflowApi, getStatusStyle, formatTime, routerJump } from '@/common/workflow-api.js';
import TreeNode from '@/components/TreeNode.vue';
export default {
components: {
TreeNode,
},
data() {
return {
loading: false,
showSearch: false,
showCategory: false,
processInstanceList: [],
categoryOptions: [],
categoryTree: [],
selectedCategory: null,
total: 0,
hasMore: true,
queryParams: {
pageNum: 1,
pageSize: 10,
flowCode: '',
category: '',
},
};
},
onLoad() {
this.initData();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
onPullDownRefresh() {
this.refreshData();
},
methods: {
async initData() {
await this.loadCategoryTree();
await this.getList();
},
async loadCategoryTree() {
try {
console.log('开始加载分类树...');
const res = await workflowApi.categoryTree();
console.log('API返回数据:', res);
if (res.code === 200) {
// 过滤掉"全部"选项,只保留真正的树形数据
this.categoryOptions = res.data || [];
this.categoryTree = (res.data || []).filter((item) => item.id !== '');
console.log('原始数据:', this.categoryOptions);
console.log('过滤后的树数据:', this.categoryTree);
console.log('树数据长度:', this.categoryTree.length);
} else {
console.error('API返回错误:', res.msg);
uni.showToast({
title: res.msg || '加载分类失败',
icon: 'error',
});
}
} catch (e) {
console.error('加载分类失败:', e);
uni.showToast({
title: '网络错误',
icon: 'error',
});
}
},
async getList() {
if (this.loading) return;
this.loading = true;
try {
const res = await workflowApi.pageByCurrent(this.queryParams);
if (res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.processInstanceList = res.rows || [];
} else {
this.processInstanceList.push(...(res.rows || []));
}
this.total = res.total;
this.hasMore = this.processInstanceList.length < this.total;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
console.error('加载数据失败:', e);
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
// 停止下拉刷新
uni.stopPullDownRefresh();
}
},
async loadMore() {
if (!this.hasMore || this.loading) return;
this.queryParams.pageNum++;
await this.getList();
},
async refreshData() {
this.queryParams.pageNum = 1;
this.hasMore = true;
await this.getList();
},
toggleSearch() {
this.showSearch = !this.showSearch;
},
toggleCategory() {
console.log('toggleCategory clicked, current showCategory:', this.showCategory);
this.showCategory = !this.showCategory;
console.log('toggleCategory after, showCategory:', this.showCategory);
},
selectCategory(category) {
this.selectedCategory = category;
this.queryParams.category = category.id;
this.showCategory = false; // 关闭弹窗
this.handleQuery();
},
clearCategory() {
this.selectedCategory = null;
this.queryParams.category = '';
this.handleQuery();
},
getCategoryPath(category) {
if (!category || !category.id) return '全部';
// 查找分类路径
const findPath = (nodes, targetId, path = []) => {
for (const node of nodes) {
const currentPath = [...path, node.label];
if (node.id === targetId) {
return currentPath;
}
if (node.children && node.children.length > 0) {
const childPath = findPath(node.children, targetId, currentPath);
if (childPath) return childPath;
}
}
return null;
};
const path = findPath(this.categoryTree, category.id);
return path ? path.join(' > ') : category.label;
},
handleQuery() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.getList();
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
flowCode: '',
category: '',
};
this.selectedCategory = null;
this.hasMore = true;
this.getList();
},
handleEdit(item) {
this.routerJump({
businessId: item.businessId,
taskId: item.id,
type: 'update',
formCustom: item.formCustom,
formPath: item.formPath,
});
},
handleView(item) {
this.routerJump({
businessId: item.businessId,
taskId: item.id,
type: 'view',
formCustom: item.formCustom,
formPath: item.formPath,
});
},
async handleDelete(item) {
try {
const res = await uni.showModal({
title: '确认删除',
content: '是否确认删除该流程?',
confirmText: '删除',
confirmColor: '#f56c6c',
});
if (res.confirm) {
this.loading = true;
const deleteRes = await workflowApi.deleteByInstanceIds([item.id]);
if (deleteRes.code === 200) {
uni.showToast({ title: '删除成功', icon: 'success' });
this.refreshData();
} else {
uni.showToast({ title: deleteRes.msg || '删除失败', icon: 'error' });
}
}
} catch (e) {
console.error('删除失败:', e);
uni.showToast({ title: '删除失败', icon: 'error' });
} finally {
this.loading = false;
}
},
async handleCancel(item) {
try {
const res = await uni.showModal({
title: '确认撤销',
content: '是否确认撤销该流程申请?',
confirmText: '撤销',
confirmColor: '#e6a23c',
});
if (res.confirm) {
this.loading = true;
const cancelData = {
businessId: item.businessId,
message: '申请人撤销流程!',
};
const cancelRes = await workflowApi.cancelProcessApply(cancelData);
if (cancelRes.code === 200) {
uni.showToast({ title: '撤销成功', icon: 'success' });
this.refreshData();
} else {
uni.showToast({ title: cancelRes.msg || '撤销失败', icon: 'error' });
}
}
} catch (e) {
console.error('撤销失败:', e);
uni.showToast({ title: '撤销失败', icon: 'error' });
} finally {
this.loading = false;
}
},
getStatusText(status) {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getStatusStyle(status) {
return getStatusStyle(status);
},
getStatusType(status) {
const typeMap = {
draft: 'info',
waiting: 'warning',
complete: 'success',
cancel: 'info',
back: 'error',
suspend: 'warning',
terminate: 'error',
};
return typeMap[status] || 'info';
},
formatTime(timeStr) {
return formatTime(timeStr);
},
routerJump(params) {
routerJump(params);
},
canEdit(item) {
return item.flowStatus === 'draft' || item.flowStatus === 'back';
},
canDelete(item) {
return (
item.flowStatus === 'draft' ||
item.flowStatus === 'cancel' ||
item.flowStatus === 'back'
);
},
},
};
</script>
<style scoped>
.my-document-container {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
.search-section {
background: #fff;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.search-input {
margin-bottom: 12px;
}
.search-actions {
display: flex;
gap: 12px;
}
.selected-category {
background: #fff;
padding: 12px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.category-label {
font-size: 14px;
color: #333;
flex: 1;
}
.category-clear {
font-size: 14px;
color: #007aff;
padding: 4px 8px;
}
.content {
flex: 1;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.loading-text {
margin-top: 8px;
color: #999;
font-size: 14px;
}
.empty {
padding: 40px 20px;
}
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 14px;
color: #999;
margin-top: 12px;
}
.process-list {
padding: 16px 20px;
}
.process-item {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s;
}
.process-item:active {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.process-header {
margin-bottom: 12px;
}
.process-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.process-name {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.process-time {
font-size: 12px;
color: #999;
}
/* 简洁卡片式样式 */
.process-actions-card {
border-radius: 12px;
}
.action-card-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.quick-actions {
display: flex;
gap: 8px;
align-items: center;
}
.quick-btn {
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.quick-btn:active {
transform: scale(0.9);
}
.quick-btn.view-btn {
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
border-color: #91d5ff;
}
.quick-btn.view-btn:active {
background: linear-gradient(135deg, #bae7ff 0%, #91d5ff 100%);
}
.quick-btn.edit-btn {
background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
border-color: #b7eb8f;
}
.quick-btn.edit-btn:active {
background: linear-gradient(135deg, #d9f7be 0%, #b7eb8f 100%);
}
.quick-btn.cancel-btn {
background: linear-gradient(135deg, #fffbe6 0%, #fff1b8 100%);
border-color: #ffe58f;
}
.quick-btn.cancel-btn:active {
background: linear-gradient(135deg, #fff1b8 0%, #ffd666 100%);
}
.quick-btn.delete-btn {
background: linear-gradient(135deg, #fff2f0 0%, #ffe2e0 100%);
border-color: #ffccc7;
}
.quick-btn.delete-btn:active {
background: linear-gradient(135deg, #ffe2e0 0%, #ffccc7 100%);
}
.pagination {
padding: 16px 20px;
}
.load-end {
text-align: center;
padding: 16px;
}
.load-end text {
font-size: 14px;
color: #999;
}
/* 分类弹窗样式 */
.category-popup {
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
}
.popup-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.popup-title {
font-size: 18px;
font-weight: 500;
color: #333;
}
.category-tree {
flex: 1;
height: 0;
}
.tree-content {
padding: 8px 0;
}
.tree-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.tree-item:active {
background-color: #f5f5f5;
}
.tree-item.active {
background-color: #e6f7ff;
border-left: 3px solid #007aff;
}
.tree-item-text {
font-size: 14px;
color: #333;
}
</style>
+434
View File
@@ -0,0 +1,434 @@
<template>
<view class="task-copy-container">
<!-- 头部标题和操作 -->
<view class="header">
<text class="title">我的抄送</text>
<view class="header-actions">
<u-icon name="search" size="20" color="#666" @click="toggleSearch"></u-icon>
</view>
</view>
<!-- 搜索栏 -->
<view v-if="showSearch" class="search-section">
<view class="search-input">
<u-input
v-model="queryParams.nodeName"
placeholder="请输入任务名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-input">
<u-input
v-model="queryParams.flowName"
placeholder="请输入流程定义名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-input">
<u-input
v-model="queryParams.flowCode"
placeholder="请输入流程定义编码"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-actions">
<u-button type="primary" size="small" @click="handleQuery">搜索</u-button>
<u-button size="small" @click="resetQuery">重置</u-button>
</view>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<u-loading-icon></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 空数据 -->
<view v-else-if="taskList.length === 0" class="empty">
<view class="empty-content">
<u-icon name="file-text" size="80" color="#ddd"></u-icon>
<text class="empty-text">暂无数据</text>
</view>
</view>
<!-- 任务列表 -->
<view v-else class="task-list">
<view
v-for="item in taskList"
:key="item.id"
class="task-item"
@click="handleView(item)"
>
<!-- 任务信息 -->
<view class="task-header">
<view class="task-title-row">
<text class="task-name">{{ item.flowName }}</text>
<view
class="task-status"
:style="{
backgroundColor: getStatusStyle(item.flowStatus).color,
color: 'white',
}"
>
<text class="status-text">
{{ getStatusText(item.flowStatus) }}
</text>
</view>
</view>
<view class="task-info">
<text class="task-code">{{ item.flowCode }}</text>
<text class="task-category">{{ item.categoryName }}</text>
</view>
<text class="task-version">版本:v{{ item.version }}.0</text>
<text class="task-node-name">任务:{{ item.nodeName }}</text>
<!-- 抄送标识 -->
<view class="copy-info">
<u-icon name="email" size="16" color="#ff9500"></u-icon>
<text class="copy-label">抄送任务</text>
</view>
<text class="task-time">{{ formatTime(item.createTime) }}</text>
</view>
<!-- 操作按钮 -->
<view class="task-actions" @click.stop>
<u-button type="info" size="mini" @click="handleView(item)">查看</u-button>
</view>
</view>
</view>
<!-- 分页加载 -->
<view v-if="taskList.length > 0" class="pagination">
<u-loadmore v-if="hasMore" status="loading" @loadmore="loadMore">
<text>正在加载更多...</text>
</u-loadmore>
<view v-else class="load-end">
<text>已加载全部数据</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { workflowApi, getStatusStyle, formatTime, routerJump } from '@/common/workflow-api.js';
export default {
data() {
return {
loading: false,
showSearch: false,
taskList: [],
total: 0,
hasMore: true,
queryParams: {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
},
};
},
onLoad() {
this.getTaskCopyList();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
onPullDownRefresh() {
this.refreshData();
},
methods: {
async getTaskCopyList() {
if (this.loading) return;
this.loading = true;
try {
const res = await workflowApi.pageByTaskCopy(this.queryParams);
if (res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.taskList = res.rows || [];
} else {
this.taskList.push(...(res.rows || []));
}
this.total = res.total;
this.hasMore = this.taskList.length < this.total;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
console.error('加载抄送任务失败:', e);
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
async loadMore() {
if (!this.hasMore || this.loading) return;
this.queryParams.pageNum++;
await this.getTaskCopyList();
},
async refreshData() {
this.queryParams.pageNum = 1;
this.hasMore = true;
await this.getTaskCopyList();
},
toggleSearch() {
this.showSearch = !this.showSearch;
},
handleQuery() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.refreshData();
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
};
this.hasMore = true;
this.refreshData();
},
handleView(item) {
this.routerJump({
businessId: item.businessId,
taskId: item.id,
type: 'view',
formCustom: item.formCustom,
formPath: item.formPath,
});
},
getStatusText(status) {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getStatusStyle(status) {
return getStatusStyle(status);
},
formatTime(timeStr) {
return formatTime(timeStr);
},
routerJump(params) {
routerJump(params);
},
},
};
</script>
<style scoped>
.task-copy-container {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
.search-section {
background: #fff;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.search-input {
margin-bottom: 12px;
}
.search-actions {
display: flex;
gap: 12px;
}
.content {
flex: 1;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.loading-text {
margin-top: 8px;
color: #999;
font-size: 14px;
}
.empty {
padding: 40px 20px;
}
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 14px;
color: #999;
margin-top: 12px;
}
.task-list {
padding: 16px 20px;
}
.task-item {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s;
/* 抄送任务特殊样式 */
border-left: 4px solid #ff9500;
}
.task-item:active {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.task-header {
margin-bottom: 12px;
}
.task-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.task-name {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.task-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.task-info {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.task-code {
font-size: 13px;
color: #666;
}
.task-category {
font-size: 13px;
color: #666;
}
.task-version,
.task-node-name {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.copy-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.copy-label {
font-size: 13px;
color: #ff9500;
font-weight: 500;
margin-left: 4px;
}
.task-time {
font-size: 12px;
color: #999;
}
.task-actions {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
}
.pagination {
padding: 16px 20px;
}
.load-end {
text-align: center;
padding: 16px;
}
.load-end text {
font-size: 14px;
color: #999;
}
</style>
+706
View File
@@ -0,0 +1,706 @@
<template>
<view class="task-finish-container">
<!-- 头部标题和操作 -->
<view class="header">
<text class="title">我的已办</text>
<view class="header-actions">
<u-icon name="search" size="20" color="#666" @click="toggleSearch"></u-icon>
<u-icon
name="account"
size="20"
color="#666"
style="margin-left: 16px"
@click="openUserSelect"
></u-icon>
</view>
</view>
<!-- 搜索栏 -->
<view v-if="showSearch" class="search-section">
<view class="search-input">
<u-input
v-model="queryParams.nodeName"
placeholder="请输入任务名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-input">
<u-input
v-model="queryParams.flowName"
placeholder="请输入流程定义名称"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-input">
<u-input
v-model="queryParams.flowCode"
placeholder="请输入流程定义编码"
border="bottom"
clearable
@confirm="handleQuery"
></u-input>
</view>
<view class="search-actions">
<u-button type="primary" size="small" @click="handleQuery">搜索</u-button>
<u-button size="small" @click="resetQuery">重置</u-button>
</view>
</view>
<!-- 申请人筛选显示 -->
<view v-if="userSelectCount > 0" class="user-filter-section">
<view class="filter-info">
<text class="filter-text">已选择 {{ userSelectCount }} 位申请人</text>
<u-button type="error" size="mini" @click="clearUserFilter">清除</u-button>
</view>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载状态 -->
<view v-if="loading" class="loading">
<u-loading-icon></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 空数据 -->
<view v-else-if="taskList.length === 0" class="empty">
<view class="empty-content">
<u-icon name="file-text" size="80" color="#ddd"></u-icon>
<text class="empty-text">暂无数据</text>
</view>
</view>
<!-- 任务列表 -->
<view v-else class="task-list">
<view
v-for="item in taskList"
:key="item.id"
class="task-item"
@click="handleView(item)"
>
<!-- 任务信息 -->
<view class="task-header">
<view class="task-title-row">
<text class="task-name">{{ item.flowName }}</text>
<view
class="task-status"
:style="{
backgroundColor: getStatusStyle(item.flowStatus).color,
color: 'white',
}"
>
<text class="status-text">
{{ getStatusText(item.flowStatus) }}
</text>
</view>
</view>
<view class="task-info">
<text class="task-code">{{ item.flowCode }}</text>
<text class="task-category">{{ item.categoryName }}</text>
</view>
<text class="task-version">版本:v{{ item.version }}.0</text>
<text class="task-node-name">任务:{{ item.nodeName }}</text>
<text class="task-applicant">申请人:{{ item.createByName }}</text>
<!-- 办理人信息 -->
<view class="assignee-info">
<text class="assignee-label">办理人:</text>
<text class="assignee-name">{{ item.approveName || '无' }}</text>
</view>
<!-- 任务状态 -->
<view class="task-status-info">
<text class="task-status-label">任务状态:</text>
<view
class="task-status-tag"
:style="{
backgroundColor: getTaskStatusStyle(item.flowTaskStatus).color,
color: 'white',
}"
>
<text class="task-status-value">
{{ getTaskStatusText(item.flowTaskStatus) }}
</text>
</view>
</view>
<text class="task-time">{{ formatTime(item.createTime) }}</text>
</view>
<!-- 操作按钮 -->
<view class="task-actions" @click.stop>
<u-button type="info" size="mini" @click="handleView(item)">查看</u-button>
</view>
</view>
</view>
<!-- 分页加载 -->
<view v-if="taskList.length > 0" class="pagination">
<u-loadmore v-if="hasMore" status="loading" @loadmore="loadMore">
<text>正在加载更多...</text>
</u-loadmore>
<view v-else class="load-end">
<text>已加载全部数据</text>
</view>
</view>
</view>
<!-- 申请人选择弹窗 -->
<u-popup v-model="showUserSelector" mode="center" width="80%" border-radius="12">
<view class="user-selector-popup">
<view class="popup-header">
<text class="popup-title">选择申请人</text>
<u-icon name="close" size="20" color="#666" @click="closeUserSelect"></u-icon>
</view>
<view class="popup-content">
<text class="popup-tip">请输入申请人姓名或工号</text>
<view class="search-user">
<u-input
v-model="userSearchKeyword"
placeholder="搜索申请人"
clearable
></u-input>
<u-button
type="primary"
size="small"
@click="searchUsers"
style="margin-top: 12px"
>
搜索
</u-button>
</view>
<view v-if="searchUsers.length > 0" class="user-results">
<view
v-for="user in searchUsers"
:key="user.userId"
class="user-item"
:class="{ selected: selectedUserIds.includes(user.userId) }"
@click="toggleUserSelect(user)"
>
<text class="user-name">{{ user.userName }}</text>
<text class="user-dept">{{ user.deptName }}</text>
</view>
</view>
</view>
<view class="popup-actions">
<u-button @click="closeUserSelect">取消</u-button>
<u-button type="primary" @click="confirmUserSelect">确定</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { workflowApi, getStatusStyle, formatTime, routerJump } from '@/common/workflow-api.js';
export default {
data() {
return {
loading: false,
showSearch: false,
showUserSelector: false,
taskList: [],
total: 0,
hasMore: true,
// 用户筛选相关
userSelectCount: 0,
selectedUserIds: [],
userSearchKeyword: '',
searchUsers: [],
queryParams: {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
createByIds: [],
},
};
},
onLoad() {
this.getFinishList();
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore();
}
},
onPullDownRefresh() {
this.refreshData();
},
methods: {
async getFinishList() {
if (this.loading) return;
this.loading = true;
try {
const res = await workflowApi.pageByTaskFinish(this.queryParams);
if (res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.taskList = res.rows || [];
} else {
this.taskList.push(...(res.rows || []));
}
this.total = res.total;
this.hasMore = this.taskList.length < this.total;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
console.error('加载已办任务失败:', e);
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
async loadMore() {
if (!this.hasMore || this.loading) return;
this.queryParams.pageNum++;
await this.getFinishList();
},
async refreshData() {
this.queryParams.pageNum = 1;
this.hasMore = true;
await this.getFinishList();
},
toggleSearch() {
this.showSearch = !this.showSearch;
},
handleQuery() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.refreshData();
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
nodeName: '',
flowName: '',
flowCode: '',
createByIds: [],
};
this.userSelectCount = 0;
this.selectedUserIds = [];
this.hasMore = true;
this.refreshData();
},
handleView(item) {
this.routerJump({
businessId: item.businessId,
taskId: item.id,
type: 'view',
formCustom: item.formCustom,
formPath: item.formPath,
});
},
// 用户选择相关方法
openUserSelect() {
this.showUserSelector = true;
},
closeUserSelect() {
this.showUserSelector = false;
this.userSearchKeyword = '';
this.searchUsers = [];
},
async searchUsers() {
if (!this.userSearchKeyword.trim()) {
uni.showToast({ title: '请输入搜索关键词', icon: 'none' });
return;
}
// 这里应该调用用户搜索API,暂时模拟数据
this.searchUsers = [
{ userId: '1', userName: '张三', deptName: '技术部' },
{ userId: '2', userName: '李四', deptName: '财务部' },
{ userId: '3', userName: '王五', deptName: '人事部' },
];
},
toggleUserSelect(user) {
const index = this.selectedUserIds.findIndex((id) => id === user.userId);
if (index > -1) {
this.selectedUserIds.splice(index, 1);
} else {
this.selectedUserIds.push(user.userId);
}
},
confirmUserSelect() {
this.userSelectCount = this.selectedUserIds.length;
this.queryParams.createByIds = [...this.selectedUserIds];
this.closeUserSelect();
this.handleQuery();
},
clearUserFilter() {
this.userSelectCount = 0;
this.selectedUserIds = [];
this.queryParams.createByIds = [];
this.handleQuery();
},
getStatusText(status) {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getTaskStatusText(status) {
const statusMap = {
agree: '同意',
reject: '驳回',
back: '退回',
delegate: '委托',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getTaskStatusStyle(status) {
const styleMap = {
agree: { color: '#67c23a' },
reject: { color: '#f56c6c' },
back: { color: '#409eff' },
delegate: { color: '#e6a23c' },
suspend: { color: '#909399' },
terminate: { color: '#f56c6c' },
};
return styleMap[status] || { color: '#909399' };
},
getStatusStyle(status) {
return getStatusStyle(status);
},
formatTime(timeStr) {
return formatTime(timeStr);
},
routerJump(params) {
routerJump(params);
},
},
};
</script>
<style scoped>
.task-finish-container {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
.search-section {
background: #fff;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.search-input {
margin-bottom: 12px;
}
.search-actions {
display: flex;
gap: 12px;
}
.user-filter-section {
background: #fff;
padding: 12px 20px;
border-bottom: 1px solid #eee;
}
.filter-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-text {
color: #007aff;
font-size: 14px;
}
.content {
flex: 1;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.loading-text {
margin-top: 8px;
color: #999;
font-size: 14px;
}
.empty {
padding: 40px 20px;
}
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 14px;
color: #999;
margin-top: 12px;
}
.task-list {
padding: 16px 20px;
}
.task-item {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s;
}
.task-item:active {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.task-header {
margin-bottom: 12px;
}
.task-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.task-name {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.task-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.task-info {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.task-code {
font-size: 13px;
color: #666;
}
.task-category {
font-size: 13px;
color: #666;
}
.task-version,
.task-node-name,
.task-applicant {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.assignee-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.assignee-label {
font-size: 13px;
color: #666;
margin-right: 4px;
}
.assignee-name {
font-size: 13px;
color: #333;
font-weight: 500;
}
.task-status-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.task-status-label {
font-size: 13px;
color: #666;
margin-right: 4px;
}
.task-status-tag {
padding: 2px 6px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
}
.task-time {
font-size: 12px;
color: #999;
}
.task-actions {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
}
.pagination {
padding: 16px 20px;
}
.load-end {
text-align: center;
padding: 16px;
}
.load-end text {
font-size: 14px;
color: #999;
}
/* 弹窗样式 */
.user-selector-popup {
padding: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.popup-title {
font-size: 18px;
font-weight: 500;
color: #333;
}
.popup-content {
margin-bottom: 16px;
}
.popup-tip {
font-size: 14px;
color: #666;
margin-bottom: 12px;
}
.search-user {
margin-bottom: 16px;
}
.user-results {
max-height: 200px;
overflow-y: auto;
}
.user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid #e6e6e6;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.2s;
}
.user-item.selected {
border-color: #007aff;
background: #f0f8ff;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.user-dept {
font-size: 12px;
color: #666;
}
.popup-actions {
display: flex;
justify-content: space-between;
gap: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
}
</style>
-167
View File
@@ -1,167 +0,0 @@
<template>
<view class="initiated-container">
<view class="header">
<text class="title">我发起的流程</text>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else-if="workflowList.length === 0" class="empty">
<text>暂无数据</text>
</view>
<view v-else class="workflow-list">
<view
v-for="item in workflowList"
:key="item.id"
class="workflow-item touchable"
@click="gotoDetail(item)"
>
<view class="workflow-header-info">
<view class="workflow-title-row">
<text class="workflow-title">{{ item.flowName }}</text>
<view class="workflow-status" :style="getStatusColor(item.flowStatus)">
<text class="status-text">{{ getStatusText(item.flowStatus) }}</text>
</view>
</view>
<text class="workflow-code">{{ item.flowCode }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { workflowApi, formatTime } from '@/utils/workflow-api';
export default {
data() {
return {
workflowList: [],
loading: true,
};
},
onLoad() {
this.loadWorkflowData();
},
methods: {
async loadWorkflowData() {
this.loading = true;
try {
const res = await workflowApi.pageByCurrent({ pageNum: 1, pageSize: 20 });
if (res.code === 200) {
this.workflowList = res.rows || [];
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
}
},
gotoDetail(item) {
const params = [
`businessId=${encodeURIComponent(item.businessId)}`,
`taskId=${encodeURIComponent(item.id)}`,
`type=view`,
`formCustom=${encodeURIComponent(item.formCustom || '')}`,
`formPath=${encodeURIComponent(item.formPath || '')}`,
].join('&');
uni.navigateTo({
url: `/pages/workflow/process?${params}`,
});
},
getStatusText(status) {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[status] || '未知';
},
getStatusColor(status) {
const colorMap = {
draft: 'background-color: #909399; color: white',
waiting: 'background-color: #e6a23c; color: white',
complete: 'background-color: #67c23a; color: white',
cancel: 'background-color: #f56c6c; color: white',
back: 'background-color: #409eff; color: white',
suspend: 'background-color: #909399; color: white',
terminate: 'background-color: #f56c6c; color: white',
};
return colorMap[status] || colorMap.draft;
},
formatTime,
},
};
</script>
<style scoped>
.initiated-container {
padding: 20px;
background: #fff;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
}
.loading {
text-align: center;
color: #999;
margin-top: 40px;
}
.empty {
text-align: center;
color: #bbb;
margin-top: 40px;
}
.workflow-list {
margin-top: 12px;
}
.workflow-item {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
transition: box-shadow 0.2s;
}
.workflow-item:active {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
.workflow-header-info {
margin-bottom: 8px;
}
.workflow-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
}
.workflow-title {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.workflow-status {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.workflow-code {
font-size: 12px;
color: #888;
}
</style>
-117
View File
@@ -1,117 +0,0 @@
<template>
<view class="process-container">
<view class="header">
<text class="title">流程详情</text>
<text v-if="isViewMode" class="view-mode">(只读模式)</text>
</view>
<view v-if="loading" class="loading">加载中...</view>
<view v-else>
<u-form :disabled="isViewMode">
<u-form-item label="流程名称">
<u-input v-model="processDetail.flowName" disabled />
</u-form-item>
<u-form-item label="流程编码">
<u-input v-model="processDetail.flowCode" disabled />
</u-form-item>
<u-form-item label="分类">
<u-input v-model="processDetail.categoryName" disabled />
</u-form-item>
<u-form-item label="状态">
<u-input :value="processDetail.flowStatusName || statusText" disabled />
</u-form-item>
<u-form-item label="创建时间">
<u-input :value="formatTime(processDetail.createTime)" disabled />
</u-form-item>
<!-- 可扩展更多字段 -->
</u-form>
<view v-if="!isViewMode" class="actions">
<u-button type="primary" @click="handleSubmit">提交</u-button>
</view>
</view>
</view>
</template>
<script>
import { workflowApi, formatTime, getStatusStyle } from '@/utils/workflow-api';
export default {
data() {
return {
processDetail: {},
loading: true,
isViewMode: false,
};
},
computed: {
statusText() {
const statusMap = {
draft: '草稿',
waiting: '待审批',
complete: '已完成',
cancel: '已取消',
back: '已退回',
suspend: '挂起',
terminate: '终止',
};
return statusMap[this.processDetail.flowStatus] || '未知';
},
},
onLoad(options) {
this.isViewMode = options.type === 'view';
this.loadDetail(options);
},
methods: {
async loadDetail(options) {
this.loading = true;
try {
// 使用新接口获取详情
const res = await workflowApi.processDetail(options.businessId);
if (res.code === 200 && res.data) {
this.processDetail = res.data;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' });
}
} catch (e) {
uni.showToast({ title: '网络错误', icon: 'error' });
} finally {
this.loading = false;
}
},
formatTime,
handleSubmit() {
uni.showToast({ title: '提交功能开发中', icon: 'none' });
},
},
};
</script>
<style scoped>
.process-container {
padding: 20px;
background: #fff;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
}
.view-mode {
color: #909399;
font-size: 14px;
}
.loading {
text-align: center;
color: #999;
margin-top: 40px;
}
.actions {
margin-top: 32px;
display: flex;
justify-content: flex-end;
}
</style>
-6
View File
@@ -1,6 +0,0 @@
export {};
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}
Binary file not shown.
+12 -240
View File
@@ -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");
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="rgb(93, 135, 255)"/>
</svg>

Before

Width:  |  Height:  |  Size: 184 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="#999999"/>
</svg>

Before

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

+10
View File
@@ -0,0 +1,10 @@
declare module 'uview-ui' {
import { VueConstructor } from 'vue'
interface UViewUIInstall {
install: (vue: VueConstructor) => void
}
const uView: UViewUIInstall
export default uView
}
+10
View File
@@ -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]));
});
},
});
+34 -265
View File
@@ -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);
}
}
-378
View File
@@ -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 端功能对齐
- 支持入参加密、出参解密、防重复提交等完整功能
- 新增配置管理系统,提升可维护性
-774
View File
@@ -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 };
-422
View File
@@ -1,422 +0,0 @@
/**
* 动画工具类
* 提供页面转场、组件动画和微交互效果的统一管理
*/
class AnimationUtils {
constructor() {
this.animationDuration = 300;
this.easingFunction = 'cubic-bezier(0.4, 0, 0.2, 1)';
}
/**
* 页面进入动画
* @param {string} selector - 选择器
* @param {string} type - 动画类型
*/
pageEnter(selector = '.page-container', type = 'fadeInUp') {
const element = document.querySelector(selector);
if (element) {
element.style.animation = `${type} 0.6s ease-out`;
}
}
/**
* 页面退出动画
* @param {string} selector - 选择器
* @param {Function} callback - 回调函数
*/
pageExit(selector = '.page-container', callback) {
const element = document.querySelector(selector);
if (element) {
element.style.animation = 'fadeOutDown 0.3s ease-in forwards';
setTimeout(() => {
if (callback) callback();
}, 300);
}
}
/**
* 卡片进入动画(错开显示)
* @param {string} selector - 选择器
*/
staggerCards(selector = '.card, .service-item, .stat-card') {
const elements = document.querySelectorAll(selector);
elements.forEach((element, index) => {
element.style.animationDelay = `${index * 0.1}s`;
element.style.animation = 'slideInUp 0.6s ease-out backwards';
});
}
/**
* 列表项动画
* @param {string} selector - 选择器
*/
animateList(selector = '.menu-item, .todo-item, .news-item') {
const elements = document.querySelectorAll(selector);
elements.forEach((element, index) => {
element.style.animationDelay = `${index * 0.05}s`;
element.style.animation = 'slideInRight 0.4s ease-out backwards';
});
}
/**
* 点击反馈动画
* @param {HTMLElement} element - 元素
*/
clickFeedback(element) {
if (!element) return;
element.style.transform = 'scale(0.95)';
element.style.transition = 'transform 0.1s ease-out';
setTimeout(() => {
element.style.transform = 'scale(1)';
}, 100);
}
/**
* 加载动画
* @param {string} selector - 选择器
*/
showLoading(selector = 'body') {
const loadingHtml = `
<div class="loading-overlay" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.3s ease-out;
">
<div class="spinner" style="
width: 40px;
height: 40px;
border: 3px solid #f0f0f0;
border-top: 3px solid rgb(93, 135, 255);
border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
</div>
`;
const container = document.querySelector(selector);
if (container) {
container.insertAdjacentHTML('beforeend', loadingHtml);
}
}
/**
* 隐藏加载动画
*/
hideLoading() {
const loading = document.querySelector('.loading-overlay');
if (loading) {
loading.style.animation = 'fadeOut 0.3s ease-in forwards';
setTimeout(() => {
loading.remove();
}, 300);
}
}
/**
* Toast 提示动画
* @param {string} message - 消息内容
* @param {number} duration - 持续时间
*/
showToast(message, duration = 2000) {
// 移除已存在的toast
const existingToast = document.querySelector('.animated-toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = 'animated-toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 25px;
z-index: 10000;
font-size: 14px;
max-width: 280px;
text-align: center;
animation: slideInDown 0.3s ease-out;
`;
document.body.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.animation = 'slideOutUp 0.3s ease-in forwards';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
}, duration);
}
/**
* 统计数字计数动画
* @param {HTMLElement} element - 元素
* @param {number} endValue - 结束值
* @param {number} duration - 持续时间
*/
countUp(element, endValue, duration = 1000) {
if (!element) return;
const startValue = 0;
const startTime = Date.now();
const animate = () => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用缓动函数
const easeProgress = 1 - Math.pow(1 - progress, 3);
const currentValue = startValue + (endValue - startValue) * easeProgress;
// 格式化显示
if (endValue % 1 === 0) {
element.textContent = Math.round(currentValue);
} else {
element.textContent = currentValue.toFixed(1);
}
if (progress < 1) {
requestAnimationFrame(animate);
}
};
animate();
}
/**
* 下拉刷新动画
* @param {HTMLElement} element - 元素
*/
pullToRefresh(element) {
if (!element) return;
element.style.transform = 'translateY(50px)';
element.style.transition = 'transform 0.3s ease-out';
setTimeout(() => {
element.style.transform = 'translateY(0)';
}, 100);
}
/**
* 搜索聚焦动画
* @param {HTMLElement} searchBar - 搜索栏元素
*/
searchFocus(searchBar) {
if (!searchBar) return;
searchBar.style.transform = 'translateY(-2px)';
searchBar.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.15)';
searchBar.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
}
/**
* 搜索失焦动画
* @param {HTMLElement} searchBar - 搜索栏元素
*/
searchBlur(searchBar) {
if (!searchBar) return;
searchBar.style.transform = 'translateY(0)';
searchBar.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.1)';
}
/**
* 模态框显示动画
* @param {HTMLElement} modal - 模态框元素
*/
showModal(modal) {
if (!modal) return;
modal.style.display = 'flex';
modal.style.animation = 'fadeIn 0.3s ease-out';
const content = modal.querySelector('.modal-content');
if (content) {
content.style.animation = 'slideInUp 0.3s ease-out';
}
}
/**
* 模态框隐藏动画
* @param {HTMLElement} modal - 模态框元素
* @param {Function} callback - 回调函数
*/
hideModal(modal, callback) {
if (!modal) return;
modal.style.animation = 'fadeOut 0.3s ease-in forwards';
const content = modal.querySelector('.modal-content');
if (content) {
content.style.animation = 'slideOutDown 0.3s ease-in forwards';
}
setTimeout(() => {
modal.style.display = 'none';
if (callback) callback();
}, 300);
}
}
// 创建全局实例
const animationUtils = new AnimationUtils();
// 定义CSS动画关键帧
const animationStyles = `
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOutDown {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideOutDown {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(30px);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideInDown {
from {
opacity: 0;
transform: translate(-50%, -100%);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
@keyframes slideOutUp {
from {
opacity: 1;
transform: translate(-50%, 0);
}
to {
opacity: 0;
transform: translate(-50%, -100%);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
`;
// 插入动画样式到页面
if (typeof document !== 'undefined') {
const styleSheet = document.createElement('style');
styleSheet.textContent = animationStyles;
document.head.appendChild(styleSheet);
}
export default animationUtils;
export { AnimationUtils };
-353
View File
@@ -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);
-204
View File
@@ -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
},
}
-158
View File
@@ -1,158 +0,0 @@
// @ts-nocheck
import CryptoJS from 'crypto-js';
// 配置信息
const ENCRYPT_CONFIG = {
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
RSA_PUBLIC_KEY:
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
TENANT_ID: '000000',
};
/**
* 生成随机AES密钥
* @param {number} length 密钥长度,默认32位
* @returns {string} AES密钥
*/
export function generateRandomAESKey(length = 32) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* AES加密数据
* @param {string} data 待加密的数据
* @param {string} key AES密钥
* @returns {string} 加密后的数据
*/
export function encryptWithAES(data, key) {
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
/**
* RSA加密AES密钥 - 浏览器兼容版本
* @param {string} aesKey AES密钥
* @returns {Promise<string|null>} RSA加密后的AES密钥
*/
export async function encryptAESKeyWithRSA(aesKey) {
try {
// 将Base64公钥转换为ArrayBuffer
const publicKeyData = Uint8Array.from(atob(ENCRYPT_CONFIG.RSA_PUBLIC_KEY), (c) =>
c.charCodeAt(0),
);
// 导入RSA公钥
const publicKey = await crypto.subtle.importKey(
'spki',
publicKeyData,
{
name: 'RSA-OAEP',
hash: 'SHA-1',
},
false,
['encrypt'],
);
// 将AES密钥转为Base64
const aesKeyBase64 = btoa(aesKey);
const encoder = new TextEncoder();
const data = encoder.encode(aesKeyBase64);
// RSA加密
const encrypted = await crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
publicKey,
data,
);
// 转换为Base64
const encryptedArray = new Uint8Array(encrypted);
return btoa(String.fromCharCode.apply(null, encryptedArray));
} catch (error) {
console.error('RSA加密失败:', error.message);
console.log('🔄 回退到简化加密方案...');
// 回退方案:使用AES密钥的Base64编码(临时方案)
try {
return btoa(aesKey);
} catch (fallbackError) {
console.error('回退加密也失败:', fallbackError.message);
return null;
}
}
}
/**
* 登录数据加密处理
* @param {Object} loginData 登录数据对象
* @returns {Promise<Object>} 包含加密数据和请求头的对象
*/
export async function encryptLoginData(loginData) {
try {
// 1. 生成AES密钥
const aesKey = generateRandomAESKey(32);
console.log('🔑 生成AES密钥:', aesKey);
// 2. 准备完整的登录数据
const fullLoginData = {
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
username: loginData.username,
password: loginData.password,
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
grantType: loginData.grantType || 'password',
};
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
// 3. AES加密登录数据
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
console.log('🔐 AES加密后数据长度:', encryptedData.length);
// 4. RSA加密AES密钥
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
if (!encryptedKey) {
throw new Error('RSA加密AES密钥失败');
}
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
// 5. 返回加密结果和请求头
return {
success: true,
encryptedData: encryptedData,
headers: {
'Content-Type': 'application/json;charset=utf-8',
clientid: ENCRYPT_CONFIG.CLIENT_ID,
'encrypt-key': encryptedKey,
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
isToken: 'false',
isEncrypt: 'true',
repeatSubmit: 'false',
},
};
} catch (error) {
console.error('登录数据加密失败:', error);
return {
success: false,
error: error.message,
};
}
}
/**
* 获取加密配置
*/
export function getEncryptConfig() {
return ENCRYPT_CONFIG;
}
-151
View File
@@ -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;
}
-283
View File
@@ -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;
}
-97
View File
@@ -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;
}
}
-199
View File
@@ -1,199 +0,0 @@
// @ts-nocheck
import CryptoJS from 'crypto-js';
// 配置信息
const ENCRYPT_CONFIG = {
// RSA公钥 - 与后端保持一致 (更新为正确版本,包含ijT7h)
RSA_PUBLIC_KEY:
'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==',
CLIENT_ID: 'e5cd7e4891bf95d1d19206ce24a7b32e',
TENANT_ID: '000000',
};
/**
* 生成随机AES密钥
* @param {number} length 密钥长度,默认32位
* @returns {string} AES密钥
*/
export function generateRandomAESKey(length = 32) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* AES加密数据
* @param {string} data 待加密的数据
* @param {string} key AES密钥
* @returns {string} 加密后的数据
*/
export function encryptWithAES(data, key) {
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
/**
* Base64字符串转ArrayBuffer
* @param {string} base64
* @returns {ArrayBuffer}
*/
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
/**
* ArrayBuffer转Base64字符串
* @param {ArrayBuffer} buffer
* @returns {string}
*/
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* 导入RSA公钥
* @param {string} publicKeyBase64 Base64格式的公钥
* @returns {Promise<CryptoKey>}
*/
async function importRSAPublicKey(publicKeyBase64) {
try {
const keyBuffer = base64ToArrayBuffer(publicKeyBase64);
const key = await crypto.subtle.importKey(
'spki',
keyBuffer,
{
name: 'RSA-OAEP',
hash: 'SHA-1',
},
false,
['encrypt'],
);
return key;
} catch (error) {
console.error('导入RSA公钥失败:', error);
throw error;
}
}
/**
* RSA加密AES密钥 - 使用Web Crypto API
* @param {string} aesKey AES密钥
* @returns {Promise<string|null>} RSA加密后的AES密钥
*/
export async function encryptAESKeyWithRSA(aesKey) {
try {
// 检查Web Crypto API支持
if (!crypto || !crypto.subtle) {
throw new Error('当前环境不支持Web Crypto API');
}
// 导入RSA公钥
const publicKey = await importRSAPublicKey(ENCRYPT_CONFIG.RSA_PUBLIC_KEY);
// 将AES密钥转为Base64(与custom-login-script.js保持一致)
const aesKeyBase64 = btoa(unescape(encodeURIComponent(aesKey)));
// 将字符串转为ArrayBuffer
const dataBuffer = new TextEncoder().encode(aesKeyBase64);
// RSA加密
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
publicKey,
dataBuffer,
);
// 转为Base64字符串
const encryptedBase64 = arrayBufferToBase64(encryptedBuffer);
console.log('✅ Web Crypto RSA加密成功,结果长度:', encryptedBase64.length);
return encryptedBase64;
} catch (error) {
console.error('Web Crypto RSA加密失败:', error.message);
return null;
}
}
/**
* 登录数据加密处理
* @param {Object} loginData 登录数据对象
* @returns {Promise<Object>} 包含加密数据和请求头的对象
*/
export async function encryptLoginData(loginData) {
try {
// 1. 生成AES密钥
const aesKey = generateRandomAESKey(32);
console.log('🔑 生成AES密钥:', aesKey);
// 2. 准备完整的登录数据
const fullLoginData = {
tenantId: loginData.tenantId || ENCRYPT_CONFIG.TENANT_ID,
username: loginData.username,
password: loginData.password,
clientId: loginData.clientId || ENCRYPT_CONFIG.CLIENT_ID,
grantType: loginData.grantType || 'password',
};
console.log('📋 登录数据:', JSON.stringify(fullLoginData, null, 2));
// 3. AES加密登录数据
const encryptedData = encryptWithAES(JSON.stringify(fullLoginData), aesKey);
console.log('🔐 AES加密后数据长度:', encryptedData.length);
// 4. RSA加密AES密钥 - 异步操作
const encryptedKey = await encryptAESKeyWithRSA(aesKey);
if (!encryptedKey) {
throw new Error('RSA加密AES密钥失败');
}
console.log('🔑 RSA加密后密钥长度:', encryptedKey.length);
// 5. 返回加密结果和请求头
return {
success: true,
encryptedData: encryptedData,
headers: {
'Content-Type': 'application/json;charset=utf-8',
clientid: ENCRYPT_CONFIG.CLIENT_ID,
'encrypt-key': encryptedKey,
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
isToken: 'false',
isEncrypt: 'true',
repeatSubmit: 'false',
},
};
} catch (error) {
console.error('登录数据加密失败:', error);
return {
success: false,
error: error.message,
};
}
}
/**
* 获取加密配置
*/
export function getEncryptConfig() {
return ENCRYPT_CONFIG;
}
-126
View File
@@ -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;
}
-509
View File
@@ -1,509 +0,0 @@
/**
* 手势识别工具类
* 支持滑动、长按、双击、捏合缩放等手势操作
*/
class GestureUtils {
constructor() {
this.touchStartTime = 0;
this.touchStartPos = { x: 0, y: 0 };
this.touchMovePos = { x: 0, y: 0 };
this.lastTouchTime = 0;
this.tapCount = 0;
this.longPressTimer = null;
this.longPressDelay = 500; // 长按延迟时间
this.doubleTapDelay = 300; // 双击延迟时间
this.swipeThreshold = 50; // 滑动阈值
this.gestureCallbacks = new Map();
}
/**
* 初始化手势监听
* @param {HTMLElement} element - 监听元素
* @param {Object} options - 配置选项
*/
init(element, options = {}) {
if (!element) return;
const config = {
enableSwipe: true,
enableLongPress: true,
enableDoubleTap: true,
enablePinch: false,
...options,
};
// 存储元素和配置
this.element = element;
this.config = config;
// 绑定事件监听器
this.bindEvents();
}
/**
* 绑定触摸事件
*/
bindEvents() {
if (!this.element) return;
// 阻止默认的触摸行为
this.element.style.touchAction = 'manipulation';
// 绑定事件
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), {
passive: false,
});
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), {
passive: false,
});
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), {
passive: false,
});
this.element.addEventListener('touchcancel', this.handleTouchCancel.bind(this), {
passive: false,
});
// 绑定鼠标事件(用于桌面端测试)
this.element.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.element.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.element.addEventListener('mouseup', this.handleMouseUp.bind(this));
}
/**
* 注册手势回调
* @param {string} gesture - 手势类型
* @param {Function} callback - 回调函数
*/
on(gesture, callback) {
if (!this.gestureCallbacks.has(gesture)) {
this.gestureCallbacks.set(gesture, []);
}
this.gestureCallbacks.get(gesture).push(callback);
}
/**
* 触发手势回调
* @param {string} gesture - 手势类型
* @param {Object} data - 手势数据
*/
trigger(gesture, data = {}) {
const callbacks = this.gestureCallbacks.get(gesture);
if (callbacks) {
callbacks.forEach((callback) => callback(data));
}
}
/**
* 处理触摸开始
* @param {TouchEvent} e - 触摸事件
*/
handleTouchStart(e) {
const touch = e.touches[0];
this.touchStartTime = Date.now();
this.touchStartPos = { x: touch.clientX, y: touch.clientY };
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
// 长按检测
if (this.config.enableLongPress) {
this.longPressTimer = setTimeout(() => {
this.trigger('longpress', {
position: this.touchStartPos,
target: e.target,
});
// 触觉反馈
if (uni && uni.vibrateShort) {
uni.vibrateShort();
}
}, this.longPressDelay);
}
// 双击检测
if (this.config.enableDoubleTap) {
const currentTime = Date.now();
if (currentTime - this.lastTouchTime < this.doubleTapDelay) {
this.tapCount++;
} else {
this.tapCount = 1;
}
this.lastTouchTime = currentTime;
}
// 阻止默认行为(可选)
if (this.config.preventDefault !== false) {
e.preventDefault();
}
}
/**
* 处理触摸移动
* @param {TouchEvent} e - 触摸事件
*/
handleTouchMove(e) {
const touch = e.touches[0];
this.touchMovePos = { x: touch.clientX, y: touch.clientY };
// 如果移动了,取消长按
if (this.longPressTimer) {
const distance = this.getDistance(this.touchStartPos, this.touchMovePos);
if (distance > 10) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
}
// 滑动检测
if (this.config.enableSwipe) {
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
this.trigger('swipemove', {
deltaX,
deltaY,
startPos: this.touchStartPos,
currentPos: this.touchMovePos,
target: e.target,
});
}
// 捏合缩放检测
if (this.config.enablePinch && e.touches.length === 2) {
this.handlePinch(e);
}
e.preventDefault();
}
/**
* 处理触摸结束
* @param {TouchEvent} e - 触摸事件
*/
handleTouchEnd(e) {
const touchEndTime = Date.now();
const touchDuration = touchEndTime - this.touchStartTime;
// 清除长按定时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 滑动检测
if (this.config.enableSwipe) {
this.detectSwipe();
}
// 点击和双击检测
if (this.config.enableDoubleTap && touchDuration < 300) {
setTimeout(() => {
if (this.tapCount === 1) {
this.trigger('tap', {
position: this.touchStartPos,
target: e.target,
});
} else if (this.tapCount === 2) {
this.trigger('doubletap', {
position: this.touchStartPos,
target: e.target,
});
// 触觉反馈
if (uni && uni.vibrateShort) {
uni.vibrateShort();
}
}
this.tapCount = 0;
}, this.doubleTapDelay);
} else if (touchDuration < 300) {
this.trigger('tap', {
position: this.touchStartPos,
target: e.target,
});
}
}
/**
* 处理触摸取消
* @param {TouchEvent} e - 触摸事件
*/
handleTouchCancel(e) {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.tapCount = 0;
}
/**
* 检测滑动方向和距离
*/
detectSwipe() {
const deltaX = this.touchMovePos.x - this.touchStartPos.x;
const deltaY = this.touchMovePos.y - this.touchStartPos.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < this.swipeThreshold) return;
const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
let direction = '';
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动
direction = deltaX > 0 ? 'right' : 'left';
} else {
// 垂直滑动
direction = deltaY > 0 ? 'down' : 'up';
}
this.trigger('swipe', {
direction,
distance,
angle,
deltaX,
deltaY,
startPos: this.touchStartPos,
endPos: this.touchMovePos,
velocity: distance / (Date.now() - this.touchStartTime),
});
// 触发特定方向的滑动事件
this.trigger(`swipe${direction}`, {
distance,
velocity: distance / (Date.now() - this.touchStartTime),
});
}
/**
* 处理捏合缩放
* @param {TouchEvent} e - 触摸事件
*/
handlePinch(e) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = this.getDistance(
{ x: touch1.clientX, y: touch1.clientY },
{ x: touch2.clientX, y: touch2.clientY },
);
if (!this.initialPinchDistance) {
this.initialPinchDistance = currentDistance;
return;
}
const scale = currentDistance / this.initialPinchDistance;
this.trigger('pinch', {
scale,
center: {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2,
},
});
}
/**
* 计算两点距离
* @param {Object} point1 - 点1
* @param {Object} point2 - 点2
* @returns {number} 距离
*/
getDistance(point1, point2) {
const deltaX = point2.x - point1.x;
const deltaY = point2.y - point1.y;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
/**
* 鼠标事件处理(用于桌面端测试)
*/
handleMouseDown(e) {
this.isMouseDown = true;
this.handleTouchStart({
touches: [{ clientX: e.clientX, clientY: e.clientY }],
target: e.target,
preventDefault: () => e.preventDefault(),
});
}
handleMouseMove(e) {
if (!this.isMouseDown) return;
this.handleTouchMove({
touches: [{ clientX: e.clientX, clientY: e.clientY }],
target: e.target,
preventDefault: () => e.preventDefault(),
});
}
handleMouseUp(e) {
if (!this.isMouseDown) return;
this.isMouseDown = false;
this.handleTouchEnd({
target: e.target,
});
}
/**
* 销毁手势监听
*/
destroy() {
if (!this.element) return;
// 移除事件监听器
this.element.removeEventListener('touchstart', this.handleTouchStart);
this.element.removeEventListener('touchmove', this.handleTouchMove);
this.element.removeEventListener('touchend', this.handleTouchEnd);
this.element.removeEventListener('touchcancel', this.handleTouchCancel);
this.element.removeEventListener('mousedown', this.handleMouseDown);
this.element.removeEventListener('mousemove', this.handleMouseMove);
this.element.removeEventListener('mouseup', this.handleMouseUp);
// 清除定时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
// 清除回调
this.gestureCallbacks.clear();
}
}
/**
* 页面滑动切换功能
*/
class PageSwipeGesture {
constructor() {
this.currentPageIndex = 0;
this.pages = ['home'];
this.isAnimating = false;
}
init() {
const gesture = new GestureUtils();
gesture.init(document.body, {
enableSwipe: true,
enableLongPress: false,
enableDoubleTap: false,
});
// 只有一个页面,禁用滑动切换
return gesture;
}
nextPage() {
// 只有一个页面,不执行切换
return;
}
prevPage() {
// 只有一个页面,不执行切换
return;
}
switchPage(pageName) {
this.isAnimating = true;
const pageUrls = {
home: '/pages/index/index',
};
if (pageUrls[pageName]) {
uni.navigateTo({
url: pageUrls[pageName],
success: () => {
setTimeout(() => {
this.isAnimating = false;
}, 300);
},
});
}
}
}
/**
* 长按菜单功能
*/
class LongPressMenu {
constructor() {
this.menu = null;
this.isShowing = false;
}
show(position, items = []) {
if (this.isShowing) return;
this.isShowing = true;
// 创建菜单HTML
const menuHtml = `
<div class="longpress-menu" style="
position: fixed;
left: ${position.x}px;
top: ${position.y}px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 10000;
animation: fadeIn 0.2s ease-out;
">
${items
.map(
(item) => `
<div class="menu-item" style="
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
font-size: 14px;
" data-action="${item.action}">
${item.icon} ${item.text}
</div>
`,
)
.join('')}
</div>
`;
document.body.insertAdjacentHTML('beforeend', menuHtml);
this.menu = document.querySelector('.longpress-menu');
// 绑定点击事件
this.menu.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (action && items.find((item) => item.action === action)) {
const item = items.find((item) => item.action === action);
if (item.callback) item.callback();
}
this.hide();
});
// 点击其他地方隐藏菜单
setTimeout(() => {
document.addEventListener('click', this.hideOnClickOutside.bind(this));
}, 100);
}
hide() {
if (!this.isShowing || !this.menu) return;
this.menu.style.animation = 'fadeOut 0.2s ease-in forwards';
setTimeout(() => {
if (this.menu && this.menu.parentNode) {
this.menu.remove();
}
this.isShowing = false;
}, 200);
document.removeEventListener('click', this.hideOnClickOutside);
}
hideOnClickOutside(e) {
if (this.menu && !this.menu.contains(e.target)) {
this.hide();
}
}
}
// 导出工具类
export { GestureUtils, PageSwipeGesture, LongPressMenu };
export default GestureUtils;
-192
View File
@@ -1,192 +0,0 @@
/**
* iconfont 图标管理工具
* 帮助开发者快速添加和管理自定义图标
*/
/**
* SVG Path 转换为 iconfont CSS 代码
* @param {string} iconName - 图标名称 (如: star, heart)
* @param {string} svgPath - SVG path 数据
* @param {string} viewBox - SVG viewBox, 默认 "0 0 24 24"
* @returns {string} CSS 代码
*/
export function generateIconCSS(iconName, svgPath, viewBox = '0 0 24 24') {
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='${viewBox}' fill='currentColor'><path d='${svgPath}'/></svg>`;
const encodedSvg = encodeURIComponent(svg).replace(/'/g, '%27').replace(/"/g, '%22');
return `/* ${iconName}图标 */
.icon-${iconName} {
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
}`;
}
/**
* 批量生成图标 CSS
* @param {Array} icons - 图标数组 [{name, path, viewBox}]
* @returns {string} 完整的 CSS 代码
*/
export function generateBatchIconCSS(icons) {
return icons.map((icon) => generateIconCSS(icon.name, icon.path, icon.viewBox)).join('\n\n');
}
/**
* 从完整SVG生成CSS
* @param {string} iconName - 图标名称
* @param {string} svgContent - 完整的SVG内容
* @returns {string} CSS 代码
*/
export function generateIconCSSFromSVG(iconName, svgContent) {
const encodedSvg = encodeURIComponent(svgContent).replace(/'/g, '%27').replace(/"/g, '%22');
return `/* ${iconName}图标 */
.icon-${iconName} {
background-image: url("data:image/svg+xml;charset=utf8,${encodedSvg}");
}`;
}
/**
* 预定义的常用图标路径
* 来源:Material Design Icons
*/
export const COMMON_ICONS = {
// 基础图标
user: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
lock: 'M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6z',
home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
setting:
'M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z',
// 操作图标
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
delete: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z',
check: 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z',
// 状态图标
success:
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
warning: 'M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z',
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
// 文件图标
download: 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z',
upload: 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z',
// 导航图标
arrowLeft: 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z',
arrowRight: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z',
arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z',
arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z',
// 社交图标
star: 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
heart: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
message:
'M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z',
phone: 'M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z',
mail: 'M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z',
};
/**
* 快速生成常用图标的CSS
* @param {string[]} iconNames - 图标名称数组
* @returns {string} CSS 代码
*/
export function generateCommonIconsCSS(iconNames) {
const selectedIcons = iconNames
.map((name) => ({
name,
path: COMMON_ICONS[name],
}))
.filter((icon) => icon.path);
return generateBatchIconCSS(selectedIcons);
}
/**
* 检查图标名称是否已存在
* @param {string} iconName - 图标名称
* @param {string} cssContent - 当前CSS文件内容
* @returns {boolean} 是否已存在
*/
export function checkIconExists(iconName, cssContent) {
return cssContent.includes(`.icon-${iconName}`);
}
/**
* 验证SVG内容
* @param {string} svgContent - SVG内容
* @returns {boolean} 是否有效
*/
export function validateSVG(svgContent) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
return !doc.querySelector('parsererror');
} catch (error) {
return false;
}
}
/**
* 使用示例
*/
export const USAGE_EXAMPLES = {
// 单个图标生成
singleIcon: {
code: `import { generateIconCSS } from '@/utils/iconHelper';
const css = generateIconCSS(
'calendar',
'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z'
);
console.log(css);`,
output: `/* calendar图标 */
.icon-calendar {
background-image: url("data:image/svg+xml;charset=utf8,...");
}`,
},
// 批量图标生成
batchIcons: {
code: `import { generateCommonIconsCSS } from '@/utils/iconHelper';
const css = generateCommonIconsCSS(['star', 'heart', 'message']);
console.log(css);`,
output: `/* star图标 */
.icon-star { ... }
/* heart图标 */
.icon-heart { ... }
/* message图标 */
.icon-message { ... }`,
},
// 从SVG生成
fromSVG: {
code: `import { generateIconCSSFromSVG } from '@/utils/iconHelper';
const svgContent = \`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
</svg>\`;
const css = generateIconCSSFromSVG('circle', svgContent);
console.log(css);`,
},
};
// 导出所有工具函数
export default {
generateIconCSS,
generateBatchIconCSS,
generateIconCSSFromSVG,
generateCommonIconsCSS,
checkIconExists,
validateSVG,
COMMON_ICONS,
USAGE_EXAMPLES,
};
+39 -39
View File
@@ -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]);
},
},
}
};
-259
View File
@@ -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,
};
-179
View File
@@ -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()
},
}
-79
View File
@@ -1,79 +0,0 @@
// Token测试工具
export function testTokenStorage() {
console.log('🧪 开始Token存储测试...');
try {
// 1. 清除现有Token
uni.removeStorageSync('access_token');
console.log('🗑️ 已清除现有Token');
// 2. 测试存储Token
const testToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxIn0.test';
uni.setStorageSync('access_token', testToken);
console.log('💾 已存储测试Token');
// 3. 测试获取Token
const retrievedToken = uni.getStorageSync('access_token');
console.log(
'📖 获取到的Token:',
retrievedToken ? retrievedToken.substring(0, 30) + '...' : '❌ 为空',
);
// 4. 验证Token是否一致
if (retrievedToken === testToken) {
console.log('✅ Token存储和获取测试通过');
return true;
} else {
console.log('❌ Token存储和获取测试失败');
return false;
}
} catch (error) {
console.error('❌ Token测试失败:', error);
return false;
}
}
/**
* 获取当前存储的Token信息
*/
export function getCurrentTokenInfo() {
try {
const token = uni.getStorageSync('access_token');
const userInfo = uni.getStorageSync('userInfo');
const isLoggedIn = uni.getStorageSync('isLoggedIn');
console.log('📊 当前存储状态:');
console.log(' 🎫 Token:', token ? `存在 (${token.length}字符)` : '❌ 不存在');
console.log(' 👤 用户信息:', userInfo ? '存在' : '❌ 不存在');
console.log(' 🔐 登录状态:', isLoggedIn ? '已登录' : '❌ 未登录');
return {
hasToken: !!token,
tokenLength: token ? token.length : 0,
hasUserInfo: !!userInfo,
isLoggedIn: !!isLoggedIn,
};
} catch (error) {
console.error('❌ 获取Token信息失败:', error);
return null;
}
}
/**
* 清除所有登录信息
*/
export function clearAllLoginInfo() {
try {
uni.removeStorageSync('access_token');
uni.removeStorageSync('userInfo');
uni.removeStorageSync('isLoggedIn');
uni.removeStorageSync('refresh_token');
console.log('🗑️ 已清除所有登录信息');
return true;
} catch (error) {
console.error('❌ 清除登录信息失败:', error);
return false;
}
}
+122 -176
View File
@@ -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;
+37
View File
@@ -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"
]
}
-47
View File
@@ -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/, ''),
},
},
},
});
+32
View File
@@ -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);
},
},
},
},
};