大量更新
This commit is contained in:
@@ -1,438 +0,0 @@
|
||||
---
|
||||
name: developing-go-gin-gorm wdd-后端开发
|
||||
|
||||
description: >
|
||||
使用 Gin + GORM 生成、编写、修改、评审 production-ready 的 Go 后端代码(Generate & Review Go backend code with Gin/GORM)。
|
||||
强制分层架构 handler → service → dao/repository(避免业务逻辑堆在 handler;DAO/Repo 只做数据访问与查询组装),并统一 API 响应包装
|
||||
consistent response envelope(code/message/data + request_id/trace_id 等可观测字段)。接口风格默认推荐 POST + JSON RequestBody
|
||||
as default(必要时遵循 REST 语义与幂等约定),规范 DTO/VO/DO 命名与字段映射 conventions(入参 DTO、出参 VO、持久化 DO/Model)。
|
||||
代码注释使用中文(Chinese comments for maintainability),时间处理默认 Asia/Shanghai(time zone aware time handling),
|
||||
采用结构化日志 structured logging(携带 request_id/trace_id/user_id/path/latency 等上下文),并遵循 Gin/GORM 工程化最佳实践
|
||||
(transactions, context propagation, error wrapping, pagination, soft delete, optimistic locking when needed)。
|
||||
触发场景 Trigger: Go 后端开发 / Gin Handler 创建 / GORM DAO/Repository 实现 / 代码走查与 Review(refactor suggestions, bug fixes, performance tips)。
|
||||
|
||||
argument-hint: "<动作 action> <目标 target>" 例如/ e.g.:
|
||||
"create user-handler", "review service/order.go", "scaffold api/v1/product", "add repo for table/users", "optimize gorm query"
|
||||
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
|
||||
---
|
||||
|
||||
# Go GIN/GORM 开发规范 Skill
|
||||
|
||||
## 触发条件
|
||||
- 用户请求创建/修改 Go 后端代码
|
||||
- 用户请求代码审查
|
||||
- 用户提及 API 开发、数据库操作、统一响应、日志、时间处理
|
||||
- 用户请求设计 API 接口、DTO 结构
|
||||
|
||||
## 上下文收集
|
||||
|
||||
执行前先收集项目信息:
|
||||
|
||||
!`ls -la go.mod go.sum 2>/dev/null || echo "No go.mod found"`
|
||||
|
||||
!`head -20 go.mod 2>/dev/null || echo ""`
|
||||
|
||||
## $ARGUMENTS 解析
|
||||
|
||||
期望格式:`<action> <target>`
|
||||
|
||||
| action | 说明 |
|
||||
|--------|------|
|
||||
| `create` | 创建新文件(handler/service/dao/dto) |
|
||||
| `review` | 审查现有代码 |
|
||||
| `scaffold` | 生成完整模块骨架 |
|
||||
| `fix` | 修复不符合规范的代码 |
|
||||
|
||||
---
|
||||
|
||||
## Plan 阶段
|
||||
|
||||
### 产物清单(按 action 确定)
|
||||
|
||||
| action | 产物 |
|
||||
|--------|------|
|
||||
| `create handler` | `/api/xxx_handler.go` 或 `/internal/handler/xxx.go` |
|
||||
| `create service` | `/internal/service/xxx_service.go` |
|
||||
| `create dao` | `/internal/dao/xxx_dao.go` |
|
||||
| `create dto` | `/internal/model/dto/xxx_dto.go` |
|
||||
| `scaffold` | 上述全部 + entity |
|
||||
|
||||
### 决策点
|
||||
|
||||
1. **目录风格**:检查项目是用 `/api` 还是 `/internal/handler`
|
||||
2. **模块命名**:从 $ARGUMENTS 提取资源名(如 `user`、`order`)
|
||||
3. **是否已存在**:先 Glob 检查目标文件
|
||||
|
||||
---
|
||||
|
||||
## Execute 阶段
|
||||
|
||||
### Handler 层编写规则
|
||||
|
||||
```
|
||||
1. 仅做:参数解析 → 调用 service → 返回响应
|
||||
2. 禁止:编写业务逻辑、直接操作数据库
|
||||
3. 必须:使用 common.ResponseSuccess / common.ResponseError
|
||||
4. 错误处理:gorm.ErrRecordNotFound → CodeNotFound
|
||||
```
|
||||
|
||||
### Service 层编写规则
|
||||
|
||||
```
|
||||
1. 编排 dao 层完成业务
|
||||
2. 记录关键业务日志(Info 级别)
|
||||
3. 错误包装:fmt.Errorf("xxx: %w", err)
|
||||
4. 业务异常记录 Warning 级别日志
|
||||
```
|
||||
|
||||
### DAO 层编写规则
|
||||
|
||||
```
|
||||
1. 封装所有 GORM 操作
|
||||
2. 禁止在 service 层写 SQL
|
||||
3. 复杂查询用 Raw/Exec
|
||||
4. 善用链式调用,但复杂场景优先原生 SQL
|
||||
```
|
||||
|
||||
### 统一响应格式(强制)
|
||||
|
||||
```go
|
||||
// 成功
|
||||
common.ResponseSuccess(c, data)
|
||||
common.ResponseSuccessWithMessage(c, data, "创建成功")
|
||||
|
||||
// 失败
|
||||
common.ResponseError(c, common.CodeParamError, "参数错误")
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "系统错误", err)
|
||||
```
|
||||
|
||||
错误码定义 → 读取 `reference/error-codes.go`
|
||||
|
||||
### 注释规范(强制中文)
|
||||
|
||||
```go
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *model.User - 用户信息,未找到返回nil
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 设计规范(强制)
|
||||
|
||||
### 核心原则:POST + RequestBody
|
||||
|
||||
```
|
||||
所有 API 优先使用 POST 方法,参数通过 RequestBody 传递
|
||||
避免使用 PathVariables 和 RequestParams
|
||||
```
|
||||
|
||||
### 禁止与推荐
|
||||
|
||||
| 禁止 | 推荐 |
|
||||
|------|------|
|
||||
| `GET /api/projects/{project_id}` | `POST /api/projects/detail` + RequestBody |
|
||||
| `GET /api/users?role=admin&page=1` | `POST /api/users/list` + RequestBody |
|
||||
| URL 中传递敏感信息 | RequestBody 传递所有参数 |
|
||||
|
||||
### API 路径命名规范
|
||||
|
||||
| 操作 | 后缀 | 示例 |
|
||||
|------|------|------|
|
||||
| 列表查询 | `/list` | `POST /api/projects/list` |
|
||||
| 详情查询 | `/detail` | `POST /api/projects/detail` |
|
||||
| 创建 | `/create` | `POST /api/projects/create` |
|
||||
| 更新 | `/update` | `POST /api/projects/update` |
|
||||
| 删除 | `/delete` | `POST /api/projects/delete` |
|
||||
| 同步 | `/sync` | `POST /api/jenkins/organizations/sync` |
|
||||
| 触发 | `/trigger` | `POST /api/builds/trigger` |
|
||||
|
||||
### DTO 命名规范
|
||||
|
||||
| 类型 | 命名格式 | 示例 |
|
||||
|------|----------|------|
|
||||
| 列表请求 | `List{资源}Request` | `ListBuildsRequest` |
|
||||
| 详情请求 | `Get{资源}Request` | `GetBuildRequest` |
|
||||
| 创建请求 | `Create{资源}Request` | `CreateProjectRequest` |
|
||||
| 更新请求 | `Update{资源}Request` | `UpdateProjectRequest` |
|
||||
| 删除请求 | `Delete{资源}Request` | `DeleteProjectRequest` |
|
||||
| 列表响应 | `List{资源}Response` | `ListBuildsResponse` |
|
||||
| 详情响应 | `{资源}DetailResponse` | `BuildDetailResponse` |
|
||||
|
||||
### 通用分页结构
|
||||
|
||||
```go
|
||||
// 请求
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
// 响应
|
||||
type ListResponse struct {
|
||||
List []interface{} `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
```
|
||||
|
||||
### 模块错误码范围
|
||||
|
||||
| 范围 | 模块 |
|
||||
|------|------|
|
||||
| 0 | 成功 |
|
||||
| 1000-1999 | 通用错误 |
|
||||
| 2000-2999 | 用户/权限 |
|
||||
| 3000-3999 | Jenkins |
|
||||
| 4000-4999 | 项目管理 |
|
||||
| 5000-5999 | Exchange-Hub |
|
||||
|
||||
详细规范 → 读取 `reference/api-design-spec.md`
|
||||
|
||||
---
|
||||
|
||||
## 日志规范(强制)
|
||||
|
||||
### 指定框架
|
||||
项目统一使用 `rmdc-common/wdd_log/log_utils.go`
|
||||
|
||||
### 日志级别使用场景
|
||||
|
||||
| 级别 | 使用场景 | 示例 |
|
||||
|------|----------|------|
|
||||
| `Debug` | 开发调试,详细流程、变量值 | `log.Debug(ctx, "查询参数", map[string]interface{}{"userID": id})` |
|
||||
| `Info` | 关键业务节点 | `log.Info(ctx, "用户登录成功", ...)` / `log.Info(ctx, "订单创建成功", ...)` |
|
||||
| `Warning` | 可预期非致命异常,程序可继续 | `log.Warning(ctx, "外部API超时,启用备用方案", ...)` |
|
||||
| `Error` | 严重错误,业务流程中断 | `log.Error(ctx, "数据库连接失败", ...)` 必须记录堆栈 |
|
||||
|
||||
### 日志内容要求
|
||||
```
|
||||
1. 简练、关键
|
||||
2. 必须包含 TraceID、UserID 等追溯信息
|
||||
3. Error 级别必须记录完整错误堆栈
|
||||
```
|
||||
|
||||
### 日志记录位置
|
||||
|
||||
| 层级 | 记录内容 |
|
||||
|------|----------|
|
||||
| Handler | 使用 `ResponseErrorWithDetail` 自动记录 Error 日志 |
|
||||
| Service | 关键业务操作记录 Info;业务异常记录 Warning |
|
||||
| DAO | 一般不记录日志,错误向上抛出 |
|
||||
|
||||
---
|
||||
|
||||
## 时间处理(强制东八区)
|
||||
|
||||
### 核心规则
|
||||
```
|
||||
时区:Asia/Shanghai (UTC+8)
|
||||
格式:RFC3339
|
||||
```
|
||||
|
||||
### 禁止与必须
|
||||
|
||||
| 禁止 | 必须使用 |
|
||||
|------|----------|
|
||||
| `time.Now()` | `TimeUtils.Now()` |
|
||||
| `time.Parse()` | `TimeUtils.Parse()` |
|
||||
| 直接格式化 | `TimeUtils.Format()` |
|
||||
|
||||
### 工具库位置
|
||||
- 后端:`rmdc-common/utils/TimeUtils.go`
|
||||
- 前端:`TonyMask/src/utils/timeUtils.ts`
|
||||
|
||||
### 使用示例
|
||||
```go
|
||||
// ✅ 正确
|
||||
now := TimeUtils.Now()
|
||||
timestamp := TimeUtils.Now().Format(time.RFC3339)
|
||||
|
||||
// ❌ 错误
|
||||
now := time.Now() // 禁止直接使用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 框架使用规范
|
||||
|
||||
### GIN 框架
|
||||
|
||||
#### 路由组织(强制分组)
|
||||
```go
|
||||
// ✅ 正确:使用路由分组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.GET("/:id", userHandler.GetByID)
|
||||
users.POST("/", userHandler.Create)
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:扁平路由
|
||||
r.GET("/api/v1/users/:id", ...)
|
||||
```
|
||||
|
||||
#### 中间件使用
|
||||
```go
|
||||
// 全局中间件
|
||||
r.Use(middleware.Recovery()) // 恢复
|
||||
r.Use(middleware.Logger()) // 日志
|
||||
r.Use(middleware.CORS()) // 跨域
|
||||
|
||||
// 路由组中间件
|
||||
authGroup := r.Group("/admin")
|
||||
authGroup.Use(middleware.Auth())
|
||||
```
|
||||
|
||||
#### 响应规范
|
||||
```
|
||||
所有 API 响应必须通过 pkg/common 统一响应函数
|
||||
禁止直接使用 c.JSON()、c.String() 等
|
||||
```
|
||||
|
||||
### GORM 框架
|
||||
|
||||
#### 操作位置
|
||||
```
|
||||
所有 GORM 操作必须在 dao 层
|
||||
严禁在 service 层拼接查询
|
||||
```
|
||||
|
||||
#### 链式调用 vs 原生 SQL
|
||||
|
||||
| 场景 | 推荐方式 |
|
||||
|------|----------|
|
||||
| 简单 CRUD | 链式调用 `db.Where().First()` |
|
||||
| 复杂查询(多表 JOIN、子查询) | `Raw()` / `Exec()` 原生 SQL |
|
||||
| 批量操作 | `Raw()` / `Exec()` 保证性能 |
|
||||
|
||||
```go
|
||||
// 简单查询 - 链式调用
|
||||
db.Where("status = ?", 1).Find(&users)
|
||||
|
||||
// 复杂查询 - 原生 SQL
|
||||
db.Raw(`
|
||||
SELECT u.*, COUNT(o.id) as order_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.status = ?
|
||||
GROUP BY u.id
|
||||
`, 1).Scan(&results)
|
||||
```
|
||||
|
||||
#### 错误处理
|
||||
```go
|
||||
// 必须处理 ErrRecordNotFound
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "资源不存在")
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify 阶段 Checklist
|
||||
|
||||
### 结构检查
|
||||
- [ ] 依赖方向正确:handler → service → dao(无反向引用)
|
||||
- [ ] handler 层无业务逻辑
|
||||
- [ ] dao 层无 service 引用
|
||||
- [ ] 使用 internal 包保护私有代码
|
||||
|
||||
### 响应检查
|
||||
- [ ] 所有 API 使用 `common.ResponseSuccess/Error`
|
||||
- [ ] 错误码来自 `common.Code*` 常量
|
||||
- [ ] 时间戳格式为 RFC3339
|
||||
- [ ] 无直接 `c.JSON()` 调用
|
||||
|
||||
### 代码检查
|
||||
- [ ] 公开函数/结构体有中文注释
|
||||
- [ ] 注释格式:`// 函数名 功能描述`
|
||||
- [ ] 无直接 `time.Now()` 调用
|
||||
- [ ] 无丢弃的 error(`_ = err` 禁止)
|
||||
- [ ] 包名小写无下划线
|
||||
|
||||
### 日志检查
|
||||
- [ ] 使用项目统一日志库
|
||||
- [ ] Error 日志包含完整堆栈
|
||||
- [ ] 关键业务操作有 Info 日志
|
||||
- [ ] 日志包含 TraceID 等追溯信息
|
||||
|
||||
### GORM 检查
|
||||
- [ ] `gorm.ErrRecordNotFound` 已处理
|
||||
- [ ] 复杂查询在 dao 层使用 Raw/Exec
|
||||
- [ ] 无 service 层直接 DB 操作
|
||||
|
||||
### GIN 检查
|
||||
- [ ] 使用路由分组组织 API
|
||||
- [ ] 通用逻辑使用中间件处理
|
||||
- [ ] 响应通过统一函数返回
|
||||
|
||||
### API 设计检查
|
||||
- [ ] 使用 POST + RequestBody(非 GET + PathVariables)
|
||||
- [ ] API 路径使用正确后缀(/list, /detail, /create 等)
|
||||
- [ ] DTO 命名符合规范(List/Get/Create/Update/Delete + 资源 + Request/Response)
|
||||
- [ ] 分页请求嵌入 PageRequest
|
||||
- [ ] 分页响应包含 list/total/page/page_size
|
||||
- [ ] 敏感信息不在 URL 中
|
||||
- [ ] 请求体必须验证(ShouldBindJSON)
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
| 陷阱 | 正确做法 |
|
||||
|------|----------|
|
||||
| handler 写业务逻辑 | 移到 service 层 |
|
||||
| 直接 `c.JSON()` | 用 `common.ResponseSuccess()` |
|
||||
| 忽略 `ErrRecordNotFound` | 转为 `CodeNotFound` 返回 |
|
||||
| `time.Now()` | `TimeUtils.Now()` |
|
||||
| 英文注释 | 改为中文 |
|
||||
| dao 引用 service | 违反依赖原则,重构 |
|
||||
| service 写 SQL | 移到 dao 层 |
|
||||
| 扁平路由 | 使用 Router Group |
|
||||
| 日志缺少上下文 | 添加 TraceID、UserID |
|
||||
| Error 日志无堆栈 | 记录完整错误信息 |
|
||||
| `GET /api/users/{id}` | `POST /api/users/detail` + RequestBody |
|
||||
| URL 传参数 `?page=1` | RequestBody 传递 |
|
||||
| DTO 命名不规范 | 使用 `List/Get/Create/Update/Delete` + 资源名 |
|
||||
| 敏感信息在 URL | 移到 RequestBody |
|
||||
|
||||
---
|
||||
|
||||
## Reference 文件索引
|
||||
|
||||
| 场景 | 读取文件 |
|
||||
|------|----------|
|
||||
| 需要完整目录结构说明 | `reference/project-structure.md` |
|
||||
| 需要响应结构体定义 | `reference/api-response-spec.md` |
|
||||
| 需要错误码完整列表 | `reference/error-codes.go` |
|
||||
| 需要编码规范细节 | `reference/coding-standards.md` |
|
||||
| 需要日志使用详细说明 | `reference/logging-standards.md` |
|
||||
| 需要时间处理详细说明 | `reference/time-handling.md` |
|
||||
| 需要框架使用详细说明 | `reference/framework-usage.md` |
|
||||
| 需要 API 设计详细说明 | `reference/api-design-spec.md` |
|
||||
| 需要代码示例 | `examples/*.go` |
|
||||
|
||||
---
|
||||
|
||||
## 快速命令
|
||||
|
||||
验证项目结构:
|
||||
```bash
|
||||
./scripts/validate-structure.sh
|
||||
```
|
||||
@@ -1,55 +0,0 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"my-project/internal/model/entity"
|
||||
)
|
||||
|
||||
// UserDAO 用户数据访问对象
|
||||
type UserDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserDAO 创建用户DAO实例
|
||||
// @param db *gorm.DB - 数据库连接
|
||||
// @return *UserDAO - DAO实例
|
||||
func NewUserDAO(db *gorm.DB) *UserDAO {
|
||||
return &UserDAO{db: db}
|
||||
}
|
||||
|
||||
// FindByID 根据ID查询用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param id int64 - 用户ID
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误,未找到返回gorm.ErrRecordNotFound
|
||||
func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param user *entity.User - 用户实体
|
||||
// @return error - 创建错误
|
||||
func (d *UserDAO) Create(ctx context.Context, user *entity.User) error {
|
||||
return d.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
// FindByEmail 根据邮箱查询用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param email string - 用户邮箱
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误
|
||||
func (d *UserDAO) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/service"
|
||||
"my-project/pkg/common"
|
||||
)
|
||||
|
||||
// UserHandler 用户相关API处理器
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户Handler实例
|
||||
// @param userService *service.UserService - 用户服务
|
||||
// @return *UserHandler - Handler实例
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
return &UserHandler{userService: userService}
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户信息
|
||||
// @param c *gin.Context - GIN上下文
|
||||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||||
// 1. 参数解析
|
||||
idStr := c.Param("id")
|
||||
userID, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
common.ResponseError(c, common.CodeParamError, "用户ID格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用Service
|
||||
user, err := h.userService.GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "获取用户失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 成功响应
|
||||
common.ResponseSuccess(c, user)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
// @param c *gin.Context - GIN上下文
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ResponseErrorWithDetail(c, common.CodeValidationFail, "参数验证失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
common.ResponseErrorWithDetail(c, common.CodeBusiness, "创建用户失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
common.ResponseSuccessWithMessage(c, user, "用户创建成功")
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"my-project/internal/dao"
|
||||
"my-project/internal/model/dto"
|
||||
"my-project/internal/model/entity"
|
||||
"my-project/pkg/log"
|
||||
)
|
||||
|
||||
// UserService 用户业务服务
|
||||
type UserService struct {
|
||||
userDAO *dao.UserDAO
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务实例
|
||||
// @param userDAO *dao.UserDAO - 用户数据访问对象
|
||||
// @return *UserService - 服务实例
|
||||
func NewUserService(userDAO *dao.UserDAO) *UserService {
|
||||
return &UserService{userDAO: userDAO}
|
||||
}
|
||||
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *entity.User - 用户实体
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*entity.User, error) {
|
||||
user, err := s.userDAO.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询用户失败: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// CreateUser 创建新用户
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param req *dto.CreateUserRequest - 创建请求
|
||||
// @return *entity.User - 创建的用户实体
|
||||
// @return error - 创建错误
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
}
|
||||
|
||||
if err := s.userDAO.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 记录关键业务日志
|
||||
log.Info(ctx, "用户创建成功", map[string]interface{}{
|
||||
"userID": user.ID,
|
||||
"username": user.Username,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
# API 设计规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 使用 POST + RequestBody
|
||||
|
||||
> **核心规范**: 所有 API 优先使用 POST 方法,参数通过 RequestBody 传递
|
||||
|
||||
```go
|
||||
// ✅ 推荐方式
|
||||
POST /api/jenkins/builds/list
|
||||
{
|
||||
"organization_folder": "Backend",
|
||||
"repository_name": "cmii-fly-center",
|
||||
"branch_name": "master",
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
}
|
||||
|
||||
// ❌ 避免使用
|
||||
GET /api/jenkins/organizations/{org}/repositories/{repo}/branches/{branch}/builds?page=1&page_size=10
|
||||
```
|
||||
|
||||
### 2. 避免 PathVariables
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
GET /api/projects/{project_id}
|
||||
GET /api/builds/{build_id}/console
|
||||
|
||||
// ✅ 推荐
|
||||
POST /api/projects/detail
|
||||
{
|
||||
"project_id": "namespace_abc12345"
|
||||
}
|
||||
|
||||
POST /api/builds/console
|
||||
{
|
||||
"organization_folder": "Backend",
|
||||
"repository_name": "cmii-fly-center",
|
||||
"branch_name": "master",
|
||||
"build_number": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 避免 RequestParams
|
||||
|
||||
```go
|
||||
// ❌ 不推荐
|
||||
GET /api/users/list?role=admin&status=active&page=1
|
||||
|
||||
// ✅ 推荐
|
||||
POST /api/users/list
|
||||
{
|
||||
"role": "admin",
|
||||
"status": "active",
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
// 业务数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [...],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1001,
|
||||
"message": "参数错误: organization_folder不能为空",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 请求结构规范
|
||||
|
||||
### 通用分页请求
|
||||
|
||||
```go
|
||||
type PageRequest struct {
|
||||
Page int `json:"page" binding:"required,min=1"`
|
||||
PageSize int `json:"page_size" binding:"required,min=1,max=100"`
|
||||
}
|
||||
```
|
||||
|
||||
### 通用筛选请求
|
||||
|
||||
```go
|
||||
type ListRequest struct {
|
||||
PageRequest
|
||||
Keyword string `json:"keyword,omitempty"` // 搜索关键词
|
||||
Status string `json:"status,omitempty"` // 状态筛选
|
||||
SortBy string `json:"sort_by,omitempty"` // 排序字段
|
||||
SortOrder string `json:"sort_order,omitempty"` // asc/desc
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 命名规范
|
||||
|
||||
### 操作类型后缀
|
||||
|
||||
| 操作 | 后缀 | 示例 |
|
||||
|------|------|------|
|
||||
| 列表查询 | `/list` | `/api/projects/list` |
|
||||
| 详情查询 | `/detail` | `/api/projects/detail` |
|
||||
| 创建 | `/create` | `/api/projects/create` |
|
||||
| 更新 | `/update` | `/api/projects/update` |
|
||||
| 删除 | `/delete` | `/api/projects/delete` |
|
||||
| 同步 | `/sync` | `/api/jenkins/organizations/sync` |
|
||||
| 触发 | `/trigger` | `/api/builds/trigger` |
|
||||
| 导出 | `/export` | `/api/projects/export` |
|
||||
|
||||
### 模块前缀
|
||||
|
||||
| 模块 | 前缀 |
|
||||
|------|------|
|
||||
| Jenkins | `/api/jenkins/` |
|
||||
| 项目管理 | `/api/projects/` |
|
||||
| 用户 | `/api/users/` |
|
||||
| 权限 | `/api/permissions/` |
|
||||
| 权限-Jenkins | `/api/permissions/jenkins/` |
|
||||
| 权限-项目 | `/api/permissions/projects/` |
|
||||
| 审计 | `/api/audit/` |
|
||||
| Exchange-Hub | `/api/exchange-hub/` |
|
||||
| DCU | `/api/dcu/` |
|
||||
|
||||
---
|
||||
|
||||
## Handler 实现模板
|
||||
|
||||
```go
|
||||
// ListBuilds 获取构建列表
|
||||
// @Summary 获取构建列表
|
||||
// @Tags 构建管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.ListBuildsRequest true "请求参数"
|
||||
// @Success 200 {object} response.Response{data=dto.ListBuildsResponse}
|
||||
// @Router /api/jenkins/builds/list [post]
|
||||
func (h *BuildHandler) ListBuilds(c *gin.Context) {
|
||||
var req dto.ListBuildsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ParamError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.buildService.ListBuilds(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, resp)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DTO 设计规范
|
||||
|
||||
### 请求 DTO 命名
|
||||
|
||||
```go
|
||||
// 列表请求: List{资源}Request
|
||||
type ListBuildsRequest struct {
|
||||
PageRequest
|
||||
OrganizationFolder string `json:"organization_folder" binding:"required"`
|
||||
RepositoryName string `json:"repository_name" binding:"required"`
|
||||
BranchName string `json:"branch_name,omitempty"`
|
||||
}
|
||||
|
||||
// 详情请求: Get{资源}Request 或 {资源}DetailRequest
|
||||
type GetBuildRequest struct {
|
||||
OrganizationFolder string `json:"organization_folder" binding:"required"`
|
||||
RepositoryName string `json:"repository_name" binding:"required"`
|
||||
BranchName string `json:"branch_name" binding:"required"`
|
||||
BuildNumber int `json:"build_number" binding:"required"`
|
||||
}
|
||||
|
||||
// 创建请求: Create{资源}Request
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Namespace string `json:"namespace" binding:"required"`
|
||||
Province string `json:"province" binding:"required"`
|
||||
City string `json:"city" binding:"required"`
|
||||
}
|
||||
|
||||
// 更新请求: Update{资源}Request
|
||||
type UpdateProjectRequest struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Province string `json:"province,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
}
|
||||
|
||||
// 删除请求: Delete{资源}Request
|
||||
type DeleteProjectRequest struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
}
|
||||
```
|
||||
|
||||
### 响应 DTO 命名
|
||||
|
||||
```go
|
||||
// 列表响应: List{资源}Response
|
||||
type ListBuildsResponse struct {
|
||||
List []*BuildDTO `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// 详情响应: {资源}DetailResponse 或直接使用 {资源}DTO
|
||||
type BuildDetailResponse struct {
|
||||
*BuildDTO
|
||||
ConsoleOutput string `json:"console_output,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码规范
|
||||
|
||||
### 错误码范围
|
||||
|
||||
| 范围 | 模块 |
|
||||
|------|------|
|
||||
| 1000-1999 | 通用错误 |
|
||||
| 2000-2999 | 用户/权限 |
|
||||
| 3000-3999 | Jenkins模块 |
|
||||
| 4000-4999 | 项目管理 |
|
||||
| 5000-5999 | Exchange-Hub |
|
||||
| 6000-6999 | Watchdog |
|
||||
|
||||
### 通用错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 0 | 成功 |
|
||||
| 1001 | 参数错误 |
|
||||
| 1002 | 未授权 |
|
||||
| 1003 | 禁止访问 |
|
||||
| 1004 | 资源不存在 |
|
||||
| 1005 | 内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
```typescript
|
||||
// api/modules/jenkins.ts
|
||||
export const jenkinsApi = {
|
||||
// 获取构建列表
|
||||
listBuilds: (data: ListBuildsRequest) =>
|
||||
request.post<ListBuildsResponse>('/api/jenkins/builds/list', data),
|
||||
|
||||
// 触发构建
|
||||
triggerBuild: (data: TriggerBuildRequest) =>
|
||||
request.post<TriggerBuildResponse>('/api/jenkins/builds/trigger', data),
|
||||
|
||||
// 获取构建详情
|
||||
getBuildDetail: (data: GetBuildRequest) =>
|
||||
request.post<BuildDetailResponse>('/api/jenkins/builds/detail', data),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全规范
|
||||
|
||||
### 1. 敏感字段不出现在 URL
|
||||
|
||||
```go
|
||||
// ❌ 敏感信息泄露到URL
|
||||
GET /api/auth/login?username=admin&password=123456
|
||||
|
||||
// ✅ 使用RequestBody
|
||||
POST /api/auth/login
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 必须验证请求体
|
||||
|
||||
```go
|
||||
func (h *Handler) CreateProject(c *gin.Context) {
|
||||
var req dto.CreateProjectRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ParamError(c, err)
|
||||
return
|
||||
}
|
||||
// 后续处理...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 审计敏感操作
|
||||
|
||||
所有写操作需通过审计中间件记录。
|
||||
@@ -1,35 +0,0 @@
|
||||
# API 响应规范
|
||||
|
||||
## 统一响应结构
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 业务状态码,0=成功
|
||||
Status int `json:"status"` // HTTP 状态码
|
||||
Timestamp string `json:"timestamp"` // RFC3339 东八区
|
||||
Data interface{} `json:"data"` // 业务数据
|
||||
Message string `json:"message,omitempty"` // 消息
|
||||
Error string `json:"error,omitempty"` // 错误详情
|
||||
}
|
||||
```
|
||||
|
||||
## 使用函数
|
||||
|
||||
| 场景 | 函数 |
|
||||
|------|------|
|
||||
| 查询成功 | `ResponseSuccess(c, data)` |
|
||||
| 操作成功 | `ResponseSuccessWithMessage(c, data, "msg")` |
|
||||
| 普通错误 | `ResponseError(c, code, "msg")` |
|
||||
| 详细错误 | `ResponseErrorWithDetail(c, code, "msg", err)` |
|
||||
|
||||
## HTTP 状态码映射
|
||||
|
||||
| 业务码 | HTTP 状态码 |
|
||||
|--------|-------------|
|
||||
| CodeSuccess | 200 |
|
||||
| CodeParamError, CodeValidationFail | 400 |
|
||||
| CodeUnauthorized | 401 |
|
||||
| CodeForbidden | 403 |
|
||||
| CodeNotFound | 404 |
|
||||
| CodeTimeout | 408 |
|
||||
| 其他 | 500 |
|
||||
@@ -1,44 +0,0 @@
|
||||
# 编码规范
|
||||
|
||||
## 命名规范
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 包名 | 小写单词,无下划线 | `service`, `utils` |
|
||||
| 变量/函数 | 驼峰命名 | `getUserByID` |
|
||||
| 公开标识 | 首字母大写 | `GetUserByID` |
|
||||
| 接口 | 单方法以 `er` 结尾 | `Reader`, `Writer` |
|
||||
|
||||
## 注释规范(中文,必须)
|
||||
|
||||
```go
|
||||
// GetUserByID 根据用户ID获取用户信息
|
||||
// @param ctx context.Context - 请求上下文
|
||||
// @param userID int64 - 用户唯一ID
|
||||
// @return *model.User - 用户信息
|
||||
// @return error - 查询错误
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*model.User, error)
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
1. 必须 `if err != nil` 处理
|
||||
2. 用 `fmt.Errorf("xxx: %w", err)` 包装
|
||||
3. 禁止 `_ = err` 丢弃错误
|
||||
4. Handler 层必须通过统一响应返回
|
||||
|
||||
## 日志级别
|
||||
|
||||
| 级别 | 用途 |
|
||||
|------|------|
|
||||
| Debug | 开发调试,详细流程 |
|
||||
| Info | 关键业务节点 |
|
||||
| Warning | 可预期非致命异常 |
|
||||
| Error | 严重错误,必须记录堆栈 |
|
||||
|
||||
## 时间处理
|
||||
|
||||
- 时区:Asia/Shanghai (UTC+8)
|
||||
- 格式:RFC3339
|
||||
- 禁止:`time.Now()`
|
||||
- 使用:`TimeUtils.Now()`
|
||||
@@ -1,35 +0,0 @@
|
||||
package common
|
||||
|
||||
// 业务状态码常量
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeServerError = 10001 // 服务器内部错误
|
||||
CodeParamError = 10002 // 参数错误
|
||||
CodeUnauthorized = 10003 // 未授权
|
||||
CodeForbidden = 10004 // 禁止访问
|
||||
CodeNotFound = 10005 // 资源不存在
|
||||
CodeTimeout = 10006 // 请求超时
|
||||
CodeValidationFail = 10007 // 验证失败
|
||||
CodeBusiness = 20001 // 业务逻辑错误 (20001-29999)
|
||||
)
|
||||
|
||||
// CodeMessage 错误码消息映射
|
||||
var CodeMessage = map[int]string{
|
||||
CodeSuccess: "success",
|
||||
CodeServerError: "服务器内部错误",
|
||||
CodeParamError: "参数错误",
|
||||
CodeUnauthorized: "未授权,请先登录",
|
||||
CodeForbidden: "权限不足,禁止访问",
|
||||
CodeNotFound: "请求的资源不存在",
|
||||
CodeTimeout: "请求超时",
|
||||
CodeValidationFail: "数据验证失败",
|
||||
CodeBusiness: "业务处理失败",
|
||||
}
|
||||
|
||||
// GetMessage 根据错误码获取默认消息
|
||||
func GetMessage(code int) string {
|
||||
if msg, ok := CodeMessage[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return "未知错误"
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
# 框架使用规范
|
||||
|
||||
## GIN 框架
|
||||
|
||||
### 路由组织
|
||||
|
||||
#### 强制使用路由分组 (Router Group)
|
||||
|
||||
```go
|
||||
func SetupRouter(r *gin.Engine) {
|
||||
// API 版本分组
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
// 用户模块
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.GET("/", userHandler.List)
|
||||
users.GET("/:id", userHandler.GetByID)
|
||||
users.POST("/", userHandler.Create)
|
||||
users.PUT("/:id", userHandler.Update)
|
||||
users.DELETE("/:id", userHandler.Delete)
|
||||
}
|
||||
|
||||
// 订单模块
|
||||
orders := v1.Group("/orders")
|
||||
{
|
||||
orders.GET("/", orderHandler.List)
|
||||
orders.GET("/:id", orderHandler.GetByID)
|
||||
orders.POST("/", orderHandler.Create)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 禁止扁平路由
|
||||
|
||||
```go
|
||||
// ❌ 错误:扁平路由,难以维护
|
||||
r.GET("/api/v1/users", ...)
|
||||
r.GET("/api/v1/users/:id", ...)
|
||||
r.POST("/api/v1/users", ...)
|
||||
r.GET("/api/v1/orders", ...)
|
||||
```
|
||||
|
||||
### 中间件使用
|
||||
|
||||
#### 全局中间件
|
||||
|
||||
```go
|
||||
func SetupMiddleware(r *gin.Engine) {
|
||||
// Recovery - 恢复 panic,防止程序崩溃
|
||||
r.Use(middleware.Recovery())
|
||||
|
||||
// Logger - 请求日志记录
|
||||
r.Use(middleware.Logger())
|
||||
|
||||
// CORS - 跨域处理
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
// TraceID - 请求追踪
|
||||
r.Use(middleware.TraceID())
|
||||
}
|
||||
```
|
||||
|
||||
#### 路由组中间件
|
||||
|
||||
```go
|
||||
// 需要认证的路由组
|
||||
authGroup := r.Group("/api/v1/admin")
|
||||
authGroup.Use(middleware.Auth())
|
||||
{
|
||||
authGroup.GET("/dashboard", adminHandler.Dashboard)
|
||||
authGroup.GET("/users", adminHandler.ListUsers)
|
||||
}
|
||||
|
||||
// 需要特定权限的路由组
|
||||
superAdmin := authGroup.Group("/super")
|
||||
superAdmin.Use(middleware.RequireRole("super_admin"))
|
||||
{
|
||||
superAdmin.DELETE("/users/:id", adminHandler.DeleteUser)
|
||||
}
|
||||
```
|
||||
|
||||
#### 常用中间件职责
|
||||
|
||||
| 中间件 | 职责 |
|
||||
|--------|------|
|
||||
| Recovery | 捕获 panic,返回 500 错误 |
|
||||
| Logger | 记录请求日志(方法、路径、耗时等) |
|
||||
| CORS | 处理跨域请求 |
|
||||
| Auth | 验证用户身份(JWT/Session) |
|
||||
| TraceID | 生成/传递请求追踪 ID |
|
||||
| RateLimit | 请求频率限制 |
|
||||
|
||||
### 响应规范
|
||||
|
||||
#### 强制使用统一响应
|
||||
|
||||
```go
|
||||
// ✅ 正确:使用统一响应函数
|
||||
common.ResponseSuccess(c, data)
|
||||
common.ResponseError(c, common.CodeParamError, "参数错误")
|
||||
|
||||
// ❌ 错误:直接使用 GIN 原生方法
|
||||
c.JSON(200, data)
|
||||
c.String(200, "success")
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": "bad request"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GORM 框架
|
||||
|
||||
### 操作位置规范
|
||||
|
||||
```
|
||||
所有 GORM 操作必须在 dao 层实现
|
||||
严禁在 service 层直接操作数据库
|
||||
```
|
||||
|
||||
### 查询方式选择
|
||||
|
||||
#### 简单 CRUD - 链式调用
|
||||
|
||||
```go
|
||||
// 单条查询
|
||||
var user entity.User
|
||||
db.Where("id = ?", userID).First(&user)
|
||||
|
||||
// 列表查询
|
||||
var users []entity.User
|
||||
db.Where("status = ?", 1).
|
||||
Order("created_at DESC").
|
||||
Limit(10).
|
||||
Offset(0).
|
||||
Find(&users)
|
||||
|
||||
// 创建
|
||||
db.Create(&user)
|
||||
|
||||
// 更新
|
||||
db.Model(&user).Updates(map[string]interface{}{
|
||||
"name": "new name",
|
||||
"status": 1,
|
||||
})
|
||||
|
||||
// 删除
|
||||
db.Delete(&user, userID)
|
||||
```
|
||||
|
||||
#### 复杂查询 - Raw/Exec
|
||||
|
||||
**推荐场景**:
|
||||
- 多表 JOIN
|
||||
- 子查询
|
||||
- 复杂聚合
|
||||
- 批量操作
|
||||
- 性能敏感场景
|
||||
|
||||
```go
|
||||
// 多表 JOIN 查询
|
||||
type UserWithOrderCount struct {
|
||||
entity.User
|
||||
OrderCount int64 `json:"order_count"`
|
||||
}
|
||||
|
||||
var results []UserWithOrderCount
|
||||
db.Raw(`
|
||||
SELECT u.*, COUNT(o.id) as order_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.status = ?
|
||||
GROUP BY u.id
|
||||
ORDER BY order_count DESC
|
||||
LIMIT ?
|
||||
`, 1, 10).Scan(&results)
|
||||
|
||||
// 批量更新
|
||||
db.Exec(`
|
||||
UPDATE orders
|
||||
SET status = ?
|
||||
WHERE user_id = ? AND status = ?
|
||||
`, "completed", userID, "pending")
|
||||
|
||||
// 复杂子查询
|
||||
db.Raw(`
|
||||
SELECT * FROM users
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM orders
|
||||
WHERE amount > ?
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ?
|
||||
)
|
||||
`, 1000, 5).Scan(&users)
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
#### 必须处理 ErrRecordNotFound
|
||||
|
||||
```go
|
||||
// DAO 层
|
||||
func (d *UserDAO) FindByID(ctx context.Context, id int64) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
return nil, err // 包含 ErrRecordNotFound
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Handler 层
|
||||
user, err := h.userService.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ResponseError(c, common.CodeNotFound, "用户不存在")
|
||||
return
|
||||
}
|
||||
common.ResponseErrorWithDetail(c, common.CodeServerError, "查询失败", err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 事务处理
|
||||
|
||||
```go
|
||||
// Service 层事务
|
||||
func (s *OrderService) CreateOrder(ctx context.Context, req *dto.CreateOrderRequest) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 创建订单
|
||||
order := &entity.Order{...}
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return fmt.Errorf("创建订单失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 扣减库存
|
||||
if err := tx.Model(&entity.Product{}).
|
||||
Where("id = ? AND stock >= ?", req.ProductID, req.Quantity).
|
||||
Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil {
|
||||
return fmt.Errorf("扣减库存失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 创建支付记录
|
||||
payment := &entity.Payment{...}
|
||||
if err := tx.Create(payment).Error; err != nil {
|
||||
return fmt.Errorf("创建支付记录失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Context 传递
|
||||
|
||||
```go
|
||||
// 必须使用 WithContext 传递上下文
|
||||
db.WithContext(ctx).First(&user, id)
|
||||
db.WithContext(ctx).Create(&order)
|
||||
|
||||
// 支持超时控制和取消
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
db.WithContext(ctx).Find(&users)
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
# 日志规范
|
||||
|
||||
## 指定框架
|
||||
|
||||
项目统一使用内部日志库:`rmdc-common/wdd_log/log_utils.go`
|
||||
|
||||
## 日志级别定义
|
||||
|
||||
### Debug
|
||||
- **用途**:开发调试,记录程序执行流程、变量值等详细信息
|
||||
- **场景**:默认开发日志级别
|
||||
- **示例**:
|
||||
```go
|
||||
log.Debug(ctx, "开始处理用户请求", map[string]interface{}{
|
||||
"userID": userID,
|
||||
"requestID": requestID,
|
||||
})
|
||||
```
|
||||
|
||||
### Info
|
||||
- **用途**:记录关键业务操作节点
|
||||
- **场景**:用户登录、订单创建、支付成功等关键业务
|
||||
- **示例**:
|
||||
```go
|
||||
log.Info(ctx, "用户登录成功", map[string]interface{}{
|
||||
"userID": user.ID,
|
||||
"username": user.Username,
|
||||
"ip": c.ClientIP(),
|
||||
})
|
||||
|
||||
log.Info(ctx, "订单创建成功", map[string]interface{}{
|
||||
"orderID": order.ID,
|
||||
"amount": order.Amount,
|
||||
"userID": order.UserID,
|
||||
})
|
||||
```
|
||||
|
||||
### Warning
|
||||
- **用途**:记录可预期的、非致命的异常情况,程序仍可继续运行
|
||||
- **场景**:外部 API 超时启用备用方案、配置缺失使用默认值等
|
||||
- **示例**:
|
||||
```go
|
||||
log.Warning(ctx, "外部API调用超时,已启用备用方案", map[string]interface{}{
|
||||
"api": "payment-gateway",
|
||||
"timeout": "5s",
|
||||
"fallback": "local-cache",
|
||||
})
|
||||
```
|
||||
|
||||
### Error
|
||||
- **用途**:记录严重错误,导致当前业务流程无法继续
|
||||
- **场景**:数据库连接失败、关键参数校验失败等
|
||||
- **要求**:必须详细记录错误信息和堆栈
|
||||
- **示例**:
|
||||
```go
|
||||
log.Error(ctx, "数据库连接失败", map[string]interface{}{
|
||||
"host": dbConfig.Host,
|
||||
"port": dbConfig.Port,
|
||||
"error": err.Error(),
|
||||
"stack": debug.Stack(),
|
||||
})
|
||||
```
|
||||
|
||||
## 日志内容规范
|
||||
|
||||
### 必须包含
|
||||
1. **TraceID** - 请求追踪 ID
|
||||
2. **UserID** - 用户标识(如适用)
|
||||
3. **操作描述** - 简练的中文描述
|
||||
4. **关键参数** - 与操作相关的关键数据
|
||||
|
||||
### 格式要求
|
||||
```go
|
||||
log.Info(ctx, "操作描述", map[string]interface{}{
|
||||
"key1": value1,
|
||||
"key2": value2,
|
||||
})
|
||||
```
|
||||
|
||||
## 各层日志职责
|
||||
|
||||
### Handler 层
|
||||
- 使用 `ResponseErrorWithDetail` 自动记录 Error 日志
|
||||
- 一般不主动记录日志
|
||||
|
||||
### Service 层
|
||||
- **Info**:关键业务操作成功(创建订单、支付、用户注册等)
|
||||
- **Warning**:业务逻辑异常但可处理
|
||||
- **Error**:通过 ResponseErrorWithDetail 在 Handler 层统一记录
|
||||
|
||||
### DAO 层
|
||||
- 一般不记录日志
|
||||
- 错误向上抛出,由 Handler 层统一处理
|
||||
|
||||
## 禁止事项
|
||||
|
||||
1. 禁止在日志中记录敏感信息(密码、Token、完整银行卡号等)
|
||||
2. 禁止使用 `fmt.Println` 或 `log.Println`
|
||||
3. 禁止在循环中大量记录日志
|
||||
4. Error 日志禁止缺少堆栈信息
|
||||
@@ -1,39 +0,0 @@
|
||||
# 项目目录结构规范
|
||||
|
||||
## 核心目录
|
||||
|
||||
| 目录 | 职责 | 禁止事项 |
|
||||
|------|------|----------|
|
||||
| `/api` 或 `/internal/handler` | GIN Handler 层,解析请求、调用 service、返回响应 | 禁止写业务逻辑 |
|
||||
| `/internal/service` | 业务逻辑核心,编排 dao 完成功能 | - |
|
||||
| `/internal/dao` 或 `/internal/repository` | 数据访问层,封装 GORM 操作 | 禁止引用 service |
|
||||
| `/internal/model/entity` | 数据库表结构对应的持久化对象 | - |
|
||||
| `/internal/model/dto` | API 数据传输对象(请求/响应) | - |
|
||||
| `/pkg/common` | 统一响应、错误码、公共工具 | - |
|
||||
| `/configs` | 配置文件 | - |
|
||||
| `/cmd` | main.go 入口 | - |
|
||||
|
||||
## 依赖规则
|
||||
|
||||
```
|
||||
handler → service → dao
|
||||
↓ ↓ ↓
|
||||
pkg/common (任意层可引用)
|
||||
```
|
||||
|
||||
**严禁反向或跨层依赖**
|
||||
|
||||
## go.mod 内部模块引用
|
||||
|
||||
```go
|
||||
module my-project
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
wdd.io/TonyCommon v1.0.0
|
||||
)
|
||||
|
||||
// 本地开发使用 replace
|
||||
replace wdd.io/TonyCommon => ../TonyCommon
|
||||
```
|
||||
@@ -1,120 +0,0 @@
|
||||
# 时间处理规范
|
||||
|
||||
## 核心原则
|
||||
|
||||
所有在前端和后端之间传输、以及在数据库中存储的时间,**必须统一为东八区时间 (Asia/Shanghai, UTC+8)**。
|
||||
|
||||
## 指定工具库
|
||||
|
||||
| 端 | 工具库路径 |
|
||||
|----|-----------|
|
||||
| 后端 | `rmdc-common/utils/TimeUtils.go` |
|
||||
| 前端 | `TonyMask/src/utils/timeUtils.ts` |
|
||||
|
||||
## 时间格式
|
||||
|
||||
- API 响应中的 `timestamp` 字段统一使用 **RFC3339** 格式
|
||||
- 示例:`2024-01-15T14:30:00+08:00`
|
||||
|
||||
## 禁止与必须
|
||||
|
||||
### 禁止直接使用
|
||||
|
||||
```go
|
||||
// ❌ 禁止
|
||||
time.Now()
|
||||
time.Parse(layout, value)
|
||||
t.Format(layout)
|
||||
```
|
||||
|
||||
### 必须使用工具库
|
||||
|
||||
```go
|
||||
// ✅ 正确
|
||||
TimeUtils.Now()
|
||||
TimeUtils.Parse(layout, value)
|
||||
TimeUtils.Format(t, layout)
|
||||
```
|
||||
|
||||
## 常用场景示例
|
||||
|
||||
### 获取当前时间
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
now := time.Now()
|
||||
|
||||
// ✅ 正确
|
||||
now := TimeUtils.Now()
|
||||
```
|
||||
|
||||
### 格式化时间戳
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
// ✅ 正确
|
||||
timestamp := TimeUtils.Now().Format(time.RFC3339)
|
||||
```
|
||||
|
||||
### 解析时间字符串
|
||||
|
||||
```go
|
||||
// ❌ 错误
|
||||
t, err := time.Parse(time.RFC3339, timeStr)
|
||||
|
||||
// ✅ 正确
|
||||
t, err := TimeUtils.Parse(time.RFC3339, timeStr)
|
||||
```
|
||||
|
||||
### 数据库时间字段
|
||||
|
||||
```go
|
||||
type Order struct {
|
||||
ID int64 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"` // GORM 自动处理
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"` // GORM 自动处理
|
||||
ExpireAt time.Time // 业务时间使用 TimeUtils
|
||||
}
|
||||
|
||||
// 设置业务时间
|
||||
order.ExpireAt = TimeUtils.Now().Add(24 * time.Hour)
|
||||
```
|
||||
|
||||
### API 响应时间
|
||||
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Status int `json:"status"`
|
||||
Timestamp string `json:"timestamp"` // RFC3339 格式
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
resp := Response{
|
||||
Timestamp: TimeUtils.Now().Format(time.RFC3339),
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## TimeUtils 常用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `Now()` | 获取当前东八区时间 |
|
||||
| `Parse(layout, value)` | 解析时间字符串(东八区) |
|
||||
| `Format(t, layout)` | 格式化时间 |
|
||||
| `StartOfDay(t)` | 获取当天零点 |
|
||||
| `EndOfDay(t)` | 获取当天 23:59:59 |
|
||||
| `AddDays(t, days)` | 增加天数 |
|
||||
|
||||
## 时区配置
|
||||
|
||||
确保服务器和数据库时区配置正确:
|
||||
|
||||
```go
|
||||
// 数据库连接配置
|
||||
dsn := "user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai"
|
||||
```
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 验证 Go GIN/GORM 项目结构
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== Go 项目结构验证 ==="
|
||||
|
||||
# 检查 go.mod
|
||||
if [ ! -f "go.mod" ]; then
|
||||
echo "❌ 缺少 go.mod"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ go.mod 存在"
|
||||
|
||||
# 检查核心目录
|
||||
DIRS=("internal/service" "internal/dao" "internal/model" "pkg/common")
|
||||
for dir in "${DIRS[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "✅ $dir 存在"
|
||||
else
|
||||
echo "⚠️ $dir 不存在"
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查 handler 目录(两种风格)
|
||||
if [ -d "api" ] || [ -d "internal/handler" ]; then
|
||||
echo "✅ handler 目录存在"
|
||||
else
|
||||
echo "⚠️ 缺少 api/ 或 internal/handler/"
|
||||
fi
|
||||
|
||||
# 检查反向依赖(dao 不应引用 service)
|
||||
echo ""
|
||||
echo "=== 检查依赖方向 ==="
|
||||
if grep -r "internal/service" internal/dao/ 2>/dev/null; then
|
||||
echo "❌ dao 层存在对 service 的反向依赖"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 无反向依赖"
|
||||
|
||||
# 检查 time.Now() 使用
|
||||
echo ""
|
||||
echo "=== 检查 time.Now() 使用 ==="
|
||||
if grep -rn "time\.Now()" --include="*.go" internal/ api/ 2>/dev/null | grep -v "_test.go"; then
|
||||
echo "⚠️ 发现直接使用 time.Now(),应使用 TimeUtils.Now()"
|
||||
else
|
||||
echo "✅ 无直接 time.Now() 调用"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== 验证完成 ==="
|
||||
@@ -1,141 +0,0 @@
|
||||
---
|
||||
name: coding-vue3-vuetify
|
||||
description: Build production-grade Vue 3 + TypeScript + Vuetify 3 interfaces with architectural rigor. 构建生产级 Vue 3 + TypeScript + Vuetify 3 界面。Use when creating Vue components, pages, layouts, Pinia stores, or API modules. 用于创建 Vue 组件、页面、布局、Pinia 状态管理或 API 模块。Enforces strict typing, Composition API patterns, Material Design 3 aesthetics, and bulletproof data handling.
|
||||
---
|
||||
|
||||
本技能指导构建架构严谨、类型安全、视觉精致的 Vue 3 + Vuetify 3 代码。每个组件都应该达到生产级代码库的标准——让资深工程师也引以为傲。
|
||||
|
||||
用户输入:$ARGUMENTS(组件规格、页面需求、功能请求或架构问题)
|
||||
|
||||
## 架构思维
|
||||
|
||||
动手写代码之前,先建立清晰认知:
|
||||
|
||||
- **组件身份**:这是页面(Page)、布局(Layout)、可复用组件(Component)、组合式函数(Composable)、状态仓库(Store),还是 API 模块?每种都有独特模式。
|
||||
- **数据重力**:状态住在哪里?Props 向下流动,Events 向上冒泡。跨组件状态用 Pinia。深层级传递用 `provide/inject`。
|
||||
- **滚动策略**:哪个容器拥有滚动权?永远不是 body。必须显式声明。必须可控。
|
||||
- **失败模式**:数据为 `null` 时怎么办?空数组?网络超时?先为不幸路径设计。
|
||||
|
||||
**关键原则**:生产代码预判混乱。为一切加类型。为一切加守卫。让一切优雅降级。
|
||||
|
||||
## 核心信条
|
||||
|
||||
### TypeScript 绝对主义
|
||||
- `<script setup lang="ts">` — 唯一可接受的写法
|
||||
- `any` 被禁止 — 使用 `unknown` + 类型守卫、泛型、工具类型
|
||||
- 每个 prop、emit、ref、API 响应都必须穿戴类型
|
||||
- 类型定义放在 `@/types/`,按领域组织:`user.d.ts`、`order.d.ts`
|
||||
|
||||
### Composition API 纯粹性
|
||||
- `ref`、`reactive`、`computed`、`watchEffect` — 掌握这四大金刚
|
||||
- `shallowRef`、`readonly`、`toRaw` — 知道何时使用优化手段
|
||||
- 生命周期用 `onMounted`、`onUnmounted` — 绝不混用 Options API
|
||||
- Pinia stores:类型化的 state、类型化的 getters、类型化的 actions — 无例外
|
||||
|
||||
### Vuetify 3 + Material Design 3
|
||||
- 所有 UI 通过 Vuetify 组件实现 — UI 元素不使用原生 HTML
|
||||
- 始终主题感知 — `rgb(var(--v-theme-surface))`,绝不 `#ffffff`
|
||||
- `useDisplay()` 处理响应式逻辑 — 断点是一等公民
|
||||
- 密度很重要 — 数据密集界面使用 `density="compact"`
|
||||
|
||||
### 布局哲学
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 工具栏 (flex-shrink-0) │
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ 内容区域 │
|
||||
│ (flex-grow-1, overflow-y-auto) │
|
||||
│ (min-height: 0) ← 关键! │
|
||||
│ │
|
||||
├─────────────────────────────────┤
|
||||
│ 底部栏 (flex-shrink-0) │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
- **禁止 body 滚动** — 视口锁定,内容在容器中滚动
|
||||
- **Flexbox 陷阱**:`flex-grow-1` 子元素必须有 `min-height: 0`
|
||||
- **粘性元素**:筛选栏、表头 — 滚动时始终可见
|
||||
|
||||
## 数据健壮性模式
|
||||
|
||||
将所有外部数据视为不可信:
|
||||
|
||||
```typescript
|
||||
// 防御性访问
|
||||
const userName = user?.profile?.name ?? '未知'
|
||||
|
||||
// 数组安全检查
|
||||
const items = Array.isArray(response.data) ? response.data : []
|
||||
|
||||
// 模板中的存在性守卫
|
||||
<template v-if="user">{{ user.name }}</template>
|
||||
<v-empty-state v-else />
|
||||
```
|
||||
|
||||
## UI 状态三位一体
|
||||
|
||||
每个数据驱动视图必须处理三种状态:
|
||||
|
||||
| 状态 | 组件 | 禁止行为 |
|
||||
|------|------|----------|
|
||||
| **加载中** | `v-skeleton-loader` | 显示过期数据或空白屏幕 |
|
||||
| **空数据** | `v-empty-state` + 操作按钮 | 留下白茫茫一片 |
|
||||
| **错误** | Snackbar + 重试选项 | 静默失败 |
|
||||
|
||||
## 表格与列表戒律
|
||||
|
||||
- 每个 `v-data-table` 都要 `fixed-header` — 没有商量余地
|
||||
- 截断文本必须配 `v-tooltip` — 用户有权 hover 看到完整内容
|
||||
- 100+ 条数据?用 `v-virtual-scroll` — DOM 节点数保持恒定
|
||||
- 列宽显式指定 — 不玩布局抽奖
|
||||
|
||||
## 反模式(绝不允许)
|
||||
|
||||
- TypeScript 项目中出现 `.js` 文件
|
||||
- 没有正当理由使用 `any`
|
||||
- 硬编码颜色:`color="#1976d2"` → 应该用 `color="primary"`
|
||||
- SPA 布局中出现 body 级滚动
|
||||
- 表格没有固定表头
|
||||
- 截断文本没有 tooltip
|
||||
- 空状态真的"空空如也"
|
||||
- 加载状态冻结 UI
|
||||
- API 调用没有错误处理
|
||||
|
||||
## 参考文件
|
||||
|
||||
需要实现细节时查阅:
|
||||
|
||||
| 需求 | 文件 |
|
||||
|------|------|
|
||||
| 高级 TypeScript 模式 | `reference/typescript-rules.md` |
|
||||
| 复杂布局结构 | `reference/layout-patterns.md` |
|
||||
| API 客户端架构 | `reference/api-patterns.md` |
|
||||
| 表格、列表、表单、反馈 | `reference/ui-interaction.md` |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # Axios 实例 + 模块
|
||||
├── components/ # 共享组件
|
||||
├── composables/ # 可复用 hooks
|
||||
├── layouts/ # 页面外壳
|
||||
├── pages/ # 路由视图
|
||||
├── plugins/ # Vuetify, Pinia, Router
|
||||
├── store/ # Pinia stores
|
||||
├── styles/ # 全局 SCSS
|
||||
├── types/ # 类型定义
|
||||
└── utils/ # 纯函数
|
||||
```
|
||||
|
||||
## 输出规范
|
||||
|
||||
1. 陈述架构方案(2-3 句话)
|
||||
2. 列出要创建的文件及其用途
|
||||
3. 完整实现每个文件 — 无占位符,无 TODO
|
||||
4. 对照反模式清单验证
|
||||
5. 指出任何假设或权衡取舍
|
||||
|
||||
---
|
||||
|
||||
记住:你不是在写"能跑的代码"。你是在写能跑、能扩展、能维护、能令人愉悦的代码。每个 `ref` 都有类型。每个边界情况都有处理。每个加载状态都很美观。这就是"生产级"的含义。
|
||||
@@ -1,113 +0,0 @@
|
||||
// @/api/modules/user.ts
|
||||
import request from '@/api'
|
||||
import type { PageParams, PageResult } from '@/types/api'
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
// ============================================
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
status: 'active' | 'disabled'
|
||||
role: 'admin' | 'user' | 'guest'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateUserDto {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
role?: User['role']
|
||||
}
|
||||
|
||||
export interface UpdateUserDto {
|
||||
name?: string
|
||||
email?: string
|
||||
status?: User['status']
|
||||
role?: User['role']
|
||||
}
|
||||
|
||||
export interface UserListParams extends PageParams {
|
||||
search?: string
|
||||
status?: User['status']
|
||||
role?: User['role']
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 封装
|
||||
// ============================================
|
||||
|
||||
export const userApi = {
|
||||
/**
|
||||
* 获取用户分页列表
|
||||
*/
|
||||
getPage: (params: UserListParams) =>
|
||||
request.get<PageResult<User>>('/users', { params }),
|
||||
|
||||
/**
|
||||
* 获取用户列表(无分页,用于下拉选择等场景)
|
||||
*/
|
||||
getList: (params?: Partial<UserListParams>) =>
|
||||
request.get<User[]>('/users/list', { params }),
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
*/
|
||||
getById: (id: string) =>
|
||||
request.get<User>(`/users/${id}`),
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
checkEmail: (email: string) =>
|
||||
request.get<{ exists: boolean }>('/users/check-email', { params: { email } }),
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
create: (data: CreateUserDto) =>
|
||||
request.post<User>('/users', data),
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
update: (id: string, data: UpdateUserDto) =>
|
||||
request.put<User>(`/users/${id}`, data),
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
remove: (id: string) =>
|
||||
request.delete<void>(`/users/${id}`),
|
||||
|
||||
/**
|
||||
* 批量删除用户
|
||||
*/
|
||||
batchRemove: (ids: string[]) =>
|
||||
request.post<void>('/users/batch-delete', { ids }),
|
||||
|
||||
/**
|
||||
* 启用/禁用用户
|
||||
*/
|
||||
toggleStatus: (id: string, status: User['status']) =>
|
||||
request.patch<User>(`/users/${id}/status`, { status }),
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*/
|
||||
resetPassword: (id: string) =>
|
||||
request.post<{ tempPassword: string }>(`/users/${id}/reset-password`),
|
||||
|
||||
/**
|
||||
* 导出用户列表
|
||||
*/
|
||||
export: (params?: UserListParams) =>
|
||||
request.get<Blob>('/users/export', {
|
||||
params,
|
||||
responseType: 'blob',
|
||||
}),
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<!-- 页面头部 -->
|
||||
<v-toolbar density="compact" class="flex-shrink-0">
|
||||
<v-toolbar-title>订单管理</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
:loading="loading"
|
||||
@click="fetchData"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-download"
|
||||
:loading="exporting"
|
||||
@click="exportData"
|
||||
>
|
||||
导出
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<!-- 筛选栏 - 粘性定位 -->
|
||||
<v-sheet class="flex-shrink-0 pa-4 sticky-filter">
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="filters.search"
|
||||
label="搜索订单号/客户名"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
hide-details
|
||||
@update:model-value="debouncedFetch"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
v-model="filters.status"
|
||||
:items="statusOptions"
|
||||
label="状态"
|
||||
clearable
|
||||
hide-details
|
||||
@update:model-value="fetchData"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-text-field
|
||||
v-model="filters.dateRange"
|
||||
label="日期范围"
|
||||
prepend-inner-icon="mdi-calendar"
|
||||
readonly
|
||||
hide-details
|
||||
@click="showDatePicker = true"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
block
|
||||
@click="resetFilters"
|
||||
>
|
||||
重置筛选
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-sheet>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- 主内容区 - 可滚动 -->
|
||||
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
|
||||
<!-- 加载状态 -->
|
||||
<v-skeleton-loader
|
||||
v-if="loading && !orders.length"
|
||||
type="table-heading, table-row@8"
|
||||
class="ma-4"
|
||||
/>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<v-empty-state
|
||||
v-else-if="!orders.length"
|
||||
icon="mdi-package-variant"
|
||||
title="暂无订单"
|
||||
text="当前筛选条件下没有找到订单记录"
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn variant="outlined" @click="resetFilters">
|
||||
清除筛选
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="fetchData">
|
||||
刷新
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-empty-state>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<v-data-table-server
|
||||
v-else
|
||||
v-model:items-per-page="pagination.pageSize"
|
||||
v-model:page="pagination.page"
|
||||
:headers="headers"
|
||||
:items="orders"
|
||||
:items-length="pagination.total"
|
||||
:loading="loading"
|
||||
fixed-header
|
||||
hover
|
||||
@update:options="onOptionsChange"
|
||||
>
|
||||
<!-- 订单号 - 可点击 -->
|
||||
<template #item.orderNo="{ item }">
|
||||
<a
|
||||
href="#"
|
||||
class="text-primary text-decoration-none"
|
||||
@click.prevent="viewDetail(item)"
|
||||
>
|
||||
{{ item.orderNo }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 客户名 - 截断 + Tooltip -->
|
||||
<template #item.customerName="{ value }">
|
||||
<v-tooltip :text="value" location="top">
|
||||
<template #activator="{ props }">
|
||||
<span
|
||||
v-bind="props"
|
||||
class="text-truncate d-inline-block"
|
||||
style="max-width: 120px"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 金额 - 格式化 -->
|
||||
<template #item.amount="{ value }">
|
||||
<span class="font-weight-medium">
|
||||
¥{{ formatNumber(value) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 状态 - Chip -->
|
||||
<template #item.status="{ value }">
|
||||
<v-chip
|
||||
:color="getStatusColor(value)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ getStatusText(value) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<!-- 备注 - 多行截断 -->
|
||||
<template #item.remark="{ value }">
|
||||
<div v-if="value" class="remark-cell">
|
||||
<v-tooltip :text="value" location="top" max-width="300">
|
||||
<template #activator="{ props }">
|
||||
<span v-bind="props" class="line-clamp-2">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<span v-else class="text-grey">-</span>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="viewDetail(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="item.status === 'completed'"
|
||||
@click="editOrder(item)"
|
||||
/>
|
||||
<v-menu>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copyOrderNo(item)">
|
||||
<template #prepend>
|
||||
<v-icon size="small">mdi-content-copy</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>复制订单号</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
:disabled="item.status !== 'pending'"
|
||||
@click="cancelOrder(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon size="small" color="error">mdi-cancel</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-error">取消订单</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<!-- 空数据插槽 -->
|
||||
<template #no-data>
|
||||
<v-empty-state
|
||||
icon="mdi-database-off"
|
||||
title="暂无数据"
|
||||
text="请尝试调整筛选条件"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计栏 -->
|
||||
<v-sheet class="flex-shrink-0 pa-2 border-t d-flex align-center justify-space-between">
|
||||
<span class="text-body-2 text-grey">
|
||||
共 {{ pagination.total }} 条记录
|
||||
</span>
|
||||
<span class="text-body-2">
|
||||
已选 <strong>{{ selectedCount }}</strong> 条
|
||||
<v-btn
|
||||
v-if="selectedCount > 0"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
@click="batchAction"
|
||||
>
|
||||
批量操作
|
||||
</v-btn>
|
||||
</span>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import type { Order, OrderStatus } from '@/types/order'
|
||||
import { orderApi } from '@/api/modules/order'
|
||||
import { useSnackbar } from '@/composables/useSnackbar'
|
||||
|
||||
// Composables
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
const orders = ref<Order[]>([])
|
||||
const selectedCount = ref(0)
|
||||
|
||||
const filters = reactive({
|
||||
search: '',
|
||||
status: null as OrderStatus | null,
|
||||
dateRange: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// Table Headers
|
||||
const headers = [
|
||||
{ title: '订单号', key: 'orderNo', width: 160 },
|
||||
{ title: '客户名称', key: 'customerName', width: 150 },
|
||||
{ title: '金额', key: 'amount', width: 120, align: 'end' as const },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '备注', key: 'remark', width: 200 },
|
||||
{ title: '创建时间', key: 'createdAt', width: 170 },
|
||||
{ title: '操作', key: 'actions', width: 140, sortable: false },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ title: '全部', value: null },
|
||||
{ title: '待处理', value: 'pending' },
|
||||
{ title: '处理中', value: 'processing' },
|
||||
{ title: '已完成', value: 'completed' },
|
||||
{ title: '已取消', value: 'cancelled' },
|
||||
]
|
||||
|
||||
// Methods
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await orderApi.getPage({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
search: filters.search || undefined,
|
||||
status: filters.status || undefined,
|
||||
})
|
||||
orders.value = result.list
|
||||
pagination.total = result.total
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch orders:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedFetch = useDebounceFn(fetchData, 300)
|
||||
|
||||
function onOptionsChange(options: { page: number; itemsPerPage: number }) {
|
||||
pagination.page = options.page
|
||||
pagination.pageSize = options.itemsPerPage
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.search = ''
|
||||
filters.status = null
|
||||
filters.dateRange = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
async function exportData() {
|
||||
exporting.value = true
|
||||
try {
|
||||
const blob = await orderApi.export(filters)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `orders_${Date.now()}.xlsx`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
snackbar.success('导出成功')
|
||||
} catch (error) {
|
||||
snackbar.error('导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function viewDetail(item: Order) {
|
||||
// Navigate to detail page
|
||||
}
|
||||
|
||||
function editOrder(item: Order) {
|
||||
// Open edit dialog
|
||||
}
|
||||
|
||||
function copyOrderNo(item: Order) {
|
||||
navigator.clipboard.writeText(item.orderNo)
|
||||
snackbar.success('订单号已复制')
|
||||
}
|
||||
|
||||
function cancelOrder(item: Order) {
|
||||
// Show confirm dialog
|
||||
}
|
||||
|
||||
function batchAction() {
|
||||
// Show batch action menu
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function formatNumber(value: number): string {
|
||||
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
function getStatusColor(status: OrderStatus): string {
|
||||
const colors: Record<OrderStatus, string> = {
|
||||
pending: 'warning',
|
||||
processing: 'info',
|
||||
completed: 'success',
|
||||
cancelled: 'grey',
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
function getStatusText(status: OrderStatus): string {
|
||||
const texts: Record<OrderStatus, string> = {
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sticky-filter {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.remark-cell {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,155 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid class="d-flex flex-column h-100 pa-0">
|
||||
<!-- 固定工具栏 -->
|
||||
<v-toolbar density="compact" class="flex-shrink-0">
|
||||
<v-toolbar-title>用户管理</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-refresh" :loading="loading" @click="fetchData" />
|
||||
<v-btn color="primary" prepend-icon="mdi-plus" @click="openCreate">
|
||||
新建
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<v-sheet class="flex-shrink-0 pa-4">
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="filters.search"
|
||||
label="搜索"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="filters.status"
|
||||
:items="statusOptions"
|
||||
label="状态"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-sheet>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- 可滚动内容区 -->
|
||||
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
|
||||
<v-skeleton-loader v-if="loading" type="table-heading, table-row@10" />
|
||||
|
||||
<v-empty-state
|
||||
v-else-if="!users.length"
|
||||
icon="mdi-account-off"
|
||||
title="暂无用户"
|
||||
text="点击新建按钮添加第一个用户"
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn color="primary" @click="openCreate">新建用户</v-btn>
|
||||
</template>
|
||||
</v-empty-state>
|
||||
|
||||
<v-data-table
|
||||
v-else
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
fixed-header
|
||||
hover
|
||||
>
|
||||
<template #item.name="{ value }">
|
||||
<v-tooltip :text="value" location="top">
|
||||
<template #activator="{ props }">
|
||||
<span
|
||||
v-bind="props"
|
||||
class="text-truncate d-inline-block"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<template #item.status="{ value }">
|
||||
<v-chip
|
||||
:color="value === 'active' ? 'success' : 'grey'"
|
||||
size="small"
|
||||
>
|
||||
{{ value === 'active' ? '活跃' : '禁用' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="edit(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="remove(item)"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import type { User } from '@/types/user'
|
||||
import { userApi } from '@/api/modules/user'
|
||||
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const users = ref<User[]>([])
|
||||
const filters = reactive({
|
||||
search: '',
|
||||
status: null as string | null,
|
||||
})
|
||||
|
||||
// Table config
|
||||
const headers = [
|
||||
{ title: '姓名', key: 'name', width: 200 },
|
||||
{ title: '邮箱', key: 'email' },
|
||||
{ title: '状态', key: 'status', width: 120 },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
{ title: '操作', key: 'actions', width: 120, sortable: false },
|
||||
]
|
||||
|
||||
const statusOptions = [
|
||||
{ title: '全部', value: null },
|
||||
{ title: '活跃', value: 'active' },
|
||||
{ title: '禁用', value: 'disabled' },
|
||||
]
|
||||
|
||||
// Methods
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
users.value = await userApi.getList(filters)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
// TODO: Open create dialog
|
||||
}
|
||||
|
||||
function edit(item: User) {
|
||||
// TODO: Open edit dialog
|
||||
}
|
||||
|
||||
function remove(item: User) {
|
||||
// TODO: Confirm and delete
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
@@ -1,238 +0,0 @@
|
||||
# API 客户端模式
|
||||
|
||||
## 标准响应类型
|
||||
|
||||
```typescript
|
||||
// @/types/api.d.ts
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
status: number
|
||||
timestamp: string
|
||||
data: T
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface PageParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
sort?: string
|
||||
order?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export enum ApiErrorCode {
|
||||
Success = 0,
|
||||
ServerError = 10001,
|
||||
ParamError = 10002,
|
||||
Unauthorized = 10003,
|
||||
Forbidden = 10004,
|
||||
NotFound = 10005,
|
||||
Timeout = 10006,
|
||||
ValidationFail = 10007,
|
||||
BusinessError = 20001,
|
||||
}
|
||||
```
|
||||
|
||||
## Axios 实例
|
||||
|
||||
```typescript
|
||||
// @/api/index.ts
|
||||
import axios from 'axios'
|
||||
import { setupInterceptors } from './interceptors'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
setupInterceptors(request)
|
||||
|
||||
export default request
|
||||
```
|
||||
|
||||
## 响应拦截器
|
||||
|
||||
```typescript
|
||||
// @/api/interceptors.ts
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { useSnackbar } from '@/composables/useSnackbar'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import router from '@/router'
|
||||
import { ApiErrorCode, type ApiResponse } from '@/types/api'
|
||||
|
||||
export function setupInterceptors(instance: AxiosInstance) {
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse<ApiResponse>) => {
|
||||
const { code, data, message } = response.data
|
||||
|
||||
// 业务成功
|
||||
if (code === ApiErrorCode.Success) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 业务失败
|
||||
const snackbar = useSnackbar()
|
||||
snackbar.error(message || '操作失败')
|
||||
|
||||
return Promise.reject(new Error(message))
|
||||
},
|
||||
(error) => {
|
||||
const snackbar = useSnackbar()
|
||||
const status = error.response?.status
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
useAuthStore().logout()
|
||||
router.push('/login')
|
||||
snackbar.error('登录已过期,请重新登录')
|
||||
break
|
||||
case 403:
|
||||
snackbar.error('无权访问')
|
||||
break
|
||||
case 404:
|
||||
snackbar.error('请求的资源不存在')
|
||||
break
|
||||
case 500:
|
||||
snackbar.error('服务器错误,请稍后重试')
|
||||
break
|
||||
default:
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
snackbar.error('请求超时,请检查网络')
|
||||
} else if (!error.response) {
|
||||
snackbar.error('网络连接失败')
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## API 模块模板
|
||||
|
||||
```typescript
|
||||
// @/api/modules/[domain].ts
|
||||
import request from '@/api'
|
||||
import type { PageParams, PageResult } from '@/types/api'
|
||||
import type { Entity, CreateDto, UpdateDto } from '@/types/[domain]'
|
||||
|
||||
export const entityApi = {
|
||||
// 分页列表
|
||||
getPage: (params: PageParams) =>
|
||||
request.get<PageResult<Entity>>('/entities', { params }),
|
||||
|
||||
// 详情
|
||||
getById: (id: string) =>
|
||||
request.get<Entity>(`/entities/${id}`),
|
||||
|
||||
// 新增
|
||||
create: (data: CreateDto) =>
|
||||
request.post<Entity>('/entities', data),
|
||||
|
||||
// 更新
|
||||
update: (id: string, data: UpdateDto) =>
|
||||
request.put<Entity>(`/entities/${id}`, data),
|
||||
|
||||
// 删除
|
||||
remove: (id: string) =>
|
||||
request.delete<void>(`/entities/${id}`),
|
||||
}
|
||||
```
|
||||
|
||||
## 请求取消处理
|
||||
|
||||
```typescript
|
||||
// composables/useCancelableRequest.ts
|
||||
import { onUnmounted } from 'vue'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
export function useCancelableRequest() {
|
||||
const controller = new AbortController()
|
||||
|
||||
onUnmounted(() => {
|
||||
controller.abort()
|
||||
})
|
||||
|
||||
function withCancel<T>(
|
||||
requestFn: (config?: AxiosRequestConfig) => Promise<T>
|
||||
): Promise<T> {
|
||||
return requestFn({ signal: controller.signal })
|
||||
}
|
||||
|
||||
return { withCancel, abort: () => controller.abort() }
|
||||
}
|
||||
```
|
||||
|
||||
## 请求重试
|
||||
|
||||
```typescript
|
||||
// utils/retryRequest.ts
|
||||
export async function retryRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: { retries?: number; delay?: number } = {}
|
||||
): Promise<T> {
|
||||
const { retries = 3, delay = 1000 } = options
|
||||
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (attempt === retries - 1) throw error
|
||||
await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)))
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded')
|
||||
}
|
||||
```
|
||||
|
||||
## 并发请求处理
|
||||
|
||||
```typescript
|
||||
// 使用 Promise.all 并发请求
|
||||
async function fetchDashboardData() {
|
||||
const [users, orders, stats] = await Promise.all([
|
||||
userApi.getList(),
|
||||
orderApi.getRecent(),
|
||||
statsApi.getSummary(),
|
||||
])
|
||||
|
||||
return { users, orders, stats }
|
||||
}
|
||||
|
||||
// 使用 Promise.allSettled 处理部分失败
|
||||
async function fetchWithFallback() {
|
||||
const results = await Promise.allSettled([
|
||||
userApi.getList(),
|
||||
orderApi.getList(),
|
||||
])
|
||||
|
||||
return results.map((result) =>
|
||||
result.status === 'fulfilled' ? result.value : []
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -1,182 +0,0 @@
|
||||
# 布局模式参考
|
||||
|
||||
## 标准页面骨架
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-container fluid class="d-flex flex-column h-100 pa-0">
|
||||
<!-- 固定头部 -->
|
||||
<v-toolbar density="compact" class="flex-shrink-0">
|
||||
<v-toolbar-title>页面标题</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-refresh" @click="refresh" />
|
||||
</v-toolbar>
|
||||
|
||||
<!-- 可滚动内容区 -->
|
||||
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-height: 0">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 固定底部(可选) -->
|
||||
<v-footer app class="flex-shrink-0">
|
||||
<v-btn block color="primary">操作</v-btn>
|
||||
</v-footer>
|
||||
</v-container>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Flexbox 滚动陷阱解决方案
|
||||
|
||||
```css
|
||||
/* 问题:子元素撑破父容器 */
|
||||
.parent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
/* 必须添加以下任一属性 */
|
||||
min-height: 0; /* 推荐 */
|
||||
/* 或 */
|
||||
overflow: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
## 粘性筛选栏
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="flex-grow-1 overflow-y-auto" style="min-height: 0">
|
||||
<!-- 粘性筛选区 -->
|
||||
<div class="sticky-top bg-surface pa-4" style="z-index: 1">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-text-field v-model="search" label="搜索" />
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-select v-model="status" :items="statusOptions" label="状态" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<v-list>...</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sticky-top {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 分栏布局(侧边栏 + 主内容)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="d-flex h-100">
|
||||
<!-- 固定宽度侧边栏 -->
|
||||
<v-navigation-drawer permanent width="280">
|
||||
<v-list nav>...</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- 自适应主内容 -->
|
||||
<div class="flex-grow-1 d-flex flex-column" style="min-width: 0">
|
||||
<v-toolbar>主内容头部</v-toolbar>
|
||||
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-height: 0">
|
||||
主内容区域
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 双栏详情布局
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<v-toolbar density="compact" class="flex-shrink-0">
|
||||
<v-btn icon="mdi-arrow-left" @click="goBack" />
|
||||
<v-toolbar-title>详情</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<div class="flex-grow-1 d-flex" style="min-height: 0">
|
||||
<!-- 左侧主信息 -->
|
||||
<div class="flex-grow-1 overflow-y-auto pa-4" style="min-width: 0">
|
||||
<v-card>...</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 -->
|
||||
<v-sheet width="320" class="flex-shrink-0 overflow-y-auto border-s">
|
||||
<v-list>相关信息</v-list>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tab 切换布局
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<v-tabs v-model="activeTab" class="flex-shrink-0">
|
||||
<v-tab value="info">基本信息</v-tab>
|
||||
<v-tab value="logs">操作日志</v-tab>
|
||||
<v-tab value="settings">设置</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-tabs-window v-model="activeTab" class="flex-grow-1" style="min-height: 0">
|
||||
<v-tabs-window-item value="info" class="h-100 overflow-y-auto">
|
||||
<!-- 内容 -->
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="logs" class="h-100 overflow-y-auto">
|
||||
<!-- 内容 -->
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="settings" class="h-100 overflow-y-auto">
|
||||
<!-- 内容 -->
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 响应式断点处理
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { mobile, mdAndUp, lgAndUp } = useDisplay()
|
||||
|
||||
// 根据屏幕尺寸调整列数
|
||||
const gridCols = computed(() => {
|
||||
if (lgAndUp.value) return 4
|
||||
if (mdAndUp.value) return 3
|
||||
return 2
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col v-for="item in items" :key="item.id" :cols="12 / gridCols">
|
||||
<v-card>...</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 移动端特殊处理 -->
|
||||
<v-bottom-navigation v-if="mobile" grow>
|
||||
<v-btn value="home">
|
||||
<v-icon>mdi-home</v-icon>
|
||||
<span>首页</span>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
</template>
|
||||
```
|
||||
@@ -1,142 +0,0 @@
|
||||
# TypeScript 严格规范
|
||||
|
||||
## 类型定义位置
|
||||
|
||||
```
|
||||
src/types/
|
||||
├── api.d.ts # ApiResponse, PageParams, etc.
|
||||
├── user.d.ts # User domain types
|
||||
├── order.d.ts # Order domain types
|
||||
└── common.d.ts # Shared utilities
|
||||
```
|
||||
|
||||
## 禁止 `any` 的替代方案
|
||||
|
||||
| 场景 | 错误 | 正确 |
|
||||
|------|------|------|
|
||||
| 未知对象 | `any` | `Record<string, unknown>` |
|
||||
| 动态数组 | `any[]` | `unknown[]` + type guard |
|
||||
| 回调函数 | `(x: any) => any` | 泛型 `<T>(x: T) => T` |
|
||||
| 第三方库缺失类型 | `any` | 创建 `.d.ts` 声明文件 |
|
||||
|
||||
## Props 与 Emits 类型化
|
||||
|
||||
```typescript
|
||||
// Props
|
||||
interface Props {
|
||||
user: User
|
||||
mode?: 'view' | 'edit'
|
||||
onSave?: (data: User) => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view',
|
||||
})
|
||||
|
||||
// Emits
|
||||
interface Emits {
|
||||
(e: 'update', value: User): void
|
||||
(e: 'cancel'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
```
|
||||
|
||||
## 泛型组合式函数
|
||||
|
||||
```typescript
|
||||
// composables/useFetch.ts
|
||||
export function useFetch<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
options?: { immediate?: boolean }
|
||||
) {
|
||||
const data = ref<T | null>(null) as Ref<T | null>
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
|
||||
async function execute() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
data.value = await fetcher()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e : new Error(String(e))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.immediate !== false) {
|
||||
onMounted(execute)
|
||||
}
|
||||
|
||||
return { data, loading, error, execute }
|
||||
}
|
||||
```
|
||||
|
||||
## 类型守卫
|
||||
|
||||
```typescript
|
||||
function isUser(obj: unknown): obj is User {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
'id' in obj &&
|
||||
'name' in obj
|
||||
)
|
||||
}
|
||||
|
||||
// 使用
|
||||
const data: unknown = await fetchData()
|
||||
if (isUser(data)) {
|
||||
console.log(data.name) // TypeScript 知道这是 User
|
||||
}
|
||||
```
|
||||
|
||||
## 联合类型与字面量类型
|
||||
|
||||
```typescript
|
||||
// 状态枚举替代方案
|
||||
type UserStatus = 'active' | 'disabled' | 'pending'
|
||||
|
||||
// 带类型的事件处理
|
||||
type TableAction =
|
||||
| { type: 'edit'; payload: User }
|
||||
| { type: 'delete'; payload: string }
|
||||
| { type: 'view'; payload: User }
|
||||
|
||||
function handleAction(action: TableAction) {
|
||||
switch (action.type) {
|
||||
case 'edit':
|
||||
openEditDialog(action.payload) // payload 是 User
|
||||
break
|
||||
case 'delete':
|
||||
confirmDelete(action.payload) // payload 是 string (id)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 工具类型使用
|
||||
|
||||
```typescript
|
||||
// Partial - 所有属性可选
|
||||
type UpdateUserDto = Partial<User>
|
||||
|
||||
// Pick - 选择部分属性
|
||||
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>
|
||||
|
||||
// Omit - 排除部分属性
|
||||
type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
|
||||
|
||||
// Required - 所有属性必填
|
||||
type CompleteUser = Required<User>
|
||||
|
||||
// Record - 键值映射
|
||||
type UserMap = Record<string, User>
|
||||
|
||||
// 自定义工具类型
|
||||
type Nullable<T> = T | null
|
||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
|
||||
T extends (...args: any) => Promise<infer R> ? R : never
|
||||
```
|
||||
@@ -1,345 +0,0 @@
|
||||
# UI 交互规范
|
||||
|
||||
## 数据表格
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:loading="loading"
|
||||
fixed-header
|
||||
height="calc(100vh - 200px)"
|
||||
>
|
||||
<!-- 截断文本 + Tooltip -->
|
||||
<template #item.description="{ value }">
|
||||
<v-tooltip :text="value" location="top">
|
||||
<template #activator="{ props }">
|
||||
<span
|
||||
v-bind="props"
|
||||
class="text-truncate d-inline-block"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #no-data>
|
||||
<v-empty-state
|
||||
icon="mdi-database-off"
|
||||
title="暂无数据"
|
||||
text="请尝试调整筛选条件或新建记录"
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn color="primary" @click="refresh">刷新</v-btn>
|
||||
</template>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 虚拟滚动列表
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-virtual-scroll :items="largeList" height="400" item-height="64">
|
||||
<template #default="{ item }">
|
||||
<v-list-item :title="item.name" :subtitle="item.description">
|
||||
<template #append>
|
||||
<v-btn icon="mdi-chevron-right" variant="text" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 骨架屏加载
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 表格骨架 -->
|
||||
<v-skeleton-loader v-if="loading" type="table-heading, table-row@5" />
|
||||
|
||||
<!-- 卡片骨架 -->
|
||||
<v-skeleton-loader v-if="loading" type="card" />
|
||||
|
||||
<!-- 列表骨架 -->
|
||||
<v-skeleton-loader v-if="loading" type="list-item-avatar-two-line@3" />
|
||||
|
||||
<!-- 文章骨架 -->
|
||||
<v-skeleton-loader v-if="loading" type="article" />
|
||||
|
||||
<!-- 自定义组合骨架 -->
|
||||
<v-skeleton-loader
|
||||
v-if="loading"
|
||||
type="heading, list-item-two-line@3, actions"
|
||||
/>
|
||||
|
||||
<!-- 实际内容 -->
|
||||
<template v-else>...</template>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 空状态组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-empty-state
|
||||
:icon="icon"
|
||||
:title="title"
|
||||
:text="description"
|
||||
>
|
||||
<template #actions>
|
||||
<v-btn v-if="showRefresh" variant="outlined" @click="$emit('refresh')">
|
||||
刷新
|
||||
</v-btn>
|
||||
<v-btn v-if="showCreate" color="primary" @click="$emit('create')">
|
||||
新建
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
icon?: string
|
||||
title?: string
|
||||
description?: string
|
||||
showRefresh?: boolean
|
||||
showCreate?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
icon: 'mdi-folder-open-outline',
|
||||
title: '暂无数据',
|
||||
description: '当前没有可显示的内容',
|
||||
showRefresh: true,
|
||||
showCreate: false,
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'refresh'): void
|
||||
(e: 'create'): void
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
## 多行文本截断
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="line-clamp-container">
|
||||
<p :class="{ 'line-clamp-2': !expanded }">{{ longText }}</p>
|
||||
<v-btn
|
||||
v-if="needsExpand"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
{{ expanded ? '收起' : '展开' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 确认对话框
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="400" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon :color="iconColor" class="mr-2">{{ icon }}</v-icon>
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
<v-card-text>{{ message }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="cancel">取消</v-btn>
|
||||
<v-btn :color="confirmColor" :loading="loading" @click="confirm">
|
||||
{{ confirmText }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title?: string
|
||||
message: string
|
||||
icon?: string
|
||||
iconColor?: string
|
||||
confirmText?: string
|
||||
confirmColor?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
title: '确认操作',
|
||||
icon: 'mdi-alert-circle-outline',
|
||||
iconColor: 'warning',
|
||||
confirmText: '确认',
|
||||
confirmColor: 'primary',
|
||||
})
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false })
|
||||
const loading = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
function confirm() {
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dialog.value = false
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 表单验证
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<v-form ref="formRef" v-model="valid" @submit.prevent="submit">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
:rules="rules.name"
|
||||
label="姓名"
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="form.email"
|
||||
:rules="rules.email"
|
||||
label="邮箱"
|
||||
type="email"
|
||||
/>
|
||||
<v-btn type="submit" :disabled="!valid" :loading="loading">
|
||||
提交
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { VForm } from 'vuetify/components'
|
||||
|
||||
const formRef = ref<VForm | null>(null)
|
||||
const valid = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
(v: string) => !!v || '姓名不能为空',
|
||||
(v: string) => v.length <= 20 || '姓名不能超过20个字符',
|
||||
],
|
||||
email: [
|
||||
(v: string) => !!v || '邮箱不能为空',
|
||||
(v: string) => /.+@.+\..+/.test(v) || '请输入有效的邮箱地址',
|
||||
],
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await api.submit(form)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Snackbar 全局通知
|
||||
|
||||
```typescript
|
||||
// composables/useSnackbar.ts
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface SnackbarOptions {
|
||||
text: string
|
||||
color?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
const snackbar = ref<SnackbarOptions & { show: boolean }>({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
timeout: 3000,
|
||||
})
|
||||
|
||||
export function useSnackbar() {
|
||||
function show(options: SnackbarOptions) {
|
||||
snackbar.value = { ...snackbar.value, ...options, show: true }
|
||||
}
|
||||
|
||||
function success(text: string) {
|
||||
show({ text, color: 'success' })
|
||||
}
|
||||
|
||||
function error(text: string) {
|
||||
show({ text, color: 'error', timeout: 5000 })
|
||||
}
|
||||
|
||||
function warning(text: string) {
|
||||
show({ text, color: 'warning' })
|
||||
}
|
||||
|
||||
function info(text: string) {
|
||||
show({ text, color: 'info' })
|
||||
}
|
||||
|
||||
return { snackbar, show, success, error, warning, info }
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- App.vue 中使用 -->
|
||||
<template>
|
||||
<v-app>
|
||||
<router-view />
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout"
|
||||
>
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSnackbar } from '@/composables/useSnackbar'
|
||||
|
||||
const { snackbar } = useSnackbar()
|
||||
</script>
|
||||
```
|
||||
Reference in New Issue
Block a user