- 后端:基于 NestJS 的分层架构设计 - 前端:基于 VbenAdmin + Element Plus 的管理系统 - 支持 SaaS + 独立版双架构模式 - 完整的用户权限管理系统 - 系统设置、文件上传、通知等核心功能 - 多租户支持和插件化扩展架构
60 KiB
📊 依赖关系图
``` ┌─────────────────┐ │ App │ ← 业务开发层(用户自定义业务模块) │ (用户业务) │ 电商、CRM、ERP等具体业务逻辑 └─────────────────┘ ↓ ┌─────────────────┐ │ Common │ ← 框架通用服务层(企业级通用功能) │ (框架通用服务) │ 用户管理、权限管理、菜单管理 └─────────────────┘ 文件上传、通知服务、系统设置 ↓ 数据字典、缓存服务、队列服务 ┌─────────────────┐ │ Core │ ← 核心基础设施层(底层基础设施) │ (基础设施) │ 认证核心、数据库核心、验证核心 └─────────────────┘ HTTP核心、缓存核心、队列核心 ↓ ┌─────────────────┐ │ Vendor │ ← 第三方服务适配层 │ (外部集成) │ 存储、支付、通信、云服务适配 └─────────────────┘
┌─────────────────┐ │ Addons │ ← 插件扩展层(可插拔功能模块) │ (插件扩展) │ 扩展框架功能,不影响核心稳定性 └─────────────────┘
## 📚 参考文档
- **Vben Admin 官方文档**: https://doc.vben.pro/
- **项目技术栈**: Vue 3 + TypeScript + Vite + Element Plus
- **UI 组件库**: Element Plus (当前项目使用)
- **表单组件**: Vben Form (支持 Element Plus、Ant Design Vue、Naive UI 等多种 UI 库适配)
---
## 📐 项目规范(代码风格与约定)
本节定义目录结构、导入顺序、命名、方法与引用等统一规范,用于指导日常开发与 Code Review。
### 1) 目录结构规范
src/ ├─ addon/ # 插件扩展层:可插拔功能模块,不影响核心稳定性 ├─ app/ # 应用层:用户业务开发模块 │ ├─ ecommerce/ # 电商业务模块 │ ├─ crm/ # CRM 业务模块 │ └─ erp/ # ERP 业务模块 ├─ common/ # 框架通用服务层:用户、权限、菜单、文件、通知、系统设置等 ├─ config/ # 配置层:集中化配置与校验 ├─ core/ # 基础设施:数据库、缓存、HTTP、认证、日志等核心能力封装 ├─ vendor/ # 第三方适配:支付、存储、短信、云服务等 └─ main.ts # 应用入口
- 依赖方向:App → Common → Core → Vendor;禁止反向依赖与跨层耦合。
- Addon 可依赖 Common/Core,但 App/Common/Core 不得依赖 Addon。
- 模块划分以领域边界为单位,保持高内聚、低耦合。
### 2) 导入顺序与分组
1. Node.js 内置模块
2. 第三方依赖(npm 包)
3. 项目内部模块
4. 父级目录(../)
5. 同级目录(./)
6. 索引文件(index)
示例:
```ts
// 1) Node 内置
import * as fs from 'fs';
import * as path from 'path';
// 2) 外部依赖
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
// 3) 内部模块
import { LoggerService } from '../../core/logger';
// 4) 父级目录
import { BaseController } from '../base';
// 5) 同级目录
import { UserService } from './user.service';
// 6) 索引
import { CreateUserDto } from './dto';
3) 命名规范
- 文件:
- 控制器
*.controller.ts,服务*.service.ts,实体*.entity.ts,DTO*.dto.ts - 接口
*.interface.ts,类型*.type.ts,常量*.constant.ts,配置*.config.ts,模块*.module.ts
- 控制器
- 类:
UserController、UserService、UserEntity、CreateUserDto - 变量/方法:camelCase;布尔值以
is/has/can/should开头;必要时私有方法可用前缀_(可选)
4) 方法与类设计
- 单一职责、短小精悍,每个方法聚焦一个清晰目标
- 依赖注入优先,避免在方法中直接构造依赖
- 显式返回类型,避免 any;公共方法尽量无副作用
- 错误优先返回与早失败(early return),减少嵌套
5) 引用与依赖约束
- 严格遵循分层依赖:App → Common → Core → Vendor
- 禁止跨域(跨子域模块)直接依赖,使用约定接口与适配器
- 避免循环依赖;必要时通过接口、token 或事件解耦
- 推荐在每层提供 index.ts 作为 barrel 导出,统一对外 API
6) 类型、实体与 DTO
- 优先使用 interface,合理使用泛型提升复用
- DTO 与 Entity 分离:DTO 负责入参校验(class-validator),Entity 负责持久化结构
- 禁止直接在控制器接收实体,必须使用 DTO 并开启全局 ValidationPipe(已开启)
7) 错误处理与日志
- 统一抛出框架异常(如 NotFoundException、BadRequestException),或自定义异常族
- 记录错误日志,包含 requestId(CLS 已接入)、上下文与栈信息
- 外部接口与关键链路增加 info 级打点,敏感信息不可入日志
8) 注释规范
- 公共类与复杂方法使用 JSDoc 注释;重要分支与特殊处理写明原因
- 使用 TODO / FIXME 标注技术债与待优化点
9) 代码质量与提交
- Prettier + ESLint 强制统一风格;提交前 lint-staged 自动修复
- Git 提交遵循 Conventional Commits;使用
npm run commit触发交互式提交
10) 模块骨架推荐
feature/
├─ feature.module.ts
├─ feature.controller.ts
├─ feature.service.ts
├─ entities/
│ └─ feature.entity.ts
├─ dto/
│ ├─ create-feature.dto.ts
│ └─ update-feature.dto.ts
└─ feature.repository.ts # 如需自定义仓储
- Common 层提供通用能力(用户、权限、菜单…),App 层仅组合与编排,尽量避免在 App 层重复造轮子。
11) API 约定
- Swagger 注解最小化:
@ApiTags、@ApiOperation、@ApiBearerAuth(如需鉴权) - 错误码与响应体保持一致性(统一响应封装可在 Common 层提供)
12) 性能与安全
- 优先分页与选择性字段;避免 N+1 查询,必要时使用关联加载或查询优化
- 合理使用缓存与索引;异步/队列处理重任务
- 输入校验与输出清洗;开启限流与安全中间件;严禁在日志中打印密钥/密码
以上规范作为默认约束,后续将根据业务与基础设施演进持续完善。
🔒 层级约束(强制)
- 允许依赖:
- App(modules) → App(common) → Core(严格单向)
- Vendor 仅提供第三方适配器,不依赖 App/Core 任何实现
- Addon 可依赖 App(common)/Core,但 App/Core 不得依赖 Addon
- 禁止依赖:
- App(common) → App(modules)(反向依赖)
- Core → App(反向依赖)
- App(common) → Vendor(直接依赖第三方),必须通过 Core 暴露的抽象端口(Port/Token)间接使用 Vendor
- 同层不同域模块严禁相互依赖其内部实现,唯一入口为该域公开的 index.ts 或导出 API
- 访问 Vendor 的约束:
- 第三方 SDK/Client 在 Vendor 层实现具体 Adapter;Core 层定义抽象 Port/Token 并注入;Common 只依赖 Core 的抽象,不直接 import Vendor
- 运行时注册约束:
- 所有外部资源(Redis、OSS、SMTP、SMS 等)统一在 Vendor 模块封装 Provider;由 Core 定义抽象并在应用 root 注册,Common 仅消费抽象
🧭 导入约束执行(建议自动化)
- ESLint 约束(示例片段,后续可合并到 eslint.config.mjs):
// import 方向约束,禁止反向与跨层内部实现依赖
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
// Common 禁止依赖 App、Vendor
{ group: ['@app/*', 'src/app/*', '@vendor/*', 'src/vendor/*'], message: 'Common 层禁止依赖 App/Vendor,请依赖 Core 抽象' },
// Core 禁止依赖 App/Common/Vendor
{ group: ['@app/*', 'src/app/*', '@common/*', 'src/common/*', '@vendor/*', 'src/vendor/*'], message: 'Core 层禁止依赖上层与 Vendor 实现' },
// 任何层禁止 import 同层其他域的内部文件(建议各域仅通过 index.ts 暴露)
{ group: ['**/*/internal/**'], message: '禁止依赖其他域内部实现,请通过其公共 API' },
],
},
],
}
- 路径别名建议(仅规范,不立即修改):
- @app/,@common/,@core/,@vendor/
- 同层跨域访问仅允许 import 其公共 API(index.ts 或 public API 文件)
🧩 层级职责与能力清单
-
Core(核心基础设施)
- 配置系统(ConfigModule + Joi 校验)
- 数据库(TypeORM 基类:BaseEntity/BaseRepository,事务、审计字段)
- 日志(Winston + nest-winston,按日切割,CLS requestId)
- 缓存抽象(Cache Port,默认内存,提供 Redis 端口定义)
- 队列抽象(Bull Port,重试、延时、并发控制)
- HTTP 客户端封装(重试、超时、熔断占位)
- 安全与鉴权抽象(密码策略、加密、加盐、签名、ACL/RBAC 接口)
- 全局管道/过滤器/拦截器(ValidationPipe、异常过滤、响应封装、日志/耗时拦截)
- 限流封装(Throttler)
- 事件总线(EventEmitter 封装)
- 请求上下文(CLS,traceId/requestId)
-
App/Common(通用业务能力,内置功能)
- 账户与组织:User、Dept/Org、Profile
- 认证与授权:Auth(登录/登出/刷新)、JWT、RBAC(Role/Permission/Menu/Route)
- 系统配置:SystemConfig、参数配置、开关项、Settings(Email/SMS/Storage/Payment/Login)
- 字典中心:Dictionary/Enum 管理
- 文件中心:上传、元数据、存储策略(通过 Core 抽象对接 Vendor)
- 通知中心:邮件/短信/站内信(通过 Core 抽象)
- 审计与操作日志:请求追踪、关键操作记录
- 任务调度:定时任务编排(cron)
- 健康检查与监控:/health、/metrics(可选)
- 国际化:i18n(可选)
- 多租户(可选,后续版本)
-
App/Modules(具体业务模块)
- 电商模块:商品管理、订单管理、购物车、支付流程
- CRM 模块:客户管理、销售线索、商机跟进
- ERP 模块:库存管理、采购管理、财务管理
- 其他业务模块:根据具体需求扩展
-
Vendor(第三方适配层)
- Redis 客户端、MySQL 连接适配(由 Core 调用)
- 对象存储:Aliyun OSS / AWS S3 / Qiniu 等适配器
- 邮件:Nodemailer/SES 适配器
- 短信:Aliyun SMS / Tencent SMS 适配器
- 支付:Alipay / WeChat Pay 适配器
- 验证码:图形/短信验证码服务
- 第三方 OAuth:GitHub/WeChat 等
-
Addon(可选插件)
- 雪花 ID / 分布式 ID
- 审批流/流程引擎
- 特性开关/灰度发布
- 代码生成/脚手架
📁 完整目录规范(建议骨架)
src/
├─ addon/
│ ├─ feature-flags/
│ │ ├─ feature-flags.module.ts
│ │ └─ services/
│ ├─ id-generator/
│ │ ├─ id-generator.module.ts
│ │ └─ services/
│ └─ workflow/
│ ├─ workflow.module.ts
│ └─ services/
│
├─ app/
│ ├─ ecommerce/
│ │ ├─ product/
│ │ │ ├─ product.module.ts
│ │ │ ├─ product.controller.ts
│ │ │ ├─ product.service.ts
│ │ │ ├─ entities/
│ │ │ └─ dto/
│ │ ├─ order/
│ │ │ ├─ order.module.ts
│ │ │ ├─ order.controller.ts
│ │ │ ├─ order.service.ts
│ │ │ ├─ entities/
│ │ │ └─ dto/
│ │ └─ cart/
│ │ ├─ cart.module.ts
│ │ ├─ cart.controller.ts
│ │ ├─ cart.service.ts
│ │ ├─ entities/
│ │ └─ dto/
│ ├─ crm/
│ │ ├─ customer/
│ │ ├─ lead/
│ │ └─ opportunity/
│ └─ erp/
│ ├─ inventory/
│ ├─ procurement/
│ └─ finance/
│
├─ common/
│ ├─ auth/
│ │ ├─ auth.module.ts
│ │ ├─ auth.controller.ts
│ │ ├─ auth.service.ts
│ │ ├─ strategies/
│ │ └─ dto/
│ ├─ user/
│ │ ├─ user.module.ts
│ │ ├─ user.controller.ts
│ │ ├─ user.service.ts
│ │ ├─ entities/
│ │ └─ dto/
│ ├─ rbac/
│ │ ├─ rbac.module.ts
│ │ ├─ role.service.ts
│ │ ├─ permission.service.ts
│ │ ├─ menu.service.ts
│ │ ├─ entities/
│ │ └─ dto/
│ ├─ settings/
│ │ ├─ settings.module.ts
│ │ ├─ email/
│ │ ├─ sms/
│ │ ├─ storage/
│ │ ├─ payment/
│ │ └─ login/
│ ├─ dict/
│ ├─ file/
│ ├─ notify/
│ ├─ audit/
│ ├─ schedule/
│ ├─ health/
│ ├─ i18n/
│ └─ shared/
│ ├─ dto/
│ ├─ constants/
│ └─ utils/
│
├─ core/
│ ├─ config/
│ │ ├─ config.module.ts
│ │ └─ schemas/
│ ├─ database/
│ │ ├─ database.module.ts
│ │ ├─ base.entity.ts
│ │ └─ base.repository.ts
│ ├─ logger/
│ │ └─ logger.module.ts
│ ├─ cache/
│ │ ├─ cache.module.ts
│ │ └─ ports/
│ │ └─ cache.port.ts
│ ├─ queue/
│ │ ├─ queue.module.ts
│ │ └─ ports/
│ │ └─ queue.port.ts
│ ├─ http/
│ │ └─ http.module.ts
│ ├─ security/
│ │ ├─ security.module.ts
│ │ ├─ guards/
│ │ └─ strategies/
│ ├─ exception/
│ │ └─ filters/
│ ├─ interceptor/
│ │ ├─ logging.interceptor.ts
│ │ └─ transform.interceptor.ts
│ ├─ validation/
│ │ └─ pipes/
│ └─ context/
│ └─ cls.module.ts
│
├─ vendor/
│ ├─ redis/
│ │ ├─ redis.module.ts
│ │ └─ redis.provider.ts
│ ├─ mailer/
│ │ ├─ mailer.module.ts
│ │ └─ nodemailer.adapter.ts
│ ├─ sms/
│ │ └─ aliyun-sms.adapter.ts
│ ├─ storage/
│ │ ├─ oss.adapter.ts
│ │ └─ s3.adapter.ts
│ ├─ payment/
│ │ ├─ alipay.adapter.ts
│ │ └─ wechatpay.adapter.ts
│ ├─ captcha/
│ │ └─ captcha.adapter.ts
│ └─ http/
│ └─ axios.adapter.ts
│
├─ config/
│ ├─ database.config.ts
│ ├─ redis.config.ts
│ └─ app.config.ts
│
└─ main.ts
🔌 依赖倒置与适配器模式约定
- Core 只定义 Port/Token(接口/抽象)与领域无关的基础能力
- Vendor 负责第三方实现(Adapter),通过 Provider 绑定到 Core 的 Token
- Common 仅通过 Core 暴露的 Token 使用能力,禁止直接引用具体 Adapter
🔔 事件与跨层通信
- 领域事件优先,使用 EventEmitter;禁止同步强耦合调用导致循环依赖
- 对跨系统/异步任务,统一走队列(Bull)或消息(后续可扩展)
⚙️ 配置命名约定(节选)
- LOG_LEVEL、THROTTLE_TTL、THROTTLE_LIMIT
- DB_HOST、DB_PORT、DB_USER、DB_PASS、DB_NAME
- REDIS_HOST、REDIS_PORT、REDIS_DB、REDIS_PASS
- JWT_SECRET、JWT_EXPIRES_IN
🛠️ 待落实的工程化检查(后续可执行)
- ESLint import 方向约束规则落地
- tsconfig 路径别名(@app/@common/@core/@vendor)
- 各层 index.ts 统一导出公共 API
🎨 前端开发规范(Vben Admin)
本项目前端基于 Vben Admin 框架,参考 Niucloud 的业务模式进行开发。前端位于 admin/ 目录,采用 Vue 3 + TypeScript + Vite 技术栈。
参考目录:
- Niucloud 前端参考:
g:\wwjcloud-nestjs\reference\niucloud-php\admin\ - 本项目前端目录:
g:\wwjcloud-nestjs\admin\apps\web-ele\
1) 前端目录结构规范
admin/
├─ apps/
│ └─ web-ele/ # 主应用
│ ├─ src/
│ │ ├─ addon/ # 插件扩展层(参考 Niucloud)
│ │ │ ├─ shop/ # 商城插件
│ │ │ ├─ cms/ # 内容管理插件
│ │ │ └─ marketing/ # 营销插件
│ │ ├─ app/ # 应用业务层(对应后端 app)
│ │ │ ├─ ecommerce/ # 电商模块
│ │ │ │ ├─ product/ # 商品管理
│ │ │ │ ├─ order/ # 订单管理
│ │ │ │ └─ cart/ # 购物车
│ │ │ ├─ crm/ # CRM 模块
│ │ │ │ ├─ customer/ # 客户管理
│ │ │ │ └─ lead/ # 销售线索
│ │ │ └─ erp/ # ERP 模块
│ │ │ ├─ inventory/ # 库存管理
│ │ │ └─ finance/ # 财务管理
│ │ ├─ common/ # 通用业务功能(对应后端 common)
│ │ │ ├─ user/ # 用户管理
│ │ │ │ ├─ api/ # 用户相关 API
│ │ │ │ └─ views/ # 用户页面
│ │ │ ├─ auth/ # 认证授权
│ │ │ │ ├─ api/ # 认证相关 API
│ │ │ │ └─ views/ # 认证页面
│ │ │ ├─ settings/ # 系统设置
│ │ │ │ ├─ api/ # 设置相关 API
│ │ │ │ │ ├─ email.ts
│ │ │ │ │ ├─ login.ts
│ │ │ │ │ ├─ sms.ts
│ │ │ │ │ └─ storage.ts
│ │ │ │ └─ views/ # 设置页面
│ │ │ │ ├─ email/
│ │ │ │ │ └─ index.vue
│ │ │ │ ├─ login/
│ │ │ │ │ └─ index.vue
│ │ │ │ ├─ sms/
│ │ │ │ │ └─ index.vue
│ │ │ │ └─ storage/
│ │ │ │ └─ index.vue
│ │ │ ├─ menu/ # 菜单管理
│ │ │ │ ├─ api/ # 菜单相关 API
│ │ │ │ └─ views/ # 菜单页面
│ │ │ ├─ upload/ # 文件上传
│ │ │ │ ├─ api/ # 上传相关 API
│ │ │ │ └─ views/ # 上传页面
│ │ │ └─ rbac/ # 角色权限管理
│ │ │ ├─ api/ # 权限相关 API
│ │ │ └─ views/ # 权限页面
│ │ ├─ api/ # API 接口层(Vben 规范)
│ │ │ └─ request.ts # 请求客户端配置
│ │ ├─ views/ # 页面视图层(Vben 规范)
│ │ │ └─ dashboard/ # 仪表板
│ │ ├─ router/ # 路由配置(Vben 规范)
│ │ │ └─ routes/
│ │ │ └─ modules/
│ │ │ ├─ settings.ts
│ │ │ └─ auth.ts
│ │ ├─ stores/ # 状态管理(Vben 规范)
│ │ ├─ components/ # 公共组件(Vben 规范)
│ │ ├─ composables/ # 组合式函数(Vben 规范)
│ │ ├─ utils/ # 工具函数(Vben 规范)
│ │ └─ types/ # 类型定义(Vben 规范)
│ ├─ package.json
│ └─ vite.config.ts
├─ packages/ # 共享包(Vben 规范)
└─ docs/ # 文档
2) 前端分层架构
前端采用分层架构设计,参考 Niucloud 业务模式并结合 Vben 框架规范:
┌─────────────────┐
│ Addon │ ← 插件扩展层(参考 Niucloud)
│ (插件扩展) │ 可插拔功能模块,如商城、CMS等
└─────────────────┘
↓
┌─────────────────┐
│ App │ ← 应用业务层(对应后端 app 层)
│ (业务应用) │ 电商、CRM、ERP 等具体业务模块
└─────────────────┘
↓
┌─────────────────┐
│ Common │ ← 通用功能层(对应后端 common 层)
│ (通用功能) │ 用户、认证、设置、菜单、权限等
└─────────────────┘
↓
┌─────────────────┐
│ Views │ ← 视图层(页面组件,Vben 规范)
│ (页面视图) │ 负责用户界面展示和交互
└─────────────────┘
↓
┌─────────────────┐
│ Composables │ ← 逻辑层(组合式函数,Vben 规范)
│ (业务逻辑) │ 封装业务逻辑和状态管理
└─────────────────┘
↓
┌─────────────────┐
│ API │ ← 接口层(API 调用,Vben 规范)
│ (数据接口) │ 封装后端接口调用
└─────────────────┘
↓
┌─────────────────┐
│ Utils │ ← 工具层(通用工具,Vben 规范)
│ (工具函数) │ 提供通用工具和辅助函数
└─────────────────┘
2.1 分层架构详细说明
- 插件扩展层 (addon/): 可插拔功能模块,参考 Niucloud 插件架构
- 应用业务层 (app/): 对应后端 app 层,具体业务模块
- 电商模块 (app/ecommerce/): 商品、订单、购物车等
- CRM 模块 (app/crm/): 客户管理、销售线索等
- ERP 模块 (app/erp/): 库存、财务等
- 通用功能层 (common/): 对应后端 common 层,框架通用功能
- 用户管理 (common/user/): 用户相关功能
- 认证授权 (common/auth/): 登录、权限等
- 系统设置 (common/settings/): 邮件、短信、存储等配置
- 菜单管理 (common/menu/): 菜单配置
- 文件上传 (common/upload/): 文件处理
- 角色权限 (common/rbac/): RBAC 权限管理
- API 接口层 (api/): 统一管理后端接口调用(Vben 规范)
- 页面视图层 (views/): 业务页面组件(Vben 规范)
- 路由配置层 (router/): 页面路由管理(Vben 规范)
- 状态管理层 (stores/): 全局状态管理(Vben 规范)
- 组件层 (components/): 可复用组件(Vben 规范)
- 工具层 (utils/): 通用工具函数(Vben 规范)
3) API 接口层规范
3.1 接口文件组织
- 按业务模块划分:
api/settings/、api/auth/、api/user/等 - 每个模块包含:接口函数、类型定义、响应处理
- 统一使用
requestClient进行 HTTP 请求
3.2 接口命名约定
// 获取数据:get{Module}Api 或 get{Module}{Action}Api
export const getEmailSettingsApi = () => requestClient.get<EmailSettingsVo>('/settings/email')
// 更新数据:update{Module}Api 或 update{Module}{Action}Api
export const updateEmailSettingsApi = (data: UpdateEmailSettingsDto) =>
requestClient.put<EmailSettingsVo>('/settings/email', data)
// 创建数据:create{Module}Api
export const createUserApi = (data: CreateUserDto) =>
requestClient.post<UserVo>('/users', data)
// 删除数据:delete{Module}Api
export const deleteUserApi = (id: string) =>
requestClient.delete(`/users/${id}`)
3.3 类型定义规范
// 响应类型以 Vo 结尾(View Object)
export interface EmailSettingsVo {
host: string
port: number
username: string
password: string
encryption: string
fromAddress: string
fromName: string
}
// 请求类型以 Dto 结尾(Data Transfer Object)
export interface UpdateEmailSettingsDto {
host?: string
port?: number
username?: string
password?: string
encryption?: string
fromAddress?: string
fromName?: string
}
4) 页面组件规范
4.1 页面结构模板(Vben Admin + Element Plus 规范)
<template>
<div class="p-4">
<!-- 页面标题和描述 -->
<div class="mb-4">
<h1 class="text-2xl font-bold mb-2">{{ pageTitle }}</h1>
<p class="text-gray-600">{{ pageDescription }}</p>
</div>
<!-- 主要内容区域 -->
<el-card :header="cardTitle" shadow="hover">
<template #header>
<div class="flex justify-between items-center">
<span>{{ cardTitle }}</span>
<el-button type="primary" @click="handleSave" :loading="loading">
保存
</el-button>
</div>
</template>
<!-- Vben 表单内容 -->
<VbenForm ref="formRef" @submit="handleSubmit" />
</el-card>
<!-- 页面底部操作栏(可选) -->
<div class="flex justify-end gap-2 mt-4">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="loading">
保存
</el-button>
</div>
</div>
</template>
4.2 组件脚本结构(Vben Admin + Element Plus 规范)
<script lang="ts" setup>
// 1. Vue 相关导入
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// 2. Element Plus 组件导入
import { ElCard, ElButton, ElMessage } from 'element-plus'
// 3. Vben 表单组件导入
import { useVbenForm, z } from '#/adapter/form'
// 4. 项目内部导入
import { $t } from '#/locales'
import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email'
import type { EmailSettingsVo, UpdateEmailSettingsDto } from '#/api/settings/email'
// 5. 页面基础信息
const pageTitle = '邮件设置'
const pageDescription = '配置系统邮件发送相关参数'
const cardTitle = '邮件服务器配置'
// 6. 路由和状态
const route = useRoute()
const router = useRouter()
const loading = ref(false)
// 7. Vben 表单配置(适配 Element Plus)
const [EmailForm, emailFormApi] = useVbenForm({
// 表单布局
layout: 'horizontal',
// 表单项通用配置
commonConfig: {
componentProps: {
class: 'w-full',
},
},
// 表单提交处理
handleSubmit: handleFormSubmit,
// 表单结构定义(使用 Element Plus 组件)
schema: [
{
component: 'Input', // 对应 el-input
fieldName: 'host',
label: 'SMTP服务器',
rules: z.string().min(1, '请输入SMTP服务器地址'),
componentProps: {
placeholder: '请输入SMTP服务器地址',
},
},
{
component: 'InputNumber', // 对应 el-input-number
fieldName: 'port',
label: '端口号',
rules: z.number().min(1, '请输入端口号'),
componentProps: {
placeholder: '请输入端口号',
min: 1,
max: 65535,
},
},
{
component: 'Input',
fieldName: 'username',
label: '用户名',
rules: z.string().min(1, '请输入用户名'),
componentProps: {
placeholder: '请输入邮箱用户名',
},
},
{
component: 'InputPassword', // 对应 el-input type="password"
fieldName: 'password',
label: '密码',
rules: z.string().min(1, '请输入密码'),
componentProps: {
placeholder: '请输入邮箱密码或授权码',
showPassword: true,
},
},
],
})
// 8. 方法定义
const fetchData = async () => {
loading.value = true
try {
const data = await getEmailSettingsApi()
emailFormApi.setValues(data)
} catch (error) {
console.error('Failed to fetch email settings:', error)
ElMessage.error('获取邮件设置失败')
} finally {
loading.value = false
}
}
async function handleFormSubmit(values: UpdateEmailSettingsDto) {
loading.value = true
try {
await updateEmailSettingsApi(values)
ElMessage.success('保存成功')
router.back()
} catch (error) {
console.error('Failed to save email settings:', error)
ElMessage.error('保存失败')
} finally {
loading.value = false
}
}
const handleSave = () => {
emailFormApi.submitForm()
}
const handleCancel = () => {
router.back()
}
const handleAction = () => {
// 自定义操作逻辑
}
// 9. 生命周期
onMounted(() => {
fetchData()
})
</script>
5) 路由配置规范(Vben Admin 规范)
5.1 路由文件组织
- 按模块划分:
src/router/routes/modules/settings.ts - 路由配置遵循 Vben 路由规范
- 使用懒加载方式导入组件
- 路由权限通过
meta.authority配置
5.2 路由配置示例
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/settings',
name: 'Settings',
component: '#/layouts/index.vue',
meta: {
title: '系统设置',
icon: 'lucide:settings',
order: 1000,
},
children: [
{
path: '/settings/email',
name: 'EmailSettings',
component: () => import('#/views/settings/email/index.vue'),
meta: {
title: '邮件设置',
icon: 'lucide:mail',
authority: ['admin', 'super'],
},
},
{
path: '/settings/sms',
name: 'SmsSettings',
component: () => import('#/views/settings/sms/index.vue'),
meta: {
title: '短信设置',
icon: 'lucide:message-square',
authority: ['admin', 'super'],
},
},
{
path: '/settings/storage',
name: 'StorageSettings',
component: () => import('#/views/settings/storage/index.vue'),
meta: {
title: '存储设置',
icon: 'lucide:hard-drive',
authority: ['admin', 'super'],
},
},
],
},
]
export default routes
5.3 路由权限配置
- 使用
meta.authority配置页面访问权限 - 支持角色权限:
['admin', 'super'] - 支持权限码:
['settings:email:read', 'settings:email:write'] - 无权限配置表示公开访问
6) 状态管理规范(Vben Admin 规范)
6.1 使用 Pinia 进行状态管理
- 按业务模块划分 Store:
src/stores/modules/ - 优先使用 Composition API 风格
- 合理使用持久化存储
- 遵循 Vben 状态管理模式
6.2 Store 示例
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { EmailSettingsVo } from '#/api/settings/email'
import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email'
export const useSettingsStore = defineStore(
'settings',
() => {
// 状态
const emailSettings = ref<EmailSettingsVo | null>(null)
const smsSettings = ref<any>(null)
const storageSettings = ref<any>(null)
const loading = ref(false)
// 计算属性
const isEmailConfigured = computed(() => {
return emailSettings.value?.host && emailSettings.value?.username
})
const isAnySettingLoading = computed(() => loading.value)
// 方法
const fetchEmailSettings = async () => {
loading.value = true
try {
const data = await getEmailSettingsApi()
emailSettings.value = data
return data
} catch (error) {
console.error('Failed to fetch email settings:', error)
throw error
} finally {
loading.value = false
}
}
const updateEmailSettings = async (settings: EmailSettingsVo) => {
loading.value = true
try {
await updateEmailSettingsApi(settings)
emailSettings.value = settings
return settings
} catch (error) {
console.error('Failed to update email settings:', error)
throw error
} finally {
loading.value = false
}
}
const resetEmailSettings = () => {
emailSettings.value = null
}
return {
// 状态
emailSettings,
smsSettings,
storageSettings,
loading,
// 计算属性
isEmailConfigured,
isAnySettingLoading,
// 方法
fetchEmailSettings,
updateEmailSettings,
resetEmailSettings,
}
},
{
// 持久化配置
persist: {
key: 'settings-store',
storage: localStorage,
paths: ['emailSettings', 'smsSettings', 'storageSettings'],
},
},
)
7) 组件开发规范(Vben Admin 规范)
7.1 组件命名
- 使用 PascalCase 命名
- 组件文件名与组件名保持一致
- 页面组件放在
views/目录 - 公共组件放在
components/目录 - 遵循 Vben 组件命名约定
7.2 组件使用优先级(基于 Vben Admin 官方规范)
组件选择原则:
- Vben 封装组件(最高优先级):如
VbenForm、VbenModal、VbenDrawer、VbenVxeTable等 - 适配器组件(中等优先级):通过
src/adapter/component和src/adapter/form适配的组件 - 原生 UI 库组件(最低优先级):如 Element Plus 的
el-card、el-button等
官方指导原则:
"如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。" —— Vben Admin 官方文档
具体使用指导:
- 表单开发:必须优先使用
useVbenForm和VbenForm,提供完整的表单解决方案,支持动态表单、验证、联动等高级功能 - 模态框:必须优先使用
useVbenModal和VbenModal,支持拖拽、全屏、自动高度等功能,提供统一的交互体验 - 抽屉:必须优先使用
useVbenDrawer和VbenDrawer,提供更好的用户体验和统一的样式 - 表格:优先使用
VbenVxeTable,基于 vxe-table 封装,结合 VbenForm 搜索;简单展示可使用 Element Plus 的ElTable - 基础组件:可根据需求选择 Vben 组件或通过适配器使用 Element Plus 组件
适配器配置要求:
- Element Plus 组件适配:在
src/adapter/component/index.ts中配置组件映射 - 表单适配:在
src/adapter/form.ts中配置表单验证、国际化等 - 适配器处理:v-model 属性映射、国际化、主题适配、验证规则等
- 灵活使用:可根据具体需求选择 Vben 组件、适配器组件或原生组件
7.3 组件结构模板(Vben 组件优先)
<template>
<Page>
<!-- 页面头部 -->
<PageHeader :title="title" :description="description">
<template #extra>
<VbenButton @click="handleReset" :disabled="loading">
重置
</VbenButton>
</template>
</PageHeader>
<!-- 主要内容卡片 -->
<Card title="基本设置">
<VbenForm
ref="formRef"
:schema="formSchema"
@submit="handleSubmit"
/>
<!-- 底部操作栏 -->
<template #footer>
<div class="flex justify-end gap-2">
<VbenButton @click="handleCancel" :disabled="loading">
取消
</VbenButton>
<VbenButton
type="primary"
@click="handleSave"
:loading="loading"
>
保存
</VbenButton>
</div>
</template>
</Card>
</Page>
</template>
<script setup lang="ts">
// 导入
import { ref, onMounted } from 'vue'
import { Page, PageHeader, Card, VbenButton } from '@vben/common-ui'
import { useVbenForm } from '#/adapter/form'
import type { VbenFormSchema } from '#/components/form'
import type { EmailSettingsVo } from '#/api/settings/email'
import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email'
// 接口定义
interface Props {
title?: string
description?: string
disabled?: boolean
}
// Props
const props = withDefaults(defineProps<Props>(), {
title: '邮件设置',
description: '配置系统邮件发送相关参数',
disabled: false
})
// Emits
const emit = defineEmits<{
save: [settings: EmailSettingsVo]
cancel: []
}>()
// 响应式数据
const loading = ref(false)
const formRef = ref()
// 表单配置
const formSchema: VbenFormSchema[] = [
{
fieldName: 'host',
label: 'SMTP服务器',
component: 'Input',
rules: [{ required: true, message: '请输入SMTP服务器地址' }],
},
{
fieldName: 'port',
label: '端口',
component: 'InputNumber',
componentProps: {
min: 1,
max: 65535,
},
rules: [{ required: true, message: '请输入端口号' }],
},
{
fieldName: 'username',
label: '用户名',
component: 'Input',
rules: [{ required: true, message: '请输入用户名' }],
},
{
fieldName: 'password',
label: '密码',
component: 'InputPassword',
rules: [{ required: true, message: '请输入密码' }],
},
]
// 生命周期
onMounted(async () => {
await fetchSettings()
})
// 方法
const fetchSettings = async () => {
loading.value = true
try {
const data = await getEmailSettingsApi()
formRef.value?.setFieldsValue(data)
} catch (error) {
message.error('获取设置失败')
} finally {
loading.value = false
}
}
const handleSubmit = async (values: EmailSettingsVo) => {
loading.value = true
try {
await updateEmailSettingsApi(values)
message.success('保存成功')
emit('save', values)
} catch (error) {
message.error('保存失败')
} finally {
loading.value = false
}
}
const handleSave = () => {
formRef.value?.submit()
}
const handleCancel = () => {
emit('cancel')
}
const handleReset = () => {
formRef.value?.resetFields()
}
</script>
<style scoped>
/* 使用 Tailwind CSS 类名,避免自定义样式 */
</style>
7.3 组件 Props 定义
interface Props {
title?: string
loading?: boolean
data?: Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
title: '',
loading: false,
data: () => ({})
})
7.4 组件事件定义
interface Emits {
save: [data: Record<string, any>]
cancel: []
change: [value: string]
}
const emit = defineEmits<Emits>()
8) 样式规范(Vben Admin 规范)
8.1 样式系统
- 优先使用 Tailwind CSS 工具类
- 使用 CSS Variables 进行主题定制
- 避免编写自定义 CSS,除非必要
- 遵循 Vben 设计系统
8.2 Tailwind CSS 使用
<template>
<!-- 使用 Tailwind 工具类 -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm">
<h2 class="text-lg font-semibold text-gray-900">
邮件设置
</h2>
<Button class="ml-4" type="primary">
保存
</Button>
</div>
<!-- 响应式设计 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card class="p-6">
<!-- 卡片内容 -->
</Card>
</div>
<!-- 状态样式 -->
<Button
:class="[
'px-4 py-2 rounded-md transition-colors',
loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
]"
>
{{ loading ? '保存中...' : '保存' }}
</Button>
</template>
8.3 主题定制
/* 在 tailwind.config.js 中定义主题 */
module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
900: '#111827',
},
},
spacing: {
'18': '4.5rem',
'88': '22rem',
},
},
},
}
8.4 自定义样式(仅在必要时使用)
<style scoped>
/* 仅在 Tailwind 无法满足需求时使用 */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: theme('colors.gray.300');
border-radius: 3px;
}
/* 使用 CSS Variables 保持主题一致性 */
.custom-component {
background-color: var(--vben-color-bg-container);
border: 1px solid var(--vben-color-border);
}
</style>
9) 国际化规范(Vben Admin 规范)
9.1 语言文件组织
src/locales/
├── langs/
│ ├── zh-CN/
│ │ ├── common.json
│ │ ├── settings.json
│ │ ├── validation.json
│ │ └── index.ts
│ ├── en-US/
│ │ ├── common.json
│ │ ├── settings.json
│ │ ├── validation.json
│ │ └── index.ts
│ └── index.ts
├── helper.ts
└── index.ts
9.2 语言文件示例
// src/locales/langs/zh-CN/settings.json
{
"title": "系统设置",
"email": {
"title": "邮件设置",
"description": "配置系统邮件发送相关参数",
"form": {
"host": "SMTP服务器",
"port": "端口",
"username": "用户名",
"password": "密码",
"encryption": "加密方式",
"fromName": "发件人名称",
"fromEmail": "发件人邮箱"
},
"placeholder": {
"host": "请输入SMTP服务器地址",
"port": "请输入端口号",
"username": "请输入用户名",
"password": "请输入密码"
},
"validation": {
"hostRequired": "请输入SMTP服务器地址",
"portRequired": "请输入端口号",
"usernameRequired": "请输入用户名",
"passwordRequired": "请输入密码"
}
},
"sms": {
"title": "短信设置",
"description": "配置短信发送服务商参数",
"form": {
"provider": "服务商",
"accessKey": "Access Key",
"secretKey": "Secret Key",
"signName": "签名"
}
},
"storage": {
"title": "存储设置",
"description": "配置文件存储相关参数",
"form": {
"driver": "存储驱动",
"bucket": "存储桶",
"region": "地域",
"endpoint": "访问域名"
}
}
}
9.3 在组件中使用
<template>
<Page :title="$t('settings.email.title')">
<template #description>
<span>{{ $t('settings.email.description') }}</span>
</template>
<Card :title="$t('settings.email.title')">
<VbenForm
ref="formRef"
:schema="formSchema"
@submit="handleSubmit"
/>
</Card>
</Page>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from '#/hooks/useI18n'
import type { VbenFormSchema } from '#/components/form'
const { t } = useI18n()
// 表单配置使用国际化
const formSchema = computed<VbenFormSchema[]>(() => [
{
fieldName: 'host',
label: t('settings.email.form.host'),
component: 'Input',
componentProps: {
placeholder: t('settings.email.placeholder.host'),
},
rules: [
{
required: true,
message: t('settings.email.validation.hostRequired')
},
],
},
{
fieldName: 'port',
label: t('settings.email.form.port'),
component: 'InputNumber',
componentProps: {
placeholder: t('settings.email.placeholder.port'),
min: 1,
max: 65535,
},
rules: [
{
required: true,
message: t('settings.email.validation.portRequired')
},
],
},
])
// 在方法中使用
const handleSubmit = async (values: any) => {
try {
// 处理提交逻辑
message.success(t('common.saveSuccess'))
} catch (error) {
message.error(t('common.saveFailed'))
}
}
</script>
9.4 国际化配置
// src/locales/index.ts
import { createI18n } from 'vue-i18n'
import { getLocale, setLocale } from './helper'
// 导入语言包
import zhCN from './langs/zh-CN'
import enUS from './langs/en-US'
const messages = {
'zh-CN': zhCN,
'en-US': enUS,
}
export const i18n = createI18n({
legacy: false,
locale: getLocale(),
fallbackLocale: 'zh-CN',
messages,
globalInjection: true,
})
export { setLocale, getLocale }
10) 错误处理规范(Vben Admin 规范)
10.1 API 错误处理
// src/api/request.ts
import { requestClient } from '#/api/request'
import { useAuthStore } from '#/stores/auth'
import { message } from '#/components'
import { $t } from '#/locales'
// 请求拦截器
requestClient.interceptors.request.use(
(config) => {
const authStore = useAuthStore()
const token = authStore.accessToken
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
requestClient.interceptors.response.use(
(response) => {
const { code, data, message: msg } = response.data
// 根据业务状态码处理
if (code === 200 || code === 0) {
return data
} else {
const errorMessage = msg || $t('common.requestFailed')
message.error(errorMessage)
return Promise.reject(new Error(errorMessage))
}
},
(error) => {
const { response } = error
if (response) {
const { status, data } = response
switch (status) {
case 401:
const authStore = useAuthStore()
authStore.logout()
message.error($t('common.tokenExpired'))
break
case 403:
message.error($t('common.noPermission'))
break
case 404:
message.error($t('common.notFound'))
break
case 500:
message.error($t('common.serverError'))
break
default:
message.error(data?.message || $t('common.requestFailed'))
}
} else {
// 网络错误
message.error($t('common.networkError'))
}
return Promise.reject(error)
}
)
10.2 组件错误处理
<script setup lang="ts">
import { ref } from 'vue'
import { message } from '#/components'
import { useI18n } from '#/hooks/useI18n'
import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email'
import type { EmailSettingsVo } from '#/api/settings/email'
const { t } = useI18n()
const loading = ref(false)
const emailSettings = ref<EmailSettingsVo | null>(null)
// 获取设置
const fetchEmailSettings = async () => {
loading.value = true
try {
const data = await getEmailSettingsApi()
emailSettings.value = data
} catch (error) {
console.error('Failed to fetch email settings:', error)
// 错误信息已在拦截器中处理,这里只需记录日志
} finally {
loading.value = false
}
}
// 更新设置
const updateEmailSettings = async (values: EmailSettingsVo) => {
loading.value = true
try {
await updateEmailSettingsApi(values)
emailSettings.value = values
message.success(t('common.saveSuccess'))
} catch (error) {
console.error('Failed to update email settings:', error)
// 具体错误处理
if (error instanceof Error) {
message.error(error.message)
} else {
message.error(t('common.saveFailed'))
}
} finally {
loading.value = false
}
}
// 表单提交错误处理
const handleSubmit = async (values: EmailSettingsVo) => {
try {
await updateEmailSettings(values)
} catch (error) {
// 错误已在 updateEmailSettings 中处理
// 可以在这里添加额外的错误处理逻辑
}
}
</script>
10.3 全局错误处理
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 全局错误处理
app.config.errorHandler = (error, instance, info) => {
console.error('Global error:', error)
console.error('Component instance:', instance)
console.error('Error info:', info)
// 发送错误到监控服务
// reportError(error, { instance, info })
}
// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason)
// reportError(event.reason)
})
app.mount('#app')
11) 性能优化规范(Vben Admin 规范)
11.1 组件懒加载
// 路由懒加载
const routes: RouteRecordRaw[] = [
{
path: '/settings',
name: 'Settings',
component: () => import('#/views/settings/index.vue'),
meta: {
title: '系统设置',
},
},
]
// 组件懒加载
import { defineAsyncComponent } from 'vue'
const AsyncHeavyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: () => import('#/components/Loading.vue'),
errorComponent: () => import('#/components/Error.vue'),
delay: 200,
timeout: 3000,
})
11.2 虚拟滚动
<template>
<VbenVirtualList
:data="largeDataList"
:height="400"
:item-height="50"
:buffer="5"
>
<template #default="{ item, index }">
<div class="flex items-center p-4 border-b">
<span class="text-sm text-gray-600 mr-2">{{ index + 1 }}</span>
<span class="font-medium">{{ item.name }}</span>
</div>
</template>
</VbenVirtualList>
</template>
<script setup lang="ts">
import { VbenVirtualList } from '#/components'
interface ListItem {
id: string
name: string
}
const largeDataList = ref<ListItem[]>([])
// 生成大量数据
const generateLargeData = () => {
const data: ListItem[] = []
for (let i = 0; i < 10000; i++) {
data.push({
id: `item-${i}`,
name: `Item ${i}`,
})
}
return data
}
onMounted(() => {
largeDataList.value = generateLargeData()
})
</script>
11.3 图片优化
<template>
<!-- 图片懒加载 -->
<VbenImage
:src="imageUrl"
:alt="imageAlt"
:lazy="true"
:preview="true"
class="w-full h-48 object-cover rounded-lg"
loading="lazy"
/>
<!-- 响应式图片 -->
<picture>
<source
media="(min-width: 768px)"
:srcset="largeImageUrl"
>
<source
media="(min-width: 480px)"
:srcset="mediumImageUrl"
>
<img
:src="smallImageUrl"
:alt="imageAlt"
class="w-full h-auto"
loading="lazy"
>
</picture>
</template>
<script setup lang="ts">
import { VbenImage } from '#/components'
interface Props {
imageUrl: string
imageAlt: string
largeImageUrl?: string
mediumImageUrl?: string
smallImageUrl?: string
}
const props = defineProps<Props>()
</script>
11.4 状态管理优化
// 使用 computed 缓存计算结果
import { computed, ref } from 'vue'
const expensiveData = ref([])
// 缓存计算结果
const processedData = computed(() => {
return expensiveData.value
.filter(item => item.active)
.map(item => ({
...item,
displayName: `${item.firstName} ${item.lastName}`,
}))
.sort((a, b) => a.displayName.localeCompare(b.displayName))
})
// 使用 shallowRef 优化大对象
import { shallowRef, triggerRef } from 'vue'
const largeObject = shallowRef({
// 大量数据
})
// 更新时手动触发响应
const updateLargeObject = (newData: any) => {
largeObject.value = { ...largeObject.value, ...newData }
triggerRef(largeObject)
}
11.5 网络请求优化
// 请求去重
import { ref } from 'vue'
const requestCache = new Map<string, Promise<any>>()
const cachedRequest = async (url: string, options?: any) => {
const cacheKey = `${url}-${JSON.stringify(options)}`
if (requestCache.has(cacheKey)) {
return requestCache.get(cacheKey)
}
const promise = fetch(url, options).then(res => res.json())
requestCache.set(cacheKey, promise)
// 请求完成后清除缓存
promise.finally(() => {
requestCache.delete(cacheKey)
})
return promise
}
// 请求防抖
import { debounce } from 'lodash-es'
const searchKeyword = ref('')
const searchResults = ref([])
const debouncedSearch = debounce(async (keyword: string) => {
if (!keyword.trim()) {
searchResults.value = []
return
}
try {
const results = await searchApi(keyword)
searchResults.value = results
} catch (error) {
console.error('Search failed:', error)
}
}, 300)
watch(searchKeyword, (newKeyword) => {
debouncedSearch(newKeyword)
})
12) 测试规范(Vben Admin 规范)
12.1 单元测试
// tests/unit/components/EmailSettings.spec.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import EmailSettings from '#/views/settings/email/index.vue'
import { VbenForm } from '#/components/form'
// Mock API
vi.mock('#/api/settings/email', () => ({
getEmailSettingsApi: vi.fn(),
updateEmailSettingsApi: vi.fn(),
}))
describe('EmailSettings', () => {
let wrapper: any
beforeEach(() => {
wrapper = mount(EmailSettings, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
}),
],
components: {
VbenForm,
},
},
})
})
it('renders correctly', () => {
expect(wrapper.find('[data-testid="email-settings-title"]').text())
.toBe('邮件设置')
})
it('displays form fields correctly', () => {
const formFields = wrapper.findAll('.vben-form-item')
expect(formFields.length).toBeGreaterThan(0)
})
it('handles form submission', async () => {
const mockUpdateApi = vi.mocked(updateEmailSettingsApi)
mockUpdateApi.mockResolvedValue({})
const formData = {
host: 'smtp.test.com',
port: 587,
username: 'test@test.com',
password: 'password123',
}
await wrapper.vm.handleSubmit(formData)
expect(mockUpdateApi).toHaveBeenCalledWith(formData)
// 验证 Element Plus 消息提示
expect(ElMessage.success).toHaveBeenCalledWith('保存成功')
})
})
12.2 API 测试
// tests/api/settings.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email'
import { requestClient } from '#/api/request'
import type { EmailSettingsVo } from '#/api/settings/email'
// Mock request client
vi.mock('#/api/request', () => ({
requestClient: {
get: vi.fn(),
put: vi.fn(),
},
}))
describe('Settings API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getEmailSettingsApi', () => {
it('should fetch email settings successfully', async () => {
const mockData: EmailSettingsVo = {
host: 'smtp.example.com',
port: 587,
username: 'test@example.com',
password: 'password123',
encryption: 'tls',
fromName: 'System',
fromEmail: 'noreply@example.com',
}
vi.mocked(requestClient.get).mockResolvedValue(mockData)
const result = await getEmailSettingsApi()
expect(requestClient.get).toHaveBeenCalledWith('/settings/email')
expect(result).toEqual(mockData)
})
})
})
12.3 E2E 测试
// tests/e2e/settings.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Settings Page', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/login')
await page.fill('[data-testid="username-input"]', 'admin')
await page.fill('[data-testid="password-input"]', 'password')
await page.click('[data-testid="login-button"]')
// 等待登录完成
await page.waitForURL('/dashboard')
})
test('should navigate to email settings', async ({ page }) => {
await page.goto('/settings/email')
await expect(page.locator('[data-testid="email-settings-title"]'))
.toHaveText('邮件设置')
})
test('should update email settings', async ({ page }) => {
await page.goto('/settings/email')
// 填写表单(Element Plus 输入框)
await page.fill('[data-testid="host-input"]', 'smtp.test.com')
await page.fill('[data-testid="port-input"]', '587')
// 提交表单(Element Plus 按钮)
await page.click('[data-testid="save-button"]')
// 验证 Element Plus 成功消息
await expect(page.locator('.el-message--success'))
.toHaveText('保存成功')
})
})
12.4 测试配置
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
},
resolve: {
alias: {
'#': resolve(__dirname, './src'),
},
},
})
13) 开发工具配置
13.1 必要的 VSCode 插件
- Vue Language Features (Volar)
- TypeScript Vue Plugin (Volar)
- Tailwind CSS IntelliSense
- ESLint
- Prettier
13.2 代码质量检查
# 代码格式化
pnpm format
# 代码检查
pnpm lint
# 类型检查
pnpm type-check
# 构建检查
pnpm build
14) 与后端协作规范
14.1 接口约定
- 前端接口路径与后端保持一致
- 使用 TypeScript 类型定义确保类型安全
- 统一错误码和响应格式
14.2 数据流约定
// 后端响应格式
interface ApiResponse<T> {
code: number
data: T
message: string
}
// 前端自动解包 data 字段
const data = await getEmailSettingsApi() // 直接返回 EmailSettingsVo
📖 重要开发说明
Vben Admin 官方文档参考
本项目基于 Vben Admin 框架开发,所有前端开发规范均应严格遵循 Vben Admin 官方文档:
- 官方文档地址: https://doc.vben.pro/
- 当前使用版本: Element Plus 适配版本
- 表单组件: 使用 Vben Form,支持多种 UI 库适配
- 组件库: Element Plus(项目已配置)
开发约束
- 禁止自创规范: 所有前端开发必须参考 Vben Admin 官方文档,禁止自创或假设性开发
- 参考项目文档: 可参考项目
docs/目录下的代码规范和示例
组件使用优先级(基于项目实际代码示例)
组件选择原则:
- Element Plus 原生组件(最高优先级):如
ElCard、ElButton、ElTable、ElMessage等 - Vben 封装组件(中等优先级):如
WorkbenchHeader、WorkbenchProject、AnalysisChartCard、Page等业务组件 - 适配器组件(最低优先级):自定义适配器组件,仅在特殊需求时使用
实际使用示例:
基础 UI 组件:直接使用 Element Plus
<template>
<!-- 卡片容器 -->
<ElCard class="mb-5 w-auto">
<template #header>按钮</template>
<!-- 按钮组 -->
<ElSpace>
<ElButton type="primary">Primary</ElButton>
<ElButton type="success">Success</ElButton>
</ElSpace>
</ElCard>
<!-- 表格 -->
<ElTable :data="tableData" stripe>
<ElTable.TableColumn label="测试列1" prop="prop1" />
<ElTable.TableColumn label="测试列2" prop="prop2" />
</ElTable>
</template>
<script setup>
import { ElCard, ElButton, ElSpace, ElTable } from 'element-plus';
</script>
业务组件:使用 Vben 封装组件
<template>
<!-- 页面容器 -->
<Page title="页面标题" description="页面描述">
<!-- 工作台头部 -->
<WorkbenchHeader :avatar="userInfo.avatar">
<template #title>欢迎回来</template>
</WorkbenchHeader>
<!-- 业务组件 -->
<WorkbenchProject :items="projectItems" @click="navTo" />
<AnalysisChartCard title="图表标题">
<CustomChart />
</AnalysisChartCard>
</Page>
</template>
<script setup>
import {
Page,
WorkbenchHeader,
WorkbenchProject,
AnalysisChartCard
} from '@vben/common-ui';
</script>
官方指导原则:
"如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。" —— Vben Admin 官方文档
具体使用指导:
- 基础 UI:优先使用 Element Plus 原生组件(
ElCard、ElButton、ElTable、ElForm等) - 页面布局:使用 Vben 的
Page组件作为页面容器 - 业务组件:使用 Vben 封装的业务组件(
WorkbenchHeader、WorkbenchProject等) - 表单开发:可选择 Element Plus 的
ElForm或 Vben 的表单组件,根据复杂度决定 - 消息提示:直接使用 Element Plus 的
ElMessage、ElNotification - 数据展示:直接使用 Element Plus 的
ElTable、图表组件等
协同开发要求
开发步骤,要前后端一起协同开发,项目的数据库账号、密码、库表均为wwjcloud。前端和后端项目均有规范工具,开发前一定要检查工具是否配置正确,是否有必要的插件,是否正常运行状态。前端开发进行修改框架核心内容,可以参考前端docs目录参考代码规范禁止自创和假设造成,没有规范的去写。
技术栈确认
- 前端框架: Vue 3 + TypeScript + Vite
- UI 组件库: Element Plus
- 表单解决方案: Vben Form(支持 Element Plus 适配)
- 状态管理: Pinia
- 路由: Vue Router
- 构建工具: Vite
- 包管理器: pnpm