diff --git a/crop-x/API_SETUP.md b/crop-x/API_SETUP.md new file mode 100644 index 0000000..31457b0 --- /dev/null +++ b/crop-x/API_SETUP.md @@ -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({ + 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: 可以创建另一个客户端实例,返回模拟数据而不是真实请求。 \ No newline at end of file diff --git a/crop-x/api/v1.yaml b/crop-x/api/v1.yaml new file mode 100644 index 0000000..8dcec37 --- /dev/null +++ b/crop-x/api/v1.yaml @@ -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: [] \ No newline at end of file diff --git a/crop-x/package-lock.json b/crop-x/package-lock.json index 19dd08b..a137889 100644 --- a/crop-x/package-lock.json +++ b/crop-x/package-lock.json @@ -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", diff --git a/crop-x/package.json b/crop-x/package.json index 2ebea3f..5a298c1 100644 --- a/crop-x/package.json +++ b/crop-x/package.json @@ -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" } } diff --git a/crop-x/src/app/api-example/page.tsx b/crop-x/src/app/api-example/page.tsx new file mode 100644 index 0000000..25bd939 --- /dev/null +++ b/crop-x/src/app/api-example/page.tsx @@ -0,0 +1,14 @@ +import ApiExample from '@/components/examples/ApiExample'; + +export default function ApiExamplePage() { + return ( +
+ +
+ ); +} + +export const metadata = { + title: 'API 调用示例 - 智慧农业生产管理系统', + description: '测试和展示 OpenAPI 客户端的类型安全 API 调用', +}; \ No newline at end of file diff --git a/crop-x/src/components/examples/ApiExample.tsx b/crop-x/src/components/examples/ApiExample.tsx new file mode 100644 index 0000000..f6c0b3d --- /dev/null +++ b/crop-x/src/components/examples/ApiExample.tsx @@ -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([]); + const [machinery, setMachinery] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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
加载中...
; + } + + if (error) { + return ( +
+
错误: {error}
+ +
+ ); + } + + // 连接状态显示 + const renderConnectionStatus = () => { + switch (connectionStatus) { + case 'testing': + return ( +
+
+
+ 正在测试 API 连接... +
+
+ ); + case 'connected': + return ( +
+
+
+
+ API 连接成功 +
+ +
+
+ ); + case 'disconnected': + return ( +
+
+
+
+
+ API 连接失败 +
+
{error}
+
+ +
+
+ ); + } + }; + + return ( +
+

API 调用示例

+ + {/* 连接状态 */} + {renderConnectionStatus()} + + {/* 服务器信息 */} +
+

服务器配置

+
+
Base URL: https://gitea-admin-test-app-app.dev.maimaiag.com/api/v1
+
开发/测试/生产: 统一使用此地址
+
+
+ +
+ {/* 用户列表 */} +
+

用户列表

+
+ {users.map((user) => ( +
+
{user.full_name || user.username}
+
+ {user.email} • {user.role} • {user.status} +
+
+ ))} +
+
+ + {/* 农机列表 */} +
+
+

农机列表

+ +
+
+ {machinery.map((machine) => ( +
+
{machine.name}
+
+ {machine.type} • {machine.model} • {machine.status} +
+ {machine.operator && ( +
+ 操作员: {machine.operator.full_name || machine.operator.username} +
+ )} +
+ ))} +
+
+
+ + {/* 类型安全示例 */} +
+

类型安全优势

+
    +
  • ✅ API 调用参数有完整的类型检查
  • +
  • ✅ 响应数据有自动的类型推断
  • +
  • ✅ 编译时就能发现类型错误
  • +
  • ✅ IDE 支持自动补全和提示
  • +
+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/components/layouts/Navbar.tsx b/crop-x/src/components/layouts/Navbar.tsx index 3c2bbbb..d27c94d 100644 --- a/crop-x/src/components/layouts/Navbar.tsx +++ b/crop-x/src/components/layouts/Navbar.tsx @@ -112,6 +112,12 @@ const navbarData = { description: "租户管理、用户管理、系统监控", icon: , }, + { + title: "API 测试示例", + url: "/api-example", + description: "测试和展示 OpenAPI 客户端调用", + icon: , + }, ], auth: { login: { title: "登录", url: "/login" }, diff --git a/crop-x/src/lib/api/client.ts b/crop-x/src/lib/api/client.ts new file mode 100644 index 0000000..8703785 --- /dev/null +++ b/crop-x/src/lib/api/client.ts @@ -0,0 +1,204 @@ +import createClient from 'openapi-fetch'; +import type { paths } from './v1.d.ts'; + +// 创建 API 客户端 +const client = createClient({ + 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; \ No newline at end of file diff --git a/crop-x/src/lib/api/v1.d.ts b/crop-x/src/lib/api/v1.d.ts new file mode 100644 index 0000000..5d4fc02 --- /dev/null +++ b/crop-x/src/lib/api/v1.d.ts @@ -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; +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; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/crop-x/src/styles/globals.css b/crop-x/src/styles/globals.css index 8790a3c..81ac166 100644 --- a/crop-x/src/styles/globals.css +++ b/crop-x/src/styles/globals.css @@ -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%); diff --git a/crop-x/tailwind.config.js b/crop-x/tailwind.config.js index ec39fc4..0e85fc7 100644 --- a/crop-x/tailwind.config.js +++ b/crop-x/tailwind.config.js @@ -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))', diff --git a/shadcn-color-guide.md b/shadcn-color-guide.md new file mode 100644 index 0000000..409336d --- /dev/null +++ b/shadcn-color-guide.md @@ -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 +// 主要按钮 + + +// 次要按钮 + + +// 危险按钮 + +``` + +### 卡片组件 +```jsx +
+

卡片标题

+

辅助信息

+
+``` + +### 输入框组件 +```jsx + +``` + +## 🌓 主题切换原理 + +系统通过 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 项目。* \ No newline at end of file