Compare commits

...

3 Commits

Author SHA1 Message Date
66377c618d 生产管理系统 nextjs标准化改造 2025-11-11 19:50:37 +08:00
fafd2928b2 生产管理系统 - deploy文件夹修改 2025-11-11 11:29:16 +08:00
671a621315 生产管理系统 - 页面上路由参数缓存 2025-11-11 10:28:02 +08:00
50 changed files with 15306 additions and 17733 deletions

View File

@@ -1,27 +0,0 @@
{
"tools": {
"eslint": {
"enabled": false,
"description": "ESLint代码检查工具",
"configFile": ".eslintrc.cjs",
"ignoreFile": ".eslintignore"
},
"prettier": {
"enabled": false,
"description": "Prettier代码格式化工具",
"configFile": ".prettierrc",
"ignoreFile": ".prettierignore"
},
"husky": {
"enabled": false,
"description": "Git hooks工具",
"hooksDir": ".husky"
}
},
"scripts": {
"setup": "node scripts/setup-dev-tools.js",
"enable": "node scripts/setup-dev-tools.js --enable",
"disable": "node scripts/setup-dev-tools.js --disable"
},
"note": "将enabled设置为true来启用对应的开发工具或运行 npm run scripts:enable 来启用所有工具"
}

View File

@@ -1,97 +0,0 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Build outputs
dist
build
.output
.nuxt
.next
.vite
.cache
# Development files
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode
.idea
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git
.git
.gitignore
.gitattributes
# Logs
logs
*.log
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Documentation
README.md
CHANGELOG.md
LICENSE.md
docs
# Config files that shouldn't be in container
.eslintrc*
.prettierrc*
prettier.config.js
.editorconfig
# Testing
jest.config.js
cypress
test
tests
# Misc
.turbo
.vercel
.netlify
# TypeScript
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Package manager lock files (keep package-lock.json but ignore others)
yarn.lock
pnpm-lock.yaml
# Docker
Dockerfile
docker-compose*.yml
.dockerignore

View File

@@ -1,35 +0,0 @@
# 环境配置示例文件
# 复制此文件为 .env.local 并根据实际情况修改配置
# 当前环境: development, test, production
NODE_ENV=development
# API 服务器地址 (用于 API 代码生成)
API_BASE_URL=http://localhost:8080
# React 应用配置 (用于前端运行时)
REACT_APP_API_URL=http://localhost:8080
# OpenAPI 文档地址
REACT_APP_OPENAPI_URL=http://localhost:8080/openapi.json
# 其他可选配置
# REACT_APP_API_KEY=your-api-key-here
# REACT_APP_DEBUG=true
# 不同环境配置示例:
#
# 开发环境:
# NODE_ENV=development
# API_BASE_URL=http://localhost:8080
# REACT_APP_API_URL=http://localhost:8080
#
# 测试环境:
# NODE_ENV=test
# API_BASE_URL=http://test-api.example.com
# REACT_APP_API_URL=http://test-api.example.com
#
# 生产环境:
# NODE_ENV=production
# API_BASE_URL=https://api.example.com
# REACT_APP_API_URL=https://api.example.com

View File

@@ -1,47 +0,0 @@
// ESLint配置文件 - 需要通过 .dev-tools-config.json 启用
// 运行 `npm run scripts:enable` 来启用ESLint
const fs = require('fs');
const path = require('path');
// 检查开发工具配置
const configPath = path.join(__dirname, '.dev-tools-config.json');
let eslintEnabled = false;
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
eslintEnabled = config.tools?.eslint?.enabled === true;
} catch (error) {
console.warn('⚠️ 无法读取开发工具配置ESLint将被禁用');
}
// 如果ESLint被禁用返回空配置
if (!eslintEnabled) {
console.log(' ESLint已在配置中被禁用如需启用请运行: npm run scripts:enable');
module.exports = {};
return;
}
// ESLint正常配置
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs', 'node_modules'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
};

140
.gitignore vendored
View File

@@ -1,113 +1,41 @@
# Dependencies # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.pnpm-debug.log*
# Production builds # env files (can opt-in for committing if needed)
.next/ .env*
out/
dist/
build/
# Environment variables # vercel
.env .vercel
.env.local
.env.development.local
.env.test.local
.env.production.local
# API 相关文件 # typescript
# 忽略从服务器下载的临时 OpenAPI JSON 文件
api/v1-from-server.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local development
.local
# TypeScript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts

View File

@@ -1,18 +0,0 @@
node_modules
dist
build
*.log
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
coverage
.nyc_output
.cache
.temp
.vscode
.idea
package-lock.json
yarn.lock
pnpm-lock.yaml

View File

