生产管理系统前端 - fetchapi 基础提交

This commit is contained in:
2025-10-27 11:08:23 +08:00
parent 1f1d94ed84
commit 2b39c1dd1a
12 changed files with 2191 additions and 7 deletions

178
crop-x/API_SETUP.md Normal file
View File

@@ -0,0 +1,178 @@
# OpenAPI TypeScript 设置指南
## 🚀 快速开始
### 1. 已完成的步骤
✅ 创建了示例 OpenAPI 规范文件 (`./api/v1.yaml`)
✅ 生成了 TypeScript 类型定义 (`./src/lib/api/v1.d.ts`)
✅ 创建了 API 客户端 (`./src/lib/api/client.ts`)
✅ 创建了使用示例组件 (`./src/components/examples/ApiExample.tsx`)
### 2. 如何使用
#### 基本用法
```typescript
import { api } from '@/lib/api/client';
// 获取用户列表
const users = await api.users.getList({ page: 1, limit: 20 });
// 获取用户详情
const user = await api.users.getDetail(1);
// 创建农机
const newMachine = await api.machinery.create({
name: '新拖拉机',
type: 'tractor',
model: 'John Deere 6M',
});
```
#### 类型安全
```typescript
// ✅ 类型安全的 API 调用
const params = {
page: 1,
limit: 20,
status: 'running' as const, // TypeScript 会检查枚举值
};
// ❌ 错误会被 TypeScript 捕获
const wrongParams = {
status: 'invalid-status', // TypeScript 错误: 类型不匹配
};
```
## 🔄 更新 API 规范
### 当后端 API 发生变化时:
1. **更新 OpenAPI 规范文件**
```bash
# 方式一:手动编辑 ./api/v1.yaml
# 方式二:从后端自动生成(如果后端支持)
curl https://gitea-admin-test-app-app.dev.maimaiag.com/v3/api-docs > ./api/v1.yaml
```
2. **重新生成 TypeScript 类型**
```bash
npx openapi-typescript ./api/v1.yaml -o ./src/lib/api/v1.d.ts
```
3. **更新客户端代码**(如果接口有重大变化)
## 🛠️ 进阶配置
### 添加认证
```typescript
import { authClient } from '@/lib/api/client';
// 使用认证客户端
const token = localStorage.getItem('jwt-token');
const authenticatedApi = authClient.withAuth(token);
const user = await authenticatedApi.users.getDetail(1);
```
### 错误处理
```typescript
try {
const result = await api.users.getList();
// 处理成功响应
} catch (error) {
if (error.message.includes('404')) {
// 处理 404 错误
} else if (error.message.includes('401')) {
// 处理认证错误
}
}
```
### 自定义配置
```typescript
const customClient = createClient<paths>({
baseUrl: 'https://gitea-admin-test-app-app.dev.maimaiag.com/api/v1',
headers: {
'User-Agent': 'Smart-Crop-UI/1.0',
'X-API-Key': process.env.API_KEY,
},
// 添加请求拦截器
onRequest: async ({ request }) => {
// 可以在这里添加日志、重试逻辑等
console.log('发送请求:', request.url);
return request;
},
// 添加响应拦截器
onResponse: async ({ response }) => {
if (response.status === 401) {
// 处理 token 过期
window.location.href = '/login';
}
return response;
},
});
```
## 📁 文件结构
```
crop-x/
├── api/
│ └── v1.yaml # OpenAPI 规范文件
├── src/
│ ├── lib/
│ │ └── api/
│ │ ├── v1.d.ts # 生成的类型定义
│ │ └── client.ts # API 客户端封装
│ └── components/
│ └── examples/
│ └── ApiExample.tsx # 使用示例
```
## 🎯 最佳实践
### 1. 版本控制
- 将 `v1.yaml` 纳入版本控制
- 将生成的 `v1.d.ts` 也纳入版本控制,方便代码审查
- 在 CI/CD 中自动重新生成类型文件
### 2. 类型安全
- 优先使用 `api.machinery.create()` 这样的封装方法
- 避免直接使用 `client.POST()`
- 充分利用 TypeScript 的类型检查
### 3. 错误处理
- 在客户端封装中统一处理 API 错误
- 提供有意义的错误信息给用户
- 实现重试机制和降级策略
### 4. 性能优化
- 实现请求缓存
- 使用 React Query 或 SWR 进行数据获取
- 考虑添加请求去重
## 🔗 相关资源
- [openapi-typescript 官方文档](https://github.com/drwp/openapi-typescript)
- [OpenAPI 规范](https://swagger.io/specification/)
- [shadcn/ui](https://ui.shadcn.com/)
## ❓ 常见问题
**Q: 如何处理分页?**
A: API 已经支持分页参数 `page` 和 `limit`,响应中包含 `total` 字段。
**Q: 如何处理文件上传?**
A: 需要在 OpenAPI 规范中定义 `multipart/form-data` 格式的接口。
**Q: 如何实现 WebSocket 连接?**
A: 当前只支持 HTTP REST APIWebSocket 需要单独实现。
**Q: 如何添加 Mock 数据?**
A: 可以创建另一个客户端实例,返回模拟数据而不是真实请求。

493
crop-x/api/v1.yaml Normal file
View File

@@ -0,0 +1,493 @@
openapi: 3.0.3
info:
title: 智慧农业生产管理系统 API
description: 农业生产管理系统的后端接口文档
version: 1.0.0
contact:
name: API Support
email: support@smart-crop.com
servers:
- url: https://gitea-admin-test-app-app.dev.maimaiag.com/api/v1
description: 开发/测试/生产环境(统一地址)
paths:
# 用户管理
/users:
get:
summary: 获取用户列表
description: 获取所有用户的分页列表
tags:
- 用户管理
parameters:
- name: page
in: query
description: 页码
required: false
schema:
type: integer
default: 1
minimum: 1
- name: limit
in: query
description: 每页数量
required: false
schema:
type: integer
default: 20
minimum: 1
maximum: 100
- name: search
in: query
description: 搜索关键词
required: false
schema:
type: string
maxLength: 100
responses:
'200':
description: 成功获取用户列表
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: "success"
data:
type: object
properties:
users:
type: array
items:
$ref: '#/components/schemas/User'
total:
type: integer
example: 100
page:
type: integer
example: 1
limit:
type: integer
example: 20
/users/{id}:
get:
summary: 获取用户详情
description: 根据用户ID获取用户详细信息
tags:
- 用户管理
parameters:
- name: id
in: path
required: true
description: 用户ID
schema:
type: integer
minimum: 1
responses:
'200':
description: 成功获取用户详情
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: "success"
data:
$ref: '#/components/schemas/User'
'404':
description: 用户不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# 农机管理
/machinery:
get:
summary: 获取农机列表
description: 获取所有农机的分页列表
tags:
- 农机管理
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
- name: status
in: query
description: 农机状态筛选
schema:
type: string
enum: [running, idle, maintenance, error, offline]
responses:
'200':
description: 成功获取农机列表
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: "success"
data:
type: object
properties:
machinery:
type: array
items:
$ref: '#/components/schemas/Machinery'
total:
type: integer
example: 50
post:
summary: 创建农机
description: 创建新的农机记录
tags:
- 农机管理
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateMachineryRequest'
responses:
'201':
description: 农机创建成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 201
message:
type: string
example: "农机创建成功"
data:
$ref: '#/components/schemas/Machinery'
/machinery/{id}:
get:
summary: 获取农机详情
tags:
- 农机管理
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: 成功获取农机详情
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: "success"
data:
$ref: '#/components/schemas/Machinery'
put:
summary: 更新农机信息
tags:
- 农机管理
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateMachineryRequest'
responses:
'200':
description: 农机更新成功
content:
application/json:
schema:
type: object
properties:
code:
type: integer
example: 200
message:
type: string
example: "农机更新成功"
data:
$ref: '#/components/schemas/Machinery'
delete:
summary: 删除农机
tags:
- 农机管理
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'204':
description: 农机删除成功
components:
schemas:
User:
type: object
properties:
id:
type: integer
description: 用户ID
example: 1
username:
type: string
description: 用户名
example: "john_doe"
minLength: 3
maxLength: 50
email:
type: string
format: email
description: 邮箱地址
example: "john@example.com"
full_name:
type: string
description: 全名
example: "John Doe"
maxLength: 100
phone:
type: string
description: 手机号码
example: "13800138000"
pattern: "^1[3-9]\\d{9}$"
role:
type: string
enum: [admin, manager, operator, viewer]
description: 用户角色
example: "operator"
status:
type: string
enum: [active, inactive, suspended]
description: 用户状态
example: "active"
created_at:
type: string
format: date-time
description: 创建时间
example: "2024-01-15T10:30:00Z"
updated_at:
type: string
format: date-time
description: 更新时间
example: "2024-01-15T10:30:00Z"
Machinery:
type: object
properties:
id:
type: integer
description: 农机ID
example: 1
name:
type: string
description: 农机名称
example: "拖拉机-001"
maxLength: 100
type:
type: string
enum: [tractor, harvester, planter, sprayer, irrigation]
description: 农机类型
example: "tractor"
model:
type: string
description: 型号
example: "John Deere 6M Series"
maxLength: 50
serial_number:
type: string
description: 序列号
example: "JD6M123456"
maxLength: 50
status:
type: string
enum: [running, idle, maintenance, error, offline]
description: 农机状态
example: "idle"
location:
$ref: '#/components/schemas/Location'
operator:
$ref: '#/components/schemas/User'
purchase_date:
type: string
format: date
description: 购买日期
example: "2024-01-01"
last_maintenance:
type: string
format: date
description: 上次维护日期
example: "2024-06-15"
next_maintenance:
type: string
format: date
description: 下次维护日期
example: "2024-09-15"
created_at:
type: string
format: date-time
description: 创建时间
updated_at:
type: string
format: date-time
description: 更新时间
Location:
type: object
properties:
field_id:
type: integer
description: 地块ID
example: 1
field_name:
type: string
description: 地块名称
example: "北区A地块"
coordinates:
$ref: '#/components/schemas/Coordinates'
Coordinates:
type: object
properties:
latitude:
type: number
format: double
description: 纬度
example: 39.9042
minimum: -90
maximum: 90
longitude:
type: number
format: double
description: 经度
example: 116.4074
minimum: -180
maximum: 180
CreateMachineryRequest:
type: object
required:
- name
- type
- model
properties:
name:
type: string
description: 农机名称
minLength: 1
maxLength: 100
type:
type: string
enum: [tractor, harvester, planter, sprayer, irrigation]
description: 农机类型
model:
type: string
description: 型号
minLength: 1
maxLength: 50
serial_number:
type: string
description: 序列号
maxLength: 50
operator_id:
type: integer
description: 操作员ID
example: 1
purchase_date:
type: string
format: date
description: 购买日期
UpdateMachineryRequest:
type: object
properties:
name:
type: string
description: 农机名称
maxLength: 100
status:
type: string
enum: [running, idle, maintenance, error, offline]
description: 农机状态
operator_id:
type: integer
description: 操作员ID
last_maintenance:
type: string
format: date
description: 上次维护日期
next_maintenance:
type: string
format: date
description: 下次维护日期
Error:
type: object
properties:
code:
type: integer
description: 错误代码
example: 404
message:
type: string
description: 错误信息
example: "资源不存在"
details:
type: object
description: 错误详情
example:
field: "id"
reason: "用户ID不存在"
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT 认证令牌
security:
- BearerAuth: []

292
crop-x/package-lock.json generated
View File

@@ -46,6 +46,7 @@
"lucide-react": "^0.487.0",
"next": "^15.5.6",
"next-themes": "^0.4.6",
"openapi-fetch": "^0.15.0",
"qrcode": "*",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
@@ -78,10 +79,11 @@
"install": "^0.13.0",
"lint-staged": "^15.2.10",
"npm": "^11.6.2",
"openapi-typescript": "^7.10.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^4.1.14",
"typescript": "^5.6.2",
"typescript": "^5.9.3",
"vite": "^6.4.0"
}
},
@@ -98,6 +100,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -2931,6 +2958,79 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@redocly/ajv": {
"version": "8.11.3",
"resolved": "https://registry.npmmirror.com/@redocly/ajv/-/ajv-8.11.3.tgz",
"integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js-replace": "^1.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@redocly/ajv/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/config": {
"version": "0.22.2",
"resolved": "https://registry.npmmirror.com/@redocly/config/-/config-0.22.2.tgz",
"integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/openapi-core": {
"version": "1.34.5",
"resolved": "https://registry.npmmirror.com/@redocly/openapi-core/-/openapi-core-1.34.5.tgz",
"integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/ajv": "^8.11.2",
"@redocly/config": "^0.22.0",
"colorette": "^1.2.0",
"https-proxy-agent": "^7.0.5",
"js-levenshtein": "^1.1.6",
"js-yaml": "^4.1.0",
"minimatch": "^5.0.1",
"pluralize": "^8.0.0",
"yaml-ast-parser": "0.0.43"
},
"engines": {
"node": ">=18.17.0",
"npm": ">=9.5.0"
}
},
"node_modules/@redocly/openapi-core/node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
"dev": true,
"license": "MIT"
},
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -4457,6 +4557,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4474,6 +4584,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-escapes": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz",
@@ -5003,6 +5123,13 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmmirror.com/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"dev": true,
"license": "MIT"
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -7031,6 +7158,20 @@
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -7094,6 +7235,19 @@
"node": ">=0.8.19"
}
},
"node_modules/index-to-position": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/index-to-position/-/index-to-position-1.2.0.tgz",
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmmirror.com/input-otp/-/input-otp-1.4.2.tgz",
@@ -7608,6 +7762,16 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-levenshtein": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -11234,6 +11398,65 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-fetch": {
"version": "0.15.0",
"resolved": "https://registry.npmmirror.com/openapi-fetch/-/openapi-fetch-0.15.0.tgz",
"integrity": "sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==",
"license": "MIT",
"dependencies": {
"openapi-typescript-helpers": "^0.0.15"
}
},
"node_modules/openapi-typescript": {
"version": "7.10.1",
"resolved": "https://registry.npmmirror.com/openapi-typescript/-/openapi-typescript-7.10.1.tgz",
"integrity": "sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@redocly/openapi-core": "^1.34.5",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
"supports-color": "^10.2.2",
"yargs-parser": "^21.1.1"
},
"bin": {
"openapi-typescript": "bin/cli.js"
},
"peerDependencies": {
"typescript": "^5.x"
}
},
"node_modules/openapi-typescript-helpers": {
"version": "0.0.15",
"resolved": "https://registry.npmmirror.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz",
"integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==",
"license": "MIT"
},
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz",
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/openapi-typescript/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -11324,6 +11547,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "8.3.0",
"resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-8.3.0.tgz",
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"index-to-position": "^1.1.0",
"type-fest": "^4.39.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -11382,6 +11623,16 @@
"node": ">=0.10"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-8.0.0.tgz",
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -11799,6 +12050,16 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -12782,6 +13043,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -12862,7 +13136,7 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
@@ -12976,6 +13250,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/uri-js-replace": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
"dev": true,
"license": "MIT"
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@@ -13350,6 +13631,13 @@
"node": ">= 14.6"
}
},
"node_modules/yaml-ast-parser": {
"version": "0.0.43",
"resolved": "https://registry.npmmirror.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",

View File

@@ -56,6 +56,7 @@
"lucide-react": "^0.487.0",
"next": "^15.5.6",
"next-themes": "^0.4.6",
"openapi-fetch": "^0.15.0",
"qrcode": "*",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
@@ -71,8 +72,8 @@
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@tailwindcss/postcss": "^4",
"@tailwindcss/vite": "^4.1.14",
"@types/node": "^20.10.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
@@ -88,10 +89,11 @@
"install": "^0.13.0",
"lint-staged": "^15.2.10",
"npm": "^11.6.2",
"openapi-typescript": "^7.10.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^4.1.14",
"typescript": "^5.6.2",
"typescript": "^5.9.3",
"vite": "^6.4.0"
}
}

