完善保险前后端、养殖端小程序

This commit is contained in:
xuqiuyun
2025-09-25 19:09:51 +08:00
parent 76b5393182
commit 852adbcfff
199 changed files with 8642 additions and 52333 deletions

View File

@@ -1,241 +0,0 @@
# 智能设备API接口集成文档
## 概述
本文档描述了智慧养殖小程序中智能设备管理系统的API接口集成情况。
## 基础配置
- **API基础地址**: `http://localhost:5350`
- **认证方式**: Bearer Token (存储在localStorage中)
- **请求超时**: 10秒
- **内容类型**: application/json
## 智能设备接口
### 1. 智能项圈管理 (`/api/smart-devices/collars`)
#### 获取项圈设备列表
- **接口**: `GET /api/smart-devices/collars`
- **参数**:
- `page` (可选): 页码
- `limit` (可选): 每页数量
- `status` (可选): 设备状态筛选
- **返回**: 项圈设备列表数据
#### 绑定项圈设备
- **接口**: `POST /api/smart-devices/collars/bind`
- **参数**:
```json
{
"collarId": "string",
"animalId": "string"
}
```
#### 解绑项圈设备
- **接口**: `POST /api/smart-devices/collars/unbind`
- **参数**:
```json
{
"collarId": "string"
}
```
### 2. 智能耳标管理 (`/api/smart-devices/eartags`)
#### 获取耳标设备列表
- **接口**: `GET /api/smart-devices/eartags`
- **参数**:
- `page` (可选): 页码
- `limit` (可选): 每页数量
- `status` (可选): 设备状态筛选
- **返回**: 耳标设备列表数据
#### 绑定耳标设备
- **接口**: `POST /api/smart-devices/eartags/bind`
- **参数**:
```json
{
"earTagId": "string",
"animalId": "string"
}
```
#### 解绑耳标设备
- **接口**: `POST /api/smart-devices/eartags/unbind`
- **参数**:
```json
{
"earTagId": "string"
}
```
#### 获取耳标设备详情
- **接口**: `GET /api/smart-devices/eartags/{earTagId}`
- **返回**: 耳标设备详细信息
#### 更新耳标设备信息
- **接口**: `PUT /api/smart-devices/eartags/{earTagId}`
- **参数**: 设备更新数据
#### 删除耳标设备
- **接口**: `DELETE /api/smart-devices/eartags/{earTagId}`
### 3. 智能脚环管理 (`/api/smart-devices/anklets`)
#### 获取脚环设备列表
- **接口**: `GET /api/smart-devices/anklets`
- **参数**:
- `page` (可选): 页码
- `limit` (可选): 每页数量
- `status` (可选): 设备状态筛选
- **返回**: 脚环设备列表数据
#### 绑定脚环设备
- **接口**: `POST /api/smart-devices/anklets/bind`
- **参数**:
```json
{
"ankleId": "string",
"animalId": "string"
}
```
#### 解绑脚环设备
- **接口**: `POST /api/smart-devices/anklets/unbind`
- **参数**:
```json
{
"ankleId": "string"
}
```
### 4. 智能主机管理 (`/api/smart-devices/hosts`)
#### 获取主机设备列表
- **接口**: `GET /api/smart-devices/hosts`
- **参数**:
- `page` (可选): 页码
- `limit` (可选): 每页数量
- `status` (可选): 设备状态筛选
- **返回**: 主机设备列表数据
#### 重启主机设备
- **接口**: `POST /api/smart-devices/hosts/restart`
- **参数**:
```json
{
"hostId": "string"
}
```
#### 启动主机设备
- **接口**: `POST /api/smart-devices/hosts/start`
- **参数**:
```json
{
"hostId": "string"
}
```
#### 停止主机设备
- **接口**: `POST /api/smart-devices/hosts/stop`
- **参数**:
```json
{
"hostId": "string"
}
```
### 5. 设备搜索和状态监控 (`/api/smart-devices`)
#### 设备搜索
- **接口**: `GET /api/smart-devices/search`
- **参数**:
- `keyword` (可选): 搜索关键词
- `deviceType` (可选): 设备类型
- `status` (可选): 设备状态
- **返回**: 搜索结果
#### 获取设备状态监控
- **接口**: `GET /api/smart-devices/status`
- **参数**:
- `deviceIds` (可选): 设备ID数组
- `deviceType` (可选): 设备类型
- **返回**: 设备状态数据
#### 获取设备统计信息
- **接口**: `GET /api/smart-devices/statistics`
- **返回**: 设备统计汇总数据
#### 批量更新设备状态
- **接口**: `POST /api/smart-devices/batch-update`
- **参数**:
```json
{
"deviceIds": ["string"],
"statusData": {}
}
```
#### 获取设备实时数据
- **接口**: `GET /api/smart-devices/{deviceType}/{deviceId}/realtime`
- **返回**: 设备实时监控数据
#### 获取设备历史数据
- **接口**: `GET /api/smart-devices/{deviceType}/{deviceId}/history`
- **参数**:
- `startTime` (可选): 开始时间
- `endTime` (可选): 结束时间
- **返回**: 设备历史数据
## 错误处理
所有API调用都包含错误处理机制
- 网络错误时显示控制台错误信息
- API错误时抛出异常供组件处理
- 提供模拟数据作为降级方案
## 使用示例
### 在Vue组件中使用API服务
```javascript
import { getCollarDevices, bindCollar } from '@/services/collarService'
export default {
async mounted() {
try {
const response = await getCollarDevices()
this.devices = response.data
} catch (error) {
console.error('加载设备失败:', error)
// 使用模拟数据
this.devices = this.getMockData()
}
},
async handleBind(device) {
try {
await bindCollar(device.id, 'animal_123')
device.isBound = true
} catch (error) {
console.error('绑定失败:', error)
}
}
}
```
## 注意事项
1. 所有API调用都需要有效的认证token
2. 请求失败时会自动使用模拟数据
3. 设备绑定操作需要提供动物ID
4. 主机操作(启动/重启/停止)需要确认设备状态
5. 搜索和筛选功能支持多参数组合查询
## 更新日志
- **2025-09-18**: 初始版本集成所有智能设备API接口
- 统一API路径为 `/api/smart-devices/*`
- 添加完整的错误处理和模拟数据支持
- 实现设备绑定/解绑、状态监控等功能

View File

@@ -1,119 +0,0 @@
# 智能耳标API集成完成报告
## ✅ 集成状态:已完成
### API接口信息
- **接口地址**: `http://localhost:5350/api/smart-devices/public/eartags`
- **请求方法**: GET
- **参数**: `page=1&limit=10&refresh=true`
- **认证**: 无需认证使用公开API
### 配置更新
#### 1. API配置文件 (`utils/api.js`)
```javascript
const config = {
baseUrl: 'http://localhost:5350/api', // 智能耳标API地址
timeout: 10000,
header: {
'Content-Type': 'application/json'
}
}
```
#### 2. 智能耳标页面 (`pages/device/eartag/eartag.js`)
```javascript
// 使用真实的智能耳标API接口公开API无需认证
const response = await get('/smart-devices/public/eartags?page=1&limit=10&refresh=true')
```
### API响应数据格式
```json
{
"success": true,
"message": "数据获取成功",
"data": {
"list": [
{
"id": 99833,
"sn": "DEV099833",
"rsrp": "-",
"bandge_status": 1,
"deviceInfo": "0",
"temperature": "39.00",
"status": "在线",
"steps": 0,
"location": "无定位",
"updateInte": "..."
}
],
"pagination": {
"page": 1,
"limit": 10,
"total": 100
}
}
}
```
### 字段映射
智能耳标页面会自动将API数据映射为前端显示格式
- `sn``eartagNumber` (耳标编号)
- `temperature``temperature` (体温)
- `status``isBound` (绑定状态)
- `steps``totalMovement` (运动量)
- `location``location` (位置)
### 测试结果
- ✅ API连接成功 (状态码: 200)
- ✅ 数据返回正常
- ✅ 字段映射正确
- ✅ 无需认证即可访问
## 🚀 使用说明
### 1. 启动后端服务
```bash
cd backend
npm start
```
后端将在 `http://localhost:5350` 运行
### 2. 微信开发者工具配置
- 打开微信开发者工具
- 点击右上角"详情"按钮
- 在"本地设置"中勾选 **"不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书"**
### 3. 测试智能耳标功能
1. 点击首页"智能设备" → "智能耳标"
2. 页面将自动调用真实API获取数据
3. 数据会实时显示在列表中
## 📋 功能特性
- **动态数据获取**: 实时从后端API获取智能耳标数据
- **字段自动映射**: 自动将API字段映射为中文显示
- **分页支持**: 支持分页加载更多数据
- **搜索功能**: 支持按耳标编号搜索
- **状态筛选**: 支持按绑定状态筛选
- **下拉刷新**: 支持下拉刷新获取最新数据
## 🔧 技术实现
- **API工具**: 使用 `utils/api.js` 统一处理API请求
- **错误处理**: 完善的错误处理和用户提示
- **数据缓存**: 支持数据缓存和实时更新
- **响应式设计**: 适配不同屏幕尺寸
## 📝 注意事项
1. **域名白名单**: 生产环境需要在微信公众平台配置域名白名单
2. **HTTPS要求**: 生产环境必须使用HTTPS协议
3. **数据安全**: 当前使用公开API生产环境建议使用认证API
4. **性能优化**: 大数据量时建议实现虚拟滚动
---
**集成完成时间**: 2025年9月23日
**API状态**: 正常运行
**测试状态**: 通过

View File

@@ -1,122 +0,0 @@
# 智能耳标API集成指南
## 📡 API接口信息
**接口地址**: `/api/iot-jbq-client`
**请求方法**: GET
**数据格式**: JSON
## 🔄 字段映射和数据处理
### 输入字段映射
智能耳标页面会自动处理以下API字段的映射
| 页面显示字段 | API可能字段名 | 处理函数 | 说明 |
|-------------|-------------|----------|------|
| 耳标编号 | `eartagNumber`, `eartag_number`, `id` | 直接映射 | 唯一标识符 |
| 绑定状态 | `isBound`, `is_bound`, `bound`, `status` | `checkIfBound()` | 判断是否已绑定 |
| 设备电量 | `batteryLevel`, `battery_level`, `battery`, `power` | `formatBatteryLevel()` | 格式化电量百分比 |
| 设备温度 | `temperature`, `temp`, `device_temp` | `formatTemperature()` | 格式化温度值 |
| 被采集主机 | `hostNumber`, `host_number`, `hostId`, `host_id`, `collector` | `formatHostNumber()` | 主机标识 |
| 总运动量 | `totalMovement`, `total_movement`, `movement_total` | `formatMovement()` | 运动量数值 |
| 今日运动量 | `todayMovement`, `today_movement`, `movement_today` | `formatMovement()` | 今日运动量 |
| 更新时间 | `updateTime`, `update_time`, `last_update` | `formatUpdateTime()` | 格式化时间显示 |
### 数据处理函数
#### 1. `checkIfBound(item)` - 绑定状态判断
```javascript
// 优先级判断逻辑:
// 1. 明确的绑定状态字段
// 2. 状态字符串匹配
// 3. 根据是否有牛只ID判断
```
#### 2. `formatBatteryLevel(item)` - 电量格式化
```javascript
// 提取电量值并四舍五入为整数
// 默认值0
```
#### 3. `formatTemperature(item)` - 温度格式化
```javascript
// 保留一位小数
// 默认值0.0
```
#### 4. `formatUpdateTime(timeStr)` - 时间格式化
```javascript
// 转换为中文格式YYYY-MM-DD HH:mm:ss
// 处理各种时间格式
```
## 🎯 功能特性
### 1. 动态数据加载
- ✅ 自动调用API接口获取数据
- ✅ 实时更新筛选标签计数
- ✅ 支持下拉刷新
### 2. 筛选功能
-**耳标总数**: 显示所有耳标数量
-**已绑定数量**: 显示已绑定耳标数量
-**未绑定数量**: 显示未绑定耳标数量
### 3. 搜索功能
- ✅ 支持按耳标编号搜索
- ✅ 支持按主机号搜索
- ✅ 实时搜索过滤
### 4. 交互功能
- ✅ 点击耳标项查看详情
- ✅ 添加新耳标功能
- ✅ 绑定状态显示
## 🎨 UI设计特点
### 严格按照图片设计实现:
1. **顶部绿色区域**: 搜索框 + 添加按钮
2. **筛选标签**: 三个标签页,蓝色下划线选中效果
3. **耳标列表**: 卡片式布局,包含所有字段信息
4. **绑定状态**: 蓝色"未绑定"按钮,绿色"已绑定"按钮
5. **响应式设计**: 适配不同屏幕尺寸
## 🔧 技术实现
### 文件结构
```
pages/device/eartag/
├── eartag.wxml # 页面结构
├── eartag.wxss # 页面样式
└── eartag.js # 页面逻辑
```
### 核心功能
- **API集成**: 使用`get('/api/iot-jbq-client')`获取数据
- **数据处理**: 自动字段映射和格式化
- **状态管理**: 筛选、搜索、加载状态
- **错误处理**: API调用失败时的用户提示
## 📱 使用说明
1. **页面访问**: 通过首页"智能设备"模块进入
2. **数据刷新**: 下拉页面刷新数据
3. **筛选查看**: 点击顶部标签切换不同视图
4. **搜索功能**: 在搜索框输入关键词
5. **添加耳标**: 点击右上角"+"按钮
## ⚠️ 注意事项
1. **API兼容性**: 支持多种字段名格式,自动适配
2. **数据验证**: 对无效数据进行默认值处理
3. **性能优化**: 使用数据缓存,避免重复请求
4. **错误处理**: 网络异常时显示友好提示
## 🚀 扩展功能
未来可以扩展的功能:
- 耳标详情页面
- 批量操作功能
- 数据导出功能
- 实时数据更新
- 历史数据查看

View File

@@ -1,161 +0,0 @@
# API接口集成更新说明
## 更新概述
根据您提供的栏舍API接口 `http://localhost:5300/api/cattle-pens?page=1&pageSize=10`我已经更新了牛只转栏记录功能确保使用正确的API接口获取栏舍数据。
## 主要更新
### 1. API服务更新
**更新了栏舍API接口**
```javascript
// 更新前
getBarnsForTransfer: (farmId) => {
return get('/barns', { farmId })
}
// 更新后
getBarnsForTransfer: (params = {}) => {
return get('/cattle-pens', params)
}
```
**支持分页参数:**
- `page`: 页码默认1
- `pageSize`: 每页数量默认10转栏功能中设置为100以获取更多数据
### 2. 数据格式适配
**根据API文档栏舍API返回格式为**
```json
{
"success": true,
"data": {
"list": [
{
"id": 1,
"name": "栏舍名称",
"code": "栏舍编号",
"type": "育成栏",
"capacity": 50,
"currentCount": 10,
"area": 100.50,
"location": "位置描述",
"status": "启用",
"remark": "备注",
"farmId": 1
}
],
"total": 100,
"page": 1,
"pageSize": 10
},
"message": "获取栏舍列表成功"
}
```
**更新了数据处理逻辑:**
```javascript
// 支持多种数据格式
if (response && Array.isArray(response)) {
this.barns = response
} else if (response && response.data && response.data.list && Array.isArray(response.data.list)) {
this.barns = response.data.list // 主要格式
} else if (response && response.data && Array.isArray(response.data)) {
this.barns = response.data
} else if (response && response.records && Array.isArray(response.records)) {
this.barns = response.records
}
```
### 3. 新增API测试功能
**创建了API测试页面**
- 路径:`/api-test-page`
- 功能测试栏舍API、转栏记录API、可用牛只API
- 显示API响应数据的JSON格式
- 调试方便开发时查看API返回的数据结构
**在首页添加了测试入口:**
- 开发环境下显示"API测试"按钮
- 点击可跳转到API测试页面
## 栏舍数据字段映射
根据API文档栏舍数据包含以下字段
| 字段名 | 类型 | 说明 | 前端使用 |
|--------|------|------|----------|
| id | Integer | 栏舍ID | 作为选择值 |
| name | String | 栏舍名称 | 显示名称 |
| code | String | 栏舍编号 | 辅助显示 |
| type | Enum | 栏舍类型 | 分类显示 |
| capacity | Integer | 栏舍容量 | 容量信息 |
| currentCount | Integer | 当前牛只数量 | 状态信息 |
| area | Decimal | 面积(平方米) | 详细信息 |
| location | Text | 位置描述 | 详细信息 |
| status | Enum | 状态(启用/停用) | 过滤条件 |
| remark | Text | 备注 | 详细信息 |
| farmId | Integer | 所属农场ID | 关联信息 |
## 转栏功能中的栏舍选择
**在转栏登记页面:**
- 转出栏舍和转入栏舍都从 `/api/cattle-pens` 接口获取
- 显示格式:`栏舍名称 - 栏舍编号`
- 支持分页加载pageSize=100
- 自动处理API返回的数据格式
**在转栏记录显示:**
- 显示栏舍的 `name` 字段
- 通过关联的 `fromPen``toPen` 对象获取栏舍信息
## 测试方法
### 1. 通过API测试页面
1. 在开发环境下访问首页
2. 点击"API测试"按钮
3. 在测试页面点击"测试栏舍API"按钮
4. 查看返回的JSON数据格式
### 2. 通过转栏功能
1. 访问转栏记录页面
2. 点击"转栏登记"按钮
3. 查看栏舍下拉选择框是否正常加载数据
### 3. 通过浏览器开发者工具
1. 打开浏览器开发者工具
2. 查看Network标签页
3. 访问转栏功能时观察API请求
4. 检查请求URL和响应数据
## 错误处理
**API调用失败时的处理**
- 显示错误提示信息
- 在控制台输出详细错误信息
- 栏舍列表为空时不影响其他功能
**数据格式异常时的处理:**
- 在控制台输出警告信息
- 尝试多种数据格式解析
- 最终解析失败时使用空数组
## 注意事项
1. **API地址** - 确保后端API `http://localhost:5300/api/cattle-pens` 正常运行
2. **认证要求** - 需要有效的认证token
3. **数据格式** - 确保API返回的数据格式符合文档规范
4. **分页参数** - 转栏功能中设置pageSize=100以获取更多栏舍数据
5. **错误处理** - 网络错误和业务错误都有相应的处理机制
## 后续优化建议
1. **缓存机制** - 栏舍数据相对稳定,可以考虑缓存
2. **搜索功能** - 栏舍数量多时可以添加搜索功能
3. **分类筛选** - 根据栏舍类型进行筛选
4. **状态筛选** - 只显示启用状态的栏舍
5. **懒加载** - 栏舍数量很大时可以考虑懒加载
现在牛只转栏记录功能已经完全集成了正确的栏舍API接口可以动态获取真实的栏舍数据

View File

@@ -1,154 +0,0 @@
# API端口更新说明
## 更新概述
根据您提供的牛只转栏记录API接口 `http://localhost:5300/api/cattle-transfer-records?page=1&pageSize=10&search=`我已经更新了API配置将端口从5350更改为5300。
## 主要更新
### 1. API基础URL更新
**更新前:**
```javascript
baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:5350/api'
```
**更新后:**
```javascript
baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:5300/api'
```
### 2. 影响的API接口
所有API接口现在都使用新的端口5300
- **转栏记录相关:**
- `GET /api/cattle-transfer-records` - 获取转栏记录列表
- `POST /api/cattle-transfer-records` - 创建转栏记录
- `GET /api/cattle-transfer-records/{id}` - 获取转栏记录详情
- `PUT /api/cattle-transfer-records/{id}` - 更新转栏记录
- `DELETE /api/cattle-transfer-records/{id}` - 删除转栏记录
- `POST /api/cattle-transfer-records/batch-delete` - 批量删除
- `GET /api/cattle-transfer-records/available-animals` - 获取可用牛只
- **栏舍相关:**
- `GET /api/cattle-pens` - 获取栏舍列表
- **其他API**
- 所有其他API接口也会使用新的端口
### 3. API测试功能增强
**新增搜索功能测试:**
- 在API测试页面添加了搜索输入框
- 可以测试带搜索参数的转栏记录API
- 支持测试 `search` 参数功能
**测试页面功能:**
- 基础API测试无搜索参数
- 搜索功能测试(带搜索参数)
- 实时显示API响应数据
- 错误处理和显示
## 转栏记录API参数
根据您提供的接口转栏记录API支持以下参数
### 查询参数
- `page`: 页码默认1
- `pageSize`: 每页数量默认10
- `search`: 搜索关键词(可选)
### 示例请求
```
GET http://localhost:5300/api/cattle-transfer-records?page=1&pageSize=10&search=
```
### 搜索功能
- 支持按耳号搜索转栏记录
- 支持按其他字段搜索(具体取决于后端实现)
- 搜索参数为空时返回所有记录
## 测试方法
### 1. 通过API测试页面
1. 访问首页,点击"API测试"按钮
2. 在转栏记录API测试区域
- 点击"测试转栏记录API"测试基础功能
- 输入搜索关键词,点击"测试搜索功能"测试搜索
3. 查看返回的JSON数据格式
### 2. 通过转栏功能
1. 访问转栏记录页面
2. 在搜索框中输入耳号进行搜索
3. 观察API请求和响应
### 3. 通过浏览器开发者工具
1. 打开开发者工具的Network标签页
2. 访问转栏功能
3. 查看API请求的URL和参数
4. 检查响应数据格式
## 数据格式预期
根据API接口转栏记录数据应该包含以下字段
```json
{
"success": true,
"data": {
"list": [
{
"id": 1,
"recordId": "TR20250101001",
"animalId": 123,
"earNumber": "123456",
"fromPenId": 1,
"toPenId": 2,
"transferDate": "2025-01-01T10:00:00Z",
"reason": "正常调栏",
"operator": "张三",
"status": "已完成",
"remark": "备注信息",
"farmId": 1,
"created_at": "2025-01-01T10:00:00Z",
"updated_at": "2025-01-01T10:00:00Z",
"fromPen": {
"id": 1,
"name": "转出栏舍",
"code": "PEN001"
},
"toPen": {
"id": 2,
"name": "转入栏舍",
"code": "PEN002"
}
}
],
"total": 100,
"page": 1,
"pageSize": 10
},
"message": "获取转栏记录列表成功"
}
```
## 注意事项
1. **端口一致性** - 确保后端API服务运行在5300端口
2. **认证要求** - 所有API都需要有效的认证token
3. **搜索功能** - 搜索参数为空时应该返回所有记录
4. **分页功能** - 支持分页查询默认每页10条记录
5. **错误处理** - API调用失败时有相应的错误处理
## 环境变量配置
如果需要通过环境变量配置API地址可以在项目根目录创建 `.env` 文件:
```env
VUE_APP_BASE_URL=http://localhost:5300/api
```
这样可以在不同环境中使用不同的API地址而不需要修改代码。
现在所有API接口都使用正确的端口5300转栏记录功能可以正常调用后端API获取数据

View File

@@ -1,65 +0,0 @@
# API 设置说明
## 问题描述
后端API返回401未授权错误需要正确的认证信息才能获取371台主机的数据。
## 当前状态
- ✅ API服务正在运行 (http://localhost:5350)
- ✅ API端点存在 (/api/smart-devices/hosts)
- ❌ 需要认证才能访问
- ❌ 前端显示10台主机而不是371台
## 解决方案
### 方案1: 获取正确的认证token
1. 联系后端开发者获取测试用的认证token
2. 在浏览器控制台执行以下代码设置token
```javascript
localStorage.setItem('token', 'YOUR_ACTUAL_TOKEN_HERE')
```
### 方案2: 检查API是否需要特殊参数
可能API需要特定的请求参数
- 用户ID
- 项目ID
- 其他业务参数
### 方案3: 临时测试token
如果后端有测试模式,可以尝试:
```javascript
localStorage.setItem('token', 'test-token')
```
## 测试步骤
1. 运行认证测试:
```bash
node auth-test.js
```
2. 运行API测试
```bash
node test-api.js
```
3. 在浏览器中测试前端:
- 打开开发者工具
- 查看控制台日志
- 检查网络请求
## 预期结果
- API应该返回371台主机的数据
- 前端应该正确显示总数371
- 分页功能应该正常工作
## 当前代码状态
- ✅ 已移除所有模拟数据
- ✅ 只使用真实API接口
- ✅ 正确处理API响应结构
- ✅ 支持分页和搜索
- ❌ 需要解决认证问题
## 下一步
1. 获取正确的认证信息
2. 测试API连接
3. 验证前端显示371台主机

View File

@@ -1,123 +0,0 @@
# API测试指南
## 问题解决状态
**401认证错误已修复**
- 修复了代理配置,从 `http://localhost:3000` 改为 `http://localhost:5350`
- 实现了真实的JWT token认证
- 移除了模拟数据回退使用真实API数据
## 测试步骤
### 1. 访问认证测试页面
```
http://localhost:8080/auth-test
```
### 2. 测试API连接
1. 点击"设置测试Token"按钮
2. 系统会自动使用 `admin/123456` 登录获取真实JWT token
3. 点击"测试所有API"按钮
4. 查看测试结果
### 3. 预期结果
- ✅ 耳标API测试成功获取到真实数据
- ✅ 项圈API测试成功获取到真实数据
- ✅ 脚环API测试成功获取到真实数据
- ✅ 主机API测试成功获取到真实数据
## 技术实现
### 认证流程
1. 前端调用 `/api/auth/login` 获取JWT token
2. 将token存储在 `localStorage`
3. 所有API请求自动添加 `Authorization: Bearer <token>`
4. 后端验证JWT token并返回数据
### API端点
- **登录**: `POST /api/auth/login`
- **耳标设备**: `GET /api/smart-devices/eartags`
- **项圈设备**: `GET /api/smart-devices/collars`
- **脚环设备**: `GET /api/smart-devices/anklets`
- **主机设备**: `GET /api/smart-devices/hosts`
### 默认账号
- **用户名**: `admin`
- **密码**: `123456`
## 故障排除
### 如果仍然出现401错误
1. 检查后端服务是否运行在 `http://localhost:5350`
2. 检查前端代理配置是否正确
3. 清除浏览器缓存和localStorage
4. 重新设置测试token
### 如果API返回空数据
1. 检查数据库是否有测试数据
2. 检查用户权限是否正确设置
3. 查看后端日志了解详细错误信息
## 开发说明
### 环境变量
```javascript
VUE_APP_API_BASE_URL=http://localhost:5350
```
### 代理配置
```javascript
// vue.config.js
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5350',
changeOrigin: true
}
}
}
```
### 认证头格式
```javascript
headers: {
'Authorization': `Bearer ${token}`
}
```
## 下一步
1. 实现用户登录页面集成真实API
2. 添加错误处理和用户提示
3. 实现token自动刷新机制
4. 添加权限控制
5. 优化API响应处理
## 测试命令
### 手动测试API
```powershell
# 登录获取token
$response = Invoke-WebRequest -Uri "http://localhost:5350/api/auth/login" -Method POST -Body '{"username":"admin","password":"123456"}' -ContentType "application/json"
$token = ($response.Content | ConvertFrom-Json).token
# 测试耳标API
Invoke-WebRequest -Uri "http://localhost:5350/api/smart-devices/eartags" -Headers @{"Authorization"="Bearer $token"} -Method GET
```
### 检查服务状态
```powershell
# 检查后端服务
netstat -an | findstr :5350
# 检查前端服务
netstat -an | findstr :8080
```
## 成功指标
- [x] 401错误已解决
- [x] 真实API数据正常返回
- [x] JWT认证正常工作
- [x] 前端代理配置正确
- [x] 所有设备API可正常调用

View File

@@ -1,88 +0,0 @@
# 401认证错误修复说明
## 问题描述
在访问智能设备页面时出现 `401 (Unauthorized)` 错误这是因为API请求需要有效的认证token。
## 解决方案
### 1. 自动修复(推荐)
应用已经自动处理了这个问题:
- 开发环境会自动设置测试token
- API请求失败时会自动使用模拟数据
- 所有设备页面都能正常显示和操作
### 2. 手动设置Token
如果需要使用真实API可以
#### 方法一:通过认证测试页面
1. 访问首页,在开发工具部分点击"🔧 认证测试"
2. 在认证测试页面点击"设置测试Token"
3. 测试各个API接口
#### 方法二:通过浏览器控制台
```javascript
// 设置测试token
localStorage.setItem('token', 'your-actual-token-here')
// 设置用户信息
localStorage.setItem('userInfo', JSON.stringify({
id: 'user-001',
name: 'AIOTAGRO',
phone: '15586823774',
role: 'admin'
}))
// 刷新页面
location.reload()
```
### 3. 当前状态
**已修复的问题:**
- 401认证错误不再阻止页面加载
- 所有API服务都有模拟数据降级
- 开发环境自动设置测试token
- 添加了认证测试工具页面
**功能正常:**
- 智能耳标页面 (`/ear-tag`)
- 智能项圈页面 (`/smart-collar`)
- 智能脚环页面 (`/smart-ankle`)
- 智能主机页面 (`/smart-host`)
- 所有设备操作(绑定、解绑、状态管理)
### 4. 测试方法
1. 访问 http://localhost:8080/
2. 点击任意智能设备进入对应页面
3. 页面应该正常加载并显示模拟数据
4. 可以正常进行搜索、筛选、绑定等操作
### 5. 生产环境配置
在生产环境中,需要:
1. 确保后端API正常运行
2. 实现真实的用户认证流程
3. 获取有效的JWT token
4. 将token存储到localStorage中
## 技术实现
### API服务改进
- 添加了401错误拦截器
- 自动降级到模拟数据
- 统一的错误处理机制
### 认证工具
- `src/utils/auth.js` - 认证管理工具
- `src/components/AuthTest.vue` - 认证测试页面
### 模拟数据
每个API服务都包含完整的模拟数据
- 智能耳标4个设备示例
- 智能项圈4个设备示例
- 智能脚环4个设备示例
- 智能主机4个设备示例
## 注意事项
- 模拟数据仅用于开发和测试
- 生产环境需要连接真实API
- 所有设备操作在模拟模式下都是本地状态更新
- 刷新页面后状态会重置为初始值

View File

@@ -1,161 +0,0 @@
# 智能耳标认证问题解决方案
## 🎯 问题总结
**问题**: 智能耳标API返回401未授权错误无法获取数据
**原因**: 需要JWT认证token才能访问API
**解决**: 自动获取并设置认证token
## ✅ 解决方案
### 方案1: 自动获取Token (推荐)
```bash
# 运行自动登录脚本
node auto-login.js
```
### 方案2: 手动设置Token
```bash
# 运行token设置工具
node set-token.js
```
### 方案3: 浏览器控制台设置
1. 打开浏览器开发者工具 (F12)
2. 在控制台中执行:
```javascript
localStorage.setItem('token', 'YOUR_TOKEN_HERE')
```
3. 刷新页面
## 🔍 测试结果
### 智能主机API
-**主机总数**: 371台
-**API状态**: 正常
-**认证**: 成功
### 智能耳标API
-**耳标总数**: 1486台
-**API状态**: 正常
-**认证**: 成功
## 📊 API测试命令
```bash
# 测试所有API
node test-api.js
# 测试认证方法
node auth-test.js
# 自动登录获取token
node auto-login.js
```
## 🔧 技术细节
### 认证流程
1. 调用 `/api/auth/login` 接口
2. 使用用户名: `admin`, 密码: `123456`
3. 获取JWT token
4. 在请求头中添加 `Authorization: Bearer TOKEN`
### API端点
- **登录**: `POST /api/auth/login`
- **智能主机**: `GET /api/smart-devices/hosts`
- **智能耳标**: `GET /api/iot-jbq-client`
### Token格式
```javascript
// JWT Token示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJpYXQiOjE3NTgyNDkwMTMsImV4cCI6MTc1ODMzNTQxM30.Ic8WGgwN3PtshHtsM6VYoqGeb5TNWdEIl15wfMSutKA
```
## 🚀 前端集成
### 1. 自动登录功能
```javascript
// 在应用启动时自动登录
async function autoLogin() {
try {
const response = await axios.post('/api/auth/login', {
username: 'admin',
password: '123456'
})
if (response.data.success) {
localStorage.setItem('token', response.data.token)
return response.data.token
}
} catch (error) {
console.error('自动登录失败:', error)
}
}
```
### 2. 请求拦截器
```javascript
// 自动添加认证头
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
```
### 3. 响应拦截器
```javascript
// 处理认证错误
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 清除过期token
localStorage.removeItem('token')
// 重新登录
autoLogin()
}
return Promise.reject(error)
}
)
```
## 📝 验证步骤
1. **运行自动登录**:
```bash
node auto-login.js
```
2. **检查输出**:
- ✅ 登录成功
- ✅ 智能主机API: 371台
- ✅ 智能耳标API: 1486台
3. **设置前端token**:
```javascript
localStorage.setItem('token', 'YOUR_TOKEN')
```
4. **刷新页面测试**
## 🎉 结果
-**认证问题已解决**
-**智能主机API正常**: 371台主机
-**智能耳标API正常**: 1486台耳标
-**前端可以正常获取数据**
-**分页功能正常工作**
## 🔄 维护说明
- Token有效期为24小时
- 过期后需要重新获取
- 建议实现自动token刷新机制
- 生产环境应使用更安全的认证方式

View File

@@ -1,177 +0,0 @@
# 智能耳标认证问题解决成功报告
## 🎉 问题解决状态
**✅ 认证问题已完全解决!**
## 📊 测试结果
### 智能主机API
-**状态**: 正常
-**主机总数**: 371台
-**分页功能**: 正常
-**搜索功能**: 正常
-**认证**: 成功
### 智能耳标API
-**状态**: 正常
-**耳标总数**: 1486台
-**分页功能**: 正常
-**认证**: 成功
## 🔧 解决方案
### 1. 自动认证系统
- 实现了自动登录获取JWT token
- 自动在API请求中添加认证头
- 支持token过期自动刷新
### 2. 创建的工具
- `auto-login.js` - 自动登录脚本
- `set-token.js` - Token设置工具
- `test-api.js` - API测试脚本
- `auth-test.js` - 认证方法测试
### 3. 认证流程
```
1. 调用 /api/auth/login
2. 用户名: admin, 密码: 123456
3. 获取JWT token
4. 在请求头添加 Authorization: Bearer TOKEN
5. 成功访问所有API
```
## 📈 性能数据
### API响应时间
- 登录API: ~200ms
- 智能主机API: ~150ms
- 智能耳标API: ~180ms
### 数据量
- 智能主机: 371台设备
- 智能耳标: 1486台设备
- 分页支持: 每页10条记录
## 🚀 前端集成
### 1. 自动登录功能
```javascript
// 应用启动时自动获取token
const token = await autoLogin()
localStorage.setItem('token', token)
```
### 2. 请求拦截器
```javascript
// 自动添加认证头
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
```
### 3. 错误处理
```javascript
// 处理401认证错误
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 重新登录
autoLogin()
}
return Promise.reject(error)
}
)
```
## 📋 使用指南
### 快速开始
```bash
# 1. 自动获取token
node auto-login.js
# 2. 测试API连接
node test-api.js
# 3. 在浏览器中设置token
localStorage.setItem('token', 'YOUR_TOKEN')
```
### 前端设置
1. 打开浏览器开发者工具 (F12)
2. 在控制台执行:
```javascript
localStorage.setItem('token', 'YOUR_TOKEN')
```
3. 刷新页面
## 🔍 验证步骤
### 1. 运行测试
```bash
node test-api.js
```
### 2. 检查输出
- ✅ 认证token获取成功
- ✅ API连接成功
- ✅ 主机总数: 371
- ✅ 分页功能正常
- ✅ 搜索功能正常
### 3. 前端验证
- 打开智能主机页面
- 检查是否显示371台主机
- 测试分页功能
- 测试搜索功能
## 📝 技术细节
### JWT Token
- **算法**: HS256
- **有效期**: 24小时
- **包含信息**: 用户ID、用户名、邮箱
### API端点
- **登录**: `POST /api/auth/login`
- **智能主机**: `GET /api/smart-devices/hosts`
- **智能耳标**: `GET /api/iot-jbq-client`
### 认证头格式
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## 🎯 最终结果
- ✅ **认证问题完全解决**
- ✅ **所有API正常工作**
- ✅ **前端可以获取真实数据**
- ✅ **分页功能正常**
- ✅ **搜索功能正常**
- ✅ **智能主机显示371台**
- ✅ **智能耳标显示1486台**
## 🔄 维护建议
1. **Token管理**: 实现自动刷新机制
2. **错误处理**: 完善401错误处理
3. **安全性**: 生产环境使用更安全的认证
4. **监控**: 添加API调用监控
## 📞 支持
如有问题,请参考:
- `AUTH_SOLUTION.md` - 详细解决方案
- `API_SETUP.md` - API设置说明
- `IMPLEMENTATION_SUMMARY.md` - 实现总结
---
**🎉 认证问题解决完成现在前端可以正常访问所有API并显示真实数据。**

View File

@@ -1,195 +0,0 @@
# 牛只档案功能说明
## 功能概述
根据提供的UI设计图片实现了完整的牛只档案管理系统包括
1. **牛只档案列表页面** (`/cattle-profile`)
2. **新增牛只档案页面** (`/cattle-add`)
3. **API接口集成** (调用 `http://localhost:5350/api/cattle-type` 等接口)
## 页面功能
### 牛只档案列表页面 (`CattleProfile.vue`)
**UI特性**
- 移动端友好的设计完全按照图片UI实现
- 顶部状态栏:返回按钮、标题、操作图标
- 搜索栏:支持按耳号精确查询
- 牛只卡片列表:显示牛只详细信息
- 分页功能:支持分页浏览
- 新增档案按钮:固定在底部的绿色按钮
**数据字段映射:**
- 耳号:`earNumber` (绿色高亮显示)
- 佩戴设备:`deviceNumber`
- 出生日期:`birthday` (格式化显示)
- 品类:`cate` (中文映射:犊牛、育成母牛等)
- 品种:`varieties` (从API获取品种名称)
- 生理阶段:`level` (中文映射)
- 性别:`sex` (公/母)
- 栏舍:`penName` (从API获取栏舍名称)
**功能特性:**
- 实时搜索:输入耳号后自动搜索
- 分页展示:支持翻页浏览
- 点击查看详情:点击卡片可查看详细信息
- 响应式设计:适配移动端屏幕
### 新增牛只档案页面 (`CattleAdd.vue`)
**表单字段:**
- 基本信息:耳号、性别、品类、品种、品系
- 出生信息:出生体重、出生日期
- 管理信息:栏舍、批次、入栏时间、当前体重
**功能特性:**
- 表单验证:必填字段验证
- 下拉选择品种、栏舍、批次从API动态加载
- 数据格式化:日期转换为时间戳
- 保存功能调用API创建牛只档案
## API接口集成
### 已实现的API接口
```javascript
// 获取牛只档案列表
cattleApi.getCattleList(params)
// 根据耳号搜索牛只
cattleApi.searchCattleByEarNumber(earNumber)
// 获取牛只详情
cattleApi.getCattleDetail(id)
// 获取牛只类型列表
cattleApi.getCattleTypes()
// 获取栏舍列表
cattleApi.getPens(farmId)
// 获取批次列表
cattleApi.getBatches(farmId)
// 创建牛只档案
cattleApi.createCattle(data)
// 更新牛只档案
cattleApi.updateCattle(id, data)
// 删除牛只档案
cattleApi.deleteCattle(id)
```
### 后端API接口
- **获取牛只列表**: `GET /api/iot-cattle/public`
- **获取牛只类型**: `GET /api/cattle-type`
- **创建牛只档案**: `POST /api/iot-cattle`
- **获取栏舍列表**: `GET /api/iot-cattle/pens`
- **获取批次列表**: `GET /api/iot-cattle/batches`
## 数据映射
### 字段中文映射
```javascript
// 性别映射
const sexMap = {
1: '公',
2: '母'
}
// 品类映射
const categoryMap = {
1: '犊牛',
2: '育成母牛',
3: '架子牛',
4: '青年牛',
5: '基础母牛',
6: '育肥牛'
}
// 生理阶段映射
const stageMap = {
1: '犊牛',
2: '育成期',
3: '青年期',
4: '成年期',
5: '老年期'
}
```
## 使用方法
### 1. 访问牛只档案页面
```javascript
// 从首页点击"档案拍照"按钮
this.$router.push('/cattle-profile')
// 或直接访问URL
http://localhost:8080/cattle-profile
```
### 2. 搜索牛只
在搜索框中输入耳号,系统会自动搜索并显示匹配的牛只信息。
### 3. 新增牛只档案
点击底部的"新增档案"按钮,填写表单信息后保存。
### 4. 测试功能
访问测试页面验证功能:
```javascript
this.$router.push('/cattle-test')
```
## 技术实现
### 前端技术栈
- Vue.js 2.x
- Vue Router
- Axios (HTTP请求)
- CSS3 (移动端样式)
### 关键特性
- 响应式设计
- 移动端优化
- 实时搜索
- 分页加载
- 表单验证
- 错误处理
### 文件结构
```
src/
├── components/
│ ├── CattleProfile.vue # 牛只档案列表页面
│ ├── CattleAdd.vue # 新增牛只档案页面
│ └── CattleTest.vue # 功能测试页面
├── services/
│ └── api.js # API接口封装
└── router/
└── index.js # 路由配置
```
## 注意事项
1. **API地址**: 确保后端服务运行在 `http://localhost:5350`
2. **认证**: 部分接口可能需要认证token
3. **数据格式**: 日期需要转换为时间戳格式
4. **错误处理**: 所有API调用都包含错误处理
5. **移动端**: 页面针对移动端进行了优化
## 后续扩展
1. 牛只详情页面
2. 编辑牛只档案功能
3. 批量操作功能
4. 数据导入导出
5. 图片上传功能
6. 更多筛选条件

View File

@@ -1,147 +0,0 @@
# 牛只转栏记录功能完善说明
## 功能概述
根据提供的API接口文档完善了牛只转栏记录功能实现了所有API接口的动态调用完全移除了模拟数据。
## 实现的API接口
### 1. 基础CRUD操作
- **GET /api/cattle-transfer-records** - 获取转栏记录列表
- **POST /api/cattle-transfer-records** - 创建转栏记录
- **GET /api/cattle-transfer-records/{id}** - 获取转栏记录详情
- **PUT /api/cattle-transfer-records/{id}** - 更新转栏记录
- **DELETE /api/cattle-transfer-records/{id}** - 删除转栏记录
### 2. 批量操作
- **POST /api/cattle-transfer-records/batch-delete** - 批量删除转栏记录
### 3. 辅助功能
- **GET /api/cattle-transfer-records/available-animals** - 获取可用的牛只列表
## 新增功能特性
### 1. 批量操作功能
- **全选/取消全选** - 支持一键选择所有记录
- **批量删除** - 可以同时删除多条记录
- **选择状态显示** - 实时显示已选择的记录数量
- **视觉反馈** - 选中的记录有特殊的视觉标识
### 2. 编辑功能
- **编辑模式** - 支持编辑现有转栏记录
- **数据回填** - 编辑时自动填充现有数据
- **动态标题** - 根据模式显示"转栏登记"或"编辑转栏记录"
- **动态按钮** - 根据模式显示"提交"或"更新"
### 3. 删除功能
- **单条删除** - 支持删除单条记录
- **确认对话框** - 删除前显示确认提示
- **批量删除** - 支持批量删除多条记录
- **操作反馈** - 删除成功后显示提示信息
### 4. 数据选择优化
- **耳号选择** - 从可用牛只列表中选择,而不是手动输入
- **栏舍选择** - 从后端获取栏舍列表进行选择
- **数据验证** - 确保选择的牛只和栏舍有效
## 界面改进
### 1. 列表视图
- **卡片式布局** - 每条记录以卡片形式展示
- **选择框** - 每条记录都有选择框
- **操作按钮** - 每条记录都有编辑和删除按钮
- **状态指示** - 选中的记录有特殊样式
### 2. 批量操作栏
- **全选控制** - 顶部有全选复选框
- **选择计数** - 显示已选择的记录数量
- **批量删除按钮** - 支持批量删除操作
### 3. 表单优化
- **耳号下拉选择** - 从可用牛只列表中选择
- **动态标题** - 根据编辑/新建模式显示不同标题
- **动态按钮** - 根据模式显示不同的按钮文本
## 技术实现
### 1. API服务层
```javascript
export const cattleTransferApi = {
getTransferRecords: (params) => get('/cattle-transfer-records', params),
createTransferRecord: (data) => post('/cattle-transfer-records', data),
getTransferRecordDetail: (id) => get(`/cattle-transfer-records/${id}`),
updateTransferRecord: (id, data) => put(`/cattle-transfer-records/${id}`, data),
deleteTransferRecord: (id) => del(`/cattle-transfer-records/${id}`),
batchDeleteTransferRecords: (ids) => post('/cattle-transfer-records/batch-delete', { ids }),
getAvailableAnimals: (params) => get('/cattle-transfer-records/available-animals', params),
getBarnsForTransfer: (farmId) => get('/barns', { farmId })
}
```
### 2. 状态管理
- **selectedRecords** - 存储选中的记录ID数组
- **selectAll** - 全选状态
- **isEdit** - 编辑模式标识
- **editId** - 编辑的记录ID
### 3. 方法实现
- **toggleSelectAll()** - 全选/取消全选逻辑
- **batchDelete()** - 批量删除逻辑
- **editRecord()** - 编辑记录逻辑
- **deleteRecord()** - 删除记录逻辑
- **loadRecordForEdit()** - 加载编辑数据逻辑
## 用户体验优化
### 1. 操作反馈
- **成功提示** - 操作成功后显示成功消息
- **错误处理** - 操作失败时显示错误信息
- **加载状态** - 操作过程中显示加载状态
### 2. 确认机制
- **删除确认** - 删除前显示确认对话框
- **批量删除确认** - 批量删除前显示确认对话框
### 3. 数据验证
- **表单验证** - 提交前验证必填字段
- **业务验证** - 验证转出和转入栏舍不能相同
## 使用方式
### 1. 查看转栏记录
- 从首页"业务办理"模块点击"牛只转栏"
- 从生产管理页面"牛只管理"模块点击"转栏记录"
### 2. 新增转栏记录
- 在转栏记录页面点击"转栏登记"按钮
- 填写完整的转栏信息
- 选择牛只耳号和栏舍信息
### 3. 编辑转栏记录
- 在记录列表中点击"编辑"按钮
- 系统自动跳转到编辑页面并填充数据
- 修改后点击"更新"按钮
### 4. 删除转栏记录
- **单条删除** - 点击记录上的"删除"按钮
- **批量删除** - 选择多条记录后点击"批量删除"按钮
### 5. 批量操作
- 使用顶部的"全选"复选框选择所有记录
- 或单独选择需要的记录
- 点击"批量删除"按钮进行批量删除
## 注意事项
1. **API依赖** - 确保后端API正常运行
2. **认证要求** - 所有API都需要有效的认证token
3. **数据格式** - 确保API返回的数据格式正确
4. **错误处理** - 网络错误和业务错误都有相应的处理
## 后续优化建议
1. **搜索功能** - 添加更多搜索和筛选条件
2. **导出功能** - 支持数据导出
3. **统计功能** - 添加转栏记录统计
4. **权限控制** - 根据用户权限控制操作按钮
5. **数据缓存** - 优化数据加载性能

View File

@@ -1,137 +0,0 @@
# 牛只转栏记录功能实现说明
## 功能概述
根据提供的UI设计图片实现了完整的牛只转栏记录功能包括记录查看、搜索、分页和新增登记功能。
## 实现的功能
### 1. 牛只转栏记录查看页面 (`CattleTransfer.vue`)
**UI特性**
- 完全按照图片设计实现,包括顶部状态栏、搜索栏、记录卡片、分页控制和底部操作按钮
- 响应式设计,适配移动端显示
- 现代化的卡片式布局,符合移动应用设计规范
**功能特性:**
- 动态调用 `http://localhost:5350/api/cattle-transfer-records` 接口获取数据
- 支持按耳号搜索转栏记录
- 分页显示记录列表
- 显示详细的转栏信息,包括:
- 耳号(绿色高亮显示)
- 转舍日期
- 转入栋舍
- 转出栋舍
- 登记人
- 登记日期
- 转栏原因
- 状态
- 备注
**字段映射:**
- `earNumber` → 耳号
- `transferDate` → 转舍日期
- `toPen.name` → 转入栋舍
- `fromPen.name` → 转出栋舍
- `operator` → 登记人
- `created_at` → 登记日期
- `reason` → 转栏原因
- `status` → 状态
- `remark` → 备注
### 2. 转栏登记页面 (`CattleTransferRegister.vue`)
**功能特性:**
- 完整的表单验证
- 支持选择转出/转入栏舍
- 转栏原因下拉选择(正常调栏、疾病治疗、配种需要、产房准备、隔离观察、其他)
- 状态选择(已完成、进行中)
- 操作人员输入
- 备注信息输入
- 自动设置当前日期时间为默认转栏时间
### 3. API服务集成 (`api.js`)
**新增API接口**
```javascript
export const cattleTransferApi = {
getTransferRecords: (params) => get('/cattle-transfer-records', params),
searchTransferRecordsByEarNumber: (earNumber, params) => get('/cattle-transfer-records', { earNumber, ...params }),
getTransferRecordDetail: (id) => get(`/cattle-transfer-records/${id}`),
createTransferRecord: (data) => post('/cattle-transfer-records', data),
updateTransferRecord: (id, data) => put(`/cattle-transfer-records/${id}`, data),
deleteTransferRecord: (id) => del(`/cattle-transfer-records/${id}`),
getBarnsForTransfer: (farmId) => get('/barns', { farmId })
}
```
### 4. 路由配置
**新增路由:**
- `/cattle-transfer` - 转栏记录查看页面
- `/cattle-transfer-register` - 转栏登记页面
### 5. 首页集成
在首页的"业务办理"模块中添加了"牛只转栏"入口,点击可跳转到转栏记录页面。
## 技术实现细节
### 数据流处理
1. 组件挂载时自动加载转栏记录
2. 支持搜索防抖处理500ms延迟
3. 分页数据动态加载
4. 错误处理和用户提示
### 响应式设计
- 移动端优先设计
- 适配不同屏幕尺寸
- 触摸友好的交互元素
### 用户体验优化
- 加载状态提示
- 空状态展示
- 表单验证反馈
- 操作成功/失败提示
## 文件结构
```
src/
├── components/
│ ├── CattleTransfer.vue # 转栏记录查看页面
│ └── CattleTransferRegister.vue # 转栏登记页面
├── services/
│ └── api.js # API服务已更新
├── router/
│ └── index.js # 路由配置(已更新)
└── components/
└── Home.vue # 首页(已更新)
```
## 使用说明
1. **查看转栏记录:**
- 在首页点击"牛只转栏"进入记录列表
- 可通过耳号搜索特定记录
- 支持分页浏览
2. **新增转栏记录:**
- 在转栏记录页面点击"转栏登记"按钮
- 填写完整的转栏信息
- 提交后自动跳转回记录列表
## 注意事项
1. 确保后端API `http://localhost:5350/api/cattle-transfer-records` 正常运行
2. 需要有效的认证token才能访问API
3. 栏舍数据需要从 `/barns` 接口获取
4. 建议在生产环境中添加更多的错误处理和用户反馈
## 后续优化建议
1. 添加编辑功能的具体实现
2. 增加批量操作功能
3. 添加数据导出功能
4. 优化搜索和筛选功能
5. 添加数据统计和图表展示

View File

@@ -1,195 +0,0 @@
# 中文映射指南
## 概述
本系统使用统一的中文映射工具来将数据库中的数字代码转换为用户友好的中文显示。所有映射规则都集中在 `src/utils/mapping.js` 文件中。
## 映射字段
### 1. 性别映射 (sexMap)
```javascript
{
1: '公',
2: '母'
}
```
### 2. 品类映射 (categoryMap)
```javascript
{
1: '犊牛',
2: '育成母牛',
3: '架子牛',
4: '青年牛',
5: '基础母牛',
6: '育肥牛'
}
```
### 3. 品种映射 (breedMap)
```javascript
{
1: '西藏高山牦牛',
2: '宁夏牛',
3: '华西牛',
4: '秦川牛',
5: '西门塔尔牛',
6: '荷斯坦牛'
}
```
### 4. 品系映射 (strainMap)
```javascript
{
1: '乳肉兼用',
2: '肉用型',
3: '乳用型',
4: '兼用型'
}
```
### 5. 生理阶段映射 (physiologicalStageMap)
```javascript
{
1: '犊牛',
2: '育成期',
3: '青年期',
4: '成年期',
5: '老年期'
}
```
### 6. 来源映射 (sourceMap)
```javascript
{
1: '合作社',
2: '农户',
3: '养殖场',
4: '进口',
5: '自繁'
}
```
### 7. 事件映射 (eventMap)
```javascript
{
1: '正常',
2: '生病',
3: '怀孕',
4: '分娩',
5: '断奶',
6: '转栏',
7: '离栏'
}
```
### 8. 销售状态映射 (sellStatusMap)
```javascript
{
100: '在栏',
200: '已售',
300: '死亡',
400: '淘汰'
}
```
## 使用方法
### 在Vue组件中使用
```javascript
import {
getSexName,
getCategoryName,
getBreedName,
formatDate
} from '@/utils/mapping'
// 在方法中使用
const sexName = getSexName(cattle.sex) // 返回 '公' 或 '母'
const categoryName = getCategoryName(cattle.cate) // 返回 '犊牛' 等
const breedName = getBreedName(cattle.varieties) // 返回 '华西牛' 等
const formattedDate = formatDate(cattle.birthday) // 返回 '2024-08-07'
```
### 在模板中使用
```vue
<template>
<div>
<span>性别: {{ getSexName(cattle.sex) }}</span>
<span>品类: {{ getCategoryName(cattle.cate) }}</span>
<span>品种: {{ getBreedName(cattle.varieties) }}</span>
</div>
</template>
```
## 数据格式化
### 日期格式化
```javascript
// 时间戳转日期字符串
const dateString = formatDate(1723017600) // 返回 '2024-08-07'
// 日期字符串转时间戳
const timestamp = formatDateToTimestamp('2024-08-07') // 返回 1723017600
```
## 扩展映射
如果需要添加新的映射字段,请按以下步骤操作:
1.`src/utils/mapping.js` 中添加新的映射对象
2. 添加对应的获取函数
3. 在默认导出中包含新的映射
4. 在需要使用的组件中导入并使用
### 示例:添加新的映射字段
```javascript
// 在 mapping.js 中添加
export const newFieldMap = {
1: '选项1',
2: '选项2',
3: '选项3'
}
export function getNewFieldName(code) {
return newFieldMap[code] || '--'
}
// 在默认导出中添加
export default {
// ... 其他映射
newFieldMap,
getNewFieldName
}
```
## 注意事项
1. **一致性**: 所有映射都应该使用相同的格式和命名规范
2. **默认值**: 当映射不到对应值时,统一返回 '--'
3. **类型安全**: 确保传入的参数类型正确
4. **维护性**: 映射规则应该集中管理,便于维护和更新
## 当前使用位置
- `CattleProfile.vue`: 牛只档案列表页面
- `CattleAdd.vue`: 新增牛只档案页面
- 其他需要显示中文的组件
## 测试
可以通过以下方式测试映射功能:
1. 在浏览器控制台中测试映射函数
2. 使用测试页面验证显示效果
3. 检查API返回的数据是否正确映射
```javascript
// 在浏览器控制台中测试
import { getSexName, getCategoryName } from '@/utils/mapping'
console.log(getSexName(1)) // 应该输出 '公'
console.log(getCategoryName(1)) // 应该输出 '犊牛'
```

View File

@@ -1,127 +0,0 @@
# 智能项圈模块 API 集成完成报告
## 概述
智能项圈模块已成功从模拟数据迁移到真实API调用模仿智能耳标模块的实现方式提供了完整的数据管理功能。
## 主要更新
### 1. collarService.js 服务层更新
- **移除模拟数据逻辑**:完全移除了 `getMockCollarDevices()` 函数
- **添加真实API调用**:实现 `getAllCollarDevices()` 函数调用 `/api/smart-devices/collars` 接口
- **数据字段映射**确保API返回的数据正确映射到前端显示字段
- **分页支持**:支持 `page``limit` 参数
- **搜索支持**:支持 `search` 参数进行设备搜索
- **状态筛选**:支持 `status` 参数进行设备状态筛选
- **统计功能**:添加 `getCollarStatistics()` 函数获取设备统计信息
- **CRUD操作**:添加设备更新、删除等操作函数
#### 字段映射关系
```javascript
// API返回字段 -> 前端显示字段
sn/deviceId -> collarId (项圈编号)
voltage -> battery (设备电量)
temperature -> temperature (设备温度)
sid -> collectedHost (被采集主机)
walk -> totalMovement (总运动量)
walk - y_steps -> todayMovement (今日运动量)
gps -> gpsLocation (GPS位置)
time/uptime -> updateTime (数据更新时间)
bandge_status/state -> isBound (绑定状态优先使用bandge_status)
```
#### 绑定状态判断逻辑
```javascript
// 绑定状态判断优先级
isBound = device.bandge_status === 1 || device.bandge_status === '1' ||
device.state === 1 || device.state === '1'
// 状态字段说明
// bandge_status: 绑带状态字段 (优先使用)
// - 1: 已绑定
// - 0: 未绑定
// state: 设备状态字段 (备用)
// - 1: 已绑定
// - 0: 未绑定
```
### 2. SmartCollar.vue 组件更新
- **分页功能**:添加完整的分页控件,支持页码跳转和每页数量选择
- **搜索功能**:实现实时搜索,支持精确匹配和模糊搜索
- **统计显示**:显示设备总数、已绑定数量、未绑定数量
- **设备操作**:添加编辑、删除、绑定/解绑等操作
- **错误处理**:完善的错误处理和用户提示
- **加载状态**:添加加载动画和状态管理
- **刷新功能**:添加数据刷新按钮
#### 新增功能特性
1. **实时搜索**输入框支持实时搜索500ms防抖
2. **分页导航**:支持页码跳转、上一页/下一页、每页数量选择
3. **设备管理**:支持编辑设备信息、删除设备
4. **状态管理**:实时显示设备绑定状态
5. **数据刷新**:支持手动刷新数据
6. **响应式设计**:适配移动端显示
### 3. API接口调用
- **GET /api/smart-devices/collars**:获取项圈设备列表(支持分页、搜索、筛选)
- **POST /api/smart-devices/collars/bind**:绑定项圈设备
- **POST /api/smart-devices/collars/unbind**:解绑项圈设备
- **PUT /api/smart-devices/collars/:id**:更新项圈设备
- **DELETE /api/smart-devices/collars/:id**:删除项圈设备
## 技术实现细节
### 数据流程
1. 组件挂载时同时加载设备列表和统计信息
2. 用户操作搜索、分页、筛选触发API调用
3. API返回数据经过字段映射后显示在界面上
4. 错误情况显示友好的错误提示
### 状态管理
- `devices`:当前页面的设备列表
- `pagination`:分页信息(当前页、每页数量、总页数等)
- `isSearching`:搜索状态标识
- `searchResults`:搜索结果列表
- `totalCount/boundCount/unboundCount`:统计信息
### 错误处理
- 网络错误:显示"加载设备失败,请检查网络连接或重新登录"
- 认证错误自动清除本地token并跳转到登录页
- API错误在控制台记录详细错误信息
## 测试验证
### 测试脚本
创建了 `test-collar-api.js` 测试脚本,可以验证:
- API接口连通性
- 数据字段映射正确性
- 搜索功能有效性
### 运行测试
```bash
cd mini_program/farm-monitor-dashboard
node test-collar-api.js
```
## 使用说明
### 基本操作
1. **查看设备**:页面加载时自动显示设备列表
2. **搜索设备**:在搜索框输入设备编号进行搜索
3. **分页浏览**:使用分页控件浏览更多设备
4. **设备操作**:点击编辑/删除按钮管理设备
5. **绑定管理**:点击绑定按钮切换设备绑定状态
6. **刷新数据**:点击刷新按钮更新最新数据
### 界面特性
- **橙色主题**:与智能项圈模块的橙色主题保持一致
- **响应式布局**:适配不同屏幕尺寸
- **直观操作**:清晰的操作按钮和状态指示
- **实时反馈**:操作结果实时显示
## 兼容性说明
- 保持与原有API接口的完全兼容
- 支持向后兼容的字段映射
- 错误情况下优雅降级
## 总结
智能项圈模块已成功完成从模拟数据到真实API的迁移提供了与智能耳标模块一致的功能体验包括分页、搜索、统计、CRUD操作等完整功能。模块现在可以动态查询和调用后端API不再依赖模拟数据。

View File

@@ -1,184 +0,0 @@
# 智能项圈分页和搜索功能完善报告
## 功能概述
智能项圈模块已完成分页展示和搜索功能的全面优化,支持所有数据的正确分页显示和根据项圈编号的精确查询。
## 主要功能特性
### 1. 分页展示功能 ✅
#### 1.1 完整分页支持
- **数据分页加载**所有数据通过API分页加载支持大数据量展示
- **分页控件**:提供上一页、下一页、页码跳转等完整分页控件
- **每页数量选择**支持5、10、20、50条每页显示数量选择
- **分页信息显示**:实时显示当前页范围和总数据量
#### 1.2 分页状态管理
```javascript
// 分页数据结构
pagination: {
current: 1, // 当前页码
pageSize: 10, // 每页数量
total: 0, // 总数据量
totalPages: 0 // 总页数
}
```
#### 1.3 分页操作
- **页码跳转**:点击页码直接跳转到指定页面
- **上一页/下一页**:支持键盘和鼠标操作
- **每页数量调整**:动态调整每页显示数量并重置到第一页
- **分页信息实时更新**:显示"第 X-Y 条,共 Z 条"格式信息
### 2. 搜索功能 ✅
#### 2.1 精确搜索实现
- **API全局搜索**调用后端API进行全局数据搜索不限于当前页面
- **项圈编号精确查询**:支持根据项圈编号进行精确匹配搜索
- **实时搜索**输入框支持实时搜索500ms防抖优化
- **搜索状态管理**:独立的搜索状态和分页管理
#### 2.2 搜索功能特性
```javascript
// 搜索参数
{
page: 1, // 搜索分页
pageSize: 10, // 搜索每页数量
search: "22012000108" // 搜索关键词
}
```
#### 2.3 搜索用户体验
- **搜索状态显示**:显示"搜索 '关键词' 的结果"
- **搜索结果分页**:搜索结果支持独立分页
- **空状态提示**:未找到结果时显示友好提示
- **清除搜索**:一键清除搜索条件并返回正常列表
### 3. 界面优化 ✅
#### 3.1 搜索界面优化
- **搜索状态栏**:动态显示搜索进度和结果
- **搜索提示**:提供搜索建议和操作提示
- **空状态处理**:区分正常空状态和搜索无结果状态
#### 3.2 分页界面优化
- **分页控件**:美观的分页按钮和页码显示
- **分页信息**:清晰的分页状态信息
- **响应式设计**:适配不同屏幕尺寸
#### 3.3 用户体验提升
- **加载状态**:搜索和分页加载时显示加载动画
- **错误处理**:网络错误时显示友好错误提示
- **操作反馈**:操作结果实时反馈
## 技术实现细节
### 1. 分页实现
```javascript
// 分页数据加载
async loadDevices() {
const params = {
page: this.pagination.current,
pageSize: this.pagination.pageSize,
search: this.searchQuery || undefined
}
const response = await getAllCollarDevices(params)
// 更新分页信息和设备列表
}
```
### 2. 搜索实现
```javascript
// 全局搜索
async performSearch() {
const params = {
page: 1,
pageSize: 10,
search: this.searchQuery.trim()
}
const response = await getAllCollarDevices(params)
// 更新搜索结果和搜索分页信息
}
```
### 3. 状态管理
```javascript
// 搜索状态管理
isSearching: false, // 是否在搜索模式
searchResults: [], // 搜索结果
searchPagination: { // 搜索分页信息
current: 1,
pageSize: 10,
total: 0,
totalPages: 0
}
```
## 测试验证
### 1. 测试脚本
创建了 `test-collar-pagination-search.js` 测试脚本,包含:
- **分页功能测试**:测试多页数据加载和分页控件
- **搜索功能测试**:测试精确搜索和模糊搜索
- **搜索分页测试**:测试搜索结果的分页功能
### 2. 运行测试
```bash
cd mini_program/farm-monitor-dashboard
node test-collar-pagination-search.js
```
### 3. 测试覆盖
- ✅ 分页数据加载
- ✅ 分页控件操作
- ✅ 每页数量调整
- ✅ 精确搜索功能
- ✅ 模糊搜索功能
- ✅ 搜索分页功能
- ✅ 空状态处理
- ✅ 错误状态处理
## 使用说明
### 1. 分页操作
1. **浏览数据**:使用分页控件浏览不同页面的数据
2. **调整每页数量**:使用下拉菜单选择每页显示数量
3. **跳转页面**:点击页码数字直接跳转到指定页面
4. **上一页/下一页**:使用箭头按钮翻页
### 2. 搜索操作
1. **输入搜索关键词**:在搜索框中输入项圈编号
2. **实时搜索**输入时自动触发搜索500ms延迟
3. **查看搜索结果**:搜索结果支持分页浏览
4. **清除搜索**:点击"清除搜索"按钮返回正常列表
### 3. 界面说明
- **搜索状态栏**:显示当前搜索状态和结果数量
- **分页信息**:显示当前页范围和总数据量
- **分页控件**:提供完整的分页操作功能
- **空状态提示**:未找到数据时显示相应提示
## 性能优化
### 1. 搜索优化
- **防抖处理**输入搜索时500ms防抖避免频繁API调用
- **API全局搜索**直接调用后端API搜索不依赖前端数据
- **分页搜索**:搜索结果支持分页,避免一次性加载大量数据
### 2. 分页优化
- **按需加载**:只加载当前页面的数据
- **状态保持**:搜索和分页状态独立管理
- **缓存优化**:合理的数据缓存和状态管理
## 总结
智能项圈模块的分页和搜索功能已全面完善:
1. **分页功能**:支持所有数据的正确分页展示,提供完整的分页控件和操作
2. **搜索功能**:实现根据项圈编号的精确查询,支持全局搜索和搜索分页
3. **用户体验**:优化界面交互,提供友好的操作反馈和状态提示
4. **性能优化**合理的API调用和状态管理确保良好的性能表现
所有功能已通过测试验证,可以正常使用。

View File

@@ -1,371 +0,0 @@
# 项目转换指南
## 转换概述
本项目已从uni-app技术栈转换为微信小程序原生技术栈。以下是详细的转换说明和注意事项。
## 技术栈对比
| 项目 | uni-app | 微信小程序原生 |
|------|---------|----------------|
| 框架 | Vue.js + uni-app | 微信小程序原生 |
| 模板 | Vue单文件组件 | WXML |
| 样式 | SCSS/CSS | WXSS |
| 逻辑 | Vue.js | JavaScript ES6+ |
| 路由 | Vue Router | 微信小程序路由 |
| 状态管理 | Pinia | 微信小程序全局数据 |
| 网络请求 | axios | wx.request |
| 组件库 | uView UI | 微信小程序原生组件 |
## 主要转换内容
### 1. 项目结构转换
#### 原uni-app结构
```
src/
├── App.vue
├── main.js
├── pages/
├── components/
├── services/
└── utils/
```
#### 转换后微信小程序结构
```
├── app.js
├── app.json
├── app.wxss
├── pages/
├── services/
└── utils/
```
### 2. 页面转换
#### Vue单文件组件 → 微信小程序页面
- `.vue``.js` + `.wxml` + `.wxss`
- `template``.wxml`
- `script``.js`
- `style``.wxss`
#### 示例对比
**Vue组件 (原)**
```vue
<template>
<view class="container">
<text>{{ message }}</text>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello World'
}
}
}
</script>
<style scoped>
.container {
padding: 20rpx;
}
</style>
```
**微信小程序页面 (转换后)**
```javascript
// .js
Page({
data: {
message: 'Hello World'
}
})
```
```xml
<!-- .wxml -->
<view class="container">
<text>{{message}}</text>
</view>
```
```css
/* .wxss */
.container {
padding: 20rpx;
}
```
### 3. 路由转换
#### uni-app路由
```javascript
// 页面跳转
uni.navigateTo({ url: '/pages/detail/detail' })
// 路由配置
export default new VueRouter({
routes: [
{ path: '/detail', component: Detail }
]
})
```
#### 微信小程序路由
```javascript
// 页面跳转
wx.navigateTo({ url: '/pages/detail/detail' })
// 路由配置 (app.json)
{
"pages": [
"pages/detail/detail"
]
}
```
### 4. 状态管理转换
#### uni-app (Pinia)
```javascript
// store/user.js
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null
}),
actions: {
setUserInfo(info) {
this.userInfo = info
}
}
})
// 组件中使用
const userStore = useUserStore()
userStore.setUserInfo(userInfo)
```
#### 微信小程序全局数据
```javascript
// app.js
App({
globalData: {
userInfo: null
},
setUserInfo(info) {
this.globalData.userInfo = info
}
})
// 页面中使用
const app = getApp()
app.setUserInfo(userInfo)
```
### 5. 网络请求转换
#### uni-app (axios)
```javascript
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
api.get('/users').then(res => {
console.log(res.data)
})
```
#### 微信小程序 (wx.request)
```javascript
wx.request({
url: 'https://api.example.com/users',
method: 'GET',
success: (res) => {
console.log(res.data)
}
})
```
### 6. 组件转换
#### Vue组件
```vue
<template>
<view class="custom-component">
<slot></slot>
</view>
</template>
<script>
export default {
props: ['title'],
methods: {
handleClick() {
this.$emit('click')
}
}
}
</script>
```
#### 微信小程序组件
```javascript
// components/custom-component.js
Component({
properties: {
title: String
},
methods: {
handleClick() {
this.triggerEvent('click')
}
}
})
```
```xml
<!-- components/custom-component.wxml -->
<view class="custom-component">
<slot></slot>
</view>
```
## 依赖处理
### 移除的依赖
- `@dcloudio/uni-app`
- `@dcloudio/uni-cli-shared`
- `@dcloudio/uni-h5`
- `@dcloudio/uni-mp-weixin`
- `@vue/composition-api`
- `vue`
- `vue-router`
- `vue-template-compiler`
- `pinia`
- `axios`
- `@vant/weapp`
### 保留的依赖
- `dayjs` - 日期处理库
### 新增的依赖
-使用微信小程序原生API
## 配置文件转换
### 1. package.json
- 移除uni-app相关依赖
- 更新项目描述和脚本
- 保留必要的开发依赖
### 2. manifest.json → app.json
- 页面配置迁移到app.json
- 移除uni-app特有配置
- 保留微信小程序配置
### 3. pages.json → app.json
- 页面路由配置
- tabBar配置
- 全局样式配置
## 样式转换
### 1. SCSS变量 → WXSS变量
```scss
// 原SCSS
$primary-color: #3cc51f;
$font-size: 14px;
.container {
color: $primary-color;
font-size: $font-size;
}
```
```css
/* 转换后WXSS */
.container {
color: #3cc51f;
font-size: 28rpx; /* 注意单位转换 */
}
```
### 2. 响应式设计
- 使用rpx单位替代px
- 适配不同屏幕尺寸
- 保持设计一致性
## 功能适配
### 1. 生命周期
- Vue生命周期 → 微信小程序生命周期
- `created``onLoad`
- `mounted``onReady`
- `destroyed``onUnload`
### 2. 事件处理
- Vue事件 → 微信小程序事件
- `@click``bindtap`
- `@input``bindinput`
### 3. 数据绑定
- Vue数据绑定 → 微信小程序数据绑定
- `v-model``value` + `bindinput`
- `v-for``wx:for`
## 注意事项
### 1. 兼容性
- 微信小程序API限制
- 网络请求域名配置
- 图片资源大小限制
### 2. 性能优化
- 避免频繁setData
- 合理使用分包
- 图片懒加载
### 3. 开发调试
- 使用微信开发者工具
- 真机调试测试
- 控制台日志查看
## 迁移检查清单
- [ ] 页面结构转换完成
- [ ] 样式适配完成
- [ ] 逻辑代码转换完成
- [ ] 路由配置正确
- [ ] 网络请求正常
- [ ] 组件功能正常
- [ ] 状态管理正确
- [ ] 生命周期适配
- [ ] 事件处理正确
- [ ] 数据绑定正常
- [ ] 依赖清理完成
- [ ] 配置文件更新
- [ ] 功能测试通过
- [ ] 性能优化完成
## 后续维护
1. 定期更新微信小程序基础库版本
2. 关注微信小程序API更新
3. 优化性能和用户体验
4. 及时修复bug和问题
5. 保持代码质量和规范
## 技术支持
如有转换相关问题,请参考:
- 微信小程序官方文档
- 项目README文档
- 代码注释和说明

View File

@@ -1,87 +0,0 @@
# API连接问题调试指南
## 问题描述
在访问牛只档案页面时出现错误:`Cannot read properties of undefined (reading 'error')`
## 可能的原因
1. **后端服务未启动**
- 后端服务需要在 `http://localhost:5350` 运行
- 检查后端服务是否正常启动
2. **API路径错误**
- 已修复API路径配置
- 牛只列表:`/api/iot-cattle/public`
- 栏舍列表:`/api/iot-cattle/public/pens/list`
- 批次列表:`/api/iot-cattle/public/batches/list`
3. **CORS跨域问题**
- 后端需要配置CORS允许前端访问
4. **网络连接问题**
- 检查前端是否能访问后端服务
## 调试步骤
### 1. 检查后端服务
```bash
# 在后端目录运行
cd backend
npm start
# 或
node server.js
```
### 2. 测试API连接
访问测试页面:`http://localhost:8080/api-test`
### 3. 检查浏览器控制台
打开浏览器开发者工具查看Network标签页和Console标签页的错误信息
### 4. 手动测试API
在浏览器中直接访问:
- `http://localhost:5350/api/cattle-type`
- `http://localhost:5350/api/iot-cattle/public`
## 已修复的问题
1. **错误处理优化**
- 修复了 `error` 属性未定义的问题
- 添加了更详细的错误信息输出
2. **API路径修正**
- 修正了栏舍和批次API的路径
- 确保路径与后端路由配置一致
3. **调试信息添加**
- 在CattleProfile组件中添加了详细的调试日志
- 可以查看请求参数和响应数据
## 临时解决方案
如果API连接有问题可以
1. **使用模拟数据**
- 在CattleProfile组件中临时使用模拟数据
- 注释掉API调用使用静态数据
2. **检查环境变量**
- 确保 `VUE_APP_BASE_URL` 正确设置
- 默认值:`http://localhost:5350/api`
## 下一步
1. 启动后端服务
2. 访问测试页面验证API连接
3. 如果仍有问题,检查后端日志
4. 确认数据库连接正常
## 测试页面功能
访问 `/api-test` 页面可以测试:
- 基础连接测试
- 牛只档案API测试
- 牛只类型API测试
- 栏舍API测试
- 批次API测试
- 直接HTTP请求测试

View File

@@ -1,95 +0,0 @@
# SharedArrayBuffer 弃用警告解决方案
## ⚠️ 警告信息
```
[Deprecation] SharedArrayBuffer will require cross-origin isolation as of M92, around July 2021.
See https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more details.
```
## 🔍 问题分析
这个警告是由微信开发者工具内部使用 SharedArrayBuffer 导致的,不会影响您的智能耳标页面功能。警告出现的原因:
1. **开发者工具版本**: 较旧版本的微信开发者工具
2. **编译模式**: 使用了旧的编译模式
3. **浏览器兼容性**: Chrome M92+ 版本的安全策略变更
## ✅ 解决方案
### 方案1: 更新开发者工具(推荐)
1. 下载最新版本的微信开发者工具
2. 在工具设置中启用"使用新的编译模式"
3. 重启开发者工具
### 方案2: 项目配置优化
已更新 `project.config.json` 配置:
- ✅ 启用 `newFeature: true` - 使用新特性
- ✅ 启用 `disableSWC: false` - 使用新的编译器
- ✅ 启用 `useCompilerModule: true` - 使用编译器模块
- ✅ 启用 `useStaticServer: true` - 使用静态服务器
- ✅ 保持其他优化设置
### 方案3: 忽略警告(临时方案)
如果警告不影响功能,可以暂时忽略:
- 警告不会影响智能耳标页面的API调用
- 不会影响数据展示和交互功能
- 只是开发者工具的兼容性提示
## 🎯 验证方法
### 检查智能耳标页面功能:
1. **API调用**: 确认 `/api/iot-jbq-client` 接口正常调用
2. **数据显示**: 确认耳标数据正常显示
3. **筛选功能**: 确认总数、已绑定、未绑定筛选正常
4. **搜索功能**: 确认搜索功能正常
5. **交互功能**: 确认点击、添加等功能正常
### 测试步骤:
```javascript
// 在开发者工具控制台测试
console.log('智能耳标页面功能测试:')
console.log('✅ API接口调用正常')
console.log('✅ 数据显示正常')
console.log('✅ 筛选功能正常')
console.log('✅ 搜索功能正常')
console.log('✅ 交互功能正常')
```
## 📱 功能确认
智能耳标页面的所有功能都正常工作:
### ✅ 已实现功能
- **API集成**: `/api/iot-jbq-client` 接口调用
- **数据映射**: 字段自动映射和格式化
- **UI设计**: 严格按照图片设计实现
- **筛选功能**: 总数、已绑定、未绑定
- **搜索功能**: 按编号和主机号搜索
- **添加功能**: 新增耳标功能
- **响应式设计**: 适配不同屏幕
### ✅ 数据处理
- **绑定状态**: 智能判断绑定状态
- **电量显示**: 格式化电量百分比
- **温度显示**: 格式化温度值
- **时间显示**: 中文时间格式
- **运动量**: 数值格式化显示
## 🚀 后续优化
1. **定期更新**: 保持微信开发者工具为最新版本
2. **监控警告**: 关注新的弃用警告
3. **功能测试**: 定期测试页面功能
4. **性能优化**: 持续优化页面性能
## 📞 技术支持
如果警告持续出现或影响功能:
1. 检查微信开发者工具版本
2. 尝试重新编译项目
3. 清除缓存后重新加载
4. 联系微信开发者工具技术支持
---
**注意**: 这个警告不会影响您的智能耳标页面功能所有API调用、数据显示、交互功能都正常工作。

View File

@@ -1,98 +0,0 @@
# 微信小程序域名配置指南
## 问题描述
错误信息:`"request:fail url not in domain list"`
这是因为微信小程序要求所有网络请求的域名必须在微信公众平台配置的域名白名单中。
## 解决方案
### 方案1开发环境 - 开启调试模式(推荐)
1. **在微信开发者工具中开启调试模式**
- 打开微信开发者工具
- 点击右上角的"详情"按钮
- 在"本地设置"中勾选"不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书"
- 这样可以在开发阶段使用本地API
2. **确保后端服务运行**
```bash
cd backend
npm start
# 后端将在 http://localhost:5350 运行
```
### 方案2生产环境 - 配置域名白名单
1. **登录微信公众平台**
- 访问 https://mp.weixin.qq.com
- 使用小程序账号登录
2. **配置服务器域名**
- 进入"开发" -> "开发管理" -> "开发设置"
- 在"服务器域名"中添加:
- request合法域名`https://your-backend-domain.com`
- socket合法域名`wss://your-backend-domain.com`
- uploadFile合法域名`https://your-backend-domain.com`
- downloadFile合法域名`https://your-backend-domain.com`
3. **更新API配置**
```javascript
// utils/api.js
const config = {
baseUrl: 'https://your-backend-domain.com/api',
// ... 其他配置
}
```
### 方案3使用ngrok内网穿透临时方案
1. **启动ngrok**
```bash
cd backend
./ngrok.exe http 5350
```
2. **获取公网地址**
- ngrok会提供一个公网地址`https://abc123.ngrok.io`
3. **更新API配置**
```javascript
// utils/api.js
const config = {
baseUrl: 'https://abc123.ngrok.io/api',
// ... 其他配置
}
```
4. **在微信公众平台添加域名**
- 将ngrok提供的域名添加到服务器域名白名单
## 当前配置
当前API配置使用本地地址
```javascript
baseUrl: 'http://localhost:5350/api'
```
## 测试步骤
1. **确保后端服务运行**
```bash
cd backend
npm start
```
2. **开启微信开发者工具调试模式**
- 勾选"不校验合法域名"
3. **测试智能耳标页面**
- 点击首页"智能设备" -> "智能耳标"
- 应该能正常加载数据
## 注意事项
- 开发环境建议使用方案1开启调试模式
- 生产环境必须使用方案2配置域名白名单
- ngrok方案仅适用于临时测试
- 确保后端API接口 `/api/iot-jbq-client` 正常工作

View File

@@ -1,187 +0,0 @@
# 智能耳标功能模块完善
## 功能概述
根据提供的API接口文档完善了智能耳标设备管理功能实现了完整的CRUD操作。
## API接口支持
### 1. 获取所有智能耳标设备
- **接口**: `GET /api/iot-jbq-client`
- **功能**: 获取所有智能耳标设备列表
- **方法**: `getAllEarTagDevices(params)`
### 2. 根据CID获取设备
- **接口**: `GET /api/iot-jbq-client/cid/{cid}`
- **功能**: 根据客户端ID获取相关设备
- **方法**: `getEarTagDevicesByCid(cid)`
### 3. 根据ID获取设备
- **接口**: `GET /api/iot-jbq-client/{id}`
- **功能**: 根据设备ID获取单个设备详情
- **方法**: `getEarTagDeviceById(id)`
### 4. 更新设备
- **接口**: `PUT /api/iot-jbq-client/{id}`
- **功能**: 更新智能耳标设备信息
- **方法**: `updateEarTagDevice(id, data)`
### 5. 删除设备
- **接口**: `DELETE /api/iot-jbq-client/{id}`
- **功能**: 删除智能耳标设备
- **方法**: `deleteEarTagDevice(id)`
## 新增功能
### 1. 设备管理功能
-**查看所有设备**: 显示完整的设备列表
-**按CID过滤**: 根据客户端ID筛选设备
-**设备详情**: 查看单个设备的详细信息
-**编辑设备**: 修改设备属性信息
-**删除设备**: 移除不需要的设备
### 2. 用户界面增强
-**操作按钮**: 每个设备卡片添加编辑和删除按钮
-**编辑对话框**: 模态对话框用于编辑设备信息
-**删除确认**: 删除前的确认对话框
-**CID过滤**: 可选的CID过滤功能
-**响应式设计**: 适配不同屏幕尺寸
### 3. 数据字段支持
-**耳标编号**: `eartagNumber`
-**设备电量**: `battery`
-**设备温度**: `temperature`
-**被采集主机**: `collectedHost`
-**总运动量**: `totalMovement`
-**今日运动量**: `dailyMovement`
-**位置信息**: `location`
-**更新时间**: `lastUpdate`
## 使用方法
### 1. 访问设备管理
```
http://localhost:8080/ear-tag
```
### 2. 基本操作
- **查看设备**: 页面加载时自动显示所有设备
- **搜索设备**: 使用搜索框按设备ID或主机ID搜索
- **编辑设备**: 点击设备卡片上的✏️按钮
- **删除设备**: 点击设备卡片上的🗑️按钮
### 3. 高级功能
- **CID过滤**: 点击"CID过滤"按钮输入CID进行筛选
- **设备详情**: 点击设备卡片查看详细信息
- **批量操作**: 支持多设备同时操作
## 技术实现
### 1. 服务层 (earTagService.js)
```javascript
// 新增的API方法
export const getAllEarTagDevices = async (params = {})
export const getEarTagDevicesByCid = async (cid)
export const getEarTagDeviceById = async (id)
export const updateEarTagDevice = async (id, data)
export const deleteEarTagDevice = async (id)
```
### 2. 组件层 (EarTag.vue)
```javascript
// 新增的数据属性
showEditDialog: false,
showDeleteDialog: false,
selectedDevice: null,
editDevice: {},
cidFilter: '',
showCidFilter: false
// 新增的方法
loadDevicesByCid()
loadDeviceById()
showEditDevice()
updateDevice()
showDeleteDevice()
confirmDeleteDevice()
```
### 3. 用户界面
```vue
<!-- 编辑对话框 -->
<div v-if="showEditDialog" class="dialog-overlay">
<!-- 编辑表单 -->
</div>
<!-- 删除确认对话框 -->
<div v-if="showDeleteDialog" class="dialog-overlay">
<!-- 删除确认 -->
</div>
```
## 样式特性
### 1. 对话框样式
- **模态覆盖**: 半透明黑色背景
- **居中显示**: 响应式居中布局
- **表单样式**: 统一的输入框和按钮样式
- **动画效果**: 平滑的显示/隐藏动画
### 2. 操作按钮
- **编辑按钮**: 蓝色主题,铅笔图标
- **删除按钮**: 红色主题,垃圾桶图标
- **悬停效果**: 鼠标悬停时的颜色变化
### 3. 响应式设计
- **移动端适配**: 小屏幕设备优化
- **触摸友好**: 适合触摸操作的按钮大小
- **滚动支持**: 长列表的滚动处理
## 错误处理
### 1. API错误处理
- **网络错误**: 显示友好的错误信息
- **认证错误**: 自动重定向到登录页面
- **数据错误**: 显示具体的错误原因
### 2. 用户操作错误
- **表单验证**: 输入数据的格式验证
- **操作确认**: 危险操作的二次确认
- **状态反馈**: 操作成功/失败的即时反馈
## 测试方法
### 1. 功能测试
1. 访问 `http://localhost:8080/ear-tag`
2. 测试设备列表加载
3. 测试编辑功能
4. 测试删除功能
5. 测试CID过滤
### 2. API测试
1. 访问 `http://localhost:8080/auth-test`
2. 点击"设置真实Token"
3. 点击"测试所有API"
4. 查看测试结果
## 注意事项
1. **API兼容性**: 确保后端API接口正常工作
2. **认证状态**: 需要有效的JWT token
3. **数据格式**: 确保API返回的数据格式正确
4. **错误处理**: 网络错误时的优雅降级
## 下一步计划
1. **添加设备功能**: 实现设备创建功能
2. **批量操作**: 支持多设备批量编辑/删除
3. **数据导出**: 支持设备数据导出
4. **实时更新**: 设备状态的实时刷新
5. **权限控制**: 基于角色的操作权限
## 相关文件
- `src/services/earTagService.js` - API服务层
- `src/components/EarTag.vue` - 设备管理组件
- `src/components/AuthTest.vue` - API测试组件
- `backend/routes/smart-devices.js` - 后端API路由

View File

@@ -1,231 +0,0 @@
# 电子围栏功能实现说明
## 功能概述
基于管理系统的ElectronicFence.vue实现为小程序完善了电子围栏功能包括围栏绘制、管理、查看等核心功能。
## 文件结构
```
src/
├── services/
│ └── fenceService.js # 电子围栏API服务
├── components/
│ ├── ElectronicFence.vue # 电子围栏主组件
│ └── MapView.vue # 地图视图组件
├── views/
│ └── ElectronicFencePage.vue # 电子围栏页面
└── router/
└── index.js # 路由配置(已更新)
```
## 核心功能
### 1. 围栏绘制
- **开始绘制**:点击"开始绘制"按钮进入绘制模式
- **坐标点添加**:在地图上点击添加围栏坐标点
- **实时反馈**:显示当前绘制状态和坐标点信息
- **完成绘制**至少3个点才能完成围栏绘制
- **取消绘制**:随时可以取消当前绘制操作
### 2. 围栏管理
- **围栏列表**:查看所有围栏,支持搜索和筛选
- **围栏信息**:显示围栏名称、类型、坐标点数量、面积等
- **围栏编辑**:修改围栏名称、类型、描述等信息
- **围栏删除**:删除不需要的围栏
- **围栏选择**:点击围栏在地图上定位显示
### 3. 围栏类型
- **放牧区** 🌿:绿色标识,用于放牧区域
- **安全区** 🛡️:蓝色标识,用于安全保护区域
- **限制区** ⚠️:红色标识,用于限制进入区域
- **收集区** 📦:橙色标识,用于收集作业区域
## API接口集成
### 围栏管理接口
```javascript
// 获取围栏列表
GET /api/electronic-fences
// 获取单个围栏
GET /api/electronic-fences/{id}
// 创建围栏
POST /api/electronic-fences
// 更新围栏
PUT /api/electronic-fences/{id}
// 删除围栏
DELETE /api/electronic-fences/{id}
// 搜索围栏
GET /api/electronic-fences/search
```
### 坐标点管理接口
```javascript
// 获取围栏坐标点
GET /api/electronic-fence-points/fence/{fenceId}
// 创建坐标点
POST /api/electronic-fence-points
// 批量创建坐标点
POST /api/electronic-fence-points/batch
// 更新坐标点
PUT /api/electronic-fence-points/{id}
// 删除坐标点
DELETE /api/electronic-fence-points/{id}
// 获取围栏边界框
GET /api/electronic-fence-points/fence/{fenceId}/bounds
// 搜索坐标点
GET /api/electronic-fence-points/search
```
## 组件说明
### ElectronicFence.vue
主组件,包含以下功能模块:
- 顶部导航栏
- 地图容器
- 绘制控制面板
- 围栏列表面板
- 围栏信息面板
- 围栏编辑模态框
### MapView.vue
地图视图组件,提供:
- 地图显示和交互
- 绘制模式切换
- 围栏显示
- 坐标点标记
- 地图控制功能
### fenceService.js
API服务类包含
- 围栏CRUD操作
- 坐标点管理
- 围栏类型配置
- 工具函数(面积计算、中心点计算等)
## 使用方式
### 1. 访问电子围栏
从首页点击"电子围栏"工具卡片,或直接访问 `/electronic-fence` 路由。
### 2. 地图功能测试
访问 `/map-test` 路由可以测试百度地图集成功能:
- 地图加载和显示
- 围栏绘制和显示
- 坐标点标记
- 地图交互控制
### 3. 绘制新围栏
1. 点击"开始绘制"按钮
2. 在地图上点击添加坐标点至少3个
3. 点击"完成绘制"按钮
4. 填写围栏信息(名称、类型、描述)
5. 点击"确定"保存围栏
### 4. 管理围栏
1. 点击右上角菜单按钮查看围栏列表
2. 使用搜索框筛选围栏
3. 点击围栏项查看详细信息
4. 使用编辑/删除按钮管理围栏
## 技术特点
### 1. 响应式设计
- 适配移动端屏幕
- 触摸友好的交互设计
- 优化的UI布局
### 2. 状态管理
- 绘制状态实时更新
- 围栏数据响应式绑定
- 错误处理和用户反馈
### 3. 百度地图集成
- 使用百度地图API v3.0
- 支持地图缩放、拖拽、点击交互
- 实时坐标点显示和绘制
- 围栏边界可视化
- 支持多种围栏类型颜色区分
### 4. 数据验证
- 围栏数据完整性检查
- 坐标点数量验证
- 面积计算和验证
## 配置说明
### 地图配置
```javascript
// 百度地图API密钥
const BAIDU_MAP_AK = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo'
// 地图中心点配置
mapCenter: { lng: 106.27, lat: 38.47 }, // 宁夏中心坐标
mapZoom: 8 // 适合宁夏全区域的缩放级别
// 百度地图API加载
const script = document.createElement('script')
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_AK}&callback=initBaiduMap`
```
### 围栏类型配置
```javascript
fenceTypes: {
grazing: { name: '放牧区', color: '#52c41a', icon: '🌿' },
safety: { name: '安全区', color: '#1890ff', icon: '🛡️' },
restricted: { name: '限制区', color: '#ff4d4f', icon: '⚠️' },
collector: { name: '收集区', color: '#fa8c16', icon: '📦' }
}
```
## 扩展功能
### 1. 地图SDK集成
**已完成百度地图API集成**
- 使用百度地图API v3.0
- 支持围栏绘制和显示
- 支持坐标点标记
- 支持地图交互控制
其他可选地图服务:
- 高德地图API
- 腾讯地图API
- 其他地图服务
### 2. 高级功能
- 围栏面积计算
- 围栏重叠检测
- 围栏历史记录
- 围栏权限管理
### 3. 数据导出
- 围栏数据导出
- 坐标点数据导出
- 围栏报告生成
## 注意事项
1. **地图SDK**需要集成实际的地图SDK才能实现完整功能
2. **坐标系统**确保使用正确的坐标系统WGS84
3. **网络请求**需要配置正确的API基础URL
4. **权限管理**:根据用户权限控制围栏操作
5. **数据同步**:确保与后端数据同步
## 开发建议
1. 优先集成地图SDK实现基础绘制功能
2. 完善错误处理和用户提示
3. 添加数据缓存机制提升性能
4. 实现离线模式支持
5. 添加围栏导入/导出功能

View File

@@ -1,168 +0,0 @@
# 智能耳标字段映射指南
## API响应字段映射
根据 `/api/iot-jbq-client` 接口的响应数据,以下是字段映射关系:
### 主要字段映射
| 中文标签 | API字段 | 数据类型 | 说明 | 示例值 |
|---------|---------|----------|------|--------|
| 耳标编号 | `cid` | number | 设备唯一标识 | 2105517333 |
| 设备电量/% | `voltage` | string | 设备电压百分比 | "98" |
| 设备温度/°C | `temperature` | string | 设备温度 | "39" |
| 被采集主机 | `sid` | string | 采集主机ID | "" |
| 总运动量 | `walk` | number | 总步数 | 1000 |
| 今日运动量 | 计算字段 | number | walk - y_steps | 500 |
| 数据更新时间 | `time` | number | Unix时间戳 | 1646969844 |
| 绑定状态 | `state` | number | 1=已绑定, 0=未绑定 | 1 |
### 计算字段
#### 今日运动量
```javascript
今日运动量 = walk - y_steps
// 示例: 1000 - 500 = 500
```
#### 数据更新时间格式化
```javascript
// Unix时间戳转换为本地时间
const date = new Date(timestamp * 1000)
const formattedTime = date.toLocaleString('zh-CN')
```
#### 绑定状态判断
```javascript
// state字段为1表示已绑定0或其他值表示未绑定
const isBound = device.state === 1 || device.state === '1'
```
### 备用字段映射
为了保持向后兼容性,系统支持以下备用字段:
| 主字段 | 备用字段 | 说明 |
|--------|----------|------|
| `cid` | `aaid`, `id` | 设备标识 |
| `voltage` | `battery` | 电量信息 |
| `walk` | `totalMovement` | 总运动量 |
| `sid` | `collectedHost` | 采集主机 |
| `time` | `uptime`, `updateTime` | 更新时间 |
### 数据处理逻辑
#### 1. 数据标准化
```javascript
const processedDevice = {
...device,
// 确保关键字段存在
cid: device.cid || device.aaid || device.id,
voltage: device.voltage || '0',
temperature: device.temperature || '0',
walk: device.walk || 0,
y_steps: device.y_steps || 0,
time: device.time || device.uptime || 0,
// 保持向后兼容
earTagId: device.cid || device.earTagId,
battery: device.voltage || device.battery,
totalMovement: device.walk || device.totalMovement,
todayMovement: (device.walk || 0) - (device.y_steps || 0),
collectedHost: device.sid || device.collectedHost,
updateTime: device.time || device.updateTime
}
```
#### 2. 今日运动量计算
```javascript
calculateTodayMovement(device) {
const walk = parseInt(device.walk) || 0
const ySteps = parseInt(device.y_steps) || 0
return Math.max(0, walk - ySteps)
}
```
#### 3. 时间格式化
```javascript
formatUpdateTime(device) {
const timestamp = device.time || device.updateTime || device.uptime
if (!timestamp) return '未知'
try {
const date = new Date(timestamp * 1000)
if (isNaN(date.getTime())) {
return new Date(timestamp).toLocaleString('zh-CN')
}
return date.toLocaleString('zh-CN')
} catch (error) {
return '时间格式错误'
}
}
```
### 搜索功能支持
搜索功能支持以下字段:
- `cid` (耳标编号)
- `earTagId` (备用耳标编号)
- `collectedHost` (被采集主机)
- `sid` (备用采集主机)
### 分页参数
API支持以下分页参数
- `page`: 页码默认1
- `pageSize`: 每页数量默认10
- `cid`: 设备CID过滤可选
### 示例API调用
```javascript
// 获取第一页数据每页10条
GET /api/iot-jbq-client?page=1&pageSize=10
// 根据CID过滤
GET /api/iot-jbq-client?cid=2105517333&page=1&pageSize=10
```
### 响应格式
```json
{
"success": true,
"data": [
{
"id": 165019,
"org_id": 326,
"cid": 2105517333,
"aaid": 2105517333,
"uid": 326,
"time": 1646969844,
"uptime": 1646969844,
"sid": "",
"walk": 1000,
"y_steps": 500,
"r_walk": 0,
"lat": "38.902401",
"lon": "106.534732",
"gps_state": "V",
"voltage": "98",
"temperature": "39",
"temperature_two": "0",
"state": 1,
"type": 1,
"sort": 1,
"ver": "0",
"weight": 0,
"start_time": 0,
"run_days": 240
}
],
"pagination": {
"current": 1,
"pageSize": 10,
"total": 100
},
"message": "获取智能耳标设备列表成功"
}
```

View File

@@ -1,141 +0,0 @@
# 主机编号显示问题修复报告
## 🎯 问题描述
**问题**: 智能主机页面中主机编号显示为空
**现象**: 前端界面显示"主机编号:" 但后面没有数值
**影响**: 用户无法识别具体的主机设备
## 🔍 问题分析
### API返回数据结构
```json
{
"success": true,
"data": [
{
"id": 4925,
"deviceNumber": "2024010103", // ← 主机编号字段
"battery": 100,
"signalValue": "强",
"temperature": 5,
"updateTime": "2024-01-10 09:39:20",
// ... 其他字段
}
]
}
```
### 前端代码问题
```javascript
// 修复前 - 错误的字段映射
<div class="device-id">主机编号: {{ device.sid || device.hostId }}</div>
// 修复后 - 正确的字段映射
<div class="device-id">主机编号: {{ device.deviceNumber || device.sid || device.hostId }}</div>
```
## ✅ 修复方案
### 1. 显示字段修复
**文件**: `src/components/SmartHost.vue` (第66行)
```javascript
// 修复前
device.sid || device.hostId
// 修复后
device.deviceNumber || device.sid || device.hostId
```
### 2. 搜索功能修复
**文件**: `src/components/SmartHost.vue` (第264行)
```javascript
// 修复前
const hostId = device.sid || device.hostId || ''
// 修复后
const hostId = device.deviceNumber || device.sid || device.hostId || ''
```
### 3. 编辑功能修复
**文件**: `src/components/SmartHost.vue` (第381行)
```javascript
// 修复前
hostId: device.sid || device.hostId
// 修复后
hostId: device.deviceNumber || device.sid || device.hostId
```
## 📊 修复验证
### 测试结果
-**主机编号显示**: 正常显示 `deviceNumber`
-**搜索功能**: 可以按主机编号搜索
-**编辑功能**: 编辑对话框正确显示主机编号
-**数据完整性**: 所有371台主机都有正确的主机编号
### 测试数据示例
```
设备 1: 2024010103
设备 2: 2072516173
设备 3: 22C0281357
设备 4: 22C0281272
设备 5: 2072515306
```
## 🔧 技术细节
### 字段优先级
```javascript
// 按优先级顺序尝试获取主机编号
device.deviceNumber || device.sid || device.hostId
```
### API字段映射
| 前端显示 | API字段 | 说明 |
|---------|---------|------|
| 主机编号 | deviceNumber | 主要字段 |
| 设备电量 | voltage | 电量百分比 |
| 设备信号 | signal | 信号强度 |
| 设备温度 | temperature | 温度值 |
| 绑带状态 | bandge_status | 连接状态 |
| 更新时间 | updateTime | 最后更新时间 |
## 🎉 修复结果
### 修复前
- ❌ 主机编号显示为空
- ❌ 搜索功能无法按主机编号搜索
- ❌ 编辑功能无法正确显示主机编号
### 修复后
- ✅ 主机编号正确显示 (如: 2024010103)
- ✅ 搜索功能正常工作
- ✅ 编辑功能正确显示主机编号
- ✅ 所有371台主机都有正确的主机编号
## 📋 相关文件
- `src/components/SmartHost.vue` - 主要修复文件
- `test-host-number-fix.js` - 修复验证脚本
- `HOST_NUMBER_FIX_REPORT.md` - 本修复报告
## 🚀 使用说明
1. **刷新页面**: 重新加载智能主机页面
2. **检查显示**: 确认主机编号正确显示
3. **测试搜索**: 尝试按主机编号搜索
4. **测试编辑**: 点击编辑按钮查看主机编号
## 🔄 维护建议
1. **字段映射**: 保持API字段与前端显示的一致性
2. **向后兼容**: 使用 `||` 操作符确保向后兼容
3. **测试验证**: 定期测试字段映射的正确性
4. **文档更新**: 及时更新API文档和前端文档
---
**🎉 主机编号显示问题已完全解决!现在所有主机都能正确显示其编号。**

View File

@@ -1,117 +0,0 @@
# 智能主机API集成实现总结
## ✅ 已完成的功能
### 1. **完全移除模拟数据**
- ✅ 移除了所有硬编码的模拟数据
- ✅ 移除了`getMockData()`方法
- ✅ 移除了API错误时的模拟数据降级
- ✅ 确保只使用真实API接口数据
### 2. **真实API集成**
- ✅ 直接调用`/api/smart-devices/hosts`接口
- ✅ 正确处理API响应结构包含`success`, `data`, `total`字段)
- ✅ 支持分页参数(`page`, `pageSize`
- ✅ 支持搜索参数(`search`
### 3. **动态数据获取**
- ✅ 主机总数使用API返回的`total`字段应该是371
- ✅ 在线/离线数量基于API返回的真实数据计算
- ✅ 分页信息完全来自API响应
- ✅ 实时更新统计数据
### 4. **分页功能**
- ✅ 完整的分页控件(上一页/下一页/页码)
- ✅ 当前页高亮显示
- ✅ 分页信息显示共X条记录第X/X页
- ✅ 智能页码显示逻辑
### 5. **搜索功能**
- ✅ 按主机编号精确搜索
- ✅ 搜索时重置到第一页
- ✅ 实时过滤结果
### 6. **错误处理**
- ✅ 详细的API调用日志
- ✅ 认证错误检测
- ✅ 网络错误处理
- ✅ 用户友好的错误提示
## 🔧 技术实现
### API服务层 (`hostService.js`)
```javascript
// 直接调用真实API无模拟数据
export const getHostDevices = async (params = {}) => {
const response = await api.get('/api/smart-devices/hosts', { params })
// 处理API响应结构
return {
data: apiData.data,
pagination: {
total: apiData.total, // 使用API返回的371
// ...
}
}
}
```
### 组件层 (`SmartHost.vue`)
```javascript
// 使用API返回的真实数据
this.totalCount = this.pagination.total || this.devices.length
this.onlineCount = this.devices.filter(device => device.isOnline).length
this.offlineCount = this.devices.filter(device => !device.isOnline).length
```
## 🚨 当前问题
### 认证问题
- ❌ API返回401未授权错误
- ❌ 需要正确的认证token才能访问
- ❌ 前端无法获取371台主机的数据
### 解决方案
1. **获取认证token**
```bash
node set-token.js
```
2. **在浏览器中设置token**
```javascript
localStorage.setItem('token', 'YOUR_ACTUAL_TOKEN')
```
3. **测试API连接**
```bash
node test-api.js
```
## 📊 预期结果
一旦解决认证问题,前端应该:
- ✅ 显示主机总数371
- ✅ 正确显示在线/离线数量
- ✅ 分页显示所有371台主机
- ✅ 搜索功能正常工作
- ✅ 编辑功能正常工作
## 🛠️ 测试工具
1. **API测试**`node test-api.js`
2. **认证测试**`node auth-test.js`
3. **Token设置**`node set-token.js`
## 📝 下一步
1. 联系后端开发者获取正确的认证信息
2. 设置认证token
3. 测试API连接
4. 验证前端显示371台主机
## 🎯 代码特点
- **无硬编码**所有数据都来自API
- **无模拟数据**:完全使用真实接口
- **统一接口**使用标准的REST API
- **动态更新**:实时获取最新数据
- **错误处理**:完善的错误处理机制

View File

@@ -1,112 +0,0 @@
# 登录页面使用说明
## 页面地址
- **登录页面**: http://localhost:8080/login
- **首页**: http://localhost:8080/
## 功能特点
### 🎨 界面设计
- **简洁美观**: 采用白色背景,绿色主题色
- **响应式设计**: 适配手机、平板、桌面等不同设备
- **状态栏**: 显示时间、信号、WiFi、电池状态
- **语言选择**: 支持简体中文和英文切换
### 🔐 登录功能
- **一键登录**: 主要登录方式,绿色按钮突出显示
- **协议同意**: 必须同意用户服务协议和隐私政策才能登录
- **其他登录方式**: 短信登录、注册账号、其它方式(预留接口)
### 🛡️ 安全机制
- **路由守卫**: 未登录用户自动跳转到登录页
- **登录状态检查**: 已登录用户访问登录页会跳转到首页
- **认证管理**: 使用localStorage存储token和用户信息
## 使用方法
### 1. 访问登录页面
```
http://localhost:8080/login
```
### 2. 登录流程
1. 勾选"我已阅读并同意《用户服务协议》及《隐私政策》"
2. 点击"一键登录"按钮
3. 系统模拟登录过程1.5秒)
4. 登录成功后自动跳转到首页
### 3. 其他功能
- **短信登录**: 点击"短信登录"(功能开发中)
- **注册账号**: 点击"注册账号"(功能开发中)
- **其它登录**: 点击"其它"(功能开发中)
- **生资监管方案**: 点击底部服务链接
- **绑定天翼账号**: 点击底部服务链接
## 技术实现
### 组件结构
```
Login.vue
├── 状态栏 (status-bar)
├── 头部导航 (header-bar)
├── 语言选择 (language-selector)
├── 主要内容 (main-content)
│ ├── 应用标题 (app-title)
│ ├── 登录区域 (login-section)
│ ├── 其他登录方式 (alternative-login)
│ ├── 底部服务 (footer-services)
│ └── 免责声明 (disclaimer)
```
### 核心功能
- **认证管理**: 集成 `@/utils/auth` 工具
- **路由守卫**: 自动处理登录状态检查
- **状态管理**: 使用Vue data管理组件状态
- **事件处理**: 完整的用户交互逻辑
### 样式特点
- **移动优先**: 响应式设计,适配各种屏幕尺寸
- **交互反馈**: 按钮悬停、点击效果
- **视觉层次**: 清晰的信息层级和视觉引导
- **品牌色彩**: 绿色主题色,符合农业应用特色
## 开发说明
### 模拟登录
当前使用模拟登录,实际项目中需要:
1. 连接真实登录API
2. 处理登录验证逻辑
3. 实现短信验证码功能
4. 添加注册功能
### 自定义配置
可以在 `Login.vue` 中修改:
- 应用名称和标题
- 主题色彩
- 登录方式
- 服务链接
- 免责声明内容
### 扩展功能
可以添加:
- 忘记密码功能
- 第三方登录微信、QQ等
- 生物识别登录
- 多语言支持
- 主题切换
## 注意事项
1. **协议同意**: 用户必须同意协议才能登录
2. **登录状态**: 登录后访问 `/login` 会自动跳转到首页
3. **退出登录**: 在"我的"页面可以退出登录
4. **路由保护**: 未登录用户无法访问其他页面
5. **数据持久化**: 登录状态保存在localStorage中
## 测试方法
1. 直接访问 http://localhost:8080/login
2. 不勾选协议,点击登录(应该提示需要同意协议)
3. 勾选协议,点击登录(应该成功登录并跳转)
4. 登录后访问 http://localhost:8080/login应该跳转到首页
5. 在"我的"页面点击"退出登录"(应该跳转到登录页)

View File

@@ -1,209 +0,0 @@
# 智能耳标页面跳转功能修复总结
## 🔍 问题分析
**问题描述**: 点击智能耳标没有跳转
**根本原因**:
1. 目标页面 `eartag-detail` 不存在
2. 缺少错误处理机制
3. 数据传递可能存在问题
## ✅ 解决方案
### 1. 创建耳标详情页面
**文件**: `pages/device/eartag-detail/`
#### 功能特性:
- ✅ 显示耳标详细信息
- ✅ 绑定/解绑牛只功能
- ✅ 编辑信息功能
- ✅ 返回上一页功能
- ✅ 加载状态处理
#### 页面结构:
```javascript
// 主要功能
- onLoad(options) // 接收耳标ID参数
- fetchEartagDetail() // 获取详情数据
- onBind() // 绑定牛只
- onUnbind() // 解绑牛只
- onEdit() // 编辑信息
- goBack() // 返回上一页
```
### 2. 创建添加耳标页面
**文件**: `pages/device/eartag-add/`
#### 功能特性:
- ✅ 表单输入验证
- ✅ API接口调用
- ✅ 成功/失败提示
- ✅ 自动返回上一页
#### 表单字段:
- 耳标编号 (必填)
- 主机号 (必填)
- 初始电量 (可选)
- 备注 (可选)
### 3. 优化点击事件处理
**文件**: `pages/device/eartag/eartag.js`
#### 改进内容:
```javascript
// 优化前
onEartagClick(e) {
const item = e.currentTarget.dataset.item
wx.navigateTo({
url: `/pages/device/eartag-detail/eartag-detail?id=${item.eartagNumber}`
})
}
// 优化后
onEartagClick(e) {
const item = e.currentTarget.dataset.item
// 数据验证
if (!item || !item.eartagNumber) {
wx.showToast({
title: '数据错误',
icon: 'none'
})
return
}
// 跳转 with 错误处理
wx.navigateTo({
url: `/pages/device/eartag-detail/eartag-detail?id=${item.eartagNumber}`,
success: () => {
console.log('跳转成功')
},
fail: (error) => {
console.error('跳转失败:', error)
wx.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
}
```
### 4. 更新页面配置
**文件**: `app.json`
#### 新增页面:
```json
{
"pages": [
"pages/device/eartag/eartag",
"pages/device/eartag-detail/eartag-detail",
"pages/device/eartag-add/eartag-add"
]
}
```
## 🎯 修复结果
### ✅ 解决的问题:
1. **页面不存在**: 创建了详情页和添加页
2. **跳转失败**: 添加了错误处理机制
3. **数据验证**: 增加了数据有效性检查
4. **用户体验**: 添加了成功/失败提示
### ✅ 新增功能:
1. **耳标详情页**: 完整的详情展示和操作
2. **添加耳标页**: 表单输入和API调用
3. **错误处理**: 完善的异常处理机制
4. **用户反馈**: 清晰的状态提示
## 📱 使用流程
### 智能耳标页面操作流程:
1. **查看列表**: 显示所有耳标数据
2. **点击耳标**: 跳转到详情页面
3. **查看详情**: 显示耳标详细信息
4. **绑定操作**: 绑定/解绑牛只
5. **编辑信息**: 修改耳标信息
6. **添加耳标**: 点击"+"按钮添加新耳标
### 跳转路径:
```
智能耳标列表页
↓ (点击耳标)
耳标详情页
↓ (点击编辑)
耳标编辑页
↓ (点击添加)
添加耳标页
```
## 🔧 技术实现
### 数据传递:
```javascript
// 列表页 → 详情页
wx.navigateTo({
url: `/pages/device/eartag-detail/eartag-detail?id=${item.eartagNumber}`
})
// 详情页接收参数
onLoad(options) {
if (options.id) {
this.setData({ eartagId: options.id })
this.fetchEartagDetail(options.id)
}
}
```
### 错误处理:
```javascript
// 数据验证
if (!item || !item.eartagNumber) {
wx.showToast({
title: '数据错误',
icon: 'none'
})
return
}
// 跳转错误处理
wx.navigateTo({
url: '...',
success: () => console.log('跳转成功'),
fail: (error) => {
console.error('跳转失败:', error)
wx.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
```
## 🚀 测试建议
### 功能测试:
1. **点击耳标**: 确认能正常跳转到详情页
2. **查看详情**: 确认数据正确显示
3. **返回功能**: 确认能正常返回列表页
4. **添加耳标**: 确认能正常跳转到添加页
5. **表单提交**: 确认能正常添加耳标
### 错误测试:
1. **数据异常**: 测试数据为空时的处理
2. **网络异常**: 测试API调用失败时的处理
3. **参数错误**: 测试参数缺失时的处理
## 📞 后续优化
1. **API集成**: 对接真实的详情和添加API
2. **数据缓存**: 优化数据加载性能
3. **离线支持**: 添加离线数据支持
4. **批量操作**: 支持批量绑定/解绑
5. **数据同步**: 实时同步数据更新
---
**修复完成**: 智能耳标页面点击跳转功能已完全修复,所有相关页面和功能都已实现!

View File

@@ -1,144 +0,0 @@
# 养殖端微信小程序底部导航栏实现总结
## 📱 项目概述
已成功实现养殖端微信小程序的底部导航栏,包含三个主要页面:**首页**、**生产管理**、**我的**。
## ✅ 完成的功能
### 1. 底部导航栏配置
- ✅ 更新 `app.json` 中的 `tabBar` 配置
- ✅ 设置三个导航页面:首页、生产管理、我的
- ✅ 配置导航栏颜色和样式
### 2. 首页页面 (pages/home/home)
- ✅ 根据图片UI样式重新设计首页
- ✅ 顶部状态栏(时间、位置、天气)
- ✅ 搜索框和扫描功能
- ✅ 数据统计卡片(总牛只数、怀孕牛只、泌乳牛只、健康牛只)
- ✅ 功能模块网格8个功能模块
- ✅ 最近活动列表
- ✅ 响应式设计
### 3. 生产管理页面 (pages/production/production)
- ✅ 创建完整的生产管理页面
- ✅ 生产数据统计卡片
- ✅ 生产管理功能模块8个模块
- ✅ 最近生产活动记录
- ✅ 与首页一致的设计风格
### 4. 我的页面 (pages/profile/profile)
- ✅ 使用现有的个人中心页面
- ✅ 保持原有功能不变
## 🎨 UI设计特点
### 首页设计亮点
1. **渐变背景**:使用现代化的渐变背景设计
2. **状态栏**:顶部显示时间、位置和天气信息
3. **搜索功能**:集成搜索框和二维码扫描功能
4. **数据可视化**:统计卡片显示趋势变化
5. **功能模块**4x2网格布局图标+文字设计
6. **活动列表**:最近活动记录,支持点击跳转
### 配色方案
- 主色调:绿色 (#3cc51f)
- 辅助色:蓝色 (#1890ff)、橙色 (#faad14)、红色 (#f5222d)
- 背景:渐变白色到浅灰色
- 文字:深灰色 (#333)、中灰色 (#666)、浅灰色 (#999)
## 📁 文件结构
```
mini_program/farm-monitor-dashboard/
├── app.json # 应用配置已更新tabBar
├── pages/
│ ├── home/ # 首页
│ │ ├── home.wxml # 页面结构
│ │ ├── home.wxss # 页面样式
│ │ └── home.js # 页面逻辑
│ ├── production/ # 生产管理页面
│ │ ├── production.wxml # 页面结构
│ │ ├── production.wxss # 页面样式
│ │ └── production.js # 页面逻辑
│ └── profile/ # 我的页面
│ ├── profile.wxml # 页面结构
│ ├── profile.wxss # 页面样式
│ └── profile.js # 页面逻辑
├── images/
│ └── ICON_REQUIREMENTS.md # 图标需求说明
└── test-navigation.js # 导航测试脚本
```
## 🔧 技术实现
### 1. 页面配置
-`app.json` 中配置了三个tabBar页面
- 设置了统一的导航栏样式和颜色
### 2. 首页功能
- 实时时间显示(每分钟更新)
- 搜索功能(支持关键词搜索)
- 二维码扫描功能
- 数据统计(支持趋势显示)
- 下拉刷新功能
### 3. 生产管理页面
- 生产数据统计
- 8个生产管理功能模块
- 最近生产活动记录
- 与首页一致的设计风格
### 4. 响应式设计
- 支持不同屏幕尺寸
- 小屏幕设备优化375px以下
- 网格布局自适应
## ⚠️ 注意事项
### 图标文件
需要创建以下图标文件(当前为占位符):
- `images/home.png` - 首页未选中图标
- `images/home-active.png` - 首页选中图标
- `images/production.png` - 生产管理未选中图标
- `images/production-active.png` - 生产管理选中图标
- `images/profile.png` - 我的未选中图标
- `images/profile-active.png` - 我的选中图标
### 图标规格
- 尺寸40x40 像素
- 格式PNG格式支持透明背景
- 颜色:未选中 #7A7E83,选中 #3cc51f
## 🚀 使用方法
1. 在微信开发者工具中打开项目
2. 确保所有页面文件存在
3. 添加所需的图标文件
4. 编译并预览小程序
5. 测试底部导航栏功能
## 📋 测试清单
- [x] 底部导航栏显示正常
- [x] 三个页面可以正常切换
- [x] 首页功能完整
- [x] 生产管理页面功能完整
- [x] 我的页面功能完整
- [x] 响应式设计正常
- [ ] 图标文件需要添加
- [ ] 实际数据接口对接
## 🎯 下一步计划
1. 添加底部导航栏图标文件
2. 对接真实的后端API接口
3. 完善搜索和扫描功能
4. 添加更多交互效果
5. 优化性能和用户体验
---
**开发完成时间**: 2024年
**开发状态**: ✅ 基础功能完成
**待办事项**: 图标文件、API对接

View File

@@ -1,121 +0,0 @@
# 网络连接错误修复说明
## 问题描述
出现错误:`API请求错误: 0 /api/smart-devices/eartags``net::ERR_CONNECTION_REFUSED`
## 问题原因
后端API服务器没有运行导致前端无法连接到 `http://localhost:5350`
## 解决方案
### 1. 启动后端服务
```bash
cd C:\nxxmdata\backend
npm start
```
### 2. 验证服务状态
```powershell
# 检查后端服务
netstat -an | findstr :5350
# 检查前端服务
netstat -an | findstr :8080
```
### 3. 测试API连接
```powershell
# 测试登录API
Invoke-WebRequest -Uri "http://localhost:5350/api/auth/login" -Method POST -Body '{"username":"admin","password":"123456"}' -ContentType "application/json"
# 测试耳标API
$response = Invoke-WebRequest -Uri "http://localhost:5350/api/auth/login" -Method POST -Body '{"username":"admin","password":"123456"}' -ContentType "application/json"
$token = ($response.Content | ConvertFrom-Json).token
Invoke-WebRequest -Uri "http://localhost:5350/api/smart-devices/eartags" -Headers @{"Authorization"="Bearer $token"} -Method GET
```
## 当前状态
### ✅ 后端服务
- **状态**: 正在运行
- **端口**: 5350
- **API**: 正常工作
- **认证**: JWT token正常
### ✅ 前端服务
- **状态**: 正在运行
- **端口**: 8080
- **代理**: 配置正确
### ✅ API测试
- **登录API**: 正常返回token
- **耳标API**: 正常返回数据
- **认证**: JWT token验证正常
## 使用方法
### 1. 访问应用
```
http://localhost:8080
```
### 2. 测试API
```
http://localhost:8080/auth-test
```
### 3. 设备管理
- 耳标设备: `http://localhost:8080/ear-tag`
- 项圈设备: `http://localhost:8080/smart-collar`
- 脚环设备: `http://localhost:8080/smart-ankle`
- 主机设备: `http://localhost:8080/smart-host`
## 故障排除
### 如果仍然出现连接错误
1. 检查后端服务是否运行:`netstat -an | findstr :5350`
2. 检查前端服务是否运行:`netstat -an | findstr :8080`
3. 重启后端服务:`cd C:\nxxmdata\backend && npm start`
4. 重启前端服务:`cd C:\nxxmdata\mini_program\farm-monitor-dashboard && npm run serve`
### 如果API返回401错误
1. 访问认证测试页面
2. 点击"设置真实Token"按钮
3. 点击"测试所有API"按钮
### 如果代理不工作
1. 检查vue.config.js配置
2. 重启前端开发服务器
3. 清除浏览器缓存
## 服务启动顺序
1. **启动后端服务**
```bash
cd C:\nxxmdata\backend
npm start
```
2. **启动前端服务**
```bash
cd C:\nxxmdata\mini_program\farm-monitor-dashboard
npm run serve
```
3. **验证服务**
- 后端: http://localhost:5350/api-docs
- 前端: http://localhost:8080
## 预期结果
- ✅ 前端可以正常访问后端API
- ✅ 所有设备API返回真实数据
- ✅ 认证系统正常工作
- ✅ 不再出现网络连接错误
## 注意事项
1. 确保两个服务都在运行
2. 后端服务必须先启动
3. 如果修改了配置,需要重启服务
4. 检查防火墙设置是否阻止了端口访问

View File

@@ -1,123 +0,0 @@
# 分页字段映射修复报告
## 问题描述
API响应分页信息为 `{page: 3, limit: 10, total: 2000, pages: 200}`但前端分页高亮显示的是第1页存在字段映射不匹配的问题。
## 问题分析
### 1. API响应字段格式
```javascript
// API实际返回的分页字段
{
page: 3, // 当前页码
limit: 10, // 每页数量
total: 2000, // 总数据量
pages: 200 // 总页数
}
```
### 2. 前端期望字段格式
```javascript
// 前端期望的分页字段
{
current: 3, // 当前页码
pageSize: 10, // 每页数量
total: 2000, // 总数据量
totalPages: 200 // 总页数
}
```
### 3. 字段映射不匹配
- `page``current`
- `limit``pageSize`
- `pages``totalPages`
- `total``total` (相同)
## 修复方案
### 1. 更新collarService.js中的字段映射
```javascript
// 确保分页信息存在并正确映射字段
if (response.data.pagination) {
// 映射API返回的分页字段到前端期望的字段
response.data.pagination = {
current: parseInt(response.data.pagination.page || response.data.pagination.current || queryParams.page) || 1,
pageSize: parseInt(response.data.pagination.limit || response.data.pagination.pageSize || queryParams.limit) || 10,
total: parseInt(response.data.pagination.total || 0) || 0,
totalPages: parseInt(response.data.pagination.pages || response.data.pagination.totalPages || 1) || 1
}
}
```
### 2. 字段映射优先级
1. **current**: `page``current``queryParams.page`
2. **pageSize**: `limit``pageSize``queryParams.limit`
3. **total**: `total``0`
4. **totalPages**: `pages``totalPages``1`
### 3. 向后兼容性
- 支持API返回 `page``current` 字段
- 支持API返回 `limit``pageSize` 字段
- 支持API返回 `pages``totalPages` 字段
- 如果API没有返回分页信息使用默认值
## 修复效果
### 修复前
- API返回: `{page: 3, limit: 10, total: 2000, pages: 200}`
- 前端显示: 分页高亮第1页 ❌
- 分页信息: "第 NaN-NaN条,共2000条" ❌
### 修复后
- API返回: `{page: 3, limit: 10, total: 2000, pages: 200}`
- 映射后: `{current: 3, pageSize: 10, total: 2000, totalPages: 200}`
- 前端显示: 分页高亮第3页 ✅
- 分页信息: "第 21-30条,共2000条" ✅
## 测试验证
### 1. 测试脚本
创建了 `test-pagination-fix.js` 测试脚本,包含:
- 原始API响应分页信息显示
- 字段映射验证
- 分页高亮正确性验证
### 2. 运行测试
```bash
cd mini_program/farm-monitor-dashboard
node test-pagination-fix.js
```
### 3. 测试覆盖
- ✅ API字段映射正确性
- ✅ 分页高亮显示正确性
- ✅ 分页信息计算正确性
- ✅ 向后兼容性
## 技术细节
### 1. 字段映射逻辑
```javascript
// 智能字段映射支持多种API响应格式
current: parseInt(response.data.pagination.page || response.data.pagination.current || queryParams.page) || 1
```
### 2. 数据类型转换
- 使用 `parseInt()` 确保数值类型
- 提供默认值防止 `NaN` 错误
- 支持字符串和数字类型转换
### 3. 错误处理
- 如果API没有返回分页信息使用默认值
- 如果字段值为空或无效,使用查询参数
- 如果所有值都无效,使用硬编码默认值
## 总结
通过修复分页字段映射问题,现在:
1. **分页高亮正确**API返回第3页时前端正确高亮第3页
2. **分页信息正确**:显示正确的"第 X-Y 条,共 Z 条"格式
3. **向后兼容**支持多种API响应格式
4. **错误处理**:提供完善的错误处理和默认值
分页功能现在可以正常工作用户界面与API数据完全同步。

View File

@@ -1,175 +0,0 @@
# 密码登录页面使用说明
## 页面地址
- **密码登录页面**: http://localhost:8080/password-login
- **一键登录页面**: http://localhost:8080/login
- **短信登录页面**: http://localhost:8080/sms-login
- **注册页面**: http://localhost:8080/register
- **首页**: http://localhost:8080/
## 功能特点
### 🎨 界面设计
- **简洁美观**: 采用白色背景,绿色主题色
- **响应式设计**: 适配手机、平板、桌面等不同设备
- **状态栏**: 显示时间、信号、WiFi、电池状态
- **语言选择**: 支持简体中文和英文切换
### 🔐 密码登录功能
- **账号输入**: 支持用户名、手机号、邮箱等格式
- **密码输入**: 支持显示/隐藏切换最少6位
- **协议同意**: 必须同意用户服务协议和隐私政策
- **实时验证**: 输入格式实时验证和错误提示
### 🔄 多种登录方式
- **一键登录**: 跳转到一键登录页面
- **短信登录**: 跳转到短信登录页面
- **注册账号**: 跳转到注册页面
- **其它方式**: 预留更多登录方式接口
## 使用方法
### 1. 访问密码登录页面
```
http://localhost:8080/password-login
```
### 2. 登录流程
1. 输入账号(用户名、手机号或邮箱)
2. 输入密码最少6位
3. 勾选"我已阅读并同意《用户服务协议》及《隐私政策》"
4. 点击"登录"按钮
5. 登录成功后自动跳转到首页
### 3. 其他功能
- **密码显示**: 点击密码框右侧眼睛图标切换显示/隐藏
- **其他登录方式**: 点击下方登录方式选项
- **服务链接**: 点击底部服务链接
## 技术实现
### 组件结构
```
PasswordLogin.vue
├── 状态栏 (status-bar)
├── 头部导航 (header-bar)
├── 语言选择 (language-selector)
├── 主要内容 (main-content)
│ ├── 应用标题 (app-title)
│ ├── 登录表单 (login-form)
│ │ ├── 账号输入框 (input-group)
│ │ ├── 密码输入框 (input-group)
│ │ └── 协议同意 (agreement-section)
│ ├── 其他登录方式 (alternative-login)
│ ├── 底部服务 (footer-services)
│ └── 免责声明 (disclaimer)
```
### 核心功能
- **表单验证**: 实时验证账号和密码格式
- **密码安全**: 支持密码显示/隐藏切换
- **协议管理**: 必须同意协议才能登录
- **状态管理**: 完整的加载和错误状态
### 登录验证
- **模拟验证**: 开发环境使用模拟验证
- **测试账号**: admin/123456开发环境
- **密码强度**: 最少6位密码要求
- **错误处理**: 详细的错误信息提示
## 开发说明
### 模拟功能
当前使用模拟登录验证,实际项目中需要:
1. 连接真实用户认证API
2. 实现密码加密传输
3. 添加记住密码功能
4. 集成单点登录(SSO)
### 验证规则
- **账号格式**: 支持用户名、手机号、邮箱
- **密码强度**: 最少6位建议包含字母和数字
- **协议同意**: 必须勾选协议才能登录
- **登录状态**: 已登录用户访问会跳转到首页
### 自定义配置
可以在 `PasswordLogin.vue` 中修改:
- 密码最小长度
- 账号格式验证
- 错误提示信息
- 样式和主题
## 测试方法
### 1. 基本功能测试
1. 访问 http://localhost:8080/password-login
2. 输入账号admin
3. 输入密码123456
4. 勾选协议同意
5. 点击"登录"
### 2. 验证测试
1. 输入空账号
2. 应该显示"请输入账号"错误
3. 输入空密码
4. 应该显示"请输入密码"错误
5. 输入短密码123
6. 应该显示"密码长度不能少于6位"错误
### 3. 协议测试
1. 不勾选协议同意
2. 登录按钮应该被禁用
3. 勾选协议同意
4. 登录按钮应该可用
## 注意事项
1. **协议同意**: 必须同意用户服务协议和隐私政策
2. **密码安全**: 支持密码显示/隐藏切换
3. **登录状态**: 已登录用户访问会跳转到首页
4. **错误处理**: 所有错误都有相应的用户提示
5. **多种登录**: 支持多种登录方式切换
## 扩展功能
可以添加的功能:
- 记住密码
- 自动登录
- 忘记密码
- 第三方登录微信、QQ、支付宝等
- 生物识别登录
- 多因素认证
- 单点登录(SSO)
## 样式特点
- **移动优先**: 响应式设计,适配各种屏幕
- **交互反馈**: 按钮状态、输入框焦点效果
- **视觉层次**: 清晰的信息层级
- **品牌一致**: 与整体应用风格保持一致
- **无障碍**: 支持键盘导航和屏幕阅读器
## 页面跳转
- **从登录页**: 点击"其它"跳转到密码登录页
- **一键登录**: 点击"一键登录"跳转到一键登录页
- **短信登录**: 点击"短信登录"跳转到短信登录页
- **注册账号**: 点击"注册账号"跳转到注册页
- **返回上一页**: 点击左上角房子图标
## 登录方式对比
| 登录方式 | 页面地址 | 特点 | 适用场景 |
|---------|---------|------|---------|
| 一键登录 | /login | 简单快速 | 首次使用 |
| 短信登录 | /sms-login | 安全可靠 | 忘记密码 |
| 密码登录 | /password-login | 传统方式 | 日常使用 |
| 注册账号 | /register | 新用户 | 首次注册 |
## 安全建议
1. **密码强度**: 建议使用强密码
2. **定期更换**: 定期更换密码
3. **安全环境**: 在安全环境下登录
4. **退出登录**: 使用完毕后及时退出
5. **协议阅读**: 仔细阅读用户协议和隐私政策

View File

@@ -1,229 +0,0 @@
# 养殖管理系统微信小程序
## 项目简介
这是一个基于微信小程序原生技术开发的养殖管理系统,用于管理牛只档案、设备监控、预警管理等养殖业务。
## 技术栈
- **框架**: 微信小程序原生开发
- **语言**: JavaScript ES6+
- **样式**: WXSS
- **模板**: WXML
- **状态管理**: 微信小程序全局数据
- **网络请求**: wx.request
- **UI组件**: 微信小程序原生组件
## 项目结构
```
farm-monitor-dashboard/
├── app.js # 小程序入口文件
├── app.json # 小程序全局配置
├── app.wxss # 小程序全局样式
├── sitemap.json # 站点地图配置
├── project.config.json # 项目配置文件
├── package.json # 项目依赖配置
├── pages/ # 页面目录
│ ├── home/ # 首页
│ │ ├── home.js
│ │ ├── home.wxml
│ │ └── home.wxss
│ ├── login/ # 登录页
│ │ ├── login.js
│ │ ├── login.wxml
│ │ └── login.wxss
│ ├── cattle/ # 牛只管理
│ │ ├── cattle.js
│ │ ├── cattle.wxml
│ │ └── cattle.wxss
│ ├── device/ # 设备管理
│ │ ├── device.js
│ │ ├── device.wxml
│ │ └── device.wxss
│ ├── alert/ # 预警中心
│ │ ├── alert.js
│ │ ├── alert.wxml
│ │ └── alert.wxss
│ └── profile/ # 个人中心
│ ├── profile.js
│ ├── profile.wxml
│ └── profile.wxss
├── services/ # 服务层
│ ├── api.js # API接口定义
│ ├── cattleService.js # 牛只管理服务
│ ├── deviceService.js # 设备管理服务
│ └── alertService.js # 预警管理服务
├── utils/ # 工具函数
│ ├── index.js # 通用工具函数
│ ├── api.js # API请求工具
│ └── auth.js # 认证工具
├── images/ # 图片资源
│ └── README.md
└── README.md # 项目说明文档
```
## 功能特性
### 1. 用户认证
- 密码登录
- 短信验证码登录
- 微信授权登录
- 自动登录状态保持
### 2. 牛只管理
- 牛只档案管理
- 牛只信息查询和搜索
- 牛只状态管理
- 牛只健康记录
- 牛只繁殖记录
- 牛只饲喂记录
### 3. 设备管理
- 智能设备监控
- 设备状态管理
- 设备配置管理
- 设备历史数据查看
- 设备实时数据监控
### 4. 预警中心
- 智能预警管理
- 预警类型分类
- 预警处理流程
- 预警统计分析
- 预警规则配置
### 5. 个人中心
- 用户信息管理
- 系统设置
- 消息通知
- 帮助中心
## 开发环境配置
### 1. 安装微信开发者工具
下载并安装 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
### 2. 导入项目
1. 打开微信开发者工具
2. 选择"导入项目"
3. 选择项目目录
4. 填写AppID测试可使用测试号
5. 点击"导入"
### 3. 配置后端API
`utils/api.js` 中修改 `baseUrl` 为实际的后端API地址
```javascript
const config = {
baseUrl: 'https://your-backend-url.com/api', // 修改为实际的后端API地址
// ...
}
```
## 开发指南
### 1. 页面开发
每个页面包含三个文件:
- `.js` - 页面逻辑
- `.wxml` - 页面结构
- `.wxss` - 页面样式
### 2. 组件开发
微信小程序支持自定义组件,可以在 `components` 目录下创建组件。
### 3. API调用
使用 `utils/api.js` 中封装的请求方法:
```javascript
const { get, post, put, del } = require('../../utils/api')
// GET请求
const data = await get('/api/endpoint', params)
// POST请求
const result = await post('/api/endpoint', data)
```
### 4. 状态管理
使用微信小程序的全局数据管理:
```javascript
// 设置全局数据
getApp().globalData.userInfo = userInfo
// 获取全局数据
const userInfo = getApp().globalData.userInfo
```
## 部署说明
### 1. 代码审核
1. 在微信开发者工具中点击"上传"
2. 填写版本号和项目备注
3. 上传代码到微信后台
### 2. 提交审核
1. 登录微信公众平台
2. 进入小程序管理后台
3. 在"版本管理"中提交审核
### 3. 发布上线
审核通过后,在版本管理页面点击"发布"即可上线。
## 注意事项
### 1. 网络请求
- 所有网络请求必须使用HTTPS
- 需要在微信公众平台配置服务器域名
- 请求超时时间建议设置为10秒
### 2. 图片资源
- 图片大小建议不超过2MB
- 支持格式JPG、PNG、GIF
- 建议使用CDN加速
### 3. 性能优化
- 合理使用分包加载
- 避免频繁的setData操作
- 使用图片懒加载
- 优化网络请求
### 4. 兼容性
- 支持微信版本7.0.0及以上
- 支持iOS 10.0和Android 5.0及以上
- 建议在真机上测试功能
## 常见问题
### 1. 网络请求失败
- 检查服务器域名是否已配置
- 确认API地址是否正确
- 检查网络连接状态
### 2. 页面显示异常
- 检查WXML语法是否正确
- 确认数据绑定是否正确
- 查看控制台错误信息
### 3. 样式问题
- 检查WXSS语法是否正确
- 确认选择器是否正确
- 注意样式优先级
## 更新日志
### v1.0.0 (2024-01-01)
- 初始版本发布
- 完成基础功能开发
- 支持牛只管理、设备监控、预警管理
## 技术支持
如有问题,请联系开发团队或查看相关文档:
- 微信小程序官方文档https://developers.weixin.qq.com/miniprogram/dev/
- 项目文档:查看项目根目录下的文档文件
## 许可证
MIT License

View File

@@ -1,162 +0,0 @@
# 注册账号页面使用说明
## 页面地址
- **注册页面**: http://localhost:8080/register
- **登录页面**: http://localhost:8080/login
- **短信登录**: http://localhost:8080/sms-login
- **首页**: http://localhost:8080/
## 功能特点
### 🎨 界面设计
- **简洁美观**: 采用白色背景,绿色主题色
- **响应式设计**: 适配手机、平板、桌面等不同设备
- **状态栏**: 显示时间、信号、WiFi、电池状态
- **返回按钮**: 左上角返回按钮,支持返回上一页
### 📝 注册功能
- **真实姓名**: 必填项,用于身份验证
- **手机号**: 必填项,支持中国大陆手机号格式
- **验证码**: 6位数字验证码60秒倒计时
- **密码**: 最少6位支持显示/隐藏切换
- **实时验证**: 输入格式实时验证和错误提示
### 🔐 安全机制
- **手机号检查**: 验证手机号是否已注册
- **验证码验证**: 短信验证码验证
- **密码强度**: 最少6位密码要求
- **重复注册检查**: 防止重复注册
- **自动登录**: 注册成功后自动登录
## 使用方法
### 1. 访问注册页面
```
http://localhost:8080/register
```
### 2. 注册流程
1. 输入真实姓名(必填)
2. 输入手机号支持1[3-9]xxxxxxxxx格式
3. 点击"发送验证码"按钮
4. 等待60秒倒计时结束
5. 输入收到的6位验证码
6. 输入密码最少6位
7. 点击"确认"按钮
8. 注册成功后自动登录并跳转到首页
### 3. 其他功能
- **已有账号登录**: 点击"已有账号?立即登录"跳转到登录页
- **密码显示**: 点击密码框右侧眼睛图标切换显示/隐藏
## 技术实现
### 组件结构
```
Register.vue
├── 状态栏 (status-bar)
├── 头部导航 (header-bar)
├── 主要内容 (main-content)
│ ├── 注册表单 (register-form)
│ │ ├── 真实姓名输入框 (input-group)
│ │ ├── 手机号输入框 (input-group)
│ │ ├── 验证码输入框 (input-group)
│ │ └── 密码输入框 (input-group)
│ └── 其他选项 (alternative-options)
```
### 核心功能
- **表单验证**: 实时验证所有输入字段
- **倒计时管理**: 60秒发送间隔保护
- **API集成**: 集成用户注册和短信服务API
- **状态管理**: 完整的加载和错误状态
### 用户服务API
- **用户注册**: `POST /api/user/register`
- **检查手机号**: `GET /api/user/check-phone/{phone}`
- **检查用户名**: `GET /api/user/check-username/{username}`
- **获取用户信息**: `GET /api/user/{userId}`
- **更新用户信息**: `PUT /api/user/{userId}`
- **修改密码**: `POST /api/user/{userId}/change-password`
- **重置密码**: `POST /api/user/reset-password`
## 开发说明
### 模拟功能
当前使用模拟注册服务,实际项目中需要:
1. 连接真实用户注册API
2. 实现用户数据存储
3. 添加邮箱验证功能
4. 集成实名认证服务
### 验证规则
- **真实姓名**: 不能为空,支持中文和英文
- **手机号**: 中国大陆手机号格式1[3-9]xxxxxxxxx
- **验证码**: 6位数字5分钟有效期
- **密码**: 最少6位支持字母数字组合
### 自定义配置
可以在 `Register.vue` 中修改:
- 密码最小长度
- 验证码长度和格式
- 错误提示信息
- 样式和主题
## 测试方法
### 1. 基本功能测试
1. 访问 http://localhost:8080/register
2. 输入真实姓名(如:张三)
3. 输入手机号13800138000
4. 点击"发送验证码"
5. 输入任意6位数字123456
6. 输入密码123456
7. 点击"确认"
### 2. 验证测试
1. 输入空真实姓名
2. 应该显示"请输入真实姓名"错误
3. 输入无效手机号123
4. 应该显示"请输入正确的手机号"错误
5. 输入短密码123
6. 应该显示"密码长度不能少于6位"错误
### 3. 重复注册测试
1. 使用已注册的手机号
2. 应该显示"该手机号已注册,请直接登录"错误
## 注意事项
1. **真实姓名**: 必须输入真实姓名,用于身份验证
2. **手机号格式**: 支持中国大陆手机号格式
3. **验证码长度**: 必须输入6位数字
4. **密码强度**: 最少6位建议包含字母和数字
5. **重复注册**: 已注册手机号不能重复注册
6. **自动登录**: 注册成功后自动登录并跳转
## 扩展功能
可以添加的功能:
- 邮箱注册选项
- 实名认证集成
- 头像上传
- 用户协议同意
- 邀请码注册
- 第三方注册微信、QQ等
- 密码强度检测
- 图形验证码
## 样式特点
- **移动优先**: 响应式设计,适配各种屏幕
- **交互反馈**: 按钮状态、输入框焦点效果
- **视觉层次**: 清晰的信息层级
- **品牌一致**: 与整体应用风格保持一致
- **无障碍**: 支持键盘导航和屏幕阅读器
## 页面跳转
- **从登录页**: 点击"注册账号"跳转到注册页
- **从短信登录页**: 点击"注册账号"跳转到注册页
- **返回上一页**: 点击左上角返回按钮
- **已有账号**: 点击"已有账号?立即登录"跳转到登录页

View File

@@ -1,107 +0,0 @@
# 路由守卫修复说明
## 问题描述
出现错误:`Redirected when going from "/password-login" to "/" via a navigation guard.`
这是由于Vue Router的导航守卫导致的无限重定向循环。
## 问题原因
1. 用户从密码登录页面跳转到首页
2. 路由守卫检测到用户已登录
3. 路由守卫重定向到首页
4. 形成无限循环
## 修复方案
### 1. 优化路由守卫逻辑
```javascript
// 如果是从登录页面跳转到首页,且用户已登录,直接允许访问
if (from.path && isLoginPage && to.path === '/' && isAuthenticated) {
console.log('允许从登录页跳转到首页')
next()
return
}
```
### 2. 修复异步token设置
```javascript
// 确保token设置完成后再跳转
await auth.setTestToken()
auth.setUserInfo(userInfo)
// 延迟跳转确保token设置完成
setTimeout(() => {
this.$router.push('/')
}, 100)
```
### 3. 添加调试日志
```javascript
console.log('路由守卫:', {
from: from.path,
to: to.path,
requiresAuth,
isLoginPage,
isAuthenticated
})
```
## 修复的文件
### 1. `src/router/index.js`
- 添加了特殊处理逻辑,允许从登录页跳转到首页
- 添加了详细的调试日志
- 优化了路由守卫的条件判断
### 2. `src/components/PasswordLogin.vue`
- 修复了异步token设置问题
- 添加了延迟跳转机制
### 3. `src/components/SmsLogin.vue`
- 修复了异步token设置问题
### 4. `src/components/Register.vue`
- 修复了异步token设置问题
- 添加了延迟跳转机制
## 测试方法
1. 访问 `http://localhost:8080/password-login`
2. 输入任意账号和密码admin/123456
3. 点击登录按钮
4. 应该能正常跳转到首页,不再出现重定向错误
## 预期结果
- ✅ 登录成功后正常跳转到首页
- ✅ 不再出现路由重定向错误
- ✅ 控制台显示详细的路由守卫日志
- ✅ 所有登录方式都能正常工作
## 调试信息
在浏览器控制台中可以看到:
```
路由守卫: {
from: "/password-login",
to: "/",
requiresAuth: true,
isLoginPage: true,
isAuthenticated: true
}
允许从登录页跳转到首页
```
## 注意事项
1. 确保所有登录页面都使用 `await auth.setTestToken()`
2. 跳转前添加适当的延迟
3. 路由守卫的逻辑要避免循环重定向
4. 添加足够的调试日志帮助排查问题
## 如果问题仍然存在
1. 清除浏览器缓存和localStorage
2. 检查控制台是否有其他错误
3. 确认token设置是否成功
4. 查看路由守卫的详细日志

View File

@@ -1,175 +0,0 @@
# 智能耳标搜索功能说明
## 功能概述
智能耳标搜索功能提供了精确和模糊两种搜索模式,支持根据耳标编号进行快速查找。
## 搜索特性
### 1. 精确搜索
- **触发条件**: 输入完整的耳标编号2105517333
- **搜索逻辑**: 完全匹配 `cid` 字段
- **结果**: 返回唯一匹配的设备
### 2. 模糊搜索
- **触发条件**: 输入部分耳标编号或其他字段
- **搜索字段**:
- `cid` (耳标编号)
- `earTagId` (备用耳标编号)
- `collectedHost` (被采集主机)
- `sid` (备用采集主机)
- **结果**: 返回包含搜索关键词的所有设备
## 用户界面
### 搜索区域
```
┌─────────────────────────────────────┐
│ 🔍 [搜索框] [+] │
└─────────────────────────────────────┘
```
### 搜索状态显示
```
┌─────────────────────────────────────┐
│ 搜索中... [清除搜索] │
└─────────────────────────────────────┘
```
## 技术实现
### 1. 搜索状态管理
```javascript
data() {
return {
searchQuery: '', // 搜索关键词
isSearching: false, // 是否正在搜索
searchResults: [], // 搜索结果
originalDevices: [], // 原始设备数据
searchTimeout: null // 搜索延迟定时器
}
}
```
### 2. 实时搜索
```javascript
handleSearchInput() {
// 500ms延迟避免频繁搜索
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.performSearch()
}, 500)
}
```
### 3. 精确搜索逻辑
```javascript
// 精确匹配耳标编号
const exactMatch = this.originalDevices.find(device =>
device.cid && device.cid.toString() === searchQuery
)
if (exactMatch) {
this.searchResults = [exactMatch]
console.log('找到精确匹配的耳标:', exactMatch.cid)
}
```
### 4. 模糊搜索逻辑
```javascript
// 模糊搜索多个字段
this.searchResults = this.originalDevices.filter(device =>
(device.cid && device.cid.toString().includes(searchQuery)) ||
(device.earTagId && device.earTagId.includes(searchQuery)) ||
(device.collectedHost && device.collectedHost.includes(searchQuery)) ||
(device.sid && device.sid.includes(searchQuery))
)
```
## 搜索流程
### 1. 用户输入
- 用户在搜索框中输入关键词
- 系统检测到输入变化
### 2. 延迟搜索
- 等待500ms避免频繁搜索
- 如果用户继续输入,取消之前的搜索
### 3. 执行搜索
- 检查输入是否为空
- 如果为空,清除搜索状态
- 如果不为空,执行搜索逻辑
### 4. 显示结果
- 精确匹配:显示单个设备
- 模糊匹配:显示多个设备
- 无匹配:显示空列表
### 5. 清除搜索
- 点击"清除搜索"按钮
- 恢复原始设备列表
- 重置分页状态
## 搜索优化
### 1. 性能优化
- **延迟搜索**: 500ms延迟避免频繁请求
- **本地搜索**: 基于已加载的数据进行搜索
- **状态缓存**: 保存原始数据避免重复加载
### 2. 用户体验
- **实时反馈**: 搜索状态实时显示
- **键盘支持**: 支持Enter键触发搜索
- **一键清除**: 快速清除搜索状态
### 3. 错误处理
- **空输入处理**: 自动清除搜索状态
- **异常捕获**: 搜索失败时显示错误信息
- **状态恢复**: 搜索失败时恢复原始状态
## 使用示例
### 精确搜索
1. 在搜索框输入:`2105517333`
2. 系统找到精确匹配的设备
3. 显示该设备的详细信息
### 模糊搜索
1. 在搜索框输入:`210551`
2. 系统找到所有包含该数字的设备
3. 显示匹配的设备列表
### 清除搜索
1. 点击"清除搜索"按钮
2. 恢复显示所有设备
3. 重置分页到第一页
## 技术细节
### 数据流
```
用户输入 → 延迟处理 → 精确搜索 → 模糊搜索 → 显示结果
清除搜索 ← 恢复原始数据 ← 重置状态
```
### 状态管理
- `isSearching`: 控制搜索状态显示
- `searchResults`: 存储搜索结果
- `originalDevices`: 保存原始数据
- `searchTimeout`: 管理搜索延迟
### 事件处理
- `@input`: 实时输入处理
- `@keyup.enter`: 回车键搜索
- `@click`: 按钮点击事件
## 扩展功能
### 未来可扩展的搜索功能
1. **高级搜索**: 支持多条件组合搜索
2. **搜索历史**: 保存常用搜索关键词
3. **搜索建议**: 输入时显示搜索建议
4. **搜索过滤**: 按设备状态、类型等过滤
5. **搜索统计**: 显示搜索结果统计信息

View File

@@ -1,141 +0,0 @@
# 智能耳标预警功能实现说明
## 功能概述
基于PC端 `SmartEartagAlert.vue` 的分析,在微信小程序端实现了完整的智能耳标预警功能,包括预警展示、筛选、搜索、处理等功能。
## 实现文件
### 1. 核心组件
- `src/components/SmartEartagAlert.vue` - 主要预警组件
- `src/views/SmartEartagAlertPage.vue` - 预警页面包装器
- `src/components/AlertTest.vue` - 功能测试组件
### 2. 服务层
- `src/services/alertService.js` - 预警相关API服务
### 3. 路由配置
-`src/router/index.js` 中添加了预警页面路由
### 4. 导航集成
-`src/components/Home.vue` 中添加了预警功能入口
## 主要功能
### 1. 预警展示
- **统计卡片**: 显示总预警数、严重预警、一般预警、已处理数量
- **预警列表**: 展示预警详情包括设备ID、预警内容、级别、状态等
- **分页功能**: 支持分页浏览大量预警数据
### 2. 筛选和搜索
- **级别筛选**: 按严重、一般、信息级别筛选
- **状态筛选**: 按未处理、已处理状态筛选
- **关键词搜索**: 支持按设备ID或预警内容搜索
### 3. 预警处理
- **详情查看**: 点击预警查看详细信息
- **状态更新**: 支持将预警标记为已处理
- **批量操作**: 支持批量处理预警API已准备
### 4. 实时功能
- **自动刷新**: 30秒自动刷新预警数据
- **手动刷新**: 支持手动刷新数据
- **刷新控制**: 可开启/关闭自动刷新
### 5. 响应式设计
- **移动端优化**: 针对手机屏幕优化的界面布局
- **触摸友好**: 适合触摸操作的按钮和交互
## 技术特点
### 1. 数据管理
- 使用Vue 2 Options API
- 响应式数据绑定
- 计算属性优化性能
### 2. 状态管理
- 本地状态管理
- 筛选状态持久化
- 分页状态管理
### 3. 用户体验
- 加载状态提示
- 空数据状态展示
- 错误处理机制
### 4. 样式设计
- 现代化UI设计
- 卡片式布局
- 颜色编码预警级别
- 响应式布局
## API接口设计
### 预警管理
- `GET /smart-eartag-alerts` - 获取预警列表
- `GET /smart-eartag-alerts/:id` - 获取预警详情
- `PUT /smart-eartag-alerts/:id/resolve` - 处理预警
- `DELETE /smart-eartag-alerts/:id` - 删除预警
### 批量操作
- `PUT /smart-eartag-alerts/batch-resolve` - 批量处理预警
### 统计分析
- `GET /smart-eartag-alerts/stats` - 获取预警统计
### 设备相关
- `GET /smart-eartag-alerts/device/:deviceId` - 获取设备预警历史
### 规则管理
- `GET /smart-eartag-alerts/rules` - 获取预警规则
- `POST /smart-eartag-alerts/rules` - 创建预警规则
- `PUT /smart-eartag-alerts/rules/:id` - 更新预警规则
- `DELETE /smart-eartag-alerts/rules/:id` - 删除预警规则
## 使用方式
### 1. 访问预警页面
- 在首页点击"智能耳标预警"按钮
- 或直接访问 `/smart-eartag-alert` 路由
### 2. 功能测试
- 访问 `/alert-test` 路由进行功能测试
- 测试各种API调用和功能
### 3. 预警处理流程
1. 查看预警列表
2. 使用筛选和搜索功能
3. 点击预警查看详情
4. 处理预警或标记为已处理
## 模拟数据
当前使用模拟数据进行功能演示,包括:
- 体温异常预警
- 活动量异常预警
- 设备离线预警
- 位置异常预警
## 后续优化
### 1. API集成
- 替换模拟数据为真实API调用
- 添加错误处理和重试机制
- 优化数据加载性能
### 2. 功能增强
- 添加预警规则配置
- 实现推送通知
- 添加数据导出功能
### 3. 性能优化
- 虚拟滚动处理大量数据
- 缓存机制减少API调用
- 懒加载优化首屏性能
## 注意事项
1. 当前使用模拟数据需要根据实际API调整数据结构
2. 自动刷新功能默认开启,可根据需要调整刷新间隔
3. 分页大小可根据实际需求调整
4. 样式可根据设计规范进一步优化

View File

@@ -1,139 +0,0 @@
# 短信登录页面使用说明
## 页面地址
- **短信登录页面**: http://localhost:8080/sms-login
- **普通登录页面**: http://localhost:8080/login
- **首页**: http://localhost:8080/
## 功能特点
### 🎨 界面设计
- **简洁美观**: 采用白色背景,绿色主题色
- **响应式设计**: 适配手机、平板、桌面等不同设备
- **状态栏**: 显示时间、信号、WiFi、电池状态
- **返回按钮**: 左上角返回按钮,支持返回上一页
### 📱 短信登录功能
- **账号输入**: 支持手机号或用户名登录
- **验证码发送**: 60秒倒计时防止频繁发送
- **实时验证**: 输入格式实时验证
- **错误提示**: 详细的错误信息提示
### 🔐 安全机制
- **手机号验证**: 检查手机号是否已注册
- **验证码验证**: 6位数字验证码验证
- **倒计时保护**: 防止频繁发送验证码
- **路由保护**: 未登录用户自动跳转
## 使用方法
### 1. 访问短信登录页面
```
http://localhost:8080/sms-login
```
### 2. 登录流程
1. 输入手机号或账号支持手机号格式1[3-9]xxxxxxxxx
2. 点击"发送验证码"按钮
3. 等待60秒倒计时结束
4. 输入收到的6位验证码
5. 点击"登录"按钮
6. 登录成功后自动跳转到首页
### 3. 其他功能
- **密码登录**: 点击"密码登录"跳转到普通登录页
- **注册账号**: 点击"注册账号"(功能开发中)
## 技术实现
### 组件结构
```
SmsLogin.vue
├── 状态栏 (status-bar)
├── 头部导航 (header-bar)
├── 主要内容 (main-content)
│ ├── 应用标题 (app-title)
│ ├── 登录表单 (login-form)
│ │ ├── 账号输入框 (input-group)
│ │ └── 验证码输入框 (input-group)
│ └── 其他登录方式 (alternative-login)
```
### 核心功能
- **表单验证**: 实时验证输入格式
- **倒计时管理**: 60秒发送间隔保护
- **API集成**: 集成短信服务API
- **状态管理**: 完整的加载和错误状态
### 短信服务API
- **发送验证码**: `POST /api/sms/send`
- **验证验证码**: `POST /api/sms/verify`
- **检查手机号**: `GET /api/user/check-phone/{phone}`
- **获取发送记录**: `GET /api/sms/history/{phone}`
## 开发说明
### 模拟功能
当前使用模拟短信服务,实际项目中需要:
1. 连接真实短信服务商(如阿里云、腾讯云)
2. 实现验证码存储和过期机制
3. 添加发送频率限制
4. 集成用户注册检查
### 验证码规则
- **长度**: 6位数字
- **有效期**: 5分钟
- **发送间隔**: 60秒
- **格式验证**: 手机号格式验证
### 自定义配置
可以在 `SmsLogin.vue` 中修改:
- 验证码长度和格式
- 倒计时时间
- 错误提示信息
- 样式和主题
## 测试方法
### 1. 基本功能测试
1. 访问 http://localhost:8080/sms-login
2. 输入手机号13800138000
3. 点击"发送验证码"
4. 输入任意6位数字123456
5. 点击"登录"
### 2. 验证测试
1. 输入无效手机号123
2. 应该显示格式错误提示
3. 输入空验证码
4. 应该显示验证码错误提示
### 3. 倒计时测试
1. 发送验证码后
2. 按钮应该显示"60s后重发"
3. 倒计时结束后恢复"发送验证码"
## 注意事项
1. **手机号格式**: 支持中国大陆手机号格式
2. **验证码长度**: 必须输入6位数字
3. **发送间隔**: 60秒内不能重复发送
4. **登录状态**: 已登录用户访问会跳转到首页
5. **错误处理**: 所有错误都有相应的用户提示
## 扩展功能
可以添加的功能:
- 图形验证码
- 语音验证码
- 国际手机号支持
- 记住手机号
- 自动填充验证码
- 生物识别登录
## 样式特点
- **移动优先**: 响应式设计,适配各种屏幕
- **交互反馈**: 按钮状态、输入框焦点效果
- **视觉层次**: 清晰的信息层级
- **品牌一致**: 与整体应用风格保持一致

View File

@@ -1,60 +0,0 @@
# Vue模板语法错误修复
## 问题描述
编译时出现Vue模板语法错误
```
SyntaxError: Unexpected token (1:7333)
```
## 问题原因
Vue 2不支持ES2020的可选链操作符(`?.`),在模板中使用了:
```vue
{{ selectedDevice?.eartagNumber || selectedDevice?.earTagId }}
```
## 解决方案
将可选链操作符替换为Vue 2兼容的语法
### 修复前
```vue
<p>确定要删除设备 "{{ selectedDevice?.eartagNumber || selectedDevice?.earTagId }}" </p>
```
### 修复后
```vue
<p>确定要删除设备 "{{ (selectedDevice && selectedDevice.eartagNumber) || (selectedDevice && selectedDevice.earTagId) || '未知设备' }}" </p>
```
## 技术说明
### Vue 2兼容性
- Vue 2使用较旧的JavaScript语法解析器
- 不支持ES2020的可选链操作符(`?.`)
- 不支持空值合并操作符(`??`)
### 替代方案
使用逻辑与操作符(`&&`)进行安全的属性访问:
```javascript
// 不兼容Vue 2
obj?.prop?.subprop
// Vue 2兼容
obj && obj.prop && obj.prop.subprop
```
## 验证结果
- ✅ 语法错误已修复
- ✅ 前端服务正常启动
- ✅ 编译成功无错误
- ✅ 功能正常工作
## 预防措施
1. 在Vue 2项目中避免使用ES2020+语法
2. 使用Babel转译器处理现代JavaScript语法
3. 在模板中使用Vue 2兼容的表达式
4. 定期检查编译错误和警告
## 相关文件
- `src/components/EarTag.vue` - 修复的文件
- Vue 2.6.14 - 当前使用的Vue版本
- vue-template-compiler - 模板编译器版本

View File

@@ -1,141 +0,0 @@
# Token错误修复说明
## 问题描述
出现错误:`无法通过API获取测试token使用模拟token: Cannot read properties of undefined (reading 'token')`
## 问题原因
1. API响应结构与预期不符
2. 前端代理可能没有正确工作
3. 网络连接问题
## 修复方案
### 1. 修复API响应处理
```javascript
// 修复前
if (response.success && response.data.token) {
// 修复后
if (response && response.success && response.token) {
```
### 2. 添加现有token检查
```javascript
// 首先检查是否已经有有效的token
const existingToken = this.getToken()
if (existingToken && existingToken.startsWith('eyJ')) {
console.log('使用现有JWT token')
return existingToken
}
```
### 3. 添加手动设置真实token的方法
```javascript
setRealToken() {
const realToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
this.setToken(realToken)
return realToken
}
```
### 4. 增强错误处理
- 添加详细的日志输出
- 优雅处理API调用失败
- 提供备用方案
## 使用方法
### 方法1使用认证测试页面
1. 访问 `http://localhost:8080/auth-test`
2. 点击"设置真实Token"按钮
3. 点击"测试所有API"按钮
### 方法2手动设置token
在浏览器控制台中执行:
```javascript
// 设置真实token
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJpYXQiOjE3NTgxODM3NjEsImV4cCI6MTc1ODI3MDE2MX0.J3DD78bULP1pe5DMF2zbQEMFzeytV6uXgOuDIKOPww0')
// 设置用户信息
localStorage.setItem('userInfo', JSON.stringify({
id: 'user-001',
name: '爱农智慧牧场用户',
account: 'admin',
role: 'user'
}))
```
### 方法3直接访问API获取token
```powershell
$response = Invoke-WebRequest -Uri "http://localhost:5350/api/auth/login" -Method POST -Body '{"username":"admin","password":"123456"}' -ContentType "application/json"
$token = ($response.Content | ConvertFrom-Json).token
Write-Host "Token: $token"
```
## 测试步骤
1. 清除浏览器缓存和localStorage
2. 访问认证测试页面
3. 点击"设置真实Token"
4. 点击"测试所有API"
5. 查看测试结果
## 预期结果
- ✅ 成功设置真实JWT token
- ✅ 所有API调用成功
- ✅ 获取到真实的设备数据
- ✅ 不再出现token相关错误
## 故障排除
### 如果仍然出现错误
1. 检查后端服务是否运行在 `http://localhost:5350`
2. 检查前端代理配置
3. 清除浏览器缓存
4. 使用手动设置token的方法
### 如果API调用失败
1. 检查网络连接
2. 检查CORS设置
3. 查看浏览器控制台错误
4. 使用直接API调用测试
## 技术细节
### API响应结构
```json
{
"success": true,
"message": "登录成功",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com"
}
}
```
### Token格式
- JWT token以 `eyJ` 开头
- 包含用户ID、用户名、邮箱等信息
- 有效期为24小时
### 代理配置
```javascript
// vue.config.js
proxy: {
'/api': {
target: 'http://localhost:5350',
changeOrigin: true
}
}
```
## 下一步
1. 修复前端代理问题
2. 实现token自动刷新
3. 添加更好的错误处理
4. 优化用户体验

View File

@@ -4,7 +4,7 @@ App({
version: '1.0.0',
platform: 'wechat',
isDevelopment: true,
baseUrl: 'https://your-backend-url.com/api', // 请替换为实际的后端API地址
baseUrl: 'https://ad.ningmuyun.com', // 请替换为实际的后端API地址
userInfo: null,
token: null
},

View File

@@ -7,6 +7,7 @@
"pages/cattle/cattle",
"pages/device/device",
"pages/device/eartag/eartag",
"pages/device/collar/collar",
"pages/device/eartag-detail/eartag-detail",
"pages/device/eartag-add/eartag-add",
"pages/alert/alert"
@@ -50,4 +51,4 @@
},
"requiredBackgroundModes": ["location"],
"sitemapLocation": "sitemap.json"
}
}

View File

@@ -1,83 +0,0 @@
// 认证测试脚本 - 帮助获取正确的认证信息
const axios = require('axios')
async function testAuthMethods() {
const baseURL = process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350'
console.log('🔐 开始测试认证方法...')
console.log('API地址:', baseURL)
// 测试1: 无认证访问
console.log('\n1. 测试无认证访问...')
try {
const response = await axios.get(`${baseURL}/api/smart-devices/hosts`, {
params: { page: 1, pageSize: 10 },
timeout: 10000
})
console.log('✅ 无认证访问成功!')
console.log('响应:', response.data)
} catch (error) {
console.log('❌ 无认证访问失败:', error.response?.status, error.response?.data?.message)
}
// 测试2: 尝试不同的认证头
const authMethods = [
{ name: 'Bearer Token', header: 'Authorization', value: 'Bearer test-token' },
{ name: 'API Key', header: 'X-API-Key', value: 'test-api-key' },
{ name: 'Basic Auth', header: 'Authorization', value: 'Basic dGVzdDp0ZXN0' },
{ name: 'Custom Token', header: 'X-Auth-Token', value: 'test-token' }
]
for (const method of authMethods) {
console.log(`\n2. 测试${method.name}...`)
try {
const response = await axios.get(`${baseURL}/api/smart-devices/hosts`, {
params: { page: 1, pageSize: 10 },
headers: { [method.header]: method.value },
timeout: 10000
})
console.log(`${method.name}成功!`)
console.log('响应:', response.data)
break
} catch (error) {
console.log(`${method.name}失败:`, error.response?.status, error.response?.data?.message)
}
}
// 测试3: 检查是否有登录接口
console.log('\n3. 检查登录接口...')
const loginEndpoints = [
'/api/auth/login',
'/api/login',
'/api/user/login',
'/api/authenticate',
'/login'
]
for (const endpoint of loginEndpoints) {
try {
const response = await axios.post(`${baseURL}${endpoint}`, {
username: 'test',
password: 'test'
}, { timeout: 5000 })
console.log(`✅ 找到登录接口: ${endpoint}`)
console.log('响应:', response.data)
break
} catch (error) {
console.log(`${endpoint}:`, error.response?.status)
}
}
console.log('\n💡 建议:')
console.log('1. 检查后端API文档了解正确的认证方式')
console.log('2. 联系后端开发者获取测试用的认证信息')
console.log('3. 检查是否有公开的API端点不需要认证')
console.log('4. 确认API是否需要特定的请求头或参数')
}
// 运行测试
if (require.main === module) {
testAuthMethods().catch(console.error)
}
module.exports = { testAuthMethods }

View File

@@ -1,85 +0,0 @@
// 自动登录脚本 - 解决认证问题
const axios = require('axios')
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350'
async function autoLogin() {
console.log('🔐 开始自动登录解决认证问题...')
console.log('API地址:', API_BASE_URL)
try {
// 尝试登录
console.log('\n1. 尝试登录...')
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login`, {
username: 'admin',
password: '123456'
})
if (loginResponse.data.success) {
const token = loginResponse.data.token
console.log('✅ 登录成功!')
console.log('Token:', token.substring(0, 20) + '...')
console.log('用户:', loginResponse.data.user.username)
console.log('角色:', loginResponse.data.role.name)
// 测试智能主机API
console.log('\n2. 测试智能主机API...')
const hostResponse = await axios.get(`${API_BASE_URL}/api/smart-devices/hosts`, {
headers: { Authorization: `Bearer ${token}` },
params: { page: 1, pageSize: 10 }
})
if (hostResponse.data.success) {
console.log('✅ 智能主机API成功!')
console.log('主机总数:', hostResponse.data.total)
console.log('当前页数据:', hostResponse.data.data.length, '条')
}
// 测试智能耳标API
console.log('\n3. 测试智能耳标API...')
const earTagResponse = await axios.get(`${API_BASE_URL}/api/iot-jbq-client`, {
headers: { Authorization: `Bearer ${token}` },
params: { page: 1, pageSize: 10 }
})
if (earTagResponse.data.success) {
console.log('✅ 智能耳标API成功!')
console.log('耳标总数:', earTagResponse.data.pagination.total)
console.log('当前页数据:', earTagResponse.data.data.length, '条')
}
console.log('\n🎉 认证问题已解决!')
console.log('\n📋 前端设置步骤:')
console.log('1. 打开浏览器开发者工具 (F12)')
console.log('2. 在控制台中执行以下代码:')
console.log(`localStorage.setItem('token', '${token}')`)
console.log('3. 刷新页面')
console.log('\n💡 或者运行以下命令设置token:')
console.log(`node set-token.js`)
return token
} else {
console.log('❌ 登录失败:', loginResponse.data.message)
}
} catch (error) {
console.error('❌ 自动登录失败:')
if (error.response) {
console.error('状态码:', error.response.status)
console.error('错误信息:', error.response.data)
} else {
console.error('网络错误:', error.message)
}
}
}
// 运行自动登录
if (require.main === module) {
autoLogin().then(token => {
if (token) {
console.log('\n✅ 认证问题解决完成!')
console.log('现在前端应该能正常访问所有API了。')
}
}).catch(console.error)
}
module.exports = { autoLogin }

View File

@@ -1,47 +0,0 @@
// 检查后端服务是否运行
const axios = require('axios');
async function checkBackend() {
const baseURL = 'http://localhost:5350/api';
console.log('检查后端服务...');
console.log('基础URL:', baseURL);
try {
// 测试基础连接
console.log('\n1. 测试基础连接...');
const response = await axios.get(`${baseURL}/cattle-type`, {
timeout: 5000
});
console.log('✅ 基础连接成功');
console.log('状态码:', response.status);
console.log('响应数据:', response.data);
// 测试牛只档案API
console.log('\n2. 测试牛只档案API...');
const cattleResponse = await axios.get(`${baseURL}/iot-cattle/public`, {
params: { page: 1, pageSize: 5 },
timeout: 5000
});
console.log('✅ 牛只档案API成功');
console.log('状态码:', cattleResponse.status);
console.log('响应数据:', cattleResponse.data);
} catch (error) {
console.error('❌ 后端服务检查失败');
if (error.code === 'ECONNREFUSED') {
console.error('错误: 无法连接到后端服务');
console.error('请确保后端服务在 http://localhost:5350 运行');
console.error('启动命令: cd backend && npm start');
} else if (error.response) {
console.error('错误: 后端返回错误');
console.error('状态码:', error.response.status);
console.error('错误信息:', error.response.data);
} else {
console.error('错误:', error.message);
}
}
}
checkBackend();

View File

@@ -1,70 +0,0 @@
// 在浏览器控制台中运行此脚本来调试API调用
// 1. 测试登录
async function testLogin() {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: '123456'
})
});
const data = await response.json();
console.log('登录结果:', data);
if (data.success) {
localStorage.setItem('token', data.token);
console.log('Token已保存到localStorage');
return data.token;
}
} catch (error) {
console.error('登录失败:', error);
}
}
// 2. 测试转栏记录API
async function testTransferRecords() {
const token = localStorage.getItem('token');
if (!token) {
console.log('请先运行 testLogin() 获取token');
return;
}
try {
const response = await fetch('/api/cattle-transfer-records?page=1&pageSize=10', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log('转栏记录结果:', data);
if (data.success) {
console.log('记录数量:', data.data.list.length);
console.log('第一条记录:', data.data.list[0]);
}
} catch (error) {
console.error('获取转栏记录失败:', error);
}
}
// 3. 完整测试流程
async function fullTest() {
console.log('开始完整测试...');
await testLogin();
await testTransferRecords();
}
// 运行测试
console.log('调试脚本已加载,请运行以下命令:');
console.log('1. testLogin() - 测试登录');
console.log('2. testTransferRecords() - 测试转栏记录');
console.log('3. fullTest() - 完整测试流程');

View File

@@ -1,50 +0,0 @@
# 底部导航栏图标要求
## 需要的图标文件
### 首页图标
- `home.png` - 首页未选中状态图标
- `home-active.png` - 首页选中状态图标
### 生产管理图标
- `production.png` - 生产管理未选中状态图标
- `production-active.png` - 生产管理选中状态图标
### 我的图标
- `profile.png` - 我的未选中状态图标
- `profile-active.png` - 我的选中状态图标
## 图标规格要求
- 尺寸:建议 40x40 像素
- 格式PNG格式支持透明背景
- 颜色:
- 未选中状态:#7A7E83 (灰色)
- 选中状态:#3cc51f (绿色)
## 图标设计建议
### 首页图标
- 使用房子或仪表板图标
- 简洁的线条风格
### 生产管理图标
- 使用牛只、工厂或齿轮图标
- 体现生产管理功能
### 我的图标
- 使用用户头像或人形图标
- 简洁明了
## 临时解决方案
如果暂时没有图标文件,可以:
1. 使用微信小程序默认图标
2. 创建简单的SVG图标并转换为PNG
3. 从图标库下载免费图标
## 注意事项
- 确保图标在不同设备上显示清晰
- 保持图标风格一致
- 测试在不同背景色下的显示效果

View File

@@ -1,57 +0,0 @@
# 图标文件说明
## 当前状态
由于无法直接创建图片文件,这里提供图标获取和创建的指导。
## 需要的图标文件
### 底部导航栏图标
1. **home.png** - 首页未选中状态
2. **home-active.png** - 首页选中状态
3. **production.png** - 生产管理未选中状态
4. **production-active.png** - 生产管理选中状态
5. **profile.png** - 我的未选中状态
6. **profile-active.png** - 我的选中状态
## 图标规格要求
- **尺寸**: 40x40 像素
- **格式**: PNG格式支持透明背景
- **颜色**:
- 未选中: #7A7E83 (灰色)
- 选中: #3cc51f (绿色)
## 获取图标的方法
### 方法1: 使用图标库
- [Iconfont](https://www.iconfont.cn/) - 阿里巴巴矢量图标库
- [Feather Icons](https://feathericons.com/) - 简洁的线性图标
- [Heroicons](https://heroicons.com/) - 现代UI图标
### 方法2: 使用设计工具
- Figma
- Sketch
- Adobe Illustrator
- Canva
### 方法3: 临时解决方案
在微信开发者工具中,可以暂时使用默认图标或文字替代。
## 推荐的图标设计
### 首页图标
- 使用房子🏠或仪表板📊图标
- 简洁的线条风格
### 生产管理图标
- 使用工厂🏭、齿轮⚙️或牛只🐄图标
- 体现生产管理功能
### 我的图标
- 使用用户头像👤或人形图标
- 简洁明了
## 注意事项
- 确保图标在不同设备上显示清晰
- 保持图标风格一致
- 测试在不同背景色下的显示效果
- 考虑无障碍访问需求

View File

@@ -1,95 +0,0 @@
// 模拟API服务器 - 用于测试前端显示
const express = require('express')
const cors = require('cors')
const app = express()
const PORT = 5351 // 使用不同端口避免冲突
app.use(cors())
app.use(express.json())
// 模拟371台主机数据
const generateMockHosts = (page = 1, pageSize = 10) => {
const totalHosts = 371
const startIndex = (page - 1) * pageSize
const endIndex = Math.min(startIndex + pageSize, totalHosts)
const hosts = []
for (let i = startIndex; i < endIndex; i++) {
const hostId = `2490246${String(426 + i).padStart(3, '0')}`
hosts.push({
hostId: hostId,
sid: hostId,
isOnline: Math.random() > 0.3, // 70% 在线概率
battery: Math.floor(Math.random() * 40) + 60, // 60-100%
voltage: Math.floor(Math.random() * 40) + 60,
signal: Math.floor(Math.random() * 50) + 10, // 10-60%
signa: Math.floor(Math.random() * 50) + 10,
temperature: (Math.random() * 10 + 20).toFixed(1), // 20-30°C
state: Math.random() > 0.2 ? 1 : 0, // 80% 连接状态
bandge_status: Math.random() > 0.2 ? 1 : 0,
updateTime: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-'),
lastUpdateTime: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-')
})
}
return {
success: true,
data: hosts,
total: totalHosts,
page: page,
pageSize: pageSize,
totalPages: Math.ceil(totalHosts / pageSize),
message: '获取智能主机列表成功'
}
}
// API路由
app.get('/api/smart-devices/hosts', (req, res) => {
const page = parseInt(req.query.page) || 1
const pageSize = parseInt(req.query.pageSize) || 10
const search = req.query.search || ''
console.log(`📡 API请求: page=${page}, pageSize=${pageSize}, search=${search}`)
let response = generateMockHosts(page, pageSize)
// 如果有搜索条件,过滤数据
if (search) {
response.data = response.data.filter(host =>
host.hostId.includes(search) || host.sid.includes(search)
)
response.total = response.data.length
response.totalPages = Math.ceil(response.total / pageSize)
}
console.log(`📊 返回数据: ${response.data.length}条,总数: ${response.total}`)
res.json(response)
})
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 模拟API服务器启动成功!`)
console.log(`📍 地址: http://localhost:${PORT}`)
console.log(`🔗 API端点: http://localhost:${PORT}/api/smart-devices/hosts`)
console.log(`📊 模拟数据: 371台主机`)
console.log(`\n💡 测试命令:`)
console.log(`curl "http://localhost:${PORT}/api/smart-devices/hosts?page=1&pageSize=10"`)
})
module.exports = app

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
{
"name": "farm-monitor-dashboard",
"version": "1.0.0",
"description": "养殖端微信小程序 - 原生版本",
"main": "app.js",
"scripts": {
"dev": "echo '请在微信开发者工具中打开项目进行开发'",
"build": "echo '请在微信开发者工具中构建项目'",
"test": "echo '测试功能'",
"lint": "echo '代码检查'"
},
"keywords": [
"微信小程序",
"养殖管理",
"物联网",
"智能设备",
"农业科技"
],
"author": "养殖管理系统开发团队",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/your-org/farm-monitor-dashboard.git"
},
"bugs": {
"url": "https://github.com/your-org/farm-monitor-dashboard/issues"
},
"homepage": "https://github.com/your-org/farm-monitor-dashboard#readme",
"dependencies": {
"dayjs": "^1.11.0"
},
"devDependencies": {
"eslint": "^8.45.0"
}
}

View File

@@ -0,0 +1,84 @@
// 状态映射
const statusMap = {
'online': '在线',
'offline': '离线',
'alarm': '告警'
}
Page({
data: {
list: [], // 项圈数据列表
searchValue: '', // 搜索值
currentPage: 1, // 当前页码
total: 0, // 总数据量
pageSize: 10 // 每页数量
},
onLoad() {
this.loadData()
},
// 加载数据
loadData() {
const { currentPage, pageSize, searchValue } = this.data
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
wx.showLoading({ title: '加载中...' })
wx.request({
url,
header: {
'Authorization': 'Bearer ' + getApp().globalData.token // 添加认证头
},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.data) {
const data = res.data.data
this.setData({
list: (data.items || []).map(item => ({
...item,
statusText: statusMap[item.status] || item.status
})),
total: data.total || 0
})
} else {
wx.showToast({
title: res.data.message || '数据加载失败',
icon: 'none'
})
}
},
fail: (err) => {
wx.showToast({
title: err.errMsg.includes('401') ? '请登录后重试' : '请求失败',
icon: 'none'
})
console.error(err)
},
complete: () => wx.hideLoading()
})
},
// 搜索输入
onSearchInput(e) {
this.setData({ searchValue: e.detail.value.trim() })
},
// 执行搜索
onSearch() {
this.setData({ currentPage: 1 }, () => this.loadData())
},
// 分页切换
onPageChange(e) {
const page = parseInt(e.currentTarget.dataset.page)
if (page !== this.data.currentPage) {
this.setData({ currentPage: page }, () => this.loadData())
}
},
// 计算分页数组
getPages(total, pageSize, currentPage) {
const pageCount = Math.ceil(total / pageSize)
return Array.from({ length: pageCount }, (_, i) => i + 1)
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationBarTitleText": "智能项圈管理"
}

View File

@@ -0,0 +1,37 @@
<view class="container">
<!-- 搜索区域 -->
<view class="search-box">
<input
placeholder="请输入项圈编号"
bindinput="onSearchInput"
value="{{searchValue}}"
/>
<button bindtap="onSearch">查询</button>
</view>
<!-- 数据列表 -->
<view class="list">
<block wx:for="{{list}}" wx:key="id">
<view class="item">
<text>项圈编号: {{item.deviceId}}</text>
<text>状态: {{item.status | statusText}}</text>
<text>电量: {{item.battery}}%</text>
<text>最后在线: {{item.lastOnlineTime}}</text>
</view>
</block>
</view>
<!-- 分页控件 -->
<view class="pagination">
<block wx:for="{{pages}}" wx:key="index">
<text
class="{{currentPage === item ? 'active' : ''}}"
bindtap="onPageChange"
data-page="{{item}}"
>
{{item}}
</text>
</block>
</view>
</view>

View File

@@ -0,0 +1,46 @@
.container {
padding: 20rpx;
}
.search-box {
display: flex;
margin-bottom: 20rpx;
}
.search-box input {
flex: 1;
border: 1rpx solid #ddd;
padding: 10rpx 20rpx;
margin-right: 20rpx;
}
.list {
margin-bottom: 30rpx;
}
.item {
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.item text {
display: block;
margin-bottom: 10rpx;
}
.pagination {
display: flex;
justify-content: center;
}
.pagination text {
margin: 0 10rpx;
padding: 5rpx 15rpx;
border: 1rpx solid #ddd;
}
.pagination .active {
background-color: #07C160;
color: white;
}

View File

@@ -28,27 +28,112 @@ Page({
this.setData({ loading: true })
try {
// 这里可以调用具体的耳标详情API
// const response = await get(`/api/iot-jbq-client/${eartagId}`)
// 获取token
const token = wx.getStorageSync('token')
console.log('详情页获取token:', token)
// 暂时使用模拟数据
const mockData = {
eartagNumber: eartagId,
isBound: false,
batteryLevel: 39,
temperature: 28.7,
hostNumber: '23107000007',
totalMovement: 3316,
todayMovement: 0,
updateTime: '2025-09-23 01:17:52'
if (!token) {
console.log('未找到token跳转到登录页')
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
return
}
this.setData({ eartagData: mockData })
// 验证token格式
if (!token.startsWith('eyJ')) {
console.log('token格式不正确清除并重新登录')
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
return
}
// 调用私有API获取耳标列表然后筛选出指定耳标
const response = await new Promise((resolve, reject) => {
wx.request({
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
method: 'GET',
data: {
page: 1,
limit: 1000, // 获取足够多的数据
refresh: true,
_t: Date.now()
},
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 10000,
success: (res) => {
console.log('耳标列表API调用成功:', res)
if (res.statusCode === 401) {
console.log('收到401错误清除token并跳转登录页')
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
reject(new Error('未授权,请重新登录'))
} else {
resolve(res)
}
},
fail: (error) => {
console.log('耳标列表API调用失败:', error)
reject(error)
}
})
})
console.log('耳标列表API响应:', response)
if (response.statusCode === 200 && response.data.success) {
// 从列表中查找指定的耳标
const eartagList = response.data.data || []
const targetEartag = eartagList.find(item =>
item.cid == eartagId ||
item.eartagNumber == eartagId ||
item.sn == eartagId ||
item.id == eartagId
)
if (targetEartag) {
// 处理API数据
const processedData = this.processEartagDetailData(targetEartag)
console.log('处理后的详情数据:', processedData)
this.setData({ eartagData: processedData })
} else {
throw new Error('未找到指定的耳标')
}
} else {
throw new Error(response.data.message || '获取详情失败')
}
} catch (error) {
console.error('获取耳标详情失败:', error)
wx.showToast({
title: '获取数据失败',
title: error.message || '获取数据失败',
icon: 'none'
})
} finally {
@@ -56,6 +141,115 @@ Page({
}
},
// 处理耳标详情数据
processEartagDetailData(apiData) {
try {
console.log('处理耳标详情数据,原始数据:', apiData)
const processedData = {
eartagNumber: apiData.cid || apiData.eartagNumber || apiData.sn || apiData.id || '未知',
isBound: this.checkIfBound(apiData),
batteryLevel: this.formatBatteryLevel(apiData),
temperature: this.formatTemperature(apiData),
hostNumber: this.formatHostNumber(apiData),
totalMovement: this.formatMovement(apiData.totalMovement || apiData.dailyMovement || apiData.walk || apiData.steps || 0),
todayMovement: this.formatMovement(apiData.dailyMovement || apiData['walk-y_steps'] || apiData.todayMovement || 0),
updateTime: this.formatUpdateTime(apiData.lastUpdate || apiData.updateTime || new Date().toISOString()),
// 新增字段
wearStatus: apiData.wearStatus || '未知',
deviceStatus: apiData.deviceStatus || '未知',
gpsState: apiData.gps_state || '未知',
location: apiData.location || '无定位',
latitude: apiData.lat || '0',
longitude: apiData.lon || '0',
bindingStatus: apiData.bindingStatus || '未知',
voltage: apiData.voltage || '0',
rawData: apiData // 保存原始数据用于调试
}
console.log('处理后的详情数据:', processedData)
return processedData
} catch (error) {
console.error('处理耳标详情数据失败:', error)
return {
eartagNumber: '处理失败',
isBound: false,
batteryLevel: 0,
temperature: '0.0',
hostNumber: '未知',
totalMovement: '0',
todayMovement: '0',
updateTime: '未知时间',
wearStatus: '未知',
deviceStatus: '未知',
gpsState: '未知',
location: '无定位',
latitude: '0',
longitude: '0',
bindingStatus: '未知',
voltage: '0'
}
}
},
// 检查是否已绑定
checkIfBound(item) {
if (item.bindingStatus === '已绑定') return true
if (item.bindingStatus === '未绑定') return false
if (item.isBound !== undefined) return item.isBound
if (item.is_bound !== undefined) return item.is_bound
if (item.bound !== undefined) return item.bound
if (item.bandge_status !== undefined) return item.bandge_status === 1
if (item.is_wear !== undefined) return item.is_wear === 1
if (item.status === 'bound' || item.status === '已绑定') return true
if (item.status === 'unbound' || item.status === '未绑定') return false
return !!(item.cattleId || item.cattle_id || item.animalId || item.animal_id)
},
// 格式化电量
formatBatteryLevel(item) {
const battery = item.voltage || item.battery || item.batteryPercent || item.batteryLevel || item.battery_level || item.power || 0
return Math.round(parseFloat(battery)) || 0
},
// 格式化温度
formatTemperature(item) {
const temp = item.temperature || item.temp || item.device_temp || 0
return parseFloat(temp).toFixed(1) || '0.0'
},
// 格式化主机号
formatHostNumber(item) {
return item.sid || item.collectedHost || item.deviceInfo || item.hostNumber || item.host_number || item.hostId || item.host_id || item.collector || '未知主机'
},
// 格式化运动量
formatMovement(movement) {
const value = parseInt(movement) || 0
return value.toString()
},
// 格式化更新时间
formatUpdateTime(timeStr) {
if (!timeStr) return '未知时间'
try {
const date = new Date(timeStr)
if (isNaN(date.getTime())) return timeStr
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-')
} catch (error) {
return timeStr
}
},
// 返回上一页
goBack() {
wx.navigateBack()

View File

@@ -44,6 +44,46 @@
<text class="value">{{eartagData.todayMovement}}</text>
</view>
<view class="detail-item">
<text class="label">佩戴状态</text>
<text class="value">{{eartagData.wearStatus}}</text>
</view>
<view class="detail-item">
<text class="label">设备状态</text>
<text class="value">{{eartagData.deviceStatus}}</text>
</view>
<view class="detail-item">
<text class="label">位置信息</text>
<text class="value">{{eartagData.location}}</text>
</view>
<view class="detail-item">
<text class="label">GPS状态</text>
<text class="value">{{eartagData.gpsState}}</text>
</view>
<view class="detail-item">
<text class="label">纬度</text>
<text class="value">{{eartagData.latitude}}</text>
</view>
<view class="detail-item">
<text class="label">经度</text>
<text class="value">{{eartagData.longitude}}</text>
</view>
<view class="detail-item">
<text class="label">绑定状态</text>
<text class="value">{{eartagData.bindingStatus}}</text>
</view>
<view class="detail-item">
<text class="label">电压值</text>
<text class="value">{{eartagData.voltage}}V</text>
</view>
<view class="detail-item">
<text class="label">数据更新时间</text>
<text class="value">{{eartagData.updateTime}}</text>

View File

@@ -4,7 +4,7 @@ const { formatTime } = require('../../../utils/index')
Page({
data: {
loading: false,
loading: false, // 初始状态设为未加载
searchKeyword: '',
currentFilter: 'all', // all, bound, unbound
filterTabs: [
@@ -14,15 +14,35 @@ Page({
],
allEartagList: [], // 所有耳标数据
eartagList: [], // 当前显示的耳标列表
originalData: [] // 原始API数据
originalData: [], // 原始API数据
// 分页相关
currentPage: 1,
pageSize: 10,
totalPages: 0,
totalCount: 0,
paginationList: [] // 分页页码列表
},
onLoad() {
console.log('eartag页面onLoad执行')
// 页面加载时获取数据
this.fetchEartagData()
},
onReady() {
console.log('eartag页面onReady执行')
// 页面加载完成
},
onShow() {
this.fetchEartagData()
console.log('eartag页面onShow执行')
// 页面显示时检查是否需要刷新数据
// 避免重复加载,只在必要时刷新
if (this.data.currentPage === 1 && this.data.eartagList.length === 0) {
console.log('onShow中调用fetchEartagData')
this.fetchEartagData()
}
},
onPullDownRefresh() {
@@ -32,90 +52,273 @@ Page({
},
// 获取耳标数据
async fetchEartagData() {
this.setData({ loading: true })
async fetchEartagData(page = 1) {
console.log('fetchEartagData函数被调用page:', page)
// 防止重复请求
if (this.data.loading) {
console.log('正在加载中,跳过重复请求')
return
}
console.log('设置loading状态为true')
this.setData({ loading: true, currentPage: page })
try {
// 使用真实的智能耳标API接口公开API无需认证
const response = await get('/smart-devices/public/eartags?page=1&limit=10&refresh=true')
console.log('智能耳标API响应:', response)
console.log('开始调用API...')
// 处理真实的API数据
const processedData = this.processApiData(response)
// 使用私有API端点需要认证
const token = wx.getStorageSync('token')
console.log('当前token:', token)
console.log('token是否存在:', !!token)
console.log('token长度:', token ? token.length : 0)
if (!token) {
console.log('未找到token跳转到登录页')
wx.showToast({
title: '请先登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
return
}
// 验证token格式
if (!token.startsWith('eyJ')) {
console.log('token格式不正确清除并重新登录')
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
return
}
const requestHeaders = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
}
console.log('请求头:', requestHeaders)
const response = await new Promise((resolve, reject) => {
wx.request({
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
method: 'GET',
data: {
page: page,
limit: this.data.pageSize,
refresh: true,
_t: Date.now()
},
header: requestHeaders,
timeout: 10000,
success: (res) => {
console.log('私有API调用成功:', res)
if (res.statusCode === 401) {
console.log('收到401错误清除token并跳转登录页')
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
wx.showToast({
title: '登录已过期,请重新登录',
icon: 'none'
})
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
})
}, 1500)
reject(new Error('未授权,请重新登录'))
} else {
resolve(res)
}
},
fail: (error) => {
console.log('私有API调用失败:', error)
reject(error)
}
})
})
console.log('API响应成功:', response)
// 处理私有API数据 - 从response.data.data中获取数组数据
console.log('完整响应结构:', response.data)
console.log('实际数据部分:', response.data?.data)
console.log('原始数据列表:', response.data?.data)
const processedData = this.processApiData(response.data?.data || [])
console.log('处理后的数据:', processedData)
// 计算分页信息
const totalCount = response.data?.total || response.data?.stats?.total || 0
const totalPages = Math.ceil(totalCount / this.data.pageSize)
const paginationList = this.generatePaginationList(page, totalPages)
console.log('分页信息:', {
totalCount: totalCount,
totalPages: totalPages,
currentPage: page,
pageSize: this.data.pageSize
})
this.setData({
originalData: response,
allEartagList: processedData,
eartagList: processedData
eartagList: processedData,
totalCount: totalCount,
totalPages: totalPages,
paginationList: paginationList
})
// 更新筛选标签计数
this.updateFilterCounts(processedData)
// 更新筛选标签计数 - 使用原始数据列表
this.updateFilterCounts(response.data?.data || [])
} catch (error) {
console.error('获取耳标数据失败:', error)
// 根据错误类型显示不同的提示
let errorMessage = '获取数据失败'
if (error.message === '请求超时') {
errorMessage = '网络超时,请检查网络连接'
} else if (error.message && error.message.includes('timeout')) {
errorMessage = '请求超时,请重试'
} else if (error.message && error.message.includes('fail')) {
errorMessage = '网络连接失败'
}
wx.showToast({
title: '获取数据失败',
icon: 'none'
title: errorMessage,
icon: 'none',
duration: 3000
})
// 如果是第一页加载失败,设置空数据状态
if (page === 1) {
this.setData({
eartagList: [],
allEartagList: [],
totalCount: 0,
totalPages: 0,
paginationList: []
})
}
} finally {
this.setData({ loading: false })
}
},
// 将API数据映射为耳标格式演示动态字段映射
mapApiDataToEartags(apiData) {
if (!apiData || !Array.isArray(apiData)) {
return []
}
return apiData.map((item, index) => ({
eartagNumber: `E${String(item.id).padStart(3, '0')}`, // 耳标编号
batteryLevel: Math.floor(Math.random() * 100), // 电量
temperature: (36.5 + Math.random() * 2).toFixed(1), // 体温
heartRate: Math.floor(60 + Math.random() * 40), // 心率
location: `位置${item.id}`, // 位置
bindingStatus: Math.random() > 0.5 ? '已绑定' : '未绑定', // 绑定状态
lastUpdateTime: new Date().toLocaleString(), // 最后更新时间
cattleId: item.id, // 牛只ID
cattleName: `牛只${item.id}`, // 牛只名称
farmId: item.userId, // 养殖场ID
farmName: `养殖场${item.userId}`, // 养殖场名称
signalStrength: Math.floor(Math.random() * 5) + 1, // 信号强度
isOnline: Math.random() > 0.2, // 在线状态
alertCount: Math.floor(Math.random() * 5), // 预警数量
originalData: item // 保留原始数据
}))
},
// 处理API数据进行字段映射和中文转换
processApiData(apiData) {
if (!apiData || !Array.isArray(apiData)) {
try {
if (!apiData || !Array.isArray(apiData)) {
console.log('API数据为空或不是数组:', apiData)
return []
}
const processedData = apiData.map((item, index) => {
try {
// 调试显示完整API字段结构
if (index === 0) {
console.log('完整API字段结构调试:', {
'cid (耳标编号)': item.cid,
'voltage (电量)': item.voltage,
'sid (主机号)': item.sid,
'totalMovement (总运动量)': item.totalMovement,
'dailyMovement (今日运动量)': item.dailyMovement,
'temperature (温度)': item.temperature,
'bindingStatus (绑定状态)': item.bindingStatus,
'wearStatus (佩戴状态)': item.wearStatus,
'deviceStatus (设备状态)': item.deviceStatus,
'gps_state (GPS状态)': item.gps_state,
'location (位置信息)': item.location,
'lat (纬度)': item.lat,
'lon (经度)': item.lon,
'lastUpdate (更新时间)': item.lastUpdate
})
}
// 字段映射和中文转换 - 根据完整API字段结构
const processedItem = {
eartagNumber: item.cid || item.eartagNumber || item.sn || item.eartag_number || item.id || `EARTAG_${index + 1}`,
isBound: this.checkIfBound(item),
batteryLevel: this.formatBatteryLevel(item),
temperature: this.formatTemperature(item),
hostNumber: this.formatHostNumber(item),
totalMovement: this.formatMovement(item.totalMovement || item.dailyMovement || item.walk || item.steps || item.total_movement || item.movement_total || 0),
todayMovement: this.formatTodayMovement(item),
updateTime: this.formatUpdateTime(item.lastUpdate || item.updateTime || item.update_time || item.last_update || new Date().toISOString()),
// 新增字段
wearStatus: item.wearStatus || '未知',
deviceStatus: item.deviceStatus || '未知',
gpsState: item.gps_state || '未知',
location: item.location || '无定位',
latitude: item.lat || '0',
longitude: item.lon || '0',
rawData: item // 保存原始数据用于调试
}
// 调试:显示字段映射结果
if (index === 0) {
console.log('字段映射结果调试:', {
'eartagNumber': processedItem.eartagNumber,
'batteryLevel': processedItem.batteryLevel,
'temperature': processedItem.temperature,
'hostNumber': processedItem.hostNumber,
'totalMovement': processedItem.totalMovement,
'todayMovement': processedItem.todayMovement,
'wearStatus': processedItem.wearStatus,
'deviceStatus': processedItem.deviceStatus,
'location': processedItem.location,
'updateTime': processedItem.updateTime
})
}
return processedItem
} catch (itemError) {
console.error('处理单个数据项失败:', item, itemError)
return {
eartagNumber: `ERROR_${index + 1}`,
isBound: false,
batteryLevel: 0,
temperature: '0.0',
hostNumber: '错误',
totalMovement: '0',
todayMovement: '0',
updateTime: '错误',
rawData: item
}
}
})
console.log('数据处理完成,共处理', processedData.length, '条数据')
return processedData
} catch (error) {
console.error('数据处理失败:', error)
return []
}
return apiData.map((item, index) => {
// 字段映射和中文转换
return {
eartagNumber: item.eartagNumber || item.eartag_number || item.id || `EARTAG_${index + 1}`,
isBound: this.checkIfBound(item),
batteryLevel: this.formatBatteryLevel(item),
temperature: this.formatTemperature(item),
hostNumber: this.formatHostNumber(item),
totalMovement: this.formatMovement(item.totalMovement || item.total_movement || item.movement_total || 0),
todayMovement: this.formatMovement(item.todayMovement || item.today_movement || item.movement_today || 0),
updateTime: this.formatUpdateTime(item.updateTime || item.update_time || item.last_update || new Date().toISOString()),
rawData: item // 保存原始数据用于调试
}
})
},
// 检查是否已绑定
checkIfBound(item) {
// 根据实API字段判断绑定状态
// 根据实API字段判断绑定状态
if (item.bindingStatus === '已绑定') return true
if (item.bindingStatus === '未绑定') return false
if (item.isBound !== undefined) return item.isBound
if (item.is_bound !== undefined) return item.is_bound
if (item.bound !== undefined) return item.bound
if (item.bandge_status !== undefined) return item.bandge_status === 1
if (item.is_wear !== undefined) return item.is_wear === 1
if (item.status === 'bound' || item.status === '已绑定') return true
if (item.status === 'unbound' || item.status === '未绑定') return false
@@ -123,21 +326,21 @@ Page({
return !!(item.cattleId || item.cattle_id || item.animalId || item.animal_id)
},
// 格式化电量
// 格式化电量 - 优先使用voltage字段完整API字段
formatBatteryLevel(item) {
const battery = item.batteryLevel || item.battery_level || item.battery || item.power || 0
const battery = item.voltage || item.battery || item.batteryPercent || item.batteryLevel || item.battery_level || item.power || 0
return Math.round(parseFloat(battery)) || 0
},
// 格式化温度
// 格式化温度 - 使用temperature字段
formatTemperature(item) {
const temp = item.temperature || item.temp || item.device_temp || 0
return parseFloat(temp).toFixed(1) || '0.0'
},
// 格式化主机号
// 格式化主机号 - 优先使用sid字段完整API字段
formatHostNumber(item) {
return item.hostNumber || item.host_number || item.hostId || item.host_id || item.collector || '未知主机'
return item.sid || item.collectedHost || item.deviceInfo || item.hostNumber || item.host_number || item.hostId || item.host_id || item.collector || '未知主机'
},
// 格式化运动量
@@ -146,6 +349,123 @@ Page({
return value.toString()
},
// 格式化今日运动量 - 优先使用dailyMovement字段完整API字段
formatTodayMovement(item) {
const todayMovement = item.dailyMovement || item['walk-y_steps'] || item.todayMovement || item.today_movement || item.movement_today || 0
return this.formatMovement(todayMovement)
},
// 生成分页页码列表
generatePaginationList(currentPage, totalPages) {
const paginationList = []
const maxVisiblePages = 5 // 最多显示5个页码
if (totalPages <= maxVisiblePages) {
// 总页数少于等于5页显示所有页码
for (let i = 1; i <= totalPages; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
} else {
// 总页数大于5页显示省略号
if (currentPage <= 3) {
// 当前页在前3页
for (let i = 1; i <= 4; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
paginationList.push({
page: -1,
active: false,
text: '...'
})
paginationList.push({
page: totalPages,
active: false,
text: totalPages.toString()
})
} else if (currentPage >= totalPages - 2) {
// 当前页在后3页
paginationList.push({
page: 1,
active: false,
text: '1'
})
paginationList.push({
page: -1,
active: false,
text: '...'
})
for (let i = totalPages - 3; i <= totalPages; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
} else {
// 当前页在中间
paginationList.push({
page: 1,
active: false,
text: '1'
})
paginationList.push({
page: -1,
active: false,
text: '...'
})
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
paginationList.push({
page: i,
active: i === currentPage,
text: i.toString()
})
}
paginationList.push({
page: -1,
active: false,
text: '...'
})
paginationList.push({
page: totalPages,
active: false,
text: totalPages.toString()
})
}
}
return paginationList
},
// 跳转到指定页面
goToPage(e) {
const page = e.currentTarget.dataset.page
if (page > 0 && page !== this.data.currentPage) {
this.fetchEartagData(page)
}
},
// 上一页
prevPage() {
if (this.data.currentPage > 1) {
this.fetchEartagData(this.data.currentPage - 1)
}
},
// 下一页
nextPage() {
if (this.data.currentPage < this.data.totalPages) {
this.fetchEartagData(this.data.currentPage + 1)
}
},
// 格式化更新时间
formatUpdateTime(timeStr) {
if (!timeStr) return '未知时间'
@@ -167,26 +487,239 @@ Page({
}
},
// 更新筛选标签计数
updateFilterCounts(data) {
const totalCount = data.length
const boundCount = data.filter(item => item.isBound).length
const unboundCount = totalCount - boundCount
// 更新筛选标签计数 - 获取所有数据的总数
async updateFilterCounts(data) {
try {
// 获取token
const token = wx.getStorageSync('token')
if (!token) {
console.log('未找到token无法获取统计数据')
return
}
const filterTabs = this.data.filterTabs.map(tab => ({
...tab,
count: tab.type === 'all' ? totalCount :
tab.type === 'bound' ? boundCount : unboundCount
}))
// 调用API获取所有数据的总数不受分页限制
const response = await new Promise((resolve, reject) => {
wx.request({
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
method: 'GET',
data: {
page: 1,
limit: 10000, // 获取足够多的数据来统计
refresh: true,
_t: Date.now()
},
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 10000,
success: (res) => {
console.log('统计数据API调用成功:', res)
resolve(res)
},
fail: (error) => {
console.log('统计数据API调用失败:', error)
reject(error)
}
})
})
this.setData({ filterTabs })
if (response.statusCode === 200 && response.data.success) {
const allData = response.data.data || []
const totalCount = allData.length
const boundCount = allData.filter(item => {
return item.bindingStatus === '已绑定' ||
item.isBound === true ||
item.bound === true ||
item.is_bound === 1 ||
item.bandge_status === 1
}).length
const unboundCount = totalCount - boundCount
console.log('统计数据:', { totalCount, boundCount, unboundCount })
const filterTabs = this.data.filterTabs.map(tab => ({
...tab,
count: tab.type === 'all' ? totalCount :
tab.type === 'bound' ? boundCount : unboundCount
}))
this.setData({ filterTabs })
}
} catch (error) {
console.error('获取统计数据失败:', error)
// 如果API调用失败使用当前数据计算
const totalCount = data.length
const boundCount = data.filter(item => {
return item.bindingStatus === '已绑定' ||
item.isBound === true ||
item.bound === true ||
item.is_bound === 1 ||
item.bandge_status === 1
}).length
const unboundCount = totalCount - boundCount
const filterTabs = this.data.filterTabs.map(tab => ({
...tab,
count: tab.type === 'all' ? totalCount :
tab.type === 'bound' ? boundCount : unboundCount
}))
this.setData({ filterTabs })
}
},
// 搜索输入
onSearchInput(e) {
const keyword = e.detail.value
this.setData({ searchKeyword: keyword })
this.filterEartagList()
// 防抖处理,避免频繁请求
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.performSearch(keyword)
}, 500) // 500ms防抖
},
// 搜索确认
onSearchConfirm(e) {
const keyword = e.detail.value
this.setData({ searchKeyword: keyword })
// 清除防抖定时器,立即执行搜索
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.performSearch(keyword)
},
// 执行搜索 - 按耳标编号精确查找
async performSearch(keyword) {
if (!keyword || keyword.trim() === '') {
// 如果搜索关键词为空,重新加载第一页数据
this.fetchEartagData(1)
return
}
this.setData({ loading: true })
try {
// 获取token
const token = wx.getStorageSync('token')
if (!token) {
wx.showToast({
title: '请先登录',
icon: 'none'
})
return
}
// 调用私有API获取所有数据然后进行客户端搜索
const response = await new Promise((resolve, reject) => {
wx.request({
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
method: 'GET',
data: {
page: 1,
limit: 10000, // 获取所有数据进行搜索
refresh: true,
_t: Date.now()
},
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 10000,
success: (res) => {
console.log('搜索API调用成功:', res)
resolve(res)
},
fail: (error) => {
console.log('搜索API调用失败:', error)
reject(error)
}
})
})
if (response.statusCode === 200 && response.data.success) {
const allData = response.data.data || []
// 按耳标编号精确查找
const searchResults = allData.filter(item => {
const cid = item.cid ? item.cid.toString() : ''
const eartagNumber = item.eartagNumber ? item.eartagNumber.toString() : ''
const sn = item.sn ? item.sn.toString() : ''
const id = item.id ? item.id.toString() : ''
const searchKeyword = keyword.trim().toString()
return cid === searchKeyword ||
eartagNumber === searchKeyword ||
sn === searchKeyword ||
id === searchKeyword ||
cid.includes(searchKeyword) ||
eartagNumber.includes(searchKeyword) ||
sn.includes(searchKeyword)
})
console.log('搜索结果:', searchResults)
// 处理搜索结果
const processedData = this.processApiData(searchResults)
// 计算分页信息
const totalCount = searchResults.length
const totalPages = Math.ceil(totalCount / this.data.pageSize)
const paginationList = this.generatePaginationList(1, totalPages)
this.setData({
originalData: response,
allEartagList: processedData,
eartagList: processedData,
totalCount: totalCount,
totalPages: totalPages,
paginationList: paginationList,
currentPage: 1
})
// 更新筛选标签计数(基于搜索结果)
this.updateFilterCounts(searchResults)
if (processedData.length === 0) {
wx.showToast({
title: '未找到匹配的耳标',
icon: 'none'
})
} else {
wx.showToast({
title: `找到 ${processedData.length} 个匹配的耳标`,
icon: 'success'
})
}
} else {
throw new Error(response.data.message || '搜索失败')
}
} catch (error) {
console.error('搜索失败:', error)
wx.showToast({
title: error.message || '搜索失败',
icon: 'none'
})
} finally {
this.setData({ loading: false })
}
},
// 清空搜索
clearSearch() {
this.setData({ searchKeyword: '' })
// 清空搜索后重新加载第一页数据
this.fetchEartagData(1)
},
// 切换筛选

View File

@@ -71,6 +71,21 @@
<text class="detail-value">{{item.todayMovement}}</text>
</view>
<view class="detail-row">
<text class="detail-label">佩戴状态:</text>
<text class="detail-value">{{item.wearStatus}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备状态:</text>
<text class="detail-value">{{item.deviceStatus}}</text>
</view>
<view class="detail-row">
<text class="detail-label">位置信息:</text>
<text class="detail-value">{{item.location}}</text>
</view>
<view class="detail-row">
<text class="detail-label">数据更新时间:</text>
<text class="detail-value">{{item.updateTime}}</text>
@@ -90,4 +105,42 @@
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 分页组件 -->
<view wx:if="{{totalPages > 1}}" class="pagination-container">
<view class="pagination-info">
<text>共 {{totalCount}} 条记录,第 {{currentPage}}/{{totalPages}} 页</text>
</view>
<view class="pagination-controls">
<!-- 上一页按钮 -->
<view
class="pagination-btn {{currentPage === 1 ? 'disabled' : ''}}"
bindtap="prevPage"
>
上一页
</view>
<!-- 页码列表 -->
<view class="pagination-pages">
<view
wx:for="{{paginationList}}"
wx:key="index"
class="pagination-page {{item.active ? 'active' : ''}} {{item.page === -1 ? 'ellipsis' : ''}}"
bindtap="goToPage"
data-page="{{item.page}}"
>
{{item.text}}
</view>
</view>
<!-- 下一页按钮 -->
<view
class="pagination-btn {{currentPage === totalPages ? 'disabled' : ''}}"
bindtap="nextPage"
>
下一页
</view>
</view>
</view>
</view>

View File

@@ -218,3 +218,68 @@
font-size: 26rpx;
}
}
/* 分页组件样式 */
.pagination-container {
padding: 32rpx;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
.pagination-info {
text-align: center;
margin-bottom: 24rpx;
font-size: 24rpx;
color: #666;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.pagination-btn {
padding: 16rpx 24rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
text-align: center;
min-width: 120rpx;
}
.pagination-btn.disabled {
background: #f0f0f0;
color: #ccc;
}
.pagination-pages {
display: flex;
gap: 8rpx;
}
.pagination-page {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 24rpx;
color: #333;
text-align: center;
}
.pagination-page.active {
background: #3cc51f;
color: #ffffff;
}
.pagination-page.ellipsis {
background: transparent;
color: #999;
font-weight: bold;
}

View File

@@ -77,12 +77,21 @@ Page({
this.setData({ loading: true })
try {
// 模拟登录API调用
const response = await this.mockLogin(username.trim(), password.trim())
// 调用真实的登录API
const response = await this.realLogin(username.trim(), password.trim())
console.log('登录API响应:', response)
if (response.success) {
console.log('登录成功token:', response.token)
// 保存登录信息
auth.login(response.data.token, response.data.userInfo)
auth.login(response.token, {
id: response.user?.id || 1,
username: response.user?.username || username.trim(),
nickname: '管理员',
avatar: '',
role: response.role?.name || 'admin'
})
console.log('用户信息已保存')
wx.showToast({
title: '登录成功',
@@ -112,7 +121,44 @@ Page({
}
},
// 模拟登录API
// 真实登录API
async realLogin(username, password) {
try {
const response = await new Promise((resolve, reject) => {
wx.request({
url: 'https://ad.ningmuyun.com/api/auth/login',
method: 'POST',
data: {
username: username,
password: password
},
header: {
'Content-Type': 'application/json'
},
timeout: 10000,
success: (res) => {
console.log('登录API调用成功:', res)
resolve(res)
},
fail: (error) => {
console.log('登录API调用失败:', error)
reject(error)
}
})
})
if (response.statusCode === 200) {
return response.data
} else {
throw new Error('登录请求失败')
}
} catch (error) {
console.error('登录API调用异常:', error)
throw error
}
},
// 模拟登录API保留作为备用
mockLogin(username, password) {
return new Promise((resolve) => {
setTimeout(() => {

View File

@@ -73,7 +73,6 @@
"disablePlugins": [],
"outputPath": ""
},
"useStaticServer": true,
"checkInvalidKey": true,
"disableUseStrict": false,
"useCompilerPlugins": false,
@@ -81,11 +80,11 @@
"localPlugins": false,
"condition": false,
"swc": false,
"disableSWC": false
"disableSWC": true
},
"compileType": "miniprogram",
"libVersion": "3.10.1",
"appid": "wx363d2520963f1853",
"appid": "wx209dbc164322f698",
"projectname": "farm-monitor-dashboard",
"isGameTourist": false,
"condition": {

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="#27ae60"/>
<text x="50" y="65" font-size="50" text-anchor="middle" fill="white">🐄</text>
</svg>

Before

Width:  |  Height:  |  Size: 203 B

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="alternate icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐄</text></svg>">
<title>智慧养殖管理系统</title>
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,63 +0,0 @@
// Token设置工具 - 自动获取并设置API认证token
const axios = require('axios')
const readline = require('readline')
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350'
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
async function getToken() {
try {
console.log('🔐 正在自动获取API认证token...')
const response = await axios.post(`${API_BASE_URL}/api/auth/login`, {
username: 'admin',
password: '123456'
})
if (response.data.success) {
return response.data.token
}
throw new Error('登录失败')
} catch (error) {
console.error('❌ 自动获取token失败:', error.message)
return null
}
}
console.log('🔐 API Token 设置工具')
console.log('====================')
console.log('')
console.log('此工具将帮助您设置API认证token以便前端能正确调用后端API。')
console.log('')
// 自动获取token
getToken().then(token => {
if (token) {
console.log('✅ 自动获取token成功!')
console.log('Token:', token.substring(0, 20) + '...')
console.log('')
console.log('📋 请在前端浏览器控制台中执行以下代码:')
console.log('')
console.log(`localStorage.setItem('token', '${token}')`)
console.log('')
console.log('然后刷新页面测试API连接。')
console.log('')
console.log('🔍 测试API连接:')
console.log('node test-api.js')
} else {
console.log('⚠️ 自动获取token失败')
console.log('')
console.log('💡 手动解决方案:')
console.log('1. 联系后端开发者获取正确的认证信息')
console.log('2. 检查API文档了解认证方式')
console.log('3. 尝试以下测试token:')
console.log(' localStorage.setItem("token", "test-token")')
console.log(' localStorage.setItem("apiKey", "test-api-key")')
}
console.log('')
rl.close()
})

View File

@@ -1,110 +0,0 @@
<template>
<div id="app">
<!-- 应用内容 -->
<router-view />
</div>
</template>
<script>
import auth from './utils/auth'
export default {
name: 'App',
data() {
return {
globalData: {
version: '1.0.0',
platform: 'web',
isDevelopment: process.env.NODE_ENV === 'development'
}
}
},
created() {
// 应用初始化
this.initApp()
},
methods: {
// 应用初始化
async initApp() {
try {
// 检查登录状态
const token = auth.getToken()
const userInfo = auth.getUserInfo()
if (token && userInfo) {
console.log('用户已登录token:', token)
console.log('用户信息:', userInfo)
} else {
console.log('用户未登录')
}
} catch (error) {
console.error('应用初始化失败:', error)
}
}
}
}
</script>
<style>
/* 全局样式 */
page {
background-color: #f6f6f6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 14px;
color: #303133;
}
.container {
padding: 16px;
background-color: #ffffff;
border-radius: 8px;
margin: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 通用工具类 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.mb-8 { margin-bottom: 8px; }
.mb-16 { margin-bottom: 16px; }
.mb-24 { margin-bottom: 24px; }
.pt-8 { padding-top: 8px; }
.pt-16 { padding-top: 16px; }
.pt-24 { padding-top: 24px; }
.pb-8 { padding-bottom: 8px; }
.pb-16 { padding-bottom: 16px; }
.pb-24 { padding-bottom: 24px; }
.flex { display: flex; }
.flex-column { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.align-center { align-items: center; }
.align-start { align-items: flex-start; }
.align-end { align-items: flex-end; }
/* 状态颜色 */
.status-normal { color: #52c41a; }
.status-pregnant { color: #faad14; }
.status-sick { color: #f5222d; }
.status-quarantine { color: #909399; }
/* 加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
animation: spin 1s linear infinite;
}
</style>

View File

@@ -1,353 +0,0 @@
// 引入uni.scss变量
@import '../uni.scss';
/* 全局样式重置 */
page {
background-color: $bg-color;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: $font-size-sm;
color: $color-text-primary;
line-height: 1.6;
}
/* 通用容器样式 */
.container {
padding: $spacing-base;
background-color: $bg-color-page;
border-radius: $border-radius-base;
margin: $spacing-base;
box-shadow: $box-shadow-light;
}
.card {
background-color: $bg-color-card;
border-radius: $border-radius-base;
padding: $spacing-base;
margin-bottom: $spacing-base;
box-shadow: $box-shadow-light;
&.shadow-none {
box-shadow: none;
}
&.border {
border: 1px solid $border-color-base;
}
}
/* 统计数字样式 */
.stat-number {
font-size: 24px;
font-weight: bold;
color: $color-primary;
&.success { color: $color-success; }
&.warning { color: $color-warning; }
&.danger { color: $color-danger; }
&.info { color: $color-info; }
}
.stat-label {
font-size: $font-size-sm;
color: $color-text-secondary;
margin-top: $spacing-xs;
}
/* 状态标签 */
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: $border-radius-sm;
font-size: $font-size-xs;
font-weight: 500;
&.normal {
background-color: rgba($color-success, 0.1);
color: $color-success;
}
&.pregnant {
background-color: rgba($color-warning, 0.1);
color: $color-warning;
}
&.sick {
background-color: rgba($color-danger, 0.1);
color: $color-danger;
}
&.quarantine {
background-color: rgba($color-info, 0.1);
color: $color-info;
}
}
/* 列表样式 */
.list-item {
padding: $spacing-base;
background-color: $bg-color-card;
border-bottom: 1px solid $border-color-light;
&:last-child {
border-bottom: none;
}
&.clickable {
&:active {
background-color: darken($bg-color-card, 5%);
}
}
}
/* 表单样式 */
.form-group {
margin-bottom: $spacing-base;
.form-label {
display: block;
margin-bottom: $spacing-xs;
color: $color-text-regular;
font-weight: 500;
}
.form-control {
width: 100%;
padding: $spacing-sm $spacing-base;
border: 1px solid $border-color-base;
border-radius: $border-radius-base;
font-size: $font-size-sm;
&:focus {
border-color: $color-primary;
outline: none;
}
&.error {
border-color: $color-danger;
}
}
.form-error {
color: $color-danger;
font-size: $font-size-xs;
margin-top: $spacing-xs;
}
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: $spacing-sm $spacing-base;
border: none;
border-radius: $border-radius-base;
font-size: $font-size-sm;
font-weight: 500;
cursor: pointer;
transition: all $transition-duration $transition-function;
&:disabled {
opacity: $opacity-disabled;
cursor: not-allowed;
}
&.btn-primary {
background-color: $color-primary;
color: white;
&:active {
background-color: darken($color-primary, 10%);
}
}
&.btn-success {
background-color: $color-success;
color: white;
&:active {
background-color: darken($color-success, 10%);
}
}
&.btn-danger {
background-color: $color-danger;
color: white;
&:active {
background-color: darken($color-danger, 10%);
}
}
&.btn-outline {
background-color: transparent;
border: 1px solid $border-color-base;
color: $color-text-regular;
&:active {
background-color: $bg-color;
}
}
}
/* 布局工具类 */
.flex {
display: flex;
&.column { flex-direction: column; }
&.wrap { flex-wrap: wrap; }
&.justify-center { justify-content: center; }
&.justify-between { justify-content: space-between; }
&.justify-end { justify-content: flex-end; }
&.align-center { align-items: center; }
&.align-start { align-items: flex-start; }
&.align-end { align-items: flex-end; }
}
/* 文本工具类 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-primary { color: $color-primary; }
.text-success { color: $color-success; }
.text-warning { color: $color-warning; }
.text-danger { color: $color-danger; }
.text-info { color: $color-info; }
.text-xs { font-size: $font-size-xs; }
.text-sm { font-size: $font-size-sm; }
.text-base { font-size: $font-size-base; }
.text-lg { font-size: $font-size-lg; }
.text-xl { font-size: $font-size-xl; }
.text-bold { font-weight: bold; }
.text-medium { font-weight: 500; }
.text-normal { font-weight: normal; }
/* 间距工具类 */
.mt-4 { margin-top: $spacing-xs; }
.mt-8 { margin-top: $spacing-sm; }
.mt-16 { margin-top: $spacing-base; }
.mt-24 { margin-top: $spacing-lg; }
.mt-32 { margin-top: $spacing-xl; }
.mb-4 { margin-bottom: $spacing-xs; }
.mb-8 { margin-bottom: $spacing-sm; }
.mb-16 { margin-bottom: $spacing-base; }
.mb-24 { margin-bottom: $spacing-lg; }
.mb-32 { margin-bottom: $spacing-xl; }
.pt-4 { padding-top: $spacing-xs; }
.pt-8 { padding-top: $spacing-sm; }
.pt-16 { padding-top: $spacing-base; }
.pt-24 { padding-top: $spacing-lg; }
.pt-32 { padding-top: $spacing-xl; }
.pb-4 { padding-bottom: $spacing-xs; }
.pb-8 { padding-bottom: $spacing-sm; }
.pb-16 { padding-bottom: $spacing-base; }
.pb-24 { padding-bottom: $spacing-lg; }
.pb-32 { padding-bottom: $spacing-xl; }
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: $spacing-xl;
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid $border-color-light;
border-top: 2px solid $color-primary;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
margin-left: $spacing-sm;
color: $color-text-secondary;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: $spacing-xl;
color: $color-text-secondary;
.empty-icon {
font-size: 48px;
margin-bottom: $spacing-base;
}
.empty-text {
font-size: $font-size-sm;
}
}
/* 响应式设计 */
@media (max-width: $breakpoint-sm) {
.container {
margin: $spacing-sm;
padding: $spacing-sm;
}
.card {
padding: $spacing-sm;
}
}
/* 动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
.slide-in-up {
animation: slideInUp 0.3s ease-in-out;
}
/* 其他工具类 */
.hidden { display: none; }
.block { display: block; }
.inline-block { display: inline-block; }
.rounded-sm { border-radius: $border-radius-sm; }
.rounded { border-radius: $border-radius-base; }
.rounded-lg { border-radius: $border-radius-lg; }
.shadow-sm { box-shadow: $box-shadow-light; }
.shadow { box-shadow: $box-shadow-base; }
.opacity-50 { opacity: 0.5; }
.opacity-75 { opacity: 0.75; }
.cursor-pointer { cursor: pointer; }
.cursor-not-allowed { cursor: not-allowed; }
.select-none { user-select: none; }

View File

@@ -1,210 +0,0 @@
<template>
<div class="alert-test">
<div class="test-header">
<h2>智能耳标预警功能测试</h2>
<button @click="goBack" class="back-btn">返回</button>
</div>
<div class="test-content">
<div class="test-section">
<h3>功能测试</h3>
<div class="test-buttons">
<button @click="testLoadAlerts" class="test-btn">测试加载预警数据</button>
<button @click="testFilterAlerts" class="test-btn">测试筛选功能</button>
<button @click="testResolveAlert" class="test-btn">测试处理预警</button>
<button @click="testAutoRefresh" class="test-btn">测试自动刷新</button>
</div>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div class="test-results">
<div v-for="(result, index) in testResults" :key="index" class="test-result">
<span class="result-time">{{ result.time }}</span>
<span class="result-message" :class="result.type">{{ result.message }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { alertService } from '@/services/alertService'
export default {
name: 'AlertTest',
data() {
return {
testResults: []
}
},
methods: {
goBack() {
this.$router.go(-1)
},
addTestResult(message, type = 'info') {
this.testResults.unshift({
time: new Date().toLocaleTimeString(),
message,
type
})
},
async testLoadAlerts() {
this.addTestResult('开始测试加载预警数据...', 'info')
try {
const response = await alertService.getAlerts()
this.addTestResult(`加载成功: ${JSON.stringify(response).substring(0, 100)}...`, 'success')
} catch (error) {
this.addTestResult(`加载失败: ${error.message}`, 'error')
}
},
testFilterAlerts() {
this.addTestResult('测试筛选功能...', 'info')
// 模拟筛选测试
setTimeout(() => {
this.addTestResult('筛选功能正常', 'success')
}, 500)
},
testResolveAlert() {
this.addTestResult('测试处理预警...', 'info')
// 模拟处理预警测试
setTimeout(() => {
this.addTestResult('处理预警功能正常', 'success')
}, 500)
},
testAutoRefresh() {
this.addTestResult('测试自动刷新...', 'info')
// 模拟自动刷新测试
setTimeout(() => {
this.addTestResult('自动刷新功能正常', 'success')
}, 1000)
}
}
}
</script>
<style scoped>
.alert-test {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-header h2 {
margin: 0;
color: #333;
}
.back-btn {
background: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
}
.test-content {
display: flex;
gap: 20px;
}
.test-section {
flex: 1;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.test-section h3 {
margin: 0 0 16px 0;
color: #333;
}
.test-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.test-btn {
background: #007bff;
color: white;
border: none;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.test-btn:hover {
background-color: #0056b3;
}
.test-results {
max-height: 400px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
}
.test-result {
display: flex;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.test-result:last-child {
border-bottom: none;
}
.result-time {
font-size: 12px;
color: #666;
min-width: 80px;
}
.result-message {
flex: 1;
font-size: 14px;
}
.result-message.success {
color: #28a745;
}
.result-message.error {
color: #dc3545;
}
.result-message.info {
color: #17a2b8;
}
@media (max-width: 768px) {
.test-content {
flex-direction: column;
}
}
</style>

View File

@@ -1,212 +0,0 @@
<template>
<div class="api-test">
<div class="test-header">
<h2>API连接测试</h2>
<p>测试后端API接口连接</p>
</div>
<div class="test-section">
<h3>1. 测试基础连接</h3>
<button @click="testBasicConnection" class="test-btn">测试基础连接</button>
<div class="result">{{ basicResult }}</div>
</div>
<div class="test-section">
<h3>2. 测试牛只档案API</h3>
<button @click="testCattleApi" class="test-btn">测试牛只档案API</button>
<div class="result">{{ cattleResult }}</div>
</div>
<div class="test-section">
<h3>3. 测试牛只类型API</h3>
<button @click="testCattleTypesApi" class="test-btn">测试牛只类型API</button>
<div class="result">{{ typesResult }}</div>
</div>
<div class="test-section">
<h3>4. 测试栏舍API</h3>
<button @click="testPensApi" class="test-btn">测试栏舍API</button>
<div class="result">{{ pensResult }}</div>
</div>
<div class="test-section">
<h3>5. 测试批次API</h3>
<button @click="testBatchesApi" class="test-btn">测试批次API</button>
<div class="result">{{ batchesResult }}</div>
</div>
<div class="test-section">
<h3>6. 直接HTTP请求测试</h3>
<button @click="testDirectHttp" class="test-btn">直接HTTP请求</button>
<div class="result">{{ httpResult }}</div>
</div>
</div>
</template>
<script>
import { cattleApi } from '@/services/api'
import axios from 'axios'
export default {
name: 'ApiTest',
data() {
return {
basicResult: '未测试',
cattleResult: '未测试',
typesResult: '未测试',
pensResult: '未测试',
batchesResult: '未测试',
httpResult: '未测试'
}
},
methods: {
async testBasicConnection() {
this.basicResult = '测试中...'
try {
const baseURL = process.env.VUE_APP_BASE_URL || 'http://localhost:5350/api'
console.log('基础URL:', baseURL)
// 测试基础连接
const response = await axios.get(`${baseURL}/cattle-type`, {
timeout: 5000
})
this.basicResult = `连接成功!状态码: ${response.status}`
console.log('基础连接响应:', response)
} catch (error) {
this.basicResult = `连接失败: ${error.message}`
console.error('基础连接错误:', error)
}
},
async testCattleApi() {
this.cattleResult = '测试中...'
try {
const response = await cattleApi.getCattleList({ page: 1, pageSize: 5 })
this.cattleResult = `成功!数据: ${JSON.stringify(response).substring(0, 200)}...`
console.log('牛只档案API响应:', response)
} catch (error) {
this.cattleResult = `失败: ${error.message}`
console.error('牛只档案API错误:', error)
}
},
async testCattleTypesApi() {
this.typesResult = '测试中...'
try {
const response = await cattleApi.getCattleTypes()
this.typesResult = `成功!数据: ${JSON.stringify(response).substring(0, 200)}...`
console.log('牛只类型API响应:', response)
} catch (error) {
this.typesResult = `失败: ${error.message}`
console.error('牛只类型API错误:', error)
}
},
async testPensApi() {
this.pensResult = '测试中...'
try {
const response = await cattleApi.getPens()
this.pensResult = `成功!数据: ${JSON.stringify(response).substring(0, 200)}...`
console.log('栏舍API响应:', response)
} catch (error) {
this.pensResult = `失败: ${error.message}`
console.error('栏舍API错误:', error)
}
},
async testBatchesApi() {
this.batchesResult = '测试中...'
try {
const response = await cattleApi.getBatches()
this.batchesResult = `成功!数据: ${JSON.stringify(response).substring(0, 200)}...`
console.log('批次API响应:', response)
} catch (error) {
this.batchesResult = `失败: ${error.message}`
console.error('批次API错误:', error)
}
},
async testDirectHttp() {
this.httpResult = '测试中...'
try {
const baseURL = process.env.VUE_APP_BASE_URL || 'http://localhost:5350/api'
const response = await axios.get(`${baseURL}/iot-cattle/public`, {
params: { page: 1, pageSize: 5 },
timeout: 10000
})
this.httpResult = `成功!状态码: ${response.status}, 数据: ${JSON.stringify(response.data).substring(0, 200)}...`
console.log('直接HTTP响应:', response)
} catch (error) {
this.httpResult = `失败: ${error.message}`
if (error.response) {
this.httpResult += ` (状态码: ${error.response.status})`
}
console.error('直接HTTP错误:', error)
}
}
}
}
</script>
<style scoped>
.api-test {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.test-header {
text-align: center;
margin-bottom: 30px;
}
.test-header h2 {
color: #333;
margin-bottom: 10px;
}
.test-header p {
color: #666;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.test-section h3 {
margin-top: 0;
color: #333;
margin-bottom: 15px;
}
.test-btn {
padding: 10px 20px;
background-color: #34c759;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
}
.test-btn:hover {
background-color: #30b54d;
}
.result {
background-color: #fff;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -1,252 +0,0 @@
<template>
<div class="api-test-page">
<div class="header">
<h1>API接口测试</h1>
</div>
<div class="test-section">
<h2>栏舍API测试</h2>
<button @click="testCattlePens" :disabled="loading">测试栏舍API</button>
<div v-if="pensData.length > 0" class="result">
<h3>栏舍数据</h3>
<pre>{{ JSON.stringify(pensData, null, 2) }}</pre>
</div>
</div>
<div class="test-section">
<h2>转栏记录API测试</h2>
<div class="test-controls">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索关键词"
class="search-input"
/>
<button @click="testTransferRecords" :disabled="loading">测试转栏记录API</button>
<button @click="testTransferRecordsWithSearch" :disabled="loading">测试搜索功能</button>
</div>
<div v-if="transferData.length > 0" class="result">
<h3>转栏记录数据</h3>
<pre>{{ JSON.stringify(transferData, null, 2) }}</pre>
</div>
</div>
<div class="test-section">
<h2>可用牛只API测试</h2>
<button @click="testAvailableAnimals" :disabled="loading">测试可用牛只API</button>
<div v-if="animalsData.length > 0" class="result">
<h3>可用牛只数据</h3>
<pre>{{ JSON.stringify(animalsData, null, 2) }}</pre>
</div>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">错误: {{ error }}</div>
</div>
</template>
<script>
import { cattleTransferApi } from '@/services/api'
export default {
name: 'ApiTestPage',
data() {
return {
loading: false,
error: null,
pensData: [],
transferData: [],
animalsData: [],
searchKeyword: ''
}
},
methods: {
async testCattlePens() {
this.loading = true
this.error = null
try {
const response = await cattleTransferApi.getBarnsForTransfer({
page: 1,
pageSize: 10
})
console.log('栏舍API响应:', response)
this.pensData = response
} catch (error) {
console.error('栏舍API错误:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async testTransferRecords() {
this.loading = true
this.error = null
try {
const response = await cattleTransferApi.getTransferRecords({
page: 1,
pageSize: 10
})
console.log('转栏记录API响应:', response)
this.transferData = response
} catch (error) {
console.error('转栏记录API错误:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async testTransferRecordsWithSearch() {
this.loading = true
this.error = null
try {
const response = await cattleTransferApi.getTransferRecords({
page: 1,
pageSize: 10,
search: this.searchKeyword
})
console.log('转栏记录搜索API响应:', response)
this.transferData = response
} catch (error) {
console.error('转栏记录搜索API错误:', error)
this.error = error.message
} finally {
this.loading = false
}
},
async testAvailableAnimals() {
this.loading = true
this.error = null
try {
const response = await cattleTransferApi.getAvailableAnimals({
page: 1,
pageSize: 10
})
console.log('可用牛只API响应:', response)
this.animalsData = response
} catch (error) {
console.error('可用牛只API错误:', error)
this.error = error.message
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.api-test-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 30px;
}
.header h1 {
color: #333;
margin: 0;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #f9f9f9;
}
.test-section h2 {
color: #333;
margin-top: 0;
margin-bottom: 15px;
}
button {
background-color: #007aff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.test-controls {
margin-bottom: 15px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.search-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 200px;
}
.search-input:focus {
outline: none;
border-color: #007aff;
box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
}
.result {
margin-top: 20px;
padding: 15px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 6px;
}
.result h3 {
margin-top: 0;
color: #333;
}
.result pre {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.loading {
color: #007aff;
font-weight: bold;
text-align: center;
padding: 20px;
}
.error {
color: #ff3b30;
font-weight: bold;
text-align: center;
padding: 20px;
background-color: #ffe6e6;
border: 1px solid #ff3b30;
border-radius: 6px;
margin: 20px 0;
}
</style>

View File

@@ -1,388 +0,0 @@
<template>
<div class="auth-test">
<div class="test-header">
<h2>认证测试页面</h2>
<p>用于测试API认证和模拟数据功能</p>
</div>
<div class="test-section">
<h3>当前状态</h3>
<div class="status-info">
<div class="status-item">
<span class="label">认证状态:</span>
<span :class="['value', authStatus ? 'success' : 'error']">
{{ authStatus ? '已认证' : '未认证' }}
</span>
</div>
<div class="status-item">
<span class="label">Token:</span>
<span class="value">{{ currentToken || '无' }}</span>
</div>
<div class="status-item">
<span class="label">用户信息:</span>
<span class="value">{{ userInfo ? userInfo.name : '无' }}</span>
</div>
</div>
</div>
<div class="test-section">
<h3>认证操作</h3>
<div class="auth-actions">
<button @click="setTestToken" class="btn btn-primary">
设置测试Token
</button>
<button @click="clearAuth" class="btn btn-secondary">
清除认证信息
</button>
<button @click="refreshStatus" class="btn btn-info">
刷新状态
</button>
</div>
</div>
<div class="test-section">
<h3>API测试</h3>
<div class="api-actions">
<button @click="testEarTagAPI" class="btn btn-success">
测试耳标API
</button>
<button @click="testCollarAPI" class="btn btn-warning">
测试项圈API
</button>
<button @click="testAnkleAPI" class="btn btn-info">
测试脚环API
</button>
<button @click="testHostAPI" class="btn btn-danger">
测试主机API
</button>
<button @click="testAllAPIs" class="btn btn-success">
测试所有API
</button>
<button @click="setRealToken" class="btn btn-warning">
设置真实Token
</button>
</div>
</div>
<div class="test-section">
<h3>测试结果</h3>
<div class="test-results">
<div v-for="(result, index) in testResults" :key="index" class="result-item">
<span class="result-time">{{ result.time }}</span>
<span :class="['result-status', result.success ? 'success' : 'error']">
{{ result.success ? '成功' : '失败' }}
</span>
<span class="result-message">{{ result.message }}</span>
</div>
</div>
</div>
<div class="test-footer">
<button @click="goHome" class="btn btn-primary">
返回首页
</button>
</div>
</div>
</template>
<script>
import auth from '@/utils/auth'
import { getAllEarTagDevices } from '@/services/earTagService'
import { getCollarDevices } from '@/services/collarService'
import { getAnkleDevices } from '@/services/ankleService'
import { getHostDevices } from '@/services/hostService'
export default {
name: 'AuthTest',
data() {
return {
authStatus: false,
currentToken: '',
userInfo: null,
testResults: []
}
},
mounted() {
this.refreshStatus()
},
methods: {
refreshStatus() {
this.authStatus = auth.isAuthenticated()
this.currentToken = auth.getToken()
this.userInfo = auth.getUserInfo()
},
setTestToken() {
auth.setTestToken()
auth.setUserInfo({
id: 'test-user-001',
name: 'AIOTAGRO',
phone: '15586823774',
role: 'admin'
})
this.refreshStatus()
this.addTestResult('设置测试Token成功', true)
},
clearAuth() {
auth.logout()
this.refreshStatus()
this.addTestResult('清除认证信息成功', true)
},
async testEarTagAPI() {
try {
// 确保有有效的token
if (!auth.isAuthenticated()) {
await auth.setTestToken()
this.addTestResult('自动设置测试token', true)
}
const result = await getAllEarTagDevices()
this.addTestResult(`耳标API测试成功获取到 ${result.data?.length || 0} 条数据`, true)
} catch (error) {
this.addTestResult(`耳标API测试失败: ${error.message}`, false)
}
},
async testCollarAPI() {
try {
const result = await getCollarDevices()
this.addTestResult(`项圈API测试成功获取到 ${result.data?.length || 0} 条数据`, true)
} catch (error) {
this.addTestResult(`项圈API测试失败: ${error.message}`, false)
}
},
async testAnkleAPI() {
try {
const result = await getAnkleDevices()
this.addTestResult(`脚环API测试成功获取到 ${result.data?.length || 0} 条数据`, true)
} catch (error) {
this.addTestResult(`脚环API测试失败: ${error.message}`, false)
}
},
async testHostAPI() {
try {
const result = await getHostDevices()
this.addTestResult(`主机API测试成功获取到 ${result.data?.length || 0} 条数据`, true)
} catch (error) {
this.addTestResult(`主机API测试失败: ${error.message}`, false)
}
},
async testAllAPIs() {
this.addTestResult('开始测试所有API...', true)
// 确保有有效的token
if (!auth.isAuthenticated()) {
await auth.setTestToken()
this.addTestResult('自动设置测试token', true)
}
// 依次测试所有API
await this.testEarTagAPI()
await this.testCollarAPI()
await this.testAnkleAPI()
await this.testHostAPI()
this.addTestResult('所有API测试完成', true)
},
setRealToken() {
try {
auth.setRealToken()
this.addTestResult('手动设置真实token成功', true)
this.refreshStatus()
} catch (error) {
this.addTestResult(`设置真实token失败: ${error.message}`, false)
}
},
addTestResult(message, success) {
this.testResults.unshift({
time: new Date().toLocaleTimeString(),
message,
success
})
// 只保留最近10条结果
if (this.testResults.length > 10) {
this.testResults = this.testResults.slice(0, 10)
}
},
goHome() {
this.$router.push('/')
}
}
}
</script>
<style scoped>
.auth-test {
padding: 20px;
max-width: 800px;
margin: 0 auto;
background-color: #ffffff;
min-height: 100vh;
}
.test-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
}
.test-header h2 {
color: #333;
margin-bottom: 10px;
}
.test-header p {
color: #666;
font-size: 14px;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.test-section h3 {
color: #333;
margin-bottom: 15px;
font-size: 16px;
}
.status-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e0e0e0;
}
.status-item:last-child {
border-bottom: none;
}
.label {
font-weight: 500;
color: #666;
}
.value {
font-weight: 600;
}
.value.success {
color: #52c41a;
}
.value.error {
color: #f5222d;
}
.auth-actions, .api-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background-color: #1890ff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.test-results {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
background-color: white;
}
.result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 0;
border-bottom: 1px solid #f0f0f0;
}
.result-item:last-child {
border-bottom: none;
}
.result-time {
font-size: 12px;
color: #999;
min-width: 80px;
}
.result-status {
font-size: 12px;
font-weight: 600;
min-width: 40px;
}
.result-status.success {
color: #52c41a;
}
.result-status.error {
color: #f5222d;
}
.result-message {
flex: 1;
font-size: 13px;
color: #333;
}
.test-footer {
text-align: center;
padding-top: 20px;
border-top: 2px solid #f0f0f0;
}
</style>

View File

@@ -1,482 +0,0 @@
<template>
<div class="cattle-add">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon"></span>
</div>
<div class="title">新增档案</div>
<div class="header-actions">
<span class="action-icon" @click="saveCattle">保存</span>
</div>
</div>
<!-- 表单内容 -->
<div class="form-container">
<div class="form-section">
<div class="section-title">基本信息</div>
<div class="form-group">
<label class="form-label">耳号 *</label>
<input
v-model="formData.earNumber"
type="number"
class="form-input"
placeholder="请输入耳号"
/>
</div>
<div class="form-group">
<label class="form-label">性别 *</label>
<select v-model="formData.sex" class="form-select">
<option value="">请选择性别</option>
<option value="1"></option>
<option value="2"></option>
</select>
</div>
<div class="form-group">
<label class="form-label">品类 *</label>
<select v-model="formData.cate" class="form-select">
<option value="">请选择品类</option>
<option value="1">犊牛</option>
<option value="2">育成母牛</option>
<option value="3">架子牛</option>
<option value="4">青年牛</option>
<option value="5">基础母牛</option>
<option value="6">育肥牛</option>
</select>
</div>
<div class="form-group">
<label class="form-label">品种 *</label>
<select v-model="formData.varieties" class="form-select">
<option value="">请选择品种</option>
<option v-for="type in cattleTypes" :key="type.id" :value="type.id">
{{ type.name }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label">品系 *</label>
<select v-model="formData.strain" class="form-select">
<option value="">请选择品系</option>
<option value="1">乳肉兼用</option>
<option value="2">肉用型</option>
<option value="3">乳用型</option>
</select>
</div>
</div>
<div class="form-section">
<div class="section-title">出生信息</div>
<div class="form-group">
<label class="form-label">出生体重(kg) *</label>
<input
v-model="formData.birthWeight"
type="number"
step="0.1"
class="form-input"
placeholder="请输入出生体重"
/>
</div>
<div class="form-group">
<label class="form-label">出生日期 *</label>
<input
v-model="formData.birthday"
type="date"
class="form-input"
/>
</div>
</div>
<div class="form-section">
<div class="section-title">管理信息</div>
<div class="form-group">
<label class="form-label">栏舍</label>
<select v-model="formData.penId" class="form-select">
<option value="">请选择栏舍</option>
<option v-for="pen in pens" :key="pen.id" :value="pen.id">
{{ pen.name }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label">批次</label>
<select v-model="formData.batchId" class="form-select">
<option value="">请选择批次</option>
<option v-for="batch in batches" :key="batch.id" :value="batch.id">
{{ batch.name }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label">入栏时间</label>
<input
v-model="formData.intoTime"
type="date"
class="form-input"
/>
</div>
<div class="form-group">
<label class="form-label">当前体重(kg)</label>
<input
v-model="formData.currentWeight"
type="number"
step="0.1"
class="form-input"
placeholder="请输入当前体重"
/>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<div class="loading-text">保存中...</div>
</div>
<!-- 底部按钮 -->
<div class="bottom-actions">
<button @click="goBack" class="btn btn-cancel">取消</button>
<button @click="saveCattle" class="btn btn-save" :disabled="loading">
{{ loading ? '保存中...' : '保存' }}
</button>
</div>
</div>
</template>
<script>
import { cattleApi } from '@/services/api'
export default {
name: 'CattleAdd',
data() {
return {
loading: false,
cattleTypes: [],
pens: [],
batches: [],
formData: {
earNumber: '',
sex: '',
cate: '',
varieties: '',
strain: '',
birthWeight: '',
birthday: '',
penId: '',
batchId: '',
intoTime: '',
currentWeight: '',
orgId: 1 // 默认农场ID
}
}
},
mounted() {
this.loadReferenceData()
},
methods: {
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 加载参考数据
async loadReferenceData() {
try {
// 加载牛只类型
const typesResponse = await cattleApi.getCattleTypes()
if (typesResponse.success) {
this.cattleTypes = typesResponse.data || []
}
// 加载栏舍列表
const pensResponse = await cattleApi.getPens()
if (pensResponse.success) {
this.pens = pensResponse.data || []
}
// 加载批次列表
const batchesResponse = await cattleApi.getBatches()
if (batchesResponse.success) {
this.batches = batchesResponse.data || []
}
} catch (error) {
console.error('加载参考数据失败:', error)
this.$message.error('加载参考数据失败')
}
},
// 保存牛只档案
async saveCattle() {
// 验证必填字段
if (!this.validateForm()) {
return
}
this.loading = true
try {
// 格式化数据
const cattleData = {
...this.formData,
earNumber: parseInt(this.formData.earNumber),
sex: parseInt(this.formData.sex),
cate: parseInt(this.formData.cate),
varieties: parseInt(this.formData.varieties),
strain: parseInt(this.formData.strain),
birthWeight: parseFloat(this.formData.birthWeight),
birthday: this.formatDateToTimestamp(this.formData.birthday),
penId: this.formData.penId ? parseInt(this.formData.penId) : 0,
batchId: this.formData.batchId ? parseInt(this.formData.batchId) : 0,
intoTime: this.formData.intoTime ? this.formatDateToTimestamp(this.formData.intoTime) : 0,
weight: this.formData.currentWeight ? parseFloat(this.formData.currentWeight) : 0,
orgId: parseInt(this.formData.orgId)
}
// 调用API创建牛只档案
const response = await cattleApi.createCattle(cattleData)
if (response.success) {
this.$message.success('牛只档案创建成功')
this.$router.push('/cattle-profile')
} else {
this.$message.error(response.message || '创建牛只档案失败')
}
} catch (error) {
console.error('创建牛只档案失败:', error)
this.$message.error('创建牛只档案失败')
} finally {
this.loading = false
}
},
// 验证表单
validateForm() {
const requiredFields = [
{ field: 'earNumber', label: '耳号' },
{ field: 'sex', label: '性别' },
{ field: 'cate', label: '品类' },
{ field: 'varieties', label: '品种' },
{ field: 'strain', label: '品系' },
{ field: 'birthWeight', label: '出生体重' },
{ field: 'birthday', label: '出生日期' }
]
for (const { field, label } of requiredFields) {
if (!this.formData[field]) {
this.$message.error(`请填写${label}`)
return false
}
}
return true
},
// 格式化日期为时间戳
formatDateToTimestamp(dateString) {
if (!dateString) return 0
return Math.floor(new Date(dateString).getTime() / 1000)
}
}
}
</script>
<style scoped>
.cattle-add {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 80px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.back-icon {
font-size: 24px;
color: #000000;
font-weight: bold;
}
.title {
font-size: 18px;
font-weight: 600;
color: #000000;
}
.header-actions {
display: flex;
gap: 12px;
}
.action-icon {
font-size: 16px;
color: #34c759;
cursor: pointer;
font-weight: 500;
}
/* 表单容器 */
.form-container {
padding: 16px;
}
.form-section {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
color: #333333;
margin-bottom: 8px;
font-weight: 500;
}
.form-input,
.form-select {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
color: #333333;
background-color: #ffffff;
box-sizing: border-box;
}
.form-input:focus,
.form-select:focus {
outline: none;
border-color: #34c759;
box-shadow: 0 0 0 2px rgba(52, 199, 89, 0.2);
}
.form-input::placeholder {
color: #999999;
}
/* 加载状态 */
.loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #34c759;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 12px;
font-size: 14px;
color: #ffffff;
}
/* 底部按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 16px;
display: flex;
gap: 12px;
border-top: 1px solid #e0e0e0;
z-index: 100;
}
.btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666666;
}
.btn-cancel:hover {
background-color: #e0e0e0;
}
.btn-save {
background-color: #34c759;
color: #ffffff;
}
.btn-save:hover:not(:disabled) {
background-color: #30b54d;
}
.btn-save:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

View File

@@ -1,829 +0,0 @@
<template>
<div class="cattle-batch">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon">&lt;</span>
</div>
<div class="title">批次设置</div>
<div class="status-icons">
<span class="icon">...</span>
<span class="icon">-</span>
<span class="icon">o</span>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchName"
type="text"
placeholder="请输入批次名称(精确匹配)"
@input="handleSearch"
class="search-input"
/>
</div>
</div>
<!-- 批量操作栏 -->
<div class="batch-actions" v-if="batches.length > 0">
<div class="batch-controls">
<label class="batch-checkbox">
<input
type="checkbox"
v-model="selectAll"
@change="toggleSelectAll"
/>
<span>全选</span>
</label>
<span class="selected-count">已选择 {{ selectedBatches.length }} </span>
<button
class="batch-delete-btn"
@click="batchDelete"
:disabled="selectedBatches.length === 0"
>
批量删除
</button>
</div>
</div>
<!-- 批次列表 -->
<div class="batches-list" v-if="batches.length > 0">
<div
v-for="(batch, index) in batches"
:key="batch.id"
class="batch-card"
:class="{ selected: selectedBatches.includes(batch.id) }"
>
<div class="batch-checkbox">
<input
type="checkbox"
:value="batch.id"
v-model="selectedBatches"
/>
</div>
<div class="batch-content" @click="selectBatch(batch)">
<div class="batch-header">
<div class="batch-name">
<span class="label">批次名称:</span>
<span class="value">{{ batch.name || '--' }}</span>
</div>
<div class="batch-actions">
<button class="edit-btn" @click.stop="editBatch(batch)">编辑</button>
<button class="delete-btn" @click.stop="deleteBatch(batch)">删除</button>
</div>
</div>
<div class="batch-details">
<div class="detail-item">
<span class="label">批次编号:</span>
<span class="value">{{ batch.code || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">批次类型:</span>
<span class="value">{{ getBatchTypeName(batch.type) }}</span>
</div>
<div class="detail-item">
<span class="label">目标数量:</span>
<span class="value">{{ batch.targetCount || 0 }} </span>
</div>
<div class="detail-item">
<span class="label">当前数量:</span>
<span class="value">{{ batch.currentCount || 0 }} </span>
</div>
<div class="detail-item">
<span class="label">负责人:</span>
<span class="value">{{ batch.manager || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">开始日期:</span>
<span class="value">{{ formatDate(batch.startDate) }}</span>
</div>
<div class="detail-item">
<span class="label">预计结束:</span>
<span class="value">{{ formatDate(batch.expectedEndDate) }}</span>
</div>
<div class="detail-item">
<span class="label">实际结束:</span>
<span class="value">{{ formatDate(batch.actualEndDate) || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value status" :class="getStatusClass(batch.status)">
{{ getBatchStatusName(batch.status) }}
</span>
</div>
<div class="detail-item" v-if="batch.remark">
<span class="label">备注:</span>
<span class="value">{{ batch.remark }}</span>
</div>
<div class="detail-item">
<span class="label">创建时间:</span>
<span class="value">{{ formatDateTime(batch.created_at) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else-if="!loading">
<div class="empty-icon">📦</div>
<div class="empty-text">暂无批次数据</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="loading-text">加载中...</div>
</div>
<!-- 分页控件 -->
<div class="pagination" v-if="batches.length > 0">
<button
class="page-btn"
@click="goToPreviousPage"
:disabled="currentPage <= 1"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} {{ totalPages }}
</span>
<button
class="page-btn"
@click="goToNextPage"
:disabled="currentPage >= totalPages"
>
下一页
</button>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<button class="add-btn" @click="addBatch">
添加批次
</button>
</div>
</div>
</template>
<script>
import { cattleBatchApi } from '@/services/api'
import auth from '@/utils/auth'
import { getBatchTypeName, getBatchStatusName } from '@/utils/mapping'
export default {
name: 'CattleBatch',
data() {
return {
searchName: '',
currentBatch: null,
batches: [],
loading: false,
currentPage: 1,
totalPages: 1,
pageSize: 10,
searchTimer: null,
selectedBatches: [],
selectAll: false,
showEditDialog: false,
editingBatch: null
}
},
async mounted() {
// 确保有有效的认证token
await this.ensureAuthentication()
this.loadBatches()
},
methods: {
// 确保认证
async ensureAuthentication() {
try {
// 检查是否已有token
if (!auth.isAuthenticated()) {
console.log('未认证尝试获取测试token...')
await auth.setTestToken()
} else {
// 验证现有token是否有效
const isValid = await auth.validateCurrentToken()
if (!isValid) {
console.log('Token无效重新获取...')
await auth.setTestToken()
}
}
console.log('认证完成token:', auth.getToken()?.substring(0, 20) + '...')
} catch (error) {
console.error('认证失败:', error)
// 即使认证失败也继续让API请求处理错误
}
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 获取批次类型中文名称
getBatchTypeName(batchType) {
return getBatchTypeName(batchType)
},
// 获取批次状态中文名称
getBatchStatusName(batchStatus) {
return getBatchStatusName(batchStatus)
},
// 获取状态样式类
getStatusClass(status) {
const statusClassMap = {
'进行中': 'status-active',
'已完成': 'status-completed',
'已暂停': 'status-paused',
'已取消': 'status-cancelled',
'待开始': 'status-pending'
}
return statusClassMap[status] || ''
},
// 选择批次
selectBatch(batch) {
this.currentBatch = batch
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedBatches = this.batches.map(batch => batch.id)
} else {
this.selectedBatches = []
}
},
// 编辑批次
editBatch(batch) {
this.editingBatch = batch
this.showEditDialog = true
},
// 删除批次
async deleteBatch(batch) {
if (confirm(`确定要删除批次 "${batch.name}" 吗?`)) {
try {
await cattleBatchApi.deleteBatch(batch.id)
this.$message && this.$message.success('删除成功')
this.loadBatches()
} catch (error) {
console.error('删除批次失败:', error)
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
}
}
},
// 加载批次列表
async loadBatches() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchName) {
params.search = this.searchName
}
const response = await cattleBatchApi.getBatches(params)
if (response && response.success && response.data && response.data.list) {
// 标准API响应格式
let batches = response.data.list
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
batches = batches.filter(batch => batch.name === this.searchName)
}
this.batches = batches
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
// 显示第一条记录
if (this.batches.length > 0) {
this.currentBatch = this.batches[0]
} else {
this.currentBatch = null
}
} else if (response && response.data && Array.isArray(response.data)) {
// 兼容直接返回数组的格式
let batches = response.data
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
batches = batches.filter(batch => batch.name === this.searchName)
}
this.batches = batches
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
if (this.batches.length > 0) {
this.currentBatch = this.batches[0]
} else {
this.currentBatch = null
}
} else if (Array.isArray(response)) {
// 如果直接返回数组
let batches = response
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
batches = batches.filter(batch => batch.name === this.searchName)
}
this.batches = batches
this.totalPages = 1
if (this.batches.length > 0) {
this.currentBatch = this.batches[0]
} else {
this.currentBatch = null
}
} else {
console.warn('API响应格式不正确:', response)
this.batches = []
this.currentBatch = null
this.totalPages = 1
}
} catch (error) {
console.error('加载批次列表失败:', error)
this.$message && this.$message.error('加载批次列表失败')
this.batches = []
this.currentBatch = null
} finally {
this.loading = false
}
},
// 搜索处理
handleSearch() {
// 防抖处理
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.currentPage = 1
this.loadBatches()
}, 500)
},
// 批量删除
async batchDelete() {
if (this.selectedBatches.length === 0) {
this.$message && this.$message.warning('请选择要删除的批次')
return
}
if (confirm(`确定要删除选中的 ${this.selectedBatches.length} 个批次吗?`)) {
try {
await cattleBatchApi.batchDeleteBatches(this.selectedBatches)
this.$message && this.$message.success('批量删除成功')
this.selectedBatches = []
this.selectAll = false
this.loadBatches()
} catch (error) {
console.error('批量删除批次失败:', error)
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
}
}
},
// 添加批次
addBatch() {
// 跳转到添加批次页面
this.$router.push('/cattle-batch-add')
},
// 上一页
goToPreviousPage() {
if (this.currentPage > 1) {
this.currentPage--
this.loadBatches()
}
},
// 下一页
goToNextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.loadBatches()
}
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return '--'
try {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
} catch (error) {
return dateString
}
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return '--'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
return dateTime
}
}
}
}
</script>
<style scoped>
.cattle-batch {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 100px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
cursor: pointer;
padding: 4px;
}
.back-icon {
font-size: 18px;
color: #333;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-icons {
display: flex;
gap: 8px;
}
.icon {
font-size: 14px;
color: #666;
cursor: pointer;
}
/* 搜索栏 */
.search-section {
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
background-color: #f8f8f8;
border-radius: 20px;
padding: 8px 16px;
}
.search-icon {
font-size: 16px;
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 批量操作栏 */
.batch-actions {
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.batch-controls {
display: flex;
align-items: center;
gap: 16px;
}
.batch-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.selected-count {
font-size: 14px;
color: #666;
}
.batch-delete-btn {
padding: 6px 12px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.batch-delete-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 批次列表 */
.batches-list {
padding: 16px 20px;
}
.batch-card {
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-start;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.batch-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.batch-card.selected {
border: 2px solid #007bff;
background-color: #f0f8ff;
}
.batch-checkbox {
margin-right: 12px;
margin-top: 4px;
}
.batch-content {
flex: 1;
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.batch-name {
display: flex;
align-items: center;
gap: 8px;
}
.label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.value {
font-size: 14px;
color: #333;
font-weight: 600;
}
.batch-actions {
display: flex;
gap: 8px;
}
.edit-btn, .delete-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.edit-btn {
background-color: #007bff;
color: white;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
.batch-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.detail-item .label {
color: #666;
min-width: 60px;
}
.detail-item .value {
color: #333;
font-weight: normal;
}
.status {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-active {
background-color: #d4edda;
color: #155724;
}
.status-completed {
background-color: #cce5ff;
color: #004085;
}
.status-paused {
background-color: #fff3cd;
color: #856404;
}
.status-cancelled {
background-color: #f8d7da;
color: #721c24;
}
.status-pending {
background-color: #e2e3e5;
color: #383d41;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: #999;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
}
.loading-text {
font-size: 14px;
color: #666;
}
/* 分页控件 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.page-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.page-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #666;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.add-btn {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.add-btn:hover {
background-color: #218838;
}
/* 响应式设计 */
@media (max-width: 768px) {
.batch-details {
grid-template-columns: 1fr;
}
.batch-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.batch-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -1,781 +0,0 @@
<template>
<div class="cattle-exit">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon">&lt;</span>
</div>
<div class="title">牛只离栏</div>
<div class="status-icons">
<span class="icon">...</span>
<span class="icon">-</span>
<span class="icon">o</span>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchEarNumber"
type="text"
placeholder="请输入耳号"
@input="handleSearch"
class="search-input"
/>
</div>
</div>
<!-- 批量操作栏 -->
<div class="batch-actions" v-if="records.length > 0">
<div class="batch-controls">
<label class="batch-checkbox">
<input
type="checkbox"
v-model="selectAll"
@change="toggleSelectAll"
/>
<span>全选</span>
</label>
<span class="selected-count">已选择 {{ selectedRecords.length }} </span>
<button
class="batch-delete-btn"
@click="batchDelete"
:disabled="selectedRecords.length === 0"
>
批量删除
</button>
</div>
</div>
<!-- 记录列表 -->
<div class="records-list" v-if="records.length > 0">
<div
v-for="(record, index) in records"
:key="record.id"
class="record-card"
:class="{ selected: selectedRecords.includes(record.id) }"
>
<div class="record-checkbox">
<input
type="checkbox"
:value="record.id"
v-model="selectedRecords"
/>
</div>
<div class="record-content" @click="selectRecord(record)">
<div class="record-header">
<div class="ear-number">
<span class="label">耳号:</span>
<span class="value">{{ record.earNumber || '--' }}</span>
</div>
<div class="record-actions">
<button class="edit-btn" @click.stop="editRecord(record)">编辑</button>
<button class="delete-btn" @click.stop="deleteRecord(record)">删除</button>
</div>
</div>
<div class="record-details">
<div class="detail-item">
<span class="label">离栏日期:</span>
<span class="value">{{ formatDateTime(record.exitDate) }}</span>
</div>
<div class="detail-item">
<span class="label">原栏舍:</span>
<span class="value">{{ getOriginalPenName(record) }}</span>
</div>
<div class="detail-item">
<span class="label">离栏原因:</span>
<span class="value">{{ getExitReasonName(record.exitReason) }}</span>
</div>
<div class="detail-item">
<span class="label">登记人:</span>
<span class="value">{{ record.handler || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">处理方式:</span>
<span class="value">{{ getDisposalMethodName(record.disposalMethod) }}</span>
</div>
<div class="detail-item">
<span class="label">登记日期:</span>
<span class="value">{{ formatDateTime(record.created_at) }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value status" :class="getStatusClass(record.status)">
{{ getExitStatusName(record.status) }}
</span>
</div>
<div class="detail-item" v-if="record.remark">
<span class="label">备注:</span>
<span class="value">{{ record.remark }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else-if="!loading">
<div class="empty-icon">🐄</div>
<div class="empty-text">暂无离栏记录</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="loading-text">加载中...</div>
</div>
<!-- 分页控件 -->
<div class="pagination" v-if="records.length > 0">
<button
class="page-btn"
@click="goToPreviousPage"
:disabled="currentPage <= 1"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} {{ totalPages }}
</span>
<button
class="page-btn"
@click="goToNextPage"
:disabled="currentPage >= totalPages"
>
下一页
</button>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<button class="register-btn" @click="registerExit">
离栏登记
</button>
</div>
</div>
</template>
<script>
import { cattleExitApi } from '@/services/api'
import auth from '@/utils/auth'
import { getExitReasonName, getDisposalMethodName, getExitStatusName } from '@/utils/mapping'
export default {
name: 'CattleExit',
data() {
return {
searchEarNumber: '',
currentRecord: null,
records: [],
loading: false,
currentPage: 1,
totalPages: 1,
pageSize: 10,
searchTimer: null,
selectedRecords: [],
selectAll: false,
showEditDialog: false,
editingRecord: null
}
},
async mounted() {
// 确保有有效的认证token
await this.ensureAuthentication()
this.loadExitRecords()
},
methods: {
// 确保认证
async ensureAuthentication() {
try {
// 检查是否已有token
if (!auth.isAuthenticated()) {
console.log('未认证尝试获取测试token...')
await auth.setTestToken()
} else {
// 验证现有token是否有效
const isValid = await auth.validateCurrentToken()
if (!isValid) {
console.log('Token无效重新获取...')
await auth.setTestToken()
}
}
console.log('认证完成token:', auth.getToken()?.substring(0, 20) + '...')
} catch (error) {
console.error('认证失败:', error)
// 即使认证失败也继续让API请求处理错误
}
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 获取原栏舍名称
getOriginalPenName(record) {
if (record && record.originalPen && record.originalPen.name) {
return record.originalPen.name
}
return '--'
},
// 获取离栏原因中文名称
getExitReasonName(exitReason) {
return getExitReasonName(exitReason)
},
// 获取处理方式中文名称
getDisposalMethodName(disposalMethod) {
return getDisposalMethodName(disposalMethod)
},
// 获取离栏状态中文名称
getExitStatusName(exitStatus) {
return getExitStatusName(exitStatus)
},
// 获取状态样式类
getStatusClass(status) {
const statusClassMap = {
'已确认': 'status-completed',
'待确认': 'status-pending',
'已取消': 'status-cancelled'
}
return statusClassMap[status] || ''
},
// 选择记录
selectRecord(record) {
this.currentRecord = record
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedRecords = this.records.map(record => record.id)
} else {
this.selectedRecords = []
}
},
// 编辑记录
editRecord(record) {
this.editingRecord = record
this.showEditDialog = true
},
// 删除记录
async deleteRecord(record) {
if (confirm(`确定要删除耳号为 ${record.earNumber} 的离栏记录吗?`)) {
try {
await cattleExitApi.deleteExitRecord(record.id)
this.$message && this.$message.success('删除成功')
this.loadExitRecords()
} catch (error) {
console.error('删除离栏记录失败:', error)
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
}
}
},
// 加载离栏记录
async loadExitRecords() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchEarNumber) {
params.search = this.searchEarNumber
}
const response = await cattleExitApi.getExitRecords(params)
if (response && response.success && response.data && response.data.list) {
// 标准API响应格式
this.records = response.data.list
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
// 显示第一条记录
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else if (response && response.data && Array.isArray(response.data)) {
// 兼容直接返回数组的格式
this.records = response.data
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else if (Array.isArray(response)) {
// 如果直接返回数组
this.records = response
this.totalPages = 1
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else {
console.warn('API响应格式不正确:', response)
this.records = []
this.currentRecord = null
this.totalPages = 1
}
} catch (error) {
console.error('加载离栏记录失败:', error)
this.$message && this.$message.error('加载离栏记录失败')
this.records = []
this.currentRecord = null
} finally {
this.loading = false
}
},
// 搜索处理
handleSearch() {
// 防抖处理
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.currentPage = 1
this.loadExitRecords()
}, 500)
},
// 批量删除
async batchDelete() {
if (this.selectedRecords.length === 0) {
this.$message && this.$message.warning('请选择要删除的记录')
return
}
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条离栏记录吗?`)) {
try {
await cattleExitApi.batchDeleteExitRecords(this.selectedRecords)
this.$message && this.$message.success('批量删除成功')
this.selectedRecords = []
this.selectAll = false
this.loadExitRecords()
} catch (error) {
console.error('批量删除离栏记录失败:', error)
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
}
}
},
// 离栏登记
registerExit() {
// 跳转到离栏登记页面
this.$router.push('/cattle-exit-register')
},
// 上一页
goToPreviousPage() {
if (this.currentPage > 1) {
this.currentPage--
this.loadExitRecords()
}
},
// 下一页
goToNextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.loadExitRecords()
}
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return '--'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
return dateTime
}
}
}
}
</script>
<style scoped>
.cattle-exit {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 100px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
cursor: pointer;
padding: 4px;
}
.back-icon {
font-size: 18px;
color: #333;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-icons {
display: flex;
gap: 8px;
}
.icon {
font-size: 14px;
color: #666;
cursor: pointer;
}
/* 搜索栏 */
.search-section {
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
background-color: #f8f8f8;
border-radius: 20px;
padding: 8px 16px;
}
.search-icon {
font-size: 16px;
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 批量操作栏 */
.batch-actions {
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.batch-controls {
display: flex;
align-items: center;
gap: 16px;
}
.batch-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.selected-count {
font-size: 14px;
color: #666;
}
.batch-delete-btn {
padding: 6px 12px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.batch-delete-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 记录列表 */
.records-list {
padding: 16px 20px;
}
.record-card {
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-start;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.record-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.record-card.selected {
border: 2px solid #007bff;
background-color: #f0f8ff;
}
.record-checkbox {
margin-right: 12px;
margin-top: 4px;
}
.record-content {
flex: 1;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.ear-number {
display: flex;
align-items: center;
gap: 8px;
}
.label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.value {
font-size: 14px;
color: #333;
font-weight: 600;
}
.record-actions {
display: flex;
gap: 8px;
}
.edit-btn, .delete-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.edit-btn {
background-color: #007bff;
color: white;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
.record-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.detail-item .label {
color: #666;
min-width: 60px;
}
.detail-item .value {
color: #333;
font-weight: normal;
}
.status {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background-color: #fff3cd;
color: #856404;
}
.status-completed {
background-color: #d4edda;
color: #155724;
}
.status-cancelled {
background-color: #f8d7da;
color: #721c24;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: #999;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
}
.loading-text {
font-size: 14px;
color: #666;
}
/* 分页控件 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.page-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.page-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #666;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.register-btn {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.register-btn:hover {
background-color: #218838;
}
/* 响应式设计 */
@media (max-width: 768px) {
.record-details {
grid-template-columns: 1fr;
}
.record-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.record-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -1,799 +0,0 @@
<template>
<div class="cattle-pen">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon">&lt;</span>
</div>
<div class="title">栏舍设置</div>
<div class="status-icons">
<span class="icon">...</span>
<span class="icon">-</span>
<span class="icon">o</span>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchName"
type="text"
placeholder="请输入栏舍名称(精确匹配)"
@input="handleSearch"
class="search-input"
/>
</div>
</div>
<!-- 批量操作栏 -->
<div class="batch-actions" v-if="pens.length > 0">
<div class="batch-controls">
<label class="batch-checkbox">
<input
type="checkbox"
v-model="selectAll"
@change="toggleSelectAll"
/>
<span>全选</span>
</label>
<span class="selected-count">已选择 {{ selectedPens.length }} </span>
<button
class="batch-delete-btn"
@click="batchDelete"
:disabled="selectedPens.length === 0"
>
批量删除
</button>
</div>
</div>
<!-- 栏舍列表 -->
<div class="pens-list" v-if="pens.length > 0">
<div
v-for="(pen, index) in pens"
:key="pen.id"
class="pen-card"
:class="{ selected: selectedPens.includes(pen.id) }"
>
<div class="pen-checkbox">
<input
type="checkbox"
:value="pen.id"
v-model="selectedPens"
/>
</div>
<div class="pen-content" @click="selectPen(pen)">
<div class="pen-header">
<div class="pen-name">
<span class="label">栏舍名称:</span>
<span class="value">{{ pen.name || '--' }}</span>
</div>
<div class="pen-actions">
<button class="edit-btn" @click.stop="editPen(pen)">编辑</button>
<button class="delete-btn" @click.stop="deletePen(pen)">删除</button>
</div>
</div>
<div class="pen-details">
<div class="detail-item">
<span class="label">栏舍编号:</span>
<span class="value">{{ pen.code || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">栏舍类型:</span>
<span class="value">{{ getPenTypeName(pen.type) }}</span>
</div>
<div class="detail-item">
<span class="label">容量:</span>
<span class="value">{{ pen.capacity || 0 }} </span>
</div>
<div class="detail-item">
<span class="label">当前数量:</span>
<span class="value">{{ pen.currentCount || 0 }} </span>
</div>
<div class="detail-item">
<span class="label">面积:</span>
<span class="value">{{ pen.area || '--' }} </span>
</div>
<div class="detail-item">
<span class="label">位置:</span>
<span class="value">{{ pen.location || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value status" :class="getStatusClass(pen.status)">
{{ getPenStatusName(pen.status) }}
</span>
</div>
<div class="detail-item" v-if="pen.remark">
<span class="label">备注:</span>
<span class="value">{{ pen.remark }}</span>
</div>
<div class="detail-item">
<span class="label">创建时间:</span>
<span class="value">{{ formatDateTime(pen.created_at) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else-if="!loading">
<div class="empty-icon">🏠</div>
<div class="empty-text">暂无栏舍数据</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="loading-text">加载中...</div>
</div>
<!-- 分页控件 -->
<div class="pagination" v-if="pens.length > 0">
<button
class="page-btn"
@click="goToPreviousPage"
:disabled="currentPage <= 1"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} {{ totalPages }}
</span>
<button
class="page-btn"
@click="goToNextPage"
:disabled="currentPage >= totalPages"
>
下一页
</button>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<button class="add-btn" @click="addPen">
添加栏舍
</button>
</div>
</div>
</template>
<script>
import { cattlePenApi } from '@/services/api'
import auth from '@/utils/auth'
import { getPenTypeName, getPenStatusName } from '@/utils/mapping'
export default {
name: 'CattlePen',
data() {
return {
searchName: '',
currentPen: null,
pens: [],
loading: false,
currentPage: 1,
totalPages: 1,
pageSize: 10,
searchTimer: null,
selectedPens: [],
selectAll: false,
showEditDialog: false,
editingPen: null
}
},
async mounted() {
// 确保有有效的认证token
await this.ensureAuthentication()
this.loadPens()
},
methods: {
// 确保认证
async ensureAuthentication() {
try {
// 检查是否已有token
if (!auth.isAuthenticated()) {
console.log('未认证尝试获取测试token...')
await auth.setTestToken()
} else {
// 验证现有token是否有效
const isValid = await auth.validateCurrentToken()
if (!isValid) {
console.log('Token无效重新获取...')
await auth.setTestToken()
}
}
console.log('认证完成token:', auth.getToken()?.substring(0, 20) + '...')
} catch (error) {
console.error('认证失败:', error)
// 即使认证失败也继续让API请求处理错误
}
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 获取栏舍类型中文名称
getPenTypeName(penType) {
return getPenTypeName(penType)
},
// 获取栏舍状态中文名称
getPenStatusName(penStatus) {
return getPenStatusName(penStatus)
},
// 获取状态样式类
getStatusClass(status) {
const statusClassMap = {
'启用': 'status-active',
'停用': 'status-inactive',
'维修': 'status-maintenance',
'废弃': 'status-abandoned'
}
return statusClassMap[status] || ''
},
// 选择栏舍
selectPen(pen) {
this.currentPen = pen
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedPens = this.pens.map(pen => pen.id)
} else {
this.selectedPens = []
}
},
// 编辑栏舍
editPen(pen) {
this.editingPen = pen
this.showEditDialog = true
},
// 删除栏舍
async deletePen(pen) {
if (confirm(`确定要删除栏舍 "${pen.name}" 吗?`)) {
try {
await cattlePenApi.deletePen(pen.id)
this.$message && this.$message.success('删除成功')
this.loadPens()
} catch (error) {
console.error('删除栏舍失败:', error)
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
}
}
},
// 加载栏舍列表
async loadPens() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchName) {
params.search = this.searchName
}
const response = await cattlePenApi.getPens(params)
if (response && response.success && response.data && response.data.list) {
// 标准API响应格式
let pens = response.data.list
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
pens = pens.filter(pen => pen.name === this.searchName)
}
this.pens = pens
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
// 显示第一条记录
if (this.pens.length > 0) {
this.currentPen = this.pens[0]
} else {
this.currentPen = null
}
} else if (response && response.data && Array.isArray(response.data)) {
// 兼容直接返回数组的格式
let pens = response.data
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
pens = pens.filter(pen => pen.name === this.searchName)
}
this.pens = pens
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
if (this.pens.length > 0) {
this.currentPen = this.pens[0]
} else {
this.currentPen = null
}
} else if (Array.isArray(response)) {
// 如果直接返回数组
let pens = response
// 如果进行了搜索,进行精确匹配过滤
if (this.searchName) {
pens = pens.filter(pen => pen.name === this.searchName)
}
this.pens = pens
this.totalPages = 1
if (this.pens.length > 0) {
this.currentPen = this.pens[0]
} else {
this.currentPen = null
}
} else {
console.warn('API响应格式不正确:', response)
this.pens = []
this.currentPen = null
this.totalPages = 1
}
} catch (error) {
console.error('加载栏舍列表失败:', error)
this.$message && this.$message.error('加载栏舍列表失败')
this.pens = []
this.currentPen = null
} finally {
this.loading = false
}
},
// 搜索处理
handleSearch() {
// 防抖处理
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.currentPage = 1
this.loadPens()
}, 500)
},
// 批量删除
async batchDelete() {
if (this.selectedPens.length === 0) {
this.$message && this.$message.warning('请选择要删除的栏舍')
return
}
if (confirm(`确定要删除选中的 ${this.selectedPens.length} 个栏舍吗?`)) {
try {
await cattlePenApi.batchDeletePens(this.selectedPens)
this.$message && this.$message.success('批量删除成功')
this.selectedPens = []
this.selectAll = false
this.loadPens()
} catch (error) {
console.error('批量删除栏舍失败:', error)
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
}
}
},
// 添加栏舍
addPen() {
// 跳转到添加栏舍页面
this.$router.push('/cattle-pen-add')
},
// 上一页
goToPreviousPage() {
if (this.currentPage > 1) {
this.currentPage--
this.loadPens()
}
},
// 下一页
goToNextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.loadPens()
}
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return '--'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
return dateTime
}
}
}
}
</script>
<style scoped>
.cattle-pen {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 100px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
cursor: pointer;
padding: 4px;
}
.back-icon {
font-size: 18px;
color: #333;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-icons {
display: flex;
gap: 8px;
}
.icon {
font-size: 14px;
color: #666;
cursor: pointer;
}
/* 搜索栏 */
.search-section {
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-bar {
position: relative;
display: flex;
align-items: center;
background-color: #f8f8f8;
border-radius: 20px;
padding: 8px 16px;
}
.search-icon {
font-size: 16px;
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 批量操作栏 */
.batch-actions {
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.batch-controls {
display: flex;
align-items: center;
gap: 16px;
}
.batch-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.selected-count {
font-size: 14px;
color: #666;
}
.batch-delete-btn {
padding: 6px 12px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.batch-delete-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 栏舍列表 */
.pens-list {
padding: 16px 20px;
}
.pen-card {
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-start;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.pen-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.pen-card.selected {
border: 2px solid #007bff;
background-color: #f0f8ff;
}
.pen-checkbox {
margin-right: 12px;
margin-top: 4px;
}
.pen-content {
flex: 1;
}
.pen-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.pen-name {
display: flex;
align-items: center;
gap: 8px;
}
.label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.value {
font-size: 14px;
color: #333;
font-weight: 600;
}
.pen-actions {
display: flex;
gap: 8px;
}
.edit-btn, .delete-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.edit-btn {
background-color: #007bff;
color: white;
}
.delete-btn {
background-color: #dc3545;
color: white;
}
.pen-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.detail-item .label {
color: #666;
min-width: 60px;
}
.detail-item .value {
color: #333;
font-weight: normal;
}
.status {
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-active {
background-color: #d4edda;
color: #155724;
}
.status-inactive {
background-color: #f8d7da;
color: #721c24;
}
.status-maintenance {
background-color: #fff3cd;
color: #856404;
}
.status-abandoned {
background-color: #d1ecf1;
color: #0c5460;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: #999;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
}
.loading-text {
font-size: 14px;
color: #666;
}
/* 分页控件 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.page-btn {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.page-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #666;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.add-btn {
width: 100%;
padding: 12px;
background-color: #28a745;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.add-btn:hover {
background-color: #218838;
}
/* 响应式设计 */
@media (max-width: 768px) {
.pen-details {
grid-template-columns: 1fr;
}
.pen-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.pen-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -1,574 +0,0 @@
<template>
<div class="cattle-profile">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon"></span>
</div>
<div class="title">牛只档案</div>
<div class="header-actions">
<span class="action-icon"></span>
<span class="action-icon"></span>
<span class="action-icon"></span>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-box">
<span class="search-icon">Q</span>
<input
v-model="searchKeyword"
type="text"
placeholder="请输入耳号"
class="search-input"
@input="handleSearch"
/>
</div>
</div>
<!-- 牛只档案列表 -->
<div class="cattle-list">
<div
v-for="cattle in cattleList"
:key="cattle.id"
class="cattle-card"
@click="viewCattleDetail(cattle)"
>
<div class="cattle-header">
<div class="ear-tag">
<span class="label">耳号:</span>
<span class="value">{{ cattle.earNumber }}</span>
</div>
<div class="arrow-icon"></div>
</div>
<div class="cattle-details">
<div class="detail-row">
<span class="detail-label">佩戴设备:</span>
<span class="detail-value">{{ cattle.deviceNumber || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">出生日期:</span>
<span class="detail-value">{{ cattle.birthdayFormatted || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">品系:</span>
<span class="detail-value">{{ cattle.strainName || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">品种:</span>
<span class="detail-value">{{ cattle.breedName || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">品类:</span>
<span class="detail-value">{{ cattle.categoryName || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">生理阶段:</span>
<span class="detail-value">{{ cattle.physiologicalStage || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">性别:</span>
<span class="detail-value">{{ cattle.sexName || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">来源:</span>
<span class="detail-value">{{ cattle.sourceName || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">栏舍:</span>
<span class="detail-value">{{ cattle.penName || '--' }}</span>
</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && cattleList.length === 0" class="empty-state">
<div class="empty-icon">🐄</div>
<div class="empty-text">暂无牛只档案</div>
</div>
<!-- 分页 -->
<div v-if="pagination.total > 0" class="pagination">
<button
:disabled="pagination.current <= 1"
@click="goToPage(pagination.current - 1)"
class="page-btn prev-btn"
>
上一页
</button>
<span class="page-info">
{{ pagination.current }} / {{ pagination.pages }}
</span>
<button
:disabled="pagination.current >= pagination.pages"
@click="goToPage(pagination.current + 1)"
class="page-btn next-btn"
>
下一页
</button>
</div>
<!-- 新增档案按钮 -->
<div class="add-button" @click="addNewProfile">
<span class="add-text">新增档案</span>
</div>
</div>
</template>
<script>
import { cattleApi } from '@/services/api'
import {
getSexName,
getCategoryName,
getBreedName,
getStrainName,
getPhysiologicalStage,
getSourceName,
formatDate
} from '@/utils/mapping'
import auth from '@/utils/auth'
export default {
name: 'CattleProfile',
data() {
return {
cattleList: [],
loading: false,
searchKeyword: '',
searchTimer: null,
pagination: {
current: 1,
pageSize: 10,
total: 0,
pages: 0
}
}
},
async mounted() {
// 确保有有效的认证token
await this.ensureAuthentication()
this.loadCattleList()
},
methods: {
// 确保认证
async ensureAuthentication() {
try {
// 检查是否已有token
if (!auth.isAuthenticated()) {
console.log('未认证尝试获取测试token...')
await auth.setTestToken()
} else {
// 验证现有token是否有效
const isValid = await auth.validateCurrentToken()
if (!isValid) {
console.log('Token无效重新获取...')
await auth.setTestToken()
}
}
console.log('认证完成token:', auth.getToken()?.substring(0, 20) + '...')
} catch (error) {
console.error('认证失败:', error)
// 即使认证失败也继续让API请求处理错误
}
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 加载牛只档案列表
async loadCattleList() {
this.loading = true
try {
const params = {
page: this.pagination.current,
pageSize: this.pagination.pageSize
}
if (this.searchKeyword) {
params.search = this.searchKeyword
}
console.log('请求参数:', params)
console.log('API基础URL:', process.env.VUE_APP_BASE_URL || 'http://localhost:5350/api')
const response = await cattleApi.getCattleList(params)
console.log('API响应:', response)
console.log('响应类型:', typeof response)
console.log('响应success属性:', response?.success)
console.log('响应data属性:', response?.data)
console.log('响应message属性:', response?.message)
if (response && response.success === true) {
console.log('API调用成功处理数据...')
this.cattleList = this.formatCattleData(response.data.list || [])
this.pagination = {
current: response.data.pagination.current,
pageSize: response.data.pagination.pageSize,
total: response.data.pagination.total,
pages: response.data.pagination.pages
}
console.log('数据处理完成,牛只数量:', this.cattleList.length)
} else {
const errorMsg = (response && response.message) || '获取牛只档案失败'
console.error('API返回错误:', errorMsg)
console.error('完整响应:', response)
alert(`API错误: ${errorMsg}`)
}
} catch (error) {
console.error('获取牛只档案失败:', error)
// 检查error对象的结构
let errorMessage = '获取牛只档案失败'
if (error && error.message) {
errorMessage = error.message
} else if (error && error.response && error.response.data && error.response.data.message) {
errorMessage = error.response.data.message
} else if (typeof error === 'string') {
errorMessage = error
}
alert(errorMessage)
console.error('详细错误信息:', error)
} finally {
this.loading = false
}
},
// 格式化牛只数据
formatCattleData(cattleList) {
return cattleList.map(cattle => {
return {
...cattle,
// 格式化日期
birthdayFormatted: formatDate(cattle.birthday),
// 性别映射
sexName: getSexName(cattle.sex),
// 品系映射strain字段显示为品系
strainName: cattle.strain || '--',
// 品种名称从API返回的varieties字段
breedName: cattle.varieties || '--',
// 品类映射cate字段显示为品类
categoryName: getCategoryName(cattle.cate),
// 生理阶段parity字段
physiologicalStage: getPhysiologicalStage(cattle.parity),
// 来源映射
sourceName: getSourceName(cattle.source),
// 设备编号(如果有的话)
deviceNumber: cattle.deviceNumber || '--'
}
})
},
// 搜索处理
handleSearch() {
// 防抖处理
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.pagination.current = 1 // 重置到第一页
this.loadCattleList()
}, 500)
},
// 查看牛只详情
viewCattleDetail(cattle) {
console.log('查看牛只详情:', cattle)
// 这里可以跳转到详情页面
// this.$router.push(`/cattle-detail/${cattle.id}`)
},
// 分页跳转
goToPage(page) {
if (page >= 1 && page <= this.pagination.pages) {
this.pagination.current = page
this.loadCattleList()
}
},
// 新增档案
addNewProfile() {
this.$router.push('/cattle-add')
}
}
}
</script>
<style scoped>
.cattle-profile {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 80px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.back-icon {
font-size: 24px;
color: #000000;
font-weight: bold;
}
.title {
font-size: 18px;
font-weight: 600;
color: #000000;
}
.header-actions {
display: flex;
gap: 12px;
}
.action-icon {
font-size: 16px;
color: #666666;
cursor: pointer;
}
/* 搜索栏 */
.search-section {
padding: 16px;
background-color: #ffffff;
}
.search-box {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8px;
padding: 8px 12px;
}
.search-icon {
font-size: 16px;
color: #999999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 14px;
color: #333333;
}
.search-input::placeholder {
color: #999999;
}
/* 牛只档案列表 */
.cattle-list {
padding: 0 16px;
}
.cattle-card {
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.cattle-card:hover {
transform: translateY(-1px);
}
.cattle-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.ear-tag {
display: flex;
align-items: center;
}
.ear-tag .label {
font-size: 14px;
color: #666666;
margin-right: 4px;
}
.ear-tag .value {
font-size: 16px;
font-weight: 600;
color: #34c759;
}
.arrow-icon {
font-size: 16px;
color: #cccccc;
}
.cattle-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 14px;
color: #666666;
flex: 1;
}
.detail-value {
font-size: 14px;
color: #333333;
text-align: right;
flex: 1;
}
/* 加载状态 */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #34c759;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 12px;
font-size: 14px;
color: #666666;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #999999;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
gap: 16px;
}
.page-btn {
padding: 8px 16px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
border-radius: 6px;
font-size: 14px;
color: #333333;
cursor: pointer;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) {
background-color: #f5f5f5;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #666666;
}
/* 新增档案按钮 */
.add-button {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #34c759;
color: #ffffff;
padding: 16px 32px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 12px rgba(52, 199, 89, 0.3);
transition: all 0.2s;
z-index: 1000;
}
.add-button:hover {
background-color: #30b54d;
transform: translateX(-50%) translateY(-2px);
box-shadow: 0 6px 16px rgba(52, 199, 89, 0.4);
}
.add-text {
color: #ffffff;
}
</style>

View File

@@ -1,125 +0,0 @@
<template>
<div class="cattle-test">
<div class="test-header">
<h2>牛只档案功能测试</h2>
<p>测试API接口和页面功能</p>
</div>
<div class="test-actions">
<button @click="testCattleList" class="test-btn">测试获取牛只列表</button>
<button @click="testCattleTypes" class="test-btn">测试获取牛只类型</button>
<button @click="goToCattleProfile" class="test-btn">跳转牛只档案页面</button>
<button @click="goToCattleAdd" class="test-btn">跳转新增档案页面</button>
</div>
<div class="test-results">
<h3>测试结果:</h3>
<pre>{{ testResult }}</pre>
</div>
</div>
</template>
<script>
import { cattleApi } from '@/services/api'
export default {
name: 'CattleTest',
data() {
return {
testResult: '点击按钮开始测试...'
}
},
methods: {
async testCattleList() {
try {
this.testResult = '正在测试获取牛只列表...'
const response = await cattleApi.getCattleList({ page: 1, pageSize: 5 })
this.testResult = JSON.stringify(response, null, 2)
} catch (error) {
this.testResult = `错误: ${error.message}`
}
},
async testCattleTypes() {
try {
this.testResult = '正在测试获取牛只类型...'
const response = await cattleApi.getCattleTypes()
this.testResult = JSON.stringify(response, null, 2)
} catch (error) {
this.testResult = `错误: ${error.message}`
}
},
goToCattleProfile() {
this.$router.push('/cattle-profile')
},
goToCattleAdd() {
this.$router.push('/cattle-add')
}
}
}
</script>
<style scoped>
.cattle-test {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.test-header {
text-align: center;
margin-bottom: 30px;
}
.test-header h2 {
color: #333;
margin-bottom: 10px;
}
.test-header p {
color: #666;
}
.test-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 30px;
}
.test-btn {
padding: 10px 20px;
background-color: #34c759;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.test-btn:hover {
background-color: #30b54d;
}
.test-results {
background-color: #f5f5f5;
padding: 20px;
border-radius: 6px;
}
.test-results h3 {
margin-top: 0;
color: #333;
}
.test-results pre {
background-color: #fff;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
line-height: 1.4;
}
</style>

View File

@@ -1,855 +0,0 @@
<template>
<div class="cattle-transfer">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon">&lt;</span>
</div>
<div class="title">牛只转舍</div>
<div class="status-icons">
<span class="icon">...</span>
<span class="icon">-</span>
<span class="icon">o</span>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchEarNumber"
type="text"
placeholder="请输入耳号"
@input="handleSearch"
class="search-input"
/>
</div>
</div>
<!-- 批量操作栏 -->
<div class="batch-actions" v-if="records.length > 0">
<div class="batch-controls">
<label class="batch-checkbox">
<input
type="checkbox"
v-model="selectAll"
@change="toggleSelectAll"
/>
<span>全选</span>
</label>
<span class="selected-count">已选择 {{ selectedRecords.length }} </span>
<button
class="batch-delete-btn"
@click="batchDelete"
:disabled="selectedRecords.length === 0"
>
批量删除
</button>
</div>
</div>
<!-- 记录列表 -->
<div class="records-list" v-if="records.length > 0">
<div
v-for="(record, index) in records"
:key="record.id"
class="record-card"
:class="{ selected: selectedRecords.includes(record.id) }"
>
<div class="record-checkbox">
<input
type="checkbox"
:value="record.id"
v-model="selectedRecords"
/>
</div>
<div class="record-content" @click="selectRecord(record)">
<div class="record-header">
<div class="ear-number">
<span class="label">耳号:</span>
<span class="value">{{ record.earNumber || '--' }}</span>
</div>
<div class="record-actions">
<button class="edit-btn" @click.stop="editRecord(record)">编辑</button>
<button class="delete-btn" @click.stop="deleteRecord(record)">删除</button>
</div>
</div>
<div class="record-details">
<div class="detail-item">
<span class="label">转舍日期:</span>
<span class="value">{{ formatDateTime(record.transferDate) }}</span>
</div>
<div class="detail-item">
<span class="label">转入栋舍:</span>
<span class="value">{{ getToPenName(record) }}</span>
</div>
<div class="detail-item">
<span class="label">转出栋舍:</span>
<span class="value">{{ getFromPenName(record) }}</span>
</div>
<div class="detail-item">
<span class="label">登记人:</span>
<span class="value">{{ record.operator || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">登记日期:</span>
<span class="value">{{ formatDateTime(record.created_at) }}</span>
</div>
<div class="detail-item">
<span class="label">转栏原因:</span>
<span class="value">{{ record.reason || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value">{{ record.status || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">备注:</span>
<span class="value">{{ record.remark || '--' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 单条记录显示兼容旧版本 -->
<div class="record-section" v-if="currentRecord && records.length === 0">
<div class="record-card">
<div class="record-header">
<div class="ear-number">
<span class="label">耳号:</span>
<span class="value">{{ currentRecord.earNumber || '--' }}</span>
</div>
<div class="action-buttons">
<button class="edit-btn" @click="editRecord">编辑</button>
<button class="delete-btn" @click="deleteRecord">删除</button>
</div>
</div>
<div class="record-details">
<div class="detail-item">
<span class="label">转舍日期:</span>
<span class="value">{{ formatDateTime(currentRecord.transferDate) }}</span>
</div>
<div class="detail-item">
<span class="label">转入栋舍:</span>
<span class="value">{{ getToPenName() }}</span>
</div>
<div class="detail-item">
<span class="label">转出栋舍:</span>
<span class="value">{{ getFromPenName() }}</span>
</div>
<div class="detail-item">
<span class="label">登记人:</span>
<span class="value">{{ currentRecord.operator || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">登记日期:</span>
<span class="value">{{ formatDateTime(currentRecord.created_at) }}</span>
</div>
<div class="detail-item">
<span class="label">转栏原因:</span>
<span class="value">{{ currentRecord.reason || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value">{{ currentRecord.status || '--' }}</span>
</div>
<div class="detail-item">
<span class="label">备注:</span>
<span class="value">{{ currentRecord.remark || '--' }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else-if="!loading">
<div class="empty-icon">🐄</div>
<div class="empty-text">暂无转栏记录</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<div class="loading-text">加载中...</div>
</div>
<!-- 分页控制 -->
<div class="pagination" v-if="totalPages > 1">
<button
class="page-btn prev-btn"
:disabled="currentPage === 1"
@click="goToPreviousPage"
>
上一页
</button>
<span class="page-info">{{ currentPage }}/{{ totalPages }}</span>
<button
class="page-btn next-btn"
:disabled="currentPage === totalPages"
@click="goToNextPage"
>
下一页
</button>
</div>
<!-- 转栏登记按钮 -->
<div class="action-section">
<button class="register-btn" @click="registerTransfer">
转栏登记
</button>
</div>
</div>
</template>
<script>
import { cattleTransferApi } from '@/services/api'
import auth from '@/utils/auth'
export default {
name: 'CattleTransfer',
data() {
return {
searchEarNumber: '',
currentRecord: null,
records: [],
loading: false,
currentPage: 1,
totalPages: 1,
pageSize: 10,
searchTimer: null,
selectedRecords: [],
selectAll: false,
showEditDialog: false,
editingRecord: null
}
},
async mounted() {
// 确保有有效的认证token
await this.ensureAuthentication()
this.loadTransferRecords()
},
methods: {
// 确保认证
async ensureAuthentication() {
try {
// 检查是否已有token
if (!auth.isAuthenticated()) {
console.log('未认证尝试获取测试token...')
await auth.setTestToken()
} else {
// 验证现有token是否有效
const isValid = await auth.validateCurrentToken()
if (!isValid) {
console.log('Token无效重新获取...')
await auth.setTestToken()
}
}
console.log('认证完成token:', auth.getToken()?.substring(0, 20) + '...')
} catch (error) {
console.error('认证失败:', error)
// 即使认证失败也继续让API请求处理错误
}
},
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 获取转入栏舍名称
getToPenName(record) {
const targetRecord = record || this.currentRecord
if (targetRecord && targetRecord.toPen && targetRecord.toPen.name) {
return targetRecord.toPen.name
}
return '--'
},
// 获取转出栏舍名称
getFromPenName(record) {
const targetRecord = record || this.currentRecord
if (targetRecord && targetRecord.fromPen && targetRecord.fromPen.name) {
return targetRecord.fromPen.name
}
return '--'
},
// 加载转栏记录
async loadTransferRecords() {
this.loading = true
try {
const params = {
page: this.currentPage,
pageSize: this.pageSize
}
if (this.searchEarNumber) {
params.search = this.searchEarNumber
}
const response = await cattleTransferApi.getTransferRecords(params)
if (response && response.success && response.data && response.data.list) {
// 标准API响应格式
this.records = response.data.list
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
// 显示第一条记录
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else if (response && response.data && Array.isArray(response.data)) {
// 兼容直接返回数组的格式
this.records = response.data
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else if (Array.isArray(response)) {
// 如果直接返回数组
this.records = response
this.totalPages = 1
if (this.records.length > 0) {
this.currentRecord = this.records[0]
} else {
this.currentRecord = null
}
} else {
console.warn('API响应格式不正确:', response)
this.records = []
this.currentRecord = null
this.totalPages = 1
}
} catch (error) {
console.error('加载转栏记录失败:', error)
this.$message && this.$message.error('加载转栏记录失败')
this.records = []
this.currentRecord = null
} finally {
this.loading = false
}
},
// 搜索处理
handleSearch() {
// 防抖处理
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.currentPage = 1
this.loadTransferRecords()
}, 500)
},
// 选择记录
selectRecord(record) {
this.currentRecord = record
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedRecords = this.records.map(record => record.id)
} else {
this.selectedRecords = []
}
},
// 编辑记录
editRecord(record) {
const targetRecord = record || this.currentRecord
if (targetRecord) {
this.editingRecord = targetRecord
this.showEditDialog = true
// 跳转到编辑页面
this.$router.push({
path: '/cattle-transfer-register',
query: { edit: true, id: targetRecord.id }
})
}
},
// 删除记录
async deleteRecord(record) {
const targetRecord = record || this.currentRecord
if (!targetRecord) return
if (confirm('确定要删除这条转栏记录吗?')) {
try {
await cattleTransferApi.deleteTransferRecord(targetRecord.id)
this.$message && this.$message.success('删除成功')
this.loadTransferRecords()
} catch (error) {
console.error('删除转栏记录失败:', error)
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
}
}
},
// 批量删除
async batchDelete() {
if (this.selectedRecords.length === 0) {
this.$message && this.$message.warning('请选择要删除的记录')
return
}
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条转栏记录吗?`)) {
try {
await cattleTransferApi.batchDeleteTransferRecords(this.selectedRecords)
this.$message && this.$message.success('批量删除成功')
this.selectedRecords = []
this.selectAll = false
this.loadTransferRecords()
} catch (error) {
console.error('批量删除转栏记录失败:', error)
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
}
}
},
// 转栏登记
registerTransfer() {
// 跳转到转栏登记页面
this.$router.push('/cattle-transfer-register')
},
// 上一页
goToPreviousPage() {
if (this.currentPage > 1) {
this.currentPage--
this.loadTransferRecords()
}
},
// 下一页
goToNextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.loadTransferRecords()
}
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return '--'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
return dateTime
}
}
}
}
</script>
<style scoped>
.cattle-transfer {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 100px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
cursor: pointer;
padding: 4px;
}
.back-icon {
font-size: 18px;
color: #333;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-icons {
display: flex;
gap: 12px;
}
.icon {
font-size: 16px;
color: #666;
}
/* 搜索栏 */
.search-section {
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
padding: 8px 12px;
border: 1px solid #e0e0e0;
}
.search-icon {
font-size: 16px;
color: #666;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 批量操作栏 */
.batch-actions {
padding: 16px 20px;
background-color: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.batch-controls {
display: flex;
align-items: center;
gap: 16px;
}
.batch-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.batch-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.selected-count {
font-size: 14px;
color: #666;
flex: 1;
}
.batch-delete-btn {
background-color: #ff3b30;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.batch-delete-btn:hover:not(:disabled) {
background-color: #d70015;
}
.batch-delete-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 记录列表 */
.records-list {
padding: 20px;
}
.record-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-start;
gap: 12px;
transition: all 0.2s;
}
.record-card.selected {
border: 2px solid #34c759;
background-color: #f0f9f0;
}
.record-checkbox {
margin-top: 4px;
}
.record-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
}
.record-content {
flex: 1;
cursor: pointer;
}
/* 记录显示区域 */
.record-section {
padding: 20px;
}
.record-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.ear-number {
display: flex;
align-items: center;
gap: 8px;
}
.ear-number .label {
font-size: 14px;
color: #666;
}
.ear-number .value {
font-size: 16px;
font-weight: 600;
color: #34c759;
}
.edit-btn {
background-color: #34c759;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.edit-btn:hover {
background-color: #30b54d;
}
.action-buttons,
.record-actions {
display: flex;
gap: 8px;
}
.delete-btn {
background-color: #ff3b30;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.delete-btn:hover {
background-color: #d70015;
}
.record-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.detail-item .label {
font-size: 14px;
color: #666;
min-width: 80px;
}
.detail-item .value {
font-size: 14px;
color: #333;
text-align: right;
flex: 1;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: #666;
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
}
.loading-text {
font-size: 16px;
color: #666;
}
/* 分页控制 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
gap: 20px;
}
.page-btn {
padding: 8px 16px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
color: #666;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #34c759;
color: #34c759;
}
.page-btn:disabled {
background-color: #f8f9fa;
color: #ccc;
cursor: not-allowed;
}
.page-info {
font-size: 14px;
color: #666;
min-width: 60px;
text-align: center;
}
/* 转栏登记按钮 */
.action-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 20px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.register-btn {
width: 100%;
background-color: #34c759;
color: white;
border: none;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.register-btn:hover {
background-color: #30b54d;
}
/* 响应式设计 */
@media (max-width: 480px) {
.status-bar {
padding: 10px 16px;
}
.search-section {
padding: 12px 16px;
}
.record-section {
padding: 16px;
}
.record-card {
padding: 12px;
}
.action-section {
padding: 12px 16px;
}
}
</style>

View File

@@ -1,540 +0,0 @@
<template>
<div class="cattle-transfer-register">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="back-btn" @click="goBack">
<span class="back-icon">&lt;</span>
</div>
<div class="title">{{ isEdit ? '编辑转栏记录' : '转栏登记' }}</div>
<div class="status-icons">
<span class="icon">...</span>
<span class="icon">-</span>
<span class="icon">o</span>
</div>
</div>
<!-- 表单区域 -->
<div class="form-section">
<div class="form-card">
<form @submit.prevent="submitForm">
<!-- 耳号选择 -->
<div class="form-group">
<label class="form-label">耳号 *</label>
<select
v-model="formData.earNumber"
class="form-select"
required
>
<option value="">请选择牛只耳号</option>
<option
v-for="animal in availableAnimals"
:key="animal.id"
:value="animal.earNumber"
>
{{ animal.earNumber }} - {{ animal.name || '未命名' }}
</option>
</select>
</div>
<!-- 转出栏舍 -->
<div class="form-group">
<label class="form-label">转出栏舍 *</label>
<select
v-model="formData.fromPenId"
class="form-select"
required
>
<option value="">请选择转出栏舍</option>
<option
v-for="pen in barns"
:key="pen.id"
:value="pen.id"
>
{{ pen.name }}
</option>
</select>
</div>
<!-- 转入栏舍 -->
<div class="form-group">
<label class="form-label">转入栏舍 *</label>
<select
v-model="formData.toPenId"
class="form-select"
required
>
<option value="">请选择转入栏舍</option>
<option
v-for="pen in barns"
:key="pen.id"
:value="pen.id"
>
{{ pen.name }}
</option>
</select>
</div>
<!-- 转栏日期 -->
<div class="form-group">
<label class="form-label">转栏日期 *</label>
<input
v-model="formData.transferDate"
type="datetime-local"
class="form-input"
required
/>
</div>
<!-- 转栏原因 -->
<div class="form-group">
<label class="form-label">转栏原因 *</label>
<select
v-model="formData.reason"
class="form-select"
required
>
<option value="">请选择转栏原因</option>
<option value="正常调栏">正常调栏</option>
<option value="疾病治疗">疾病治疗</option>
<option value="配种需要">配种需要</option>
<option value="产房准备">产房准备</option>
<option value="隔离观察">隔离观察</option>
<option value="其他">其他</option>
</select>
</div>
<!-- 操作人员 -->
<div class="form-group">
<label class="form-label">操作人员 *</label>
<input
v-model="formData.operator"
type="text"
placeholder="请输入操作人员姓名"
class="form-input"
required
/>
</div>
<!-- 状态 -->
<div class="form-group">
<label class="form-label">状态 *</label>
<select
v-model="formData.status"
class="form-select"
required
>
<option value="已完成">已完成</option>
<option value="进行中">进行中</option>
</select>
</div>
<!-- 备注 -->
<div class="form-group">
<label class="form-label">备注</label>
<textarea
v-model="formData.remark"
placeholder="请输入备注信息"
class="form-textarea"
rows="3"
></textarea>
</div>
<!-- 提交按钮 -->
<div class="form-actions">
<button
type="button"
class="btn-cancel"
@click="goBack"
>
取消
</button>
<button
type="submit"
class="btn-submit"
:disabled="submitting"
>
{{ submitting ? (isEdit ? '更新中...' : '提交中...') : (isEdit ? '更新' : '提交') }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import { cattleTransferApi, cattleApi } from '@/services/api'
export default {
name: 'CattleTransferRegister',
data() {
return {
formData: {
earNumber: '',
fromPenId: '',
toPenId: '',
transferDate: '',
reason: '',
operator: '',
status: '已完成',
remark: ''
},
barns: [],
submitting: false,
isEdit: false,
editId: null,
availableAnimals: []
}
},
mounted() {
this.checkEditMode()
this.loadBarns()
this.loadAvailableAnimals()
this.setDefaultDateTime()
},
methods: {
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 检查是否为编辑模式
checkEditMode() {
const { edit, id } = this.$route.query
if (edit === 'true' && id) {
this.isEdit = true
this.editId = id
this.loadRecordForEdit(id)
}
},
// 加载要编辑的记录
async loadRecordForEdit(id) {
try {
const response = await cattleTransferApi.getTransferRecordDetail(id)
if (response) {
this.formData = {
earNumber: response.earNumber || '',
fromPenId: response.fromPenId || '',
toPenId: response.toPenId || '',
transferDate: response.transferDate ? new Date(response.transferDate).toISOString().slice(0, 16) : '',
reason: response.reason || '',
operator: response.operator || '',
status: response.status || '已完成',
remark: response.remark || ''
}
}
} catch (error) {
console.error('加载转栏记录失败:', error)
this.$message && this.$message.error('加载转栏记录失败')
}
},
// 加载可用牛只列表
async loadAvailableAnimals() {
try {
const response = await cattleTransferApi.getAvailableAnimals()
if (response && Array.isArray(response)) {
this.availableAnimals = response
} else if (response && response.data && Array.isArray(response.data)) {
this.availableAnimals = response.data
} else {
console.warn('可用牛只数据格式异常:', response)
this.availableAnimals = []
}
} catch (error) {
console.error('加载可用牛只列表失败:', error)
this.availableAnimals = []
}
},
// 设置默认日期时间
setDefaultDateTime() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
this.formData.transferDate = `${year}-${month}-${day}T${hours}:${minutes}`
},
// 加载栏舍列表
async loadBarns() {
try {
const params = {
page: 1,
pageSize: 100 // 获取更多栏舍数据
}
const response = await cattleTransferApi.getBarnsForTransfer(params)
if (response && Array.isArray(response)) {
this.barns = response
} else if (response && response.data && response.data.list && Array.isArray(response.data.list)) {
this.barns = response.data.list
} else if (response && response.data && Array.isArray(response.data)) {
this.barns = response.data
} else if (response && response.records && Array.isArray(response.records)) {
this.barns = response.records
} else {
console.warn('栏舍数据格式异常:', response)
this.barns = []
}
} catch (error) {
console.error('加载栏舍列表失败:', error)
this.$message && this.$message.error('加载栏舍列表失败')
this.barns = []
}
},
// 提交表单
async submitForm() {
if (this.submitting) return
// 验证表单
if (!this.validateForm()) {
return
}
this.submitting = true
try {
// 准备提交数据
const submitData = {
...this.formData,
animalId: 1, // 这里需要根据耳号查询动物ID暂时使用默认值
farmId: 1 // 这里需要获取当前农场ID
}
// 转换日期格式
if (submitData.transferDate) {
submitData.transferDate = new Date(submitData.transferDate).toISOString()
}
let response
if (this.isEdit) {
response = await cattleTransferApi.updateTransferRecord(this.editId, submitData)
this.$message && this.$message.success('转栏记录更新成功')
} else {
response = await cattleTransferApi.createTransferRecord(submitData)
this.$message && this.$message.success('转栏记录创建成功')
}
// 跳转回转栏记录列表
this.$router.push('/cattle-transfer')
} catch (error) {
console.error('创建转栏记录失败:', error)
this.$message && this.$message.error('创建转栏记录失败: ' + (error.message || '未知错误'))
} finally {
this.submitting = false
}
},
// 验证表单
validateForm() {
const { earNumber, fromPenId, toPenId, transferDate, reason, operator } = this.formData
if (!earNumber.trim()) {
this.$message && this.$message.error('请输入牛只耳号')
return false
}
if (!fromPenId) {
this.$message && this.$message.error('请选择转出栏舍')
return false
}
if (!toPenId) {
this.$message && this.$message.error('请选择转入栏舍')
return false
}
if (fromPenId === toPenId) {
this.$message && this.$message.error('转出栏舍和转入栏舍不能相同')
return false
}
if (!transferDate) {
this.$message && this.$message.error('请选择转栏日期')
return false
}
if (!reason) {
this.$message && this.$message.error('请选择转栏原因')
return false
}
if (!operator.trim()) {
this.$message && this.$message.error('请输入操作人员')
return false
}
return true
}
}
}
</script>
<style scoped>
.cattle-transfer-register {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 20px;
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.back-btn {
cursor: pointer;
padding: 4px;
}
.back-icon {
font-size: 18px;
color: #333;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.status-icons {
display: flex;
gap: 12px;
}
.icon {
font-size: 16px;
color: #666;
}
/* 表单区域 */
.form-section {
padding: 20px;
}
.form-card {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
color: #333;
background-color: #ffffff;
transition: border-color 0.2s;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: #34c759;
box-shadow: 0 0 0 2px rgba(52, 199, 89, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.btn-cancel,
.btn-submit {
flex: 1;
padding: 14px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background-color: #f8f9fa;
color: #666;
border: 1px solid #e0e0e0;
}
.btn-cancel:hover {
background-color: #e9ecef;
}
.btn-submit {
background-color: #34c759;
color: white;
}
.btn-submit:hover:not(:disabled) {
background-color: #30b54d;
}
.btn-submit:disabled {
background-color: #ccc;
cursor: not-allowed;
}
/* 响应式设计 */
@media (max-width: 480px) {
.status-bar {
padding: 10px 16px;
}
.form-section {
padding: 16px;
}
.form-card {
padding: 16px;
}
.form-actions {
flex-direction: column;
}
.btn-cancel,
.btn-submit {
flex: none;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,703 +0,0 @@
<template>
<div class="home">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">11:29</div>
<div class="title">首页</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">85%</span>
</div>
</div>
<!-- 智能预警模块 -->
<div class="alert-section">
<div class="alert-tabs">
<div
v-for="tab in alertTabs"
:key="tab.key"
:class="['alert-tab', { active: activeAlertTab === tab.key }]"
@click="activeAlertTab = tab.key"
>
{{ tab.name }}
</div>
</div>
<div class="alert-grid">
<div
v-for="alert in currentAlerts"
:key="alert.key"
:class="['alert-card', { urgent: alert.urgent }]"
>
<div class="alert-icon">{{ alert.icon }}</div>
<div class="alert-content">
<div class="alert-label">{{ alert.label }}</div>
<div class="alert-value">{{ alert.value }}</div>
</div>
</div>
</div>
</div>
<!-- 跳转生资保险 -->
<div class="insurance-link">
<span>跳转生资保险</span>
</div>
<!-- 智能设备模块 -->
<div class="module-section">
<div class="module-title">智能设备</div>
<div class="device-grid">
<div
v-for="device in smartDevices"
:key="device.key"
class="device-card"
@click="handleDeviceClick(device)"
>
<div class="device-icon">{{ device.icon }}</div>
<div class="device-label">{{ device.label }}</div>
</div>
</div>
</div>
<!-- 智能工具模块 -->
<div class="module-section">
<div class="module-title">智能工具</div>
<div class="tool-grid">
<div
v-for="tool in smartTools"
:key="tool.key"
class="tool-card"
@click="handleToolClick(tool)"
>
<div class="tool-icon">{{ tool.icon }}</div>
<div class="tool-label">{{ tool.label }}</div>
</div>
</div>
</div>
<!-- 业务办理模块 -->
<div class="module-section">
<div class="module-title">业务办理</div>
<div class="business-grid">
<div
v-for="business in businessModules"
:key="business.key"
class="business-card"
@click="handleBusinessClick(business)"
>
<div class="business-icon">{{ business.icon }}</div>
<div class="business-label">{{ business.label }}</div>
</div>
</div>
</div>
<!-- 开发环境认证测试 -->
<div v-if="isDevelopment" class="dev-section">
<div class="dev-header">
<h3>开发工具</h3>
</div>
<div class="dev-actions">
<button @click="goToAuthTest" class="dev-btn">
🔧 认证测试
</button>
<button @click="goToApiTest" class="dev-btn">
🧪 API测试
</button>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div
v-for="nav in bottomNavItems"
:key="nav.key"
:class="['nav-item', { active: activeNav === nav.key }]"
@click="handleNavClick(nav)"
>
<div class="nav-icon">{{ nav.icon }}</div>
<div class="nav-label">{{ nav.label }}</div>
</div>
</div>
</div>
</template>
<script>
import { alertApi } from '@/services/api'
import { getAlertTypeName } from '@/utils/mapping'
export default {
name: 'Home',
data() {
return {
activeAlertTab: 'collar',
activeNav: 'home',
alertStats: {
collar: {},
ear: {},
ankle: {},
host: {}
},
loading: false,
alertTabs: [
{ key: 'collar', name: '项圈预警' },
{ key: 'ear', name: '耳标预警' },
{ key: 'ankle', name: '脚环预警' },
{ key: 'host', name: '主机预警' }
],
smartDevices: [
{ key: 'collar', icon: 'A', label: '智能项圈', color: '#ff9500' },
{ key: 'ear', icon: '👂', label: '智能耳标', color: '#007aff' },
{ key: 'ankle', icon: '🔗', label: '智能脚环', color: '#007aff' },
{ key: 'host', icon: '🖥️', label: '智能主机', color: '#007aff' },
{ key: 'camera', icon: '📹', label: '视频监控', color: '#ff9500' }
],
smartTools: [
{ key: 'fence', icon: '🎯', label: '电子围栏', color: '#ff9500' },
{ key: 'smart-eartag-alert', icon: '⚠️', label: '智能耳标预警', color: '#ff3b30' },
{ key: 'smart-collar-alert', icon: '⚠️', label: '智能项圈预警', color: '#ff3b30' },
{ key: 'scan', icon: '🛡️', label: '扫码溯源', color: '#007aff' },
{ key: 'photo', icon: '📷', label: '档案拍照', color: '#ff3b30' },
{ key: 'detect', icon: '📊', label: '检测工具', color: '#af52de' },
],
businessModules: [
{ key: 'quarantine', icon: '📋', label: '电子检疫', color: '#ff9500' },
{ key: 'rights', icon: '🆔', label: '电子确权', color: '#007aff' },
{ key: 'disposal', icon: '♻️', label: '无害化处理申报', color: '#af52de' },
{ key: 'cattle-transfer', icon: '🔄', label: '牛只转栏', color: '#34c759' }
],
bottomNavItems: [
{ key: 'home', icon: '🏠', label: '首页', color: '#34c759' },
{ key: 'production', icon: '📦', label: '生产管理', color: '#8e8e93' },
{ key: 'profile', icon: '👤', label: '我的', color: '#8e8e93' }
]
}
},
computed: {
currentAlerts() {
const stats = this.alertStats[this.activeAlertTab] || {}
// 根据不同的预警类型生成预警卡片 - 与PC端保持一致
if (this.activeAlertTab === 'collar') {
return [
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true },
{ key: 'wear', icon: '✂️', label: '佩戴异常预警', value: stats.wearOff || 0, urgent: false }
]
} else if (this.activeAlertTab === 'ear') {
return [
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true }
]
} else if (this.activeAlertTab === 'ankle') {
// 脚环预警暂时使用耳标数据因为API中没有单独的脚环统计
return [
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true }
]
} else if (this.activeAlertTab === 'host') {
// 主机预警暂时使用固定数据因为API中没有主机统计
return [
{ key: 'offline', icon: '📴', label: '主机离线', value: '0', urgent: false },
{ key: 'low_storage', icon: '💾', label: '存储空间不足', value: '1', urgent: true },
{ key: 'network_error', icon: '🌐', label: '网络异常', value: '0', urgent: false }
]
}
return []
}
},
async mounted() {
await this.loadAlertStats()
},
methods: {
// 加载预警统计数据
async loadAlertStats() {
this.loading = true
try {
// 并行加载项圈和耳标预警数据
const [collarResponse, eartagResponse] = await Promise.all([
alertApi.getCollarStats(),
alertApi.getEartagStats()
])
if (collarResponse && collarResponse.success) {
this.alertStats.collar = collarResponse.data
}
if (eartagResponse && eartagResponse.success) {
this.alertStats.ear = eartagResponse.data
// 脚环预警暂时使用耳标数据
this.alertStats.ankle = eartagResponse.data
}
console.log('预警统计数据加载成功:', this.alertStats)
} catch (error) {
console.error('加载预警统计数据失败:', error)
// 如果API调用失败使用默认值
this.alertStats = {
collar: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0, wearOff: 0 },
ear: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0 },
ankle: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0 },
host: {}
}
} finally {
this.loading = false
}
},
handleDeviceClick(device) {
console.log('点击设备:', device.label)
// 根据设备类型跳转到不同页面
switch(device.key) {
case 'ear':
this.$router.push('/ear-tag')
break
case 'collar':
this.$router.push('/smart-collar')
break
case 'ankle':
this.$router.push('/smart-ankle')
break
case 'host':
this.$router.push('/smart-host')
break
case 'camera':
console.log('跳转到视频监控页面')
break
default:
console.log('未知设备类型')
}
},
handleToolClick(tool) {
console.log('点击工具:', tool.label)
// 根据工具类型跳转到不同页面
switch(tool.key) {
case 'fence':
this.$router.push('/electronic-fence')
break
case 'scan':
console.log('跳转到扫码溯源页面')
break
case 'photo':
this.$router.push('/cattle-profile')
break
case 'detect':
console.log('跳转到检测工具页面')
break
case 'api-test':
this.$router.push('/api-test')
break
case 'smart-eartag-alert':
this.$router.push('/smart-eartag-alert')
break
case 'smart-collar-alert':
this.$router.push('/smart-collar-alert')
break
default:
console.log('未知工具类型')
}
},
handleBusinessClick(business) {
console.log('点击业务:', business.label)
// 根据业务类型跳转到不同页面
switch(business.key) {
case 'quarantine':
console.log('跳转到电子检疫页面')
break
case 'rights':
console.log('跳转到电子确权页面')
break
case 'disposal':
console.log('跳转到无害化处理申报页面')
break
case 'cattle-transfer':
this.$router.push('/cattle-transfer')
break
default:
console.log('未知业务类型')
}
},
navigateTo(route) {
this.$router.push(route)
},
handleNavClick(nav) {
this.activeNav = nav.key
const routes = {
home: '/',
production: '/production',
profile: '/profile'
}
this.navigateTo(routes[nav.key])
},
goToAuthTest() {
this.$router.push('/auth-test')
},
goToApiTest() {
this.$router.push('/api-test-page')
},
get isDevelopment() {
return process.env.NODE_ENV === 'development'
}
}
}
</script>
<style scoped>
.home {
background-color: #ffffff;
min-height: 100vh;
padding-bottom: 80px; /* 为底部导航栏留出空间 */
}
/* 顶部状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.time {
font-size: 16px;
font-weight: 500;
color: #000000;
}
.title {
font-size: 18px;
font-weight: 600;
color: #000000;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 14px;
}
/* 智能预警模块 */
.alert-section {
margin: 20px;
background-color: #f8f9fa;
border-radius: 12px;
padding: 16px;
}
.alert-tabs {
display: flex;
margin-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
}
.alert-tab {
flex: 1;
text-align: center;
padding: 12px 8px;
font-size: 14px;
color: #666666;
cursor: pointer;
position: relative;
}
.alert-tab.active {
color: #007aff;
font-weight: 500;
}
.alert-tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background-color: #007aff;
}
.alert-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.alert-card {
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.alert-card.urgent .alert-value {
color: #ff3b30;
}
.alert-icon {
font-size: 20px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 6px;
}
.alert-content {
flex: 1;
}
.alert-label {
font-size: 12px;
color: #666666;
margin-bottom: 4px;
}
.alert-value {
font-size: 16px;
font-weight: 600;
color: #000000;
}
/* 跳转生资保险 */
.insurance-link {
margin: 0 20px 20px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
text-align: center;
color: #007aff;
font-size: 14px;
cursor: pointer;
}
/* 模块标题 */
.module-title {
font-size: 18px;
font-weight: 600;
color: #000000;
margin: 0 20px 16px;
}
/* 智能设备网格 */
.device-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin: 0 20px 24px;
}
.device-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.device-card:hover {
transform: translateY(-2px);
}
.device-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 8px;
margin-bottom: 8px;
}
.device-label {
font-size: 12px;
color: #000000;
text-align: center;
line-height: 1.2;
}
/* 智能工具网格 */
.tool-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 0 20px 24px;
}
.tool-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.tool-card:hover {
transform: translateY(-2px);
}
.tool-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 8px;
margin-bottom: 8px;
}
.tool-label {
font-size: 12px;
color: #000000;
text-align: center;
line-height: 1.2;
}
/* 业务办理网格 */
.business-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin: 0 20px 24px;
}
.business-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
}
.business-card:hover {
transform: translateY(-2px);
}
.business-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
border-radius: 8px;
margin-bottom: 8px;
}
.business-label {
font-size: 12px;
color: #000000;
text-align: center;
line-height: 1.2;
}
/* 底部导航栏 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
display: flex;
height: 60px;
z-index: 1000;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: color 0.2s;
}
.nav-item.active .nav-icon {
color: #34c759;
}
.nav-item.active .nav-label {
color: #34c759;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
color: #8e8e93;
}
.nav-label {
font-size: 12px;
color: #8e8e93;
font-weight: 500;
}
/* 开发工具样式 */
.dev-section {
margin: 16px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.dev-header h3 {
margin: 0 0 12px 0;
color: #495057;
font-size: 14px;
font-weight: 600;
}
.dev-actions {
display: flex;
gap: 8px;
}
.dev-btn {
padding: 8px 12px;
background-color: #6c757d;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.dev-btn:hover {
background-color: #5a6268;
}
</style>

View File

@@ -1,553 +0,0 @@
<template>
<div class="login-page">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">14:38</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">92%</span>
</div>
</div>
<!-- 头部导航栏 -->
<div class="header-bar">
<div class="header-left">
<span class="home-icon">🏠</span>
</div>
<div class="header-right">
<span class="menu-icon"></span>
<span class="minus-icon"></span>
<span class="target-icon"></span>
</div>
</div>
<!-- 语言选择 -->
<div class="language-selector">
<select v-model="selectedLanguage" class="language-dropdown">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 应用标题 -->
<div class="app-title">
<h1>爱农智慧牧场</h1>
</div>
<!-- 一键登录按钮 -->
<div class="login-section">
<button
class="login-btn"
@click="handleOneClickLogin"
:disabled="isLoading"
>
{{ isLoading ? '登录中...' : '一键登录' }}
</button>
<!-- 协议同意 -->
<div class="agreement-section">
<label class="agreement-checkbox">
<input
type="checkbox"
v-model="agreedToTerms"
class="checkbox-input"
/>
<span class="checkbox-custom"></span>
</label>
<div class="agreement-text">
我已阅读并同意
<a href="#" class="agreement-link" @click="showUserAgreement">用户服务协议</a>
<a href="#" class="agreement-link" @click="showPrivacyPolicy">隐私政策</a>
</div>
</div>
</div>
<!-- 其他登录方式 -->
<div class="alternative-login">
<div class="login-option" @click="handleSmsLogin">
<span class="option-icon">📱</span>
<span class="option-text">短信登录</span>
</div>
<div class="login-option" @click="handleRegister">
<span class="option-icon">📝</span>
<span class="option-text">注册账号</span>
</div>
<div class="login-option" @click="handleOtherLogin">
<span class="option-icon"></span>
<span class="option-text">其它</span>
</div>
</div>
<!-- 底部服务信息 -->
<div class="footer-services">
<div class="service-item" @click="handleProductionSupervision">
<span class="service-icon">📊</span>
<span class="service-text">生资监管方案</span>
</div>
<div class="service-item" @click="handleTianyiBinding">
<span class="service-icon">🔗</span>
<span class="service-text">绑定天翼账号</span>
</div>
</div>
<!-- 免责声明 -->
<div class="disclaimer">
<p>说明该系统仅对在小程序主体公司备案的客户开放</p>
</div>
</div>
</div>
</template>
<script>
import auth from '@/utils/auth'
export default {
name: 'Login',
data() {
return {
selectedLanguage: 'zh-CN',
agreedToTerms: false,
isLoading: false
}
},
mounted() {
// 检查是否已经登录
if (auth.isAuthenticated()) {
this.$router.push('/')
}
},
methods: {
// 一键登录
async handleOneClickLogin() {
if (!this.agreedToTerms) {
alert('请先同意用户服务协议和隐私政策')
return
}
this.isLoading = true
try {
// 模拟登录过程
await this.simulateLogin()
// 设置认证信息
auth.setTestToken()
auth.setUserInfo({
id: 'user-' + Date.now(),
name: '爱农智慧牧场用户',
phone: '138****8888',
role: 'user',
loginTime: new Date().toISOString()
})
// 跳转到首页
this.$router.push('/')
console.log('登录成功')
} catch (error) {
console.error('登录失败:', error)
alert('登录失败,请重试')
} finally {
this.isLoading = false
}
},
// 模拟登录过程
simulateLogin() {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 1500)
})
},
// 短信登录
handleSmsLogin() {
console.log('短信登录')
this.$router.push('/sms-login')
},
// 注册账号
handleRegister() {
console.log('注册账号')
this.$router.push('/register')
},
// 其他登录方式
handleOtherLogin() {
console.log('其他登录方式')
this.$router.push('/password-login')
},
// 生资监管方案
handleProductionSupervision() {
console.log('生资监管方案')
alert('生资监管方案功能开发中...')
},
// 绑定天翼账号
handleTianyiBinding() {
console.log('绑定天翼账号')
alert('绑定天翼账号功能开发中...')
},
// 显示用户服务协议
showUserAgreement() {
console.log('显示用户服务协议')
alert('用户服务协议内容...')
},
// 显示隐私政策
showPrivacyPolicy() {
console.log('显示隐私政策')
alert('隐私政策内容...')
}
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background-color: #ffffff;
font-size: 14px;
color: #000000;
}
.time {
font-weight: 500;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 12px;
}
/* 头部导航栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
}
.header-left .home-icon {
font-size: 20px;
cursor: pointer;
}
.header-right {
display: flex;
gap: 12px;
align-items: center;
}
.header-right span {
font-size: 16px;
cursor: pointer;
padding: 4px;
}
/* 语言选择 */
.language-selector {
display: flex;
justify-content: flex-end;
padding: 0 20px 20px 0;
}
.language-dropdown {
border: none;
background: transparent;
font-size: 14px;
color: #333;
cursor: pointer;
outline: none;
}
/* 主要内容区域 */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
min-height: calc(100vh - 200px);
}
/* 应用标题 */
.app-title {
margin-bottom: 60px;
text-align: center;
}
.app-title h1 {
font-size: 28px;
font-weight: bold;
color: #000000;
margin: 0;
letter-spacing: 1px;
}
/* 登录区域 */
.login-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60px;
}
/* 一键登录按钮 */
.login-btn {
width: 280px;
height: 50px;
background-color: #52c41a;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.login-btn:hover:not(:disabled) {
background-color: #45a018;
transform: translateY(-1px);
}
.login-btn:disabled {
background-color: #a0d468;
cursor: not-allowed;
}
/* 协议同意区域 */
.agreement-section {
display: flex;
align-items: flex-start;
gap: 8px;
max-width: 280px;
}
.agreement-checkbox {
display: flex;
align-items: center;
cursor: pointer;
margin-top: 2px;
}
.checkbox-input {
display: none;
}
.checkbox-custom {
width: 16px;
height: 16px;
border: 2px solid #d9d9d9;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.checkbox-input:checked + .checkbox-custom {
background-color: #52c41a;
border-color: #52c41a;
}
.checkbox-input:checked + .checkbox-custom::after {
content: '✓';
color: white;
font-size: 12px;
font-weight: bold;
}
.agreement-text {
font-size: 12px;
color: #666666;
line-height: 1.4;
}
.agreement-link {
color: #52c41a;
text-decoration: none;
}
.agreement-link:hover {
text-decoration: underline;
}
/* 其他登录方式 */
.alternative-login {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 60px;
}
.login-option {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 24px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
min-width: 200px;
}
.login-option:hover {
background-color: #e9ecef;
transform: translateY(-1px);
}
.option-icon {
font-size: 18px;
}
.option-text {
font-size: 16px;
color: #333333;
font-weight: 500;
}
/* 底部服务信息 */
.footer-services {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
}
.service-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s;
}
.service-item:hover {
background-color: #f8f9fa;
border-radius: 6px;
}
.service-icon {
font-size: 16px;
}
.service-text {
font-size: 14px;
color: #666666;
}
/* 免责声明 */
.disclaimer {
text-align: left;
max-width: 320px;
}
.disclaimer p {
font-size: 12px;
color: #999999;
line-height: 1.5;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-content {
padding: 20px 16px;
}
.app-title h1 {
font-size: 24px;
}
.login-btn {
width: 100%;
max-width: 280px;
}
.agreement-section {
max-width: 100%;
}
.login-option {
min-width: 160px;
}
.footer-services {
margin-bottom: 20px;
}
.disclaimer {
max-width: 100%;
}
}
/* 小屏幕优化 */
@media (max-width: 375px) {
.app-title h1 {
font-size: 22px;
}
.login-btn {
height: 45px;
font-size: 16px;
}
.login-option {
padding: 10px 20px;
}
.option-text {
font-size: 14px;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.main-content {
padding: 20px;
min-height: auto;
}
.app-title {
margin-bottom: 30px;
}
.login-section {
margin-bottom: 30px;
}
.alternative-login {
margin-bottom: 30px;
}
.footer-services {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,237 +0,0 @@
<template>
<div class="map-test">
<div class="test-header">
<h2>百度地图测试</h2>
<p>测试百度地图API集成和电子围栏功能</p>
</div>
<div class="test-controls">
<button @click="addTestFence" class="test-btn">添加测试围栏</button>
<button @click="clearTestFences" class="test-btn">清除围栏</button>
<button @click="toggleDrawing" class="test-btn">
{{ isDrawing ? '停止绘制' : '开始绘制' }}
</button>
</div>
<div class="map-wrapper">
<MapView
:center="mapCenter"
:zoom="mapZoom"
:drawing-mode="isDrawing"
:fences="testFences"
:current-points="currentPoints"
@map-ready="onMapReady"
@map-click="onMapClick"
@fence-click="onFenceClick"
@toggle-drawing="onToggleDrawing"
@center-map="onCenterMap"
@clear-map="onClearMap"
/>
</div>
<div class="test-info">
<h3>测试信息</h3>
<div class="info-item">
<span class="label">地图状态:</span>
<span class="value">{{ mapReady ? '已加载' : '加载中...' }}</span>
</div>
<div class="info-item">
<span class="label">绘制模式:</span>
<span class="value">{{ isDrawing ? '开启' : '关闭' }}</span>
</div>
<div class="info-item">
<span class="label">当前点数:</span>
<span class="value">{{ currentPoints.length }}</span>
</div>
<div class="info-item">
<span class="label">围栏数量:</span>
<span class="value">{{ testFences.length }}</span>
</div>
<div class="info-item" v-if="selectedFence">
<span class="label">选中围栏:</span>
<span class="value">{{ selectedFence.name }}</span>
</div>
</div>
</div>
</template>
<script>
import MapView from '@/components/MapView.vue'
export default {
name: 'MapTest',
components: {
MapView
},
data() {
return {
mapReady: false,
isDrawing: false,
mapCenter: { lng: 106.27, lat: 38.47 },
mapZoom: 8,
currentPoints: [],
testFences: [],
selectedFence: null
}
},
methods: {
onMapReady(mapInstance) {
console.log('地图准备就绪:', mapInstance)
this.mapReady = true
},
onMapClick(event) {
if (!this.isDrawing) return
const point = {
lng: event.point.lng,
lat: event.point.lat,
id: Date.now() + Math.random(),
order: this.currentPoints.length
}
this.currentPoints.push(point)
console.log('点击坐标:', point)
},
onFenceClick(fence) {
console.log('点击围栏:', fence)
this.selectedFence = fence
},
onToggleDrawing(isDrawing) {
this.isDrawing = isDrawing
},
onCenterMap(center) {
console.log('居中地图到:', center)
},
onClearMap() {
this.currentPoints = []
this.selectedFence = null
},
addTestFence() {
const testFence = {
id: Date.now(),
name: `测试围栏_${this.testFences.length + 1}`,
type: 'grazing',
description: '这是一个测试围栏',
coordinates: [
{ lng: 106.27, lat: 38.47 },
{ lng: 106.28, lat: 38.47 },
{ lng: 106.28, lat: 38.48 },
{ lng: 106.27, lat: 38.48 }
],
center_lng: 106.275,
center_lat: 38.475,
area: 1000
}
this.testFences.push(testFence)
console.log('添加测试围栏:', testFence)
},
clearTestFences() {
this.testFences = []
this.selectedFence = null
console.log('清除所有测试围栏')
},
toggleDrawing() {
this.isDrawing = !this.isDrawing
if (!this.isDrawing) {
this.currentPoints = []
}
}
}
}
</script>
<style scoped>
.map-test {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.test-header {
padding: 20px;
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
}
.test-header h2 {
margin: 0 0 8px 0;
color: #333;
font-size: 24px;
}
.test-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.test-controls {
padding: 16px 20px;
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 12px;
}
.test-btn {
padding: 8px 16px;
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.test-btn:hover {
background-color: #40a9ff;
}
.map-wrapper {
flex: 1;
position: relative;
min-height: 400px;
}
.test-info {
padding: 20px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
max-height: 200px;
overflow-y: auto;
}
.test-info h3 {
margin: 0 0 16px 0;
color: #333;
font-size: 18px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding: 4px 0;
}
.label {
font-weight: 500;
color: #666;
}
.value {
color: #333;
font-family: monospace;
}
</style>

View File

@@ -1,589 +0,0 @@
<template>
<div class="map-view">
<div class="map-container" ref="mapContainer">
<!-- 地图加载状态 -->
<div v-if="!mapLoaded" class="map-loading">
<div class="loading-spinner"></div>
<div class="loading-text">地图加载中...</div>
</div>
</div>
<!-- 地图控制按钮 -->
<div class="map-controls" v-if="mapLoaded">
<button class="control-btn" @click="centerMap">
<span class="btn-icon">🎯</span>
<span class="btn-text">定位</span>
</button>
<button class="control-btn" @click="toggleDrawing">
<span class="btn-icon">{{ isDrawing ? '✋' : '✏️' }}</span>
<span class="btn-text">{{ isDrawing ? '停止绘制' : '开始绘制' }}</span>
</button>
<button class="control-btn" @click="clearMap">
<span class="btn-icon">🗑</span>
<span class="btn-text">清除</span>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'MapView',
props: {
// 地图中心点
center: {
type: Object,
default: () => ({ lng: 106.27, lat: 38.47 }) // 宁夏中心坐标
},
// 缩放级别
zoom: {
type: Number,
default: 8
},
// 是否启用绘制模式
drawingMode: {
type: Boolean,
default: false
},
// 围栏数据
fences: {
type: Array,
default: () => []
},
// 当前绘制的坐标点
currentPoints: {
type: Array,
default: () => []
}
},
data() {
return {
mapLoaded: false,
isDrawing: false,
mapInstance: null,
allPolygons: [],
allMarkers: [],
tempMarkers: [],
tempPolygon: null,
baiduMapLoaded: false
}
},
mounted() {
this.loadBaiduMapScript()
},
watch: {
drawingMode(newVal) {
this.isDrawing = newVal
},
fences: {
handler(newFences) {
this.displayFences(newFences)
},
deep: true
},
currentPoints: {
handler(newPoints) {
this.updateDrawingPoints(newPoints)
},
deep: true
}
},
methods: {
// 加载百度地图脚本
loadBaiduMapScript() {
if (window.BMap) {
this.initMap()
return
}
// 创建script标签加载百度地图API
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = `https://api.map.baidu.com/api?v=3.0&ak=SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo&callback=initBaiduMap`
script.onerror = () => {
console.error('百度地图API加载失败')
this.mapLoaded = true // 即使失败也显示地图容器
}
// 设置全局回调函数
window.initBaiduMap = () => {
this.baiduMapLoaded = true
this.initMap()
}
document.head.appendChild(script)
},
// 初始化地图
initMap() {
if (!window.BMap) {
console.error('百度地图API未加载')
this.mapLoaded = true
return
}
try {
// 创建地图实例
this.mapInstance = new BMap.Map(this.$refs.mapContainer)
// 设置地图中心点和缩放级别
const centerPoint = new BMap.Point(this.center.lng, this.center.lat)
this.mapInstance.centerAndZoom(centerPoint, this.zoom)
// 启用地图控件
this.mapInstance.enableScrollWheelZoom(true)
this.mapInstance.enableDoubleClickZoom(true)
this.mapInstance.enableKeyboard(true)
this.mapInstance.enableDragging(true)
// 添加地图点击事件
this.mapInstance.addEventListener('click', this.onMapClick)
// 地图加载完成
this.mapInstance.addEventListener('tilesloaded', () => {
this.mapLoaded = true
this.$emit('map-ready', this.mapInstance)
console.log('百度地图初始化完成')
})
console.log('百度地图初始化开始')
} catch (error) {
console.error('地图初始化失败:', error)
this.mapLoaded = true
}
},
// 地图点击事件
onMapClick(event) {
if (!this.mapInstance) return
console.log('地图点击事件:', event)
// 获取坐标信息,兼容不同的百度地图事件格式
let lnglat = null
if (event.lnglat) {
lnglat = event.lnglat
} else if (event.point) {
lnglat = event.point
} else if (event.lng !== undefined && event.lat !== undefined) {
lnglat = { lng: event.lng, lat: event.lat }
} else {
console.error('无法获取地图点击坐标:', event)
return
}
console.log('解析的坐标:', lnglat)
// 如果处于绘制模式,处理绘制逻辑
if (this.drawingMode) {
const point = {
lng: lnglat.lng,
lat: lnglat.lat,
id: Date.now() + Math.random(),
order: this.currentPoints.length
}
console.log('绘制模式 - 添加坐标点:', point)
this.$emit('drawing-click', point)
}
// 发射地图点击事件
this.$emit('map-click', {
lnglat: lnglat,
point: lnglat
})
},
// 显示围栏
displayFences(fences) {
if (!this.mapInstance) return
// 清除现有围栏
this.clearFences()
// 显示新围栏
fences.forEach(fence => {
if (fence.coordinates && fence.coordinates.length >= 3) {
this.addFenceToMap(fence)
}
})
},
// 添加围栏到地图
addFenceToMap(fence) {
if (!this.mapInstance) return
try {
// 创建多边形点数组
const points = fence.coordinates.map(coord =>
new BMap.Point(coord.lng, coord.lat)
)
// 根据围栏类型设置颜色
const colors = {
grazing: '#52c41a',
safety: '#1890ff',
restricted: '#ff4d4f',
collector: '#fa8c16'
}
const color = colors[fence.type] || '#52c41a'
// 创建多边形
const polygon = new BMap.Polygon(points, {
strokeColor: color,
strokeWeight: 2,
strokeOpacity: 0.8,
fillColor: color,
fillOpacity: 0.2
})
// 添加点击事件
polygon.addEventListener('click', () => {
this.$emit('fence-click', fence)
})
// 添加到地图
this.mapInstance.addOverlay(polygon)
this.allPolygons.push(polygon)
// 添加中心点标记
if (fence.center_lng && fence.center_lat) {
const marker = new BMap.Marker(
new BMap.Point(fence.center_lng, fence.center_lat)
)
// 添加标签
const label = new BMap.Label(fence.name, {
offset: new BMap.Size(0, -30)
})
marker.setLabel(label)
// 添加点击事件
marker.addEventListener('click', () => {
this.$emit('fence-click', fence)
})
this.mapInstance.addOverlay(marker)
this.allMarkers.push(marker)
}
console.log('围栏已添加到地图:', fence.name)
} catch (error) {
console.error('添加围栏到地图失败:', error)
}
},
// 更新绘制点
updateDrawingPoints(points) {
if (!this.mapInstance) return
// 清除临时标记
this.clearTempOverlays()
if (points.length === 0) return
try {
// 添加临时标记
points.forEach((point, index) => {
const marker = new BMap.Marker(
new BMap.Point(point.lng, point.lat)
)
// 添加序号标签
const label = new BMap.Label(`${index + 1}`, {
offset: new BMap.Size(0, -30),
style: {
color: '#fff',
backgroundColor: '#1890ff',
border: '1px solid #fff',
borderRadius: '50%',
padding: '2px 6px',
fontSize: '12px',
fontWeight: 'bold'
}
})
marker.setLabel(label)
this.mapInstance.addOverlay(marker)
this.tempMarkers.push(marker)
})
// 如果点数大于2绘制临时多边形
if (points.length > 2) {
const polygonPoints = points.map(point =>
new BMap.Point(point.lng, point.lat)
)
const isComplete = points.length >= 3
this.tempPolygon = new BMap.Polygon(polygonPoints, {
strokeColor: isComplete ? '#52c41a' : '#ff4d4f',
strokeWeight: 3,
strokeOpacity: 0.8,
fillColor: isComplete ? '#52c41a' : '#ff4d4f',
fillOpacity: 0.1,
strokeStyle: 'solid'
})
this.mapInstance.addOverlay(this.tempPolygon)
}
console.log('绘制点已更新:', points.length)
} catch (error) {
console.error('更新绘制点失败:', error)
}
},
// 清除临时覆盖物
clearTempOverlays() {
if (!this.mapInstance) return
// 清除临时标记
this.tempMarkers.forEach(marker => {
this.mapInstance.removeOverlay(marker)
})
this.tempMarkers = []
// 清除临时多边形
if (this.tempPolygon) {
this.mapInstance.removeOverlay(this.tempPolygon)
this.tempPolygon = null
}
},
// 清除围栏
clearFences() {
if (!this.mapInstance) return
// 清除围栏多边形
this.allPolygons.forEach(polygon => {
this.mapInstance.removeOverlay(polygon)
})
this.allPolygons = []
// 清除围栏标记
this.allMarkers.forEach(marker => {
this.mapInstance.removeOverlay(marker)
})
this.allMarkers = []
},
// 居中地图
centerMap(center = null) {
if (!this.mapInstance) {
console.warn('地图实例未初始化')
return
}
const targetCenter = center || this.center
console.log('MapView centerMap 接收到的坐标:', targetCenter)
if (!targetCenter || typeof targetCenter.lng !== 'number' || typeof targetCenter.lat !== 'number') {
console.error('无效的坐标数据:', targetCenter)
return
}
const centerPoint = new BMap.Point(targetCenter.lng, targetCenter.lat)
console.log('创建的百度地图坐标点:', centerPoint)
// 使用更高的缩放级别来更好地显示围栏
const zoomLevel = Math.max(this.zoom, 15)
this.mapInstance.centerAndZoom(centerPoint, zoomLevel)
this.$emit('center-map', targetCenter)
console.log('地图已定位到:', targetCenter)
},
// 定位并放大显示围栏
focusOnFence(coordinates) {
if (!this.mapInstance || !coordinates || coordinates.length === 0) {
console.warn('地图实例未初始化或围栏坐标为空')
return
}
console.log('开始定位围栏,坐标点数量:', coordinates.length)
// 计算围栏的边界
const bounds = this.calculateFenceBounds(coordinates)
console.log('围栏边界:', bounds)
// 使用fitBounds方法自动调整地图视图以包含整个围栏
this.mapInstance.setViewport(bounds)
// 稍微缩小一点,让围栏周围有一些边距
setTimeout(() => {
const currentZoom = this.mapInstance.getZoom()
if (currentZoom > 10) {
this.mapInstance.setZoom(currentZoom - 1)
}
}, 300)
console.log('围栏定位完成,地图已放大显示')
},
// 计算围栏边界
calculateFenceBounds(coordinates) {
if (!coordinates || coordinates.length === 0) {
return null
}
let minLng = coordinates[0].lng
let maxLng = coordinates[0].lng
let minLat = coordinates[0].lat
let maxLat = coordinates[0].lat
coordinates.forEach(point => {
minLng = Math.min(minLng, point.lng)
maxLng = Math.max(maxLng, point.lng)
minLat = Math.min(minLat, point.lat)
maxLat = Math.max(maxLat, point.lat)
})
// 添加一些边距
const lngMargin = (maxLng - minLng) * 0.1
const latMargin = (maxLat - minLat) * 0.1
const sw = new BMap.Point(minLng - lngMargin, minLat - latMargin)
const ne = new BMap.Point(maxLng + lngMargin, maxLat + latMargin)
return new BMap.Bounds(sw, ne)
},
// 切换绘制模式
toggleDrawing() {
this.isDrawing = !this.isDrawing
this.$emit('toggle-drawing', this.isDrawing)
},
// 清除地图
clearMap() {
this.clearFences()
this.clearTempOverlays()
this.$emit('clear-map')
}
},
beforeDestroy() {
// 清理全局回调函数
if (window.initBaiduMap) {
delete window.initBaiduMap
}
}
}
</script>
<style scoped>
.map-view {
position: relative;
width: 100%;
height: 100%;
background-color: #f0f0f0;
}
.map-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
/* 地图加载状态 */
.map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #666;
z-index: 1;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
font-weight: 500;
color: #666;
}
/* 地图控制按钮 */
.map-controls {
position: absolute;
top: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 100;
}
.control-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background-color: rgba(255, 255, 255, 0.9);
border: none;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
color: #333;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s;
backdrop-filter: blur(10px);
}
.control-btn:hover {
background-color: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.control-btn:active {
transform: translateY(0);
}
.btn-icon {
font-size: 14px;
}
.btn-text {
font-size: 12px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.map-controls {
top: 12px;
right: 12px;
}
.control-btn {
padding: 6px 10px;
font-size: 11px;
}
.btn-text {
display: none;
}
}
</style>

View File

@@ -1,756 +0,0 @@
<template>
<div class="password-login-page">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">14:38</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">92%</span>
</div>
</div>
<!-- 头部导航栏 -->
<div class="header-bar">
<div class="header-left">
<span class="home-icon">🏠</span>
</div>
<div class="header-right">
<span class="menu-icon"></span>
<span class="minus-icon"></span>
<span class="target-icon"></span>
</div>
</div>
<!-- 语言选择 -->
<div class="language-selector">
<select v-model="selectedLanguage" class="language-dropdown">
<option value="zh-CN">简体中文</option>
<option value="en-US">English</option>
</select>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 应用标题 -->
<div class="app-title">
<h1>爱农智慧牧场</h1>
</div>
<!-- 登录表单 -->
<div class="login-form">
<!-- 账号输入框 -->
<div class="input-group">
<div class="input-icon">
<span class="icon-user">👤</span>
</div>
<input
v-model="formData.account"
type="text"
placeholder="请输入账号"
class="form-input"
:class="{ 'error': errors.account }"
@input="clearError('account')"
/>
</div>
<!-- 密码输入框 -->
<div class="input-group">
<div class="input-icon">
<span class="icon-lock">🔒</span>
</div>
<input
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
class="form-input"
:class="{ 'error': errors.password }"
@input="clearError('password')"
/>
<button
class="password-toggle"
@click="togglePasswordVisibility"
>
{{ showPassword ? '👁️' : '👁️‍🗨️' }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin || isLoading"
@click="handleLogin"
>
{{ isLoading ? '登录中...' : '登录' }}
</button>
<!-- 协议同意 -->
<div class="agreement-section">
<label class="agreement-checkbox">
<input
type="checkbox"
v-model="agreedToTerms"
class="checkbox-input"
/>
<span class="checkbox-custom"></span>
</label>
<div class="agreement-text">
我已阅读并同意
<a href="#" class="agreement-link" @click="showUserAgreement">用户服务协议</a>
<a href="#" class="agreement-link" @click="showPrivacyPolicy">隐私政策</a>
</div>
</div>
</div>
<!-- 其他登录方式 -->
<div class="alternative-login">
<div class="login-option" @click="handleOneClickLogin">
<span class="option-icon"></span>
<span class="option-text">一键登录</span>
</div>
<div class="login-option" @click="handleSmsLogin">
<span class="option-icon">📱</span>
<span class="option-text">短信登录</span>
</div>
<div class="login-option" @click="handleRegister">
<span class="option-icon">📝</span>
<span class="option-text">注册账号</span>
</div>
<div class="login-option" @click="handleOtherLogin">
<span class="option-icon"></span>
<span class="option-text">其它</span>
</div>
</div>
<!-- 底部服务信息 -->
<div class="footer-services">
<div class="service-item" @click="handleProductionSupervision">
<span class="service-icon">📊</span>
<span class="service-text">生资监管方案</span>
</div>
<div class="service-item" @click="handleTianyiBinding">
<span class="service-icon">🔗</span>
<span class="service-text">绑定天翼账号</span>
</div>
</div>
<!-- 免责声明 -->
<div class="disclaimer">
<p>说明该系统仅对在小程序主体公司备案的客户开放</p>
</div>
</div>
</div>
</template>
<script>
import auth from '@/utils/auth'
export default {
name: 'PasswordLogin',
data() {
return {
selectedLanguage: 'zh-CN',
formData: {
account: '',
password: ''
},
errors: {
account: false,
password: false
},
errorMessage: '',
isLoading: false,
showPassword: false,
agreedToTerms: true
}
},
computed: {
canLogin() {
return this.formData.account.length > 0 &&
this.formData.password.length > 0 &&
this.agreedToTerms
}
},
mounted() {
// 检查是否已经登录
if (auth.isAuthenticated()) {
this.$router.push('/')
}
},
methods: {
// 清除错误状态
clearError(field) {
this.errors[field] = false
this.errorMessage = ''
},
// 切换密码显示
togglePasswordVisibility() {
this.showPassword = !this.showPassword
},
// 验证表单
validateForm() {
if (!this.formData.account.trim()) {
this.errors.account = true
this.errorMessage = '请输入账号'
return false
}
if (!this.formData.password) {
this.errors.password = true
this.errorMessage = '请输入密码'
return false
}
if (this.formData.password.length < 6) {
this.errors.password = true
this.errorMessage = '密码长度不能少于6位'
return false
}
return true
},
// 处理登录
async handleLogin() {
if (!this.validateForm()) {
return
}
this.isLoading = true
this.errorMessage = ''
try {
// 模拟登录验证
await this.simulateLogin()
// 设置认证信息
await auth.setTestToken()
auth.setUserInfo({
id: 'user-' + Date.now(),
name: '爱农智慧牧场用户',
account: this.formData.account,
role: 'user',
loginTime: new Date().toISOString(),
loginType: 'password'
})
// 延迟一下再跳转确保token设置完成
setTimeout(() => {
this.$router.push('/')
}, 100)
console.log('密码登录成功')
this.$message && this.$message.success('登录成功')
} catch (error) {
console.error('登录失败:', error)
this.errorMessage = '账号或密码错误,请重试'
} finally {
this.isLoading = false
}
},
// 模拟登录过程
simulateLogin() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟验证逻辑(开发环境)
if (this.formData.account === 'admin' && this.formData.password === '123456') {
resolve()
} else if (this.formData.password.length >= 6) {
// 其他情况也允许登录(开发环境)
resolve()
} else {
reject(new Error('账号或密码错误'))
}
}, 1500)
})
},
// 一键登录
handleOneClickLogin() {
console.log('一键登录')
this.$router.push('/login')
},
// 短信登录
handleSmsLogin() {
console.log('短信登录')
this.$router.push('/sms-login')
},
// 注册账号
handleRegister() {
console.log('注册账号')
this.$router.push('/register')
},
// 其他登录方式
handleOtherLogin() {
console.log('其他登录方式')
// 可以添加更多登录方式如微信登录、QQ登录等
alert('其他登录方式开发中...')
},
// 生资监管方案
handleProductionSupervision() {
console.log('生资监管方案')
alert('生资监管方案功能开发中...')
},
// 绑定天翼账号
handleTianyiBinding() {
console.log('绑定天翼账号')
alert('绑定天翼账号功能开发中...')
},
// 显示用户服务协议
showUserAgreement() {
console.log('显示用户服务协议')
alert('用户服务协议内容...')
},
// 显示隐私政策
showPrivacyPolicy() {
console.log('显示隐私政策')
alert('隐私政策内容...')
}
}
}
</script>
<style scoped>
.password-login-page {
min-height: 100vh;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background-color: #ffffff;
font-size: 14px;
color: #000000;
}
.time {
font-weight: 500;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 12px;
}
/* 头部导航栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
}
.header-left .home-icon {
font-size: 20px;
cursor: pointer;
}
.header-right {
display: flex;
gap: 12px;
align-items: center;
}
.header-right span {
font-size: 16px;
cursor: pointer;
padding: 4px;
}
/* 语言选择 */
.language-selector {
display: flex;
justify-content: flex-end;
padding: 0 20px 20px 0;
}
.language-dropdown {
border: none;
background: transparent;
font-size: 14px;
color: #333;
cursor: pointer;
outline: none;
}
/* 主要内容区域 */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
min-height: calc(100vh - 200px);
}
/* 应用标题 */
.app-title {
margin-bottom: 60px;
text-align: center;
}
.app-title h1 {
font-size: 28px;
font-weight: bold;
color: #000000;
margin: 0;
letter-spacing: 1px;
}
/* 登录表单 */
.login-form {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60px;
width: 100%;
max-width: 320px;
}
/* 输入框组 */
.input-group {
position: relative;
margin-bottom: 20px;
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
width: 100%;
}
.input-group:focus-within {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.1);
}
.input-icon {
padding: 0 16px;
display: flex;
align-items: center;
color: #999;
font-size: 16px;
}
.icon-user,
.icon-lock {
font-size: 16px;
}
.form-input {
flex: 1;
padding: 16px 0;
border: none;
background: transparent;
font-size: 16px;
color: #333;
outline: none;
}
.form-input::placeholder {
color: #999;
}
.form-input.error {
color: #ff4d4f;
}
/* 密码显示切换按钮 */
.password-toggle {
padding: 8px 16px;
margin-right: 8px;
background-color: transparent;
border: none;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.password-toggle:hover {
opacity: 0.7;
}
/* 错误提示 */
.error-message {
color: #ff4d4f;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 50px;
background-color: #52c41a;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.login-btn:hover:not(:disabled) {
background-color: #45a018;
transform: translateY(-1px);
}
.login-btn:disabled {
background-color: #a0d468;
cursor: not-allowed;
}
/* 协议同意区域 */
.agreement-section {
display: flex;
align-items: flex-start;
gap: 8px;
max-width: 100%;
}
.agreement-checkbox {
display: flex;
align-items: center;
cursor: pointer;
margin-top: 2px;
}
.checkbox-input {
display: none;
}
.checkbox-custom {
width: 16px;
height: 16px;
border: 2px solid #d9d9d9;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.checkbox-input:checked + .checkbox-custom {
background-color: #52c41a;
border-color: #52c41a;
}
.checkbox-input:checked + .checkbox-custom::after {
content: '✓';
color: white;
font-size: 12px;
font-weight: bold;
}
.agreement-text {
font-size: 12px;
color: #666666;
line-height: 1.4;
}
.agreement-link {
color: #52c41a;
text-decoration: none;
}
.agreement-link:hover {
text-decoration: underline;
}
/* 其他登录方式 */
.alternative-login {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 60px;
}
.login-option {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 24px;
background-color: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
min-width: 200px;
}
.login-option:hover {
background-color: #e9ecef;
transform: translateY(-1px);
}
.option-icon {
font-size: 18px;
}
.option-text {
font-size: 16px;
color: #333333;
font-weight: 500;
}
/* 底部服务信息 */
.footer-services {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 40px;
}
.service-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s;
}
.service-item:hover {
background-color: #f8f9fa;
border-radius: 6px;
}
.service-icon {
font-size: 16px;
}
.service-text {
font-size: 14px;
color: #666666;
}
/* 免责声明 */
.disclaimer {
text-align: left;
max-width: 320px;
}
.disclaimer p {
font-size: 12px;
color: #999999;
line-height: 1.5;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-content {
padding: 20px 16px;
}
.app-title h1 {
font-size: 24px;
}
.login-form {
max-width: 100%;
}
.input-group {
margin-bottom: 16px;
}
.form-input {
padding: 14px 0;
font-size: 15px;
}
.login-btn {
height: 45px;
font-size: 16px;
}
.agreement-section {
max-width: 100%;
}
.login-option {
min-width: 160px;
}
.footer-services {
margin-bottom: 20px;
}
.disclaimer {
max-width: 100%;
}
}
/* 小屏幕优化 */
@media (max-width: 375px) {
.app-title h1 {
font-size: 22px;
}
.login-btn {
height: 45px;
font-size: 16px;
}
.login-option {
padding: 10px 20px;
}
.option-text {
font-size: 14px;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.main-content {
padding: 20px;
min-height: auto;
}
.app-title {
margin-bottom: 30px;
}
.login-form {
margin-bottom: 30px;
}
.alternative-login {
margin-bottom: 30px;
}
.footer-services {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,419 +0,0 @@
<template>
<div class="production">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">11:29</div>
<div class="title">生产管理</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">85%</span>
</div>
</div>
<!-- 生产管理内容 -->
<div class="content">
<!-- 牛只管理 -->
<div class="management-section">
<div class="section-header">
<div class="section-bar"></div>
<h2>牛只管理</h2>
</div>
<div class="function-grid">
<div
v-for="func in cattleFunctions"
:key="func.key"
class="function-card"
@click="handleFunctionClick('cattle', func)"
>
<div class="function-icon" :style="{ backgroundColor: func.color }">
{{ func.icon }}
</div>
<div class="function-label">{{ func.label }}</div>
</div>
</div>
</div>
<!-- 猪只管理 -->
<div class="management-section">
<div class="section-header">
<div class="section-bar"></div>
<h2>猪只管理</h2>
</div>
<div class="function-grid">
<div
v-for="func in pigFunctions"
:key="func.key"
class="function-card"
@click="handleFunctionClick('pig', func)"
>
<div class="function-icon" :style="{ backgroundColor: func.color }">
{{ func.icon }}
</div>
<div class="function-label">{{ func.label }}</div>
</div>
</div>
</div>
<!-- 羊只管理 -->
<div class="management-section">
<div class="section-header">
<div class="section-bar"></div>
<h2>羊只管理</h2>
</div>
<div class="function-grid">
<div
v-for="func in sheepFunctions"
:key="func.key"
class="function-card"
@click="handleFunctionClick('sheep', func)"
>
<div class="function-icon" :style="{ backgroundColor: func.color }">
{{ func.icon }}
</div>
<div class="function-label">{{ func.label }}</div>
</div>
</div>
</div>
<!-- 家禽管理 -->
<div class="management-section">
<div class="section-header">
<div class="section-bar"></div>
<h2>家禽管理</h2>
</div>
<div class="function-grid">
<div
v-for="func in poultryFunctions"
:key="func.key"
class="function-card"
@click="handleFunctionClick('poultry', func)"
>
<div class="function-icon" :style="{ backgroundColor: func.color }">
{{ func.icon }}
</div>
<div class="function-label">{{ func.label }}</div>
</div>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div
v-for="nav in bottomNavItems"
:key="nav.key"
:class="['nav-item', { active: activeNav === nav.key }]"
@click="handleNavClick(nav)"
>
<div class="nav-icon">{{ nav.icon }}</div>
<div class="nav-label">{{ nav.label }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Production',
data() {
return {
activeNav: 'production',
bottomNavItems: [
{ key: 'home', icon: '🏠', label: '首页', color: '#8e8e93' },
{ key: 'production', icon: '📦', label: '生产管理', color: '#34c759' },
{ key: 'profile', icon: '👤', label: '我的', color: '#8e8e93' }
],
// 牛只管理功能
cattleFunctions: [
{ key: 'archive', icon: '🐄', label: '牛档案', color: '#007aff' },
{ key: 'estrus', icon: '🏠', label: '发情记录', color: '#ff9500' },
{ key: 'mating', icon: '🧪', label: '配种记录', color: '#007aff' },
{ key: 'pregnancy', icon: '📅', label: '妊检记录', color: '#ffcc00' },
{ key: 'calving', icon: '🐣', label: '分娩记录', color: '#34c759' },
{ key: 'weaning', icon: '✏️', label: '断奶记录', color: '#007aff' },
{ key: 'transfer', icon: '🏠', label: '转栏记录', color: '#ff3b30' },
{ key: 'departure', icon: '🏠', label: '离栏记录', color: '#ff9500' },
{ key: 'pen_setting', icon: '🏠', label: '栏舍设置', color: '#34c759' },
{ key: 'batch_setting', icon: '📄', label: '批次设置', color: '#007aff' },
{ key: 'epidemic', icon: '🏠', label: '防疫预警', color: '#007aff' }
],
// 猪只管理功能
pigFunctions: [
{ key: 'archive', icon: '🐷', label: '猪档案', color: '#007aff' },
{ key: 'estrus', icon: '🏠', label: '发情记录', color: '#ff9500' },
{ key: 'mating', icon: '🧪', label: '配种记录', color: '#007aff' },
{ key: 'pregnancy', icon: '📅', label: '妊检记录', color: '#ffcc00' },
{ key: 'farrowing', icon: '🐣', label: '分娩记录', color: '#34c759' },
{ key: 'weaning', icon: '✏️', label: '断奶记录', color: '#007aff' },
{ key: 'transfer', icon: '🏠', label: '转栏记录', color: '#ff3b30' },
{ key: 'departure', icon: '🏠', label: '离栏记录', color: '#ff9500' },
{ key: 'pen_setting', icon: '🏠', label: '栏舍设置', color: '#34c759' },
{ key: 'batch_setting', icon: '📄', label: '批次设置', color: '#007aff' },
{ key: 'epidemic', icon: '🏠', label: '防疫预警', color: '#007aff' }
],
// 羊只管理功能
sheepFunctions: [
{ key: 'archive', icon: '🐑', label: '羊档案', color: '#007aff' },
{ key: 'estrus', icon: '🏠', label: '发情记录', color: '#ff9500' },
{ key: 'mating', icon: '🧪', label: '配种记录', color: '#007aff' },
{ key: 'pregnancy', icon: '📅', label: '妊检记录', color: '#ffcc00' },
{ key: 'lambing', icon: '🐣', label: '分娩记录', color: '#34c759' },
{ key: 'weaning', icon: '✏️', label: '断奶记录', color: '#007aff' },
{ key: 'transfer', icon: '🏠', label: '转栏记录', color: '#ff3b30' },
{ key: 'departure', icon: '🏠', label: '离栏记录', color: '#ff9500' },
{ key: 'pen_setting', icon: '🏠', label: '栏舍设置', color: '#34c759' },
{ key: 'batch_setting', icon: '📄', label: '批次设置', color: '#007aff' },
{ key: 'epidemic', icon: '🏠', label: '防疫预警', color: '#007aff' }
],
// 家禽管理功能
poultryFunctions: [
{ key: 'archive', icon: '🐔', label: '家禽档案', color: '#007aff' },
{ key: 'departure', icon: '🏠', label: '离栏记录', color: '#ff9500' },
{ key: 'pen_setting', icon: '🏠', label: '栏舍设置', color: '#34c759' },
{ key: 'batch_setting', icon: '📄', label: '批次设置', color: '#007aff' },
{ key: 'scan_entry', icon: '📱', label: '扫码录入', color: '#af52de' },
{ key: 'scan_print', icon: '🖨️', label: '扫码打印', color: '#34c759' }
]
}
},
methods: {
handleFunctionClick(animalType, func) {
console.log('点击功能:', animalType, func.label)
// 根据动物类型和功能类型进行跳转
if (animalType === 'cattle' && func.key === 'archive') {
// 牛档案跳转到牛只档案页面
this.$router.push('/cattle-profile')
} else if (animalType === 'cattle' && func.key === 'transfer') {
// 牛只转栏记录跳转
this.$router.push('/cattle-transfer')
} else if (animalType === 'cattle' && func.key === 'departure') {
// 牛只离栏记录跳转
this.$router.push('/cattle-exit')
} else if (animalType === 'cattle' && func.key === 'pen_setting') {
// 牛只栏舍设置跳转
this.$router.push('/cattle-pen')
} else if (animalType === 'cattle' && func.key === 'batch_setting') {
// 牛只批次设置跳转
this.$router.push('/cattle-batch')
} else if (animalType === 'pig' && func.key === 'archive') {
// 猪档案跳转(待实现)
console.log('跳转到猪档案页面')
// this.$router.push('/pig-profile')
} else if (animalType === 'pig' && func.key === 'transfer') {
// 猪只转栏记录跳转(待实现)
console.log('跳转到猪只转栏记录页面')
// this.$router.push('/pig-transfer')
} else if (animalType === 'sheep' && func.key === 'archive') {
// 羊档案跳转(待实现)
console.log('跳转到羊档案页面')
// this.$router.push('/sheep-profile')
} else if (animalType === 'sheep' && func.key === 'transfer') {
// 羊只转栏记录跳转(待实现)
console.log('跳转到羊只转栏记录页面')
// this.$router.push('/sheep-transfer')
} else if (animalType === 'poultry' && func.key === 'archive') {
// 家禽档案跳转(待实现)
console.log('跳转到家禽档案页面')
// this.$router.push('/poultry-profile')
} else {
// 其他功能暂时显示提示
console.log(`${animalType} - ${func.label} 功能开发中`)
// 可以添加用户提示
// this.$message.info(`${func.label} 功能开发中`)
}
},
handleNavClick(nav) {
this.activeNav = nav.key
const routes = {
home: '/',
production: '/production',
profile: '/profile'
}
this.$router.push(routes[nav.key])
}
}
}
</script>
<style scoped>
.production {
background-color: #ffffff;
min-height: 100vh;
padding-bottom: 80px;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.time {
font-size: 16px;
font-weight: 500;
color: #000000;
}
.title {
font-size: 18px;
font-weight: 600;
color: #000000;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 14px;
}
.content {
padding: 20px;
}
.management-section {
margin-bottom: 30px;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.section-bar {
width: 4px;
height: 20px;
background-color: #34c759;
margin-right: 12px;
border-radius: 2px;
}
.section-header h2 {
font-size: 18px;
font-weight: 600;
color: #000000;
margin: 0;
}
.function-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
}
.function-card {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.2s;
padding: 8px;
}
.function-card:hover {
transform: translateY(-2px);
}
.function-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 8px;
color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.function-label {
font-size: 12px;
color: #000000;
text-align: center;
line-height: 1.2;
font-weight: 500;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
display: flex;
height: 60px;
z-index: 1000;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: color 0.2s;
}
.nav-item.active .nav-icon {
color: #34c759;
}
.nav-item.active .nav-label {
color: #34c759;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
color: #8e8e93;
}
.nav-label {
font-size: 12px;
color: #8e8e93;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 480px) {
.function-grid {
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.function-icon {
width: 45px;
height: 45px;
font-size: 20px;
}
.function-label {
font-size: 11px;
}
}
@media (max-width: 360px) {
.function-grid {
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.function-icon {
width: 40px;
height: 40px;
font-size: 18px;
}
.function-label {
font-size: 10px;
}
}
</style>

View File

@@ -1,385 +0,0 @@
<template>
<div class="profile">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">11:29</div>
<div class="title">我的</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="network">4G</span>
<span class="battery">90%</span>
<span class="close">×</span>
</div>
</div>
<!-- 用户资料区域 -->
<div class="user-profile">
<div class="avatar">
<div class="avatar-circle">
<span class="avatar-text">A</span>
</div>
</div>
<div class="user-info">
<h3>AIOTAGRO</h3>
<p>15586823774</p>
</div>
</div>
<!-- 菜单项 -->
<div class="menu-section">
<div class="menu-item" @click="handleMenuClick('system-settings')">
<div class="menu-icon"></div>
<div class="menu-label">养殖系统设置</div>
<div class="menu-arrow">></div>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleMenuClick('switch-farm')">
<div class="menu-icon">🔄</div>
<div class="menu-label">切换养殖场</div>
<div class="menu-arrow">></div>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleMenuClick('farm-id')">
<div class="menu-icon">🆔</div>
<div class="menu-label">养殖场识别码</div>
<div class="menu-arrow">></div>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleMenuClick('associated-org')">
<div class="menu-icon">📄</div>
<div class="menu-label">关联机构</div>
<div class="menu-arrow">></div>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleMenuClick('homepage-custom')">
<div class="menu-icon"></div>
<div class="menu-label">首页自定义</div>
<div class="menu-arrow">></div>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleMenuClick('farm-settings')">
<div class="menu-icon">🏠</div>
<div class="menu-label">养殖场设置</div>
<div class="menu-arrow">></div>
</div>
</div>
<!-- 退出登录按钮 -->
<div class="logout-section">
<button class="logout-btn" @click="handleLogout">退出登录</button>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div
v-for="nav in bottomNavItems"
:key="nav.key"
:class="['nav-item', { active: activeNav === nav.key }]"
@click="handleNavClick(nav)"
>
<div class="nav-icon">{{ nav.icon }}</div>
<div class="nav-label">{{ nav.label }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Profile',
data() {
return {
activeNav: 'profile',
bottomNavItems: [
{ key: 'home', icon: '🏠', label: '首页', color: '#8e8e93' },
{ key: 'production', icon: '📦', label: '生产管理', color: '#8e8e93' },
{ key: 'profile', icon: '👤', label: '我的', color: '#34c759' }
]
}
},
methods: {
handleMenuClick(menu) {
console.log('点击菜单:', menu)
// 这里可以添加具体的菜单点击逻辑
switch(menu) {
case 'system-settings':
console.log('打开养殖系统设置')
break
case 'switch-farm':
console.log('切换养殖场')
break
case 'farm-id':
console.log('查看养殖场识别码')
break
case 'associated-org':
console.log('查看关联机构')
break
case 'homepage-custom':
console.log('首页自定义')
break
case 'farm-settings':
console.log('养殖场设置')
break
}
},
handleLogout() {
console.log('退出登录')
if (confirm('确定要退出登录吗?')) {
// 使用auth工具清除认证信息
const auth = require('@/utils/auth').default
auth.logout()
// 跳转到登录页面
this.$router.push('/login')
}
},
handleNavClick(nav) {
this.activeNav = nav.key
const routes = {
home: '/',
production: '/production',
profile: '/profile'
}
this.$router.push(routes[nav.key])
}
}
}
</script>
<style scoped>
.profile {
background-color: #ffffff;
min-height: 100vh;
padding-bottom: 80px;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.time {
font-size: 16px;
font-weight: 500;
color: #000000;
}
.title {
font-size: 18px;
font-weight: 600;
color: #000000;
}
.status-icons {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.signal {
color: #000000;
}
.network {
color: #000000;
font-size: 12px;
}
.battery {
color: #000000;
font-size: 12px;
}
.close {
color: #000000;
font-size: 16px;
font-weight: bold;
}
.user-profile {
display: flex;
align-items: center;
padding: 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.avatar {
margin-right: 16px;
}
.avatar-circle {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #34c759;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(52, 199, 89, 0.3);
}
.avatar-text {
color: #ffffff;
font-size: 24px;
font-weight: bold;
}
.user-info h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: #000000;
}
.user-info p {
margin: 0;
font-size: 14px;
color: #8e8e93;
}
.menu-section {
background-color: #ffffff;
margin-bottom: 20px;
}
.menu-item {
display: flex;
align-items: center;
padding: 16px 20px;
cursor: pointer;
transition: background-color 0.2s;
}
.menu-item:hover {
background-color: #f8f9fa;
}
.menu-icon {
font-size: 20px;
width: 24px;
text-align: center;
margin-right: 16px;
}
.menu-label {
flex: 1;
font-size: 16px;
color: #000000;
font-weight: 400;
}
.menu-arrow {
font-size: 16px;
color: #cccccc;
font-weight: bold;
}
.menu-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.logout-section {
padding: 20px;
text-align: center;
}
.logout-btn {
background: none;
border: none;
color: #8e8e93;
font-size: 16px;
cursor: pointer;
padding: 8px 16px;
transition: color 0.2s;
}
.logout-btn:hover {
color: #ff3b30;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
display: flex;
height: 60px;
z-index: 1000;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: color 0.2s;
}
.nav-item.active .nav-icon {
color: #34c759;
}
.nav-item.active .nav-label {
color: #34c759;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
color: #8e8e93;
}
.nav-label {
font-size: 12px;
color: #8e8e93;
font-weight: 500;
}
/* 响应式设计 */
@media (max-width: 480px) {
.user-profile {
padding: 16px;
}
.avatar-circle {
width: 50px;
height: 50px;
}
.avatar-text {
font-size: 20px;
}
.user-info h3 {
font-size: 16px;
}
.user-info p {
font-size: 13px;
}
.menu-item {
padding: 14px 16px;
}
.menu-label {
font-size: 15px;
}
}
</style>

View File

@@ -1,630 +0,0 @@
<template>
<div class="register-page">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">14:38</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">92%</span>
</div>
</div>
<!-- 头部导航栏 -->
<div class="header-bar">
<div class="back-btn" @click="goBack">
<span class="back-arrow"></span>
</div>
<div class="header-right">
<span class="menu-icon"></span>
<span class="minus-icon"></span>
<span class="target-icon"></span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 注册表单 -->
<div class="register-form">
<!-- 真实姓名输入框 -->
<div class="input-group">
<input
v-model="formData.realName"
type="text"
placeholder="请输入真实姓名"
class="form-input"
:class="{ 'error': errors.realName }"
@input="clearError('realName')"
/>
</div>
<!-- 手机号输入框 -->
<div class="input-group">
<input
v-model="formData.phone"
type="tel"
placeholder="请输入手机号"
class="form-input"
:class="{ 'error': errors.phone }"
@input="clearError('phone')"
maxlength="11"
/>
</div>
<!-- 验证码输入框 -->
<div class="input-group">
<input
v-model="formData.verificationCode"
type="text"
placeholder="请输入验证码"
class="form-input"
:class="{ 'error': errors.verificationCode }"
@input="clearError('verificationCode')"
maxlength="6"
/>
<button
class="send-code-btn"
:disabled="!canSendCode || isSending"
@click="sendVerificationCode"
>
{{ sendCodeText }}
</button>
</div>
<!-- 密码输入框 -->
<div class="input-group">
<input
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
class="form-input"
:class="{ 'error': errors.password }"
@input="clearError('password')"
/>
<button
class="password-toggle"
@click="togglePasswordVisibility"
>
{{ showPassword ? '👁️' : '👁️‍🗨️' }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<!-- 确认按钮 -->
<button
class="confirm-btn"
:disabled="!canRegister || isLoading"
@click="handleRegister"
>
{{ isLoading ? '注册中...' : '确认' }}
</button>
</div>
<!-- 其他选项 -->
<div class="alternative-options">
<div class="login-option" @click="goToLogin">
<span class="option-text">已有账号立即登录</span>
</div>
</div>
</div>
</div>
</template>
<script>
import auth from '@/utils/auth'
import { sendSmsCode, verifySmsCode } from '@/services/smsService'
import { registerUser, checkPhoneExists } from '@/services/userService'
export default {
name: 'Register',
data() {
return {
formData: {
realName: '',
phone: '',
verificationCode: '',
password: ''
},
errors: {
realName: false,
phone: false,
verificationCode: false,
password: false
},
errorMessage: '',
isLoading: false,
isSending: false,
showPassword: false,
countdown: 0,
timer: null
}
},
computed: {
canSendCode() {
return this.formData.phone.length === 11 && this.countdown === 0
},
canRegister() {
return this.formData.realName.length > 0 &&
this.formData.phone.length === 11 &&
this.formData.verificationCode.length === 6 &&
this.formData.password.length >= 6
},
sendCodeText() {
if (this.isSending) {
return '发送中...'
} else if (this.countdown > 0) {
return `${this.countdown}s后重发`
} else {
return '发送验证码'
}
}
},
mounted() {
// 检查是否已经登录
if (auth.isAuthenticated()) {
this.$router.push('/')
}
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
},
methods: {
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 清除错误状态
clearError(field) {
this.errors[field] = false
this.errorMessage = ''
},
// 切换密码显示
togglePasswordVisibility() {
this.showPassword = !this.showPassword
},
// 发送验证码
async sendVerificationCode() {
if (!this.validatePhone()) {
return
}
this.isSending = true
this.errorMessage = ''
try {
// 检查手机号是否已注册
const checkResult = await checkPhoneExists(this.formData.phone)
if (checkResult.data.exists) {
this.errors.phone = true
this.errorMessage = '该手机号已注册,请直接登录'
return
}
// 发送验证码
const result = await sendSmsCode(this.formData.phone, 'register')
if (result.success) {
// 开始倒计时
this.startCountdown()
console.log('验证码已发送到:', this.formData.phone)
this.$message && this.$message.success('验证码已发送')
} else {
this.errorMessage = result.message || '发送验证码失败,请重试'
}
} catch (error) {
console.error('发送验证码失败:', error)
this.errorMessage = '发送验证码失败,请重试'
} finally {
this.isSending = false
}
},
// 开始倒计时
startCountdown() {
this.countdown = 60
this.timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.timer)
this.timer = null
}
}, 1000)
},
// 验证手机号
validatePhone() {
const phoneRegex = /^1[3-9]\d{9}$/
if (!this.formData.phone) {
this.errors.phone = true
this.errorMessage = '请输入手机号'
return false
}
if (!phoneRegex.test(this.formData.phone)) {
this.errors.phone = true
this.errorMessage = '请输入正确的手机号'
return false
}
return true
},
// 验证表单
validateForm() {
let isValid = true
// 验证真实姓名
if (!this.formData.realName.trim()) {
this.errors.realName = true
this.errorMessage = '请输入真实姓名'
isValid = false
}
// 验证手机号
if (!this.validatePhone()) {
isValid = false
}
// 验证验证码
if (!this.formData.verificationCode) {
this.errors.verificationCode = true
this.errorMessage = '请输入验证码'
isValid = false
} else if (this.formData.verificationCode.length !== 6) {
this.errors.verificationCode = true
this.errorMessage = '请输入6位验证码'
isValid = false
}
// 验证密码
if (!this.formData.password) {
this.errors.password = true
this.errorMessage = '请输入密码'
isValid = false
} else if (this.formData.password.length < 6) {
this.errors.password = true
this.errorMessage = '密码长度不能少于6位'
isValid = false
}
return isValid
},
// 处理注册
async handleRegister() {
if (!this.validateForm()) {
return
}
this.isLoading = true
this.errorMessage = ''
try {
// 验证短信验证码
const verifyResult = await verifySmsCode(this.formData.phone, this.formData.verificationCode, 'register')
if (!verifyResult.success) {
this.errors.verificationCode = true
this.errorMessage = verifyResult.message || '验证码错误或已过期,请重新获取'
return
}
// 调用注册API
const registerResult = await registerUser({
realName: this.formData.realName,
phone: this.formData.phone,
password: this.formData.password,
verificationCode: this.formData.verificationCode
})
if (!registerResult.success) {
this.errorMessage = registerResult.message || '注册失败,请重试'
return
}
// 注册成功后自动登录
await auth.setTestToken()
auth.setUserInfo(registerResult.data.userInfo)
// 跳转到首页
setTimeout(() => {
this.$router.push('/')
}, 100)
console.log('注册成功')
this.$message && this.$message.success('注册成功,欢迎使用爱农智慧牧场!')
} catch (error) {
console.error('注册失败:', error)
this.errorMessage = '注册失败,请重试'
} finally {
this.isLoading = false
}
},
// 模拟注册API
simulateRegister() {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 1500)
})
},
// 跳转到登录页
goToLogin() {
this.$router.push('/login')
}
}
}
</script>
<style scoped>
.register-page {
min-height: 100vh;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background-color: #ffffff;
font-size: 14px;
color: #000000;
}
.time {
font-weight: 500;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 12px;
}
/* 头部导航栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.back-btn {
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
}
.back-arrow {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-right {
display: flex;
gap: 12px;
align-items: center;
}
.header-right span {
font-size: 16px;
cursor: pointer;
padding: 4px;
}
/* 主要内容区域 */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
min-height: calc(100vh - 200px);
}
/* 注册表单 */
.register-form {
width: 100%;
max-width: 320px;
margin-bottom: 40px;
}
/* 输入框组 */
.input-group {
position: relative;
margin-bottom: 20px;
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.input-group:focus-within {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.1);
}
.form-input {
flex: 1;
padding: 16px 20px;
border: none;
background: transparent;
font-size: 16px;
color: #333;
outline: none;
}
.form-input::placeholder {
color: #999;
}
.form-input.error {
color: #ff4d4f;
}
/* 发送验证码按钮 */
.send-code-btn {
padding: 8px 16px;
margin-right: 8px;
background-color: transparent;
border: 1px solid #1890ff;
border-radius: 4px;
color: #1890ff;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.send-code-btn:hover:not(:disabled) {
background-color: #1890ff;
color: white;
}
.send-code-btn:disabled {
border-color: #d9d9d9;
color: #999;
cursor: not-allowed;
}
/* 密码显示切换按钮 */
.password-toggle {
padding: 8px 16px;
margin-right: 8px;
background-color: transparent;
border: none;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.password-toggle:hover {
opacity: 0.7;
}
/* 错误提示 */
.error-message {
color: #ff4d4f;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
}
/* 确认按钮 */
.confirm-btn {
width: 100%;
height: 50px;
background-color: #52c41a;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.confirm-btn:hover:not(:disabled) {
background-color: #45a018;
transform: translateY(-1px);
}
.confirm-btn:disabled {
background-color: #a0d468;
cursor: not-allowed;
}
/* 其他选项 */
.alternative-options {
display: flex;
justify-content: center;
}
.login-option {
cursor: pointer;
transition: all 0.2s;
}
.login-option:hover {
opacity: 0.7;
}
.option-text {
font-size: 14px;
color: #666;
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-content {
padding: 30px 16px 20px;
}
.register-form {
max-width: 100%;
}
.input-group {
margin-bottom: 16px;
}
.form-input {
padding: 14px 16px;
font-size: 15px;
}
.send-code-btn {
padding: 6px 12px;
font-size: 13px;
}
.confirm-btn {
height: 45px;
font-size: 16px;
}
}
/* 小屏幕优化 */
@media (max-width: 375px) {
.form-input {
padding: 12px 16px;
}
.send-code-btn {
padding: 6px 10px;
font-size: 12px;
}
.option-text {
font-size: 13px;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.main-content {
padding: 20px;
min-height: auto;
}
.register-form {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,547 +0,0 @@
<template>
<div class="smart-ankle">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">14:38</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">92%</span>
</div>
</div>
<!-- 头部导航栏 -->
<div class="header-bar">
<div class="back-btn" @click="goBack">
<span class="back-arrow"></span>
</div>
<div class="title">智能脚环</div>
<div class="header-actions">
<span class="dots"></span>
<span class="circle-icon"></span>
</div>
</div>
<!-- 搜索和添加区域 -->
<div class="search-section">
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
v-model="searchQuery"
type="text"
placeholder="搜索"
class="search-input"
@input="handleSearch"
/>
</div>
<button class="add-btn" @click="handleAdd">
<span class="add-icon">+</span>
</button>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<div class="tab-item">
<span class="tab-label">脚环总数:</span>
<span class="tab-value">{{ totalCount }}</span>
</div>
<div class="tab-item">
<span class="tab-label">已绑定数量:</span>
<span class="tab-value">{{ boundCount }}</span>
</div>
<div class="tab-item">
<span class="tab-label">未绑定数量:</span>
<span class="tab-value">{{ unboundCount }}</span>
</div>
</div>
<!-- 设备列表 -->
<div class="device-list">
<div
v-for="device in filteredDevices"
:key="device.ankleId"
class="device-card"
>
<div class="device-info">
<div class="device-id">脚环编号: {{ device.ankleId }}</div>
<div class="device-data">
<div class="data-row">
<span class="data-label">设备电量/%:</span>
<span class="data-value">{{ device.battery }}%</span>
</div>
<div class="data-row">
<span class="data-label">设备温度/°C:</span>
<span class="data-value">{{ device.temperature }}</span>
</div>
<div class="data-row">
<span class="data-label">被采集主机:</span>
<span class="data-value">{{ device.collectedHost }}</span>
</div>
<div class="data-row">
<span class="data-label">总运动量:</span>
<span class="data-value">{{ device.totalMovement }}</span>
</div>
<div class="data-row">
<span class="data-label">今日运动量:</span>
<span class="data-value">{{ device.todayMovement }}</span>
</div>
<div class="data-row">
<span class="data-label">步数统计:</span>
<span class="data-value">{{ device.stepCount }}</span>
</div>
<div class="data-row">
<span class="data-label">数据更新时间:</span>
<span class="data-value">{{ device.updateTime }}</span>
</div>
</div>
</div>
<div class="device-actions">
<button
:class="['bind-btn', device.isBound ? 'bound' : 'unbound']"
@click="handleBind(device)"
>
{{ device.isBound ? '已绑定' : '未绑定' }}
</button>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && filteredDevices.length === 0" class="empty-state">
<div class="empty-icon">📱</div>
<div class="empty-text">暂无脚环设备</div>
</div>
</div>
</template>
<script>
import { getAnkleDevices, bindAnkle, unbindAnkle } from '@/services/ankleService'
export default {
name: 'SmartAnkle',
data() {
return {
loading: false,
searchQuery: '',
devices: [],
totalCount: 0,
boundCount: 0,
unboundCount: 0
}
},
computed: {
filteredDevices() {
if (!this.searchQuery) {
return this.devices
}
return this.devices.filter(device =>
device.ankleId.includes(this.searchQuery) ||
device.collectedHost.includes(this.searchQuery)
)
}
},
async mounted() {
await this.loadDevices()
},
methods: {
async loadDevices() {
this.loading = true
try {
const response = await getAnkleDevices()
this.devices = response.data || []
this.updateCounts()
} catch (error) {
console.error('加载脚环设备失败:', error)
// 使用模拟数据
this.devices = this.getMockData()
this.updateCounts()
} finally {
this.loading = false
}
},
getMockData() {
return [
{
ankleId: '2409501317',
battery: 68,
temperature: 28.8,
collectedHost: '2490246426',
totalMovement: 3456,
todayMovement: 234,
stepCount: 8923,
updateTime: '2025-09-18 14:30:15',
isBound: true
},
{
ankleId: '2407300110',
battery: 52,
temperature: 29.5,
collectedHost: '23107000007',
totalMovement: 4567,
todayMovement: 189,
stepCount: 12345,
updateTime: '2025-09-18 14:25:30',
isBound: false
},
{
ankleId: '2406600007',
battery: 38,
temperature: 30.1,
collectedHost: '2490246426',
totalMovement: 6789,
todayMovement: 312,
stepCount: 15678,
updateTime: '2025-09-18 14:20:45',
isBound: true
},
{
ankleId: '2502300008',
battery: 91,
temperature: 27.9,
collectedHost: '23C0270112',
totalMovement: 2345,
todayMovement: 145,
stepCount: 6789,
updateTime: '2025-09-18 14:15:20',
isBound: false
}
]
},
updateCounts() {
this.totalCount = this.devices.length
this.boundCount = this.devices.filter(device => device.isBound).length
this.unboundCount = this.devices.filter(device => !device.isBound).length
},
handleSearch() {
// 搜索逻辑已在computed中处理
},
handleAdd() {
console.log('添加新脚环设备')
},
async handleBind(device) {
try {
if (device.isBound) {
await unbindAnkle(device.ankleId)
device.isBound = false
} else {
await bindAnkle(device.ankleId, 'animal_' + device.ankleId)
device.isBound = true
}
this.updateCounts()
console.log('设备绑定状态更新成功:', device.ankleId)
} catch (error) {
console.error('设备绑定操作失败:', error)
// 可以添加用户提示
}
},
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.smart-ankle {
background-color: #ffffff;
min-height: 100vh;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.time {
font-size: 16px;
font-weight: 500;
color: #000000;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 14px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: #007aff;
color: #ffffff;
}
.back-btn {
cursor: pointer;
padding: 8px;
}
.back-arrow {
font-size: 20px;
font-weight: bold;
}
.title {
font-size: 18px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
.dots {
font-size: 20px;
font-weight: bold;
}
.circle-icon {
font-size: 16px;
width: 24px;
height: 24px;
border: 2px solid #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.search-section {
display: flex;
align-items: center;
padding: 16px 20px;
background-color: #007aff;
gap: 12px;
}
.search-bar {
flex: 1;
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 20px;
padding: 8px 16px;
gap: 8px;
}
.search-icon {
font-size: 16px;
color: #8e8e93;
}
.search-input {
flex: 1;
border: none;
outline: none;
font-size: 16px;
color: #000000;
}
.search-input::placeholder {
color: #8e8e93;
}
.add-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ffffff;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.add-icon {
font-size: 20px;
color: #007aff;
font-weight: bold;
}
.filter-tabs {
display: flex;
padding: 16px 20px;
background-color: #f8f9fa;
gap: 20px;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.tab-label {
font-size: 12px;
color: #8e8e93;
}
.tab-value {
font-size: 16px;
font-weight: 600;
color: #000000;
}
.device-list {
padding: 16px 20px;
}
.device-card {
background-color: #ffffff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.device-info {
flex: 1;
}
.device-id {
font-size: 16px;
font-weight: 600;
color: #000000;
margin-bottom: 12px;
}
.device-data {
display: flex;
flex-direction: column;
gap: 6px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.data-label {
font-size: 14px;
color: #8e8e93;
}
.data-value {
font-size: 14px;
color: #000000;
font-weight: 500;
}
.device-actions {
margin-left: 16px;
display: flex;
align-items: center;
}
.bind-btn {
padding: 8px 16px;
border-radius: 20px;
border: none;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.bind-btn.bound {
background-color: #34c759;
color: #ffffff;
}
.bind-btn.unbound {
background-color: #007aff;
color: #ffffff;
}
.bind-btn:hover {
opacity: 0.8;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: #8e8e93;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top: 3px solid #007aff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
.loading-text {
font-size: 14px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #8e8e93;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 480px) {
.device-card {
flex-direction: column;
gap: 12px;
}
.device-actions {
margin-left: 0;
align-self: flex-end;
}
.filter-tabs {
gap: 12px;
}
.tab-item {
flex: 1;
}
}
</style>

View File

@@ -1,618 +0,0 @@
<template>
<div class="sms-login-page">
<!-- 顶部状态栏 -->
<div class="status-bar">
<div class="time">14:38</div>
<div class="status-icons">
<span class="signal">📶</span>
<span class="wifi">📶</span>
<span class="battery">92%</span>
</div>
</div>
<!-- 头部导航栏 -->
<div class="header-bar">
<div class="back-btn" @click="goBack">
<span class="back-arrow"></span>
</div>
<div class="header-title">短信登录</div>
<div class="header-right"></div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 应用标题 -->
<div class="app-title">
<h1>爱农智慧牧场</h1>
</div>
<!-- 登录表单 -->
<div class="login-form">
<!-- 账号输入框 -->
<div class="input-group">
<div class="input-icon">
<span class="icon-person">👤</span>
</div>
<input
v-model="account"
type="text"
placeholder="请输入账号"
class="form-input"
:class="{ 'error': accountError }"
@input="clearAccountError"
/>
</div>
<!-- 验证码输入框 -->
<div class="input-group">
<div class="input-icon">
<span class="icon-check"></span>
</div>
<input
v-model="verificationCode"
type="text"
placeholder="请输入验证码"
class="form-input"
:class="{ 'error': codeError }"
@input="clearCodeError"
maxlength="6"
/>
<button
class="send-code-btn"
:disabled="!canSendCode || isSending"
@click="sendVerificationCode"
>
{{ sendCodeText }}
</button>
</div>
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin || isLoading"
@click="handleLogin"
>
{{ isLoading ? '登录中...' : '登录' }}
</button>
</div>
<!-- 其他登录方式 -->
<div class="alternative-login">
<div class="login-option" @click="goToPasswordLogin">
<span class="option-text">密码登录</span>
</div>
<div class="login-option" @click="goToRegister">
<span class="option-text">注册账号</span>
</div>
</div>
</div>
</div>
</template>
<script>
import auth from '@/utils/auth'
import { sendSmsCode, verifySmsCode, checkPhoneExists } from '@/services/smsService'
export default {
name: 'SmsLogin',
data() {
return {
account: '',
verificationCode: '',
accountError: false,
codeError: false,
errorMessage: '',
isLoading: false,
isSending: false,
countdown: 0,
timer: null
}
},
computed: {
canSendCode() {
return this.account.length > 0 && this.countdown === 0
},
canLogin() {
return this.account.length > 0 && this.verificationCode.length === 6
},
sendCodeText() {
if (this.isSending) {
return '发送中...'
} else if (this.countdown > 0) {
return `${this.countdown}s后重发`
} else {
return '发送验证码'
}
}
},
mounted() {
// 检查是否已经登录
if (auth.isAuthenticated()) {
this.$router.push('/')
}
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
},
methods: {
// 返回上一页
goBack() {
this.$router.go(-1)
},
// 清除账号错误
clearAccountError() {
this.accountError = false
this.errorMessage = ''
},
// 清除验证码错误
clearCodeError() {
this.codeError = false
this.errorMessage = ''
},
// 发送验证码
async sendVerificationCode() {
if (!this.account) {
this.accountError = true
this.errorMessage = '请输入账号'
return
}
// 简单的手机号验证
if (!this.validateAccount(this.account)) {
this.accountError = true
this.errorMessage = '请输入正确的手机号或账号'
return
}
this.isSending = true
this.errorMessage = ''
try {
// 检查手机号是否存在
const checkResult = await checkPhoneExists(this.account)
if (!checkResult.data.exists) {
this.errorMessage = '该手机号未注册,请先注册账号'
return
}
// 发送验证码
const result = await sendSmsCode(this.account, 'login')
if (result.success) {
// 开始倒计时
this.startCountdown()
console.log('验证码已发送到:', this.account)
this.$message && this.$message.success('验证码已发送')
} else {
this.errorMessage = result.message || '发送验证码失败,请重试'
}
} catch (error) {
console.error('发送验证码失败:', error)
this.errorMessage = '发送验证码失败,请重试'
} finally {
this.isSending = false
}
},
// 模拟发送验证码
simulateSendCode() {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 1000)
})
},
// 开始倒计时
startCountdown() {
this.countdown = 60
this.timer = setInterval(() => {
this.countdown--
if (this.countdown <= 0) {
clearInterval(this.timer)
this.timer = null
}
}, 1000)
},
// 验证账号格式
validateAccount(account) {
// 简单的手机号验证
const phoneRegex = /^1[3-9]\d{9}$/
// 简单的账号验证字母数字组合3-20位
const accountRegex = /^[a-zA-Z0-9]{3,20}$/
return phoneRegex.test(account) || accountRegex.test(account)
},
// 处理登录
async handleLogin() {
if (!this.validateForm()) {
return
}
this.isLoading = true
this.errorMessage = ''
try {
// 验证短信验证码
const verifyResult = await verifySmsCode(this.account, this.verificationCode, 'login')
if (!verifyResult.success) {
this.errorMessage = verifyResult.message || '验证码错误或已过期,请重新获取'
return
}
// 设置认证信息
await auth.setTestToken()
auth.setUserInfo({
id: 'user-' + Date.now(),
name: '爱农智慧牧场用户',
phone: this.account,
role: 'user',
loginTime: new Date().toISOString(),
loginType: 'sms'
})
// 跳转到首页
this.$router.push('/')
console.log('短信登录成功')
this.$message && this.$message.success('登录成功')
} catch (error) {
console.error('登录失败:', error)
this.errorMessage = '验证码错误或已过期,请重新获取'
} finally {
this.isLoading = false
}
},
// 模拟验证码验证
simulateVerifyCode() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟验证码验证这里简单验证是否为6位数字
if (this.verificationCode === '123456' || this.verificationCode.length === 6) {
resolve()
} else {
reject(new Error('验证码错误'))
}
}, 1000)
})
},
// 验证表单
validateForm() {
if (!this.account) {
this.accountError = true
this.errorMessage = '请输入账号'
return false
}
if (!this.validateAccount(this.account)) {
this.accountError = true
this.errorMessage = '请输入正确的手机号或账号'
return false
}
if (!this.verificationCode) {
this.codeError = true
this.errorMessage = '请输入验证码'
return false
}
if (this.verificationCode.length !== 6) {
this.codeError = true
this.errorMessage = '请输入6位验证码'
return false
}
return true
},
// 跳转到密码登录
goToPasswordLogin() {
console.log('跳转到密码登录')
// 这里可以跳转到密码登录页面
this.$router.push('/login')
},
// 跳转到注册
goToRegister() {
console.log('跳转到注册页面')
this.$router.push('/register')
}
}
}
</script>
<style scoped>
.sms-login-page {
min-height: 100vh;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background-color: #ffffff;
font-size: 14px;
color: #000000;
}
.time {
font-weight: 500;
}
.status-icons {
display: flex;
gap: 8px;
font-size: 12px;
}
/* 头部导航栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.back-btn {
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
}
.back-arrow {
font-size: 20px;
font-weight: bold;
color: #333;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-right {
width: 36px; /* 保持布局平衡 */
}
/* 主要内容区域 */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 60px 20px 40px;
min-height: calc(100vh - 200px);
}
/* 应用标题 */
.app-title {
margin-bottom: 60px;
text-align: center;
}
.app-title h1 {
font-size: 28px;
font-weight: bold;
color: #000000;
margin: 0;
letter-spacing: 1px;
}
/* 登录表单 */
.login-form {
width: 100%;
max-width: 320px;
margin-bottom: 40px;
}
/* 输入框组 */
.input-group {
position: relative;
margin-bottom: 20px;
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.input-group:focus-within {
border-color: #52c41a;
box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.1);
}
.input-icon {
padding: 0 16px;
display: flex;
align-items: center;
color: #999;
font-size: 16px;
}
.icon-person,
.icon-check {
font-size: 16px;
}
.form-input {
flex: 1;
padding: 16px 0;
border: none;
background: transparent;
font-size: 16px;
color: #333;
outline: none;
}
.form-input::placeholder {
color: #999;
}
.form-input.error {
color: #ff4d4f;
}
/* 发送验证码按钮 */
.send-code-btn {
padding: 8px 16px;
margin-right: 8px;
background-color: transparent;
border: 1px solid #1890ff;
border-radius: 4px;
color: #1890ff;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.send-code-btn:hover:not(:disabled) {
background-color: #1890ff;
color: white;
}
.send-code-btn:disabled {
border-color: #d9d9d9;
color: #999;
cursor: not-allowed;
}
/* 错误提示 */
.error-message {
color: #ff4d4f;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 50px;
background-color: #52c41a;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.login-btn:hover:not(:disabled) {
background-color: #45a018;
transform: translateY(-1px);
}
.login-btn:disabled {
background-color: #a0d468;
cursor: not-allowed;
}
/* 其他登录方式 */
.alternative-login {
display: flex;
gap: 24px;
}
.login-option {
cursor: pointer;
transition: all 0.2s;
}
.login-option:hover {
opacity: 0.7;
}
.option-text {
font-size: 14px;
color: #666;
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.main-content {
padding: 40px 16px 20px;
}
.app-title h1 {
font-size: 24px;
}
.login-form {
max-width: 100%;
}
.input-group {
margin-bottom: 16px;
}
.form-input {
padding: 14px 0;
font-size: 15px;
}
.send-code-btn {
padding: 6px 12px;
font-size: 13px;
}
.login-btn {
height: 45px;
font-size: 16px;
}
}
/* 小屏幕优化 */
@media (max-width: 375px) {
.app-title h1 {
font-size: 22px;
}
.alternative-login {
gap: 16px;
}
.option-text {
font-size: 13px;
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.main-content {
padding: 20px;
min-height: auto;
}
.app-title {
margin-bottom: 30px;
}
.login-form {
margin-bottom: 20px;
}
}
</style>

View File

@@ -1,24 +0,0 @@
import Vue from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import VueCompositionAPI from '@vue/composition-api'
import router from './router'
// 引入全局样式
import './app.scss'
// 安装composition-api插件
Vue.use(VueCompositionAPI)
// 创建应用实例
const app = new Vue({
pinia: createPinia(),
router,
render: h => h(App)
})
// 挂载应用
app.$mount('#app')
// 导出应用实例
export default app

View File

@@ -1,414 +0,0 @@
<template>
<view class="home-container">
<!-- 顶部统计卡片 -->
<view class="stats-grid">
<view class="stat-card" @click="navigateTo('/pages/cattle/list')">
<view class="stat-icon">🐄</view>
<view class="stat-content">
<view class="stat-number">{{ stats.totalCattle || 0 }}</view>
<view class="stat-label">总牛只数</view>
</view>
</view>
<view class="stat-card" @click="navigateTo('/pages/cattle/list?status=pregnant')">
<view class="stat-icon pregnant">🤰</view>
<view class="stat-content">
<view class="stat-number">{{ stats.pregnantCattle || 0 }}</view>
<view class="stat-label">怀孕牛只</view>
</view>
</view>
<view class="stat-card" @click="navigateTo('/pages/cattle/list?status=sick')">
<view class="stat-icon sick">🤒</view>
<view class="stat-content">
<view class="stat-number">{{ stats.sickCattle || 0 }}</view>
<view class="stat-label">生病牛只</view>
</view>
</view>
<view class="stat-card" @click="navigateTo('/pages/farm/list')">
<view class="stat-icon farm">🏡</view>
<view class="stat-content">
<view class="stat-number">{{ stats.totalFarms || 0 }}</view>
<view class="stat-label">养殖场数</view>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="section-title">
<text>快捷操作</text>
</view>
<view class="actions-grid">
<view class="action-item" @click="navigateTo('/pages/cattle/add')">
<view class="action-icon add"></view>
<text class="action-text">添加牛只</text>
</view>
<view class="action-item" @click="navigateTo('/pages/breed/record')">
<view class="action-icon breed">👶</view>
<text class="action-text">配种记录</text>
</view>
<view class="action-item" @click="navigateTo('/pages/medical/record')">
<view class="action-icon medical">💊</view>
<text class="action-text">医疗记录</text>
</view>
<view class="action-item" @click="navigateTo('/pages/feed/record')">
<view class="action-icon feed">🌾</view>
<text class="action-text">饲喂记录</text>
</view>
</view>
</view>
<!-- 最近活动 -->
<view class="recent-activities">
<view class="section-title">
<text>最近活动</text>
<text class="view-all" @click="navigateTo('/pages/activity/list')">查看全部</text>
</view>
<view class="activity-list">
<view
v-for="(activity, index) in recentActivities"
:key="index"
class="activity-item"
>
<view class="activity-icon">
<text>{{ getActivityIcon(activity.type) }}</text>
</view>
<view class="activity-content">
<view class="activity-title">{{ activity.title }}</view>
<view class="activity-desc">{{ activity.description }}</view>
<view class="activity-time">{{ formatTime(activity.time) }}</view>
</view>
</view>
<view v-if="recentActivities.length === 0" class="empty-state">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无活动记录</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getHomeStats, getRecentActivities } from '@/services/homeService'
const stats = ref({})
const recentActivities = ref([])
const loading = ref(false)
// 获取首页数据
const fetchHomeData = async () => {
loading.value = true
try {
const [statsData, activitiesData] = await Promise.all([
getHomeStats(),
getRecentActivities()
])
stats.value = statsData
recentActivities.value = activitiesData
} catch (error) {
console.error('获取首页数据失败:', error)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 页面显示时刷新数据
onShow(() => {
fetchHomeData()
})
// 页面加载时获取数据
onMounted(() => {
fetchHomeData()
})
// 导航到指定页面
const navigateTo = (url) => {
uni.navigateTo({ url })
}
// 获取活动图标
const getActivityIcon = (type) => {
const icons = {
'add_cattle': '🐄',
'breed': '👶',
'medical': '💊',
'feed': '🌾',
'vaccine': '💉',
'birth': '🎉',
'move': '🚚',
'sell': '💰'
}
return icons[type] || '📋'
}
// 格式化时间
const formatTime = (time) => {
if (!time) return ''
const now = new Date()
const targetTime = new Date(time)
const diff = now - targetTime
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return targetTime.toLocaleDateString()
}
</script>
<style lang="scss" scoped>
.home-container {
padding: 16rpx;
background-color: $bg-color-page;
min-height: 100vh;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
margin-bottom: 32rpx;
}
.stat-card {
background: linear-gradient(135deg, $color-primary-light, $color-primary);
border-radius: $border-radius-lg;
padding: 24rpx;
color: white;
display: flex;
align-items: center;
box-shadow: $box-shadow-light;
&:active {
opacity: 0.9;
transform: scale(0.98);
}
&.pregnant {
background: linear-gradient(135deg, $color-warning-light, $color-warning);
}
&.sick {
background: linear-gradient(135deg, $color-danger-light, $color-danger);
}
&.farm {
background: linear-gradient(135deg, $color-success-light, $color-success);
}
}
.stat-icon {
font-size: 48rpx;
margin-right: 16rpx;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
line-height: 1.2;
}
.stat-label {
font-size: 24rpx;
opacity: 0.9;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
font-size: 32rpx;
font-weight: bold;
color: $color-text-primary;
.view-all {
font-size: 24rpx;
color: $color-primary;
font-weight: normal;
}
}
.quick-actions {
margin-bottom: 32rpx;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.action-item {
text-align: center;
padding: 24rpx 16rpx;
background-color: $bg-color-card;
border-radius: $border-radius-base;
box-shadow: $box-shadow-light;
&:active {
background-color: darken($bg-color-card, 5%);
}
}
.action-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
&.add { color: $color-primary; }
&.breed { color: $color-warning; }
&.medical { color: $color-danger; }
&.feed { color: $color-success; }
}
.action-text {
font-size: 24rpx;
color: $color-text-regular;
}
.recent-activities {
margin-bottom: 32rpx;
}
.activity-list {
background-color: $bg-color-card;
border-radius: $border-radius-base;
overflow: hidden;
box-shadow: $box-shadow-light;
}
.activity-item {
display: flex;
align-items: center;
padding: 24rpx;
border-bottom: 1px solid $border-color-light;
&:last-child {
border-bottom: none;
}
&:active {
background-color: darken($bg-color-card, 5%);
}
}
.activity-icon {
font-size: 48rpx;
margin-right: 24rpx;
flex-shrink: 0;
}
.activity-content {
flex: 1;
}
.activity-title {
font-size: 28rpx;
font-weight: 500;
color: $color-text-primary;
margin-bottom: 4rpx;
}
.activity-desc {
font-size: 24rpx;
color: $color-text-secondary;
margin-bottom: 8rpx;
}
.activity-time {
font-size: 20rpx;
color: $color-text-placeholder;
}
.empty-state {
text-align: center;
padding: 64rpx 32rpx;
color: $color-text-secondary;
}
.empty-icon {
font-size: 64rpx;
display: block;
margin-bottom: 16rpx;
}
.empty-text {
font-size: 28rpx;
}
.loading-container {
text-align: center;
padding: 64rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid $border-color-light;
border-top: 4rpx solid $color-primary;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16rpx;
}
.loading-text {
font-size: 28rpx;
color: $color-text-secondary;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.actions-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12rpx;
}
.action-item {
padding: 16rpx 12rpx;
}
.action-icon {
font-size: 40rpx;
}
.action-text {
font-size: 22rpx;
}
}
</style>

View File

@@ -1,549 +0,0 @@
<template>
<view class="login-container">
<!-- 顶部logo和标题 -->
<view class="login-header">
<view class="logo">
<text class="logo-icon">🐄</text>
<text class="logo-text">智慧养殖</text>
</view>
<text class="welcome-text">欢迎使用养殖管理系统</text>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- 用户名输入 -->
<view class="form-group">
<view class="input-container">
<text class="input-icon">👤</text>
<input
v-model="form.username"
class="form-input"
type="text"
placeholder="请输入用户名"
:disabled="loading"
@focus="clearError"
/>
</view>
</view>
<!-- 密码输入 -->
<view class="form-group">
<view class="input-container">
<text class="input-icon">🔒</text>
<input
v-model="form.password"
class="form-input"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
:disabled="loading"
@focus="clearError"
/>
<text class="password-toggle" @click="togglePasswordVisibility">
{{ showPassword ? '🙈' : '👁' }}
</text>
</view>
</view>
<!-- 记住我 -->
<view class="remember-me">
<label class="checkbox-container">
<checkbox
:checked="rememberMe"
@change="rememberMe = !rememberMe"
:disabled="loading"
/>
<text class="checkbox-label">记住密码</text>
</label>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text class="error-icon"></text>
<text class="error-text">{{ errorMessage }}</text>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
:class="{ loading: loading }"
:disabled="loading || !form.username || !form.password"
@click="handleLogin"
>
<text v-if="!loading">登录</text>
<text v-else class="loading-text">
<text class="loading-spinner"></text>
登录中...
</text>
</button>
<!-- 微信登录 -->
<view class="wx-login-section">
<view class="divider">
<text class="divider-text">或使用以下方式登录</text>
</view>
<button
class="wx-login-btn"
:disabled="loading"
@click="handleWxLogin"
>
<text class="wx-icon">💬</text>
<text>微信一键登录</text>
</button>
</view>
</view>
<!-- 底部链接 -->
<view class="login-footer">
<text class="footer-link" @click="navigateTo('/pages/register/register')">注册账号</text>
<text class="footer-separator">|</text>
<text class="footer-link" @click="navigateTo('/pages/forgot-password/forgot-password')">忘记密码</text>
</view>
<!-- 版本信息 -->
<view class="version-info">
<text>版本号: {{ appVersion }}</text>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { login, wxLogin } from '@/services/authService'
import { setToken, setUserInfo, setTokenTime } from '@/utils/auth'
const form = reactive({
username: '',
password: ''
})
const showPassword = ref(false)
const rememberMe = ref(false)
const loading = ref(false)
const errorMessage = ref('')
const appVersion = ref(process.env.VUE_APP_VERSION || '1.0.0')
// 页面加载时检查记住的密码
onMounted(() => {
const savedUsername = uni.getStorageSync('savedUsername')
const savedPassword = uni.getStorageSync('savedPassword')
if (savedUsername && savedPassword) {
form.username = savedUsername
form.password = savedPassword
rememberMe.value = true
}
})
// 切换密码可见性
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value
}
// 清除错误信息
const clearError = () => {
errorMessage.value = ''
}
// 处理表单登录
const handleLogin = async () => {
if (!form.username.trim()) {
errorMessage.value = '请输入用户名'
return
}
if (!form.password.trim()) {
errorMessage.value = '请输入密码'
return
}
loading.value = true
errorMessage.value = ''
try {
const result = await login(form.username, form.password)
if (result && result.token) {
// 保存token和用户信息
setToken(result.token)
setUserInfo(result.userInfo)
setTokenTime()
// 记住密码功能
if (rememberMe.value) {
uni.setStorageSync('savedUsername', form.username)
uni.setStorageSync('savedPassword', form.password)
} else {
uni.removeStorageSync('savedUsername')
uni.removeStorageSync('savedPassword')
}
// 登录成功提示
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
} else {
errorMessage.value = '登录失败,请稍后重试'
}
} catch (error) {
console.error('登录失败:', error)
if (error.response && error.response.status === 401) {
errorMessage.value = '用户名或密码错误'
} else if (error.code === 'NETWORK_ERROR') {
errorMessage.value = '网络连接失败,请检查网络设置'
} else {
errorMessage.value = error.message || '登录失败,请稍后重试'
}
} finally {
loading.value = false
}
}
// 处理微信登录
const handleWxLogin = async () => {
loading.value = true
errorMessage.value = ''
try {
const result = await wxLogin()
if (result && result.token) {
// 保存token和用户信息
setToken(result.token)
setUserInfo(result.userInfo)
setTokenTime()
// 登录成功提示
uni.showToast({
title: '微信登录成功',
icon: 'success',
duration: 1500
})
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
} else {
errorMessage.value = '微信登录失败,请稍后重试'
}
} catch (error) {
console.error('微信登录失败:', error)
if (error.code === 'WX_LOGIN_FAILED') {
errorMessage.value = '微信登录失败,请重试'
} else if (error.code === 'WX_AUTH_DENIED') {
errorMessage.value = '您拒绝了授权,无法使用微信登录'
} else {
errorMessage.value = error.message || '微信登录失败,请稍后重试'
}
} finally {
loading.value = false
}
}
// 导航到其他页面
const navigateTo = (url) => {
uni.navigateTo({ url })
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, $color-primary-light, $color-primary);
padding: 64rpx 48rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.login-header {
text-align: center;
margin-bottom: 64rpx;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.logo-icon {
font-size: 64rpx;
margin-right: 16rpx;
}
.logo-text {
font-size: 48rpx;
font-weight: bold;
color: white;
}
.welcome-text {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
}
.login-form {
background-color: white;
border-radius: $border-radius-lg;
padding: 48rpx;
box-shadow: $box-shadow-base;
}
.form-group {
margin-bottom: 32rpx;
}
.input-container {
position: relative;
display: flex;
align-items: center;
border: 1px solid $border-color-base;
border-radius: $border-radius-base;
padding: 0 24rpx;
background-color: $bg-color;
&:focus-within {
border-color: $color-primary;
box-shadow: 0 0 0 2px rgba($color-primary, 0.2);
}
&.error {
border-color: $color-danger;
}
}
.input-icon {
font-size: 32rpx;
margin-right: 16rpx;
color: $color-text-secondary;
}
.form-input {
flex: 1;
height: 96rpx;
font-size: 28rpx;
color: $color-text-primary;
&::placeholder {
color: $color-text-placeholder;
}
&:disabled {
opacity: $opacity-disabled;
}
}
.password-toggle {
font-size: 32rpx;
color: $color-text-secondary;
cursor: pointer;
padding: 8rpx;
&:active {
opacity: 0.7;
}
}
.remember-me {
margin-bottom: 32rpx;
}
.checkbox-container {
display: flex;
align-items: center;
cursor: pointer;
}
.checkbox-label {
margin-left: 8rpx;
font-size: 24rpx;
color: $color-text-secondary;
}
.error-message {
display: flex;
align-items: center;
background-color: rgba($color-danger, 0.1);
border: 1px solid rgba($color-danger, 0.2);
border-radius: $border-radius-base;
padding: 16rpx;
margin-bottom: 32rpx;
color: $color-danger;
}
.error-icon {
margin-right: 8rpx;
font-size: 28rpx;
}
.error-text {
font-size: 24rpx;
}
.login-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, $color-primary, $color-primary-dark);
border: none;
border-radius: $border-radius-base;
color: white;
font-size: 32rpx;
font-weight: 500;
&:active:not(:disabled) {
opacity: 0.9;
transform: scale(0.98);
}
&:disabled {
opacity: $opacity-disabled;
background: $color-disabled;
}
&.loading {
opacity: 0.8;
}
}
.loading-text {
display: flex;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8rpx;
}
.wx-login-section {
margin-top: 48rpx;
}
.divider {
position: relative;
text-align: center;
margin-bottom: 32rpx;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background-color: $border-color-light;
}
}
.divider-text {
position: relative;
background-color: white;
padding: 0 16rpx;
font-size: 24rpx;
color: $color-text-secondary;
}
.wx-login-btn {
width: 100%;
height: 80rpx;
background-color: #07C160;
border: none;
border-radius: $border-radius-base;
color: white;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
&:active:not(:disabled) {
opacity: 0.9;
}
&:disabled {
opacity: $opacity-disabled;
}
}
.wx-icon {
font-size: 32rpx;
margin-right: 8rpx;
}
.login-footer {
text-align: center;
margin-top: 48rpx;
}
.footer-link {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
&:active {
color: white;
}
}
.footer-separator {
margin: 0 16rpx;
color: rgba(255, 255, 255, 0.5);
}
.version-info {
text-align: center;
margin-top: 32rpx;
font-size: 20rpx;
color: rgba(255, 255, 255, 0.6);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.login-container {
padding: 48rpx 32rpx;
}
.login-form {
padding: 32rpx;
}
.logo-icon {
font-size: 56rpx;
}
.logo-text {
font-size: 40rpx;
}
.welcome-text {
font-size: 28rpx;
}
}
</style>

View File

@@ -1,223 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/components/Home.vue'
import Production from '@/components/Production.vue'
import Profile from '@/components/Profile.vue'
import EarTag from '@/components/EarTag.vue'
import SmartCollar from '@/components/SmartCollar.vue'
import SmartAnkle from '@/components/SmartAnkle.vue'
import SmartHost from '@/components/SmartHost.vue'
import AuthTest from '@/components/AuthTest.vue'
import Login from '@/components/Login.vue'
import SmsLogin from '@/components/SmsLogin.vue'
import Register from '@/components/Register.vue'
import PasswordLogin from '@/components/PasswordLogin.vue'
import ElectronicFencePage from '@/views/ElectronicFencePage.vue'
import SmartEartagAlertPage from '@/views/SmartEartagAlertPage.vue'
import AlertTest from '@/components/AlertTest.vue'
import MapTest from '@/components/MapTest.vue'
import ApiTest from '@/components/ApiTest.vue'
import WechatFenceDrawer from '@/components/WechatFenceDrawer.vue'
import CattleProfile from '@/components/CattleProfile.vue'
import CattleAdd from '@/components/CattleAdd.vue'
import CattleTest from '@/components/CattleTest.vue'
import CattleTransfer from '@/components/CattleTransfer.vue'
import CattleTransferRegister from '@/components/CattleTransferRegister.vue'
import CattleExit from '@/components/CattleExit.vue'
import CattlePen from '@/components/CattlePen.vue'
import CattleBatch from '@/components/CattleBatch.vue'
import SmartCollarAlert from '@/components/SmartCollarAlert.vue'
import ApiTestPage from '@/components/ApiTestPage.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/sms-login',
name: 'SmsLogin',
component: SmsLogin
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/password-login',
name: 'PasswordLogin',
component: PasswordLogin
},
{
path: '/production',
name: 'Production',
component: Production
},
{
path: '/profile',
name: 'Profile',
component: Profile
},
{
path: '/ear-tag',
name: 'EarTag',
component: EarTag
},
{
path: '/smart-collar',
name: 'SmartCollar',
component: SmartCollar
},
{
path: '/smart-ankle',
name: 'SmartAnkle',
component: SmartAnkle
},
{
path: '/smart-host',
name: 'SmartHost',
component: SmartHost
},
{
path: '/auth-test',
name: 'AuthTest',
component: AuthTest
},
{
path: '/electronic-fence',
name: 'ElectronicFence',
component: ElectronicFencePage
},
{
path: '/smart-eartag-alert',
name: 'SmartEartagAlert',
component: SmartEartagAlertPage
},
{
path: '/alert-test',
name: 'AlertTest',
component: AlertTest
},
{
path: '/map-test',
name: 'MapTest',
component: MapTest
},
{
path: '/api-test',
name: 'ApiTest',
component: ApiTest
},
{
path: '/wechat-fence-drawer',
name: 'WechatFenceDrawer',
component: WechatFenceDrawer
},
{
path: '/cattle-profile',
name: 'CattleProfile',
component: CattleProfile
},
{
path: '/cattle-add',
name: 'CattleAdd',
component: CattleAdd
},
{
path: '/cattle-test',
name: 'CattleTest',
component: CattleTest
},
{
path: '/cattle-transfer',
name: 'CattleTransfer',
component: CattleTransfer
},
{
path: '/cattle-transfer-register',
name: 'CattleTransferRegister',
component: CattleTransferRegister
},
{
path: '/cattle-exit',
name: 'CattleExit',
component: CattleExit
},
{
path: '/cattle-pen',
name: 'CattlePen',
component: CattlePen
},
{
path: '/cattle-batch',
name: 'CattleBatch',
component: CattleBatch
},
{
path: '/smart-collar-alert',
name: 'SmartCollarAlert',
component: SmartCollarAlert
},
{
path: '/api-test-page',
name: 'ApiTestPage',
component: ApiTestPage
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 导入认证工具
const auth = require('@/utils/auth').default
// 检查是否需要登录
const requiresAuth = !['/login', '/sms-login', '/register', '/password-login'].includes(to.path)
const isLoginPage = ['/login', '/sms-login', '/register', '/password-login'].includes(to.path)
const isAuthenticated = auth.isAuthenticated()
console.log('路由守卫:', {
from: from.path,
to: to.path,
requiresAuth,
isLoginPage,
isAuthenticated
})
// 如果是从登录页面跳转到首页,且用户已登录,直接允许访问
if (from.path && isLoginPage && to.path === '/' && isAuthenticated) {
console.log('允许从登录页跳转到首页')
next()
return
}
if (requiresAuth && !isAuthenticated) {
// 需要登录但未登录,跳转到登录页
console.log('需要登录,跳转到登录页')
next('/login')
} else if (isLoginPage && isAuthenticated) {
// 已登录但访问登录页,跳转到首页
console.log('已登录,跳转到首页')
next('/')
} else {
// 正常访问
console.log('正常访问')
next()
}
})
export default router

View File

@@ -1,127 +0,0 @@
import api from './api'
// 智能耳标预警相关API服务
export const alertService = {
// 获取预警列表
async getAlerts(params = {}) {
try {
const response = await api.get('/smart-eartag-alerts', { params })
return response
} catch (error) {
console.error('获取预警列表失败:', error)
throw error
}
},
// 获取预警详情
async getAlertById(id) {
try {
const response = await api.get(`/smart-eartag-alerts/${id}`)
return response
} catch (error) {
console.error('获取预警详情失败:', error)
throw error
}
},
// 处理预警(标记为已处理)
async resolveAlert(id) {
try {
const response = await api.put(`/smart-eartag-alerts/${id}/resolve`)
return response
} catch (error) {
console.error('处理预警失败:', error)
throw error
}
},
// 批量处理预警
async batchResolveAlerts(ids) {
try {
const response = await api.put('/smart-eartag-alerts/batch-resolve', { ids })
return response
} catch (error) {
console.error('批量处理预警失败:', error)
throw error
}
},
// 删除预警
async deleteAlert(id) {
try {
const response = await api.delete(`/smart-eartag-alerts/${id}`)
return response
} catch (error) {
console.error('删除预警失败:', error)
throw error
}
},
// 获取预警统计
async getAlertStats() {
try {
const response = await api.get('/smart-eartag-alerts/stats')
return response
} catch (error) {
console.error('获取预警统计失败:', error)
throw error
}
},
// 获取设备预警历史
async getDeviceAlertHistory(deviceId, params = {}) {
try {
const response = await api.get(`/smart-eartag-alerts/device/${deviceId}`, { params })
return response
} catch (error) {
console.error('获取设备预警历史失败:', error)
throw error
}
},
// 设置预警规则
async setAlertRule(ruleData) {
try {
const response = await api.post('/smart-eartag-alerts/rules', ruleData)
return response
} catch (error) {
console.error('设置预警规则失败:', error)
throw error
}
},
// 获取预警规则
async getAlertRules() {
try {
const response = await api.get('/smart-eartag-alerts/rules')
return response
} catch (error) {
console.error('获取预警规则失败:', error)
throw error
}
},
// 更新预警规则
async updateAlertRule(id, ruleData) {
try {
const response = await api.put(`/smart-eartag-alerts/rules/${id}`, ruleData)
return response
} catch (error) {
console.error('更新预警规则失败:', error)
throw error
}
},
// 删除预警规则
async deleteAlertRule(id) {
try {
const response = await api.delete(`/smart-eartag-alerts/rules/${id}`)
return response
} catch (error) {
console.error('删除预警规则失败:', error)
throw error
}
}
}
export default alertService

Some files were not shown because too many files have changed in this diff Show More