生产管理系统前端 - fetchapi 基础提交
This commit is contained in:
178
crop-x/API_SETUP.md
Normal file
178
crop-x/API_SETUP.md
Normal 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 API,WebSocket 需要单独实现。
|
||||
|
||||
**Q: 如何添加 Mock 数据?**
|
||||
A: 可以创建另一个客户端实例,返回模拟数据而不是真实请求。
|
||||
493
crop-x/api/v1.yaml
Normal file
493
crop-x/api/v1.yaml
Normal 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
292
crop-x/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
14
crop-x/src/app/api-example/page.tsx
Normal file
14
crop-x/src/app/api-example/page.tsx
Normal 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 调用',
|
||||
};
|
||||
231
crop-x/src/components/examples/ApiExample.tsx
Normal file
231
crop-x/src/components/examples/ApiExample.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
204
crop-x/src/lib/api/client.ts
Normal file
204
crop-x/src/lib/api/client.ts
Normal 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
526
crop-x/src/lib/api/v1.d.ts
vendored
Normal 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>;
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
242
shadcn-color-guide.md
Normal 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 项目。*
|
||||
Reference in New Issue
Block a user