# 前端技术设计规范 ## 目录 1. [项目概述](#项目概述) 2. [技术栈](#技术栈) 3. [项目架构](#项目架构) 4. [页面结构规范](#页面结构规范) 5. [组件使用规范](#组件使用规范) 6. [业务组件规范](#业务组件规范) 7. [图标使用规范](#图标使用规范) 8. [样式规范](#样式规范) 9. [文件预览规范](#文件预览规范) 10. [路由规范](#路由规范) 11. [状态管理规范](#状态管理规范) 12. [接口处理规范](#接口处理规范) 13. [最佳实践](#最佳实践) ## 项目概述 本项目是一个RMO管理网站(vdano.com/GCP-RMO),采用 Vue 3 + Vite + Element Plus 技术栈构建,提供完整的权限管理、知识库管理、CRM管理、OA审批等功能模块。 ### 设计原则 - **统一性**:所有页面采用相同的布局结构和交互模式 - **简洁性**:基于Element Plus组件库,避免重度定制样式 - **可维护性**:使用composable模式和组件分离提高代码质量 - **可扩展性**:模块化设计,支持功能快速迭代 ## 技术栈 ### 核心框架 - **Vue 3.3.4** - 渐进式JavaScript框架 - **Vite 4.4.9** - 下一代前端构建工具 - **Vue Router 4.2.4** - 官方路由管理器 - **Pinia 2.1.6** - Vue状态管理库 ### UI组件库 - **Element Plus 2.10.4** - 基于Vue 3的组件库 - **@element-plus/icons-vue 2.1.0** - Element Plus图标库 ### 表格组件 - **vxe-table 4.14.1** - 高性能表格组件 - **xe-utils 3.7.6** - 表格工具库 ### 编辑器组件 - **@umoteam/editor 7.0.1** - 富文本编辑器 - **@vueup/vue-quill 1.2.0** - Quill编辑器Vue组件 - **@vue-office/** - Office文档预览组件 ### 工具库 - **@vueuse/core 13.1.0** - Vue组合式API工具集 - **axios 1.5.0** - HTTP客户端 - **crypto-js 4.2.0** - 加密库 - **echarts 5.6.0** - 数据可视化图表库 ### 架构特点 - **分层架构**: API层、组件层、页面层清晰分离 - **模块化设计**: 按业务功能模块组织代码 - **组件化开发**: 可复用的公共组件和业务组件 - **状态管理**: 使用Pinia进行全局状态管理 - **Composable模式**: 使用composable函数实现逻辑复用 - **动态组件**: 支持按需加载和动态组件管理 ## 页面结构规范 ### 1. 页面容器规范 #### 必须使用 PageContainer 包裹 所有页面必须使用 `PageContainer` 组件作为根容器: ```vue ``` ### 2. 标准页面结构 #### 页面布局容器规范 **必须使用 `page-body` 包裹除 header 外的所有内容** 所有使用 `page-header` 的页面,必须使用 `page-body` 容器包裹除 header 外的所有内容区域: ```vue ``` **`page-body` 的作用**: - **高度管理**:自动计算并占据除 `page-header` 外的剩余空间 - **布局容器**:提供统一的 flex 布局容器,支持内部内容的灵活布局 - **响应式支持**:自动适配移动端和桌面端的高度计算 **高度计算逻辑**: - 桌面端:`calc(100vh - 导航栏 - 标签栏 - PageContainer的padding - page-header高度 - page-header的margin-bottom)` - 移动端:使用移动端的导航栏和标签栏高度变量进行计算 - 所有高度值都通过 CSS 变量定义在 `src/assets/styles/index.scss` 中 **CSS 变量定义**(在 `index.scss` 中): ```scss --page-header-height: 50px; // page-header的高度 --page-header-margin-bottom: 16px; // page-header的margin-bottom --page-content-height: calc(100vh - var(--navbar-height) - var(--tabs-height) - var(--page-container-padding)); --page-body-height: calc(var(--page-content-height) - var(--page-header-height) - var(--page-header-margin-bottom)); ``` #### 普通列表页面结构 ```vue ``` #### 标签页布局结构(分页固定在底部) **使用场景**:当页面使用 `el-tabs` 标签页,且需要将分页固定在底部、内容区域可滚动时: ```vue ``` **布局原理**: 1. **`page-body`**:提供固定高度的 flex 容器 2. **`el-tabs`**:设置为 `flex: 1`,占据全部高度 3. **`el-tab-pane`**:设置为 `display: flex; flex-direction: column`,垂直排列子元素 4. **搜索区域**:`flex-shrink: 0`,固定在顶部 5. **内容区域**:`flex: 1` + `overflow-y: auto`,占据剩余空间并可滚动 6. **分页区域**:`flex-shrink: 0` + `margin-top: auto`,自动推到底部 **关键样式说明**: - `margin-top: auto`:在 flex 容器中,会将元素推到底部 - `flex: 1`:占据剩余空间 - `overflow-y: auto`:内容超出时可滚动 - `min-height: 0`:允许 flex 子元素收缩,配合 `overflow` 使用 #### 一级菜单首页结构(模块Dashboard) **重要说明**:一级菜单的首页(如 `/email`、`/crm`、`/knowledge` 等路由的根页面)必须采用特殊的模块首页布局,不能使用普通列表页面结构: **标准首页结构(带动画效果)** - **推荐使用**: ```vue ``` **简化首页结构(无动画效果)**: ```vue ``` **关键区别**: - **模块首页**:使用 `
` 直接包裹内容区域 - **普通列表页**:使用 `` 包裹内容区域 **样式选择**: - **`module-home-header`**:华丽的动画效果,适合重要的模块首页(推荐) - **`page-header`**:简洁的渐变样式,适合功能型页面或简化的首页 **类名对照表**: | 用途 | 类名 | 说明 | |------|------|------| | 模块首页容器 | `module-dashboard-container` | 统一的模块首页容器 | | 动画头部 | `module-home-header` | 带背景动画的头部样式 | | 简化头部 | `page-header` | 简洁的渐变头部样式 | | 模块图片 | `module-image` | 模块banner图片容器 | | 文字内容 | `header-text` | 头部文字内容区域 | | 状态指示器 | `status-indicator` | 系统状态显示 | **应用场景**: - **模块首页**:如 `/email`、`/crm`、`/knowledge` 等一级菜单的入口页面 - **普通列表页**:如 `/email/management`、`/crm/customer`、`/knowledge/repository` 等功能页面 **识别规则**: 1. **路由层级**:一级菜单的根路由页面(如 `/email/index/Dashboard.vue`) 2. **页面性质**:展示模块概览、统计数据、功能入口的首页 3. **文件位置**:通常位于 `views/模块名/index/Dashboard.vue` 或 `views/模块名/Dashboard.vue` 4. **菜单结构**:在左侧菜单中作为一级菜单的主页面 **样式文件优化**: - ✅ **统一封装**:所有首页样式都在 `src/assets/styles/common.scss` 中定义 - ✅ **完整动画**:包含流星漂移、浮动形状、呼吸状态指示器等动画效果 - ✅ **响应式设计**:自动适配移动端和桌面端 - ✅ **模块特定样式**:各模块只需要在自己的文件中添加特定样式 - ✅ **性能优化**:避免重复代码,减少CSS体积 **使用指南**: 1. **导入样式**:在Vue文件中使用 `@import '@/assets/styles/common.scss'` 2. **统一结构**:使用标准的模板结构和类名 3. **内容区域**:使用 `.module-content` 包裹所有模块内容 4. **首页布局**:在 `.module-content` 内使用 `.dashboard-layout` 进行布局 5. **模块特定样式**:在模块的style中添加特定的内容样式 6. **图片资源**:将模块banner图片放在 `src/assets/images/模块名/banner.png` **正确的首页布局结构**: ```vue ``` **层级说明**: - `module-dashboard-container`:模块首页的根容器 - `module-home-header`:带动画效果的模块头部 - `module-content`:模块内容区域(所有模块页面的标准容器) - `dashboard-layout`:首页专用的布局容器(只在Dashboard页面使用) - `dashboard-row-1/2`:首页布局的行容器 **动画效果**: - `meteor-drift`:流星漂移动画,3个不同延迟的流星在XY平面内移动 - `float`:装饰形状的旋转和缩放动画(**已优化:移除上下浮动**) - `pulse`:状态指示器的呼吸灯效果 - **交互动画**:使用阴影、亮度、边框等效果替代Y轴位移 - 所有动画都经过性能优化,使用硬件加速 **设计原则**: - ❌ **禁止使用** `translateY` 进行上下浮动动画 - ✅ **推荐使用** 阴影变化、颜色过渡、缩放、旋转等效果 - ✅ **保持稳定** 所有元素位置固定,避免页面跳动 - ✅ **流畅体验** 使用平滑的视觉反馈替代位移动画 ### 动画设计规范 **推荐的悬停效果**: ```scss // ✅ 推荐:使用阴影和亮度变化 &:hover { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); filter: brightness(1.05); border-color: #primary-color; } // ✅ 推荐:使用缩放和旋转 &:hover { transform: scale(1.05) rotate(2deg); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); } // ❌ 禁止:使用Y轴位移 &:hover { transform: translateY(-5px); // 不要使用这种效果 } ``` **装饰性动画原则**: - 旋转动画:`transform: rotate(0deg to 360deg)` - 缩放动画:`transform: scale(0.9 to 1.1)` - 透明度变化:`opacity: 0.3 to 0.8` - 颜色渐变:`background`, `border-color`, `color` 过渡 - 禁止位移:避免 `translateX`, `translateY` 造成的位置跳动 ### 快捷操作组件封装 **快捷操作区域**已在 `common.scss` 中统一封装,支持自适应布局和多种样式: ```vue ``` **自适应特性**: - **最小宽度控制**:每个操作项最小宽度 `120px` - **自动换行**:使用 `grid-template-columns: repeat(auto-fit, minmax(120px, 1fr))` - **响应式布局**: - 桌面端:最多8列 - 平板端:自动适应 - 手机端:最少2列,最小宽度100px **预设颜色系统**: | 类名 | 颜色 | 图标渐变 | 悬停背景 | |------|------|----------|----------| | `color-0` | 蓝色 | `#6366f1 → #4f46e5` | 紫蓝色浅色 | | `color-1` | 绿色 | `#10b981 → #059669` | 绿色浅色 | | `color-2` | 橙色 | `#f59e0b → #d97706` | 橙色浅色 | | `color-3` | 红色 | `#ef4444 → #dc2626` | 红色浅色 | | `color-4` | 紫色 | `#8b5cf6 → #7c3aed` | 紫色浅色 | | `color-5` | 青色 | `#06b6d4 → #0891b2` | 青色浅色 | | `color-6` | 粉色 | `#ec4899 → #db2777` | 粉色浅色 | | `color-7` | 草绿色 | `#84cc16 → #65a30d` | 草绿色浅色 | | `color-8` | 灰色 | `#6b7280 → #4b5563` | 灰色浅色 | | `color-9` | 深橙色 | `#f97316 → #ea580c` | 深橙色浅色 | **使用规则**: - **顺序分配**:按照操作项的顺序依次使用 `color-0` 到 `color-9` - **循环使用**:超过10个操作时,重新从 `color-0` 开始循环 - **主要操作**:仍可使用 `primary-action` 类来突出显示最重要的操作 - **视觉一致**:所有颜色都经过精心调配,确保视觉和谐 **布局规则**: 1. **网格优先**:优先显示更多列,到达最小宽度后自动换行 2. **均匀分布**:使用 `1fr` 确保每项宽度相等 3. **最小保证**:极小屏幕下确保至少显示2列 4. **最大限制**:大屏幕下限制最大8列,避免过度拉伸 ## 组件使用规范 ### 1. 表格组件使用规范 #### 使用场景 - **数据列表展示**: 用户列表、文件列表、订单列表等 - **数据筛选排序**: 需要分页、搜索、排序的数据表格 - **复杂数据操作**: 批量操作、行内编辑、树形数据等 #### 技术选型 **必须使用 vxe-table 作为表格组件** ```vue ``` #### 表格配置规范 ```javascript // 标准分页配置 const pagerConfig = { currentPage: 1, pageSize: 20, total: 0, layouts: ['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total'] } ``` ### 2. 按钮组件使用规范 #### 按钮类型和使用场景 ```vue 确认 取消 删除 新增 ``` ### 3. 表单组件使用规范 ```vue ``` #### 下拉框组件使用规范 **必须支持搜索功能** 所有下拉框(`el-select`)组件必须添加 `filterable` 属性,支持用户搜索过滤选项,提升用户体验: ```vue ``` **可输入可选择的场景** 对于需要支持用户手动输入新值的场景(如下拉框没有对应选项时),应添加 `allow-create` 和 `default-first-option` 属性: ```vue ``` **属性说明** | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | `filterable` | Boolean | ✅ 是 | 是否可搜索,所有下拉框必须添加此属性 | | `allow-create` | Boolean | ⚠️ 可选 | 是否允许用户创建新条目,适用于需要手动输入的场景 | | `default-first-option` | Boolean | ⚠️ 可选 | 在输入框按下回车,选择第一个匹配项,配合 `allow-create` 使用 | **使用场景** 1. **标准下拉框**(选项固定,只需搜索): ```vue ``` 2. **可输入下拉框**(选项不固定,需要手动输入): ```vue ``` 3. **多选下拉框**(支持搜索和创建): ```vue ``` **Placeholder 规范** - **仅搜索**:`placeholder="请选择或搜索XXX"` - **搜索+输入**:`placeholder="请选择或输入XXX"` **注意事项** 1. ✅ **必须添加**:所有下拉框必须添加 `filterable` 属性 2. ✅ **用户体验**:搜索功能可以快速定位选项,特别是选项较多时 3. ✅ **一致性**:保持所有下拉框的交互方式一致 4. ⚠️ **性能考虑**:选项数量超过 100 条时,建议使用远程搜索(`remote` 属性) 5. ⚠️ **数据验证**:使用 `allow-create` 时,需要在表单验证中处理用户输入的新值 ### 4. 卡片组件使用规范 ```vue ``` ### 5. 骨架屏组件使用规范 #### 使用场景 - **数据加载中**: 列表、表格、卡片等数据加载时显示 - **内容占位**: 避免页面空白,提升用户体验 - **异步操作**: API调用、文件上传等异步操作期间 #### 标准用法 ```vue ``` #### 骨架屏配置 ```vue ``` #### 样式规范 ```scss // 骨架屏容器样式 .skeleton-container { padding: 16px; // 与内容区域保持一致的padding // 自定义骨架屏样式 :deep(.el-skeleton__item) { background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%); background-size: 400% 100%; animation: loading 1.4s ease infinite; } } @keyframes loading { 0% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } ``` #### 使用原则 1. **一致性**: 骨架屏行数应与实际内容行数相近 2. **动画效果**: 使用 `animated` 属性提供更好的视觉反馈 3. **适当间距**: 骨架屏容器padding应与实际内容保持一致 4. **状态管理**: 配合loading状态正确显示和隐藏 5. **性能考虑**: 避免在骨架屏上添加过多复杂样式 ### 6. 通知详情组件使用规范 #### 使用场景 - **通知详情**: 显示通知的完整内容、标题和时间 - **弹框展示**: 在弹框中展示通知详情 - **工作首页**: 工作台首页的通知详情展示 - **消息中心**: 消息提醒页面的详情展示 #### 标准用法 ```vue ``` #### 在弹框中使用 ```vue ``` #### 组件属性 | 属性名 | 类型 | 必填 | 默认值 | 说明 | |--------|------|------|--------|------| | notification | Object | 是 | {} | 通知对象,包含title、content、displayTime等属性 | #### 样式特性 - **响应式设计**: 支持移动端和桌面端适配 - **内容滚动**: 长内容自动滚动显示 - **HTML支持**: 支持富文本内容显示 - **统一样式**: 使用项目统一的设计令牌 #### 使用原则 1. **组件复用**: 在多个页面中复用该组件 2. **数据完整**: 确保传入的notification对象包含必要属性 3. **样式统一**: 使用组件内置样式,避免外部覆盖 4. **内容安全**: 使用v-html时注意内容安全性 ### 7. 分页组件使用规范 #### 使用场景 - **数据列表**: 表格、列表等大量数据的分页展示 - **内容浏览**: 文章、图片等内容的分页浏览 - **数据筛选**: 配合搜索和筛选功能进行数据分页 #### 标准用法 ```vue ``` #### 分页配置选项 ```vue ``` #### 布局选项 | 布局值 | 说明 | 使用场景 | |--------|------|----------| | `prev, pager, next` | 上一页 + 页码 + 下一页 | 简洁列表页面 | | `total, prev, pager, next` | 总数 + 上一页 + 页码 + 下一页 | 需要显示总数的页面 | | `sizes, prev, pager, next` | 每页条数 + 上一页 + 页码 + 下一页 | 需要调整每页条数的页面 | | `total, sizes, prev, pager, next, jumper` | 完整分页功能 | 复杂的数据管理页面 | #### 样式规范 ```scss // 分页区域样式 .pagination-section { display: flex; justify-content: center; margin-top: 16px; padding: 12px 0; border-top: 1px solid var(--customer-board-color); } // 响应式设计 @media (max-width: 768px) { .pagination-section { // 移动端可以调整间距 margin-top: 12px; padding: 8px 0; } } ``` #### 使用原则 1. **简洁优先**: 优先使用 `prev, pager, next` 布局,保持界面简洁 2. **居中显示**: 分页组件应在页面中居中显示 3. **状态管理**: 正确绑定 `current-page` 和 `total` 数据 4. **事件处理**: 实现 `@current-change` 事件处理分页切换 5. **条件显示**: 只在有数据时显示分页组件 `v-if="total > 0"` 6. **默认样式**: 使用Element Plus默认样式,避免过度自定义 ### 5. 统计卡片组件规范 #### 统计卡片标准结构 项目已在 `src/assets/styles/common.scss` 中封装了统一的统计卡片样式,支持多种布局和主题: ```vue ``` #### 布局类型 | 类名 | 布局 | 响应式断点 | 使用场景 | |------|------|------------|----------| | `grid-4` | 4列布局 | 1200px→2列, 768px→1列 | 完整的统计面板 | | `grid-3` | 3列布局 | 900px→2列, 768px→1列 | 知识库风格布局 | | `grid-2` | 2列布局 | 768px→1列 | 简化的统计展示 | #### 主题颜色 | 主题类名 | 颜色系 | 渐变色 | 适用场景 | |----------|--------|--------|----------| | `theme-blue` | 蓝色 | `#3b82f6 → #1d4ed8` | 主要数据、消息类 | | `theme-red` | 红色 | `#ef4444 → #dc2626` | 紧急、警告类 | | `theme-orange` | 橙色 | `#f59e0b → #d97706` | 待处理、提醒类 | | `theme-green` | 绿色 | `#10b981 → #059669` | 完成、成功类 | | `theme-purple` | 紫色 | `#a855f7 → #7c3aed` | 特殊、高级功能 | | `theme-cyan` | 青色 | `#06b6d4 → #0891b2` | 活动、记录类 | | `theme-pink` | 粉色 | `#ec4899 → #db2777` | 收藏、喜欢类 | | `theme-lime` | 草绿 | `#84cc16 → #65a30d` | 新增、增长类 | #### CountUp数字动画组件 统计卡片配合使用CountUp组件实现数字滚动动画效果: ```vue ``` #### 完整示例 ```vue ``` #### 设计原则 1. **视觉一致性**:所有统计卡片使用相同的尺寸和样式 2. **颜色语义化**:根据数据类型选择合适的主题颜色 3. **纯展示功能**:统计卡片仅用于数据展示,不支持点击交互 4. **动画增强**:使用CountUp组件提升用户体验 5. **响应式设计**:自动适配不同屏幕尺寸 6. **悬浮反馈**:支持轻微的阴影变化,但不包含Y轴移动效果 ## 业务组件规范 ### 1. 组件文件组织结构 #### 业务模块组件结构 ``` views/ ├── module-name/ # 业务模块 │ ├── stores/ # 模块状态管理 │ │ └── useModuleStore.js │ ├── components/ # 模块内组件 │ │ ├── ComponentA.vue │ │ └── ComponentB.vue │ ├── composables/ # 业务逻辑函数 │ │ ├── useModuleData.js │ │ └── useModuleOperations.js │ ├── utils/ # 模块工具函数 │ │ └── moduleUtils.js │ ├── api/ # 模块API │ │ └── moduleApi.js │ ├── Dashboard.vue # 模块首页 │ └── index.vue # 模块入口 ``` ### 2. Composable 设计模式 #### 推荐使用 Composable 分离业务逻辑 ```javascript // composables/useTableOperations.js export function useTableOperations() { const loading = ref(false) const tableData = ref([]) const loadData = async () => { loading.value = true try { // 数据加载逻辑 } finally { loading.value = false } } return { loading, tableData, loadData } } ``` ## 图标使用规范 ### 必须使用 Element Plus 图标库 **基础操作图标** ```javascript import { // 基础操作 Plus, // 新增 Edit, // 编辑 Delete, // 删除 Search, // 搜索 Refresh, // 刷新 View, // 查看 Setting, // 设置 // 文件操作 Download, // 下载 Upload, // 上传 Document, // 文档 Folder, // 文件夹 FolderOpened, // 打开的文件夹 // 导航和布局 Home, // 首页 ArrowLeft, // 左箭头 ArrowRight, // 右箭头 ArrowUp, // 上箭头 ArrowDown, // 下箭头 // 状态指示 Check, // 成功/确认 Close, // 关闭/失败 Warning, // 警告 Info, // 信息 Success, // 成功 Error, // 错误 // 用户和权限 User, // 用户 UserFilled, // 用户(填充) Lock, // 锁定 Unlock, // 解锁 // 通信和分享 Message, // 消息 Phone, // 电话 Email, // 邮件 Share, // 分享 Link, // 链接 // 媒体类型 Picture, // 图片 Video, // 视频 Audio, // 音频 // 业务功能 Management, // 管理 DataBoard, // 数据看板 Grid, // 网格 List, // 列表 Calendar, // 日历 Clock, // 时钟 Location, // 位置 // 交互反馈 Star, // 星标 StarFilled, // 星标(填充) Heart, // 喜欢 HeartFilled, // 喜欢(填充) // 圆形图标变体 CircleCheck, // 圆形确认 CircleClose, // 圆形关闭 CirclePlus, // 圆形加号 // 更多操作 More, // 更多 MoreFilled, // 更多(填充) // 展开收起 Expand, // 展开 Fold, // 收起 // 其他常用 Filter, // 筛选 Sort, // 排序 Export, // 导出 Import, // 导入 Copy, // 复制 Files, // 文件集合 Histogram, // 柱状图 PieChart, // 饼图 TrendCharts // 趋势图 } from '@element-plus/icons-vue' ``` ### 图标使用原则 1. **统一性**:同一功能在不同页面必须使用相同图标 2. **语义性**:图标含义要与功能匹配 3. **一致性**:图标大小和颜色保持项目统一风格 ### 常用图标映射 | 功能 | 图标 | 使用场景 | |------|------|----------| | 新增 | Plus | 新建数据、添加项目 | | 编辑 | Edit | 修改数据、编辑内容 | | 删除 | Delete | 删除数据、移除项目 | | 搜索 | Search | 搜索框、查询功能 | | 刷新 | Refresh | 重新加载数据 | | 下载 | Download | 文件下载、数据导出 | | 上传 | Upload | 文件上传、导入数据 | | 查看 | View | 查看详情、预览 | | 设置 | Setting | 配置、参数设置 | | 首页 | Home | 返回首页、主页导航 | ## 样式规范 ### 1. 样式架构 项目采用分层样式架构: - `src/assets/styles/index.scss` - 设计令牌和全局变量 - `src/assets/styles/common.scss` - 公共页面样式 ### 2. 间距规范 #### 统一Padding标准 为确保界面的一致性和视觉平衡,项目采用统一的padding规范: **默认padding值:16px** ```scss // 标准间距规范 .default-padding { padding: 16px; // 默认内边距16px } // 卡片组件标准间距 .dashboard-card { :deep(.el-card__body) { padding: 16px !important; // 卡片内容区域16px } .card-header { padding: 16px 16px 12px; // 卡片头部:水平16px,底部12px } } // 统计卡片间距 .stat-card { padding: 16px; // 统计卡片16px } ``` #### 特殊情况说明 以下情况可根据设计需求调整: | 场景 | Padding值 | 说明 | |------|-----------|------| | **密集信息展示** | `12px` | 需要显示更多内容时 | | **强调内容区域** | `20px` | 需要突出显示的重要内容 | | **移动端适配** | `12px` | 小屏幕下的紧凑布局 | | **表单输入区域** | `16px` | 保持与默认值一致 | | **列表项内容** | `12px - 16px` | 根据列表密度调整 | #### 间距层级系统 ```scss // 间距变量定义 :root { --padding-xs: 8px; // 极小间距 --padding-sm: 12px; // 小间距 --padding-md: 16px; // 默认间距(推荐) --padding-lg: 20px; // 大间距 --padding-xl: 24px; // 超大间距 } // 使用示例 .content-area { padding: var(--padding-md); // 使用默认16px } .compact-list { padding: var(--padding-sm); // 使用紧凑12px } ``` ### 3. 全局样式规范 #### 整体颜色风格规范 **品牌主色调** 项目采用红色作为品牌主色调,配合白色、黑色和蓝色系形成统一的视觉风格: ```scss // 品牌主色定义(在 index.scss 中) --brand-primary: #D70001; // 主红色 (R215 G0 B1) --brand-primary-rgb: 215, 0, 1; // RGB值,用于rgba --brand-primary-dark: #B8000D; // 深红色,用于hover等状态 --brand-primary-light: #FF4D4D; // 浅红色 --brand-primary-lighter: #FF6B6B; // 更浅的红色 --brand-text-default: #333333; // 默认文字颜色(黑色) --brand-text-active: #ffffff; // 选中状态文字颜色(白色) ``` **布局组件颜色规范** **1. 左侧边栏(LeftSidebar)** - **背景色**:`#D70001`(品牌红色) - **默认文字/图标**:`#ffffff`(白色) - **选中状态**: - 背景:`rgba(255, 255, 255, 0.85)`(白色半透明,85%不透明度) - 文字/图标:`#333333`(黑色) - 圆角:`8px` - **Hover状态**: - 背景:`rgba(255, 255, 255, 0.35)`(白色半透明,35%不透明度) - 文字/图标:`#ffffff`(白色) - 圆角:`8px` - **底部信息项**(租户、分辨率、版本): - 背景:`rgba(255, 255, 255, 0.8)`(白色半透明,80%不透明度) - 文字/图标:`#333333`(黑色) - 圆角:`24px` - Hover:`rgba(255, 255, 255, 0.9)`(90%不透明度) - **分割线**:`rgba(255, 255, 255, 0.5)`(白色半透明,50%不透明度) ```scss .sidebar-container { background: #D70001; .el-menu-item, .el-sub-menu__title { color: #ffffff; &:hover { background-color: rgba(255, 255, 255, 0.35); color: #ffffff; } &.is-active { background-color: rgba(255, 255, 255, 0.85); color: #333333; } } .sidebar-footer { border-top: 1px solid rgba(255, 255, 255, 0.5); background: #D70001; .tenant-item, .footer-item { background: rgba(255, 255, 255, 0.8); color: #333333; border-radius: 24px; &:hover { background: rgba(255, 255, 255, 0.9); } } } } ``` **2. 顶部菜单(TopHeader)** - **背景色**:`#D70001`(品牌红色) - **默认文字/图标**:`#ffffff`(白色) - **一级菜单项**: - 默认:透明背景,`#ffffff`文字 - 选中状态: - 背景:`rgba(255, 255, 255, 0.85)`(白色半透明,85%不透明度) - 文字/图标:`#333333`(黑色) - 圆角:`32px` - 字体大小:`16px`,`font-weight: 500` - Hover状态: - 背景:`rgba(255, 255, 255, 0.35)`(白色半透明,35%不透明度) - 文字/图标:`#ffffff`(白色) - 圆角:`32px` - **工具栏项**(全屏、页面缓存): - 背景:`rgba(255, 255, 255, 0.15)`(白色半透明,15%不透明度) - 图标:`#ffffff`(白色) - Hover:`rgba(255, 255, 255, 0.25)`(25%不透明度) - **公司信息项**: - 背景:`rgba(255, 255, 255, 0.8)`(白色半透明,80%不透明度) - 文字/图标:`#333333`(黑色) - 圆角:`24px` - Hover:`rgba(215, 0, 1, 0.1)`(红色半透明) - **用户信息项**: - 背景:`rgba(255, 255, 255, 0.15)`(白色半透明,15%不透明度) - 文字/图标:`#ffffff`(白色) - Hover:`rgba(255, 255, 255, 0.25)`(25%不透明度) ```scss .header-container { background: #D70001; .primary-menu .menu-item { color: #ffffff; &:hover { background: rgba(255, 255, 255, 0.35); color: #ffffff; border-radius: 32px; } &.active { background: rgba(255, 255, 255, 0.85); color: #333333; border-radius: 32px; font-weight: 500; } } .company-item { background: rgba(255, 255, 255, 0.8); color: #333333; border-radius: 24px; &:hover { background: rgba(215, 0, 1, 0.1); } } .tool-item { background: rgba(255, 255, 255, 0.15); color: #ffffff; &:hover { background: rgba(255, 255, 255, 0.25); } } } ``` **3. 页面头部(Page Header)** - **背景**:红色到黄色的渐变(从左到右) ```scss background: linear-gradient(to right, rgba(255, 200, 200, 0.9) 0%, // 起始:浅红色 rgba(255, 210, 190, 0.8) 70%, // 中间:浅红橙色 rgba(255, 220, 200, 0.75) 100% // 结束:浅橙黄色 ); ``` - **文字颜色**:`#333333`(黑色) - **边框**:`rgba(255, 200, 150, 0.3)`(红黄色边框) - **阴影**:`rgba(255, 180, 120, 0.15)`(红黄色阴影) - **不规则圆形装饰**: - `&::before`:`rgba(255, 140, 140, 0.4)`(红色径向渐变) - `&::after`:`rgba(255, 160, 130, 0.35)`(红橙色径向渐变) ```scss .page-header, .module-home-header, .personal-homepage-header { background: linear-gradient(to right, rgba(255, 200, 200, 0.9) 0%, rgba(255, 210, 190, 0.8) 70%, rgba(255, 220, 200, 0.75) 100% ); color: #333333; border: 1px solid rgba(255, 200, 150, 0.3); box-shadow: 0 2px 8px rgba(255, 180, 120, 0.15); &::before { background: radial-gradient(circle, rgba(255, 140, 140, 0.4), transparent); } &::after { background: radial-gradient(circle, rgba(255, 160, 130, 0.35), transparent); } } ``` **4. 页面容器(PageContainer)** - **背景**:蓝色系渐变 ```scss background: linear-gradient(135deg, rgba(49, 171, 218, 0.06) 0%, // 起始:中等蓝色 rgba(87, 190, 216, 0.05) 33%, // 中间:浅蓝色/青色 rgba(49, 171, 218, 0.04) 66%, // 中间:中等蓝色 rgba(35, 115, 149, 0.03) 100% // 结束:深蓝色 ); ``` **5. 页面内容区域(page-body)** - **作用**:包裹除 `page-header` 外的所有页面内容,提供统一的高度管理和布局容器 - **高度计算**:使用 CSS 变量自动计算,占据除 `page-header` 外的剩余空间 - **布局特性**:提供 flex 布局容器,支持内部内容的灵活布局 ```scss // page-body 样式定义(在 common.scss 中) .page-body { height: var(--page-body-height); display: flex; flex-direction: column; overflow: hidden; min-height: 0; } ``` **CSS 变量定义**(在 `index.scss` 中): ```scss --page-header-height: 50px; // page-header的高度 --page-header-margin-bottom: 16px; // page-header的margin-bottom --page-content-height: calc(100vh - var(--navbar-height) - var(--tabs-height) - var(--page-container-padding)); --page-body-height: calc(var(--page-content-height) - var(--page-header-height) - var(--page-header-margin-bottom)); ``` **使用规范**: - ✅ **必须使用**:所有使用 `page-header` 的页面都必须使用 `page-body` 包裹内容 - ✅ **位置**:紧跟在 `page-header` 之后 - ✅ **内容**:包裹所有页面内容(卡片、表格、标签页等) - ❌ **禁止**:不要在 `page-body` 内再嵌套 `page-body` **6. 标签页(AppTabs)** - **默认标签**: - 背景:透明 - 文字:`#666666`(灰色) - **激活标签**: - 背景:`rgba(215, 0, 1, 0.1)`(红色半透明,10%不透明度) - 文字:`#D70001`(品牌红色) - **Hover状态**: - 背景:`rgba(0, 0, 0, 0.06)`(黑色半透明,6%不透明度) - 文字:`#333333`(黑色) ```scss .tab-item { color: #666666; &:hover { background: rgba(0, 0, 0, 0.06); color: #333333; } &.active-tab { background: rgba(215, 0, 1, 0.1); color: #D70001; } } ``` **颜色使用原则** 1. **主色调**:红色(`#D70001`)用于主要背景和强调色 2. **对比色**:白色(`#ffffff`)用于红色背景上的文字和图标 3. **选中状态**:白色半透明背景(`rgba(255, 255, 255, 0.85)`)+ 黑色文字(`#333333`) 4. **Hover状态**:白色半透明背景(`rgba(255, 255, 255, 0.35)`)+ 白色文字 5. **信息项**:白色半透明背景(`rgba(255, 255, 255, 0.8)`)+ 黑色文字,圆角 `24px` 6. **页面头部**:红色到黄色渐变,营造温暖感 7. **页面容器**:蓝色系渐变,与红色形成对比,保持清新感 **禁止使用的颜色** - ❌ 硬编码的颜色值(应使用CSS变量或统一的颜色定义) - ❌ 与品牌主色不协调的颜色(如紫色、绿色等,除非特殊场景) - ❌ 过于鲜艳或刺眼的颜色组合 #### 页面头部样式规范 **统一使用红色到黄色渐变背景** 所有页面头部(`.page-header`、`.module-home-header`、`.personal-homepage-header`)统一使用红色到黄色的渐变背景,确保视觉一致性: ```scss // 标准渐变背景配置(从左到右) background: linear-gradient(to right, rgba(255, 200, 200, 0.9) 0%, // 起始:浅红色,90%不透明度 rgba(255, 210, 190, 0.8) 70%, // 中间:浅红橙色,80%不透明度 rgba(255, 220, 200, 0.75) 100% // 结束:浅橙黄色,75%不透明度 ); // 边框和阴影 border: 1px solid rgba(255, 200, 150, 0.3); // 红黄色边框 box-shadow: 0 2px 8px rgba(255, 180, 120, 0.15); // 红黄色阴影 // 不规则圆形装饰 &::before { content: ''; position: absolute; width: 150px; height: 150px; border-radius: 50%; background: radial-gradient(circle, rgba(255, 140, 140, 0.4), transparent); top: -75px; left: -75px; z-index: 0; pointer-events: none; } &::after { content: ''; position: absolute; width: 130px; height: 130px; border-radius: 50%; background: radial-gradient(circle, rgba(255, 160, 130, 0.35), transparent); bottom: -65px; right: -65px; z-index: 0; pointer-events: none; } ``` **颜色规范**: - **主色调**:红色系渐变(`rgba(255, 200, 200, ...)` → `rgba(255, 220, 200, ...)`) - **装饰色**:`rgba(255, 140, 140, 0.4)`(红色径向渐变)和 `rgba(255, 160, 130, 0.35)`(红橙色径向渐变) - **文字颜色**:`#333333`(黑色) **应用场景**: - `.page-header` - 普通列表页头部 - `.module-home-header` - 模块首页头部(不规则圆形装饰尺寸更大) - `.personal-homepage-header` - 工作台首页头部 **注意事项**: 1. 所有 header 必须使用相同的渐变配置 2. 内容区域需要设置 `z-index: 1` 或更高,确保显示在背景装饰之上 3. 不规则圆形装饰使用 `pointer-events: none` 避免影响交互 4. 渐变方向为 `to right`(从左到右),不使用角度渐变避免波浪效果 #### 使用 common.scss 统一样式 项目已在 `src/assets/styles/common.scss` 中定义了页面头部样式,所有页面应使用统一的样式类: **页面布局类**: - `.page-header` - 页面头部容器 - `.page-body` - 页面内容区域(包裹除header外的所有内容) - `.header-content` - 头部内容区域 - `.header-left` - 头部左侧区域 - `.page-title` - 页面标题 - `.page-description` - 页面描述 - `.header-actions` - 头部操作区域 - `.add-btn` - 特殊的新增按钮样式 #### CSS 变量和设计令牌 项目已在 `src/assets/styles/index.scss` 中定义了完整的设计令牌系统: - **颜色系统**: `--primary-color`, `--success-color` 等 - **间距系统**: `--spacing-small`, `--spacing-medium` 等 - **高度系统**: `--full-content-height`, `--card-content-height` 等 - **阴影系统**: `--shadow-light`, `--shadow-medium` 等 - **Z-Index层级**: `--z-index-header`, `--z-index-modal` 等 ### 3. 边框颜色规范 #### 统一边框颜色使用 为确保界面的一致性和品牌统一性,项目采用统一的边框颜色规范: **标准边框颜色:`var(--customer-board-color)`** ```scss // 标准边框样式 .standard-border { border: 1px solid var(--customer-board-color); } // 卡片边框 .card-container { border: 1px solid var(--customer-board-color); border-radius: 8px; } // 分割线边框 .divider-line { border-bottom: 1px solid var(--customer-board-color); } // 输入框边框 .input-field { border: 1px solid var(--customer-board-color); border-radius: 4px; } ``` #### 边框使用场景 | 场景 | 边框样式 | 说明 | |------|----------|------| | **容器边框** | `1px solid var(--customer-board-color)` | 卡片、面板、弹窗等容器 | | **分割线** | `1px solid var(--customer-board-color)` | 列表项、内容区域之间的分割 | | **输入框** | `1px solid var(--customer-board-color)` | 表单输入框、选择器等 | | **表格边框** | `1px solid var(--customer-board-color)` | 表格单元格、表头等 | | **悬浮状态** | `1px solid var(--customer-board-color)` | 按钮、链接等悬浮时的边框 | #### 禁止使用的边框颜色 - ❌ 硬编码的颜色值:`#e4e7ed`, `#f1f5f9`, `#e5e7eb` 等 - ❌ 直接使用 Element Plus 默认边框色 - ❌ 使用不统一的颜色变量 #### 正确示例 ```scss // ✅ 正确:使用统一的边框颜色变量 .email-list { border-bottom: 1px solid var(--customer-board-color); } // ✅ 正确:使用统一的边框颜色变量 .sidebar-section { border-bottom: 1px solid var(--customer-board-color); } // ✅ 正确:使用统一的边框颜色变量 .dialog-content { border: 1px solid var(--customer-board-color); } ``` #### 错误示例 ```scss // ❌ 错误:使用硬编码颜色值 .email-list { border-bottom: 1px solid #e4e7ed; } // ❌ 错误:使用不统一的颜色变量 .sidebar-section { border-bottom: 1px solid #f1f5f9; } ``` ### 4. 组件样式规范 #### Element Plus 组件样式增强 项目已对 Element Plus 组件进行了样式增强: - **卡片组件**: 统一圆角和阴影 - **抽屉组件**: 标准化头部和底部样式 - **对话框组件**: 统一边框和间距 - **滚动条**: 自定义滚动条样式 #### 页面内容布局类 - `.main-card` - 主要内容卡片 - `.search-section` - 搜索区域 - `.table-section` - 表格区域 - `.stats-section` - 统计区域 - `.feature-section` - 功能区域 - `.pagination-section` - 分页区域(配合 flex 布局可实现固定在底部) ## 文件预览规范 ### 使用 FileViewer 组件 **统一使用 FileViewer 组件处理文件预览** ```vue ``` ### 支持的文件类型 - **Office文档**: DOC, DOCX, XLS, XLSX, PPT, PPTX - **PDF文档**: PDF - **图片文件**: JPG, PNG, GIF, BMP, SVG - **文本文件**: TXT, MD, JSON, XML - **视频文件**: MP4, AVI, MOV - **音频文件**: MP3, WAV, OGG - **在线文档**: 富文本内容 ### FileViewer 组件特性 - 自动识别文件类型 - 支持在线预览 - 统一的预览界面 - 不支持预览的文件显示下载提示 ## 路由规范 ### 1. 路由配置规范 #### 路由命名规范 ```javascript // 路由命名遵循以下规则: // 1. 使用 kebab-case 格式 // 2. 层级结构清晰 // 3. 名称具有语义性 const routes = [ { path: '/crm', name: 'Crm', component: Layout, meta: { title: 'CRM', icon: 'UserFilled', permission: 'crm' }, children: [ { path: 'customer', name: 'CrmCustomer', component: () => import('@/views/crm/CustomerInfo.vue'), meta: { title: '客户信息', icon: 'User', permission: 'crm:customer', cache: true } } ] } ] ``` #### 路由 Meta 信息规范 ```javascript // Meta 字段标准定义 meta: { title: '页面标题', // 必填:页面标题 icon: 'ElementIcon', // 可选:菜单图标 permission: 'module:action', // 可选:权限标识 cache: true, // 可选:是否缓存页面 hidden: false, // 可选:是否隐藏菜单 ``` ### 2. 路由守卫规范 #### 权限检查流程 ```javascript // 统一的路由守卫逻辑 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const permissionStore = usePermissionStore() // 1. 公开页面直接放行 if (PUBLIC_PAGES.has(to.path)) { next() return } // 2. 登录状态检查 if (!userStore.isLoggedIn) { userStore.redirectToLoginWithUrl(to.fullPath, router) return } // 3. 权限检查 if (to.meta.permission && !permissionStore.hasPermission(to.meta.permission)) { next('/403') return } next() }) ``` ### 3. 路由跳转规范 #### 编程式导航 ```javascript // 推荐:使用命名路由 router.push({ name: 'CrmCustomer', params: { id: '123' } }) // 推荐:带查询参数 router.push({ name: 'CrmCustomer', query: { tab: 'projects', status: 'active' } }) // 避免:硬编码路径 // router.push('/crm/customer/123') ``` #### 路由参数处理 ```javascript // 在组件中获取路由参数 const route = useRoute() const router = useRouter() // 获取路径参数 const customerId = route.params.id // 获取查询参数 const activeTab = route.query.tab || 'basic' // 监听路由变化 watch(() => route.params.id, (newId) => { if (newId) { loadCustomerData(newId) } }) ``` ## 状态管理规范 ### 1. Store 组织结构 #### Store 命名和文件组织 ``` # 全局状态管理 stores/ ├── user.js # 用户状态 ├── permission.js # 权限状态 ├── app.js # 应用全局状态 └── utils/ # 状态工具 └── storeHelpers.js # 业务模块状态管理(放在各自模块内) views/ ├── crm/ │ ├── stores/ │ │ └── useCrmStore.js # CRM模块状态 │ ├── components/ │ └── ... ├── knowledge/ │ ├── stores/ │ │ └── useKnowledgeStore.js # 知识库模块状态 │ ├── components/ │ └── ... └── email/ ├── stores/ │ └── useEmailStore.js # 邮件模块状态 ├── components/ └── ... ``` ### 2. Store 设计规范 #### 标准 Store 结构 ```javascript // views/crm/stores/useCrmStore.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' import * as crmApi from '../api/crmApi' export const useCrmStore = defineStore('crm', () => { // ========== 状态定义 ========== const customers = ref([]) const projects = ref([]) const loading = ref(false) const error = ref(null) // ========== 计算属性 ========== const activeCustomers = computed(() => customers.value.filter(customer => customer.status === 'active') ) const totalProjects = computed(() => projects.value.length) // ========== Actions ========== const loadCustomers = async (params = {}) => { loading.value = true error.value = null try { const response = await crmApi.getCustomerPage(params) customers.value = response.records || [] return response } catch (err) { error.value = err.message throw err } finally { loading.value = false } } const addCustomer = async (customerData) => { try { const newCustomer = await crmApi.createCustomer(customerData) customers.value.unshift(newCustomer) return newCustomer } catch (err) { error.value = err.message throw err } } const updateCustomer = async (id, customerData) => { try { const updatedCustomer = await crmApi.updateCustomer(id, customerData) const index = customers.value.findIndex(c => c.id === id) if (index !== -1) { customers.value[index] = updatedCustomer } return updatedCustomer } catch (err) { error.value = err.message throw err } } const removeCustomer = async (id) => { try { await crmApi.deleteCustomer(id) customers.value = customers.value.filter(c => c.id !== id) } catch (err) { error.value = err.message throw err } } // ========== 工具方法 ========== const resetState = () => { customers.value = [] projects.value = [] loading.value = false error.value = null } const findCustomerById = (id) => { return customers.value.find(customer => customer.id === id) } return { // 状态 customers, projects, loading, error, // 计算属性 activeCustomers, totalProjects, // 方法 loadCustomers, addCustomer, updateCustomer, removeCustomer, resetState, findCustomerById } }) ``` ### 3. Store 使用规范 #### 在组件中使用 Store ```javascript // 组件中的标准用法 ``` #### 跨 Store 通信 ```javascript // 在业务模块 Store 中使用全局 Store export const useCrmStore = defineStore('crm', () => { const userStore = useUserStore() // 全局用户状态 const permissionStore = usePermissionStore() // 全局权限状态 const loadCustomers = async () => { // 检查权限 if (!permissionStore.hasPermission('crm:customer:read')) { throw new Error('没有访问权限') } // 使用用户信息 const params = { userId: userStore.userId, tenantId: userStore.currentTenant?.id } // API调用... } }) ``` ### 4. 状态持久化 #### 使用 localStorage 持久化 ```javascript import { defineStore } from 'pinia' export const useAppStore = defineStore('app', () => { // 从 localStorage 恢复状态 const sidebarCollapsed = ref( localStorage.getItem('sidebarCollapsed') === 'true' ) const toggleSidebar = () => { sidebarCollapsed.value = !sidebarCollapsed.value // 持久化到 localStorage localStorage.setItem('sidebarCollapsed', sidebarCollapsed.value.toString()) } return { sidebarCollapsed, toggleSidebar } }) ``` ## 接口处理规范 ### 1. 接口调用规范 #### 基于 http.js 的统一调用 项目已使用 `src/utils/http.js` 进行 Ajax 响应封装,所有接口调用必须使用统一的 request 方法: ```javascript // 正确:使用统一的 http 实例 import request from '@/utils/http' export function getCustomerList(params) { return request({ url: '/kx-pv/crm/customer/page', method: 'post', data: params }) } // 错误:直接使用 axios // import axios from 'axios' // axios.post('/api/customer', data) ``` ### 2. 接口文件组织规范 #### API 文件结构 ``` # 全局API接口 api/ ├── index.js # API 统一导出 ├── user.js # 用户相关接口 ├── permission.js # 权限相关接口 └── wiki/ # Wiki相关接口 ├── comment.js ├── favorite.js └── version.js # 业务模块API接口(放在各自模块内) views/ ├── crm/ │ └── api/ │ └── crmApi.js # CRM模块接口 ├── email/ │ └── api/ │ └── emailApi.js # 邮件模块接口 └── knowledge/ └── api/ └── knowledgeApi.js # 知识库模块接口 ``` #### 接口命名规范 ```javascript // 命名规范:动词 + 资源名 + 操作类型 // CRUD 操作标准命名 export function getCustomerList(params) // 获取列表 export function getCustomerPage(params) // 分页查询 export function getCustomerDetail(id) // 获取详情 export function createCustomer(data) // 创建 export function updateCustomer(id, data) // 更新 export function deleteCustomer(id) // 删除 // 特殊操作命名 export function exportCustomers(params) // 导出 export function importCustomers(file) // 导入 export function batchDeleteCustomers(ids) // 批量删除 export function enableCustomer(id) // 启用 export function disableCustomer(id) // 禁用 ``` ### 3. 接口文档规范 #### 标准接口注释 ```javascript /** * 获取客户分页列表 * @param {Object} params - 查询参数 * @param {number} params.pageNo - 页码(从1开始) * @param {number} params.pageSize - 每页大小 * @param {string} [params.keyword] - 搜索关键词 * @param {string} [params.status] - 客户状态:active|inactive * @param {string} [params.createTimeStart] - 创建时间开始 * @param {string} [params.createTimeEnd] - 创建时间结束 * @returns {Promise} 返回分页数据 * @returns {Object[]} returns.records - 客户列表 * @returns {number} returns.total - 总数量 * @returns {number} returns.pageNo - 当前页码 * @returns {number} returns.pageSize - 每页大小 * * @example * const result = await getCustomerPage({ * pageNo: 1, * pageSize: 20, * keyword: '客户名称', * status: 'active' * }) */ export function getCustomerPage(params) { return request({ url: '/kx-pv/crm/customer/page', method: 'post', data: params }) } ``` ### 4. Mock 数据规范 #### Mock 开关管理 ```javascript // views/email/api/emailApi.js - Mock模式示例 import http from '@/utils/http' import { EMAIL_LIST, createMockResponse } from '@/mock/emailData' // Mock模式开关 const USE_MOCK = process.env.NODE_ENV === 'development' export function getEmailList(params) { if (USE_MOCK) { // Mock 数据处理逻辑 let filteredEmails = [...EMAIL_LIST] // 搜索筛选 if (params.search) { const query = params.search.toLowerCase() filteredEmails = filteredEmails.filter(email => email.subject?.toLowerCase().includes(query) ) } return Promise.resolve(createMockResponse(filteredEmails)) } // 真实接口调用 return http({ url: '/kx-pv/email/list', method: 'post', data: params }) } ``` ### 5. 错误处理规范 #### 统一错误处理模式 ```javascript // 在 composable 中处理接口错误 export function useCustomerOperations() { const loading = ref(false) const error = ref(null) const loadCustomers = async (params) => { loading.value = true error.value = null try { const result = await getCustomerPage(params) return result } catch (err) { error.value = err.message || '获取客户列表失败' ElMessage.error(error.value) throw err } finally { loading.value = false } } return { loading, error, loadCustomers } } ``` #### 标准错误处理 ```javascript // 统一的错误处理模式 export function createCustomer(data) { return request({ url: '/kx-pv/crm/customer/create', method: 'post', data }) } // 在 composable 中处理错误 export function useCustomerOperations() { const createCustomer = async (data) => { try { const result = await createCustomerApi(data) ElMessage.success('创建客户成功') return result } catch (error) { ElMessage.error(error.message || '创建客户失败') throw error } } return { createCustomer } } ``` ### 6. 日期时间处理规范 #### 日期时间格式转换规范 **技术规范说明**: - 前端日期选择器(Element Plus)返回的格式为 `yyyy-MM-dd HH:mm:ss`(本地时间) - 后端需要 ISO 8601 格式 `yyyy-MM-ddTHH:mm:ss+08:00`(东八区时区) - 只处理包含时间部分的日期字符串(`yyyy-MM-dd HH:mm:ss`),纯日期(`yyyy-MM-dd`)不需要转换 - 如果已经是 ISO 格式,检查是否已有 Z 或时区信息,没有则添加 +08:00 #### 统一使用 time.js 工具函数 **必须使用 `formatDateToISO` 函数进行日期时间格式转换** ```javascript // 正确:使用 time.js 中的 formatDateToISO 函数 import { formatDateToISO } from '@/utils/time' export function createActionItem(data) { const processedData = { ...data, dueDate: formatDateToISO(data.dueDate) // 自动转换日期时间格式 } return request({ url: '/kx-pv/action-item/create', method: 'post', data: processedData }) } ``` #### 转换规则 | 输入格式 | 输出格式 | 说明 | |---------|---------|------| | `"2025-12-11 00:00:00"` | `"2025-12-11T00:00:00+08:00"` | 日期时间字符串,转换为 ISO 格式并添加 +08:00 | | `"2025-12-11"` | `"2025-12-11"` | 纯日期,不处理,原样返回 | | `"2025-12-11T00:00:00"` | `"2025-12-11T00:00:00+08:00"` | 已有 T 但无时区,添加 +08:00 | | `"2025-12-11T00:00:00Z"` | `"2025-12-11T00:00:00Z"` | 已有 Z,保持不变 | | `"2025-12-11T00:00:00+08:00"` | `"2025-12-11T00:00:00+08:00"` | 已有时区信息,保持不变 | | `null` 或 `undefined` | `null` | 空值返回 null | #### 使用场景 **1. 创建/更新数据时转换日期时间字段** ```javascript // API 文件中 import { formatDateToISO } from '@/utils/time' export function createTask(data) { const processedData = { ...data, startDate: formatDateToISO(data.startDate), // 日期时间字段需要转换 endDate: formatDateToISO(data.endDate) } return request({ url: '/kx-pv/task/create', method: 'post', data: processedData }) } ``` **2. 查询参数中的日期时间范围** ```javascript export function getTaskPage(params) { const processedParams = { ...params, startDateStart: params.startDateStart ? formatDateToISO(params.startDateStart) : undefined, startDateEnd: params.startDateEnd ? formatDateToISO(params.startDateEnd) : undefined } return request({ url: '/kx-pv/task/page', method: 'post', data: processedParams }) } ``` #### 日期选择器配置 **Element Plus 日期选择器标准配置**: ```vue ``` #### 注意事项 1. **只转换日期时间字段**:只有包含时间部分的日期字符串(`yyyy-MM-dd HH:mm:ss`)才需要转换 2. **纯日期不处理**:纯日期格式(`yyyy-MM-dd`)不需要转换,直接传递给后端 3. **统一使用工具函数**:所有日期时间格式转换必须使用 `time.js` 中的 `formatDateToISO` 函数 4. **API 层统一处理**:在 API 文件中统一进行格式转换,不要在组件中手动转换 5. **空值处理**:空值(`null`、`undefined`、空字符串)返回 `null`,不进行转换 #### 错误示例 ```javascript // ❌ 错误:手动拼接字符串 const dueDate = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+08:00` // ❌ 错误:在组件中转换 const handleSubmit = () => { const data = { ...formData, dueDate: formData.dueDate.replace(' ', 'T') + '+08:00' // 不应该在组件中处理 } createActionItem(data) } // ❌ 错误:转换纯日期 const startDate = formatDateToISO('2025-12-11') // 纯日期不需要转换 ``` #### 正确示例 ```javascript // ✅ 正确:在 API 文件中统一转换 import { formatDateToISO } from '@/utils/time' export function createActionItem(data) { const processedData = { ...data, dueDate: formatDateToISO(data.dueDate) // 日期时间字段转换 } return request({ url: '/kx-pv/action-item/create', method: 'post', data: processedData }) } // ✅ 正确:纯日期不转换 export function createTask(data) { const processedData = { ...data, startDate: data.startDate, // 纯日期,不转换 endDate: data.endDate // 纯日期,不转换 } return request({ url: '/kx-pv/task/create', method: 'post', data: processedData }) } ``` ### 7. 文件上传/下载规范 #### 文件上传规范 **必须使用已封装的阿里云OSS上传组件** ```javascript // 使用项目封装的OSS上传工具 import { uesOssUpload } from '@/utils/uesOssUpload' // 在组件中使用OSS上传 export function useFileUpload() { const uploadRequest = uesOssUpload() const handleUpload = async (file, options = {}) => { try { const result = await uploadRequest.upload(file, { onProgress: (progress) => { console.log(`上传进度: ${progress}%`) }, ...options }) return { fileId: result.fileId, fileName: result.fileName, originalName: file.name, fileSize: file.size, fileType: file.type, filePath: result.filePath } } catch (error) { ElMessage.error('文件上传失败') throw error } } return { handleUpload } } ``` #### 文件下载规范 **使用 FileUtils 工具类统一处理文件下载** ```javascript // utils/fileUtils.js - 添加标准下载方法 import SystemConfig from '@/config/config' import { ElMessage } from 'element-plus' class FileUtils { /** * 下载文件 * @param {string} fileId - 文件ID * @param {string} fileName - 文件名 * @returns {Promise} */ static async downloadFile(fileId, fileName) { if (!fileId) { ElMessage.error('文件ID不能为空') return } if (!fileName) { ElMessage.error('文件名不能为空') return } const downloadUrl = SystemConfig.FILE_DOWNLOAD_BASE_URL + `/${fileId}?fileName=${encodeURIComponent(fileName)}` try { const response = await fetch(downloadUrl) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = fileName document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) ElMessage.success('开始下载文件') } catch (error) { console.error('下载文件失败:', error) ElMessage.error('下载文件失败') throw error } } /** * 下载文件(兼容对象参数) * @param {Object} file - 文件信息对象 * @returns {Promise} */ static async downloadFileByObject(file) { if (!file) { ElMessage.error('文件信息为空') return } const fileId = file.fileId || file.id const fileName = file.originalName || file.fileName || file.name || '文件' return this.downloadFile(fileId, fileName) } } export default FileUtils ``` #### 在组件中使用 ```javascript // 在业务组件中使用 import FileUtils from '@/utils/fileUtils' // 方式1:直接传入fileId和fileName const handleDownload = async () => { try { await FileUtils.downloadFile(fileId, fileName) } catch (error) { console.error('下载失败:', error) } } // 方式2:传入文件对象(兼容现有代码) const handleDownloadByObject = async (file) => { try { await FileUtils.downloadFileByObject(file) } catch (error) { console.error('下载失败:', error) } } ``` ## 最佳实践 ### 1. 组件设计原则 #### 单一职责原则 - 每个组件只负责一个功能领域 - 使用 composable 函数分离业务逻辑 - 避免组件代码超过 500 行 #### 可复用性原则 - 提取公共逻辑到 composable 函数 - 业务组件放在模块的 components 文件夹 - 通用组件放在 src/components 文件夹 #### Composable 模式 ```javascript // 推荐:使用 composable 管理复杂逻辑 export function useFileOperations() { const loading = ref(false) const uploadFile = async (file) => { loading.value = true try { // 上传逻辑 } finally { loading.value = false } } return { loading, uploadFile } } ``` ### 2. 代码质量规范 #### 组件文档注释 ```javascript /** * 文件预览组件 * @description 支持多种文件类型的在线预览 * @author 团队名称 * @date 2024-01-01 */ // Props 定义 const props = defineProps({ file: { type: Object, required: true, default: () => ({}) } }) ``` #### 统一错误处理 ```javascript // 在 composable 中处理错误 export function useApiCall() { const loading = ref(false) const error = ref(null) const execute = async (apiFunction, errorMessage = '操作失败') => { loading.value = true error.value = null try { const result = await apiFunction() return result } catch (err) { error.value = err ElMessage.error(errorMessage) throw err } finally { loading.value = false } } return { loading, error, execute } } ``` ### 3. 性能优化建议 #### 组件懒加载 ```javascript // 推荐:使用动态组件加载器 import { useDynamicLoader } from '@/utils/dynamicComponentLoader' const { getAsyncComponent } = useDynamicLoader() const LazyDialog = getAsyncComponent('CreateDialog') ``` #### 大数据优化 ```vue ``` ## 总结 本设计规范确保了项目的统一性和可维护性,所有开发成员应严格遵循。 ### 核心要求 1. **页面结构统一**: 使用 PageContainer + 标准页面结构 2. **组件库优先**: 基于 Element Plus,避免重度定制 3. **表格统一**: 必须使用 vxe-table 4. **文件预览统一**: 使用 FileViewer 组件 5. **图标规范**: 只使用 Element Plus 图标库 6. **样式统一**: 使用 common.scss 中的全局样式 7. **路由规范**: 统一路由配置和守卫机制 8. **状态管理**: 业务模块内管理,全局状态分离 9. **接口调用**: 基于 http.js 统一处理 10. **业务组件**: 按模块组织,自包含结构 11. **代码质量**: 单一职责、可复用、高性能 12. **骨架屏规范**: 数据加载时使用 el-skeleton 组件 13. **分页规范**: 使用 el-pagination 默认样式,简洁布局 14. **通知详情组件**: 使用 NotificationDetail 组件统一展示通知详情 ### 开发流程 1. **新建页面**: 复制标准页面结构模板 2. **配置路由**: 按照路由规范配置路由和权限 3. **创建模块**: 在业务模块内创建 stores 和 api 目录 4. **开发 Store**: 使用标准 Store 结构管理模块状态 5. **开发 API**: 基于 http.js 创建模块接口调用 6. **添加功能**: 优先使用现有组件和 composable 7. **样式开发**: 使用全局样式类,避免重复定义 8. **图标选择**: 从规范的图标列表中选择 9. **测试验证**: 确保功能正常和性能良好 10. **代码审查**: 确保符合所有规范要求 --- *版本: v2.0* *更新时间: 2024年1月* *维护团队: 前端架构团队*