大量更新

This commit is contained in:
zeaslity
2026-03-18 16:16:47 +08:00
parent 8efefcc230
commit ed945abdf1
136 changed files with 28252 additions and 16 deletions

View File

@@ -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避免业务逻辑堆在 handlerDAO/Repo 只做数据访问与查询组装),并统一 API 响应包装
consistent response envelopecode/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/Shanghaitime 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 实现 / 代码走查与 Reviewrefactor 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
```

View File

@@ -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
}

View File

@@ -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, "用户创建成功")
}

View File

@@ -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
}

View File

@@ -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. 审计敏感操作
所有写操作需通过审计中间件记录。

View File

@@ -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 |

View File

@@ -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()`

View File

@@ -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 "未知错误"
}

View File

@@ -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)
```

View File

@@ -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 日志禁止缺少堆栈信息

View File

@@ -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
```

View File

@@ -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"
```

View File

@@ -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 "=== 验证完成 ==="

View File

@@ -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` 都有类型。每个边界情况都有处理。每个加载状态都很美观。这就是"生产级"的含义。

View File

@@ -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',
}),
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 : []
)
}
```

View File

@@ -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>
```

View File

@@ -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
```

View File

@@ -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>
```