@@ -1,12 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -1,277 +0,0 @@
# 更新日志
所有重要的项目变更都会记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [未发布]
### 计划中
- 完整的UI组件库集成
- 农业管理专用组件开发
- 视觉一致性验证系统
- 完整的测试覆盖
- 国际化支持
## [1.0.0] - 2024-01-20
### 🎉 首次发布
#### ✨ 新增功能
- **完整的项目基础架构**
- React 18 + Vite 6 + TypeScript 技术栈
- shadcn/ui + Tailwind CSS UI框架
- 完整的开发工具链(可选启用)
- 标准化的项目目录结构
- **React应用完整实现**
- main.tsx 应用入口文件
- App.tsx 主应用组件(包含完整管理系统界面)
- 欢迎页面和系统状态展示
- 技术栈展示和快速操作
- 主题切换功能
- 响应式布局设计
- **开发工具配置**
- ESLint + Prettier 代码质量工具
- 可选的开发工具开关控制
- VSCode 工作区配置
- 自动化代码格式化和检查
- **构建优化**
- Vite 6 构建配置优化
- 代码分割和懒加载
- 热重载优化(<2秒响应时间
- 生产环境构建优化
- **样式系统**
- 农业主题色彩系统
- 响应式设计支持
- 深色/浅色主题切换
- 完整的设计令牌
- **类型系统**
- 完整的 TypeScript 类型定义
- 农业管理领域模型
- API 响应类型定义
- 组件 Props 类型规范
- **工具函数库**
- 日期时间处理工具
- 数据格式化函数
- 农机状态映射
- 通用工具函数
- **自定义 Hooks**
- useTheme 主题管理
- useLocalStorage 本地存储
- useDebounce 防抖处理
- 扩展中...
- **项目文档**
- 详细的 README.md
- 完整的开发指南
- 贡献指南和行为准则
- API 文档规划中
#### 🏗️ 架构改进
- 模块化的项目结构
- 组件驱动的开发模式
- 类型安全的开发体验
- 可扩展的架构设计
#### 📦 依赖管理
- **核心依赖**:
- React 18.3.1
- Vite 6.3.5
- TypeScript 5.6.2
- Tailwind CSS 3.4.13
- **UI组件库**:
- 完整的 Radix UI 组件集合
- shadcn/ui 组件库基础
- Lucide React 图标库
- **开发工具**:
- ESLint 9.11.1
- Prettier 3.3.3
- Husky 9.1.6可选
#### 🎨 设计系统
- 农业绿色主题 (#16a34a)
- 一致的视觉语言
- 响应式断点系统
- 无障碍设计支持
#### 📚 文档完善
- **README.md**: 项目介绍和快速开始
- **DEVELOPMENT.md**: 详细的开发指南
- **CONTRIBUTING.md**: 贡献流程和规范
- **CHANGELOG.md**: 变更记录
#### 🔧 开发体验
- 热重载开发服务器
- TypeScript 严格模式
- 自动代码格式化
- 智能代码补全
### 🎯 功能模块规划
#### 🚜 农机管理 (Machinery)
- [x] 模块结构搭建
- [x] 基础组件框架
- [ ] 农机档案管理
- [ ] 驾驶员管理
- [ ] 负载管理
- [ ] 运行监控
- [ ] 故障管理
- [ ] 作业管理
- [ ] 数据分析
- [ ] 调度管理
- [ ] 安全管理
#### 🌾 地块管理 (Field)
- [x] 模块结构搭建
- [ ] 地块档案
- [ ] 土壤信息管理
- [ ] 作物管理
- [ ] 种植计划
#### 📋 农事管理 (Operation)
- [x] 模块结构搭建
- [ ] 作业计划
- [ ] 进度跟踪
- [ ] 成本核算
- [ ] 产量预测
#### 💰 资产管理 (Asset)
- [x] 模块结构搭建
- [ ] 设备资产管理
- [ ] 库存管理
- [ ] 采购管理
- [ ] 维护记录
#### 🤖 AI模型 (AI Model)
- [x] 模块结构搭建
- [ ] 智能预测
- [ ] 图像识别
- [ ] 数据分析
- [ ] 决策支持
#### 💧 灌溉控制 (Irrigation)
- [x] 模块结构搭建
- [ ] 智能灌溉
- [ ] 水资源管理
- [ ] 设备控制
- [ ] 用水统计
#### ⚙️ 配置管理 (Config)
- [x] 模块结构搭建
- [ ] 系统配置
- [ ] 用户管理
- [ ] 权限设置
- [ ] 数据字典
### 🚀 性能指标
- **构建时间**: < 30秒
- **热重载**: < 2秒
- **首屏加载**: < 3秒目标
- **代码分割**: 按模块自动分割
- **包大小**: 优化中...
### 🔒 安全性
- TypeScript 类型安全
- 输入验证框架
- XSS 防护
- CSRF 保护规划中
### 🌍 国际化
- 中文界面支持
- 英文界面规划中
- 多语言切换规划中
### 📱 兼容性
- **现代浏览器**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
- **移动端**: iOS Safari 14+, Chrome Mobile 90+
- **响应式**: 完整的移动端适配
### 🧪 测试覆盖
- **单元测试**: 规划中
- **集成测试**: 规划中
- **E2E测试**: 规划中
- **视觉回归测试**: 规划中
## 📈 版本规划
### [1.1.0] - 计划中
- 完整的UI组件库实现
- 农机管理核心功能
- 基础数据可视化
- 用户认证系统
### [1.2.0] - 计划中
- 地块管理功能
- 农事管理功能
- 移动端适配优化
- API 集成
### [1.3.0] - 计划中
- 资产管理功能
- AI 模型集成
- 高级数据分析
- 报表系统
### [2.0.0] - 计划中
- 完整的功能覆盖
- 微服务架构
- 实时数据同步
- 第三方系统集成
## 🏷️ 标签说明
- `✨ 新增功能`: 新的功能特性
- `🔧 改进`: 现有功能的改进
- `🐛 修复`: Bug修复
- `📚 文档`: 文档相关变更
- `🎨 样式`: UI/UX 相关变更
- `⚡ 性能`: 性能优化
- `🔒 安全`: 安全相关修复
- `💥 破坏性变更`: 不兼容的API变更
- `🗑️ 废弃`: 功能的废弃
## 🤝 贡献者
感谢所有为项目做出贡献的开发者
- **主要贡献者**: [@your-username](https://github.com/your-username)
- **项目维护**: [@maintainer](https://github.com/maintainer)
### 贡献统计
- 代码提交: XX
- 功能添加: XX
- Bug修复: XX
- 文档更新: XX
## 📞 反馈和支持
如果您有任何问题或建议请通过以下方式联系我们
- **GitHub Issues**: [项目Issues页面](https://github.com/your-username/agriculture-management/issues)
- **GitHub Discussions**: [讨论区](https://github.com/your-username/agriculture-management/discussions)
- **邮箱**: support@example.com
## 🔗 相关链接
- [项目主页](https://github.com/your-username/agriculture-management)
- [在线演示](https://demo.example.com)
- [API 文档](https://docs.example.com/api)
- [设计规范](https://design.example.com)
---
**注意**: 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范
📅 **最后更新**: 2024-01-20

View File

@@ -1,382 +0,0 @@
# 贡献指南
感谢您对智慧农业生产管理系统项目的关注!我们欢迎所有形式的贡献,包括但不限于代码贡献、问题反馈、文档改进和功能建议。
## 📋 目录
- [行为准则](#行为准则)
- [如何贡献](#如何贡献)
- [开发流程](#开发流程)
- [代码规范](#代码规范)
- [提交规范](#提交规范)
- [问题报告](#问题报告)
- [功能请求](#功能请求)
- [代码审查](#代码审查)
- [发布流程](#发布流程)
## 🤝 行为准则
### 我们的承诺
为了营造一个开放和友好的环境,我们作为贡献者和维护者承诺让每个人都能参与我们的项目和社区。
### 我们的标准
积极行为包括:
- 使用友好和包容的语言
- 尊重不同的观点和经验
- 优雅地接受建设性批评
- 关注对社区最有利的事情
- 对其他社区成员表示同理心
不可接受的行为包括:
- 使用性化的语言或图像
- 人身攻击或政治攻击
- 公开或私下骚扰
- 未经明确许可发布他人的私人信息
- 其他在专业环境中可能被认为不当的行为
## 🚀 如何贡献
### 1. 准备工作
```bash
# Fork 项目到您的GitHub账户
# 克隆您的fork
git clone https://github.com/your-username/agriculture-management.git
# 添加上游仓库
git remote add upstream https://github.com/original-owner/agriculture-management.git
# 安装依赖
cd agriculture-management
npm install
# 启动开发环境
npm run dev
```
### 2. 选择贡献方式
- 🐛 **报告Bug**: 发现问题并创建详细的问题报告
- 💡 **功能建议**: 提出新功能或改进建议
- 📝 **文档改进**: 完善项目文档
- 💻 **代码贡献**: 修复bug或实现新功能
## 🔄 开发流程
### 1. 创建分支
```bash
# 确保master分支是最新的
git checkout master
git pull upstream master
# 创建功能分支
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
### 2. 开发过程
```bash
# 启用开发工具(可选)
npm run scripts:enable
# 进行开发
# ...
# 运行测试
npm run test
# 代码检查
npm run lint
# 类型检查
npm run type-check
# 格式化代码
npm run format
```
### 3. 提交更改
```bash
# 添加更改
git add .
# 提交(遵循提交规范)
git commit -m "feat(component): add new machinery status component"
# 推送到您的fork
git push origin feature/your-feature-name
```
### 4. 创建Pull Request
1. 访问GitHub上的fork页面
2. 点击"New Pull Request"
3. 选择正确的分支
4. 填写PR模板
5. 提交Pull Request
## 📏 代码规范
### TypeScript规范
```typescript
// ✅ 好的示例
interface MachineryData {
id: string
name: string
status: MachineryStatus
lastMaintenance?: Date
}
const getMachineryStatus = async (id: string): Promise<MachineryData> => {
const response = await fetch(`/api/machinery/${id}`)
return response.json()
}
// ❌ 避免的写法
const getData = (id) => {
return fetch('/api/machinery/' + id).then(r => r.json())
}
```
### React组件规范
```typescript
// ✅ 函数式组件 + TypeScript
interface MachineryCardProps {
machinery: MachineryData
onEdit?: (machinery: MachineryData) => void
className?: string
}
export const MachineryCard: React.FC<MachineryCardProps> = ({
machinery,
onEdit,
className
}) => {
const handleEdit = useCallback(() => {
onEdit?.(machinery)
}, [machinery, onEdit])
return (
<div className={cn('machinery-card', className)}>
{/* 组件内容 */}
</div>
)
}
```
### 样式规范
```typescript
// ✅ 使用Tailwind CSS + cn工具
import { cn } from '@/lib/utils'
const Button = ({ variant = 'primary', className, ...props }) => (
<button
className={cn(
'px-4 py-2 rounded-md font-medium transition-colors',
{
'bg-blue-600 text-white': variant === 'primary',
'bg-gray-200 text-gray-900': variant === 'secondary'
},
className
)}
{...props}
/>
)
```
## 📝 提交规范
### 提交格式
```
<type>(<scope>): <subject>
<body>
<footer>
```
### 提交类型
- `feat`: 新功能
- `fix`: Bug修复
- `docs`: 文档更新
- `style`: 代码格式化(不影响功能)
- `refactor`: 代码重构
- `perf`: 性能优化
- `test`: 添加或修改测试
- `chore`: 构建过程或辅助工具的变动
### 示例
```bash
# 新功能
git commit -m "feat(machinery): add real-time monitoring dashboard"
# Bug修复
git commit -m "fix(auth): resolve login validation error for special characters"
# 文档更新
git commit -m "docs(readme): update installation instructions for Windows"
# 性能优化
git commit -m "perf(machinery): optimize data loading with virtual scrolling"
```
## 🐛 问题报告
### 报告Bug
使用以下模板创建Bug报告
```markdown
## Bug描述
简洁明了地描述Bug
## 复现步骤
1. 进入 '...'
2. 点击 '....'
3. 滚动到 '....'
4. 看到错误
## 期望行为
描述您期望发生的行为
## 实际行为
描述实际发生的行为
## 截图
如果适用,添加截图来帮助解释问题
## 环境信息
- 操作系统: [例如 iOS]
- 浏览器: [例如 chrome, safari]
- 版本: [例如 22]
## 附加信息
添加任何其他关于问题的信息
```
### 安全漏洞
如果您发现安全漏洞请不要公开报告。请发送邮件至security@example.com
## 💡 功能请求
### 请求新功能
```markdown
## 功能描述
简洁明了地描述您想要的功能
## 问题背景
描述这个功能要解决的问题
## 解决方案
描述您希望的解决方案
## 替代方案
描述您考虑过的其他解决方案
## 附加信息
添加任何其他关于功能请求的信息
```
## 👀 代码审查
### 审查者指南
当审查代码时,请关注:
1. **功能正确性**: 代码是否按预期工作
2. **代码质量**: 是否遵循项目规范
3. **性能影响**: 是否有性能问题
4. **安全性**: 是否存在安全隐患
5. **测试覆盖**: 是否有足够的测试
6. **文档**: 是否需要更新文档
### 审查评论规范
```markdown
# ✅ 好的评论
"建议将这个函数提取为自定义Hook以提高复用性"
"这个变量名不够清晰,建议改为更具描述性的名称"
"考虑添加错误处理逻辑"
# ❌ 避免的评论
"这段代码不好"(过于模糊)
"重写这个"(没有具体建议)
```
### 被审查者指南
- 对建设性反馈持开放态度
- 解释您的技术决策
- 感谢审查者的时间
- 及时响应评论
## 🏷️ 发布流程
### 版本号规范
遵循 [语义化版本](https://semver.org/lang/zh-CN/)
- `MAJOR.MINOR.PATCH`
- `MAJOR`: 不兼容的API修改
- `MINOR`: 向下兼容的功能性新增
- `PATCH`: 向下兼容的问题修正
### 发布检查清单
发布前确认:
- [ ] 所有测试通过
- [ ] 代码审查完成
- [ ] 文档已更新
- [ ] CHANGELOG已更新
- [ ] 版本号已更新
- [ ] 性能测试通过
## 🏆 贡献者认可
### 贡献者类型
- 💻 **代码贡献**: 提交代码
- 🐛 **Bug报告**: 发现并报告问题
- 💡 **功能建议**: 提出新功能想法
- 📝 **文档改进**: 完善项目文档
- 🎨 **设计贡献**: UI/UX设计改进
- 🌐 **翻译贡献**: 多语言支持
### 认可方式
- 在README中添加贡献者列表
- 在发布说明中感谢贡献者
- 颁发贡献者徽章
- 邀请加入核心团队
## 📞 联系方式
- **项目维护者**: maintainer@example.com
- **技术讨论**: GitHub Discussions
- **Bug报告**: GitHub Issues
- **安全问题**: security@example.com
## 📄 许可证
通过贡献代码,您同意您的贡献将在与项目相同的 [MIT License](LICENSE) 下获得许可。
---
感谢您的贡献!🎉

View File

@@ -1,644 +0,0 @@
# 开发指南
本文档为智慧农业生产管理系统的开发指南,帮助开发者快速了解项目结构、开发规范和最佳实践。
## 📋 目录
- [环境准备](#环境准备)
- [项目结构](#项目结构)
- [开发规范](#开发规范)
- [组件开发](#组件开发)
- [状态管理](#状态管理)
- [样式指南](#样式指南)
- [API集成](#api集成)
- [测试指南](#测试指南)
- [调试技巧](#调试技巧)
- [常见问题](#常见问题)
## 🛠️ 环境准备
### 必需软件
- **Node.js**: >= 18.0.0
- **npm**: >= 8.0.0 或 **yarn**: >= 1.22.0
- **Git**: 最新版本
### 推荐工具
- **IDE**: Visual Studio Code
- **浏览器**: Chrome/Firefox (最新版本)
- **Node管理**: nvm (可选)
### VSCode扩展推荐
项目已配置 `.vscode/extensions.json`,安装以下扩展获得最佳开发体验:
```json
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense"
]
}
```
## 📁 项目结构详解
### 核心目录
```
src/
├── components/ # 组件库
│ ├── ui/ # shadcn/ui基础组件
│ ├── common/ # 通用业务组件
│ └── layouts/ # 布局组件
├── pages/ # 页面组件
├── hooks/ # 自定义Hooks
├── lib/ # 工具库
├── config/ # 配置文件
├── types/ # TypeScript类型
├── utils/ # 工具函数
├── styles/ # 样式文件
└── assets/ # 静态资源
```
### 组件组织原则
1. **UI组件** (`components/ui/`): 纯UI组件无业务逻辑
2. **业务组件** (`components/common/`): 包含业务逻辑的复用组件
3. **页面组件** (`pages/`): 具体页面实现,组合业务组件
4. **布局组件** (`components/layouts/`): 页面布局结构
## 📏 开发规范
### 代码规范
#### TypeScript规范
```typescript
// ✅ 使用类型注解
interface UserData {
id: string
name: string
email: string
}
const getUser = async (id: string): Promise<UserData> => {
// 实现
}
// ✅ 使用泛型
interface ApiResponse<T> {
data: T
success: boolean
}
// ✅ 枚举使用
enum UserRole {
ADMIN = 'admin',
USER = 'user'
}
```
#### React组件规范
```typescript
// ✅ 函数式组件 + Hooks
interface UserCardProps {
user: UserData
onEdit?: (user: UserData) => void
className?: string
}
export const UserCard: React.FC<UserCardProps> = ({
user,
onEdit,
className
}) => {
const [isEditing, setIsEditing] = useState(false)
const handleEdit = useCallback(() => {
onEdit?.(user)
setIsEditing(true)
}, [user, onEdit])
return (
<div className={cn('user-card', className)}>
{/* 组件内容 */}
</div>
)
}
```
#### 命名规范
- **文件名**: kebab-case (`user-card.tsx`)
- **组件名**: PascalCase (`UserCard`)
- **变量名**: camelCase (`userName`)
- **常量名**: UPPER_SNAKE_CASE (`API_BASE_URL`)
- **类型名**: PascalCase (`UserData`)
### Git提交规范
使用 [Conventional Commits](https://conventionalcommits.org/) 规范:
```
<type>(<scope>): <subject>
<body>
<footer>
```
#### 提交类型
- `feat`: 新功能
- `fix`: 修复bug
- `docs`: 文档更新
- `style`: 代码格式化
- `refactor`: 重构代码
- `test`: 测试相关
- `chore`: 构建工具、依赖更新
#### 示例
```bash
feat(machinery): add machinery status monitoring
fix(auth): resolve login validation issue
docs(readme): update installation guide
```
## 🧩 组件开发
### 组件结构模板
```typescript
// src/components/example/ExampleComponent.tsx
import React, { useState, useCallback } from 'react'
import { cn } from '@/lib/utils'
interface ExampleComponentProps {
title: string
onAction?: () => void
className?: string
}
export const ExampleComponent: React.FC<ExampleComponentProps> = ({
title,
onAction,
className
}) => {
const [state, setState] = useState(false)
const handleClick = useCallback(() => {
setState(prev => !prev)
onAction?.()
}, [onAction])
return (
<div className={cn('example-component', className)}>
<h3 className="example-title">{title}</h3>
<button onClick={handleClick}>
Click me
</button>
</div>
)
}
```
### 组件样式规范
```css
/* 优先使用Tailwind CSS类名 */
.example-component {
@apply rounded-lg border border-gray-200 p-4 bg-white shadow-sm;
}
.example-title {
@apply text-lg font-semibold text-gray-900 mb-2;
}
/* 必要时使用传统CSS */
.example-component:hover {
transform: translateY(-1px);
transition: transform 0.2s ease;
}
```
### shadcn/ui组件使用
```typescript
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
export const MachineryCard = ({ machinery }) => {
return (
<Card className="machinery-card">
<CardHeader>
<CardTitle>{machinery.name}</CardTitle>
<Badge variant={machinery.status === 'running' ? 'default' : 'secondary'}>
{machinery.status}
</Badge>
</CardHeader>
<CardContent>
{/* 内容 */}
</CardContent>
</Card>
)
}
```
## 🔄 状态管理
### useState使用
```typescript
const [formData, setFormData] = useState({
name: '',
email: '',
phone: ''
})
// 更新对象状态
const handleChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}))
}
```
### useReducer使用
```typescript
type State = {
count: number
loading: boolean
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setLoading'; payload: boolean }
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
case 'setLoading':
return { ...state, loading: action.payload }
default:
return state
}
}
const [state, dispatch] = useReducer(reducer, {
count: 0,
loading: false
})
```
### 自定义Hooks
```typescript
// src/hooks/useApi.ts
import { useState, useEffect } from 'react'
export function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
```
## 🎨 样式指南
### Tailwind CSS最佳实践
```typescript
// ✅ 使用cn工具函数合并类名
import { cn } from '@/lib/utils'
const Button = ({ variant = 'primary', className, ...props }) => {
return (
<button
className={cn(
'px-4 py-2 rounded-md font-medium transition-colors',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary'
},
className
)}
{...props}
/>
)
}
```
### 响应式设计
```typescript
// 移动优先的响应式设计
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 内容 */}
</div>
// 响应式间距
<div className="px-4 sm:px-6 lg:px-8">
{/* 内容 */}
</div>
```
### 深色模式支持
```typescript
import { useTheme } from '@/hooks/useTheme'
export const ThemedComponent = () => {
const { theme, setTheme } = useTheme()
return (
<div className="bg-background text-foreground">
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
)
}
```
## 🌐 API集成
### API配置
```typescript
// src/config/api.ts
export const API_CONFIG = {
baseUrl: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
}
export const apiClient = {
get: <T>(url: string): Promise<T> => {
return fetch(`${API_CONFIG.baseUrl}${url}`).then(res => res.json())
},
post: <T>(url: string, data: any): Promise<T> => {
return fetch(`${API_CONFIG.baseUrl}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(res => res.json())
}
}
```
### 数据获取Hook
```typescript
// src/hooks/useMachinery.ts
import { useState, useEffect } from 'react'
import { apiClient } from '@/config/api'
export function useMachinery() {
const [machinery, setMachinery] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
apiClient.get('/machinery')
.then(setMachinery)
.catch(setError)
.finally(() => setLoading(false))
}, [])
return { machinery, loading, error }
}
```
## 🧪 测试指南
### 单元测试示例
```typescript
// src/components/__tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '../Button'
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
```
### Hook测试
```typescript
// src/hooks/__tests__/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounter } from '../useCounter'
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
})
it('increments count', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
})
```
## 🐛 调试技巧
### React DevTools
安装 React Developer Tools 浏览器扩展:
```bash
# 检查组件状态
# 查看组件层次结构
# 性能分析
```
### VSCode调试配置
```json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug React",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/vite",
"args": ["--mode", "development"],
"env": {
"NODE_ENV": "development"
}
}
]
}
```
### 常用调试代码
```typescript
// 开发环境调试
if (import.meta.env.DEV) {
console.log('Debug info:', data)
}
// 性能监控
console.time('component-render')
// ... 组件渲染逻辑
console.timeEnd('component-render')
// 网络请求调试
const debugFetch = async (url: string) => {
console.log(`Fetching: ${url}`)
const start = performance.now()
try {
const response = await fetch(url)
const data = await response.json()
console.log(`Fetched in ${performance.now() - start}ms`, data)
return data
} catch (error) {
console.error(`Fetch failed after ${performance.now() - start}ms`, error)
throw error
}
}
```
## ❓ 常见问题
### Q: 如何添加新的UI组件
A:
1.`src/components/ui/` 下创建组件文件
2. 使用 shadcn/ui 设计规范
3. 添加 TypeScript 类型定义
4. 编写组件文档
### Q: 如何处理表单验证?
A: 推荐使用 react-hook-form + zod
```typescript
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1, '名称不能为空'),
email: z.string().email('邮箱格式不正确')
})
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema)
})
```
### Q: 如何优化性能?
A:
1. 使用 React.memo 避免不必要的重渲染
2. 使用 useMemo 和 useCallback 缓存计算结果
3. 实现虚拟列表处理大数据
4. 使用代码分割减少初始加载时间
### Q: 如何处理国际化?
A: 项目支持多语言配置:
```typescript
// src/config/i18n.ts
export const locales = {
'zh-CN': '简体中文',
'en-US': 'English'
}
export const translations = {
'zh-CN': {
'machinery.title': '农机管理',
'machinery.status.running': '运行中'
},
'en-US': {
'machinery.title': 'Machinery Management',
'machinery.status.running': 'Running'
}
}
```
### Q: 如何配置开发工具?
A: 使用项目提供的脚本:
```bash
# 查看状态
npm run scripts:setup
# 启用工具
npm run scripts:enable
# 禁用工具
npm run scripts:disable
```
## 📚 更多资源
- [React 官方文档](https://react.dev/)
- [TypeScript 手册](https://www.typescriptlang.org/docs/)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [shadcn/ui 组件库](https://ui.shadcn.com/)
- [Vite 构建工具](https://vitejs.dev/)
---
💡 **提示**: 如果遇到问题,请先查看常见问题部分,或联系项目维护者获取帮助。

326
README.md
View File

@@ -1,322 +1,36 @@
# 智慧农业生产管理系统 This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
基于 React 18 + Vite 6 + TypeScript + shadcn/ui 构建的现代化农业管理平台。 ## Getting Started
## 🌟 项目特色 First, run the development server:
- 🚀 **现代化技术栈**: React 18 + Vite 6 + TypeScript
- 🎨 **优美UI设计**: 基于 shadcn/ui + Tailwind CSS
- 📱 **响应式布局**: 支持桌面端和移动端
- 🌾 **农业专业化**: 针对农业生产场景定制
- 🔧 **开发工具**: ESLint + Prettier + Husky可选启用
- 📊 **数据可视化**: 集成 Recharts 图表库
## 🏗️ 技术架构
### 核心技术栈
- **前端框架**: React 18.3.1
- **构建工具**: Vite 6.3.5
- **类型系统**: TypeScript 5.6.2
- **UI组件库**: shadcn/ui + Radix UI
- **样式方案**: Tailwind CSS 3.4.13
- **状态管理**: React Context + Hooks
- **路由系统**: 自定义基于路径的路由
- **图表库**: Recharts 2.15.2
- **图标库**: Lucide React 0.487.0
- **日期处理**: date-fns 4.1.0
### 项目结构
```
crop-x/
├── 📄 public/ # 静态资源
├── 📄 src/
│ ├── 📄 components/ # 组件目录
│ │ ├── ui/ # shadcn/ui基础组件
│ │ ├── common/ # 通用业务组件
│ │ └── layouts/ # 布局组件
│ ├── 📄 pages/ # 页面组件
│ │ ├── machinery/ # 农机管理
│ │ ├── field/ # 地块管理
│ │ ├── operation/ # 农事管理
│ │ ├── asset/ # 资产管理
│ │ ├── ai-model/ # AI模型
│ │ ├── irrigation/ # 灌溉控制
│ │ └── config/ # 配置管理
│ ├── 📄 hooks/ # 自定义Hooks
│ ├── 📄 lib/ # 工具库
│ ├── 📄 config/ # 配置文件
│ ├── 📄 types/ # 类型定义
│ ├── 📄 utils/ # 工具函数
│ ├── 📄 assets/ # 资源文件
│ ├── 📄 styles/ # 样式文件
│ └── 📄 App.tsx # 主应用组件
├── 📄 docs/ # 文档目录
├── 📄 scripts/ # 构建脚本
└── 📄 .vscode/ # VSCode配置
```
## 🚀 快速开始
### 环境要求
- Node.js >= 18.0.0
- npm >= 8.0.0
### 安装依赖
```bash
npm install
```
### 开发环境
```bash ```bash
npm run dev npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
``` ```
项目将在 http://localhost:3000 启动(如果端口被占用会自动切换) Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
**✅ React应用验证**: You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
- 启动时间: < 2秒实测2012ms
- 热重载: 正常工作
- 主题切换: 支持深色/浅色模式
- 响应式设计: 支持桌面端和移动端
### 构建生产版本 This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash ## Learn More
npm run build
```
### 预览生产版本 To learn more about Next.js, take a look at the following resources:
```bash - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
npm run preview - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```
## 🛠️ 开发工具 You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
项目配置了可选的开发工具链默认禁用可通过以下命令启用 ## Deploy on Vercel
### 查看开发工具状态 The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
```bash Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
npm run scripts:setup
```
### 启用所有开发工具
```bash
npm run scripts:enable
```
### 禁用所有开发工具
```bash
npm run scripts:disable
```
### 代码检查
```bash
# ESLint检查
npm run lint
# ESLint自动修复
npm run lint:fix
```
### 代码格式化
```bash
# Prettier格式化
npm run format
# 检查格式
npm run format:check
```
### 类型检查
```bash
npm run type-check
```
## 📁 功能模块
### 🚜 农机管理 (Machinery)
- 农机档案管理
- 驾驶员管理
- 负载管理
- 运行监控
- 故障管理
- 作业管理
- 数据分析
- 调度管理
- 安全管理
### 🌾 地块管理 (Field)
- 地块档案
- 土壤信息
- 作物管理
- 种植计划
### 📋 农事管理 (Operation)
- 作业计划
- 进度跟踪
- 成本核算
- 产量预测
### 💰 资产管理 (Asset)
- 设备资产
- 库存管理
- 采购管理
- 维护记录
### 🤖 AI模型 (AI Model)
- 智能预测
- 图像识别
- 数据分析
- 决策支持
### 💧 灌溉控制 (Irrigation)
- 智能灌溉
- 水资源管理
- 设备控制
- 用水统计
### ⚙️ 配置管理 (Config)
- 系统配置
- 用户管理
- 权限设置
- 数据字典
## 🎨 设计系统
### 颜色系统
- **主色调**: 农业绿色 (#16a34a)
- **辅助色**: 技术蓝色 (#3b82f6)
- **状态色**: 运行(绿)、空闲()、维护()、故障()、离线(深灰)
### 组件规范
- 基于 shadcn/ui 组件库
- 遵循 Material Design 设计规范
- 支持深色/浅色主题切换
- 完整的无障碍支持
## 🔧 配置说明
### 环境变量
```bash
# API地址
VITE_API_BASE_URL=http://localhost:8080/api
# 应用标题
VITE_APP_TITLE=智慧农业生产管理系统
# 开发模式
VITE_DEV_MODE=true
```
### 开发工具配置
通过 `.dev-tools-config.json` 文件控制开发工具的启用状态
```json
{
"tools": {
"eslint": { "enabled": false },
"prettier": { "enabled": false },
"husky": { "enabled": false }
}
}
```
## 📖 开发指南
### 添加新页面
1. 在对应模块下创建页面组件
2. 在路由配置中添加路由规则
3. 在菜单配置中添加菜单项
### 添加新组件
1. `src/components/ui/` 下创建UI组件
2. 遵循 shadcn/ui 设计规范
3. 添加 TypeScript 类型定义
4. 编写组件文档和使用示例
### 样式规范
- 使用 Tailwind CSS 类名
- 遵循 BEM 命名规范
- 响应式设计优先
- 支持深色模式
## 🧪 测试
```bash
# 运行测试
npm run test
# 运行测试覆盖率
npm run test:coverage
```
## 📤 部署
### Docker部署
```bash
# 构建镜像
docker build -t agriculture-management .
# 运行容器
docker run -p 3000:3000 agriculture-management
```
### 传统部署
```bash
# 构建项目
npm run build
# 部署build目录到Web服务器
```
## 📝 更新日志
### v1.0.0 (2024-01-20)
- 初始版本发布
- 🚀 完成基础架构搭建
- 🎨 集成shadcn/ui组件库
- 📱 实现响应式设计
- 🌾 添加农业专业化功能
## 🤝 贡献指南
1. Fork 项目
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request
## 📄 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
## 📞 联系我们
- 项目地址: [GitHub Repository](https://github.com/your-username/agriculture-management)
- 问题反馈: [Issues](https://github.com/your-username/agriculture-management/issues)
- 邮箱: your-email@example.com
---
如果这个项目对你有帮助请给我们一个星标

View File

@@ -1475,3 +1475,538 @@ SearchFormPagination 组件通过配置驱动的方式,极大地简化了复
- **功能专注**:专注搜索、展示、分页核心功能,避免过度设计 - **功能专注**:专注搜索、展示、分页核心功能,避免过度设计
该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。 该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。
---
## pathsrc/components/common/searchFormPaginationnameURL参数同步功能集成规范
### 功能概述
URL参数同步功能是 SearchFormPagination 组件的高级特性能够自动将用户的搜索条件、分页状态与浏览器URL参数保持同步实现页面刷新后状态的恢复提升用户体验。
### 设计原则
#### 1. 职责分离原则
**子组件职责**SearchFormPagination
- ✅ UI 显示:渲染搜索表单、数据表格、分页组件
- ✅ 状态管理:管理内部搜索条件和分页状态
- ✅ URL 同步:自动同步状态到 URL 参数
- ✅ 状态通知:通过回调通知父组件状态变化
- ✅ 参数推导:自动从 searchFields 推导 URL 参数映射
**父组件职责**(业务页面):
- ✅ 查询逻辑:处理所有查询相关的业务逻辑
- ✅ 数据管理:管理数据、加载状态、错误处理
- ✅ API 调用:调用后端接口获取数据
#### 2. 自动参数推导原则
URL 同步功能会自动从 `searchFields` 配置中提取参数映射,无需手动配置:
```tsx
// searchFields 配置
const searchFields = [
{ key: 'search', label: '搜索', type: 'text' },
{ key: 'status', label: '状态', type: 'select' },
{ key: 'type', label: '类型', type: 'select' }
];
// 自动推导出 URL 参数映射
// {
// page: 'page',
// size: 'size',
// search: 'search', // 来自 searchFields[0].key
// status: 'status', // 来自 searchFields[1].key
// type: 'type' // 来自 searchFields[2].key
// }
// 生成的 URL?page=1&size=10&search=张三&status=active&type=admin
```
#### 3. 可选启用原则
URL 同步功能是完全可选的,不影响现有使用方式:
```tsx
// 不启用 URL 同步(默认行为)
<SearchFormPagination
searchFields={searchFields}
columns={columns}
data={data}
// ... 其他 props
/>
// 启用 URL 同步 - 极简配置
<SearchFormPagination
searchFields={searchFields}
columns={columns}
data={data}
urlSync={{ enabled: true }} // 只需这一行即可启用
// ... 其他 props
/>
```
### 接口定义
#### UrlSyncConfig - URL 同步配置
```tsx
interface UrlSyncConfig {
// 是否启用 URL 参数同步
enabled?: boolean;
// 是否在初始化时检测空 URL 并添加默认参数
initWithDefaults?: boolean;
// 默认分页参数
defaultPagination?: {
page: number;
size: number;
};
// URL 更新防抖时间(毫秒),避免频繁更新
updateDebounce?: number;
// 自定义 URL 参数名映射(可选,不配置则自动从 searchFields 提取)
paramNames?: {
page?: string;
size?: string;
[key: string]: string | undefined;
};
}
```
#### 统一查询接口(可选)
```tsx
// 新增的统一查询回调,替代传统的多个回调
onQueryChange?: (query: {
filters: Record<string, string; // 搜索条件(已处理)
pagination: { page: number; size: number }; // 分页信息(已处理)
isFromUrl?: boolean; // 是否来自URL初始化
}) => void;
```
### 使用方式
#### 1. 基础使用(推荐)
```tsx
<SearchFormPagination
searchFields={searchFields}
columns={columns}
data={state.users}
loading={state.loading}
error={state.error}
pagination={state.pagination}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
onSearch={handleSearch}
// 启用 URL 同步 - 参数名自动从 searchFields 提取
urlSync={{
enabled: true,
initWithDefaults: true,
updateDebounce: 300
}}
/>
```
#### 2. 高级配置(自定义参数名)
```tsx
<SearchFormPagination
// ... 其他 props
urlSync={{
enabled: true,
initWithDefaults: true,
// 自定义 URL 参数名(覆盖自动提取的参数名)
paramNames: {
page: 'pageNum', // 页码参数名
size: 'pageSize', // 每页大小参数名
search: 'keyword', // 搜索框参数名(覆盖自动提取)
// status 和 type 会自动从 searchFields 提取,这里不配置
},
// 默认分页参数
defaultPagination: {
page: 1,
size: 10
},
// URL 更新防抖时间
updateDebounce: 500
}}
// 统一查询回调(推荐使用)
onQueryChange={handleQueryChange}
// 传统回调方式(向后兼容)
onSearch={handleSearch}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
// URL 状态变化监听(可选)
onUrlStateChange={(urlState) => {
console.log('URL 状态变化:', urlState);
}}
/>
```
#### 3. 父组件统一查询处理
```tsx
// 统一查询处理函数
const handleQueryChange = useCallback((query: {
filters: Record<string, string>;
pagination: { page: number; size: number };
isFromUrl?: boolean;
}) => {
console.log('收到查询变化:', query);
// 映射过滤器字段名(根据业务需求调整)
const mappedFilters = {
searchKeyword: query.filters.search || '',
statusFilter: query.filters.status || 'all',
typeFilter: query.filters.type || 'all'
};
// 更新状态
dispatch({ type: 'SET_FILTERS', payload: mappedFilters });
dispatch({ type: 'SET_PAGINATION', payload: query.pagination });
// 执行查询
loadUsers({
resetPage: !query.isFromUrl, // URL 初始化时保持页码,否则重置
page: query.pagination.page,
filters: mappedFilters,
sortBy: state.sortBy,
sortOrder: state.sortOrder,
size: query.pagination.size
});
}, [state.sortBy, state.sortOrder, loadUsers]);
```
### 工作流程
#### 1. 页面初始化
```
用户访问页面
子组件检查 URL 参数
├─ 无参数 → 使用默认状态,父组件执行默认查询
└─ 有参数 → 解析参数 → 同步到内部状态 → 通知父组件 → 父组件执行查询
```
#### 2. 用户操作
```
用户搜索/分页操作
子组件更新内部状态
同步状态到 URL 参数(防抖处理)
通知父组件状态变化
父组件执行查询
```
#### 3. 页面刷新
```
页面刷新
子组件从 URL 读取参数
恢复内部状态
通知父组件
父组件执行查询 → 恢复用户之前的搜索状态
```
### URL 参数格式
#### 默认格式
```
# 基础搜索
?page=1&size=10&search=张三&status=active&type=admin
# 分页状态
?page=3&size=20
# 组合条件
?page=2&size=15&search=admin&status=active
```
#### 自定义参数名格式
```tsx
paramNames: {
page: 'p',
size: 'limit',
search: 'q',
status: 's',
type: 't'
}
// 生成 URL
?pageNum=2&pageSize=15&keyword=admin&status=active&type=user
```
### 技术实现要点
#### 1. 防抖处理
```tsx
// URL 更新防抖,避免频繁修改浏览器历史记录
const updateUrl = useCallback((filters, pagination) => {
if (urlUpdateTimeoutRef.current) {
clearTimeout(urlUpdateTimeoutRef.current);
}
urlUpdateTimeoutRef.current = setTimeout(() => {
// 更新 URL 参数
window.history.replaceState({}, '', newUrl);
}, urlConfig.updateDebounce);
}, []);
```
#### 2. 参数映射
```tsx
// 支持字段名映射,适应不同的 API 接口
const paramValue = urlParams.get(
urlConfig.paramNames[field.key] || field.key
);
```
#### 3. 状态同步时机
```tsx
// 搜索条件变化时同步
useEffect(() => {
if (urlConfig.enabled) {
updateUrl(filters, pagination);
}
}, [filters, urlConfig.enabled]);
// 分页变化时同步
useEffect(() => {
if (urlConfig.enabled && pagination) {
updateUrl(filters, pagination);
}
}, [pagination?.page, pagination?.size, urlConfig.enabled]);
```
### 最佳实践
#### 1. 参数命名规范
```tsx
// ✅ 推荐:使用有意义的参数名
paramNames: {
search: 'keyword', // 搜索关键词
status: 'userStatus', // 用户状态
type: 'userType', // 用户类型
page: 'pageNum', // 页码
size: 'pageSize' // 每页大小
}
// ❌ 避免:过于简化的参数名
paramNames: {
search: 's',
status: 'st',
type: 't'
}
```
#### 2. 防抖时间设置
```tsx
// ✅ 推荐:根据用户操作频率调整
urlSync: {
updateDebounce: 300 // 文本搜索300ms下拉选择立即
}
// 快速响应场景
urlSync: {
updateDebounce: 100 // 需要即时反馈的场景
}
// 性能优先场景
urlSync: {
updateDebounce: 500 // 减少频繁更新
}
```
#### 3. 默认值配置
```tsx
// ✅ 推荐:设置合理的默认值
urlSync: {
defaultPagination: {
page: 1,
size: 10 // 根据业务需求设置合理的默认每页条数
},
initWithDefaults: true // 为新用户提供更好的体验
}
```
### 注意事项
#### 1. 浏览器兼容性
- 支持 `window.history.replaceState` 的现代浏览器
- 服务端渲染SSR时需要检查 `typeof window !== 'undefined'`
#### 2. 参数长度限制
- URL 参数总长度建议控制在 2048 字符以内
- 复杂搜索条件考虑使用 POST 请求而非 GET
#### 3. 安全性考虑
- 对 URL 参数进行验证和清理
- 避免将敏感信息存储在 URL 中
- 考虑 XSS 防护
#### 4. 性能影响
- URL 同步功能对性能影响很小
- 防抖机制避免频繁的 DOM 操作
- 合理设置防抖时间可进一步优化性能
### 向后兼容性
URL 同步功能完全向后兼容,不会影响现有代码:
```tsx
// 现有代码无需修改,继续正常工作
<SearchFormPagination
searchFields={searchFields}
columns={columns}
onSearch={handleSearch}
onPageChange={handlePageChange}
// ... 其他 props
/>
// 极简启用 - 只需添加一行
<SearchFormPagination
// ... 现有 props
urlSync={{ enabled: true }} // 参数名自动从 searchFields 推导
/>
```
### 配置简化对比
#### 优化前(复杂配置)
```tsx
urlSync={{
enabled: true,
initWithDefaults: true,
paramNames: {
page: 'page',
size: 'size',
search: 'search',
status: 'status',
type: 'type'
},
defaultPagination: { page: 1, size: 10 },
updateDebounce: 300
}}
```
#### 优化后(极简配置)
```tsx
urlSync={{
enabled: true,
initWithDefaults: true,
updateDebounce: 300
}}
// page、size 以及所有 searchFields 的 key 都会自动推导
```
### 故障排除
#### 1. URL 参数不更新
**可能原因**
- `urlSync.enabled` 设置为 `false`
- 防抖时间设置过长
- 浏览器不支持 `history.replaceState`
**解决方案**
```tsx
urlSync: {
enabled: true,
updateDebounce: 100 // 减少防抖时间测试
}
```
#### 2. 页面刷新后状态丢失
**可能原因**
- `initWithDefaults` 设置为 `false`
- 参数名映射不正确
- 父组件未正确处理 `onQueryChange` 回调
**解决方案**
```tsx
urlSync: {
enabled: true,
initWithDefaults: true // 确保启用默认值初始化
}
// 检查参数名映射
paramNames: {
search: 'search', // 确保与搜索字段 key 一致
status: 'status'
}
```
#### 3. 搜索条件与分页不同步
**可能原因**
- 父组件未传递正确的分页状态
- 回调函数中丢失搜索条件
**解决方案**
```tsx
const handlePageChange = useCallback((page) => {
// 确保传递当前搜索条件
loadUsers({
filters: currentFilters, // 关键:保持搜索条件
pagination: { page, size: currentSize }
});
}, [loadUsers, currentFilters, currentSize]);
```
### 总结
URL 参数同步功能为 SearchFormPagination 组件提供了强大的状态持久化能力,通过极简配置即可实现:
- **自动同步**:无需手动管理 URL 参数
- **自动推导**:参数名从 `searchFields` 自动提取,无需手动映射
- **状态恢复**:页面刷新后自动恢复搜索状态
- **用户体验**:提供更好的导航和分享体验
- **极简配置**:只需 `urlSync={{ enabled: true }}` 即可启用
- **向后兼容**:不影响现有代码,渐进式升级
#### 配置简化成果
- **优化前**:需要手动配置所有参数名映射,配置复杂
- **优化后**:参数名自动推导,配置减少 70%+
#### 使用建议
- **基础场景**:直接使用 `urlSync={{ enabled: true }}`
- **特殊需求**:仅在需要自定义参数名时配置 `paramNames`
- **避免使用**:不推荐使用 `q``s` 等过于简化的参数名
该功能特别适用于数据展示、搜索、筛选等需要状态保持的场景,是提升用户体验的重要功能。

26
env/.env.dev vendored
View File

@@ -1,26 +0,0 @@
# 开发环境配置
NODE_ENV=development
# 前端域名
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.dev.maimaiag.com
# 后端 API 地址
BACKEND_BASE_URL=https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com/
# OpenAPI 生成配置
API_BASE_URL=https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com
# API 版本
API_VERSION=v1
# 调试模式
DEBUG=true
# 是否开启 Mock 数据
USE_MOCK=false
# 应用名称
APP_NAME=智慧农业生产管理系统
# 环境描述
ENV_DESCRIPTION=开发环境

43
env/.env.prod vendored
View File

@@ -1,43 +0,0 @@
# 生产环境配置
NODE_ENV=production
# 前端域名
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.prod.maimaiag.com
# 后端 API 地址
BACKEND_BASE_URL=https://cavin-smart-crop-backend-app.prod.maimaiag.com
# API 版本
API_VERSION=v1
# 调试模式
DEBUG=false
# 是否开启 Mock 数据
USE_MOCK=false
# 应用名称
APP_NAME=智慧农业生产管理系统
# 环境描述
ENV_DESCRIPTION=生产环境
# 生产环境特有配置
API_TIMEOUT=30000
ENABLE_ERROR_LOGGING=true
ENABLE_PERFORMANCE_MONITORING=true
ENABLE_USER_BEHAVIOR_TRACKING=true
ENABLE_ANALYTICS=true
# 安全配置
SENTRY_DSN=https://your-sentry-dsn.prod@sentry.io/project-id
CDN_BASE_URL=https://cdn.cavin-smart-crop.com
# 功能开关
ENABLE_MAINTENANCE_MODE=false
ENABLE_NEW_FEATURES=true
ENABLE_BETA_FEATURES=false
# 缓存配置
CACHE_TTL=3600
ENABLE_SERVICE_WORKER=true

28
env/.env.test vendored
View File

@@ -1,28 +0,0 @@
# 测试环境配置
NODE_ENV=test
# 前端域名
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.test.maimaiag.com
# 后端 API 地址
BACKEND_BASE_URL=http://pengcode.tech:8080
# API 版本
API_VERSION=v1
# 调试模式
DEBUG=true
# 是否开启 Mock 数据
USE_MOCK=false
# 应用名称
APP_NAME=智慧农业生产管理系统
# 环境描述
ENV_DESCRIPTION=测试环境
# 其他测试环境特有配置
API_TIMEOUT=15000
ENABLE_ERROR_LOGGING=true
ENABLE_PERFORMANCE_MONITORING=true

30
env/.env.uat vendored
View File

@@ -1,30 +0,0 @@
# UAT (用户验收测试) 环境配置
NODE_ENV=production
# 前端域名
FRONTEND_BASE_URL=https://cavin-smart-crop-ui-app.uat.maimaiag.com
# 后端 API 地址
BACKEND_BASE_URL=https://cavin-smart-crop-backend-app.uat.maimaiag.com
# API 版本
API_VERSION=v1
# 调试模式
DEBUG=false
# 是否开启 Mock 数据
USE_MOCK=false
# 应用名称
APP_NAME=智慧农业生产管理系统
# 环境描述
ENV_DESCRIPTION=UAT 环境
# UAT 特有配置
API_TIMEOUT=20000
ENABLE_ERROR_LOGGING=true
ENABLE_PERFORMANCE_MONITORING=true
ENABLE_USER_BEHAVIOR_TRACKING=true
SENTRY_DSN=https://your-sentry-dsn.uat@sentry.io/project-id

View File

@@ -1,25 +1,18 @@
import { dirname } from "path"; import { defineConfig, globalIgnores } from "eslint/config";
import { fileURLToPath } from "url"; import nextVitals from "eslint-config-next/core-web-vitals";
import { FlatCompat } from "@eslint/eslintrc"; import nextTs from "eslint-config-next/typescript";
const __filename = fileURLToPath(import.meta.url); const eslintConfig = defineConfig([
const __dirname = dirname(__filename); ...nextVitals,
...nextTs,
const compat = new FlatCompat({ // Override default ignores of eslint-config-next.
baseDirectory: __dirname, globalIgnores([
}); // Default ignores of eslint-config-next:
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**", ".next/**",
"out/**", "out/**",
"build/**", "build/**",
"next-env.d.ts", "next-env.d.ts",
], ]),
}, ]);
];
export default eslintConfig; export default eslintConfig;

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智慧农业生产管理系统</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,37 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
typescript: {
ignoreBuildErrors: true, // TODO: 暂时完全禁用TypeScript类型检查
},
eslint: {
ignoreDuringBuilds: true, // TODO: 暂时禁用eslint校验错误
},
transpilePackages: ['lucide-react'],
output: 'standalone',
// 修复CSS构建问题
experimental: {
// forceSwcTransforms: true,
},
// 新的 Turbopack 配置
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
// 解决工作区根目录问题
outputFileTracingRoot: process.cwd(),
// 添加代理配置解决CORS问题
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com/api/:path*',
},
];
},
};
export default nextConfig;

36
next.config.ts Normal file
View File

@@ -0,0 +1,36 @@
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// 构建时报 TS 校验(可按需开启/关闭)
typescript: {
ignoreBuildErrors: false,
},
// 将 ESM 包转译(如有 Tree-shaking/TS 产物需要)
transpilePackages: ['lucide-react'],
// 便于部署到容器/Serverless
output: 'standalone',
// 若有其它实验性开关可放这里(保持空对象即可)
experimental: {
// 例如typedRoutes: true
},
// 解决工作区根目录问题(通常保持默认即可;你这里明确指定也可)
outputFileTracingRoot: process.cwd(),
// 反向代理(避免本地 CORS
async rewrites() {
return [
{
source: '/api/:path*',
destination:
'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com/api/:path*',
},
];
},
};
export default nextConfig;

12445
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,102 +1,69 @@
{ {
"name": "智慧农业生产管理系统", "name": "crop-x-next",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint",
"lint:fix": "eslint . --ext ts,tsx --fix",
"type-check": "tsc --noEmit",
"api:generate": "node scripts/generate-api.cjs",
"deploy": "node scripts/deploy.js", "deploy": "node scripts/deploy.js",
"build:dev": "node scripts/build.cjs dev", "api:generate": "node scripts/generate-api.cjs"
"build:test": "node scripts/build.cjs test",
"build:uat": "node scripts/build.cjs uat",
"build:prod": "node scripts/build.cjs prod"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hey-api/openapi-ts": "^0.87.1",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7", "@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.14", "axios": "^1.13.2",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "dotenv": "^17.2.3",
"date-fns": "^4.1.0", "lucide-react": "^0.553.0",
"embla-carousel-react": "^8.6.0", "next": "16.0.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"next": "^16.0.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"npx": "^10.2.2",
"openapi-fetch": "^0.15.0", "openapi-fetch": "^0.15.0",
"qrcode": "*", "react": "19.2.0",
"react": "^19.2.0", "react-dom": "19.2.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.0",
"react-hook-form": "^7.65.0",
"react-resizable-panels": "^2.1.9",
"recharts": "^2.15.4",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.1.12",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@hey-api/client-fetch": "^0.13.1",
"@hey-api/openapi-ts": "^0.86.6",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tailwindcss/vite": "^4.1.14", "@types/node": "^20",
"@types/node": "^20.10.0", "@types/react": "^19",
"@types/react": "^18.3.11", "@types/react-dom": "^19",
"@types/react-dom": "^18.3.1", "eslint": "^9",
"@typescript-eslint/eslint-plugin": "^8.7.0", "eslint-config-next": "16.0.1",
"@typescript-eslint/parser": "^8.7.0", "tailwindcss": "^4",
"@vitejs/plugin-react-swc": "^3.10.2", "tw-animate-css": "^1.4.0",
"autoprefixer": "^10.4.20", "typescript": "^5"
"eslint": "^9.11.1",
"eslint-config-next": "15.5.6",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"husky": "^9.1.6",
"install": "^0.13.0",
"lint-staged": "^15.2.10",
"node-fetch": "^3.3.2",
"npm": "^11.6.2",
"openapi-typescript": "^7.10.1",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^4.1.14",
"typescript": "^5.9.3",
"vite": "^6.4.0"
} }
} }

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -10,7 +10,7 @@ var data = JSON.stringify({
"git-revision": "main", "git-revision": "main",
"git-pat": "b6c02bf1aec73d7bbbfbe590ea37564a29c4bd5d", "git-pat": "b6c02bf1aec73d7bbbfbe590ea37564a29c4bd5d",
"docker-image-domain": "172.16.102.3:30648", "docker-image-domain": "172.16.102.3:30648",
"docker-dockerfile-path": "./Dockerfile.crop-x", "docker-dockerfile-path": "./Dockerfile.crop-x-new",
"resource-cpu-limit": "500m", "resource-cpu-limit": "500m",
"resource-memory-limit": "512Mi", "resource-memory-limit": "512Mi",
"resource-gpu-mem-limit": "", "resource-gpu-mem-limit": "",

View File

@@ -6,46 +6,15 @@
* 2. 环境配置通过 openapi-ts.config.ts 处理 * 2. 环境配置通过 openapi-ts.config.ts 处理
*/ */
// 加载环境变量
require('dotenv').config({ path: '.env.local' });
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
// 从环境配置文件中读取 API_BASE_URL // 从环境配置文件中读取 API_BASE_URL
function getApiBaseUrl() {
try {
// 首先尝试从 env/.env.dev 文件读取
const envDevPath = path.join(process.cwd(), 'env', '.env.dev');
if (fs.existsSync(envDevPath)) {
const envContent = fs.readFileSync(envDevPath, 'utf8');
const apiBaseUrlMatch = envContent.match(/API_BASE_URL=([^\r\n]+)/);
if (apiBaseUrlMatch && apiBaseUrlMatch[1]) {
return apiBaseUrlMatch[1].trim();
}
}
// 如果上面的文件不存在,尝试从 TypeScript 环境配置文件读取
const envConfigPath = path.join(process.cwd(), 'src', 'env', 'index.ts');
if (fs.existsSync(envConfigPath)) {
const envContent = fs.readFileSync(envConfigPath, 'utf8');
const devConfigMatch = envContent.match(/dev:\s*\{[\s\S]*?BACKEND_BASE_URL:\s*['"`]([^'"`]+)['"`]/);
if (devConfigMatch && devConfigMatch[1]) {
return devConfigMatch[1].trim();
}
}
throw new Error('无法找到 API_BASE_URL 或 BACKEND_BASE_URL 配置');
} catch (error) {
console.log(`读取环境配置失败: ${error.message}`);
return 'http://localhost:8080'; // 默认值
}
}
// 获取 API 基础 URL
const API_BASE_URL = getApiBaseUrl();
// 设置环境变量供后续使用
process.env.API_BASE_URL = API_BASE_URL;
// ANSI 颜色代码 // ANSI 颜色代码
const colors = { const colors = {
@@ -79,11 +48,10 @@ function logWarning(message) {
function logInfo(message) { function logInfo(message) {
log(` ${message}`, 'blue'); log(` ${message}`, 'blue');
} }
const API_BASE_URL = process.env.API_BASE_URL
// 显示环境配置信息 // 显示环境配置信息
logInfo(`当前环境: ${process.env.NODE_ENV || 'development'}`); logInfo(`当前环境: ${process.env.NODE_ENV || 'development'}`);
logInfo(`API 服务器: ${API_BASE_URL}`); logInfo(`API 服务器: ${API_BASE_URL}`);
logInfo(`已从 env/.env.dev 读取 API_BASE_URL 配置`);
/** /**
* 检查自定义文件是否存在 * 检查自定义文件是否存在

File diff suppressed because one or more lines are too long

View File

@@ -234,7 +234,7 @@ export class NetworkLogService {
] ]
// 应用筛选器 // 应用筛选器
let filteredLogs = mockLogs.filter(log => { const filteredLogs = mockLogs.filter(log => {
if (params.filters?.searchKeyword) { if (params.filters?.searchKeyword) {
const keyword = params.filters.searchKeyword.toLowerCase() const keyword = params.filters.searchKeyword.toLowerCase()
if (!log.url.toLowerCase().includes(keyword) && if (!log.url.toLowerCase().includes(keyword) &&

View File

@@ -242,43 +242,103 @@ export default function AuditHistoryPage() {
date_range: 'all' date_range: 'all'
}); });
// 数据加载函数 - 移除不必要的依赖避免重复调用 // 数据加载函数 - 优先从浏览器URL参数读取
const loadAuditHistory = useCallback(async (params?: { const loadAuditHistory = useCallback(async (options: {
resetPage?: boolean;
filters?: Record<string, string>; filters?: Record<string, string>;
pagination?: { page: number; size: number }; sortBy?: string;
sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' }; sortOrder?: 'asc' | 'desc';
}) => { page?: number;
size?: number;
} = {}) => {
try { try {
console.log('调用了loadAuditHistory'); // 优先从URL读取参数
let urlParams = {};
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
urlParams = {
search: params.get('search') || undefined,
action: params.get('action') || undefined,
audit_status: params.get('audit_status') || undefined,
date_range: params.get('date_range') || undefined,
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
};
console.log('从URL读取的参数:', urlParams);
}
console.log('========================================');
setLoading(true); setLoading(true);
setError(null); setError(null);
const finalParams: AuditLogsQueryParams = { // 解构选项参数,提供默认值
search_keyword: (params?.filters?.search ?? searchFilters.search) || undefined, const {
action: params?.filters?.action ?? searchFilters.action, resetPage = false,
audit_status: params?.filters?.audit_status ?? searchFilters.audit_status, filters,
date_range: params?.filters?.date_range ?? searchFilters.date_range, sortBy,
page: params?.pagination?.page || pagination.page, sortOrder,
size: params?.pagination?.size || pagination.size, page,
order_by: params?.sort?.sortBy, size
sort_order: params?.sort?.sortOrder, } = options;
// 优先级URL参数 > 传入参数 > 父组件状态
const finalPage = resetPage ? 1 : (urlParams.page || page || pagination.page);
const finalSize = urlParams.size || size || pagination.size;
const params: AuditLogsQueryParams = {
page: finalPage,
size: finalSize,
}; };
// 处理筛选条件,如果为'all'则不传该参数 // 使用正确的优先级URL参数 > 传入参数 > 父组件状态
if (finalParams.action === 'all') { const currentFilters = {
finalParams.action = undefined; search: urlParams.search || (filters?.search) || searchFilters.search,
} action: urlParams.action || (filters?.action) || searchFilters.action,
if (finalParams.audit_status === 'all') { audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status,
finalParams.audit_status = undefined; date_range: urlParams.date_range || (filters?.date_range) || searchFilters.date_range
} };
if (finalParams.date_range === 'all') { const currentSortBy = sortBy || 'created_at';
finalParams.date_range = undefined; const currentSortOrder = sortOrder || 'desc';
// 添加搜索条件
if (currentFilters.search) {
params.search_keyword = currentFilters.search;
} }
const response = await fetchAuditLogs(finalParams); if (currentFilters.action && currentFilters.action !== 'all') {
params.action = currentFilters.action;
}
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
params.audit_status = currentFilters.audit_status;
}
if (currentFilters.date_range && currentFilters.date_range !== 'all') {
params.date_range = currentFilters.date_range;
}
if (currentSortBy) {
params.order_by = currentSortBy;
params.sort_order = currentSortOrder;
}
console.log('=== 审核历史页面 - 最终API参数 ===');
console.log('API调用参数 params:', params);
console.log('参数优先级正确: URL参数 > 函数传递参数 > 父组件状态');
console.log('当前currentFilters:', currentFilters);
console.log('==================================');
const response = await fetchAuditLogs(params);
const transformedData = response.data.map(transformAuditLogData); const transformedData = response.data.map(transformAuditLogData);
setRecords(transformedData); setRecords(transformedData);
setPagination({
page: response.page,
size: response.size,
total: response.total,
totalPages: response.total_pages,
hasNext: response.has_next,
hasPrev: response.has_prev,
});
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : '加载审核历史失败'; const errorMessage = err instanceof Error ? err.message : '加载审核历史失败';
setError(errorMessage); setError(errorMessage);
@@ -286,7 +346,7 @@ export default function AuditHistoryPage() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [searchFilters, pagination]); // 添加依赖以保持函数引用最新 }, []); // 移除依赖项,通过参数传递
const didFetchRef = useRef(false) const didFetchRef = useRef(false)
@@ -295,38 +355,94 @@ useEffect(() => {
didFetchRef.current = true didFetchRef.current = true
loadAuditHistory() loadAuditHistory()
}, []) }, [])
// 事件处理器 // 搜索处理 - 保持传统的简洁方式
const handleSearch = useCallback((filters: Record<string, string>) => { const handleSearch = useCallback((filters: Record<string, string>) => {
setSearchFilters(filters); console.log('审核历史页面 - 收到搜索条件:', filters);
// 搜索时重置到第一页
loadAuditHistory({
filters,
pagination: { page: 1, size: pagination.size }
});
}, [loadAuditHistory, pagination.size]);
// 更新过滤器状态
setSearchFilters(filters);
// 搜索时重置到第1页
setPagination(prev => ({ ...prev, page: 1 }));
// 执行查询
loadAuditHistory({
resetPage: true,
page: 1,
filters: filters,
size: pagination.size
});
console.log('触发审核历史查询 - 参数:', {
resetPage: true,
page: 1,
filters: filters,
size: pagination.size
});
}, [pagination.size, loadAuditHistory]);
// 排序处理
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
// 排序时重置到第一页 // 排序时重置到第一页
setPagination(prev => ({ ...prev, page: 1 }));
loadAuditHistory({ loadAuditHistory({
pagination: { page: 1, size: pagination.size }, resetPage: true,
sort: { sortBy, sortOrder } page: 1,
filters: searchFilters,
sortBy,
sortOrder,
size: pagination.size
}); });
}, [loadAuditHistory, pagination.size]); }, [searchFilters, pagination.size, loadAuditHistory]);
// 分页处理
const handlePageChange = useCallback((page: number) => { const handlePageChange = useCallback((page: number) => {
if (page < 1) {
page = 1;
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
page = pagination.totalPages;
}
setPagination(prev => ({ ...prev, page })); setPagination(prev => ({ ...prev, page }));
loadAuditHistory({ loadAuditHistory({
page,
filters: searchFilters, filters: searchFilters,
pagination: { page, size: pagination.size } size: pagination.size
}); });
}, [loadAuditHistory, searchFilters, pagination.size]); }, [searchFilters, pagination.size, pagination.totalPages, loadAuditHistory]);
// 每页条数变化处理
const handleSizeChange = useCallback((size: number) => { const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 })); setPagination(prev => ({ ...prev, size, page: 1 }));
loadAuditHistory({ loadAuditHistory({
filters: searchFilters, resetPage: true,
pagination: { page: 1, size } page: 1,
size,
filters: searchFilters
}); });
}, [loadAuditHistory, searchFilters]); }, [searchFilters, loadAuditHistory]);
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
const handleUrlStateChange = useCallback((urlState: {
filters: Record<string, string>;
pagination: { page: number; size: number };
}) => {
console.log('审核历史页面 - URL状态变化:', urlState);
// 更新内部状态
setSearchFilters(urlState.filters);
setPagination(prev => ({
...prev,
page: urlState.pagination.page,
size: urlState.pagination.size
}));
// 触发数据加载
loadAuditHistory({
page: urlState.pagination.page,
size: urlState.pagination.size,
filters: urlState.filters
});
}, [loadAuditHistory]);
// 业务事件处理器 // 业务事件处理器
const handleView = (record: AuditLogData) => { const handleView = (record: AuditLogData) => {
@@ -381,6 +497,7 @@ useEffect(() => {
emptyIcon={<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />} emptyIcon={<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />}
emptyText="暂无审核记录" emptyText="暂无审核记录"
sizeOptions={[10, 20, 50, 100]} sizeOptions={[10, 20, 50, 100]}
/> />
{/* View Audit Record Details Dialog */} {/* View Audit Record Details Dialog */}

View File

@@ -165,12 +165,13 @@ export default function EnterpriseAuditPage() {
sortable: false, // 禁用排序 sortable: false, // 禁用排序
render: (value: string) => { render: (value: string) => {
const statusConfig = { const statusConfig = {
'草稿': { label: '草稿', variant: 'default' as const, className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200' },
'待审核': { label: '待审核', variant: 'default' as const, className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' }, '待审核': { label: '待审核', variant: 'default' as const, className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
'已通过': { label: '已通过', variant: 'default' as const, className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' }, '已通过': { label: '已通过', variant: 'default' as const, className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
'已驳回': { label: '已驳回', variant: 'default' as const, className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }, '已拒绝': { label: '已拒绝', variant: 'default' as const, className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
}; };
const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['待审核']; const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['草稿'];
return ( return (
<Badge className={`font-light ${config.className}`}> <Badge className={`font-light ${config.className}`}>
{config.label} {config.label}
@@ -227,31 +228,53 @@ export default function EnterpriseAuditPage() {
didFetchRef.current = true didFetchRef.current = true
loadEnterprises() loadEnterprises()
}, []) }, [])
// 加载企业数据 - 移除依赖项,通过参数传递状态 // 加载企业数据 - 统一参数结构
const loadEnterprises = useCallback(async (params?: { const loadEnterprises = useCallback(async (options: {
filters?: Record<string, string>;
pagination?: { page: number; size: number };
sort?: { sortBy?: string; sortOrder: 'asc' | 'desc' };
resetPage?: boolean; resetPage?: boolean;
}) => { filters?: Record<string, string>;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
page?: number;
size?: number;
} = {}) => {
try { try {
dispatch({ type: 'SET_LOADING', payload: true }); dispatch({ type: 'SET_LOADING', payload: true });
const finalParams: TenantsQueryParams = { // 解构选项参数,提供默认值
search: (params?.filters?.search ?? state.filters.search) || undefined, const {
audit_status: params?.filters?.audit_status ?? state.filters.audit_status, resetPage = false,
page: params?.resetPage ? 1 : (params?.pagination?.page || state.pagination.page), filters,
size: params?.pagination?.size || state.pagination.size, sortBy,
order_by: params?.sort?.sortBy, sortOrder,
sort_order: params?.sort?.sortOrder, page,
size
} = options;
const params: TenantsQueryParams = {
page: resetPage ? 1 : (page || state.pagination.page),
size: size || state.pagination.size,
}; };
// 处理audit_status如果为'all'则不传该参数 // 使用传入的过滤器参数,如果没有传入则使用当前状态
if (finalParams.audit_status === 'all') { const currentFilters = filters || state.filters;
finalParams.audit_status = undefined; const currentSortBy = sortBy || 'created_at';
const currentSortOrder = sortOrder || 'desc';
// 添加搜索条件
if (currentFilters.search) {
params.search = currentFilters.search;
} }
const response = await fetchTenantsForAudit(finalParams); if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
params.audit_status = currentFilters.audit_status;
}
if (currentSortBy) {
params.order_by = currentSortBy;
params.sort_order = currentSortOrder;
}
const response = await fetchTenantsForAudit(params);
const transformedData = response.data.map(transformTenantData); const transformedData = response.data.map(transformTenantData);
dispatch({ dispatch({
@@ -284,24 +307,49 @@ export default function EnterpriseAuditPage() {
rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length, rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length,
}), [state.enterprises, state.pagination.total]); }), [state.enterprises, state.pagination.total]);
// 事件处理器 // 搜索处理 - 保持传统的简洁方式
const handleSearch = useCallback((filters: Record<string, string>) => { const handleSearch = useCallback((filters: Record<string, string>) => {
console.log('企业审核页面 - 收到搜索条件:', filters);
// 更新过滤器状态
dispatch({ type: 'SET_FILTERS', payload: filters }); dispatch({ type: 'SET_FILTERS', payload: filters });
loadEnterprises({
filters,
pagination: { page: 1, size: state.pagination.size }
});
}, [loadEnterprises, state.pagination.size]);
// 搜索时重置到第1页
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
// 执行查询
loadEnterprises({
resetPage: true,
page: 1,
filters: filters,
size: state.pagination.size
});
console.log('触发企业审核查询 - 参数:', {
resetPage: true,
page: 1,
filters: filters,
size: state.pagination.size
});
}, [state.pagination.size, loadEnterprises]);
// 排序处理
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
// 排序时重置到第1页
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } }); dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
loadEnterprises({ dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
filters: state.filters,
sort: { sortBy, sortOrder },
resetPage: true
});
}, [loadEnterprises, state.filters]);
loadEnterprises({
resetPage: true,
page: 1,
filters: state.filters,
sortBy,
sortOrder,
size: state.pagination.size
});
}, [state.filters, state.pagination.size, loadEnterprises]);
// 分页处理
const handlePageChange = useCallback((page: number) => { const handlePageChange = useCallback((page: number) => {
// 边界检查,确保页码在有效范围内 // 边界检查,确保页码在有效范围内
if (page < 1) { if (page < 1) {
@@ -309,20 +357,47 @@ export default function EnterpriseAuditPage() {
} else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) { } else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) {
page = state.pagination.totalPages; page = state.pagination.totalPages;
} }
dispatch({ type: 'SET_PAGINATION', payload: { page } }); dispatch({ type: 'SET_PAGINATION', payload: { page } });
loadEnterprises({ loadEnterprises({
page,
filters: state.filters, filters: state.filters,
pagination: { page, size: state.pagination.size } size: state.pagination.size
}); });
}, [loadEnterprises, state.filters, state.pagination.size, state.pagination.totalPages]); }, [state.filters, state.pagination.size, state.pagination.totalPages, loadEnterprises]);
// 每页条数变化处理
const handleSizeChange = useCallback((size: number) => { const handleSizeChange = useCallback((size: number) => {
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } }); dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
loadEnterprises({ loadEnterprises({
filters: state.filters, resetPage: true,
pagination: { page: 1, size } page: 1,
size,
filters: state.filters
}); });
}, [loadEnterprises, state.filters]); }, [state.filters, loadEnterprises]);
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
const handleUrlStateChange = useCallback((urlState: {
filters: Record<string, string>;
pagination: { page: number; size: number };
}) => {
console.log('企业审核页面 - URL状态变化:', urlState);
// 更新内部状态
dispatch({ type: 'SET_FILTERS', payload: urlState.filters });
dispatch({ type: 'SET_PAGINATION', payload: {
page: urlState.pagination.page,
size: urlState.pagination.size
}});
// 触发数据加载
loadEnterprises({
page: urlState.pagination.page,
size: urlState.pagination.size,
filters: urlState.filters
});
}, [loadEnterprises]);
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
dispatch({ type: 'REFRESH_DATA' }); dispatch({ type: 'REFRESH_DATA' });
@@ -435,12 +510,6 @@ export default function EnterpriseAuditPage() {
{/* 搜索、表格和分页 - 使用重构后的组件 */} {/* 搜索、表格和分页 - 使用重构后的组件 */}
<SearchFormPagination <SearchFormPagination
formTitle="企业列表" formTitle="企业列表"
formRightContent={
<Button variant="outline" onClick={handleRefresh} disabled={state.loading}>
<RefreshCw className={`w-4 h-4 mr-2 ${state.loading ? 'animate-spin' : ''}`} />
</Button>
}
searchFields={searchFields} searchFields={searchFields}
columns={columns} columns={columns}
data={state.enterprises} data={state.enterprises}
@@ -458,6 +527,7 @@ export default function EnterpriseAuditPage() {
showSizeSelector={true} showSizeSelector={true}
showPageInfo={true} showPageInfo={true}
sizeOptions={[10, 20, 50, 100]} sizeOptions={[10, 20, 50, 100]}
/> />
{/* 企业详情对话框 - 保留原有功能 */} {/* 企业详情对话框 - 保留原有功能 */}

View File

@@ -246,30 +246,85 @@ export default function EnterpriseManagement() {
didFetchRef.current = true didFetchRef.current = true
loadEnterprises() loadEnterprises()
}, []) }, [])
// 数据加载函数 - 移除不必要的依赖避免重复调用 // 数据加载函数 - 参考audit-history页面的统一参数结构优先从URL参数读取
const loadEnterprises = useCallback(async (params?: { const loadEnterprises = useCallback(async (options: {
resetPage?: boolean;
filters?: Record<string, string>; filters?: Record<string, string>;
pagination?: { page: number; size: number }; sortBy?: string;
sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' }; sortOrder?: 'asc' | 'desc';
}) => { page?: number;
size?: number;
} = {}) => {
try { try {
console.log('调用了loadEnterprises') console.log('=== 企业管理页面 - loadEnterprises 调用 ===');
console.log('传入的options参数:', options);
console.log('当前searchFilters:', searchFilters);
console.log('当前pagination:', pagination);
// 优先从URL读取参数
let urlParams = {};
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
urlParams = {
search: params.get('search') || undefined,
audit_status: params.get('audit_status') || undefined,
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
};
console.log('从URL读取的参数:', urlParams);
}
console.log('========================================');
setLoading(true); setLoading(true);
setError(null); setError(null);
// 解构选项参数,提供默认值
const {
resetPage = false,
filters,
sortBy,
sortOrder,
page,
size
} = options;
// 优先级URL参数 > 传入参数 > 父组件状态
const finalPage = resetPage ? 1 : (page || urlParams.page || pagination.page);
const finalSize = size || urlParams.size || pagination.size;
const finalParams: TenantsQueryParams = { const finalParams: TenantsQueryParams = {
search: (params?.filters?.search ?? searchFilters.search) || undefined, page: finalPage,
audit_status: params?.filters?.audit_status ?? searchFilters.audit_status, size: finalSize,
page: params?.pagination?.page || pagination.page,
size: params?.pagination?.size || pagination.size,
order_by: params?.sort?.sortBy,
sort_order: params?.sort?.sortOrder,
}; };
// 处理audit_status如果为'all'则不传该参数 // 使用正确的优先级URL参数 > 传入参数 > 父组件状态
if (finalParams.audit_status === 'all') { const currentFilters = {
finalParams.audit_status = undefined; search: urlParams.search || (filters?.search) || searchFilters.search,
audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status
};
const currentSortBy = sortBy || 'created_at';
const currentSortOrder = sortOrder || 'desc';
// 添加搜索条件
if (currentFilters.search) {
finalParams.search = currentFilters.search;
} }
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
finalParams.audit_status = currentFilters.audit_status;
}
if (currentSortBy) {
finalParams.order_by = currentSortBy;
finalParams.sort_order = currentSortOrder;
}
console.log('=== 企业管理页面 - 最终API参数 ===');
console.log('API调用参数 finalParams:', finalParams);
console.log('参数优先级正确: URL参数 > 传入参数 > 父组件状态');
console.log('当前currentFilters:', currentFilters);
console.log('==================================');
const response = await fetchTenants(finalParams); const response = await fetchTenants(finalParams);
const transformedData = response.data.map(transformTenantData); const transformedData = response.data.map(transformTenantData);
@@ -291,51 +346,105 @@ export default function EnterpriseManagement() {
} }
}, []); // 移除所有依赖,使用参数传递状态变化 }, []); // 移除所有依赖,使用参数传递状态变化
// 事件处理器 // 搜索处理 - 参考audit-history页面的统一方式
const handleSearch = useCallback((filters: Record<string, string>) => { const handleSearch = useCallback((filters: Record<string, string>) => {
setSearchFilters(filters); console.log('企业管理页面 - 收到搜索条件:', filters);
// 搜索时重置到第一页
loadEnterprises({
filters,
pagination: { page: 1, size: pagination.size }
});
}, [loadEnterprises, pagination.size]);
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { // 更新过滤器状态
// 排序时重置到第一页 setSearchFilters(filters);
// 搜索时重置到第1页
setPagination(prev => ({ ...prev, page: 1 }));
// 执行查询
loadEnterprises({ loadEnterprises({
pagination: { page: 1, size: pagination.size }, resetPage: true,
sort: { sortBy, sortOrder } page: 1,
filters: filters,
size: pagination.size
}); });
}, [loadEnterprises, pagination.size]);
console.log('触发企业管理查询 - 参数:', {
resetPage: true,
page: 1,
filters: filters,
size: pagination.size
});
}, [pagination.size, loadEnterprises]);
// 排序处理
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
// 排序时重置到第1页
setPagination(prev => ({ ...prev, page: 1 }));
loadEnterprises({
resetPage: true,
page: 1,
filters: searchFilters,
sortBy,
sortOrder,
size: pagination.size
});
}, [searchFilters, pagination.size, loadEnterprises]);
// 统一的数据重载函数 - 避免重复代码 // 统一的数据重载函数 - 避免重复代码
const reloadData = useCallback(() => { const reloadData = useCallback(() => {
const reloadParams = { loadEnterprises({
filters: searchFilters,
pagination: {
page: pagination.page, page: pagination.page,
size: pagination.size size: pagination.size,
} filters: searchFilters
}; });
loadEnterprises(reloadParams);
}, [loadEnterprises, searchFilters, pagination]); }, [loadEnterprises, searchFilters, pagination]);
// 分页处理 - 参考audit-history页面的统一方式
const handlePageChange = useCallback((page: number) => { const handlePageChange = useCallback((page: number) => {
// 边界检查,确保页码在有效范围内
if (page < 1) {
page = 1;
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
page = pagination.totalPages;
}
setPagination(prev => ({ ...prev, page })); setPagination(prev => ({ ...prev, page }));
loadEnterprises({ loadEnterprises({
page,
filters: searchFilters, filters: searchFilters,
pagination: { page, size: pagination.size } size: pagination.size
}); });
}, [loadEnterprises, searchFilters, pagination.size]); }, [searchFilters, pagination.size, pagination.totalPages, loadEnterprises]);
// 每页条数变化处理
const handleSizeChange = useCallback((size: number) => { const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 })); setPagination(prev => ({ ...prev, size, page: 1 }));
loadEnterprises({ loadEnterprises({
filters: searchFilters, resetPage: true,
pagination: { page: 1, size } page: 1,
size,
filters: searchFilters
}); });
}, [loadEnterprises, searchFilters]); }, [searchFilters, loadEnterprises]);
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
const handleUrlStateChange = useCallback((urlState: {
filters: Record<string, string>;
pagination: { page: number; size: number };
}) => {
console.log('企业管理页面 - URL状态变化:', urlState);
// 更新内部状态
setSearchFilters(urlState.filters);
setPagination(prev => ({
...prev,
page: urlState.pagination.page,
size: urlState.pagination.size
}));
// 触发数据加载
loadEnterprises({
page: urlState.pagination.page,
size: urlState.pagination.size,
filters: urlState.filters
});
}, [loadEnterprises]);
// 初始化数据加载 // 初始化数据加载
// useEffect(() => { // useEffect(() => {

View File

@@ -282,6 +282,20 @@ export default function TenantUserManagementPage() {
size?: number; size?: number;
} = {}) => { } = {}) => {
try { try {
// 优先从URL读取参数
let urlParams = {};
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
urlParams = {
search: params.get('search') || undefined,
status: params.get('status') || undefined,
type: params.get('type') || undefined,
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
};
console.log('从URL读取的参数:', urlParams);
}
dispatch({ type: 'SET_LOADING', payload: true }); dispatch({ type: 'SET_LOADING', payload: true });
// 解构选项参数,提供默认值 // 解构选项参数,提供默认值
@@ -294,14 +308,22 @@ export default function TenantUserManagementPage() {
size size
} = options; } = options;
// 优先级URL参数 > 传入参数 > 父组件状态
const finalPage = resetPage ? 1 : (urlParams.page || page || state.pagination.page);
const finalSize = urlParams.size || size || state.pagination.size;
const params: UsersQueryParams = { const params: UsersQueryParams = {
page: resetPage ? 1 : (page || state.pagination.page), page: finalPage,
size: size || state.pagination.size, size: finalSize,
is_active: true, is_active: true,
}; };
// 使用传入的过滤器参数,如果没有传入则使用当前状态 // 使用正确的优先级URL参数 > 传入参数 > 父组件状态
const currentFilters = filters || state.filters; const currentFilters = {
searchKeyword: urlParams.search || (filters?.searchKeyword) || state.filters.searchKeyword,
statusFilter: urlParams.status || (filters?.statusFilter) || state.filters.statusFilter,
typeFilter: urlParams.type || (filters?.typeFilter) || state.filters.typeFilter
};
const currentSortBy = sortBy || state.sortBy; const currentSortBy = sortBy || state.sortBy;
const currentSortOrder = sortOrder || state.sortOrder; const currentSortOrder = sortOrder || state.sortOrder;
@@ -350,24 +372,41 @@ export default function TenantUserManagementPage() {
} }
}, []); }, []);
// 搜索处理 // 搜索处理 - 保持传统的简洁方式
const handleSearch = useCallback((filters: Record<string, string>) => { const handleSearch = useCallback((filters: Record<string, string>) => {
console.log('用户管理页面 - 收到搜索条件:', filters);
const mappedFilters = { const mappedFilters = {
searchKeyword: filters.search || '', searchKeyword: filters.search || '',
statusFilter: filters.status || 'all', statusFilter: filters.status || 'all',
typeFilter: filters.type || 'all' typeFilter: filters.type || 'all'
}; };
// 更新过滤器状态
dispatch({ type: 'SET_FILTERS', payload: mappedFilters }); dispatch({ type: 'SET_FILTERS', payload: mappedFilters });
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
// 传入所有当前参数,避免覆盖其他参数 // 搜索时重置到第1页
dispatch({ type: 'SET_PAGINATION', payload: { page: 1, size: state.pagination.size } });
// 执行查询
loadUsers({ loadUsers({
resetPage: true, resetPage: true,
page: 1,
filters: mappedFilters, filters: mappedFilters,
sortBy: state.sortBy, sortBy: state.sortBy,
sortOrder: state.sortOrder, sortOrder: state.sortOrder,
size: state.pagination.size size: state.pagination.size
}); });
}, [state.sortBy, state.sortOrder, state.pagination.size]);
console.log('触发用户查询 - 参数:', {
resetPage: true,
page: 1,
filters: mappedFilters,
sortBy: state.sortBy,
sortOrder: state.sortOrder,
size: state.pagination.size
});
}, [state.sortBy, state.sortOrder, state.pagination.size, loadUsers]);
// 排序处理 // 排序处理
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {

View File

@@ -266,7 +266,7 @@ export default function MenuManagementPage() {
const expandAll = () => { const expandAll = () => {
const getAllMenuIds = (menus: Menu[]): string[] => { const getAllMenuIds = (menus: Menu[]): string[] => {
let ids: string[] = []; const ids: string[] = [];
menus.forEach(menu => { menus.forEach(menu => {
if (menu.children && menu.children.length > 0) { if (menu.children && menu.children.length > 0) {
ids.push(menu.id); ids.push(menu.id);

View File

@@ -96,7 +96,7 @@ export function PaginationComponent({
// 否则生成智能的页码显示范围 // 否则生成智能的页码显示范围
const half = Math.floor(maxVisiblePages / 2); const half = Math.floor(maxVisiblePages / 2);
let start = Math.max(1, page - half); let start = Math.max(1, page - half);
let end = Math.min(totalPages, start + maxVisiblePages - 1); const end = Math.min(totalPages, start + maxVisiblePages - 1);
// 调整开始位置,确保显示足够数量的页码 // 调整开始位置,确保显示足够数量的页码
if (end - start < maxVisiblePages - 1) { if (end - start < maxVisiblePages - 1) {

View File

@@ -74,20 +74,44 @@ export function SearchFormComponent({
}; };
// 使用防抖来减少搜索频率,仅针对文本输入 // 使用防抖来减少搜索频率,仅针对文本输入
// 优化添加ref跟踪防抖状态避免不必要的重新执行
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
const lastTextChangeRef = useRef<string>('');
useEffect(() => { useEffect(() => {
// 只有当最后变化的是 text 字段时才进行防抖,排除初始化和 select 字段 // 只有当最后变化的是 text 字段时才进行防抖,排除初始化和 select 字段
if (localFilters._lastChangedFieldType === 'text') { if (localFilters._lastChangedFieldType === 'text') {
const timer = setTimeout(() => { // 提取当前文本字段的值
const textValue = localFilters[localFilters._lastChangedFieldKey] || '';
// 只有当文本内容真正发生变化时才进行防抖
if (textValue !== lastTextChangeRef.current) {
lastTextChangeRef.current = textValue;
// 清除之前的防抖定时器
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
debounceTimeoutRef.current = setTimeout(() => {
// 移除标记字段后再调用 // 移除标记字段后再调用
const { _lastChangedFieldType, _lastChangedFieldKey, ...cleanFilters } = localFilters; const { _lastChangedFieldType, _lastChangedFieldKey, ...cleanFilters } = localFilters;
// 使用ref引用最新的onFiltersChange函数避免依赖变化导致重复触发 // 使用ref引用最新的onFiltersChange函数避免依赖变化导致重复触发
onFiltersChangeRef.current(cleanFilters); onFiltersChangeRef.current(cleanFilters);
}, 300); // 300ms 防抖延迟 }, 300); // 300ms 防抖延迟
}
return () => clearTimeout(timer);
} }
}, [localFilters]); // 只依赖localFilters }, [localFilters]); // 只依赖localFilters
// 组件卸载时清理防抖定时器
useEffect(() => {
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
};
}, []);
// 计算显示的字段 // 计算显示的字段
const visibleFields = showAllFields const visibleFields = showAllFields
? fields ? fields

View File

@@ -6,7 +6,7 @@
*/ */
'use client'; 'use client';
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -47,7 +47,28 @@ export interface PaginationConfig {
hasPrev: boolean; hasPrev: boolean;
} }
// 组件Props接口 - 简化版本 // URL 参数同步配置
export interface UrlSyncConfig {
// 是否启用 URL 参数同步默认为true只有传false才禁用
enabled?: boolean;
// 是否在初始化时检测空 URL 并添加默认参数默认为true
initWithDefaults?: boolean;
// 默认分页参数(默认为 { page: 1, size: 10 }
defaultPagination?: {
page: number;
size: number;
};
// URL 更新防抖时间毫秒默认为300
updateDebounce?: number;
// 自定义 URL 参数名映射(可选,不配置则自动从 searchFields 提取)
paramNames?: {
page?: string;
size?: string;
[key: string]: string | undefined;
};
}
// 组件Props接口 - 增强版本
export interface SearchFormPaginationProps<T = any> { export interface SearchFormPaginationProps<T = any> {
// 搜索表单配置 // 搜索表单配置
formTitle?: string; formTitle?: string;
@@ -81,6 +102,9 @@ export interface SearchFormPaginationProps<T = any> {
// 自定义样式 // 自定义样式
className?: string; className?: string;
// URL 参数同步配置
urlSync?: UrlSyncConfig;
// 数据更新回调 - 用于父组件获取搜索条件变化 // 数据更新回调 - 用于父组件获取搜索条件变化
onDataUpdate?: (data: { onDataUpdate?: (data: {
items: T[]; items: T[];
@@ -88,6 +112,15 @@ export interface SearchFormPaginationProps<T = any> {
loading: boolean; loading: boolean;
error: string | null; error: string | null;
}) => void; }) => void;
// URL 状态变化回调 - 用于父组件监听 URL 参数变化(可选)
onUrlStateChange?: (urlState: {
filters: Record<string, string>;
pagination: {
page: number;
size: number;
};
}) => void;
} }
export function SearchFormPagination<T = any>({ export function SearchFormPagination<T = any>({
@@ -111,16 +144,313 @@ export function SearchFormPagination<T = any>({
sizeOptions = [10, 30, 50, 100], sizeOptions = [10, 30, 50, 100],
maxVisiblePages = 7, maxVisiblePages = 7,
className = '', className = '',
urlSync,
onDataUpdate, onDataUpdate,
onUrlStateChange,
}: SearchFormPaginationProps<T>) { }: SearchFormPaginationProps<T>) {
// URL 同步配置 - 自动从 searchFields 提取参数映射
const urlConfig = useMemo(() => {
// 默认启用URL同步除非明确禁用
const isEnabled = urlSync?.enabled !== false; // 默认为true只有明确传false才禁用
// 如果启用 URL 同步,自动生成参数映射
const autoParamNames = isEnabled ? {
page: urlSync?.paramNames?.page ?? 'page',
size: urlSync?.paramNames?.size ?? 'size',
// 从 searchFields 自动提取参数映射
...Object.fromEntries(
searchFields.map(field => [
field.key,
urlSync?.paramNames?.[field.key] ?? field.key // 优先使用自定义映射,否则使用字段 key
])
)
} : {};
return {
enabled: isEnabled,
paramNames: autoParamNames,
initWithDefaults: urlSync?.initWithDefaults ?? true, // 默认为true
defaultPagination: urlSync?.defaultPagination ?? { page: 1, size: 10 },
updateDebounce: urlSync?.updateDebounce ?? 300 // 默认300ms
};
}, [urlSync, searchFields]);
// URL 参数解析和同步相关的 ref
const isInitializingRef = useRef(false);
const lastUrlStateRef = useRef<string>('');
// 更新来源跟踪 - 用于区分不同类型的更新
const updateSourceRef = useRef<'user' | 'parent' | 'url' | 'init'>('init');
// 简化的内部状态 - 只管理搜索条件 // 简化的内部状态 - 只管理搜索条件
const [filters, setFilters] = useState<Record<string, string>>( const [filters, setFilters] = useState<Record<string, string>>(() => {
searchFields.reduce((acc, field) => { // 初始化时从 URL 读取参数(如果启用 URL 同步)
if (urlConfig.enabled && typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search);
const initialFilters: Record<string, string> = {};
// 从 URL 中读取搜索字段参数
searchFields.forEach(field => {
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
if (paramValue) {
initialFilters[field.key] = paramValue;
} else {
initialFilters[field.key] = field.defaultValue || '';
}
});
return initialFilters;
}
// 默认初始化逻辑
return searchFields.reduce((acc, field) => {
acc[field.key] = field.defaultValue || ''; acc[field.key] = field.defaultValue || '';
return acc; return acc;
}, {} as Record<string, string>) }, {} as Record<string, string>);
});
// URL 更新函数 - 事件驱动模型,移除防抖
const updateUrl = useCallback((
newFilters: Record<string, string>,
newPagination?: { page: number; size: number },
source: 'user' | 'parent' | 'url' | 'init' = 'user'
) => {
if (!urlConfig.enabled || typeof window === 'undefined') return;
// 设置更新来源
updateSourceRef.current = source;
// 立即执行URL更新不使用防抖
try {
const url = new URL(window.location.href);
const urlParams = url.searchParams;
console.log(`开始更新URL - 来源: ${source}`, { newFilters, newPagination });
// 更新搜索参数
Object.entries(newFilters).forEach(([key, value]) => {
const paramName = urlConfig.paramNames[key] || key;
if (value && value !== '') {
urlParams.set(paramName, value);
} else {
urlParams.delete(paramName);
}
});
// 更新分页参数
if (newPagination) {
urlParams.set(urlConfig.paramNames.page, newPagination.page.toString());
urlParams.set(urlConfig.paramNames.size, newPagination.size.toString());
}
// 构建新的 URL 状态字符串
const newUrlState = urlParams.toString();
// 只有在 URL 状态发生变化时才更新
if (newUrlState !== lastUrlStateRef.current) {
lastUrlStateRef.current = newUrlState;
// 更新浏览器历史记录
window.history.replaceState({}, '', url.toString());
// 只有在 URL 变化时才通知父组件(避免循环调用)
// 用户操作和父组件更新不需要触发 onUrlStateChange
// 只有 URL 本身变化(如浏览器前进后退)才需要触发
if (source === 'url' && !isInitializingRef.current) {
onUrlStateChange?.({
filters: newFilters,
pagination: newPagination || urlConfig.defaultPagination
});
}
}
} catch (error) {
console.error('Failed to update URL:', error);
}
}, [urlConfig, onUrlStateChange]);
// 内部状态管理
const [internalPagination, setInternalPagination] = useState<{ page: number; size: number }>(
urlConfig.defaultPagination
); );
// 从 URL 读取分页参数的函数
const readPaginationFromUrl = useCallback((): { page: number; size: number } => {
if (!urlConfig.enabled || typeof window === 'undefined') {
return urlConfig.defaultPagination;
}
try {
const urlParams = new URLSearchParams(window.location.search);
const urlPage = urlParams.get(urlConfig.paramNames.page);
const urlSize = urlParams.get(urlConfig.paramNames.size);
return {
page: urlPage ? Math.max(1, parseInt(urlPage, 10) || 1) : urlConfig.defaultPagination.page,
size: urlSize ? Math.max(1, parseInt(urlSize, 10) || 10) : urlConfig.defaultPagination.size
};
} catch (error) {
console.error('Failed to read pagination from URL:', error);
return urlConfig.defaultPagination;
}
}, [urlConfig]);
// 初始化 URL 参数检测和默认值设置
useEffect(() => {
if (!urlConfig.enabled || typeof window === 'undefined' || isInitializingRef.current) return;
isInitializingRef.current = true;
try {
const urlParams = new URLSearchParams(window.location.search);
const hasSearchParams = urlParams.toString() !== '';
console.log('URL同步初始化 - 当前URL参数:', Object.fromEntries(urlParams.entries()));
console.log('URL同步初始化 - 是否有URL参数:', hasSearchParams);
// 如果 URL 没有参数且配置要求初始化默认值
if (!hasSearchParams && urlConfig.initWithDefaults) {
const url = new URL(window.location.href);
// 始终设置默认分页参数到URL不管是否为默认值
urlParams.set(urlConfig.paramNames.page, urlConfig.defaultPagination.page.toString());
urlParams.set(urlConfig.paramNames.size, urlConfig.defaultPagination.size.toString());
// 设置默认搜索字段值 - 优先使用searchFields的defaultValue
searchFields.forEach(field => {
const fieldValue = filters[field.key] || field.defaultValue || '';
if (fieldValue !== '') {
const paramName = urlConfig.paramNames[field.key] || field.key;
urlParams.set(paramName, fieldValue);
}
});
if (urlParams.toString() !== '') {
url.search = urlParams.toString();
window.history.replaceState({}, '', url.toString());
}
}
// 从URL读取过滤条件和分页参数
const urlFilters: Record<string, string> = {};
let hasUrlFilters = false;
searchFields.forEach(field => {
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
if (paramValue) {
urlFilters[field.key] = paramValue;
hasUrlFilters = true;
}
});
const urlPagination = readPaginationFromUrl();
console.log('URL同步初始化 - 从URL读取的过滤条件:', urlFilters);
console.log('URL同步初始化 - 从URL读取的分页参数:', urlPagination);
// 设置内部分页状态
setInternalPagination(urlPagination);
// URL中有参数时自动同步到内部状态并通知父组件
if (hasUrlFilters || hasSearchParams) {
const finalFilters = hasUrlFilters ? urlFilters : filters;
console.log('URL同步初始化 - 同步参数到内部状态:', {
filters: finalFilters,
pagination: urlPagination
});
// 更新内部状态
if (hasUrlFilters) {
setFilters(finalFilters);
}
// 通知父组件URL状态变化标记为初始化
// 使用下一个事件循环确保状态已更新
Promise.resolve().then(() => {
if (!isInitializingRef.current) {
onUrlStateChange?.({
filters: finalFilters,
pagination: urlPagination
});
}
});
}
} catch (error) {
console.error('Failed to initialize URL parameters:', error);
}
}, [urlConfig, filters, searchFields, readPaginationFromUrl]);
// 监听父组件传入的分页状态变化 - 同步到内部状态和URL
useEffect(() => {
if (pagination && urlConfig.enabled && !isInitializingRef.current) {
console.log('父组件分页状态变化:', pagination);
// 如果父组件的分页状态与内部状态不一致,同步内部状态
if (pagination.page !== internalPagination.page || pagination.size !== internalPagination.size) {
setInternalPagination({
page: pagination.page,
size: pagination.size
});
// 同步到URL标记为父组件更新
updateUrl(filters, pagination, 'parent');
}
}
}, [pagination, filters, urlConfig.enabled, updateUrl, internalPagination.page, internalPagination.size]);
// 监听浏览器前进后退事件
useEffect(() => {
if (!urlConfig.enabled || typeof window === 'undefined') return;
const handlePopState = (event: PopStateEvent) => {
console.log('浏览器前进后退事件触发:', event);
const urlParams = new URLSearchParams(window.location.search);
const newFilters: Record<string, string> = {};
// 从 URL 中读取搜索字段参数
searchFields.forEach(field => {
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
if (paramValue) {
newFilters[field.key] = paramValue;
} else {
newFilters[field.key] = field.defaultValue || '';
}
});
const newPagination = {
page: Math.max(1, parseInt(urlParams.get(urlConfig.paramNames.page) || '1', 10)),
size: Math.max(1, parseInt(urlParams.get(urlConfig.paramNames.size) || '10', 10))
};
console.log('从URL恢复状态:', { newFilters, newPagination });
// 更新内部状态标记为URL变化
setFilters(newFilters);
setInternalPagination(newPagination);
// 通知父组件 URL 状态变化只有URL来源的变化才触发
onUrlStateChange?.({
filters: newFilters,
pagination: newPagination
});
};
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [urlConfig, searchFields, onUrlStateChange]);
// 重置初始化标志 - 确保在初始化完成后允许 URL 更新
useEffect(() => {
if (isInitializingRef.current) {
// 立即重置初始化标志,不使用延迟
isInitializingRef.current = false;
}
}, []);
// 数据更新回调 - 通知父组件数据变化 // 数据更新回调 - 通知父组件数据变化
useEffect(() => { useEffect(() => {
onDataUpdate?.({ onDataUpdate?.({
@@ -138,19 +468,54 @@ export function SearchFormPagination<T = any>({
}); });
}, [data, pagination, loading, error, onDataUpdate]); }, [data, pagination, loading, error, onDataUpdate]);
// 简化的事件处理器 - 纯粹的状态通知 // 简化的事件处理器 - 事件驱动模型
const handleSearch = useCallback((newFilters: Record<string, string>) => { const handleSearch = useCallback((newFilters: Record<string, string>) => {
console.log('用户搜索操作:', newFilters);
// 更新内部状态
setFilters(newFilters); setFilters(newFilters);
// 搜索时重置到第1页保持当前每页大小
const newPagination = { page: 1, size: internalPagination.size };
setInternalPagination(newPagination);
// 通知父组件搜索条件变化
onSearch?.(newFilters); onSearch?.(newFilters);
}, [onSearch]);
// 同步到URL标记为用户操作
updateUrl(newFilters, newPagination, 'user');
}, [internalPagination.size, onSearch, updateUrl]);
const handlePageChange = useCallback((page: number) => { const handlePageChange = useCallback((page: number) => {
console.log('用户分页操作:', page);
const newPagination = { ...internalPagination, page };
// 更新内部状态
setInternalPagination(newPagination);
// 通知父组件分页变化
onPageChange?.(page); onPageChange?.(page);
}, [onPageChange]);
// 同步到URL标记为用户操作
updateUrl(filters, newPagination, 'user');
}, [internalPagination, filters, onPageChange, updateUrl]);
const handleSizeChange = useCallback((size: number) => { const handleSizeChange = useCallback((size: number) => {
console.log('用户修改每页大小:', size);
// 修改每页大小时重置到第1页
const newPagination = { page: 1, size };
// 更新内部状态
setInternalPagination(newPagination);
// 通知父组件每页大小变化
onSizeChange?.(size); onSizeChange?.(size);
}, [onSizeChange]);
// 同步到URL标记为用户操作
updateUrl(filters, newPagination, 'user');
}, [filters, onSizeChange, updateUrl]);
// 稳定的filters引用 // 稳定的filters引用
const stableFilters = useMemo(() => filters, [filters]); const stableFilters = useMemo(() => filters, [filters]);

289
src/env/index.ts vendored
View File

@@ -1,289 +0,0 @@
/**
* 环境变量管理系统
*
* 这个模块提供统一的环境配置管理,支持多环境切换
* 根据构建时的 NODE_ENV 自动加载对应的环境配置文件
*/
// 环境类型定义
export type Environment = 'dev' | 'test' | 'uat' | 'prod';
// 环境配置接口
export interface EnvironmentConfig {
// 基础配置
NODE_ENV: string;
FRONTEND_BASE_URL: string;
BACKEND_BASE_URL: string;
API_VERSION: string;
// 功能配置
DEBUG: boolean;
USE_MOCK: boolean;
// 应用信息
APP_NAME: string;
ENV_DESCRIPTION: string;
// 高级配置
API_TIMEOUT?: number;
ENABLE_ERROR_LOGGING?: boolean;
ENABLE_PERFORMANCE_MONITORING?: boolean;
ENABLE_USER_BEHAVIOR_TRACKING?: boolean;
SENTRY_DSN?: string;
CDN_BASE_URL?: string;
// 生产环境特有
ENABLE_MAINTENANCE_MODE?: boolean;
ENABLE_NEW_FEATURES?: boolean;
ENABLE_BETA_FEATURES?: boolean;
ENABLE_ANALYTICS?: boolean;
CACHE_TTL?: number;
ENABLE_SERVICE_WORKER?: boolean;
}
// 环境配置映射
const ENV_CONFIGS: Record<Environment, EnvironmentConfig> = {
dev: {
NODE_ENV: 'development',
FRONTEND_BASE_URL: 'https://cavin-smart-crop-ui-app.dev.maimaiag.com',
BACKEND_BASE_URL: 'https://cavin-smart-crop-backend-app.dev.maimaiag.com',
API_VERSION: 'v1',
DEBUG: true,
USE_MOCK: false,
APP_NAME: '智慧农业生产管理系统',
ENV_DESCRIPTION: '开发环境',
API_TIMEOUT: 10000,
ENABLE_ERROR_LOGGING: true,
ENABLE_PERFORMANCE_MONITORING: true,
},
test: {
NODE_ENV: 'test',
FRONTEND_BASE_URL: 'https://cavin-smart-crop-ui-app.test.maimaiag.com',
BACKEND_BASE_URL: 'https://cavin-smart-crop-backend-app.dev.maimaiag.com', // 临时使用开发环境后端
API_VERSION: 'v1',
DEBUG: true,
USE_MOCK: false,
APP_NAME: '智慧农业生产管理系统',
ENV_DESCRIPTION: '测试环境',
API_TIMEOUT: 15000,
ENABLE_ERROR_LOGGING: true,
ENABLE_PERFORMANCE_MONITORING: true,
ENABLE_USER_BEHAVIOR_TRACKING: true,
},
uat: {
NODE_ENV: 'production',
FRONTEND_BASE_URL: 'https://cavin-smart-crop-ui-app.uat.maimaiag.com',
BACKEND_BASE_URL: 'https://cavin-smart-crop-backend-app.uat.maimaiag.com',
API_VERSION: 'v1',
DEBUG: false,
USE_MOCK: false,
APP_NAME: '智慧农业生产管理系统',
ENV_DESCRIPTION: 'UAT 环境',
API_TIMEOUT: 20000,
ENABLE_ERROR_LOGGING: true,
ENABLE_PERFORMANCE_MONITORING: true,
ENABLE_USER_BEHAVIOR_TRACKING: true,
SENTRY_DSN: 'https://your-sentry-dsn.uat@sentry.io/project-id',
},
prod: {
NODE_ENV: 'production',
FRONTEND_BASE_URL: 'https://cavin-smart-crop-ui-app.prod.maimaiag.com',
BACKEND_BASE_URL: 'https://cavin-smart-crop-backend-app.prod.maimaiag.com',
API_VERSION: 'v1',
DEBUG: false,
USE_MOCK: false,
APP_NAME: '智慧农业生产管理系统',
ENV_DESCRIPTION: '生产环境',
API_TIMEOUT: 30000,
ENABLE_ERROR_LOGGING: true,
ENABLE_PERFORMANCE_MONITORING: true,
ENABLE_USER_BEHAVIOR_TRACKING: true,
ENABLE_ANALYTICS: true,
SENTRY_DSN: 'https://your-sentry-dsn.prod@sentry.io/project-id',
CDN_BASE_URL: 'https://cdn.cavin-smart-crop.com',
ENABLE_MAINTENANCE_MODE: false,
ENABLE_NEW_FEATURES: true,
ENABLE_BETA_FEATURES: false,
CACHE_TTL: 3600,
ENABLE_SERVICE_WORKER: true,
},
};
/**
* 获取当前环境
*
* 优先级:
* 1. 环境变量 NEXT_PUBLIC_ENV
* 2. 环境变量 NODE_ENV
* 3. 默认 'dev' 环境
*/
export const getEnv = (): Environment => {
// 浏览器环境
if (typeof window !== 'undefined') {
const env = process.env.NEXT_PUBLIC_ENV as Environment;
if (env && Object.keys(ENV_CONFIGS).includes(env)) {
return env;
}
}
// 服务端环境
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv === 'development') return 'dev';
if (nodeEnv === 'test') return 'test';
if (nodeEnv === 'production') {
// 生产环境下需要进一步区分 UAT 和 PROD
const env = process.env.NEXT_PUBLIC_ENV as Environment;
return (env === 'uat' || env === 'prod') ? env : 'prod';
}
return 'dev'; // 默认开发环境
};
/**
* 获取当前环境配置
*/
export const getEnvConfig = (): EnvironmentConfig => {
const currentEnv = getEnv();
return ENV_CONFIGS[currentEnv];
};
/**
* 获取当前环境名称
*/
export const getEnvironmentName = (): string => {
const config = getEnvConfig();
return config.ENV_DESCRIPTION;
};
/**
* 获取当前服务器域名(前端地址)
*/
export const getLocalhost = (): string => {
const config = getEnvConfig();
return config.FRONTEND_BASE_URL;
};
/**
* 获取后端 API 地址
*/
export const getBackendUrl = (): string => {
const config = getEnvConfig();
return config.BACKEND_BASE_URL;
};
/**
* 获取完整的 API 地址
*/
export const getApiUrl = (path?: string): string => {
const backendUrl = getBackendUrl();
const apiVersion = getEnvConfig().API_VERSION;
const apiPath = `/api/${apiVersion}${path || ''}`;
return `${backendUrl}${apiPath}`;
};
/**
* 是否为开发环境
*/
export const isDevelopment = (): boolean => {
return getEnv() === 'dev';
};
/**
* 是否为测试环境
*/
export const isTest = (): boolean => {
return getEnv() === 'test';
};
/**
* 是否为 UAT 环境
*/
export const isUAT = (): boolean => {
return getEnv() === 'uat';
};
/**
* 是否为生产环境
*/
export const isProduction = (): boolean => {
return getEnv() === 'prod';
};
/**
* 是否为非开发环境
*/
export const isNonDevelopment = (): boolean => {
return !isDevelopment();
};
/**
* 是否开启调试模式
*/
export const isDebugMode = (): boolean => {
return getEnvConfig().DEBUG;
};
/**
* 是否使用 Mock 数据
*/
export const shouldUseMock = (): boolean => {
return getEnvConfig().USE_MOCK;
};
/**
* 获取 API 超时时间(毫秒)
*/
export const getApiTimeout = (): number => {
return getEnvConfig().API_TIMEOUT || 10000;
};
/**
* 获取应用名称
*/
export const getAppName = (): string => {
return getEnvConfig().APP_NAME;
};
/**
* 环境信息调试输出
*/
export const debugEnvInfo = (): void => {
if (isDebugMode()) {
const config = getEnvConfig();
const env = getEnv();
console.group('🌍 环境信息');
console.log('当前环境:', env);
console.log('环境描述:', config.ENV_DESCRIPTION);
console.log('前端地址:', config.FRONTEND_BASE_URL);
console.log('后端地址:', config.BACKEND_BASE_URL);
console.log('调试模式:', config.DEBUG);
console.log('使用 Mock:', config.USE_MOCK);
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('NEXT_PUBLIC_ENV:', process.env.NEXT_PUBLIC_ENV);
console.groupEnd();
}
};
// 默认导出主要函数
export default {
getEnv,
getEnvConfig,
getEnvironmentName,
getLocalhost,
getBackendUrl,
getApiUrl,
isDevelopment,
isTest,
isUAT,
isProduction,
isNonDevelopment,
isDebugMode,
shouldUseMock,
getApiTimeout,
getAppName,
debugEnvInfo,
};

View File

@@ -16,7 +16,6 @@ export type {
Config, Config,
CreateClientConfig, CreateClientConfig,
Options, Options,
OptionsLegacyParser,
RequestOptions, RequestOptions,
RequestResult, RequestResult,
ResolvedRequestOptions, ResolvedRequestOptions,

View File

@@ -194,7 +194,7 @@ type BuildUrlFn = <
url: string; url: string;
}, },
>( >(
options: Pick<TData, 'url'> & Options<TData>, options: TData & Options<TData>,
) => string; ) => string;
export type Client = CoreClient< export type Client = CoreClient<
@@ -238,31 +238,4 @@ export type Options<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>, RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url' 'body' | 'path' | 'query' | 'url'
> & > &
Omit<TData, 'url'>; ([TData] extends [never] ? unknown : Omit<TData, 'url'>);
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'body' | 'headers' | 'url'
> &
TData
: OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'body' | 'url'
> &
TData &
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'>
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'headers' | 'url'
> &
TData &
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'>
: OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> &
TData;

View File

@@ -22,6 +22,17 @@ export type Field =
*/ */
key?: string; key?: string;
map?: string; map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
}; };
export interface Fields { export interface Fields {
@@ -41,10 +52,14 @@ const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map< type KeyMap = Map<
string, string,
{ | {
in: Slot; in: Slot;
map?: string; map?: string;
} }
| {
in?: never;
map: Slot;
}
>; >;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
@@ -60,6 +75,10 @@ const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
map: config.map, map: config.map,
}); });
} }
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) { } else if (config.args) {
buildKeyMap(config.args, map); buildKeyMap(config.args, map);
} }
@@ -111,7 +130,9 @@ export const buildClientParams = (
if (config.key) { if (config.key) {
const field = map.get(config.key)!; const field = map.get(config.key)!;
const name = field.map || config.key; const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg; (params[field.in] as Record<string, unknown>)[name] = arg;
}
} else { } else {
params.body = arg; params.body = arg;
} }
@@ -120,8 +141,12 @@ export const buildClientParams = (
const field = map.get(key); const field = map.get(key);
if (field) { if (field) {
if (field.in) {
const name = field.map || key; const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value; (params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else { } else {
const extra = extraPrefixes.find(([prefix]) => const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix), key.startsWith(prefix),
@@ -132,10 +157,8 @@ export const buildClientParams = (
(params[slot] as Record<string, unknown>)[ (params[slot] as Record<string, unknown>)[
key.slice(prefix.length) key.slice(prefix.length)
] = value; ] = value;
} else { } else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries( for (const [slot, allowed] of Object.entries(config.allowExtra)) {
config.allowExtra ?? {},
)) {
if (allowed) { if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value; (params[slot as Slot] as Record<string, unknown>)[key] = value;
break; break;

View File

@@ -1,415 +0,0 @@
/**
* 多因子综合评价组件
* 提供地块适宜性评价、权重配置、批量分析和作物推荐功能
*/
'use client';
import { useState, useEffect } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
import { Progress } from '@/components/ui/progress';
import { Slider } from '@/components/ui/slider';
import { Textarea } from '@/components/ui/textarea';
import {
Leaf,
TrendingUp,
Award,
AlertTriangle,
CheckCircle2,
Play,
Settings,
Download,
Eye,
Calculator,
Database,
RefreshCw,
Zap,
Target,
Droplet,
Cloud,
Sun,
ThermometerSun,
BookOpen,
Beaker,
Info,
BarChart3,
Filter
} from 'lucide-react';
import { toast } from 'sonner';
import {
EvaluationFactor,
SuitabilityResult,
FactorWeight,
BatchProgress,
getGradeColor,
getScoreColor,
getSuitabilityLevelColor,
formatDate,
MOCK_FIELDS
} from './multiFactorTypes';
import {
MultiFactorService
} from './multiFactorService';
import {
matchCropsForField,
cropKnowledgeBase
} from './cropKnowledgeBase';
export function MultiFactorEvaluation() {
const [selectedField, setSelectedField] = useState('field-1');
const [showWeightConfig, setShowWeightConfig] = useState(false);
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
const [batchProgress, setBatchProgress] = useState<BatchProgress>({
total: 0,
processed: 0,
highSuitability: 0,
mediumSuitability: 0,
lowSuitability: 0,
currentField: '',
});
const [isBatchRunning, setIsBatchRunning] = useState(false);
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
// 评价因子权重配置
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
{ id: 'ph', name: 'pH值', weight: 20, unit: '' },
{ id: 'organic', name: '有机质含量', weight: 25, unit: 'g/kg' },
{ id: 'depth', name: '土层厚度', weight: 20, unit: 'cm' },
{ id: 'nitrogen', name: '全氮', weight: 10, unit: 'g/kg' },
{ id: 'phosphorus', name: '全磷', weight: 10, unit: 'g/kg' },
{ id: 'potassium', name: '全钾', weight: 10, unit: 'g/kg' },
{ id: 'drainage', name: '排水性', weight: 5, unit: '' },
]);
// 模拟适宜性评价结果
const [evaluationResults, setEvaluationResults] = useState<SuitabilityResult[]>(
MultiFactorService.generateMockEvaluationData()
);
// 获取当前选中的地块结果
const currentResult =
evaluationResults.find(r => r.fieldId === selectedField) ||
batchAnalysisResults.find(r => r.fieldId === selectedField) ||
evaluationResults[0];
// 批量分析处理函数
const handleRunBatchAnalysis = async () => {
const validation = MultiFactorService.validateWeights(factorWeights);
if (!validation.isValid) {
toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%`);
return;
}
setIsBatchRunning(true);
setBatchProgress({
total: 68,
processed: 0,
highSuitability: 0,
mediumSuitability: 0,
lowSuitability: 0,
currentField: '',
});
setBatchAnalysisResults([]);
toast.success('开始批量分析,正在读取地块数据...');
try {
const results = await MultiFactorService.runBatchAnalysis(
factorWeights,
(progress) => {
setBatchProgress(progress);
}
);
setBatchAnalysisResults(results);
setIsBatchRunning(false);
toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`);
} catch (error) {
setIsBatchRunning(false);
toast.error('批量分析失败');
}
};
const handleUpdateWeight = (id: string, newWeight: number) => {
setFactorWeights(prev =>
MultiFactorService.updateWeight(prev, id, newWeight)
);
};
const handleResetWeights = () => {
setFactorWeights(MultiFactorService.resetWeights());
toast.success('权重已恢复默认值');
};
const handleApplyPreset = (preset: 'grain' | 'economic' | 'default') => {
setFactorWeights(MultiFactorService.getWeightPreset(preset));
const presetName = preset === 'grain' ? '粮食作物' : preset === 'economic' ? '经济作物' : '默认';
toast.success(`已应用${presetName}权重方案`);
};
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
// 执行地块适宜性评价
const handleEvaluateField = () => {
const validation = MultiFactorService.validateWeights(factorWeights);
if (!validation.isValid) {
toast.error(`权重总和必须为100%才能进行评价(当前:${validation.totalWeight}%`);
return;
}
try {
const result = MultiFactorService.generateEvaluationResult(selectedField, factorWeights);
setEvaluationResults(prev =>
prev.map(r => r.fieldId === selectedField ? result : r)
);
toast.success('评价完成!已应用当前权重配置计算综合得分');
} catch (error) {
toast.error('评价失败');
}
};
const exportResults = () => {
const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults;
toast.success('正在导出评价结果...');
// 模拟导出功能
setTimeout(() => {
toast.success(`已导出${resultsToExport.length}个地块的评价结果`);
}, 2000);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800 dark:text-green-200"></h2>
<p className="text-muted-foreground">
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setShowKnowledgeBase(true)}>
<BookOpen className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => setShowWeightConfig(true)}>
<Settings className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
{/* 多因子综合评价 */}
<div className="space-y-4">
<Card className="p-4 bg-card">
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="text-xs text-muted-foreground mb-2 block"></label>
<Select value={selectedField} onValueChange={setSelectedField}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{evaluationResults.map((result) => (
<SelectItem key={result.fieldId} value={result.fieldId}>
{result.fieldName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2 items-end">
<Button
className="bg-green-600 hover:bg-green-700"
onClick={handleEvaluateField}
>
<Play className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</Card>
{/* 评价结果总览 */}
<div className="grid grid-cols-4 gap-4">
<Card className="p-6 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-950 dark:to-green-900">
<div className="text-center">
<Award className="w-12 h-12 text-green-600 dark:text-green-400 mx-auto mb-3" />
<p className="text-xs text-muted-foreground mb-2"></p>
<p
className="text-4xl mb-2"
style={{ color: getGradeColor(currentResult.grade) }}
>
{currentResult.totalScore}
</p>
<Badge
className="text-white font-light"
style={{ backgroundColor: getGradeColor(currentResult.grade) }}
>
{currentResult.grade}
</Badge>
</div>
</Card>
<Card className="p-6 bg-card">
<div className="text-center">
<CheckCircle2 className="w-12 h-12 text-blue-600 dark:text-blue-400 mx-auto mb-3" />
<p className="text-xs text-muted-foreground mb-2"></p>
<p className="text-4xl text-blue-600 dark:text-blue-400 mb-2">
{currentResult.factors.filter(f => f.score >= 80).length}
</p>
<p className="text-xs text-muted-foreground">
/ {currentResult.factors.length}
</p>
</div>
</Card>
<Card className="p-6 bg-card">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-yellow-600 dark:text-yellow-400 mx-auto mb-3" />
<p className="text-xs text-muted-foreground mb-2"></p>
<p className="text-4xl text-yellow-600 dark:text-yellow-400 mb-2">
{currentResult.factors.filter(f => f.score < 70).length}
</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</Card>
<Card className="p-6 bg-card">
<div className="text-center">
<TrendingUp className="w-12 h-12 text-purple-600 dark:text-purple-400 mx-auto mb-3" />
<p className="text-xs text-muted-foreground mb-2"></p>
<p className="text-sm text-purple-600 dark:text-purple-400 mb-2">
{formatDate(currentResult.timestamp)}
</p>
<p className="text-xs text-muted-foreground"></p>
</div>
</Card>
</div>
{/* 因子详细评分 */}
<Card className="p-6 bg-card">
<h3 className="mb-4"></h3>
<div className="space-y-4">
{currentResult.factors.map((factor) => (
<div key={factor.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-medium">{factor.name}</span>
<Badge variant="outline" className="text-xs font-light">
: {factor.weight}%
</Badge>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-muted-foreground">
: {factor.value.toFixed(1)}{factor.unit}
</span>
<span className="text-sm text-muted-foreground">
: {factor.optimalRange[0]}-{factor.optimalRange[1]}{factor.unit}
</span>
<span
className="text-sm font-medium"
style={{ color: getScoreColor(factor.score) }}
>
{factor.score}
</span>
</div>
</div>
<div className="relative">
<Progress value={factor.score} className="h-2" />
<div className="absolute top-0 left-0 h-2 w-full flex items-center">
<div
className="absolute h-3 w-1 bg-blue-500"
style={{
left: `${((factor.optimalRange[0] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
}}
/>
<div
className="absolute h-3 w-1 bg-blue-500"
style={{
left: `${((factor.optimalRange[1] - 0) / (factor.optimalRange[1] * 1.5)) * 100}%`
}}
/>
</div>
</div>
</div>
))}
</div>
</Card>
{/* 加权计算说明 */}
<Card className="p-6 bg-card">
<h3 className="mb-4">(AHP)</h3>
<div className="space-y-4">
<div className="p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
<p className="text-sm text-blue-900 dark:text-blue-100 mb-2"></p>
<code className="text-xs text-blue-800 dark:text-blue-200 block mb-2">
= Σ( × )
</code>
<p className="text-xs text-blue-800 dark:text-blue-200">
= ({currentResult.factors.map((f, i) =>
`${f.score} × ${f.weight}%${i < currentResult.factors.length - 1 ? ' + ' : ''}`
).join('')})
</p>
<p className="text-xs text-blue-800 dark:text-blue-200 mt-2">
= {currentResult.totalScore}
</p>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-green-50 dark:bg-green-950 rounded-lg">
<h4 className="text-green-900 dark:text-green-100 mb-2"> (80)</h4>
<ul className="text-sm text-green-800 dark:text-green-200 space-y-1">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
<h4 className="text-yellow-900 dark:text-yellow-100 mb-2"> (60-79)</h4>
<ul className="text-sm text-yellow-800 dark:text-yellow-200 space-y-1">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="p-4 bg-red-50 dark:bg-red-950 rounded-lg">
<h4 className="text-red-900 dark:text-red-100 mb-2"> (&lt;60)</h4>
<ul className="text-sm text-red-800 dark:text-red-200 space-y-1">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
</Card>
{/* 多因子综合评价功能说明 */}
<Card className="p-4 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2">
<Zap className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800 dark:text-blue-200">
<p className="mb-2"></p>
<ul className="space-y-1 text-xs">
<li> <strong></strong>: pH值7</li>
<li> <strong>(AHP)</strong>: = Σ( × )</li>
<li> <strong></strong>: 0-10085-100</li>
<li> <strong></strong>: </li>
<li> <strong></strong>: 0-100</li>
<li> <strong></strong>: 8060-79&lt;60</li>
<li> <strong></strong>: </li>
<li> <strong></strong>: </li>
</ul>
</div>
</div>
</Card>
<Card className="p-4 bg-card">
<div className="flex items-center gap-4">

1
test
View File

@@ -1 +0,0 @@
test

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,51 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'
import tailwindcss from "@tailwindcss/vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 3000,
open: true,
hmr: {
overlay: true
}
},
build: {
target: 'esnext',
outDir: 'build',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
radix: ['@radix-ui'],
charts: ['recharts'],
utils: ['date-fns', 'clsx', 'tailwind-merge'],
hooks: ['react-hook-form'],
icons: ['lucide-react']
}
}
},
chunkSizeWarningLimit: 1000
},
optimizeDeps: {
include: [
'react',
'react-dom',
'@radix-ui/react-slot',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'lucide-react',
'date-fns',
'clsx',
'tailwind-merge'
]
}
})