RMO-Front/PRD/前端技术设计规范.md

3009 lines
83 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 前端技术设计规范
## 目录
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
<template>
<PageContainer @refresh="handlePageRefresh">
<!-- 页面内容 -->
</PageContainer>
</template>
<script setup>
import PageContainer from '@/components/PageContainer.vue'
const handlePageRefresh = () => {
// 刷新页面数据逻辑
}
</script>
```
### 2. 标准页面结构
#### 页面布局容器规范
**必须使用 `page-body` 包裹除 header 外的所有内容**
所有使用 `page-header` 的页面,必须使用 `page-body` 容器包裹除 header 外的所有内容区域:
```vue
<template>
<PageContainer @refresh="handlePageRefresh">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">
<el-icon class="title-icon"><Document /></el-icon>
页面标题
</h2>
<p class="page-description">页面描述信息</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
</div>
</div>
<!-- 页面内容区域必须使用 page-body -->
<div class="page-body">
<!-- 所有页面内容都在这里 -->
<!-- 可以是 el-cardel-tabs表格等 -->
</div>
</PageContainer>
</template>
```
**`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
<template>
<PageContainer @refresh="handlePageRefresh">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">
<el-icon class="title-icon"><Document /></el-icon>
页面标题
</h2>
<p class="page-description">页面描述信息</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
</div>
</div>
<!-- 页面内容区域 -->
<div class="page-body">
<!-- 主要内容区域 -->
<el-card class="main-card" shadow="never">
<!-- 搜索表单 -->
<div class="search-section">
<!-- 搜索条件 -->
</div>
<!-- 数据表格 -->
<div class="table-section">
<!-- vxe-table 组件 -->
</div>
</el-card>
</div>
</PageContainer>
</template>
```
#### 标签页布局结构(分页固定在底部)
**使用场景**:当页面使用 `el-tabs` 标签页,且需要将分页固定在底部、内容区域可滚动时:
```vue
<template>
<PageContainer @refresh="handlePageRefresh">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">
<el-icon class="title-icon"><Document /></el-icon>
页面标题
</h2>
<p class="page-description">页面描述信息</p>
</div>
<div class="header-actions">
<el-button type="primary" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
</div>
</div>
<!-- 页面内容区域 -->
<div class="page-body">
<!-- 标签页 -->
<el-tabs v-model="activeTab" type="border-card" @tab-change="handleTabChange">
<el-tab-pane label="标签1" name="tab1">
<!-- 搜索区域(固定在顶部) -->
<div class="search-section">
<!-- 搜索条件 -->
</div>
<!-- 内容区域(可滚动) -->
<div class="file-cards-container">
<!-- 卡片列表或其他内容 -->
</div>
<!-- 分页(固定在底部) -->
<div class="pagination-section">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</el-tab-pane>
<el-tab-pane label="标签2" name="tab2">
<!-- 其他标签页内容 -->
</el-tab-pane>
</el-tabs>
</div>
</PageContainer>
</template>
<style lang="scss" scoped>
@import '@/assets/styles/common.scss';
// el-tabs 容器样式
:deep(.el-tabs) {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: hidden;
min-height: 0;
.el-tabs__content {
flex: 1;
overflow: hidden;
min-height: 0;
}
}
// 标签页内容区域样式
:deep(.el-tabs__content) {
padding: 16px;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
// 每个 tab-pane 的内容区域
.el-tab-pane {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
}
// 搜索区域样式
.search-section {
flex-shrink: 0; // 固定在顶部,不收缩
// ... 其他样式
}
// 内容容器样式(可滚动)
.file-cards-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0; // 允许flex子元素收缩
// ... 其他样式
}
// 分页样式
.pagination-section {
flex-shrink: 0; // 固定在底部,不收缩
margin-top: auto; // 自动推到底部
background: white; // 确保背景色,避免滚动时覆盖
// ... 其他样式
}
</style>
```
**布局原理**
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
<template>
<PageContainer @refresh="handlePageRefresh">
<div class="module-dashboard-container">
<!-- 模块首页头部带背景动画 -->
<div class="module-home-header">
<!-- 背景装饰 -->
<div class="header-decoration">
<!-- 流星动画效果 -->
<div class="meteor meteor-1"></div>
<div class="meteor meteor-2"></div>
<div class="meteor meteor-3"></div>
<!-- 装饰形状 -->
<div class="decoration-shape shape-1"></div>
<div class="decoration-shape shape-2"></div>
<div class="decoration-shape shape-3"></div>
</div>
<div class="header-main">
<div class="header-content">
<!-- 左侧模块图标 -->
<div class="module-image">
<img src="@/assets/images/模块名/banner.png" alt="模块名称" />
</div>
<!-- 右侧文字内容 -->
<div class="header-text">
<h1>模块名称</h1>
<p>模块功能的详细描述说明模块的价值和特色功能让用户了解模块的核心价值和使用场景</p>
<div class="status-indicator">
<div class="status-dot"></div>
<span>系统正在为您服务</span>
</div>
</div>
</div>
</div>
</div>
<!-- 模块内容区域 -->
<div class="module-content">
<!-- 统计卡片区域 -->
<div class="stats-section">
<!-- 数据统计卡片 -->
</div>
<!-- 功能入口区域 -->
<div class="feature-section">
<!-- 功能模块卡片 -->
</div>
</div>
</div>
</PageContainer>
</template>
<style lang="scss" scoped>
@import '@/assets/styles/common.scss';
/* 模块特定样式 */
.module-content {
// 在这里添加模块特定的内容样式
}
</style>
```
**简化首页结构(无动画效果)**
```vue
<template>
<PageContainer @refresh="handlePageRefresh">
<!-- 模块首页头部简化版 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h2 class="page-title">
<el-icon class="title-icon"><Management /></el-icon>
模块名称
</h2>
<p class="page-description">模块功能描述</p>
</div>
<div class="header-actions">
<!-- 模块级操作按钮 -->
</div>
</div>
</div>
<!-- 模块内容区域 -->
<div class="module-content">
<!-- 统计卡片区域 -->
<div class="stats-section">
<!-- 数据统计卡片 -->
</div>
<!-- 功能入口区域 -->
<div class="feature-section">
<!-- 功能模块卡片 -->
</div>
</div>
</PageContainer>
</template>
```
**关键区别**
- **模块首页**:使用 `<div class="module-content">` 直接包裹内容区域
- **普通列表页**:使用 `<el-card class="main-card" shadow="never">` 包裹内容区域
**样式选择**
- **`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
<template>
<PageContainer @refresh="handlePageRefresh">
<div class="module-dashboard-container">
<!-- 模块首页头部带动画效果 -->
<div class="module-home-header">
<!-- ... 头部内容 ... -->
</div>
<!-- 模块内容区域 -->
<div class="module-content">
<!-- 首页专用布局容器 -->
<div class="dashboard-layout">
<!-- 第一行智能分析摘要 + 快捷操作 -->
<div class="dashboard-row-1">
<div class="summary-section"><!-- 智能分析摘要 --></div>
<div class="feature-section"><!-- 快捷操作 --></div>
</div>
<!-- 第二行未读内容 + 活动记录 -->
<div class="dashboard-row-2">
<div class="unread-section"><!-- 未读内容 --></div>
<div class="activity-section"><!-- 活动记录 --></div>
</div>
</div>
</div>
</div>
</PageContainer>
</template>
```
**层级说明**
- `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
<template>
<div class="quick-actions-section">
<!-- 头部标题区域 -->
<div class="quick-actions-header">
<h3 class="actions-title">
<el-icon class="title-icon"><Lightning /></el-icon>
快捷操作
</h3>
<span class="actions-extra">6 项服务</span>
</div>
<!-- 自适应网格 -->
<div class="quick-actions-grid">
<!-- 按顺序使用预设颜色 -->
<div class="quick-action-item color-0" @click="handleAction1">
<div class="action-icon">
<el-icon><Edit /></el-icon>
</div>
<div class="action-text">写邮件</div>
</div>
<div class="quick-action-item color-1" @click="handleAction2">
<div class="action-icon">
<el-icon><Folder /></el-icon>
</div>
<div class="action-text">文件管理</div>
</div>
<div class="quick-action-item color-2" @click="handleAction3">
<div class="action-icon">
<el-icon><Setting /></el-icon>
</div>
<div class="action-text">设置</div>
</div>
<div class="quick-action-item color-3" @click="handleAction4">
<div class="action-icon">
<el-icon><DataAnalysis /></el-icon>
</div>
<div class="action-text">数据分析</div>
</div>
<div class="quick-action-item color-4" @click="handleAction5">
<div class="action-icon">
<el-icon><User /></el-icon>
</div>
<div class="action-text">用户管理</div>
</div>
<div class="quick-action-item color-5" @click="handleAction6">
<div class="action-icon">
<el-icon><Bell /></el-icon>
</div>
<div class="action-text">消息通知</div>
</div>
</div>
</div>
</template>
```
**自适应特性**
- **最小宽度控制**:每个操作项最小宽度 `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
<vxe-table
:data="tableData"
:loading="loading"
:pager-config="pagerConfig"
@page-change="handlePageChange"
>
<vxe-column field="name" title="名称" />
<vxe-column field="status" title="状态" />
<vxe-column title="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
</template>
</vxe-column>
</vxe-table>
```
#### 表格配置规范
```javascript
// 标准分页配置
const pagerConfig = {
currentPage: 1,
pageSize: 20,
total: 0,
layouts: ['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total']
}
```
### 2. 按钮组件使用规范
#### 按钮类型和使用场景
```vue
<!-- 主要操作按钮 -->
<el-button type="primary" @click="handleSubmit">
<el-icon><Check /></el-icon>
确认
</el-button>
<!-- 次要操作按钮 -->
<el-button @click="handleCancel">
<el-icon><Close /></el-icon>
取消
</el-button>
<!-- 危险操作按钮 -->
<el-button type="danger" @click="handleDelete">
<el-icon><Delete /></el-icon>
删除
</el-button>
<!-- 图标按钮 -->
<el-button circle size="small" @click="handleRefresh">
<el-icon><Refresh /></el-icon>
</el-button>
<!-- 页面头部特殊按钮使用add-btn类 -->
<el-button type="primary" @click="handleAdd" class="add-btn">
<el-icon><Plus /></el-icon>
新增
</el-button>
```
### 3. 表单组件使用规范
```vue
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeHolder="请输入名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeHolder="请选择状态">
<el-option label="启用" value="enabled" />
<el-option label="禁用" value="disabled" />
</el-select>
</el-form-item>
</el-form>
```
#### 下拉框组件使用规范
**必须支持搜索功能**
所有下拉框(`el-select`)组件必须添加 `filterable` 属性,支持用户搜索过滤选项,提升用户体验:
```vue
<!-- 正确支持搜索的下拉框 -->
<el-form-item label="严重程度" prop="severity">
<el-select
v-model="formData.severity"
placeHolder="请选择或输入严重程度"
style="width: 100%"
filterable
>
<el-option label="非严重" value="非严重" />
<el-option label="严重" value="严重" />
<el-option label="危机生命、致死" value="危机生命、致死" />
</el-select>
</el-form-item>
<!-- ❌ 错误:不支持搜索的下拉框 -->
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeHolder="请选择状态">
<el-option label="启用" value="enabled" />
<el-option label="禁用" value="disabled" />
</el-select>
</el-form-item>
```
**可输入可选择的场景**
对于需要支持用户手动输入新值的场景(如下拉框没有对应选项时),应添加 `allow-create``default-first-option` 属性:
```vue
<!-- 支持搜索和手动输入的下拉框 -->
<el-form-item label="严重程度" prop="severity">
<el-select
v-model="formData.severity"
placeHolder="请选择或输入严重程度"
style="width: 100%"
filterable
allow-create
default-first-option
>
<el-option label="非严重" value="非严重" />
<el-option label="严重" value="严重" />
<el-option label="危机生命、致死" value="危机生命、致死" />
</el-select>
</el-form-item>
```
**属性说明**
| 属性 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `filterable` | Boolean | ✅ 是 | 是否可搜索,所有下拉框必须添加此属性 |
| `allow-create` | Boolean | ⚠️ 可选 | 是否允许用户创建新条目,适用于需要手动输入的场景 |
| `default-first-option` | Boolean | ⚠️ 可选 | 在输入框按下回车,选择第一个匹配项,配合 `allow-create` 使用 |
**使用场景**
1. **标准下拉框**(选项固定,只需搜索):
```vue
<el-select v-model="value" filterable>
<el-option label="选项1" value="1" />
<el-option label="选项2" value="2" />
</el-select>
```
2. **可输入下拉框**(选项不固定,需要手动输入):
```vue
<el-select
v-model="value"
filterable
allow-create
default-first-option
>
<el-option label="选项1" value="1" />
<el-option label="选项2" value="2" />
</el-select>
```
3. **多选下拉框**(支持搜索和创建):
```vue
<el-select
v-model="value"
multiple
filterable
allow-create
default-first-option
>
<el-option label="选项1" value="1" />
<el-option label="选项2" value="2" />
</el-select>
```
**PlaceHolder 规范**
- **仅搜索**`placeHolder="请选择或搜索XXX"`
- **搜索+输入**`placeHolder="请选择或输入XXX"`
**注意事项**
1.**必须添加**:所有下拉框必须添加 `filterable` 属性
2.**用户体验**:搜索功能可以快速定位选项,特别是选项较多时
3.**一致性**:保持所有下拉框的交互方式一致
4. ⚠️ **性能考虑**:选项数量超过 100 条时,建议使用远程搜索(`remote` 属性)
5. ⚠️ **数据验证**:使用 `allow-create` 时,需要在表单验证中处理用户输入的新值
### 4. 卡片组件使用规范
```vue
<!-- 主要内容卡片 -->
<el-card class="main-card" shadow="never">
<!-- 卡片内容 -->
</el-card>
<!-- 功能模块卡片 -->
<el-card class="feature-card" shadow="hover">
<!-- 功能入口内容 -->
</el-card>
```
### 5. 骨架屏组件使用规范
#### 使用场景
- **数据加载中**: 列表、表格、卡片等数据加载时显示
- **内容占位**: 避免页面空白,提升用户体验
- **异步操作**: API调用、文件上传等异步操作期间
#### 标准用法
```vue
<template>
<div class="content-container">
<!-- 骨架屏加载状态 -->
<div v-if="loading" class="skeleton-container">
<el-skeleton :rows="5" animated />
</div>
<!-- 空状态 -->
<el-empty v-else-if="dataList.length === 0" description="暂无数据" />
<!-- 实际内容 -->
<div v-else class="content-list">
<!-- 列表内容 -->
</div>
</div>
</template>
```
#### 骨架屏配置
```vue
<!-- 基础骨架屏 -->
<el-skeleton :rows="3" />
<!-- 带动画效果 -->
<el-skeleton :rows="5" animated />
<!-- 自定义行数 -->
<el-skeleton :rows="8" animated />
<!-- 自定义样式 -->
<el-skeleton :rows="4" animated class="custom-skeleton" />
```
#### 样式规范
```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
<template>
<div class="notification-detail">
<div class="detail-header">
<h3 class="detail-title">{{ notification.title }}</h3>
<div class="detail-time">{{ notification.displayTime }}</div>
</div>
<div class="detail-content">
<div class="content-text" v-html="notification.content"></div>
</div>
</div>
</template>
<script setup>
defineProps({
notification: {
type: Object,
required: true,
default: () => ({})
}
})
</script>
```
#### 在弹框中使用
```vue
<template>
<div>
<!-- 触发弹框的按钮或元素 -->
<el-button @click="showDetail">查看详情</el-button>
</div>
</template>
<script setup>
import { h } from 'vue'
import { ElMessageBox } from 'element-plus'
import NotificationDetail from '@/components/NotificationDetail.vue'
const showDetail = (notification) => {
ElMessageBox({
title: '通知详情',
message: h(NotificationDetail, { notification }),
showCancelButton: false,
confirmButtonText: '确定',
customClass: 'notification-detail-dialog'
})
}
</script>
```
#### 组件属性
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| notification | Object | 是 | {} | 通知对象包含title、content、displayTime等属性 |
#### 样式特性
- **响应式设计**: 支持移动端和桌面端适配
- **内容滚动**: 长内容自动滚动显示
- **HTML支持**: 支持富文本内容显示
- **统一样式**: 使用项目统一的设计令牌
#### 使用原则
1. **组件复用**: 在多个页面中复用该组件
2. **数据完整**: 确保传入的notification对象包含必要属性
3. **样式统一**: 使用组件内置样式,避免外部覆盖
4. **内容安全**: 使用v-html时注意内容安全性
### 7. 分页组件使用规范
#### 使用场景
- **数据列表**: 表格、列表等大量数据的分页展示
- **内容浏览**: 文章、图片等内容的分页浏览
- **数据筛选**: 配合搜索和筛选功能进行数据分页
#### 标准用法
```vue
<template>
<div class="content-container">
<!-- 数据列表 -->
<div class="data-list">
<!-- 列表内容 -->
</div>
<!-- 分页组件 -->
<div class="pagination-section" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const handlePageChange = (page) => {
currentPage.value = page
// 重新加载数据
loadData()
}
</script>
```
#### 分页配置选项
```vue
<!-- 基础分页 -->
<el-pagination layout="prev, pager, next" :total="50" />
<!-- 带页码数量控制 -->
<el-pagination
layout="prev, pager, next"
:total="1000"
:pager-count="11"
/>
<!-- 完整分页功能 -->
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
```
#### 布局选项
| 布局值 | 说明 | 使用场景 |
|--------|------|----------|
| `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
<template>
<!-- 统计卡片区域 -->
<div class="stats-section">
<div class="stats-grid grid-4"> <!-- 4列布局 -->
<div class="stat-card theme-blue" @click="handleClick">
<div class="stat-icon">
<el-icon><Message /></el-icon>
</div>
<div class="stat-content">
<div class="stat-number">
<CountUp :target="statisticsData.count" />
</div>
<div class="stat-label">统计标签</div>
</div>
</div>
<!-- 更多统计卡片... -->
</div>
</div>
</template>
<script setup>
import CountUp from '@/components/CountUp.vue'
</script>
```
#### 布局类型
| 类名 | 布局 | 响应式断点 | 使用场景 |
|------|------|------------|----------|
| `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
<script setup>
import CountUp from '@/components/CountUp.vue'
// CountUp组件参数
const countUpProps = {
target: 150, // 目标数字
duration: 1000, // 动画持续时间(毫秒)
decimals: 0, // 小数位数
delay: 0, // 动画延迟(毫秒)
preset: 'easeOutCubic' // 动画预设
}
</script>
<template>
<div class="stat-number">
<!-- 基础用法 -->
<CountUp :target="statistics.count" />
<!-- 带小数位 -->
<CountUp :target="storage.used" :decimals="1" />
<span class="stat-unit">GB</span>
<!-- 自定义动画 -->
<CountUp
:target="performance.score"
:duration="1500"
:delay="200"
preset="easeOutBounce"
/>
</div>
</template>
```
#### 完整示例
```vue
<template>
<!-- 邮件模块统计示例 -->
<div class="stats-section">
<div class="stats-grid grid-3">
<div class="stat-card theme-blue">
<div class="stat-icon">
<el-icon><Message /></el-icon>
</div>
<div class="stat-content">
<div class="stat-number">
<CountUp :target="emailStats.unreadCount" />
</div>
<div class="stat-label">未读邮件</div>
</div>
</div>
<div class="stat-card theme-green">
<div class="stat-icon">
<el-icon><Calendar /></el-icon>
</div>
<div class="stat-content">
<div class="stat-number">
<CountUp :target="emailStats.todayCount" />
</div>
<div class="stat-label">今日最新邮件</div>
</div>
</div>
<div class="stat-card theme-orange">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-content">
<div class="stat-number">
<CountUp :target="emailStats.accountCount" />
</div>
<div class="stat-label">邮件账户数量</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Message, Calendar, User } from '@element-plus/icons-vue'
import CountUp from '@/components/CountUp.vue'
const emailStats = ref({
unreadCount: 0,
todayCount: 0,
accountCount: 0
})
const loadStatistics = async () => {
// 加载统计数据的逻辑
}
onMounted(() => {
loadStatistics()
})
</script>
```
#### 设计原则
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
<template>
<!-- 文件预览 -->
<FileViewer :file="currentFile" @download="handleDownload" />
</template>
<script setup>
import FileViewer from '@/views/knowledge/repository/browser/FileViewer.vue'
const currentFile = ref(null)
const handleDownload = (file) => {
// 下载逻辑
}
</script>
```
### 支持的文件类型
- **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
// 组件中的标准用法
<script setup>
import { useCrmStore } from './stores/useCrmStore'
import { storeToRefs } from 'pinia'
const crmStore = useCrmStore()
// 响应式引用状态(使用 storeToRefs
const { customers, loading, error } = storeToRefs(crmStore)
// 直接使用方法
const { loadCustomers, addCustomer } = crmStore
// 组件挂载时加载数据
onMounted(() => {
loadCustomers()
})
// 处理错误
watch(error, (newError) => {
if (newError) {
ElMessage.error(newError)
}
})
</script>
```
#### 跨 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<Object>} 返回分页数据
* @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
<template>
<!-- 日期时间选择器 -->
<el-date-picker
v-model="formData.dueDate"
type="datetime"
placeHolder="选择截止时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<!-- 日期选择器纯日期不需要转换 -->
<el-date-picker
v-model="formData.startDate"
type="date"
placeHolder="选择开始日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</template>
```
#### 注意事项
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<void>}
*/
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<void>}
*/
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
<!-- vxe-table 虚拟滚动 -->
<vxe-table
:data="tableData"
:scroll-y="{ enabled: true, gt: 100 }"
:height="var(--card-content-height)"
>
<!-- 表格配置 -->
</vxe-table>
```
## 总结
本设计规范确保了项目的统一性和可维护性,所有开发成员应严格遵循。
### 核心要求
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月*
*维护团队: 前端架构团队*