3009 lines
83 KiB
Markdown
3009 lines
83 KiB
Markdown
# 前端技术设计规范
|
||
|
||
## 目录
|
||
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-card、el-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月*
|
||
*维护团队: 前端架构团队* |