diff --git a/ARCHITECTURE_ANALYSIS.md b/ARCHITECTURE_ANALYSIS.md new file mode 100644 index 0000000..720dd7b --- /dev/null +++ b/ARCHITECTURE_ANALYSIS.md @@ -0,0 +1,479 @@ +# OfficeSystem 项目架构与缺陷分析报告 + +## 📋 项目概述 + +**项目名称**: OfficeSystem(办公管理系统) +**技术栈**: uni-app + Vue 3 + Pinia + uv-ui +**项目类型**: 跨平台移动应用(支持H5、小程序、App) + +--- + +## 🏗️ 项目架构分析 + +### 1. 整体架构层次 + +``` +┌─────────────────────────────────────┐ +│ 页面层 (Pages) │ +│ - 任务管理、客户管理、项目管理等 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 组件层 (Components) │ +│ - 业务组件、UI组件 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 状态管理层 (Store/Pinia) │ +│ - user, task, customer, dict │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ API层 (api/) │ +│ - 统一接口封装 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ 工具层 (utils/) │ +│ - 请求封装、工具函数、指令 │ +└─────────────────────────────────────┘ +``` + +### 2. 目录结构分析 + +#### ✅ 优点 +- **模块化清晰**: 按功能模块划分(task、customer、project等) +- **职责分离**: API、Store、组件分离明确 +- **工具函数集中**: utils目录统一管理工具函数 + +#### ⚠️ 问题 +- **缺少类型定义**: 无TypeScript或JSDoc类型定义 +- **测试缺失**: 无单元测试、集成测试 +- **文档不足**: 缺少API文档、组件文档 + +--- + +## 🔍 详细架构分析 + +### 1. API层 (`api/`) + +#### 架构设计 +- ✅ 按业务模块划分(user、task、customer等) +- ✅ 统一使用 `uni.$uv.http` 封装 +- ✅ 支持自定义配置(auth、toast、catch) + +#### 存在的问题 + +**🔴 严重问题** + +1. **硬编码的baseURL** + ```javascript + // utils/request/index.js:13-14 + config.baseURL = 'http://192.168.1.2:4001'; // 开发环境 + config.baseURL = 'https://pm.ccttiot.com/prod-api'; // 生产环境 + ``` + - ❌ 环境配置未分离,需要手动注释切换 + - ❌ 应该使用环境变量或配置文件 + +2. **URL构建方式不统一** + ```javascript + // api/customer.js - 手动拼接URL + const queryString = queryParams.join('&'); + const url = `bst/customer/list${queryString ? `?${queryString}` : ''}`; + + // api/task.js - 也是手动拼接 + queryParams.push(`pageNum=${params.pageNum}`); + ``` + - ❌ 应该使用URLSearchParams或统一工具函数 + - ❌ 容易出错,维护困难 + +3. **错误处理不一致** + ```javascript + // 有些API使用 custom.catch: true + // 有些使用默认的Promise pending + // 导致错误处理方式不统一 + ``` + +**🟡 中等问题** + +4. **调试代码未清理** + ```javascript + // api/customer.js:25,55 + console.log('api params:', params); + console.log('最终请求URL:', url); + ``` + +5. **缺少请求重试机制** + - 网络错误时无自动重试 + - 无请求去重机制 + +### 2. 请求拦截器 (`utils/request/index.js`) + +#### 优点 +- ✅ 统一的token管理 +- ✅ 网络状态检测 +- ✅ 401/403统一处理 + +#### 存在的问题 + +**🔴 严重问题** + +1. **Promise pending导致内存泄漏风险** + ```javascript + // 第112行 + return new Promise(() => { }) // 永远pending的Promise + ``` + - ❌ 可能导致内存泄漏 + - ❌ 无法正确追踪错误 + +2. **重复的登录跳转逻辑** + ```javascript + // 响应拦截和错误拦截都有401处理 + // 代码重复,维护困难 + ``` + +3. **网络检测逻辑重复** + - 请求拦截和错误拦截都检测网络 + - 可以提取为公共函数 + +**🟡 中等问题** + +4. **错误提示不够友好** + ```javascript + uni.$uv.toast('请求失败') // 过于简单 + ``` + +5. **缺少请求超时处理** + - 无超时配置 + - 无超时重试 + +### 3. 状态管理 (`store/`) + +#### 优点 +- ✅ 使用Pinia,符合Vue 3最佳实践 +- ✅ 模块化设计清晰 +- ✅ 支持持久化(localStorage) + +#### 存在的问题 + +**🟡 中等问题** + +1. **Store职责不清晰** + ```javascript + // store/task.js - 只存储taskDetailData + // 但很多页面直接调用API,不使用Store + ``` + +2. **缺少数据缓存策略** + - 无缓存过期机制 + - 无数据同步策略 + +3. **权限数据格式不统一** + ```javascript + // 需要处理数组、对象、字符串多种格式 + // 应该在API层统一转换 + ``` + +### 4. 组件层 (`components/`) + +#### 优点 +- ✅ 按业务模块组织 +- ✅ 组件职责相对清晰 + +#### 存在的问题 + +**🟡 中等问题** + +1. **组件复用性不足** + - 很多业务逻辑写在页面中 + - 缺少通用业务组件 + +2. **样式管理混乱** + - 部分使用scoped,部分未使用 + - 缺少统一的样式变量 + +### 5. 工具层 (`utils/`) + +#### 优点 +- ✅ 权限工具函数已提取(`utils/permission.js`) +- ✅ 请求封装统一 + +#### 存在的问题 + +**🟡 中等问题** + +1. **工具函数分散** + - `textSolve/` 目录下只有少量文件 + - 可以考虑合并到主utils目录 + +2. **缺少常用工具函数** + - 无日期格式化工具(多处重复实现) + - 无数据验证工具 + - 无防抖节流工具 + +### 6. 指令系统 (`directives/`) + +#### 优点 +- ✅ `v-permission` 指令设计良好 +- ✅ 支持Vue 2和Vue 3 +- ✅ 文档完善 + +#### 存在的问题 + +**🟢 轻微问题** + +1. **Pinia实例获取逻辑复杂** + ```javascript + // 尝试多个路径获取Pinia实例 + // 可以简化 + ``` + +--- + +## 🐛 代码质量问题 + +### 1. 调试代码未清理 + +**位置**: 多处 +```javascript +// api/customer.js +console.log('api params:', params); +console.log('最终请求URL:', url); + +// pages/task/detail/index.vue +console.log('任务详情数据:', res); + +// composables/usePagination.js +console.log(`获取数据成功: 第${queryParams.value.pageNum}页`); +``` + +**影响**: +- 生产环境性能影响 +- 可能泄露敏感信息 + +**建议**: +- 使用环境变量控制日志输出 +- 或使用日志工具(如winston) + +### 2. 硬编码值 + +**位置**: +- `App.vue:15` - 测试token硬编码 +- `utils/request/index.js:13-14` - baseURL硬编码 + +**建议**: +- 使用环境变量或配置文件 + +### 3. 错误处理不完善 + +**问题**: +- 很多地方只有 `console.error`,无用户提示 +- 错误信息不够详细 +- 缺少错误上报机制 + +### 4. 代码重复 + +**示例**: +- 日期格式化函数在多处重复实现 +- 权限判断逻辑在多个页面重复 +- 网络检测逻辑重复 + +--- + +## 🔒 安全性问题 + +### 1. Token存储 +- ✅ 使用uni.getStorageSync存储(相对安全) +- ⚠️ 但无token加密 +- ⚠️ 无token刷新机制 + +### 2. 敏感信息 +- ❌ `App.vue` 中硬编码测试token +- ❌ 生产环境可能泄露 + +### 3. 权限校验 +- ✅ 前端有权限指令 +- ⚠️ 但后端校验是最终保障(前端校验可被绕过) + +--- + +## 📊 性能问题 + +### 1. 图片处理 +- ⚠️ 无图片懒加载 +- ⚠️ 无图片压缩 +- ⚠️ 无CDN配置 + +### 2. 数据加载 +- ⚠️ 部分页面无加载状态 +- ⚠️ 无数据预加载 +- ⚠️ 无虚拟列表(长列表性能问题) + +### 3. 打包优化 +- ⚠️ 无代码分割 +- ⚠️ 无Tree Shaking配置检查 + +--- + +## 🧪 测试与质量保证 + +### ❌ 严重缺失 + +1. **无单元测试** + - 无测试文件(.test.js, .spec.js) + - 无测试框架配置 + +2. **无集成测试** + - 无E2E测试 + - 无API测试 + +3. **无代码检查工具** + - 无ESLint配置 + - 无Prettier配置 + - 无Git hooks + +4. **无CI/CD** + - 无自动化构建 + - 无自动化部署 + +--- + +## 📝 文档问题 + +### ❌ 缺失的文档 + +1. **API文档** + - 无接口文档 + - 无参数说明 + +2. **组件文档** + - 无组件使用说明 + - 无Props文档 + +3. **开发文档** + - 无开发规范 + - 无部署文档 + - 无环境配置说明 + +--- + +## 🎯 改进建议 + +### 高优先级(立即修复) + +1. **环境配置分离** + ```javascript + // config/env.js + export const config = { + dev: { baseURL: 'http://192.168.1.2:4001' }, + prod: { baseURL: 'https://pm.ccttiot.com/prod-api' } + } + ``` + +2. **修复Promise pending问题** + ```javascript + // 应该返回rejected Promise或统一错误格式 + return Promise.reject(new Error(data.message || '请求失败')) + ``` + +3. **清理调试代码** + - 移除所有console.log + - 移除硬编码的测试token + +4. **统一URL构建** + ```javascript + // utils/request/buildUrl.js + export function buildUrl(path, params) { + const url = new URL(path, baseURL) + Object.entries(params).forEach(([key, value]) => { + if (value != null) url.searchParams.append(key, value) + }) + return url.toString() + } + ``` + +### 中优先级(近期改进) + +1. **添加错误处理工具** + ```javascript + // utils/errorHandler.js + export function handleError(error, showToast = true) { + // 统一错误处理逻辑 + } + ``` + +2. **添加常用工具函数库** + - 日期格式化 + - 数据验证 + - 防抖节流 + +3. **完善Store设计** + - 统一数据缓存策略 + - 添加数据同步机制 + +4. **添加代码检查工具** + - ESLint配置 + - Prettier配置 + - Git hooks + +### 低优先级(长期优化) + +1. **引入TypeScript** + - 类型安全 + - 更好的IDE支持 + +2. **添加单元测试** + - Jest配置 + - 核心功能测试 + +3. **性能优化** + - 图片懒加载 + - 代码分割 + - 虚拟列表 + +4. **完善文档** + - API文档 + - 组件文档 + - 开发规范 + +--- + +## 📈 架构评分 + +| 维度 | 评分 | 说明 | +|------|------|------| +| **代码组织** | ⭐⭐⭐⭐ | 模块化清晰,但缺少类型定义 | +| **错误处理** | ⭐⭐ | 有基础处理,但不完善 | +| **安全性** | ⭐⭐⭐ | 基础安全措施,但有硬编码问题 | +| **性能** | ⭐⭐⭐ | 基础优化,但缺少高级优化 | +| **可维护性** | ⭐⭐⭐ | 结构清晰,但代码重复较多 | +| **可测试性** | ⭐ | 无测试,难以保证质量 | +| **文档** | ⭐⭐ | 缺少完整文档 | + +**总体评分**: ⭐⭐⭐ (3/5) + +--- + +## 🎓 总结 + +### 优点 +1. ✅ 项目结构清晰,模块化设计良好 +2. ✅ 使用现代技术栈(Vue 3 + Pinia) +3. ✅ 权限系统设计合理 +4. ✅ 请求拦截器功能完善 + +### 主要问题 +1. ❌ 环境配置硬编码 +2. ❌ 缺少测试和质量保证 +3. ❌ 代码重复和调试代码未清理 +4. ❌ 错误处理不完善 +5. ❌ 文档缺失 + +### 建议 +优先解决高优先级问题(环境配置、Promise pending、调试代码),然后逐步完善测试、文档和性能优化。建议引入代码检查工具和CI/CD流程,提升代码质量和开发效率。 + +--- + +**报告生成时间**: 2024年 +**分析工具**: 代码审查 + 架构分析 + diff --git a/api/common.js b/api/common.js index 997d483..8dc0062 100644 --- a/api/common.js +++ b/api/common.js @@ -1,6 +1,7 @@ /** * 通用 API(工具类接口) */ +import { buildUrl } from '@/utils/url' /** * 获取七牛云上传token @@ -44,19 +45,8 @@ export const getDictDataList = (params = {}) => { // 合并参数 const requestParams = { ...defaultParams, ...params }; - // 构建查询参数数组(兼容uniapp) - const queryParams = []; - - Object.entries(requestParams).forEach(([key, value]) => { - if (value !== undefined && value !== null && value !== '') { - queryParams.push(`${key}=${encodeURIComponent(value.toString())}`); - } - }); - - const queryString = queryParams.join('&'); - const url = `/system/dict/data/list${queryString ? `?${queryString}` : ''}`; - - console.log('请求URL:', url); // 调试用 + // 使用统一URL构建工具 + const url = buildUrl('/system/dict/data/list', requestParams); return uni.$uv.http.get(url, { custom: { diff --git a/api/customer.js b/api/customer.js index a7ce95d..4dda9e0 100644 --- a/api/customer.js +++ b/api/customer.js @@ -2,6 +2,8 @@ * 客户相关 API */ +import { buildUrl, mergeParams } from '@/utils/url' + /** * 获取客户列表 * @param {Object} params 请求参数(可选) @@ -20,12 +22,7 @@ * @param {string[]} params.saleList 销售列表 * @returns {Promise} 返回客户列表 { total: number, rows: array } */ -// 最终推荐版本(添加默认排序参数) export const getCustomerList = (params = {}) => { - console.log('api params:', params); - - const queryParams = []; - // 添加默认排序参数 const defaultParams = { orderByColumn: 'nextFollowTime', @@ -33,26 +30,10 @@ export const getCustomerList = (params = {}) => { }; // 合并参数:默认参数 + 用户传入参数(用户参数优先) - const finalParams = { ...defaultParams, ...params }; + const finalParams = mergeParams(defaultParams, params); - Object.entries(finalParams).forEach(([key, value]) => { - if (value == null) return; // 过滤 null 和 undefined - - if (Array.isArray(value)) { - value.forEach(item => { - if (item != null) { - queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(item.toString())}`); - } - }); - } else { - queryParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`); - } - }); - - const queryString = queryParams.join('&'); - const url = `bst/customer/list${queryString ? `?${queryString}` : ''}`; - - console.log('最终请求URL:', url); + // 使用统一URL构建工具 + const url = buildUrl('bst/customer/list', finalParams); return uni.$uv.http.get(url, { custom: { auth: true } }); }; diff --git a/api/dashboard.js b/api/dashboard.js index ef4a298..77a694c 100644 --- a/api/dashboard.js +++ b/api/dashboard.js @@ -1,6 +1,7 @@ /** * 仪表板相关 API */ +import { buildUrl } from '@/utils/url' /** * 获取仪表板简要信息 @@ -10,20 +11,17 @@ * @returns {Promise} 返回仪表板简要信息 */ export const getDashboardBrief = ({ joinUserId, keys }) => { - // 构建查询参数字符串 - let params=[] + const params = {}; if (joinUserId) { - params = [`joinUserId=${joinUserId}`]; + params.joinUserId = joinUserId; } - if (keys && Array.isArray(keys)) { - keys.forEach((key) => { - params.push(`keys=${encodeURIComponent(key)}`); - }); + params.keys = keys; } - const queryString = params.join('&'); - return uni.$uv.http.get(`dashboard/brief?${queryString}`, { + const url = buildUrl('dashboard/brief', params); + + return uni.$uv.http.get(url, { custom: { auth: true // 启用 token 认证 } @@ -37,12 +35,11 @@ export const getDashboardBrief = ({ joinUserId, keys }) => { export const getCustomerStatistics = (params = {}) => { // 期望传入为数组: [start, end] 或 [day, day] const dateRange = Array.isArray(params) ? params : []; - const query = - dateRange.length > 0 - ? `?${dateRange.map(d => `queryDateRange=${encodeURIComponent(d)}`).join('&')}` - : ''; + const queryParams = dateRange.length > 0 ? { queryDateRange: dateRange } : {}; + + const url = buildUrl('/dashboard/customer/statistics', queryParams); - return uni.$uv.http.get(`/dashboard/customer/statistics${query}`, { + return uni.$uv.http.get(url, { custom: { auth: true // 启用 token 认证 } @@ -63,36 +60,9 @@ export const getCustomerStatistics = (params = {}) => { * @returns {Promise} 返回公告列表 */ export const getNoticeList = (params = {}) => { - const queryParams = []; + const url = buildUrl('bst/notice/list', params); - if (params.pageNum !== undefined) { - queryParams.push(`pageNum=${params.pageNum}`); - } - if (params.pageSize !== undefined) { - queryParams.push(`pageSize=${params.pageSize}`); - } - if (params.title) { - queryParams.push(`title=${encodeURIComponent(params.title)}`); - } - if (params.userName) { - queryParams.push(`userName=${encodeURIComponent(params.userName)}`); - } - if (params.level) { - queryParams.push(`level=${encodeURIComponent(params.level)}`); - } - if (params.top !== undefined && params.top !== null && params.top !== '') { - queryParams.push(`top=${params.top}`); - } - if (params.orderByColumn) { - queryParams.push(`orderByColumn=${encodeURIComponent(params.orderByColumn)}`); - } - if (params.isAsc) { - queryParams.push(`isAsc=${encodeURIComponent(params.isAsc)}`); - } - - const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; - - return uni.$uv.http.get(`bst/notice/list${queryString}`, { + return uni.$uv.http.get(url, { custom: { auth: true // 启用 token 认证 } diff --git a/api/project.js b/api/project.js index f9de51b..63746c4 100644 --- a/api/project.js +++ b/api/project.js @@ -1,6 +1,7 @@ /** * 项目相关 API */ +import { buildUrl } from '@/utils/url' /** * 获取项目列表 @@ -16,76 +17,31 @@ * @returns {Promise} 返回项目列表 */ export const getProjectList = (params = {}) => { - const queryParams = []; + // 处理特殊字段映射 + const mappedParams = { ...params }; - // 分页参数 - if (params.pageNum !== undefined) { - queryParams.push(`pageNum=${params.pageNum}`); - } - if (params.pageSize !== undefined) { - queryParams.push(`pageSize=${params.pageSize}`); + // 将overdue映射为devOverdue + if (mappedParams.overdue !== undefined) { + mappedParams.devOverdue = mappedParams.overdue; + delete mappedParams.overdue; } - // 排序参数 - if (params.orderByColumn !== undefined && params.orderByColumn !== '') { - queryParams.push(`orderByColumn=${encodeURIComponent(params.orderByColumn)}`); - } - if (params.isAsc !== undefined && params.isAsc !== '') { - queryParams.push(`isAsc=${encodeURIComponent(params.isAsc)}`); + // 将projectName映射为name + if (mappedParams.projectName !== undefined) { + mappedParams.name = mappedParams.projectName; + delete mappedParams.projectName; } - // 状态列表 - if (params.statusList !== undefined && Array.isArray(params.statusList) && params.statusList.length > 0) { - params.statusList.forEach((status, index) => { - queryParams.push(`statusList=${status}`); - }); - } - - // 是否曾经逾期 - if (params.overdue !== undefined && params.overdue !== '') { - queryParams.push(`devOverdue=${params.overdue}`); - } - - // 创建人ID - if (params.createId !== undefined && params.createId !== '') { - queryParams.push(`createId=${params.createId}`); + // 将projectId映射为no(用于搜索) + if (mappedParams.projectId !== undefined && !mappedParams.no) { + mappedParams.no = mappedParams.projectId; + // 注意:这里不删除projectId,因为可能还有其他用途 } - // 负责人ID - if (params.ownerId !== undefined && params.ownerId !== '') { - queryParams.push(`ownerId=${params.ownerId}`); - } + // 使用统一URL构建工具,keys数组使用bracket格式 + const url = buildUrl('bst/project/list', mappedParams); - // 搜索关键词 - if (params.keys !== undefined && Array.isArray(params.keys) && params.keys.length > 0) { - params.keys.forEach((key, index) => { - queryParams.push(`keys[${index}]=${encodeURIComponent(key)}`); - }); - } - - // 项目名称搜索 - if (params.projectName !== undefined && params.projectName !== '') { - queryParams.push(`name=${encodeURIComponent(params.projectName)}`); - } - - // 项目编号搜索 - if (params.projectId !== undefined && params.projectId !== '') { - queryParams.push(`no=${encodeURIComponent(params.projectId)}`); - } - - // 客户名称搜索 - if (params.customerName !== undefined && params.customerName !== '') { - queryParams.push(`customerName=${encodeURIComponent(params.customerName)}`); - } - - // 成员ID搜索 - if (params.joinUserId !== undefined && params.joinUserId !== '') { - queryParams.push(`joinUserId=${params.joinUserId}`); - } - - const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; - - return uni.$uv.http.get(`bst/project/list${queryString}`, { + return uni.$uv.http.get(url, { custom: { auth: true // 启用 token 认证 } diff --git a/api/task.js b/api/task.js index e3b5a67..64a9870 100644 --- a/api/task.js +++ b/api/task.js @@ -1,6 +1,7 @@ /** * 任务相关 API */ +import { buildUrl } from '@/utils/url' /** * 获取任务列表 @@ -23,87 +24,10 @@ * @returns {Promise} 返回任务列表 */ export const getTaskList = (params = {}) => { - const queryParams = []; + // 使用统一URL构建工具 + const url = buildUrl('bst/task/list', params); - // 分页参数 - if (params.pageNum !== undefined) { - queryParams.push(`pageNum=${params.pageNum}`); - } - if (params.pageSize !== undefined) { - queryParams.push(`pageSize=${params.pageSize}`); - } - - // 排序参数 - if (params.orderByColumn !== undefined && params.orderByColumn !== '') { - queryParams.push(`orderByColumn=${encodeURIComponent(params.orderByColumn)}`); - } - if (params.isAsc !== undefined && params.isAsc !== '') { - queryParams.push(`isAsc=${encodeURIComponent(params.isAsc)}`); - } - - // 项目ID - if (params.projectId !== undefined && params.projectId !== '') { - queryParams.push(`projectId=${params.projectId}`); - } - - // 任务类型 - if (params.type !== undefined && params.type !== '') { - queryParams.push(`type=${params.type}`); - } - - // 优先级 - if (params.level !== undefined && params.level !== '') { - queryParams.push(`level=${params.level}`); - } - - // 创建人ID - if (params.createId !== undefined && params.createId !== '') { - queryParams.push(`createId=${params.createId}`); - } - - // 负责人ID - if (params.ownerId !== undefined && params.ownerId !== '') { - queryParams.push(`ownerId=${params.ownerId}`); - } - - // 状态列表 - if (params.statusList !== undefined && Array.isArray(params.statusList) && params.statusList.length > 0) { - // 将数组转换为重复参数格式,例如 [1,2] => "statusList=1&statusList=2" - params.statusList.forEach(status => { - queryParams.push(`statusList=${status}`); - }); - } - - // 任务状态(单个) - if (params.status !== undefined && params.status !== '') { - queryParams.push(`status=${params.status}`); - } - - // 是否逾期 - if (params.overdue !== undefined) { - queryParams.push(`overdue=${params.overdue}`); - } - - // 过期时间范围 - if (params.expireTimeStart !== undefined && params.expireTimeStart !== null && params.expireTimeStart !== '') { - queryParams.push(`expireTimeStart=${encodeURIComponent(params.expireTimeStart)}`); - } - if (params.expireTimeEnd !== undefined && params.expireTimeEnd !== null && params.expireTimeEnd !== '') { - queryParams.push(`expireTimeEnd=${encodeURIComponent(params.expireTimeEnd)}`); - } - - // 通过日期范围 - if (params.passDateRange !== undefined && Array.isArray(params.passDateRange) && params.passDateRange.length > 0) { - params.passDateRange.forEach((date, index) => { - if (date) { - queryParams.push(`passDateRange=${encodeURIComponent(date)}`); - } - }); - } - - const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; - - return uni.$uv.http.get(`bst/task/list${queryString}`, { + return uni.$uv.http.get(url, { custom: { auth: true // 启用 token 认证 } diff --git a/api/user.js b/api/user.js index c017ee8..c4cdcbd 100644 --- a/api/user.js +++ b/api/user.js @@ -1,6 +1,7 @@ /** * 用户相关 API */ +import { buildUrl, mergeParams } from '@/utils/url' /** * 获取用户信息 @@ -82,19 +83,19 @@ export const getUserListAll = () => { * @returns {Promise} 用户列表 */ export const getUserList = (params = {}) => { - // 合并参数并过滤空值 - const requestParams = Object.entries({ + // 设置默认参数 + const defaultParams = { pageNum: 1, pageSize: 100, status: 0, - delFlag: 0, - ...params - }) - .filter(([_, value]) => value !== undefined && value !== null && value !== '') - .map(([key, value]) => `${key}=${encodeURIComponent(value.toString())}`) - .join('&'); + delFlag: 0 + }; - const url = `/system/user/list${requestParams ? `?${requestParams}` : ''}`; + // 合并参数 + const requestParams = mergeParams(defaultParams, params); + + // 使用统一URL构建工具 + const url = buildUrl('/system/user/list', requestParams); return uni.$uv.http.get(url, { custom: { auth: true } diff --git a/api/utils.js b/api/utils.js index 7537fef..22ae773 100644 --- a/api/utils.js +++ b/api/utils.js @@ -1,5 +1,6 @@ /** * 将参数对象中的数组转换为查询字符串 + * @deprecated 请使用 @/utils/url 中的 buildUrl 函数 * @param {Object} params - 参数对象 * @returns {Object} { params: 处理后的参数对象, queryString: 查询字符串 } */ diff --git a/api/verify.js b/api/verify.js index b34b114..33cf401 100644 --- a/api/verify.js +++ b/api/verify.js @@ -1,4 +1,5 @@ -import {convertArrayParamsToQuery} from "@/api/utils"; +import { buildUrl, mergeParams } from '@/utils/url' + // 审批相关 API /** @@ -7,23 +8,22 @@ import {convertArrayParamsToQuery} from "@/api/utils"; * @returns {Promise} 返回审核列表 */ export const getVerifyList = (params = {}) => { - // 使用通用函数处理数组参数 - const { params: processedParams, queryString } = convertArrayParamsToQuery(params); + // 设置默认参数 + const defaultParams = { + pageNum: 1, + pageSize: 20, + orderByColumn: 'createTime', + isAsc: 'descending', + bstType: 'UPDATE_TASK' + }; - // 构建请求 URL(去掉查询字符串开头的 &) - const url = queryString - ? `/bst/verify/list?${queryString}` - : '/bst/verify/list'; + // 合并参数 + const requestParams = mergeParams(defaultParams, params); + + // 使用统一URL构建工具 + const url = buildUrl('/bst/verify/list', requestParams); return uni.$uv.http.get(url, { - params: { - pageNum: 1, - pageSize: 20, - orderByColumn: 'createTime', - isAsc: 'descending', - bstType: 'UPDATE_TASK', - ...processedParams - }, custom: { auth: true } diff --git a/utils/url.js b/utils/url.js new file mode 100644 index 0000000..518dbc5 --- /dev/null +++ b/utils/url.js @@ -0,0 +1,165 @@ +/** + * URL构建工具 + * 统一处理查询参数的构建,支持数组、对象、特殊字符编码等 + */ + +/** + * 构建带查询参数的URL + * @param {string} basePath - 基础路径(如 'bst/task/list') + * @param {Object} params - 查询参数对象 + * @param {Object} options - 配置选项 + * @param {boolean} options.encodeValues - 是否对值进行URL编码(默认true) + * @param {boolean} options.skipEmpty - 是否跳过空值(默认true) + * @param {string} options.arrayFormat - 数组格式:'repeat'(重复参数)或 'bracket'(带索引),默认'repeat' + * @returns {string} 完整的URL字符串 + * + * @example + * // 基础用法 + * buildUrl('bst/task/list', { pageNum: 1, pageSize: 10 }) + * // => 'bst/task/list?pageNum=1&pageSize=10' + * + * @example + * // 数组参数(重复格式) + * buildUrl('bst/task/list', { statusList: [1, 2, 3] }) + * // => 'bst/task/list?statusList=1&statusList=2&statusList=3' + * + * @example + * // 数组参数(带索引格式) + * buildUrl('bst/project/list', { keys: ['key1', 'key2'] }, { arrayFormat: 'bracket' }) + * // => 'bst/project/list?keys[0]=key1&keys[1]=key2' + * + * @example + * // 包含特殊字符 + * buildUrl('bst/task/list', { orderByColumn: 'create time', isAsc: 'ascending' }) + * // => 'bst/task/list?orderByColumn=create%20time&isAsc=ascending' + */ +export function buildUrl(basePath, params = {}, options = {}) { + const { + encodeValues = true, + skipEmpty = true, + arrayFormat = 'repeat' // 'repeat' | 'bracket' + } = options + + // 如果没有参数,直接返回基础路径 + if (!params || Object.keys(params).length === 0) { + return basePath + } + + const queryParams = [] + + /** + * 编码值 + * @param {any} value - 要编码的值 + * @returns {string} 编码后的字符串 + */ + const encodeValue = (value) => { + if (encodeValues) { + return encodeURIComponent(String(value)) + } + return String(value) + } + + /** + * 添加查询参数 + * @param {string} key - 参数名 + * @param {any} value - 参数值 + */ + const addParam = (key, value) => { + // 跳过空值 + if (skipEmpty && (value === null || value === undefined || value === '')) { + return + } + + const encodedKey = encodeURIComponent(key) + + // 处理数组 + if (Array.isArray(value)) { + if (value.length === 0) return + + value.forEach((item, index) => { + // 跳过数组中的空值 + if (skipEmpty && (item === null || item === undefined || item === '')) { + return + } + + if (arrayFormat === 'bracket') { + // 带索引格式:keys[0]=value1 + queryParams.push(`${encodedKey}[${index}]=${encodeValue(item)}`) + } else { + // 重复格式:key=value1&key=value2 + queryParams.push(`${encodedKey}=${encodeValue(item)}`) + } + }) + return + } + + // 处理对象(转换为JSON字符串) + if (typeof value === 'object' && value !== null) { + queryParams.push(`${encodedKey}=${encodeValue(JSON.stringify(value))}`) + return + } + + // 处理布尔值 + if (typeof value === 'boolean') { + queryParams.push(`${encodedKey}=${value}`) + return + } + + // 处理普通值 + queryParams.push(`${encodedKey}=${encodeValue(value)}`) + } + + // 遍历所有参数 + Object.entries(params).forEach(([key, value]) => { + addParam(key, value) + }) + + // 构建查询字符串 + const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '' + + return `${basePath}${queryString}` +} + +/** + * 合并默认参数和用户参数 + * @param {Object} defaultParams - 默认参数 + * @param {Object} userParams - 用户传入的参数 + * @returns {Object} 合并后的参数对象(用户参数优先) + * + * @example + * mergeParams({ pageNum: 1, pageSize: 10 }, { pageNum: 2 }) + * // => { pageNum: 2, pageSize: 10 } + */ +export function mergeParams(defaultParams = {}, userParams = {}) { + return { ...defaultParams, ...userParams } +} + +/** + * 从URL中提取查询参数 + * @param {string} url - 完整URL或查询字符串 + * @returns {Object} 参数对象 + * + * @example + * parseUrl('bst/task/list?pageNum=1&pageSize=10') + * // => { pageNum: '1', pageSize: '10' } + */ +export function parseUrl(url) { + const params = {} + + try { + const queryString = url.includes('?') ? url.split('?')[1] : url + if (!queryString) return params + + queryString.split('&').forEach(param => { + const [key, value] = param.split('=') + if (key) { + params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '' + } + }) + } catch (error) { + console.error('解析URL参数失败:', error) + } + + return params +} +