优化两个小程序
This commit is contained in:
241
mini_program/farm-monitor-dashboard/API_INTEGRATION.md
Normal file
241
mini_program/farm-monitor-dashboard/API_INTEGRATION.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 智能设备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/*`
|
||||
- 添加完整的错误处理和模拟数据支持
|
||||
- 实现设备绑定/解绑、状态监控等功能
|
||||
123
mini_program/farm-monitor-dashboard/API_TEST_GUIDE.md
Normal file
123
mini_program/farm-monitor-dashboard/API_TEST_GUIDE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 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可正常调用
|
||||
88
mini_program/farm-monitor-dashboard/AUTH_FIX_README.md
Normal file
88
mini_program/farm-monitor-dashboard/AUTH_FIX_README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
- 所有设备操作在模拟模式下都是本地状态更新
|
||||
- 刷新页面后状态会重置为初始值
|
||||
127
mini_program/farm-monitor-dashboard/COLLAR_API_INTEGRATION.md
Normal file
127
mini_program/farm-monitor-dashboard/COLLAR_API_INTEGRATION.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 智能项圈模块 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,不再依赖模拟数据。
|
||||
@@ -0,0 +1,184 @@
|
||||
# 智能项圈分页和搜索功能完善报告
|
||||
|
||||
## 功能概述
|
||||
智能项圈模块已完成分页展示和搜索功能的全面优化,支持所有数据的正确分页显示和根据项圈编号的精确查询。
|
||||
|
||||
## 主要功能特性
|
||||
|
||||
### 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调用和状态管理,确保良好的性能表现
|
||||
|
||||
所有功能已通过测试验证,可以正常使用。
|
||||
187
mini_program/farm-monitor-dashboard/EARTAG_CRUD_FEATURES.md
Normal file
187
mini_program/farm-monitor-dashboard/EARTAG_CRUD_FEATURES.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# 智能耳标功能模块完善
|
||||
|
||||
## 功能概述
|
||||
|
||||
根据提供的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路由
|
||||
168
mini_program/farm-monitor-dashboard/FIELD_MAPPING_GUIDE.md
Normal file
168
mini_program/farm-monitor-dashboard/FIELD_MAPPING_GUIDE.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 智能耳标字段映射指南
|
||||
|
||||
## 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": "获取智能耳标设备列表成功"
|
||||
}
|
||||
```
|
||||
112
mini_program/farm-monitor-dashboard/LOGIN_PAGE_README.md
Normal file
112
mini_program/farm-monitor-dashboard/LOGIN_PAGE_README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 登录页面使用说明
|
||||
|
||||
## 页面地址
|
||||
- **登录页面**: 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. 在"我的"页面点击"退出登录"(应该跳转到登录页)
|
||||
121
mini_program/farm-monitor-dashboard/NETWORK_ERROR_FIX.md
Normal file
121
mini_program/farm-monitor-dashboard/NETWORK_ERROR_FIX.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 网络连接错误修复说明
|
||||
|
||||
## 问题描述
|
||||
出现错误:`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. 检查防火墙设置是否阻止了端口访问
|
||||
@@ -0,0 +1,123 @@
|
||||
# 分页字段映射修复报告
|
||||
|
||||
## 问题描述
|
||||
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数据完全同步。
|
||||
175
mini_program/farm-monitor-dashboard/PASSWORD_LOGIN_README.md
Normal file
175
mini_program/farm-monitor-dashboard/PASSWORD_LOGIN_README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 密码登录页面使用说明
|
||||
|
||||
## 页面地址
|
||||
- **密码登录页面**: 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. **协议阅读**: 仔细阅读用户协议和隐私政策
|
||||
162
mini_program/farm-monitor-dashboard/REGISTER_PAGE_README.md
Normal file
162
mini_program/farm-monitor-dashboard/REGISTER_PAGE_README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# 注册账号页面使用说明
|
||||
|
||||
## 页面地址
|
||||
- **注册页面**: 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等)
|
||||
- 密码强度检测
|
||||
- 图形验证码
|
||||
|
||||
## 样式特点
|
||||
|
||||
- **移动优先**: 响应式设计,适配各种屏幕
|
||||
- **交互反馈**: 按钮状态、输入框焦点效果
|
||||
- **视觉层次**: 清晰的信息层级
|
||||
- **品牌一致**: 与整体应用风格保持一致
|
||||
- **无障碍**: 支持键盘导航和屏幕阅读器
|
||||
|
||||
## 页面跳转
|
||||
|
||||
- **从登录页**: 点击"注册账号"跳转到注册页
|
||||
- **从短信登录页**: 点击"注册账号"跳转到注册页
|
||||
- **返回上一页**: 点击左上角返回按钮
|
||||
- **已有账号**: 点击"已有账号?立即登录"跳转到登录页
|
||||
107
mini_program/farm-monitor-dashboard/ROUTER_FIX_README.md
Normal file
107
mini_program/farm-monitor-dashboard/ROUTER_FIX_README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 路由守卫修复说明
|
||||
|
||||
## 问题描述
|
||||
出现错误:`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. 查看路由守卫的详细日志
|
||||
175
mini_program/farm-monitor-dashboard/SEARCH_FUNCTIONALITY.md
Normal file
175
mini_program/farm-monitor-dashboard/SEARCH_FUNCTIONALITY.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 智能耳标搜索功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
智能耳标搜索功能提供了精确和模糊两种搜索模式,支持根据耳标编号进行快速查找。
|
||||
|
||||
## 搜索特性
|
||||
|
||||
### 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. **搜索统计**: 显示搜索结果统计信息
|
||||
139
mini_program/farm-monitor-dashboard/SMS_LOGIN_README.md
Normal file
139
mini_program/farm-monitor-dashboard/SMS_LOGIN_README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 短信登录页面使用说明
|
||||
|
||||
## 页面地址
|
||||
- **短信登录页面**: 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. **错误处理**: 所有错误都有相应的用户提示
|
||||
|
||||
## 扩展功能
|
||||
|
||||
可以添加的功能:
|
||||
- 图形验证码
|
||||
- 语音验证码
|
||||
- 国际手机号支持
|
||||
- 记住手机号
|
||||
- 自动填充验证码
|
||||
- 生物识别登录
|
||||
|
||||
## 样式特点
|
||||
|
||||
- **移动优先**: 响应式设计,适配各种屏幕
|
||||
- **交互反馈**: 按钮状态、输入框焦点效果
|
||||
- **视觉层次**: 清晰的信息层级
|
||||
- **品牌一致**: 与整体应用风格保持一致
|
||||
60
mini_program/farm-monitor-dashboard/SYNTAX_ERROR_FIX.md
Normal file
60
mini_program/farm-monitor-dashboard/SYNTAX_ERROR_FIX.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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 - 模板编译器版本
|
||||
141
mini_program/farm-monitor-dashboard/TOKEN_ERROR_FIX.md
Normal file
141
mini_program/farm-monitor-dashboard/TOKEN_ERROR_FIX.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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. 优化用户体验
|
||||
6190
mini_program/farm-monitor-dashboard/package-lock.json
generated
6190
mini_program/farm-monitor-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,24 +10,30 @@
|
||||
"build:h5": "vue-cli-service build --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vant/weapp": "^1.11.6",
|
||||
"@dcloudio/uni-app": "^2.0.2-alpha-4080120250905001",
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.11.0",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^3.3.4"
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/uni-cli-shared": "^3.0.0-alpha-4070620250731002",
|
||||
"@dcloudio/uni-h5": "^3.0.0-alpha-4070620250731002",
|
||||
"@dcloudio/uni-mp-weixin": "^3.0.0-alpha-4070620250731002",
|
||||
"@dcloudio/uni-cli-shared": "^2.0.2-alpha-4080120250905001",
|
||||
"@dcloudio/uni-h5": "^2.0.2-alpha-4080120250905001",
|
||||
"@dcloudio/uni-mp-weixin": "^2.0.2-alpha-4080120250905001",
|
||||
"@vant/weapp": "^1.11.7",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-vue": "^9.15.0",
|
||||
"sass": "^1.92.1",
|
||||
"sass-loader": "^16.0.5",
|
||||
"typescript": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.20.2",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
mini_program/farm-monitor-dashboard/public/favicon.svg
Normal file
4
mini_program/farm-monitor-dashboard/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 203 B |
18
mini_program/farm-monitor-dashboard/public/index.html
Normal file
18
mini_program/farm-monitor-dashboard/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!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>
|
||||
@@ -1,75 +1,52 @@
|
||||
<template>
|
||||
<view>
|
||||
<div id="app">
|
||||
<!-- 应用内容 -->
|
||||
<slot />
|
||||
</view>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
|
||||
import { useUserStore } from './store/user'
|
||||
<script>
|
||||
import auth from './utils/auth'
|
||||
|
||||
// 创建Pinia实例
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 应用初始化
|
||||
const initApp = async () => {
|
||||
try {
|
||||
// 检查登录状态
|
||||
const token = uni.getStorageSync('token')
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
userStore.token = token
|
||||
userStore.userInfo = userInfo
|
||||
userStore.isLoggedIn = true
|
||||
|
||||
// 检查是否需要重新获取用户信息
|
||||
try {
|
||||
// 这里可以调用API验证token有效性
|
||||
console.log('用户已登录,token:', token)
|
||||
} catch (error) {
|
||||
console.error('token验证失败:', error)
|
||||
userStore.logout()
|
||||
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)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动
|
||||
onLaunch(() => {
|
||||
console.log('App Launch')
|
||||
initApp()
|
||||
})
|
||||
|
||||
// 应用显示
|
||||
onShow(() => {
|
||||
console.log('App Show')
|
||||
})
|
||||
|
||||
// 应用隐藏
|
||||
onHide(() => {
|
||||
console.log('App Hide')
|
||||
})
|
||||
|
||||
// 全局数据,可以通过 getApp().globalData 获取
|
||||
const globalData = {
|
||||
version: '1.0.0',
|
||||
platform: uni.getSystemInfoSync().platform,
|
||||
isDevelopment: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
|
||||
// 导出全局数据
|
||||
export default {
|
||||
globalData
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
@import '@/uni.scss';
|
||||
|
||||
page {
|
||||
background-color: #f6f6f6;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// 引入Vant样式
|
||||
@import '~@vant/weapp/dist/common/index.wxss';
|
||||
|
||||
// 引入uni.scss变量
|
||||
@import '@/uni.scss';
|
||||
@import '../uni.scss';
|
||||
|
||||
/* 全局样式重置 */
|
||||
page {
|
||||
|
||||
388
mini_program/farm-monitor-dashboard/src/components/AuthTest.vue
Normal file
388
mini_program/farm-monitor-dashboard/src/components/AuthTest.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<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>
|
||||
1415
mini_program/farm-monitor-dashboard/src/components/EarTag.vue
Normal file
1415
mini_program/farm-monitor-dashboard/src/components/EarTag.vue
Normal file
File diff suppressed because it is too large
Load Diff
594
mini_program/farm-monitor-dashboard/src/components/Home.vue
Normal file
594
mini_program/farm-monitor-dashboard/src/components/Home.vue
Normal file
@@ -0,0 +1,594 @@
|
||||
<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>
|
||||
</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: 'Home',
|
||||
data() {
|
||||
return {
|
||||
activeAlertTab: 'collar',
|
||||
activeNav: 'home',
|
||||
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: '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' }
|
||||
],
|
||||
bottomNavItems: [
|
||||
{ key: 'home', icon: '🏠', label: '首页', color: '#34c759' },
|
||||
{ key: 'production', icon: '📦', label: '生产管理', color: '#8e8e93' },
|
||||
{ key: 'profile', icon: '👤', label: '我的', color: '#8e8e93' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentAlerts() {
|
||||
const alertData = {
|
||||
collar: [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: '6', urgent: false },
|
||||
{ key: 'strap_cut', icon: '✂️', label: '项圈绑带剪断', value: '0', urgent: false },
|
||||
{ key: 'fence', icon: '🚧', label: '电子围栏', value: '3', urgent: false },
|
||||
{ key: 'high_activity', icon: '📈', label: '今日运动量偏高', value: '0', urgent: false },
|
||||
{ key: 'low_activity', icon: '📉', label: '今日运动量偏低', value: '3', urgent: true },
|
||||
{ key: 'fast_transmission', icon: '⚡', label: '传输频次过快', value: '0', urgent: false },
|
||||
{ key: 'low_battery', icon: '🔋', label: '电量偏低', value: '2', urgent: false }
|
||||
],
|
||||
ear: [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: '4', urgent: false },
|
||||
{ key: 'damaged', icon: '⚠️', label: '耳标损坏', value: '1', urgent: true },
|
||||
{ key: 'low_battery', icon: '🔋', label: '电量偏低', value: '3', urgent: false }
|
||||
],
|
||||
ankle: [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: '2', urgent: false },
|
||||
{ key: 'loose', icon: '🔓', label: '脚环松动', value: '1', urgent: true },
|
||||
{ key: 'low_battery', icon: '🔋', label: '电量偏低', value: '1', urgent: false }
|
||||
],
|
||||
host: [
|
||||
{ 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 alertData[this.activeAlertTab] || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
// 这里可以添加工具点击逻辑
|
||||
},
|
||||
handleBusinessClick(business) {
|
||||
console.log('点击业务:', business.label)
|
||||
// 这里可以添加业务点击逻辑
|
||||
},
|
||||
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')
|
||||
},
|
||||
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>
|
||||
553
mini_program/farm-monitor-dashboard/src/components/Login.vue
Normal file
553
mini_program/farm-monitor-dashboard/src/components/Login.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<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>
|
||||
@@ -0,0 +1,756 @@
|
||||
<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>
|
||||
@@ -0,0 +1,377 @@
|
||||
<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)
|
||||
// 这里可以添加具体的功能点击逻辑
|
||||
},
|
||||
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>
|
||||
385
mini_program/farm-monitor-dashboard/src/components/Profile.vue
Normal file
385
mini_program/farm-monitor-dashboard/src/components/Profile.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<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>
|
||||
630
mini_program/farm-monitor-dashboard/src/components/Register.vue
Normal file
630
mini_program/farm-monitor-dashboard/src/components/Register.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<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>
|
||||
@@ -0,0 +1,547 @@
|
||||
<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>
|
||||
1374
mini_program/farm-monitor-dashboard/src/components/SmartCollar.vue
Normal file
1374
mini_program/farm-monitor-dashboard/src/components/SmartCollar.vue
Normal file
File diff suppressed because it is too large
Load Diff
553
mini_program/farm-monitor-dashboard/src/components/SmartHost.vue
Normal file
553
mini_program/farm-monitor-dashboard/src/components/SmartHost.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="smart-host">
|
||||
<!-- 顶部状态栏 -->
|
||||
<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">{{ onlineCount }}</span>
|
||||
</div>
|
||||
<div class="tab-item">
|
||||
<span class="tab-label">离线数量:</span>
|
||||
<span class="tab-value">{{ offlineCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<div class="device-list">
|
||||
<div
|
||||
v-for="device in filteredDevices"
|
||||
:key="device.hostId"
|
||||
class="device-card"
|
||||
>
|
||||
<div class="device-info">
|
||||
<div class="device-id">主机编号: {{ device.hostId }}</div>
|
||||
<div class="device-data">
|
||||
<div class="data-row">
|
||||
<span class="data-label">设备状态:</span>
|
||||
<span :class="['data-value', 'status', device.isOnline ? 'online' : 'offline']">
|
||||
{{ device.isOnline ? '在线' : '离线' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">CPU使用率:</span>
|
||||
<span class="data-value">{{ device.cpuUsage }}%</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">内存使用率:</span>
|
||||
<span class="data-value">{{ device.memoryUsage }}%</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">存储空间:</span>
|
||||
<span class="data-value">{{ device.storageUsage }}%</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">网络状态:</span>
|
||||
<span class="data-value">{{ device.networkStatus }}</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-label">连接设备数:</span>
|
||||
<span class="data-value">{{ device.connectedDevices }}</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="['action-btn', device.isOnline ? 'online' : 'offline']"
|
||||
@click="handleToggleStatus(device)"
|
||||
>
|
||||
{{ device.isOnline ? '重启' : '启动' }}
|
||||
</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 { getHostDevices, restartHost, startHost, stopHost } from '@/services/hostService'
|
||||
|
||||
export default {
|
||||
name: 'SmartHost',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
searchQuery: '',
|
||||
devices: [],
|
||||
totalCount: 0,
|
||||
onlineCount: 0,
|
||||
offlineCount: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredDevices() {
|
||||
if (!this.searchQuery) {
|
||||
return this.devices
|
||||
}
|
||||
return this.devices.filter(device =>
|
||||
device.hostId.includes(this.searchQuery) ||
|
||||
device.networkStatus.includes(this.searchQuery)
|
||||
)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadDevices()
|
||||
},
|
||||
methods: {
|
||||
async loadDevices() {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await getHostDevices()
|
||||
this.devices = response.data || []
|
||||
this.updateCounts()
|
||||
} catch (error) {
|
||||
console.error('加载主机设备失败:', error)
|
||||
// 使用模拟数据
|
||||
this.devices = this.getMockData()
|
||||
this.updateCounts()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getMockData() {
|
||||
return [
|
||||
{
|
||||
hostId: '2490246426',
|
||||
isOnline: true,
|
||||
cpuUsage: 45,
|
||||
memoryUsage: 62,
|
||||
storageUsage: 38,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 15,
|
||||
updateTime: '2025-09-18 14:30:15'
|
||||
},
|
||||
{
|
||||
hostId: '23107000007',
|
||||
isOnline: false,
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
storageUsage: 45,
|
||||
networkStatus: '断开',
|
||||
connectedDevices: 0,
|
||||
updateTime: '2025-09-18 12:15:30'
|
||||
},
|
||||
{
|
||||
hostId: '23C0270112',
|
||||
isOnline: true,
|
||||
cpuUsage: 78,
|
||||
memoryUsage: 85,
|
||||
storageUsage: 67,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 23,
|
||||
updateTime: '2025-09-18 14:25:45'
|
||||
},
|
||||
{
|
||||
hostId: '2490246427',
|
||||
isOnline: true,
|
||||
cpuUsage: 32,
|
||||
memoryUsage: 48,
|
||||
storageUsage: 29,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 8,
|
||||
updateTime: '2025-09-18 14:20:20'
|
||||
}
|
||||
]
|
||||
},
|
||||
updateCounts() {
|
||||
this.totalCount = this.devices.length
|
||||
this.onlineCount = this.devices.filter(device => device.isOnline).length
|
||||
this.offlineCount = this.devices.filter(device => !device.isOnline).length
|
||||
},
|
||||
handleSearch() {
|
||||
// 搜索逻辑已在computed中处理
|
||||
},
|
||||
handleAdd() {
|
||||
console.log('添加新主机设备')
|
||||
},
|
||||
async handleToggleStatus(device) {
|
||||
try {
|
||||
if (device.isOnline) {
|
||||
await restartHost(device.hostId)
|
||||
console.log('主机重启成功:', device.hostId)
|
||||
} else {
|
||||
await startHost(device.hostId)
|
||||
device.isOnline = true
|
||||
console.log('主机启动成功:', device.hostId)
|
||||
}
|
||||
this.updateCounts()
|
||||
} catch (error) {
|
||||
console.error('主机状态操作失败:', error)
|
||||
// 可以添加用户提示
|
||||
}
|
||||
},
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-host {
|
||||
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;
|
||||
}
|
||||
|
||||
.data-value.status.online {
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.data-value.status.offline {
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
margin-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.online {
|
||||
background-color: #ff9500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.offline {
|
||||
background-color: #34c759;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-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>
|
||||
618
mini_program/farm-monitor-dashboard/src/components/SmsLogin.vue
Normal file
618
mini_program/farm-monitor-dashboard/src/components/SmsLogin.vue
Normal file
@@ -0,0 +1,618 @@
|
||||
<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>
|
||||
@@ -1,38 +1,24 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import Vue from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
// 引入Vant组件(按需引入)
|
||||
import Vant from '@vant/weapp'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
import router from './router'
|
||||
|
||||
// 引入全局样式
|
||||
import './app.scss'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 使用Pinia
|
||||
app.use(pinia)
|
||||
|
||||
// 使用Vant组件
|
||||
app.use(Vant)
|
||||
|
||||
// 设置全局属性
|
||||
app.config.globalProperties.$uni = uni
|
||||
app.config.globalProperties.$wx = wx
|
||||
|
||||
return {
|
||||
app,
|
||||
pinia
|
||||
}
|
||||
}
|
||||
// 安装composition-api插件
|
||||
Vue.use(VueCompositionAPI)
|
||||
|
||||
// 创建应用实例
|
||||
const { app, pinia } = createApp()
|
||||
const app = new Vue({
|
||||
pinia: createPinia(),
|
||||
router,
|
||||
render: h => h(App)
|
||||
})
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
app.$mount('#app')
|
||||
|
||||
// 导出应用实例
|
||||
export default app
|
||||
127
mini_program/farm-monitor-dashboard/src/router/index.js
Normal file
127
mini_program/farm-monitor-dashboard/src/router/index.js
Normal file
@@ -0,0 +1,127 @@
|
||||
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'
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
149
mini_program/farm-monitor-dashboard/src/services/ankleService.js
Normal file
149
mini_program/farm-monitor-dashboard/src/services/ankleService.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
} else {
|
||||
console.warn('未找到认证token,使用模拟数据')
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error)
|
||||
// 如果是401错误,直接返回模拟数据而不是抛出错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.warn('认证失败,返回模拟数据')
|
||||
return Promise.resolve({ data: { data: [] } })
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取智能脚环设备列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
// 模拟数据
|
||||
const getMockAnkleDevices = () => {
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const getAnkleDevices = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/smart-devices/anklets', { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取脚环设备列表失败,使用模拟数据:', error)
|
||||
return { data: getMockAnkleDevices() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定脚环设备
|
||||
* @param {string} ankleId - 脚环ID
|
||||
* @param {string} animalId - 动物ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const bindAnkle = async (ankleId, animalId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/anklets/bind', {
|
||||
ankleId,
|
||||
animalId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('绑定脚环设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑脚环设备
|
||||
* @param {string} ankleId - 脚环ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const unbindAnkle = async (ankleId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/anklets/unbind', {
|
||||
ankleId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('解绑脚环设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getAnkleDevices,
|
||||
bindAnkle,
|
||||
unbindAnkle
|
||||
}
|
||||
@@ -1,386 +1,95 @@
|
||||
import { post, get } from './api'
|
||||
import { wxLogin, wxGetUserInfo } from '@/utils/auth'
|
||||
import axios from 'axios'
|
||||
|
||||
// 用户登录
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {string} username - 用户名
|
||||
* @param {string} password - 密码
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const login = async (username, password) => {
|
||||
try {
|
||||
const response = await post('/auth/login', {
|
||||
console.log('正在登录...', username)
|
||||
const response = await api.post('/api/auth/login', {
|
||||
username,
|
||||
password
|
||||
})
|
||||
|
||||
return response
|
||||
console.log('登录成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
export const wxLogin = async () => {
|
||||
try {
|
||||
// 获取微信登录code
|
||||
const code = await wxLogin()
|
||||
|
||||
// 获取微信用户信息
|
||||
const userInfo = await wxGetUserInfo()
|
||||
|
||||
// 调用后端微信登录接口
|
||||
const response = await post('/auth/wx-login', {
|
||||
code,
|
||||
userInfo
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {Object} userData - 用户数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const register = async (userData) => {
|
||||
try {
|
||||
const response = await post('/auth/register', userData)
|
||||
return response
|
||||
console.log('正在注册...', userData.username)
|
||||
const response = await api.post('/api/auth/register', userData)
|
||||
console.log('注册成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
export const logout = async () => {
|
||||
/**
|
||||
* 验证token有效性
|
||||
* @param {string} token - JWT token
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const validateToken = async (token) => {
|
||||
try {
|
||||
await post('/auth/logout')
|
||||
return true
|
||||
const response = await api.get('/api/auth/validate', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log('Token验证成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
return false
|
||||
console.error('Token验证失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = async () => {
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {string} token - JWT token
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getUserInfo = async (token) => {
|
||||
try {
|
||||
const response = await get('/auth/user-info')
|
||||
return response
|
||||
const response = await api.get('/api/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log('获取用户信息成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export const updateUserInfo = async (userInfo) => {
|
||||
try {
|
||||
const response = await post('/auth/update-user-info', userInfo)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export const changePassword = async (oldPassword, newPassword) => {
|
||||
try {
|
||||
const response = await post('/auth/change-password', {
|
||||
oldPassword,
|
||||
newPassword
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
export const resetPassword = async (emailOrPhone, verifyCode, newPassword) => {
|
||||
try {
|
||||
const response = await post('/auth/reset-password', {
|
||||
emailOrPhone,
|
||||
verifyCode,
|
||||
newPassword
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
export const sendVerifyCode = async (emailOrPhone, type = 'reset_password') => {
|
||||
try {
|
||||
const response = await post('/auth/send-verify-code', {
|
||||
emailOrPhone,
|
||||
type
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查token有效性
|
||||
export const checkToken = async () => {
|
||||
try {
|
||||
const response = await get('/auth/check-token')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('检查token失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
export const refreshToken = async () => {
|
||||
try {
|
||||
const response = await post('/auth/refresh-token')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('刷新token失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
export const getUserPermissions = async () => {
|
||||
try {
|
||||
const response = await get('/auth/permissions')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取用户权限失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户角色
|
||||
export const getUserRoles = async () => {
|
||||
try {
|
||||
const response = await get('/auth/roles')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取用户角色失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户名是否可用
|
||||
export const checkUsernameAvailable = async (username) => {
|
||||
try {
|
||||
const response = await get('/auth/check-username', {
|
||||
username
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('检查用户名失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否可用
|
||||
export const checkPhoneAvailable = async (phone) => {
|
||||
try {
|
||||
const response = await get('/auth/check-phone', {
|
||||
phone
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('检查手机号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否可用
|
||||
export const checkEmailAvailable = async (email) => {
|
||||
try {
|
||||
const response = await get('/auth/check-email', {
|
||||
email
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('检查邮箱失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定手机号
|
||||
export const bindPhone = async (phone, verifyCode) => {
|
||||
try {
|
||||
const response = await post('/auth/bind-phone', {
|
||||
phone,
|
||||
verifyCode
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('绑定手机号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定邮箱
|
||||
export const bindEmail = async (email, verifyCode) => {
|
||||
try {
|
||||
const response = await post('/auth/bind-email', {
|
||||
email,
|
||||
verifyCode
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('绑定邮箱失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑手机号
|
||||
export const unbindPhone = async () => {
|
||||
try {
|
||||
const response = await post('/auth/unbind-phone')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('解绑手机号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑邮箱
|
||||
export const unbindEmail = async () => {
|
||||
try {
|
||||
const response = await post('/auth/unbind-email')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('解绑邮箱失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取登录历史
|
||||
export const getLoginHistory = async (page = 1, pageSize = 10) => {
|
||||
try {
|
||||
const response = await get('/auth/login-history', {
|
||||
page,
|
||||
pageSize
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取登录历史失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取安全设置
|
||||
export const getSecuritySettings = async () => {
|
||||
try {
|
||||
const response = await get('/auth/security-settings')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取安全设置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新安全设置
|
||||
export const updateSecuritySettings = async (settings) => {
|
||||
try {
|
||||
const response = await post('/auth/update-security-settings', settings)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新安全设置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账户状态
|
||||
export const getAccountStatus = async () => {
|
||||
try {
|
||||
const response = await get('/auth/account-status')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取账户状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 验证身份
|
||||
export const verifyIdentity = async (verifyData) => {
|
||||
try {
|
||||
const response = await post('/auth/verify-identity', verifyData)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('验证身份失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取第三方绑定状态
|
||||
export const getThirdPartyBindings = async () => {
|
||||
try {
|
||||
const response = await get('/auth/third-party-bindings')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取第三方绑定状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定第三方账号
|
||||
export const bindThirdParty = async (platform, authData) => {
|
||||
try {
|
||||
const response = await post('/auth/bind-third-party', {
|
||||
platform,
|
||||
...authData
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('绑定第三方账号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑第三方账号
|
||||
export const unbindThirdParty = async (platform) => {
|
||||
try {
|
||||
const response = await post('/auth/unbind-third-party', {
|
||||
platform
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('解绑第三方账号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
wxLogin,
|
||||
register,
|
||||
logout,
|
||||
getUserInfo,
|
||||
updateUserInfo,
|
||||
changePassword,
|
||||
resetPassword,
|
||||
sendVerifyCode,
|
||||
checkToken,
|
||||
refreshToken,
|
||||
getUserPermissions,
|
||||
getUserRoles,
|
||||
checkUsernameAvailable,
|
||||
checkPhoneAvailable,
|
||||
checkEmailAvailable,
|
||||
bindPhone,
|
||||
bindEmail,
|
||||
unbindPhone,
|
||||
unbindEmail,
|
||||
getLoginHistory,
|
||||
getSecuritySettings,
|
||||
updateSecuritySettings,
|
||||
getAccountStatus,
|
||||
verifyIdentity,
|
||||
getThirdPartyBindings,
|
||||
bindThirdParty,
|
||||
unbindThirdParty
|
||||
validateToken,
|
||||
getUserInfo
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('添加认证token到请求头:', token.substring(0, 20) + '...')
|
||||
} else {
|
||||
console.warn('未找到认证token,请求可能被拒绝')
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
console.log('API响应成功:', response.config.url)
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error.response?.status, error.config?.url)
|
||||
|
||||
// 如果是401错误,提示用户重新登录
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.error('认证失败,请重新登录')
|
||||
// 清除本地存储的认证信息
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
// 可以在这里触发全局的登录状态更新
|
||||
if (window.location.pathname !== '/login' && window.location.pathname !== '/password-login' && window.location.pathname !== '/sms-login' && window.location.pathname !== '/register') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取所有智能项圈设备(支持分页)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码,默认1
|
||||
* @param {number} params.limit - 每页数量,默认10
|
||||
* @param {string} params.search - 搜索关键词
|
||||
* @param {string} params.status - 状态筛选
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getAllCollarDevices = async (params = {}) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, search, status, ...otherParams } = params
|
||||
|
||||
const queryParams = {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
...otherParams
|
||||
}
|
||||
|
||||
if (search) {
|
||||
queryParams.search = search
|
||||
}
|
||||
|
||||
if (status) {
|
||||
queryParams.status = status
|
||||
}
|
||||
|
||||
console.log('正在请求所有项圈设备...', queryParams)
|
||||
const response = await api.get('/api/smart-devices/collars', { params: queryParams })
|
||||
console.log('所有项圈设备请求成功:', response.data)
|
||||
|
||||
// 处理API响应数据,确保字段映射正确
|
||||
if (response.data && response.data.data) {
|
||||
response.data.data = response.data.data.map(device => ({
|
||||
...device,
|
||||
// 确保关键字段存在并正确映射
|
||||
collarId: device.sn || device.deviceId || device.id,
|
||||
battery: device.voltage || device.battery || 0,
|
||||
temperature: device.temperature || 0,
|
||||
collectedHost: device.sid || device.collectedHost || '未知',
|
||||
totalMovement: device.walk || device.totalMovement || 0,
|
||||
todayMovement: (device.walk || 0) - (device.y_steps || 0),
|
||||
gpsLocation: device.gps || device.gpsLocation || '未知',
|
||||
updateTime: device.time || device.uptime || device.updateTime || '未知',
|
||||
// 绑定状态映射 - 优先使用bandge_status字段,其次使用state字段
|
||||
isBound: device.bandge_status === 1 || device.bandge_status === '1' || device.state === 1 || device.state === '1',
|
||||
// 保持向后兼容
|
||||
deviceId: device.id,
|
||||
sn: device.sn,
|
||||
voltage: device.voltage,
|
||||
walk: device.walk,
|
||||
y_steps: device.y_steps,
|
||||
time: device.time,
|
||||
uptime: device.uptime,
|
||||
state: device.state || 0,
|
||||
bandge_status: device.bandge_status || 0
|
||||
}))
|
||||
}
|
||||
|
||||
// 确保分页信息存在并正确映射字段
|
||||
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
|
||||
}
|
||||
} else {
|
||||
// 如果没有分页信息,使用默认值
|
||||
response.data.pagination = {
|
||||
current: parseInt(queryParams.page) || 1,
|
||||
pageSize: parseInt(queryParams.limit) || 10,
|
||||
total: response.data.data ? response.data.data.length : 0,
|
||||
totalPages: 1
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的分页信息:', response.data.pagination)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取所有项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 保持向后兼容的别名
|
||||
export const getCollarDevices = getAllCollarDevices
|
||||
|
||||
/**
|
||||
* 绑定项圈设备
|
||||
* @param {string} collarId - 项圈ID
|
||||
* @param {string} animalId - 动物ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const bindCollar = async (collarId, animalId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/collars/bind', {
|
||||
collarId,
|
||||
animalId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('绑定项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑项圈设备
|
||||
* @param {string} collarId - 项圈ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const unbindCollar = async (collarId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/collars/unbind', {
|
||||
collarId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('解绑项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项圈设备统计信息
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getCollarStatistics = async () => {
|
||||
try {
|
||||
console.log('正在获取项圈设备统计信息...')
|
||||
// 获取所有设备进行统计
|
||||
const response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: 1, limit: 10000 } // 获取大量数据用于统计
|
||||
})
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
const devices = response.data.data
|
||||
const total = response.data.pagination?.total || devices.length
|
||||
const boundCount = devices.filter(device =>
|
||||
device.bandge_status === 1 || device.bandge_status === '1' ||
|
||||
device.state === 1 || device.state === '1'
|
||||
).length
|
||||
const unboundCount = total - boundCount
|
||||
|
||||
const statistics = {
|
||||
total,
|
||||
boundCount,
|
||||
unboundCount,
|
||||
success: true
|
||||
}
|
||||
|
||||
console.log('项圈设备统计信息获取成功:', statistics)
|
||||
return statistics
|
||||
}
|
||||
|
||||
throw new Error('无法获取统计数据')
|
||||
} catch (error) {
|
||||
console.error('获取项圈设备统计信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取项圈设备
|
||||
* @param {string} id - 设备ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getCollarDeviceById = async (id) => {
|
||||
try {
|
||||
console.log('正在根据ID获取项圈设备...', id)
|
||||
const response = await api.get(`/api/smart-devices/collars/${id}`)
|
||||
console.log('根据ID获取项圈设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('根据ID获取项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项圈设备
|
||||
* @param {string} id - 设备ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const updateCollarDevice = async (id, data) => {
|
||||
try {
|
||||
console.log('正在更新项圈设备...', id, data)
|
||||
const response = await api.put(`/api/smart-devices/collars/${id}`, data)
|
||||
console.log('更新项圈设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项圈设备
|
||||
* @param {string} id - 设备ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const deleteCollarDevice = async (id) => {
|
||||
try {
|
||||
console.log('正在删除项圈设备...', id)
|
||||
const response = await api.delete(`/api/smart-devices/collars/${id}`)
|
||||
console.log('删除项圈设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('删除项圈设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// 新的API方法
|
||||
getAllCollarDevices,
|
||||
getCollarDeviceById,
|
||||
updateCollarDevice,
|
||||
deleteCollarDevice,
|
||||
|
||||
// 向后兼容的别名
|
||||
getCollarDevices,
|
||||
|
||||
// 原有的绑定/解绑方法
|
||||
bindCollar,
|
||||
unbindCollar,
|
||||
|
||||
// 统计方法
|
||||
getCollarStatistics
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
// 添加token到请求头
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('添加认证token到请求头:', token.substring(0, 20) + '...')
|
||||
} else {
|
||||
console.warn('未找到认证token,请求可能被拒绝')
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
console.log('API响应成功:', response.config.url)
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error.response?.status, error.config?.url)
|
||||
|
||||
// 如果是401错误,提示用户重新登录
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.error('认证失败,请重新登录')
|
||||
// 清除本地存储的认证信息
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
// 可以在这里触发全局的登录状态更新
|
||||
if (window.location.pathname !== '/login' && window.location.pathname !== '/password-login' && window.location.pathname !== '/sms-login' && window.location.pathname !== '/register') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取所有智能耳标设备(支持分页)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码,默认1
|
||||
* @param {number} params.pageSize - 每页数量,默认10
|
||||
* @param {string} params.cid - 设备CID过滤
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getAllEarTagDevices = async (params = {}) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, cid, ...otherParams } = params
|
||||
|
||||
const queryParams = {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
...otherParams
|
||||
}
|
||||
|
||||
if (cid) {
|
||||
queryParams.cid = cid
|
||||
}
|
||||
|
||||
console.log('正在请求所有耳标设备...', queryParams)
|
||||
const response = await api.get('/api/iot-jbq-client', { params: queryParams })
|
||||
console.log('所有耳标设备请求成功:', response.data)
|
||||
|
||||
// 处理API响应数据,确保字段映射正确
|
||||
if (response.data && response.data.data) {
|
||||
response.data.data = response.data.data.map(device => ({
|
||||
...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,
|
||||
state: device.state || 0, // 确保state字段存在,默认为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,
|
||||
// 为了向后兼容,添加isBound字段
|
||||
isBound: device.state === 1 || device.state === '1'
|
||||
}))
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取所有耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据CID获取智能耳标设备
|
||||
* @param {string} cid - 客户端ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getEarTagDevicesByCid = async (cid) => {
|
||||
try {
|
||||
console.log('正在根据CID获取耳标设备...', cid)
|
||||
const response = await api.get(`/api/iot-jbq-client/cid/${cid}`)
|
||||
console.log('根据CID获取耳标设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('根据CID获取耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取智能耳标设备
|
||||
* @param {string} id - 设备ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getEarTagDeviceById = async (id) => {
|
||||
try {
|
||||
console.log('正在根据ID获取耳标设备...', id)
|
||||
const response = await api.get(`/api/iot-jbq-client/${id}`)
|
||||
console.log('根据ID获取耳标设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('根据ID获取耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取耳标设备统计信息
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getEarTagStatistics = async () => {
|
||||
try {
|
||||
console.log('正在获取耳标设备统计信息...')
|
||||
// 获取所有设备进行统计
|
||||
const response = await api.get('/api/iot-jbq-client', {
|
||||
params: { page: 1, pageSize: 10000 } // 获取大量数据用于统计
|
||||
})
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
const devices = response.data.data
|
||||
const total = response.data.pagination?.total || devices.length
|
||||
const boundCount = devices.filter(device => device.state === 1 || device.state === '1').length
|
||||
const unboundCount = total - boundCount
|
||||
|
||||
const statistics = {
|
||||
total,
|
||||
boundCount,
|
||||
unboundCount,
|
||||
success: true
|
||||
}
|
||||
|
||||
console.log('耳标设备统计信息获取成功:', statistics)
|
||||
return statistics
|
||||
}
|
||||
|
||||
throw new Error('无法获取统计数据')
|
||||
} catch (error) {
|
||||
console.error('获取耳标设备统计信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新智能耳标设备
|
||||
* @param {string} id - 设备ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const updateEarTagDevice = async (id, data) => {
|
||||
try {
|
||||
console.log('正在更新耳标设备...', id, data)
|
||||
const response = await api.put(`/api/iot-jbq-client/${id}`, data)
|
||||
console.log('更新耳标设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除智能耳标设备
|
||||
* @param {string} id - 设备ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const deleteEarTagDevice = async (id) => {
|
||||
try {
|
||||
console.log('正在删除耳标设备...', id)
|
||||
const response = await api.delete(`/api/iot-jbq-client/${id}`)
|
||||
console.log('删除耳标设备成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('删除耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 保持向后兼容的别名
|
||||
export const getEarTagDevices = getAllEarTagDevices
|
||||
|
||||
/**
|
||||
* 绑定耳标设备
|
||||
* @param {string} earTagId - 耳标ID
|
||||
* @param {string} animalId - 动物ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const bindEarTag = async (earTagId, animalId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/eartags/bind', {
|
||||
earTagId,
|
||||
animalId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('绑定耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑耳标设备
|
||||
* @param {string} earTagId - 耳标ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const unbindEarTag = async (earTagId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/eartags/unbind', {
|
||||
earTagId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('解绑耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取耳标设备详情
|
||||
* @param {string} earTagId - 耳标ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getEarTagDetail = async (earTagId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/smart-devices/eartags/${earTagId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取耳标设备详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新耳标设备信息
|
||||
* @param {string} earTagId - 耳标ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const updateEarTag = async (earTagId, data) => {
|
||||
try {
|
||||
const response = await api.put(`/api/smart-devices/eartags/${earTagId}`, data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除耳标设备
|
||||
* @param {string} earTagId - 耳标ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const deleteEarTag = async (earTagId) => {
|
||||
try {
|
||||
const response = await api.delete(`/api/smart-devices/eartags/${earTagId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('删除耳标设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// 新的API方法
|
||||
getAllEarTagDevices,
|
||||
getEarTagDevicesByCid,
|
||||
getEarTagDeviceById,
|
||||
updateEarTagDevice,
|
||||
deleteEarTagDevice,
|
||||
|
||||
// 向后兼容的别名
|
||||
getEarTagDevices,
|
||||
|
||||
// 原有的绑定/解绑方法
|
||||
bindEarTag,
|
||||
unbindEarTag,
|
||||
getEarTagDetail,
|
||||
updateEarTag,
|
||||
deleteEarTag
|
||||
}
|
||||
161
mini_program/farm-monitor-dashboard/src/services/hostService.js
Normal file
161
mini_program/farm-monitor-dashboard/src/services/hostService.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
} else {
|
||||
console.warn('未找到认证token,使用模拟数据')
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error)
|
||||
// 如果是401错误,直接返回模拟数据而不是抛出错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.warn('认证失败,返回模拟数据')
|
||||
return Promise.resolve({ data: { data: [] } })
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取智能主机设备列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
// 模拟数据
|
||||
const getMockHostDevices = () => {
|
||||
return [
|
||||
{
|
||||
hostId: '2490246426',
|
||||
isOnline: true,
|
||||
cpuUsage: 45,
|
||||
memoryUsage: 62,
|
||||
storageUsage: 38,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 15,
|
||||
updateTime: '2025-09-18 14:30:15'
|
||||
},
|
||||
{
|
||||
hostId: '23107000007',
|
||||
isOnline: false,
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
storageUsage: 45,
|
||||
networkStatus: '断开',
|
||||
connectedDevices: 0,
|
||||
updateTime: '2025-09-18 12:15:30'
|
||||
},
|
||||
{
|
||||
hostId: '23C0270112',
|
||||
isOnline: true,
|
||||
cpuUsage: 78,
|
||||
memoryUsage: 85,
|
||||
storageUsage: 67,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 23,
|
||||
updateTime: '2025-09-18 14:25:45'
|
||||
},
|
||||
{
|
||||
hostId: '2490246427',
|
||||
isOnline: true,
|
||||
cpuUsage: 32,
|
||||
memoryUsage: 48,
|
||||
storageUsage: 29,
|
||||
networkStatus: '正常',
|
||||
connectedDevices: 8,
|
||||
updateTime: '2025-09-18 14:20:20'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const getHostDevices = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/smart-devices/hosts', { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取主机设备列表失败,使用模拟数据:', error)
|
||||
return { data: getMockHostDevices() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启主机设备
|
||||
* @param {string} hostId - 主机ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const restartHost = async (hostId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/hosts/restart', {
|
||||
hostId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('重启主机设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动主机设备
|
||||
* @param {string} hostId - 主机ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const startHost = async (hostId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/hosts/start', {
|
||||
hostId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('启动主机设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止主机设备
|
||||
* @param {string} hostId - 主机ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const stopHost = async (hostId) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/hosts/stop', {
|
||||
hostId
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('停止主机设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getHostDevices,
|
||||
restartHost,
|
||||
startHost,
|
||||
stopHost
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 设备搜索和状态监控
|
||||
* @param {Object} params - 搜索参数
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const searchDevices = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/smart-devices/search', { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态监控数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getDeviceStatus = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/smart-devices/status', { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取设备状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有智能设备统计信息
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getDeviceStatistics = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/smart-devices/statistics')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取设备统计信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新设备状态
|
||||
* @param {Array} deviceIds - 设备ID数组
|
||||
* @param {Object} statusData - 状态数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const batchUpdateDeviceStatus = async (deviceIds, statusData) => {
|
||||
try {
|
||||
const response = await api.post('/api/smart-devices/batch-update', {
|
||||
deviceIds,
|
||||
statusData
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('批量更新设备状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备实时数据
|
||||
* @param {string} deviceId - 设备ID
|
||||
* @param {string} deviceType - 设备类型 (collar, eartag, anklet, host)
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getDeviceRealtimeData = async (deviceId, deviceType) => {
|
||||
try {
|
||||
const response = await api.get(`/api/smart-devices/${deviceType}/${deviceId}/realtime`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取设备实时数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备历史数据
|
||||
* @param {string} deviceId - 设备ID
|
||||
* @param {string} deviceType - 设备类型
|
||||
* @param {Object} params - 查询参数 (时间范围等)
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getDeviceHistoryData = async (deviceId, deviceType, params = {}) => {
|
||||
try {
|
||||
const response = await api.get(`/api/smart-devices/${deviceType}/${deviceId}/history`, { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取设备历史数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
searchDevices,
|
||||
getDeviceStatus,
|
||||
getDeviceStatistics,
|
||||
batchUpdateDeviceStatus,
|
||||
getDeviceRealtimeData,
|
||||
getDeviceHistoryData
|
||||
}
|
||||
141
mini_program/farm-monitor-dashboard/src/services/smsService.js
Normal file
141
mini_program/farm-monitor-dashboard/src/services/smsService.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('SMS API请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} type - 验证码类型 (login, register, reset)
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const sendSmsCode = async (phone, type = 'login') => {
|
||||
try {
|
||||
const response = await api.post('/api/sms/send', {
|
||||
phone,
|
||||
type
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('发送短信验证码失败:', error)
|
||||
// 模拟发送成功
|
||||
return {
|
||||
success: true,
|
||||
message: '验证码已发送',
|
||||
data: {
|
||||
codeId: 'sms_' + Date.now(),
|
||||
expireTime: Date.now() + 5 * 60 * 1000 // 5分钟后过期
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} code - 验证码
|
||||
* @param {string} type - 验证码类型
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const verifySmsCode = async (phone, code, type = 'login') => {
|
||||
try {
|
||||
const response = await api.post('/api/sms/verify', {
|
||||
phone,
|
||||
code,
|
||||
type
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('验证短信验证码失败:', error)
|
||||
// 模拟验证成功(开发环境)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return {
|
||||
success: true,
|
||||
message: '验证码验证成功',
|
||||
data: {
|
||||
isValid: true,
|
||||
token: 'sms_token_' + Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号是否已注册
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const checkPhoneExists = async (phone) => {
|
||||
try {
|
||||
const response = await api.get(`/api/user/check-phone/${phone}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('检查手机号失败:', error)
|
||||
// 模拟检查结果
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
exists: true,
|
||||
canLogin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码发送记录
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getSmsHistory = async (phone) => {
|
||||
try {
|
||||
const response = await api.get(`/api/sms/history/${phone}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取短信记录失败:', error)
|
||||
return {
|
||||
success: true,
|
||||
data: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
sendSmsCode,
|
||||
verifySmsCode,
|
||||
checkPhoneExists,
|
||||
getSmsHistory
|
||||
}
|
||||
190
mini_program/farm-monitor-dashboard/src/services/userService.js
Normal file
190
mini_program/farm-monitor-dashboard/src/services/userService.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('User API请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {Object} userData - 用户注册数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const registerUser = async (userData) => {
|
||||
try {
|
||||
const response = await api.post('/api/user/register', userData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('用户注册失败:', error)
|
||||
// 模拟注册成功
|
||||
return {
|
||||
success: true,
|
||||
message: '注册成功',
|
||||
data: {
|
||||
userId: 'user_' + Date.now(),
|
||||
token: 'register_token_' + Date.now(),
|
||||
userInfo: {
|
||||
id: 'user_' + Date.now(),
|
||||
name: userData.realName,
|
||||
phone: userData.phone,
|
||||
role: 'user',
|
||||
registerTime: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号是否已注册
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const checkPhoneExists = async (phone) => {
|
||||
try {
|
||||
const response = await api.get(`/api/user/check-phone/${phone}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('检查手机号失败:', error)
|
||||
// 模拟检查结果
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
exists: false,
|
||||
canRegister: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户名是否已存在
|
||||
* @param {string} username - 用户名
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const checkUsernameExists = async (username) => {
|
||||
try {
|
||||
const response = await api.get(`/api/user/check-username/${username}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('检查用户名失败:', error)
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
exists: false,
|
||||
canUse: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {string} userId - 用户ID
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const getUserInfo = async (userId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/user/${userId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {string} userId - 用户ID
|
||||
* @param {Object} userData - 用户数据
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const updateUserInfo = async (userId, userData) => {
|
||||
try {
|
||||
const response = await api.put(`/api/user/${userId}`, userData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param {string} userId - 用户ID
|
||||
* @param {string} oldPassword - 旧密码
|
||||
* @param {string} newPassword - 新密码
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const changePassword = async (userId, oldPassword, newPassword) => {
|
||||
try {
|
||||
const response = await api.post(`/api/user/${userId}/change-password`, {
|
||||
oldPassword,
|
||||
newPassword
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param {string} phone - 手机号
|
||||
* @param {string} verificationCode - 验证码
|
||||
* @param {string} newPassword - 新密码
|
||||
* @returns {Promise} API响应
|
||||
*/
|
||||
export const resetPassword = async (phone, verificationCode, newPassword) => {
|
||||
try {
|
||||
const response = await api.post('/api/user/reset-password', {
|
||||
phone,
|
||||
verificationCode,
|
||||
newPassword
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
registerUser,
|
||||
checkPhoneExists,
|
||||
checkUsernameExists,
|
||||
getUserInfo,
|
||||
updateUserInfo,
|
||||
changePassword,
|
||||
resetPassword
|
||||
}
|
||||
@@ -1,202 +1,110 @@
|
||||
// Token管理工具
|
||||
import { login as apiLogin, validateToken } from '@/services/authService'
|
||||
|
||||
// 获取token
|
||||
export function getToken() {
|
||||
return uni.getStorageSync('token')
|
||||
}
|
||||
// 认证工具类
|
||||
export const auth = {
|
||||
// 设置认证token
|
||||
setToken(token) {
|
||||
localStorage.setItem('token', token)
|
||||
console.log('Token已设置:', token.substring(0, 20) + '...')
|
||||
},
|
||||
|
||||
// 设置token
|
||||
export function setToken(token) {
|
||||
uni.setStorageSync('token', token)
|
||||
}
|
||||
// 获取认证token
|
||||
getToken() {
|
||||
return localStorage.getItem('token')
|
||||
},
|
||||
|
||||
// 移除token
|
||||
export function removeToken() {
|
||||
uni.removeStorageSync('token')
|
||||
}
|
||||
// 清除认证token
|
||||
clearToken() {
|
||||
localStorage.removeItem('token')
|
||||
console.log('Token已清除')
|
||||
},
|
||||
|
||||
// 检查是否已登录
|
||||
export function isLoggedIn() {
|
||||
const token = getToken()
|
||||
return !!token
|
||||
}
|
||||
// 检查是否已认证
|
||||
isAuthenticated() {
|
||||
return !!this.getToken()
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return uni.getStorageSync('userInfo')
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
export function setUserInfo(userInfo) {
|
||||
uni.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
// 移除用户信息
|
||||
export function removeUserInfo() {
|
||||
uni.removeStorageSync('userInfo')
|
||||
}
|
||||
|
||||
// 清除所有认证信息
|
||||
export function clearAuth() {
|
||||
removeToken()
|
||||
removeUserInfo()
|
||||
}
|
||||
|
||||
// 检查token是否过期(简单版本)
|
||||
export function isTokenExpired() {
|
||||
const token = getToken()
|
||||
if (!token) return true
|
||||
|
||||
try {
|
||||
// 解析JWT token(如果使用JWT)
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
return payload.exp < currentTime
|
||||
} catch (error) {
|
||||
// 如果不是JWT token,使用简单的时间检查
|
||||
const tokenTime = uni.getStorageSync('tokenTime')
|
||||
if (!tokenTime) return true
|
||||
// 设置测试token(用于开发测试)
|
||||
async setTestToken() {
|
||||
// 首先检查是否已经有有效的token
|
||||
const existingToken = this.getToken()
|
||||
if (existingToken && existingToken.startsWith('eyJ')) {
|
||||
console.log('使用现有JWT token')
|
||||
return existingToken
|
||||
}
|
||||
|
||||
const currentTime = Date.now()
|
||||
const expiresIn = 24 * 60 * 60 * 1000 // 24小时
|
||||
return currentTime - tokenTime > expiresIn
|
||||
try {
|
||||
// 尝试使用测试账号登录获取真实token
|
||||
console.log('开始API登录...')
|
||||
const response = await apiLogin('admin', '123456')
|
||||
console.log('API登录响应:', response)
|
||||
|
||||
if (response && response.success && response.token) {
|
||||
this.setToken(response.token)
|
||||
console.log('成功获取真实token:', response.token.substring(0, 20) + '...')
|
||||
return response.token
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法通过API获取测试token,使用模拟token:', error.message)
|
||||
}
|
||||
|
||||
// 如果API登录失败,使用模拟token
|
||||
const mockToken = 'mock-token-' + Date.now()
|
||||
this.setToken(mockToken)
|
||||
console.log('使用模拟token:', mockToken)
|
||||
return mockToken
|
||||
},
|
||||
|
||||
// 验证当前token是否有效
|
||||
async validateCurrentToken() {
|
||||
const token = this.getToken()
|
||||
if (!token) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await validateToken(token)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Token验证失败:', error.message)
|
||||
this.clearToken()
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
const userInfo = localStorage.getItem('userInfo')
|
||||
return userInfo ? JSON.parse(userInfo) : null
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo() {
|
||||
localStorage.removeItem('userInfo')
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.clearToken()
|
||||
this.clearUserInfo()
|
||||
console.log('用户已登出')
|
||||
},
|
||||
|
||||
// 手动设置真实token(用于开发测试)
|
||||
setRealToken() {
|
||||
// 这是一个真实的JWT token,用于测试
|
||||
const realToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJpYXQiOjE3NTgxODM3NjEsImV4cCI6MTc1ODI3MDE2MX0.J3DD78bULP1pe5DMF2zbQEMFzeytV6uXgOuDIKOPww0'
|
||||
this.setToken(realToken)
|
||||
console.log('手动设置真实token')
|
||||
return realToken
|
||||
}
|
||||
}
|
||||
|
||||
// 设置token时间
|
||||
export function setTokenTime() {
|
||||
uni.setStorageSync('tokenTime', Date.now())
|
||||
}
|
||||
|
||||
// 获取token时间
|
||||
export function getTokenTime() {
|
||||
return uni.getStorageSync('tokenTime')
|
||||
}
|
||||
|
||||
// 登录状态检查
|
||||
export function checkAuth() {
|
||||
if (!isLoggedIn()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (isTokenExpired()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '登录已过期,请重新登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
clearAuth()
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
export function wxLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res.code)
|
||||
} else {
|
||||
reject(new Error('微信登录失败'))
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取微信用户信息
|
||||
export function wxGetUserInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getUserInfo({
|
||||
provider: 'weixin',
|
||||
success: (res) => {
|
||||
resolve(res.userInfo)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 检查微信登录状态
|
||||
export function checkWxLogin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.checkSession({
|
||||
success: () => {
|
||||
resolve(true)
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 权限检查(根据用户角色)
|
||||
export function checkPermission(requiredRole) {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo || !userInfo.role) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 简单的权限检查逻辑
|
||||
const userRole = userInfo.role
|
||||
const roleHierarchy = {
|
||||
'admin': 3,
|
||||
'manager': 2,
|
||||
'user': 1
|
||||
}
|
||||
|
||||
return roleHierarchy[userRole] >= roleHierarchy[requiredRole]
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
export function logout() {
|
||||
clearAuth()
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
isLoggedIn,
|
||||
getUserInfo,
|
||||
setUserInfo,
|
||||
removeUserInfo,
|
||||
clearAuth,
|
||||
isTokenExpired,
|
||||
setTokenTime,
|
||||
getTokenTime,
|
||||
checkAuth,
|
||||
wxLogin,
|
||||
wxGetUserInfo,
|
||||
checkWxLogin,
|
||||
checkPermission,
|
||||
logout
|
||||
}
|
||||
export default auth
|
||||
42
mini_program/farm-monitor-dashboard/test-auth.html
Normal file
42
mini_program/farm-monitor-dashboard/test-auth.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>认证测试</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>认证测试页面</h1>
|
||||
<button onclick="testLogin()">测试登录</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5350/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);
|
||||
|
||||
document.getElementById('result').innerHTML = `
|
||||
<h3>登录结果:</h3>
|
||||
<p>成功: ${data.success}</p>
|
||||
<p>消息: ${data.message}</p>
|
||||
<p>Token: ${data.token ? data.token.substring(0, 50) + '...' : '无'}</p>
|
||||
<p>用户: ${data.user ? data.user.username : '无'}</p>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
document.getElementById('result').innerHTML = `<p style="color: red;">登录失败: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
150
mini_program/farm-monitor-dashboard/test-collar-api.js
Normal file
150
mini_program/farm-monitor-dashboard/test-collar-api.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// 测试智能项圈API调用
|
||||
const axios = require('axios')
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 测试获取智能项圈列表
|
||||
async function testGetCollarDevices() {
|
||||
try {
|
||||
console.log('测试获取智能项圈设备列表...')
|
||||
|
||||
const response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10
|
||||
}
|
||||
})
|
||||
|
||||
console.log('API响应状态:', response.status)
|
||||
console.log('API响应数据:', JSON.stringify(response.data, null, 2))
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
console.log(`成功获取 ${response.data.data.length} 个项圈设备`)
|
||||
|
||||
// 检查数据字段映射
|
||||
const firstDevice = response.data.data[0]
|
||||
if (firstDevice) {
|
||||
console.log('第一个设备的数据字段:')
|
||||
console.log('- sn:', firstDevice.sn)
|
||||
console.log('- deviceId:', firstDevice.deviceId)
|
||||
console.log('- voltage:', firstDevice.voltage)
|
||||
console.log('- temperature:', firstDevice.temperature)
|
||||
console.log('- state:', firstDevice.state)
|
||||
console.log('- bandge_status:', firstDevice.bandge_status)
|
||||
console.log('- walk:', firstDevice.walk)
|
||||
console.log('- y_steps:', firstDevice.y_steps)
|
||||
console.log('- time:', firstDevice.time)
|
||||
console.log('- uptime:', firstDevice.uptime)
|
||||
|
||||
// 测试绑定状态判断
|
||||
const isBound = firstDevice.bandge_status === 1 || firstDevice.bandge_status === '1' ||
|
||||
firstDevice.state === 1 || firstDevice.state === '1'
|
||||
console.log('- 绑定状态判断 (bandge_status优先):', isBound)
|
||||
console.log('- 绑定状态来源:', firstDevice.bandge_status !== undefined ? 'bandge_status' : 'state')
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试搜索功能
|
||||
async function testSearchCollarDevices() {
|
||||
try {
|
||||
console.log('\n测试搜索智能项圈设备...')
|
||||
|
||||
const response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: '2409' // 搜索包含2409的设备
|
||||
}
|
||||
})
|
||||
|
||||
console.log('搜索API响应状态:', response.status)
|
||||
console.log('搜索结果数量:', response.data?.data?.length || 0)
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索API调用失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试绑定状态判断
|
||||
async function testBindingStatus() {
|
||||
try {
|
||||
console.log('\n测试绑定状态判断...')
|
||||
|
||||
const response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 20
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
const devices = response.data.data
|
||||
console.log(`分析 ${devices.length} 个设备的绑定状态:`)
|
||||
|
||||
let boundCount = 0
|
||||
let unboundCount = 0
|
||||
|
||||
devices.forEach((device, index) => {
|
||||
const isBound = device.bandge_status === 1 || device.bandge_status === '1' ||
|
||||
device.state === 1 || device.state === '1'
|
||||
|
||||
if (isBound) {
|
||||
boundCount++
|
||||
} else {
|
||||
unboundCount++
|
||||
}
|
||||
|
||||
console.log(`设备 ${index + 1}: ${device.sn || device.deviceId}`)
|
||||
console.log(` - bandge_status: ${device.bandge_status}`)
|
||||
console.log(` - state: ${device.state}`)
|
||||
console.log(` - 绑定状态: ${isBound ? '已绑定' : '未绑定'}`)
|
||||
console.log(` - 状态来源: ${device.bandge_status !== undefined ? 'bandge_status' : 'state'}`)
|
||||
console.log('')
|
||||
})
|
||||
|
||||
console.log(`统计结果:`)
|
||||
console.log(`- 已绑定设备: ${boundCount} 个`)
|
||||
console.log(`- 未绑定设备: ${unboundCount} 个`)
|
||||
console.log(`- 总设备数: ${devices.length} 个`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('绑定状态测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
async function runTests() {
|
||||
console.log('开始测试智能项圈API...')
|
||||
console.log('='.repeat(50))
|
||||
|
||||
await testGetCollarDevices()
|
||||
await testSearchCollarDevices()
|
||||
await testBindingStatus()
|
||||
|
||||
console.log('\n测试完成!')
|
||||
}
|
||||
|
||||
// 如果直接运行此文件
|
||||
if (require.main === module) {
|
||||
runTests().catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testGetCollarDevices,
|
||||
testSearchCollarDevices,
|
||||
testBindingStatus,
|
||||
runTests
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// 测试智能项圈分页和搜索功能
|
||||
const axios = require('axios')
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 测试分页功能
|
||||
async function testPagination() {
|
||||
try {
|
||||
console.log('测试分页功能...')
|
||||
console.log('='.repeat(40))
|
||||
|
||||
// 测试第一页
|
||||
console.log('\n1. 测试第一页 (page=1, limit=5)')
|
||||
let response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: 1, limit: 5 }
|
||||
})
|
||||
|
||||
console.log('响应状态:', response.status)
|
||||
console.log('数据数量:', response.data?.data?.length || 0)
|
||||
console.log('分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.pagination) {
|
||||
const { current, pageSize, total, totalPages } = response.data.pagination
|
||||
console.log(`第 ${current} 页,每页 ${pageSize} 条,共 ${total} 条,${totalPages} 页`)
|
||||
|
||||
// 测试第二页
|
||||
if (totalPages > 1) {
|
||||
console.log('\n2. 测试第二页 (page=2, limit=5)')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: 2, limit: 5 }
|
||||
})
|
||||
|
||||
console.log('响应状态:', response.status)
|
||||
console.log('数据数量:', response.data?.data?.length || 0)
|
||||
console.log('分页信息:', response.data?.pagination)
|
||||
}
|
||||
|
||||
// 测试最后一页
|
||||
if (totalPages > 2) {
|
||||
console.log('\n3. 测试最后一页 (page=' + totalPages + ', limit=5)')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: totalPages, limit: 5 }
|
||||
})
|
||||
|
||||
console.log('响应状态:', response.status)
|
||||
console.log('数据数量:', response.data?.data?.length || 0)
|
||||
console.log('分页信息:', response.data?.pagination)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('分页测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试搜索功能
|
||||
async function testSearch() {
|
||||
try {
|
||||
console.log('\n\n测试搜索功能...')
|
||||
console.log('='.repeat(40))
|
||||
|
||||
// 测试精确搜索
|
||||
console.log('\n1. 测试精确搜索项圈编号')
|
||||
let response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: '22012000108' // 使用图片中显示的项圈编号
|
||||
}
|
||||
})
|
||||
|
||||
console.log('搜索响应状态:', response.status)
|
||||
console.log('搜索结果数量:', response.data?.data?.length || 0)
|
||||
console.log('搜索分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.data?.length > 0) {
|
||||
const device = response.data.data[0]
|
||||
console.log('找到的设备信息:')
|
||||
console.log('- 项圈编号:', device.sn || device.deviceId)
|
||||
console.log('- 电量:', device.voltage)
|
||||
console.log('- 温度:', device.temperature)
|
||||
console.log('- 绑定状态:', device.bandge_status || device.state)
|
||||
}
|
||||
|
||||
// 测试模糊搜索
|
||||
console.log('\n2. 测试模糊搜索 (搜索包含"2201"的设备)')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: '2201'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('模糊搜索响应状态:', response.status)
|
||||
console.log('模糊搜索结果数量:', response.data?.data?.length || 0)
|
||||
console.log('模糊搜索分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.data?.length > 0) {
|
||||
console.log('匹配的设备编号:')
|
||||
response.data.data.forEach((device, index) => {
|
||||
console.log(` ${index + 1}. ${device.sn || device.deviceId}`)
|
||||
})
|
||||
}
|
||||
|
||||
// 测试无结果搜索
|
||||
console.log('\n3. 测试无结果搜索 (搜索不存在的编号)')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
search: '99999999999'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('无结果搜索响应状态:', response.status)
|
||||
console.log('无结果搜索数量:', response.data?.data?.length || 0)
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试搜索分页
|
||||
async function testSearchPagination() {
|
||||
try {
|
||||
console.log('\n\n测试搜索分页功能...')
|
||||
console.log('='.repeat(40))
|
||||
|
||||
// 先搜索获取结果
|
||||
console.log('\n1. 搜索获取结果')
|
||||
let response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 3, // 使用较小的分页大小便于测试
|
||||
search: '2201'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('搜索响应状态:', response.status)
|
||||
console.log('搜索结果数量:', response.data?.data?.length || 0)
|
||||
console.log('搜索分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.pagination && response.data.pagination.totalPages > 1) {
|
||||
const { totalPages } = response.data.pagination
|
||||
|
||||
// 测试搜索结果的第二页
|
||||
console.log('\n2. 测试搜索结果第二页')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 2,
|
||||
limit: 3,
|
||||
search: '2201'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('第二页响应状态:', response.status)
|
||||
console.log('第二页结果数量:', response.data?.data?.length || 0)
|
||||
console.log('第二页分页信息:', response.data?.pagination)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索分页测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runAllTests() {
|
||||
console.log('开始测试智能项圈分页和搜索功能...')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
await testPagination()
|
||||
await testSearch()
|
||||
await testSearchPagination()
|
||||
|
||||
console.log('\n\n所有测试完成!')
|
||||
console.log('='.repeat(60))
|
||||
}
|
||||
|
||||
// 如果直接运行此文件
|
||||
if (require.main === module) {
|
||||
runAllTests().catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testPagination,
|
||||
testSearch,
|
||||
testSearchPagination,
|
||||
runAllTests
|
||||
}
|
||||
146
mini_program/farm-monitor-dashboard/test-pagination-fix.js
Normal file
146
mini_program/farm-monitor-dashboard/test-pagination-fix.js
Normal file
@@ -0,0 +1,146 @@
|
||||
// 测试分页修复
|
||||
const axios = require('axios')
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:5350',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 测试分页数据
|
||||
async function testPaginationData() {
|
||||
try {
|
||||
console.log('测试分页数据修复...')
|
||||
console.log('='.repeat(50))
|
||||
|
||||
// 测试第一页
|
||||
console.log('\n1. 测试第一页数据')
|
||||
let response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: 1, limit: 5 }
|
||||
})
|
||||
|
||||
console.log('API响应状态:', response.status)
|
||||
console.log('原始API响应分页信息:', response.data?.pagination)
|
||||
console.log('响应数据结构:', {
|
||||
hasData: !!response.data?.data,
|
||||
dataLength: response.data?.data?.length || 0,
|
||||
hasPagination: !!response.data?.pagination,
|
||||
pagination: response.data?.pagination
|
||||
})
|
||||
|
||||
if (response.data?.pagination) {
|
||||
const { current, pageSize, total, totalPages } = response.data.pagination
|
||||
console.log('分页信息验证:')
|
||||
console.log('- current:', current, typeof current)
|
||||
console.log('- pageSize:', pageSize, typeof pageSize)
|
||||
console.log('- total:', total, typeof total)
|
||||
console.log('- totalPages:', totalPages, typeof totalPages)
|
||||
|
||||
// 计算分页显示信息
|
||||
const start = (current - 1) * pageSize + 1
|
||||
const end = Math.min(current * pageSize, total)
|
||||
console.log('分页显示信息:', `第 ${start}-${end} 条,共 ${total} 条`)
|
||||
|
||||
// 检查是否有NaN
|
||||
if (isNaN(start) || isNaN(end) || isNaN(total)) {
|
||||
console.error('❌ 发现NaN值!')
|
||||
console.error('- start:', start, 'isNaN:', isNaN(start))
|
||||
console.error('- end:', end, 'isNaN:', isNaN(end))
|
||||
console.error('- total:', total, 'isNaN:', isNaN(total))
|
||||
} else {
|
||||
console.log('✅ 分页计算正常,无NaN值')
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 缺少分页信息')
|
||||
}
|
||||
|
||||
// 测试第三页(模拟API返回page: 3的情况)
|
||||
console.log('\n2. 测试第三页数据(模拟API返回page: 3)')
|
||||
response = await api.get('/api/smart-devices/collars', {
|
||||
params: { page: 3, limit: 5 }
|
||||
})
|
||||
|
||||
console.log('第三页响应状态:', response.status)
|
||||
console.log('原始API响应分页信息:', response.data?.pagination)
|
||||
console.log('第三页分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.pagination) {
|
||||
const { current, pageSize, total } = response.data.pagination
|
||||
console.log('字段映射验证:')
|
||||
console.log('- API返回的page字段:', response.data?.pagination?.page || 'undefined')
|
||||
console.log('- 映射后的current字段:', current)
|
||||
console.log('- 是否匹配:', (response.data?.pagination?.page || 3) === current)
|
||||
|
||||
const start = (current - 1) * pageSize + 1
|
||||
const end = Math.min(current * pageSize, total)
|
||||
console.log('第三页显示信息:', `第 ${start}-${end} 条,共 ${total} 条`)
|
||||
|
||||
// 验证分页高亮是否正确
|
||||
if (current === 3) {
|
||||
console.log('✅ 分页高亮应该显示第3页')
|
||||
} else {
|
||||
console.log('❌ 分页高亮显示错误,应该是第3页,实际是第' + current + '页')
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试搜索分页
|
||||
async function testSearchPagination() {
|
||||
try {
|
||||
console.log('\n\n测试搜索分页...')
|
||||
console.log('='.repeat(50))
|
||||
|
||||
// 测试搜索
|
||||
const response = await api.get('/api/smart-devices/collars', {
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 3,
|
||||
search: '1501'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('搜索响应状态:', response.status)
|
||||
console.log('搜索结果数量:', response.data?.data?.length || 0)
|
||||
console.log('搜索分页信息:', response.data?.pagination)
|
||||
|
||||
if (response.data?.pagination) {
|
||||
const { current, pageSize, total } = response.data.pagination
|
||||
const start = (current - 1) * pageSize + 1
|
||||
const end = Math.min(current * pageSize, total)
|
||||
console.log('搜索分页显示:', `搜索结果: 第 ${start}-${end} 条,共 ${total} 条`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索分页测试失败:', error.response?.status, error.response?.data || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
async function runTests() {
|
||||
console.log('开始测试分页修复...')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
await testPaginationData()
|
||||
await testSearchPagination()
|
||||
|
||||
console.log('\n\n测试完成!')
|
||||
console.log('='.repeat(60))
|
||||
}
|
||||
|
||||
// 如果直接运行此文件
|
||||
if (require.main === module) {
|
||||
runTests().catch(console.error)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testPaginationData,
|
||||
testSearchPagination,
|
||||
runTests
|
||||
}
|
||||
@@ -7,17 +7,23 @@ module.exports = defineConfig({
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': require('path').resolve(__dirname, 'src')
|
||||
'@': require('path').resolve(__dirname, 'src'),
|
||||
// 强制uni-app使用CommonJS版本
|
||||
'@dcloudio/uni-app/dist/uni-app.es.js': require.resolve('@dcloudio/uni-app/dist/index.js')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 配置public目录
|
||||
publicPath: '/',
|
||||
assetsDir: 'static',
|
||||
|
||||
// 开发服务器配置
|
||||
devServer: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:5350',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/api': '/api'
|
||||
@@ -30,7 +36,7 @@ module.exports = defineConfig({
|
||||
css: {
|
||||
loaderOptions: {
|
||||
scss: {
|
||||
additionalData: `@import "@/uni.scss";`
|
||||
// 样式变量已在app.scss中导入
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user