2105 lines
60 KiB
Markdown
2105 lines
60 KiB
Markdown
|
|
# 📊 依赖关系图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────┐
|
|||
|
|
│ 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):
|
|||
|
|
```js
|
|||
|
|
// 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 接口命名约定
|
|||
|
|
```typescript
|
|||
|
|
// 获取数据: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 类型定义规范
|
|||
|
|
```typescript
|
|||
|
|
// 响应类型以 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 规范)
|
|||
|
|
```vue
|
|||
|
|
<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 规范)
|
|||
|
|
```vue
|
|||
|
|
<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 路由配置示例
|
|||
|
|
```typescript
|
|||
|
|
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 示例
|
|||
|
|
```typescript
|
|||
|
|
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 官方规范)
|
|||
|
|
|
|||
|
|
**组件选择原则:**
|
|||
|
|
1. **Vben 封装组件**(最高优先级):如 `VbenForm`、`VbenModal`、`VbenDrawer`、`VbenVxeTable` 等
|
|||
|
|
2. **适配器组件**(中等优先级):通过 `src/adapter/component` 和 `src/adapter/form` 适配的组件
|
|||
|
|
3. **原生 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 组件优先)
|
|||
|
|
```vue
|
|||
|
|
<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 定义
|
|||
|
|
```typescript
|
|||
|
|
interface Props {
|
|||
|
|
title?: string
|
|||
|
|
loading?: boolean
|
|||
|
|
data?: Record<string, any>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
title: '',
|
|||
|
|
loading: false,
|
|||
|
|
data: () => ({})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.4 组件事件定义
|
|||
|
|
```typescript
|
|||
|
|
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 使用
|
|||
|
|
```vue
|
|||
|
|
<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 主题定制
|
|||
|
|
```css
|
|||
|
|
/* 在 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 自定义样式(仅在必要时使用)
|
|||
|
|
```vue
|
|||
|
|
<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 语言文件示例
|
|||
|
|
```json
|
|||
|
|
// 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 在组件中使用
|
|||
|
|
```vue
|
|||
|
|
<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 国际化配置
|
|||
|
|
```typescript
|
|||
|
|
// 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 错误处理
|
|||
|
|
```typescript
|
|||
|
|
// 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 组件错误处理
|
|||
|
|
```vue
|
|||
|
|
<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 全局错误处理
|
|||
|
|
```typescript
|
|||
|
|
// 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 组件懒加载
|
|||
|
|
```typescript
|
|||
|
|
// 路由懒加载
|
|||
|
|
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 虚拟滚动
|
|||
|
|
```vue
|
|||
|
|
<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 图片优化
|
|||
|
|
```vue
|
|||
|
|
<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 状态管理优化
|
|||
|
|
```typescript
|
|||
|
|
// 使用 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 网络请求优化
|
|||
|
|
```typescript
|
|||
|
|
// 请求去重
|
|||
|
|
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 单元测试
|
|||
|
|
```typescript
|
|||
|
|
// 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 测试
|
|||
|
|
```typescript
|
|||
|
|
// 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 测试
|
|||
|
|
```typescript
|
|||
|
|
// 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 测试配置
|
|||
|
|
```typescript
|
|||
|
|
// 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 代码质量检查
|
|||
|
|
```bash
|
|||
|
|
# 代码格式化
|
|||
|
|
pnpm format
|
|||
|
|
|
|||
|
|
# 代码检查
|
|||
|
|
pnpm lint
|
|||
|
|
|
|||
|
|
# 类型检查
|
|||
|
|
pnpm type-check
|
|||
|
|
|
|||
|
|
# 构建检查
|
|||
|
|
pnpm build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 14) 与后端协作规范
|
|||
|
|
|
|||
|
|
#### 14.1 接口约定
|
|||
|
|
- 前端接口路径与后端保持一致
|
|||
|
|
- 使用 TypeScript 类型定义确保类型安全
|
|||
|
|
- 统一错误码和响应格式
|
|||
|
|
|
|||
|
|
#### 14.2 数据流约定
|
|||
|
|
```typescript
|
|||
|
|
// 后端响应格式
|
|||
|
|
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(项目已配置)
|
|||
|
|
|
|||
|
|
### 开发约束
|
|||
|
|
|
|||
|
|
1. **禁止自创规范**: 所有前端开发必须参考 Vben Admin 官方文档,禁止自创或假设性开发
|
|||
|
|
2. **参考项目文档**: 可参考项目 `docs/` 目录下的代码规范和示例
|
|||
|
|
|
|||
|
|
#### 组件使用优先级(基于项目实际代码示例)
|
|||
|
|
|
|||
|
|
**组件选择原则:**
|
|||
|
|
1. **Element Plus 原生组件**(最高优先级):如 `ElCard`、`ElButton`、`ElTable`、`ElMessage` 等
|
|||
|
|
2. **Vben 封装组件**(中等优先级):如 `WorkbenchHeader`、`WorkbenchProject`、`AnalysisChartCard`、`Page` 等业务组件
|
|||
|
|
3. **适配器组件**(最低优先级):自定义适配器组件,仅在特殊需求时使用
|
|||
|
|
|
|||
|
|
**实际使用示例:**
|
|||
|
|
|
|||
|
|
**基础 UI 组件**:直接使用 Element Plus
|
|||
|
|
```vue
|
|||
|
|
<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 封装组件
|
|||
|
|
```vue
|
|||
|
|
<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
|