View File

@@ -0,0 +1,14 @@
import ApiExample from '@/components/examples/ApiExample';
export default function ApiExamplePage() {
return (
<div className="min-h-screen bg-background">
<ApiExample />
</div>
);
}
export const metadata = {
title: 'API 调用示例 - 智慧农业生产管理系统',
description: '测试和展示 OpenAPI 客户端的类型安全 API 调用',
};

View File

@@ -0,0 +1,231 @@
'use client';
import React, { useState, useEffect } from 'react';
import { api, testConnection, type User, type Machinery } from '@/lib/api/client';
export default function ApiExample() {
const [users, setUsers] = useState<User[]>([]);
const [machinery, setMachinery] = useState<Machinery[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [connectionStatus, setConnectionStatus] = useState<'testing' | 'connected' | 'disconnected'>('testing');
// 获取用户列表
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const result = await api.users.getList({ page: 1, limit: 10 });
if (result?.data?.users) {
setUsers(result.data.users);
}
} catch (err) {
setError(err instanceof Error ? err.message : '获取用户失败');
} finally {
setLoading(false);
}
};
// 获取农机列表
const fetchMachinery = async () => {
setLoading(true);
setError(null);
try {
const result = await api.machinery.getList({ page: 1, limit: 10 });
if (result?.data?.machinery) {
setMachinery(result.data.machinery);
}
} catch (err) {
setError(err instanceof Error ? err.message : '获取农机失败');
} finally {
setLoading(false);
}
};
// 创建新农机
const createMachinery = async () => {
try {
const newMachinery = await api.machinery.create({
name: '新拖拉机',
type: 'tractor',
model: 'John Deere 6M',
serial_number: 'JD6M123456',
purchase_date: new Date().toISOString().split('T')[0],
});
console.log('创建成功:', newMachinery);
// 刷新列表
fetchMachinery();
} catch (err) {
setError(err instanceof Error ? err.message : '创建农机失败');
}
};
// 测试 API 连接
const testApiConnection = async () => {
setConnectionStatus('testing');
const result = await testConnection();
if (result.success) {
setConnectionStatus('connected');
// 连接成功后获取数据
fetchUsers();
fetchMachinery();
} else {
setConnectionStatus('disconnected');
setError(`API 连接失败: ${result.error}`);
}
};
useEffect(() => {
testApiConnection();
}, []);
if (loading) {
return <div className="p-4">...</div>;
}
if (error) {
return (
<div className="p-4">
<div className="text-red-600">: {error}</div>
<button
onClick={() => {
setError(null);
fetchUsers();
fetchMachinery();
}}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
}
// 连接状态显示
const renderConnectionStatus = () => {
switch (connectionStatus) {
case 'testing':
return (
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
<span className="text-blue-800"> API ...</span>
</div>
</div>
);
case 'connected':
return (
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
<span className="text-green-800">API </span>
</div>
<button
onClick={testApiConnection}
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
>
</button>
</div>
</div>
);
case 'disconnected':
return (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
<span className="text-red-800">API </span>
</div>
<div className="text-sm text-red-600 mt-1">{error}</div>
</div>
<button
onClick={testApiConnection}
className="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700"
>
</button>
</div>
</div>
);
}
};
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">API </h1>
{/* 连接状态 */}
{renderConnectionStatus()}
{/* 服务器信息 */}
<div className="mb-6 p-4 bg-muted rounded-lg">
<h3 className="font-semibold mb-2"></h3>
<div className="text-sm text-muted-foreground space-y-1">
<div><strong>Base URL:</strong> https://gitea-admin-test-app-app.dev.maimaiag.com/api/v1</div>
<div><strong>//:</strong> 使</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 用户列表 */}
<div className="bg-card border rounded-lg p-4">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2">
{users.map((user) => (
<div key={user.id} className="p-3 bg-muted rounded">
<div className="font-medium">{user.full_name || user.username}</div>
<div className="text-sm text-muted-foreground">
{user.email} {user.role} {user.status}
</div>
</div>
))}
</div>
</div>
{/* 农机列表 */}
<div className="bg-card border rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold"></h2>
<button
onClick={createMachinery}
className="px-3 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90"
>
</button>
</div>
<div className="space-y-2">
{machinery.map((machine) => (
<div key={machine.id} className="p-3 bg-muted rounded">
<div className="font-medium">{machine.name}</div>
<div className="text-sm text-muted-foreground">
{machine.type} {machine.model} {machine.status}
</div>
{machine.operator && (
<div className="text-xs text-muted-foreground mt-1">
: {machine.operator.full_name || machine.operator.username}
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* 类型安全示例 */}
<div className="mt-8 p-4 bg-muted rounded-lg">
<h3 className="text-lg font-semibold mb-2"></h3>
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
<li> API </li>
<li> </li>
<li> </li>
<li> IDE </li>
</ul>
</div>
</div>
);
}

View File

@@ -112,6 +112,12 @@ const navbarData = {
description: "租户管理、用户管理、系统监控",
icon: <Settings className="size-5 shrink-0" />,
},
{
title: "API 测试示例",
url: "/api-example",
description: "测试和展示 OpenAPI 客户端调用",
icon: <Brain className="size-5 shrink-0" />,
},
],
auth: {
login: { title: "登录", url: "/login" },

View File

@@ -0,0 +1,204 @@
import createClient from 'openapi-fetch';
import type { paths } from './v1.d.ts';
// 创建 API 客户端
const client = createClient<paths>({
baseUrl: 'https://gitea-admin-test-app-app.dev.maimaiag.com/docs',
// 可以添加默认 headers
headers: {
'Content-Type': 'application/json',
},
});
// 添加认证的客户端
export const authClient = {
...client,
// 包装添加 token 的方法
withAuth: (token: string) => ({
...client,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
}),
};
// 测试连接
export const testConnection = async () => {
try {
// 尝试获取一个简单的接口来测试连接
const { data, error, response } = await client.GET('/users', {
params: {
query: { page: 1, limit: 1 }
}
});
if (error) {
console.warn('API 连接测试失败:', error);
return { success: false, error };
}
console.log('API 连接测试成功:', { status: response?.status, data });
return { success: true, data };
} catch (err) {
console.error('API 连接测试出错:', err);
return { success: false, error: err instanceof Error ? err.message : '未知错误' };
}
};
// API 方法封装
export const api = {
// 用户管理 API
users: {
// 获取用户列表
getList: async (params?: {
page?: number;
limit?: number;
search?: string;
}) => {
const { data, error, response } = await client.GET('/users', {
params: {
query: params,
},
});
if (error) {
console.error('获取用户列表失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
// 获取用户详情
getDetail: async (id: number) => {
const { data, error } = await client.GET('/users/{id}', {
params: {
path: { id },
},
});
if (error) {
console.error('获取用户详情失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
},
// 农机管理 API
machinery: {
// 获取农机列表
getList: async (params?: {
page?: number;
limit?: number;
status?: 'running' | 'idle' | 'maintenance' | 'error' | 'offline';
}) => {
const { data, error } = await client.GET('/machinery', {
params: {
query: params,
},
});
if (error) {
console.error('获取农机列表失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
// 获取农机详情
getDetail: async (id: number) => {
const { data, error } = await client.GET('/machinery/{id}', {
params: {
path: { id },
},
});
if (error) {
console.error('获取农机详情失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
// 创建农机
create: async (machineryData: {
name: string;
type: 'tractor' | 'harvester' | 'planter' | 'sprayer' | 'irrigation';
model: string;
serial_number?: string;
operator_id?: number;
purchase_date?: string;
}) => {
const { data, error } = await client.POST('/machinery', {
body: machineryData,
});
if (error) {
console.error('创建农机失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
// 更新农机
update: async (
id: number,
updateData: {
name?: string;
status?: 'running' | 'idle' | 'maintenance' | 'error' | 'offline';
operator_id?: number;
last_maintenance?: string;
next_maintenance?: string;
}
) => {
const { data, error } = await client.PUT('/machinery/{id}', {
params: {
path: { id },
},
body: updateData,
});
if (error) {
console.error('更新农机失败:', error);
throw new Error(`API Error: ${error}`);
}
return data;
},
// 删除农机
delete: async (id: number) => {
const { error } = await client.DELETE('/machinery/{id}', {
params: {
path: { id },
},
});
if (error) {
console.error('删除农机失败:', error);
throw new Error(`API Error: ${error}`);
}
return true; // 删除成功
},
},
};
// 类型导出(供组件使用)
export type {
User,
Machinery,
Location,
Coordinates,
CreateMachineryRequest,
UpdateMachineryRequest,
Error as ApiError
} from './v1.d.ts';
export default client;

526
crop-x/src/lib/api/v1.d.ts vendored Normal file
View File

@@ -0,0 +1,526 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/users": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 获取用户列表
* @description 获取所有用户的分页列表
*/
get: {
parameters: {
query?: {
/** @description 页码 */
page?: number;
/** @description 每页数量 */
limit?: number;
/** @description 搜索关键词 */
search?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 成功获取用户列表 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 200 */
code?: number;
/** @example success */
message?: string;
data?: {
users?: components["schemas"]["User"][];
/** @example 100 */
total?: number;
/** @example 1 */
page?: number;
/** @example 20 */
limit?: number;
};
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/users/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 获取用户详情
* @description 根据用户ID获取用户详细信息
*/
get: {
parameters: {
query?: never;
header?: never;
path: {
/** @description 用户ID */
id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 成功获取用户详情 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 200 */
code?: number;
/** @example success */
message?: string;
data?: components["schemas"]["User"];
};
};
};
/** @description 用户不存在 */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/machinery": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 获取农机列表
* @description 获取所有农机的分页列表
*/
get: {
parameters: {
query?: {
page?: number;
limit?: number;
/** @description 农机状态筛选 */
status?: "running" | "idle" | "maintenance" | "error" | "offline";
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 成功获取农机列表 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 200 */
code?: number;
/** @example success */
message?: string;
data?: {
machinery?: components["schemas"]["Machinery"][];
/** @example 50 */
total?: number;
};
};
};
};
};
};
put?: never;
/**
* 创建农机
* @description 创建新的农机记录
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateMachineryRequest"];
};
};
responses: {
/** @description 农机创建成功 */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 201 */
code?: number;
/** @example 农机创建成功 */
message?: string;
data?: components["schemas"]["Machinery"];
};
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/machinery/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** 获取农机详情 */
get: {
parameters: {
query?: never;
header?: never;
path: {
id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 成功获取农机详情 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 200 */
code?: number;
/** @example success */
message?: string;
data?: components["schemas"]["Machinery"];
};
};
};
};
};
/** 更新农机信息 */
put: {
parameters: {
query?: never;
header?: never;
path: {
id: number;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateMachineryRequest"];
};
};
responses: {
/** @description 农机更新成功 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @example 200 */
code?: number;
/** @example 农机更新成功 */
message?: string;
data?: components["schemas"]["Machinery"];
};
};
};
};
};
post?: never;
/** 删除农机 */
delete: {
parameters: {
query?: never;
header?: never;
path: {
id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 农机删除成功 */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
User: {
/**
* @description 用户ID
* @example 1
*/
id?: number;
/**
* @description 用户名
* @example john_doe
*/
username?: string;
/**
* Format: email
* @description 邮箱地址
* @example john@example.com
*/
email?: string;
/**
* @description 全名
* @example John Doe
*/
full_name?: string;
/**
* @description 手机号码
* @example 13800138000
*/
phone?: string;
/**
* @description 用户角色
* @example operator
* @enum {string}
*/
role?: "admin" | "manager" | "operator" | "viewer";
/**
* @description 用户状态
* @example active
* @enum {string}
*/
status?: "active" | "inactive" | "suspended";
/**
* Format: date-time
* @description 创建时间
* @example 2024-01-15T10:30:00Z
*/
created_at?: string;
/**
* Format: date-time
* @description 更新时间
* @example 2024-01-15T10:30:00Z
*/
updated_at?: string;
};
Machinery: {
/**
* @description 农机ID
* @example 1
*/
id?: number;
/**
* @description 农机名称
* @example 拖拉机-001
*/
name?: string;
/**
* @description 农机类型
* @example tractor
* @enum {string}
*/
type?: "tractor" | "harvester" | "planter" | "sprayer" | "irrigation";
/**
* @description 型号
* @example John Deere 6M Series
*/
model?: string;
/**
* @description 序列号
* @example JD6M123456
*/
serial_number?: string;
/**
* @description 农机状态
* @example idle
* @enum {string}
*/
status?: "running" | "idle" | "maintenance" | "error" | "offline";
location?: components["schemas"]["Location"];
operator?: components["schemas"]["User"];
/**
* Format: date
* @description 购买日期
* @example 2024-01-01
*/
purchase_date?: string;
/**
* Format: date
* @description 上次维护日期
* @example 2024-06-15
*/
last_maintenance?: string;
/**
* Format: date
* @description 下次维护日期
* @example 2024-09-15
*/
next_maintenance?: string;
/**
* Format: date-time
* @description 创建时间
*/
created_at?: string;
/**
* Format: date-time
* @description 更新时间
*/
updated_at?: string;
};
Location: {
/**
* @description 地块ID
* @example 1
*/
field_id?: number;
/**
* @description 地块名称
* @example 北区A地块
*/
field_name?: string;
coordinates?: components["schemas"]["Coordinates"];
};
Coordinates: {
/**
* Format: double
* @description 纬度
* @example 39.9042
*/
latitude?: number;
/**
* Format: double
* @description 经度
* @example 116.4074
*/
longitude?: number;
};
CreateMachineryRequest: {
/** @description 农机名称 */
name: string;
/**
* @description 农机类型
* @enum {string}
*/
type: "tractor" | "harvester" | "planter" | "sprayer" | "irrigation";
/** @description 型号 */
model: string;
/** @description 序列号 */
serial_number?: string;
/**
* @description 操作员ID
* @example 1
*/
operator_id?: number;
/**
* Format: date
* @description 购买日期
*/
purchase_date?: string;
};
UpdateMachineryRequest: {
/** @description 农机名称 */
name?: string;
/**
* @description 农机状态
* @enum {string}
*/
status?: "running" | "idle" | "maintenance" | "error" | "offline";
/** @description 操作员ID */
operator_id?: number;
/**
* Format: date
* @description 上次维护日期
*/
last_maintenance?: string;
/**
* Format: date
* @description 下次维护日期
*/
next_maintenance?: string;
};
Error: {
/**
* @description 错误代码
* @example 404
*/
code?: number;
/**
* @description 错误信息
* @example 资源不存在
*/
message?: string;
/**
* @description 错误详情
* @example {
* "field": "id",
* "reason": "用户ID不存在"
* }
*/
details?: Record<string, never>;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;

View File

@@ -25,7 +25,7 @@
--accent-foreground: 240 10% 10%;
--destructive: 0 84% 60%;
--destructive-foreground: 240 10% 98%;
--border: 240 4% 90%;
--border: rgba(0, 0, 0, 0.1);
--input: 240 4% 90%;
--ring: 142 76% 36%;
--radius: 0.5rem;
@@ -91,7 +91,7 @@
--accent-foreground: 240 10% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 240 10% 98%;
--border: 240 3% 15%;
--border: rgba(255, 255, 255, 0.1);
--input: 240 3% 15%;
--ring: 142 70% 45%;
--sidebar: hsl(240 5.9% 10%);

View File

@@ -11,7 +11,7 @@ export default {
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
border: 'var(--border)',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',

242
shadcn-color-guide.md Normal file
View File

@@ -0,0 +1,242 @@
# shadcn UI 配色系统完整指南
## 概述
shadcn UI 使用基于 CSS 变量的分层配色系统,通过语义化的变量名来管理整个应用的色彩方案。系统分为亮色主题(`:root`)和暗色主题(`.dark`)两套配色。
## 🎨 颜色层级分类
### 1. 核心系统颜色 (Core System Colors)
#### 主要交互色 (Primary Colors)
- **`--primary`**: 应用主题色,用于最重要的交互元素
- 亮色: `hsl(349.5238 100% 87.6471%)` - 粉红色
- 暗色: `hsl(330 100.0000% 80%)` - 粉红色
- **`--primary-foreground`**: 主要色上的文字/图标颜色
- 亮色: `hsl(0 0% 0%)` - 黑色
- 暗色: `hsl(0 0% 0%)` - 黑色
**用途**: 按钮、链接、选中状态、重要高亮等主要交互元素
#### 次要交互色 (Secondary Colors)
- **`--secondary`**: 次要按钮和元素背景色
- 亮色: `hsl(197.4000 71.4286% 72.5490%)` - 蓝色
- 暗色: `hsl(120 60.0000% 50%)` - 绿色
- **`--secondary-foreground`**: 次要色上的文字/图标颜色
- 亮色: `hsl(0 0% 0%)` - 黑色
- 暗色: `hsl(0 0% 0%)` - 黑色
**用途**: 次要按钮、标签、分页器等辅助交互元素
#### 强调色 (Accent Colors)
- **`--accent`**: 悬停和强调状态
- 亮色: `hsl(60 100% 50%)` - 黄色
- 暗色: `hsl(197.4000 71.4286% 72.5490%)` - 蓝色
- **`--accent-foreground`**: 强调色上的文字/图标颜色
- 亮色: `hsl(0 0% 0%)` - 黑色
- 暗色: `hsl(0 0% 0%)` - 黑色
**用途**: 悬停状态、选中背景、高亮区域
### 2. 背景和前景色 (Background & Foreground)
#### 基础背景色
- **`--background`**: 应用主背景色
- 亮色: `hsl(200 23.0769% 97.4510%)` - 浅蓝灰
- 暗色: `hsl(220.0000 14.7541% 11.9608%)` - 深蓝灰
- **`--foreground`**: 主要文字颜色
- 亮色: `hsl(0 0% 20%)` - 深灰
- 暗色: `hsl(0 0% 89.8039%)` - 浅灰
#### 卡片背景色
- **`--card`**: 卡片和弹出层背景
- 亮色: `hsl(0 0% 100%)` - 纯白
- 暗色: `hsl(197.1429 6.9307% 19.8039%)` - 深蓝灰
- **`--card-foreground`**: 卡片上的文字颜色
- 亮色: `hsl(0 0% 20%)` - 深灰
- 暗色: `hsl(0 0% 89.8039%)` - 浅灰
#### 弹出层背景色
- **`--popover`**: 弹出层、下拉菜单背景
- 亮色: `hsl(0 0% 100%)` - 纯白
- 暗色: `hsl(197.1429 6.9307% 19.8039%)` - 深蓝灰
- **`--popover-foreground`**: 弹出层文字颜色
- 亮色: `hsl(0 0% 20%)` - 深灰
- 暗色: `hsl(0 0% 89.8039%)` - 浅灰
### 3. 弱化和辅助色 (Muted & Support)
#### 弱化元素
- **`--muted`**: 弱化背景色
- 亮色: `hsl(50.4000 26.8817% 81.7647%)` - 浅黄灰
- 暗色: `hsl(0 0% 26.6667%)` - 深灰
- **`--muted-foreground`**: 弱化文字颜色
- 亮色: `hsl(0 0% 43.1373%)` - 中灰
- 暗色: `hsl(0 0% 63.9216%)` - 中浅灰
**用途**: 禁用状态、占位符、不重要的信息
### 4. 边框和输入色 (Border & Input)
- **`--border`**: 边框颜色
- 亮色: `hsl(0 0% 83.1373%)` - 浅灰
- 暗色: `hsl(0 0% 26.6667%)` - 深灰
- **`--input`**: 输入框边框色
- 亮色: `hsl(0 0% 83.1373%)` - 浅灰
- 暗色: `hsl(0 0% 26.6667%)` - 深灰
### 5. 危险和警告色 (Destructive)
- **`--destructive`**: 危险操作色(删除、警告等)
- 亮色: `hsl(0 84.2365% 60.1961%)` - 红色
- 暗色: `hsl(0 84.2365% 60.1961%)` - 红色
- **`--destructive-foreground`**: 危险色上的文字颜色
- 亮色: `hsl(0 0% 100%)` - 白色
- 暗色: `hsl(0 0% 100%)` - 白色
**用途**: 删除按钮、错误提示、警告信息
### 6. 焦点和环色 (Ring)
- **`--ring`**: 焦点环颜色
- 亮色: `hsl(349.5238 100% 87.6471%)` - 粉红色
- 暗色: `hsl(330 100.0000% 80%)` - 粉红色
**用途**: 键盘焦点环、表单验证高亮
### 7. 图表色 (Chart Colors)
- **`--chart-1`**: 图表主色 (粉色系)
- **`--chart-2`**: 图表次要色 (蓝色/绿色系)
- **`--chart-3`**: 图表第三色 (黄色/蓝色系)
- **`--chart-4`**: 图表第四色 (黄色系)
- **`--chart-5`**: 图表第五色 (黄绿色系)
### 8. 侧边栏色 (Sidebar Colors)
- **`--sidebar`**: 侧边栏背景色
- **`--sidebar-foreground`**: 侧边栏文字色
- **`--sidebar-primary`**: 侧边栏主要交互色
- **`--sidebar-primary-foreground`**: 侧边栏主要文字色
- **`--sidebar-accent`**: 侧边栏强调色
- **`--sidebar-accent-foreground`**: 侧边栏强调文字色
- **`--sidebar-border`**: 侧边栏边框色
- **`--sidebar-ring`**: 侧边栏焦点环色
## 🏗️ 使用优先级和层级
### 1. 主要色 (Primary) - 最高优先级
- 主按钮
- 重要链接
- 选中状态
- 进度指示器
- 主要数据展示
### 2. 次要色 (Secondary) - 中等优先级
- 次要按钮
- 标签和徽章
- 分页组件
- 辅助交互元素
### 3. 强调色 (Accent) - 低优先级
- 悬停状态
- 选中背景
- 高亮区域
- 过渡效果
### 4. 弱化色 (Muted) - 最低优先级
- 禁用状态
- 占位符文本
- 辅助信息
- 背景装饰
## 🎯 实际应用示例
### 按钮组件
```jsx
// 主要按钮
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
主要操作
</button>
// 次要按钮
<button className="bg-secondary text-secondary-foreground hover:bg-secondary/80">
次要操作
</button>
// 危险按钮
<button className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
删除
</button>
```
### 卡片组件
```jsx
<div className="bg-card text-card-foreground border border-border rounded-lg p-4">
<h2 className="text-foreground">卡片标题</h2>
<p className="text-muted-foreground">辅助信息</p>
</div>
```
### 输入框组件
```jsx
<input className="bg-background text-foreground border border-input focus:ring-2 focus:ring-ring"
placeholder="占位符文本" />
```
## 🌓 主题切换原理
系统通过 CSS 变量实现主题切换:
```css
/* 亮色主题 */
:root {
--background: hsl(200 23.0769% 97.4510%);
--foreground: hsl(0 0% 20%);
/* ... */
}
/* 暗色主题 */
.dark {
--background: hsl(220.0000 14.7541% 11.9608%);
--foreground: hsl(0 0% 89.8039%);
/* ... */
}
```
当 HTML 元素添加 `.dark` 类时,所有使用这些变量的组件都会自动切换颜色。
## 🎨 定制建议
### 1. 保持语义化
- 保持变量名的语义含义,不要为了视觉效果而改变变量用途
- `primary` 始终用于主要交互,`destructive` 始终用于危险操作
### 2. 确保对比度
- 确保 `*-foreground` 颜色与对应的背景色有足够的对比度
- 在两个主题下都要测试可读性
### 3. 渐进增强
- 先定义好核心颜色,再扩展其他颜色
- 使用 HSL 色彩空间便于调整饱和度和亮度
### 4. 一致性原则
- 保持相同类型的组件使用相同的颜色变量
- 避免在组件中硬编码颜色值
## 🔧 维护工具
### 检查颜色使用
```bash
# 查找所有使用 primary 变量的地方
grep -r "bg-primary\|text-primary\|border-primary" src/
```
### 主题切换测试
1. 在浏览器开发者工具中切换 HTML 元素的 class
2. 检查所有组件的颜色是否正确切换
3. 验证对比度和可读性
---
*本文档基于 shadcn UI 标准配色系统编写,适用于 React + Tailwind CSS 项目。*