--- name: "web-dev-best-practices" description: "Provides best practices for web development including date handling, internationalization, pagination, and data source configuration. Invoke when developing new pages or troubleshooting common web app issues." --- # Web开发最佳实践指南 ## 1. 时间日期处理最佳实践 ### 问题类型 - 前端日期字符串与后端Date对象转换问题 - GET请求参数中的日期格式处理 - JSON序列化中的日期格式处理 - 前端显示的日期格式化 ### 解决方案 - **后端接收GET参数**:使用 `@DateTimeFormat(pattern = "yyyy-MM-dd")` - **后端JSON序列化**:使用 `@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")` - **前后端同时使用**:两个注解同时使用确保兼容性 - **前端显示格式化**:解析字符串日期并格式化为所需格式 ### 示例代码 ```java @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date planTimeStart; ``` ```javascript // 前端日期解析和格式化 const parsedDate = new Date(dateString); if (!isNaN(parsedDate.getTime())) { // 格式化为所需格式 return `${year}-${month}-${day} ${hours}:${minutes}`; } ``` ## 2. 前端国际化配置 ### 问题类型 - 组件库默认语言为英文 - 日期控件显示语言不符 ### 解决方案 - 在main.js中引入语言包 - 配置Element Plus使用中文语言环境 ### 示例代码 ```javascript import zhCn from 'element-plus/es/locale/lang/zh-cn' app.use(ElementPlus, { locale: zhCn }) ``` ## 3. 搜索和分页功能最佳实践 ### 问题类型 - 搜索和列表功能分离 - 大数据量性能问题 - 用户体验不佳 ### 解决方案 - 合并搜索和列表功能,通过参数判断 - 实现数据库分页,减少内存占用 - 添加瀑布流加载,提升用户体验 - 使用 `COALESCE` 函数处理空值判断 ### 示例代码 ```java @Query("SELECT new com.example.dto(...) " + "FROM ... " + "WHERE ... AND (COALESCE(:query, '') = '' OR ...) ") Page searchWithPage(@Param("query") String query, Pageable pageable); ``` ## 4. 数据库读写分离和数据源配置 ### 问题类型 - 误解数据库架构 - 错误的数据源配置 - 混淆读写分离概念 ### 解决方案 - 明确区分两种数据库类型: - **tugboatcommon**:系统公共库,存储所有机构共享数据 - **liandatugboatmis**:分支机构业务库,存储各机构业务数据 - 使用有意义的命名而非READ/WRITE: - `COMMON` → tugboatcommon - `BRANCH` → liandatugboatmis - 只对同时存在于两库的表使用分离 - 正确配置默认数据源 ### 示例代码 ```java public enum DataSourceType { COMMON("common"), // tugboatcommon: 系统公共库 BRANCH("branch"); // liandatugboatmis: 分支机构业务库 } ``` ## 5. 前端组件优化 ### 问题类型 - 下拉选择器性能问题 - 数据显示字段错误 - 用户交互体验差 ### 解决方案 - 实现虚拟滚动或分页加载 - 明确显示字段与值字段的区别 - 添加加载状态提示 - 实现滚动加载更多功能 ### 示例代码 ```html
加载中...
:value="item.valueField">
``` ## 6. 调试和开发流程 ### 最佳实践 - **针对性启动**:修改哪个项目就重新调试哪个项目,不要每次都全部重启 - **渐进式开发**:先实现基础功能,再逐步优化 - **前后端分离调试**:分别启动前后端服务进行调试 - **日志监控**:关注错误信息和警告信息 - **版本控制**:及时保存中间状态,避免丢失重要修改 ## 7. 前端防重复点击最佳实践 ### 问题类型 - 用户快速多次点击按钮或触发异步操作 - 多次发送相同请求 - 数据重复提交 - 用户体验不佳 - 系统资源浪费 ### 解决方案 在所有触发后台操作的前端事件中添加进度条和防重复点击机制。 ### 实现方法 1. **使用loading状态控制**:在按钮上绑定loading属性,在请求期间禁用按钮 2. **全局Loading覆盖**:对于页面级别的操作,使用全局Loading遮罩 3. **防抖和节流技术**:对于频繁触发的操作,使用debounce或throttle 4. **统一组件封装**:创建可复用的防重复点击组件 ### 示例代码 ```vue ``` ## 8. 常见陷阱和注意事项 1. **注解使用场景**: - `@DateTimeFormat` 用于请求参数绑定 - `@JsonFormat` 用于JSON序列化/反序列化 2. **命名规范**: - 避免使用可能引起误解的命名(如READ/WRITE) - 使用业务含义明确的命名 3. **数据一致性**: - 确保前后端日期格式一致 - 保证显示字段与业务需求一致 4. **性能考虑**: - 大数据量时使用分页 - 合理使用缓存 - 避免不必要的数据传输 ## 使用时机 在开发新页面功能、调试常见Web应用问题、处理日期时间、配置数据源、优化前端组件时使用此技能。 ## 9. 前端UI布局最佳实践 ### 问题类型 - 查询条件控件过多导致页面拥挤 - 表格列头没有排序提示 - 表格内容换行影响阅读 - 表格标题显示不完整 - 横向滚动时标题和固定列无法锁定 ### 解决方案 #### 9.1 查询条件布局 - **每行三个控件**:将查询条件控件按每行三个进行布局 - **默认显示第一行**:只显示第一行的三个控件,其他控件折叠 - **展开/收起功能**:添加"展开/收起"按钮,用户点击后显示/隐藏剩余控件 - **按钮并排布局**:将"展开/收起"、"查询"、"重置"三个按钮并排放在第一行查询条件的右边,与查询条件控件在同一行对齐 **按钮并排布局实现**: - 在第一行查询条件容器的末尾添加按钮组 - 按钮与查询条件控件使用相同的flex布局 - 确保按钮与查询条件控件垂直居中对齐 - 节省垂直空间,让表格可看到更多内容 #### 9.2 表格列头排序(数据库级排序和分页) - **排序箭头**:表格列头显示排序箭头(↑↓) - **点击排序**:点击列头可切换排序方向(升序→降序→无排序) - **当前排序标识**:当前排序的列头显示箭头,未排序列头显示空心箭头 - **数据库级排序**:排序参数传递到后端,在数据库层面进行排序,而不是在前端排序 - **数据库级分页**:分页参数传递到后端,在数据库层面进行分页,而不是在前端分页 - **BaseQueryDTO基类**:使用BaseQueryDTO作为查询参数基类,包含page、pageSize、sortBy、sortOrder字段 - **字段映射**:前端表格列prop映射到后端数据库字段名 **数据库级排序和分页实现**: - 前端监听表格的@sort-change事件 - 将排序字段和方向传递给后端 - 前端将分页参数(page、pageSize)传递给后端 - 后端使用BaseQueryDTO接收分页和排序参数 - 后端使用Spring Data的PageRequest和Sort对象进行数据库分页和排序 - 确保分页和排序在数据库层面执行,提高性能 - page参数需要减1,因为Spring Data JPA的页码从0开始 #### 9.3 表格紧凑布局 - **紧凑模式**:使用`size="small"`或`size="default"`属性 - **禁止换行**:设置`white-space: nowrap`防止内容换行 - **文本截断**:内容过长时使用`text-overflow: ellipsis`截断显示 - **工具提示**:鼠标悬停时显示完整内容的tooltip #### 9.4 表格标题锁定 - **固定标题行**:使用`position: sticky`或Element Plus的`sticky-header`属性 - **滚动时锁定**:表格滚动时标题行保持在顶部 #### 9.5 固定列锁定 - **左侧固定列**:使用`fixed="left"`锁定序号列、操作列等 - **右侧固定列**:使用`fixed="right"`锁定操作列 - **横向滚动锁定**:固定列在横向滚动时保持不动 ### 示例代码 #### 9.1 查询条件布局(按钮并排版本) ```vue ``` #### 9.2 表格列头排序 ```vue ``` #### 9.3 完整的表格配置 ```vue ``` #### 9.4 后端Service实现(数据库级分页和排序) ```java @Service public class PilotPlanService { @Autowired private DispPilotPlanReadRepository dispPilotPlanReadRepository; /** * 获取引航计划列表(支持数据库级分页和排序) */ @DataSource(value = RoutingDataSourceConfig.DataSourceType.BRANCH) public List getPilotPlans(PilotPlanQueryDTO queryDTO) { // 获取分页参数(BaseQueryDTO中定义) Integer page = queryDTO.getPage() != null ? queryDTO.getPage() - 1 : 0; Integer pageSize = queryDTO.getPageSize() != null ? queryDTO.getPageSize() : 20; // 创建分页对象 Pageable pageable = PageRequest.of(page, pageSize); // 如果有排序参数,则添加排序 if (queryDTO.getSortBy() != null && !queryDTO.getSortBy().isEmpty()) { String sortBy = queryDTO.getSortBy(); String sortOrder = queryDTO.getSortOrder(); if (sortOrder != null && !sortOrder.isEmpty()) { Sort sort = Sort.by(Sort.Order.asc(sortBy)); if ("DESC".equalsIgnoreCase(sortOrder)) { sort = Sort.by(Sort.Order.desc(sortBy)); } // 更新分页对象,添加排序 pageable = PageRequest.of(page, pageSize, sort); } } // 使用Repository查询,传入分页和排序参数 Page result = dispPilotPlanReadRepository.findWithFiltersProjected( planTimeStart, planTimeEnd, null, isEmpty(queryDTO.getPortId()) ? null : queryDTO.getPortId(), null, isEmpty(queryDTO.getStatus()) ? null : queryDTO.getStatus(), isEmpty(queryDTO.getShipownerId()) ? null : queryDTO.getShipownerId(), queryDTO.getDeepStart(), queryDTO.getDeepEnd(), isEmpty(queryDTO.getTrafficboat()) ? null : queryDTO.getTrafficboat(), queryDTO.getIsInnerPort(), isEmpty(queryDTO.getEnglishShipName()) ? null : queryDTO.getEnglishShipName(), pageable // 传入分页和排序参数 ); // 获取查询结果 List projections = result.getContent(); // 转换为DTO并返回 List pilotPlanDTOs = new ArrayList<>(); for (int i = 0; i < projections.size(); i++) { PilotPlanProjection projection = projections.get(i); PilotPlanDTO pilotPlanDTO = new PilotPlanDTO(); // 设置序号(基于分页计算) pilotPlanDTO.setIndex(page * pageSize + i + 1); // 设置其他字段... pilotPlanDTO.setPilotPlanId(projection.getPilotPlanId()); pilotPlanDTO.setPlanTime(projection.getPlanTime()); pilotPlanDTOs.add(pilotPlanDTO); } return pilotPlanDTOs; } } ``` **关键点说明**: 1. **分页参数处理**:从BaseQueryDTO获取page和pageSize,page需要减1(Spring Data JPA页码从0开始) 2. **排序参数处理**:从BaseQueryDTO获取sortBy和sortOrder,创建Sort对象 3. **Pageable对象**:使用PageRequest.of()创建分页对象,支持分页和排序 4. **Repository查询**:将Pageable对象传递给Repository方法,在数据库层面执行分页和排序 5. **序号计算**:序号基于分页计算:`page * pageSize + i + 1`,确保序号连续且正确 6. **性能优化**:数据库级分页和排序比前端分页和排序性能更好,特别是大数据量时 7. **分页响应结构**:使用PageResponse包装返回数据,包含content(数据列表)、totalElements(总记录数)、currentPage(当前页)、pageSize(每页大小)、totalPages(总页数) #### 9.5 Controller层实现(返回分页响应) ```java @RestController @RequestMapping("/pilot-plan") public class PilotPlanController { @Autowired private PilotPlanService pilotPlanService; /** * 获取引航计划列表(支持分页和排序) */ @GetMapping("/list") public ResponseEntity> getPilotPlans(PilotPlanQueryDTO queryDTO) { PageResponse pilotPlans = pilotPlanService.getPilotPlans(queryDTO); return new ResponseEntity<>(pilotPlans, HttpStatus.OK); } } ``` #### 9.6 前端API调用(处理分页响应) ```javascript methods: { getPilotPlans() { const params = { planTimeStart: this.searchForm.planTimeRange && this.searchForm.planTimeRange.length > 0 ? this.formatDate(this.searchForm.planTimeRange[0]) : '', planTimeEnd: this.searchForm.planTimeRange && this.searchForm.planTimeRange.length > 1 ? this.formatDate(this.searchForm.planTimeRange[1]) : '', portAreaOperationType: this.searchForm.portAreaOperationType, portId: this.searchForm.portId, englishShipName: this.searchForm.englishShipName, status: this.searchForm.status, shipownerId: this.searchForm.shipownerId, deepStart: this.searchForm.deepStart, deepEnd: this.searchForm.deepEnd, trafficboat: this.searchForm.trafficboat, page: this.currentPage, // 当前页码 pageSize: this.pageSize, // 每页大小 sortBy: this.sortMap.sortBy, // 排序字段 sortOrder: this.sortMap.sortOrder // 排序方向 }; this.$axios.get('/api/pilot-plan/list', { params }) .then(response => { // 从PageResponse中获取数据 this.pilotPlans = response.data.content; // 当前页数据 this.total = response.data.totalElements; // 总记录数 // 可选:获取其他分页信息 // this.totalPages = response.data.totalPages; // 总页数 // this.currentPage = response.data.currentPage; // 当前页码 }) .catch(error => { console.error('获取引航计划失败:', error); this.$message.error('获取引航计划失败,请稍后重试'); }); } } ``` **关键点说明**: 1. **分页响应结构**:后端返回PageResponse对象,包含完整的分页信息 2. **数据提取**:从response.data.content获取当前页的数据列表 3. **总记录数**:从response.data.totalElements获取总记录数,用于分页控件显示 4. **分页控件绑定**:将total绑定到el-pagination的total属性,currentPage绑定到current-page属性 5. **分页事件**:监听el-pagination的@current-change和@size-change事件,更新currentPage和pageSize并重新查询 ## 10. 前端权限控制最佳实践 ### 问题类型 - 用户登录信息即将失效时需要自动更新权限 - 不同用户角色需要显示不同的按钮 - 需要根据功能代码控制按钮显示/隐藏 ### 解决方案 - **Token刷新机制**:使用TokenRefreshManager监控token并在即将过期时自动刷新 - **权限自动更新**:在token刷新成功后自动更新userFunctionCodes - **权限指令**:使用v-permission指令控制按钮显示/隐藏 - **功能代码命名**:使用规范的功能代码命名(如:pilotplan:query、pilotplan:import等) ### Token刷新和权限更新机制 - **TokenRefreshManager**:监控token并在即将过期前10分钟自动刷新 - **权限更新回调**:在token刷新成功后自动调用PermissionManager更新权限 - **localStorage存储**:权限信息存储在localStorage的userFunctionCodes中 ### 权限指令实现 - **指令注册**:在main.js中注册v-permission指令 - **权限检查**:指令从localStorage读取userFunctionCodes并检查是否包含指定功能代码 - **自动隐藏**:用户没有权限时自动隐藏按钮(display: none) - **响应式更新**:当权限信息更新时自动重新检查并更新按钮显示状态 ### 功能代码命名规范 - **模块:操作**:如pilotplan:query(引航计划查询) - **模块:操作:子操作**:如pilotplan:import:parse(引航计划导入解析) - **英文小写**:使用英文小写,用冒号分隔层级 ### 示例代码 #### 10.1 main.js中注册权限指令和设置token刷新回调 ```javascript import { createApp } from 'vue' import App from './App.vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import router from './router' import axios from 'axios' import tokenRefreshManager from './utils/tokenRefreshManager' import PermissionManager from './utils/permission' import permissionDirective from './directives/permission' const app = createApp(App) app.use(ElementPlus, { locale: zhCn }) app.use(router) // 注册权限指令 app.directive('permission', permissionDirective) // 配置axios app.config.globalProperties.$axios = axios // 设置token刷新回调,刷新时更新权限信息 tokenRefreshManager.setOnTokenRefresh(async (newToken) => { console.log('Token已刷新,更新权限信息...'); try { await PermissionManager.initializePermissions(`Bearer ${newToken}`); console.log('权限信息更新完成'); } catch (error) { console.error('更新权限信息失败:', error); } }); app.mount('#app') ``` #### 10.2 权限指令实现(directives/permission.js) ```javascript // 权限指令,用于控制按钮显示 const permissionDirective = { // 在绑定元素插入父节点时调用 mounted(el, binding) { // 获取按钮需要的功能代码 const requiredFunctionCode = binding.value; // 从localStorage中获取当前用户的功能代码列表 const userFunctionCodesStr = localStorage.getItem('userFunctionCodes'); if (!userFunctionCodesStr) { // 如果没有功能代码列表,隐藏按钮 el.style.display = 'none'; return; } try { const userFunctionCodes = JSON.parse(userFunctionCodesStr); // 检查用户是否拥有该功能代码 if (!userFunctionCodes.includes(requiredFunctionCode)) { // 用户没有权限,隐藏按钮 el.style.display = 'none'; } } catch (error) { console.error('解析用户功能代码失败:', error); // 解析失败时隐藏按钮 el.style.display = 'none'; } }, // 在绑定元素的父节点更新时调用 updated(el, binding) { // 获取按钮需要的功能代码 const requiredFunctionCode = binding.value; // 从localStorage中获取当前用户的功能代码列表 const userFunctionCodesStr = localStorage.getItem('userFunctionCodes'); if (!userFunctionCodesStr) { // 如果没有功能代码列表,隐藏按钮 el.style.display = 'none'; return; } try { const userFunctionCodes = JSON.parse(userFunctionCodesStr); // 检查用户是否拥有该功能代码 if (!userFunctionCodes.includes(requiredFunctionCode)) { // 用户没有权限,隐藏按钮 el.style.display = 'none'; } else { // 用户有权限,显示按钮 el.style.display = ''; } } catch (error) { console.error('解析用户功能代码失败:', error); // 解析失败时隐藏按钮 el.style.display = 'none'; } } }; export default permissionDirective; ``` #### 10.3 在Vue组件中使用权限指令 ```vue ``` #### 10.4 引航计划功能代码示例 根据常见命名规范,引航计划相关的功能代码可能如下: | 按钮 | 功能代码 | 说明 | |------|----------|------| | 展开 | pilotplan:expand | 展开/收起查询条件 | | 查询 | pilotplan:query | 查询引航计划列表 | | 重置 | pilotplan:reset | 重置查询条件 | | 导入 | pilotplan:import | 导入引航计划 | | 导出 | pilotplan:export | 导出引航计划Excel | | 取消(导入) | pilotplan:import:cancel | 取消导入操作 | | 解析数据 | pilotplan:import:parse | 解析导入数据 | | 取消(预览) | pilotplan:import:preview:cancel | 取消预览 | | 确认导入 | pilotplan:import:preview:confirm | 确认导入数据 | **注意**:以上功能代码是根据常见命名规范推测的,实际功能代码需要从数据库Sys_FunctionCode表中查询确认。 ### 关键点说明 1. **Token自动刷新**:TokenRefreshManager在token即将过期前10分钟自动刷新,避免用户需要重新登录 2. **权限自动更新**:token刷新成功后自动调用PermissionManager更新userFunctionCodes,确保权限信息最新 3. **指令响应式**:v-permission指令在mounted和updated时都会检查权限,确保权限更新后按钮显示状态正确 4. **功能代码规范**:使用模块:操作:子操作的命名规范,便于管理和理解 5. **安全第一**:默认隐藏按钮,只有明确有权限时才显示,避免权限泄露 6. **数据库确认**:实际功能代码需要从Sys_FunctionCode表查询确认,不要随意猜测