完善保险端前后端和养殖端小程序
This commit is contained in:
371
mini_program/farm-monitor-dashboard/CONVERSION_GUIDE.md
Normal file
371
mini_program/farm-monitor-dashboard/CONVERSION_GUIDE.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 项目转换指南
|
||||
|
||||
## 转换概述
|
||||
|
||||
本项目已从uni-app技术栈转换为微信小程序原生技术栈。以下是详细的转换说明和注意事项。
|
||||
|
||||
## 技术栈对比
|
||||
|
||||
| 项目 | uni-app | 微信小程序原生 |
|
||||
|------|---------|----------------|
|
||||
| 框架 | Vue.js + uni-app | 微信小程序原生 |
|
||||
| 模板 | Vue单文件组件 | WXML |
|
||||
| 样式 | SCSS/CSS | WXSS |
|
||||
| 逻辑 | Vue.js | JavaScript ES6+ |
|
||||
| 路由 | Vue Router | 微信小程序路由 |
|
||||
| 状态管理 | Pinia | 微信小程序全局数据 |
|
||||
| 网络请求 | axios | wx.request |
|
||||
| 组件库 | uView UI | 微信小程序原生组件 |
|
||||
|
||||
## 主要转换内容
|
||||
|
||||
### 1. 项目结构转换
|
||||
|
||||
#### 原uni-app结构
|
||||
```
|
||||
src/
|
||||
├── App.vue
|
||||
├── main.js
|
||||
├── pages/
|
||||
├── components/
|
||||
├── services/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
#### 转换后微信小程序结构
|
||||
```
|
||||
├── app.js
|
||||
├── app.json
|
||||
├── app.wxss
|
||||
├── pages/
|
||||
├── services/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
### 2. 页面转换
|
||||
|
||||
#### Vue单文件组件 → 微信小程序页面
|
||||
- `.vue` → `.js` + `.wxml` + `.wxss`
|
||||
- `template` → `.wxml`
|
||||
- `script` → `.js`
|
||||
- `style` → `.wxss`
|
||||
|
||||
#### 示例对比
|
||||
|
||||
**Vue组件 (原)**
|
||||
```vue
|
||||
<template>
|
||||
<view class="container">
|
||||
<text>{{ message }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
message: 'Hello World'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**微信小程序页面 (转换后)**
|
||||
```javascript
|
||||
// .js
|
||||
Page({
|
||||
data: {
|
||||
message: 'Hello World'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- .wxml -->
|
||||
<view class="container">
|
||||
<text>{{message}}</text>
|
||||
</view>
|
||||
```
|
||||
|
||||
```css
|
||||
/* .wxss */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 路由转换
|
||||
|
||||
#### uni-app路由
|
||||
```javascript
|
||||
// 页面跳转
|
||||
uni.navigateTo({ url: '/pages/detail/detail' })
|
||||
|
||||
// 路由配置
|
||||
export default new VueRouter({
|
||||
routes: [
|
||||
{ path: '/detail', component: Detail }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#### 微信小程序路由
|
||||
```javascript
|
||||
// 页面跳转
|
||||
wx.navigateTo({ url: '/pages/detail/detail' })
|
||||
|
||||
// 路由配置 (app.json)
|
||||
{
|
||||
"pages": [
|
||||
"pages/detail/detail"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 状态管理转换
|
||||
|
||||
#### uni-app (Pinia)
|
||||
```javascript
|
||||
// store/user.js
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
userInfo: null
|
||||
}),
|
||||
actions: {
|
||||
setUserInfo(info) {
|
||||
this.userInfo = info
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 组件中使用
|
||||
const userStore = useUserStore()
|
||||
userStore.setUserInfo(userInfo)
|
||||
```
|
||||
|
||||
#### 微信小程序全局数据
|
||||
```javascript
|
||||
// app.js
|
||||
App({
|
||||
globalData: {
|
||||
userInfo: null
|
||||
},
|
||||
setUserInfo(info) {
|
||||
this.globalData.userInfo = info
|
||||
}
|
||||
})
|
||||
|
||||
// 页面中使用
|
||||
const app = getApp()
|
||||
app.setUserInfo(userInfo)
|
||||
```
|
||||
|
||||
### 5. 网络请求转换
|
||||
|
||||
#### uni-app (axios)
|
||||
```javascript
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
api.get('/users').then(res => {
|
||||
console.log(res.data)
|
||||
})
|
||||
```
|
||||
|
||||
#### 微信小程序 (wx.request)
|
||||
```javascript
|
||||
wx.request({
|
||||
url: 'https://api.example.com/users',
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
console.log(res.data)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 6. 组件转换
|
||||
|
||||
#### Vue组件
|
||||
```vue
|
||||
<template>
|
||||
<view class="custom-component">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['title'],
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 微信小程序组件
|
||||
```javascript
|
||||
// components/custom-component.js
|
||||
Component({
|
||||
properties: {
|
||||
title: String
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.triggerEvent('click')
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- components/custom-component.wxml -->
|
||||
<view class="custom-component">
|
||||
<slot></slot>
|
||||
</view>
|
||||
```
|
||||
|
||||
## 依赖处理
|
||||
|
||||
### 移除的依赖
|
||||
- `@dcloudio/uni-app`
|
||||
- `@dcloudio/uni-cli-shared`
|
||||
- `@dcloudio/uni-h5`
|
||||
- `@dcloudio/uni-mp-weixin`
|
||||
- `@vue/composition-api`
|
||||
- `vue`
|
||||
- `vue-router`
|
||||
- `vue-template-compiler`
|
||||
- `pinia`
|
||||
- `axios`
|
||||
- `@vant/weapp`
|
||||
|
||||
### 保留的依赖
|
||||
- `dayjs` - 日期处理库
|
||||
|
||||
### 新增的依赖
|
||||
- 无(使用微信小程序原生API)
|
||||
|
||||
## 配置文件转换
|
||||
|
||||
### 1. package.json
|
||||
- 移除uni-app相关依赖
|
||||
- 更新项目描述和脚本
|
||||
- 保留必要的开发依赖
|
||||
|
||||
### 2. manifest.json → app.json
|
||||
- 页面配置迁移到app.json
|
||||
- 移除uni-app特有配置
|
||||
- 保留微信小程序配置
|
||||
|
||||
### 3. pages.json → app.json
|
||||
- 页面路由配置
|
||||
- tabBar配置
|
||||
- 全局样式配置
|
||||
|
||||
## 样式转换
|
||||
|
||||
### 1. SCSS变量 → WXSS变量
|
||||
```scss
|
||||
// 原SCSS
|
||||
$primary-color: #3cc51f;
|
||||
$font-size: 14px;
|
||||
|
||||
.container {
|
||||
color: $primary-color;
|
||||
font-size: $font-size;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* 转换后WXSS */
|
||||
.container {
|
||||
color: #3cc51f;
|
||||
font-size: 28rpx; /* 注意单位转换 */
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 响应式设计
|
||||
- 使用rpx单位替代px
|
||||
- 适配不同屏幕尺寸
|
||||
- 保持设计一致性
|
||||
|
||||
## 功能适配
|
||||
|
||||
### 1. 生命周期
|
||||
- Vue生命周期 → 微信小程序生命周期
|
||||
- `created` → `onLoad`
|
||||
- `mounted` → `onReady`
|
||||
- `destroyed` → `onUnload`
|
||||
|
||||
### 2. 事件处理
|
||||
- Vue事件 → 微信小程序事件
|
||||
- `@click` → `bindtap`
|
||||
- `@input` → `bindinput`
|
||||
|
||||
### 3. 数据绑定
|
||||
- Vue数据绑定 → 微信小程序数据绑定
|
||||
- `v-model` → `value` + `bindinput`
|
||||
- `v-for` → `wx:for`
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 兼容性
|
||||
- 微信小程序API限制
|
||||
- 网络请求域名配置
|
||||
- 图片资源大小限制
|
||||
|
||||
### 2. 性能优化
|
||||
- 避免频繁setData
|
||||
- 合理使用分包
|
||||
- 图片懒加载
|
||||
|
||||
### 3. 开发调试
|
||||
- 使用微信开发者工具
|
||||
- 真机调试测试
|
||||
- 控制台日志查看
|
||||
|
||||
## 迁移检查清单
|
||||
|
||||
- [ ] 页面结构转换完成
|
||||
- [ ] 样式适配完成
|
||||
- [ ] 逻辑代码转换完成
|
||||
- [ ] 路由配置正确
|
||||
- [ ] 网络请求正常
|
||||
- [ ] 组件功能正常
|
||||
- [ ] 状态管理正确
|
||||
- [ ] 生命周期适配
|
||||
- [ ] 事件处理正确
|
||||
- [ ] 数据绑定正常
|
||||
- [ ] 依赖清理完成
|
||||
- [ ] 配置文件更新
|
||||
- [ ] 功能测试通过
|
||||
- [ ] 性能优化完成
|
||||
|
||||
## 后续维护
|
||||
|
||||
1. 定期更新微信小程序基础库版本
|
||||
2. 关注微信小程序API更新
|
||||
3. 优化性能和用户体验
|
||||
4. 及时修复bug和问题
|
||||
5. 保持代码质量和规范
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有转换相关问题,请参考:
|
||||
- 微信小程序官方文档
|
||||
- 项目README文档
|
||||
- 代码注释和说明
|
||||
@@ -1,282 +1,229 @@
|
||||
# 智慧养殖微信小程序
|
||||
# 养殖管理系统微信小程序
|
||||
|
||||
基于uni-app开发的养殖管理微信小程序,提供牛只管理、养殖场管理、配种记录、医疗记录等功能。
|
||||
## 项目简介
|
||||
|
||||
这是一个基于微信小程序原生技术开发的养殖管理系统,用于管理牛只档案、设备监控、预警管理等养殖业务。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**: Vue 3.x + Composition API
|
||||
- **UI组件库**: Vant Weapp
|
||||
- **构建工具**: Vue CLI + uni-app
|
||||
- **状态管理**: Pinia
|
||||
- **HTTP客户端**: Axios
|
||||
- **样式预处理**: SCSS
|
||||
- **开发环境**: Node.js 16.20.2
|
||||
- **框架**: 微信小程序原生开发
|
||||
- **语言**: JavaScript ES6+
|
||||
- **样式**: WXSS
|
||||
- **模板**: WXML
|
||||
- **状态管理**: 微信小程序全局数据
|
||||
- **网络请求**: wx.request
|
||||
- **UI组件**: 微信小程序原生组件
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
farm-mini-program/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── index/ # 首页
|
||||
│ │ ├── login/ # 登录页
|
||||
│ │ └── ...
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── services/ # API服务
|
||||
│ │ ├── api.js # HTTP请求封装
|
||||
│ │ ├── authService.js # 认证服务
|
||||
│ │ └── homeService.js # 首页服务
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── auth.js # 认证工具
|
||||
│ │ └── index.js # 通用工具
|
||||
│ ├── App.vue # 应用入口
|
||||
│ └── main.js # 应用配置
|
||||
├── static/ # 静态资源
|
||||
├── uni.scss # 全局样式变量
|
||||
├── pages.json # 页面配置
|
||||
├── manifest.json # 应用配置
|
||||
├── package.json # 项目依赖
|
||||
├── vue.config.js # Vue配置
|
||||
└── README.md # 项目说明
|
||||
farm-monitor-dashboard/
|
||||
├── app.js # 小程序入口文件
|
||||
├── app.json # 小程序全局配置
|
||||
├── app.wxss # 小程序全局样式
|
||||
├── sitemap.json # 站点地图配置
|
||||
├── project.config.json # 项目配置文件
|
||||
├── package.json # 项目依赖配置
|
||||
├── pages/ # 页面目录
|
||||
│ ├── home/ # 首页
|
||||
│ │ ├── home.js
|
||||
│ │ ├── home.wxml
|
||||
│ │ └── home.wxss
|
||||
│ ├── login/ # 登录页
|
||||
│ │ ├── login.js
|
||||
│ │ ├── login.wxml
|
||||
│ │ └── login.wxss
|
||||
│ ├── cattle/ # 牛只管理
|
||||
│ │ ├── cattle.js
|
||||
│ │ ├── cattle.wxml
|
||||
│ │ └── cattle.wxss
|
||||
│ ├── device/ # 设备管理
|
||||
│ │ ├── device.js
|
||||
│ │ ├── device.wxml
|
||||
│ │ └── device.wxss
|
||||
│ ├── alert/ # 预警中心
|
||||
│ │ ├── alert.js
|
||||
│ │ ├── alert.wxml
|
||||
│ │ └── alert.wxss
|
||||
│ └── profile/ # 个人中心
|
||||
│ ├── profile.js
|
||||
│ ├── profile.wxml
|
||||
│ └── profile.wxss
|
||||
├── services/ # 服务层
|
||||
│ ├── api.js # API接口定义
|
||||
│ ├── cattleService.js # 牛只管理服务
|
||||
│ ├── deviceService.js # 设备管理服务
|
||||
│ └── alertService.js # 预警管理服务
|
||||
├── utils/ # 工具函数
|
||||
│ ├── index.js # 通用工具函数
|
||||
│ ├── api.js # API请求工具
|
||||
│ └── auth.js # 认证工具
|
||||
├── images/ # 图片资源
|
||||
│ └── README.md
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 已实现功能
|
||||
- ✅ 用户登录认证(账号密码 + 微信登录)
|
||||
- ✅ 首页数据统计展示
|
||||
- ✅ 响应式布局设计
|
||||
- ✅ 全局样式系统
|
||||
- ✅ API请求封装
|
||||
- ✅ 状态管理
|
||||
- ✅ 工具函数集合
|
||||
### 1. 用户认证
|
||||
- 密码登录
|
||||
- 短信验证码登录
|
||||
- 微信授权登录
|
||||
- 自动登录状态保持
|
||||
|
||||
### 待实现功能
|
||||
- 🔄 牛只管理模块
|
||||
- 🔄 养殖场管理模块
|
||||
- 🔄 配种记录模块
|
||||
- 🔄 医疗记录模块
|
||||
- 🔄 饲喂记录模块
|
||||
- 🔄 数据统计报表
|
||||
- 🔄 消息通知系统
|
||||
### 2. 牛只管理
|
||||
- 牛只档案管理
|
||||
- 牛只信息查询和搜索
|
||||
- 牛只状态管理
|
||||
- 牛只健康记录
|
||||
- 牛只繁殖记录
|
||||
- 牛只饲喂记录
|
||||
|
||||
## 环境要求
|
||||
### 3. 设备管理
|
||||
- 智能设备监控
|
||||
- 设备状态管理
|
||||
- 设备配置管理
|
||||
- 设备历史数据查看
|
||||
- 设备实时数据监控
|
||||
|
||||
- Node.js: 16.20.2
|
||||
- npm: 8.19.4+
|
||||
- 微信开发者工具
|
||||
- MySQL 8.0+
|
||||
### 4. 预警中心
|
||||
- 智能预警管理
|
||||
- 预警类型分类
|
||||
- 预警处理流程
|
||||
- 预警统计分析
|
||||
- 预警规则配置
|
||||
|
||||
## 安装依赖
|
||||
### 5. 个人中心
|
||||
- 用户信息管理
|
||||
- 系统设置
|
||||
- 消息通知
|
||||
- 帮助中心
|
||||
|
||||
```bash
|
||||
# 使用淘宝镜像安装依赖
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
## 开发环境配置
|
||||
|
||||
# 或者使用yarn
|
||||
yarn install
|
||||
```
|
||||
### 1. 安装微信开发者工具
|
||||
下载并安装 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
|
||||
|
||||
## 开发运行
|
||||
### 2. 导入项目
|
||||
1. 打开微信开发者工具
|
||||
2. 选择"导入项目"
|
||||
3. 选择项目目录
|
||||
4. 填写AppID(测试可使用测试号)
|
||||
5. 点击"导入"
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# 构建生产版本
|
||||
npm run build:mp-weixin
|
||||
|
||||
# 检查代码规范
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
创建 `.env.development` 和 `.env.production` 文件:
|
||||
|
||||
```env
|
||||
# 开发环境
|
||||
NODE_ENV=development
|
||||
VUE_APP_BASE_URL=http://localhost:3000/api
|
||||
VUE_APP_WEIXIN_APPID=wx-your-dev-appid
|
||||
VUE_APP_DEBUG=true
|
||||
|
||||
# 生产环境
|
||||
NODE_ENV=production
|
||||
VUE_APP_BASE_URL=https://your-domain.com/api
|
||||
VUE_APP_WEIXIN_APPID=wx-your-prod-appid
|
||||
VUE_APP_DEBUG=false
|
||||
```
|
||||
|
||||
### 微信小程序配置
|
||||
|
||||
在 `manifest.json` 中配置微信小程序相关设置:
|
||||
|
||||
```json
|
||||
{
|
||||
"mp-weixin": {
|
||||
"appid": "your-wechat-appid",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API接口规范
|
||||
|
||||
### 请求格式
|
||||
### 3. 配置后端API
|
||||
在 `utils/api.js` 中修改 `baseUrl` 为实际的后端API地址:
|
||||
|
||||
```javascript
|
||||
// GET请求
|
||||
const data = await get('/api/endpoint', { params })
|
||||
|
||||
// POST请求
|
||||
const result = await post('/api/endpoint', { data })
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {},
|
||||
"message": "成功"
|
||||
const config = {
|
||||
baseUrl: 'https://your-backend-url.com/api', // 修改为实际的后端API地址
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
## 开发指南
|
||||
|
||||
- 401: 未授权,需要重新登录
|
||||
- 403: 禁止访问
|
||||
- 404: 资源不存在
|
||||
- 500: 服务器错误
|
||||
### 1. 页面开发
|
||||
每个页面包含三个文件:
|
||||
- `.js` - 页面逻辑
|
||||
- `.wxml` - 页面结构
|
||||
- `.wxss` - 页面样式
|
||||
|
||||
## 样式规范
|
||||
### 2. 组件开发
|
||||
微信小程序支持自定义组件,可以在 `components` 目录下创建组件。
|
||||
|
||||
### 颜色系统
|
||||
### 3. API调用
|
||||
使用 `utils/api.js` 中封装的请求方法:
|
||||
|
||||
使用 SCSS 变量定义颜色系统:
|
||||
```javascript
|
||||
const { get, post, put, del } = require('../../utils/api')
|
||||
|
||||
```scss
|
||||
// 主色
|
||||
$color-primary: #1890ff;
|
||||
$color-primary-light: #40a9ff;
|
||||
$color-primary-dark: #096dd9;
|
||||
// GET请求
|
||||
const data = await get('/api/endpoint', params)
|
||||
|
||||
// 功能色
|
||||
$color-success: #52c41a;
|
||||
$color-warning: #faad14;
|
||||
$color-danger: #f5222d;
|
||||
$color-info: #1890ff;
|
||||
|
||||
// 文字色
|
||||
$color-text-primary: #333333;
|
||||
$color-text-regular: #666666;
|
||||
$color-text-secondary: #999999;
|
||||
$color-text-placeholder: #cccccc;
|
||||
// POST请求
|
||||
const result = await post('/api/endpoint', data)
|
||||
```
|
||||
|
||||
### 间距系统
|
||||
### 4. 状态管理
|
||||
使用微信小程序的全局数据管理:
|
||||
|
||||
使用统一的间距变量:
|
||||
```javascript
|
||||
// 设置全局数据
|
||||
getApp().globalData.userInfo = userInfo
|
||||
|
||||
```scss
|
||||
$spacing-xs: 4rpx;
|
||||
$spacing-sm: 8rpx;
|
||||
$spacing-base: 16rpx;
|
||||
$spacing-lg: 24rpx;
|
||||
$spacing-xl: 32rpx;
|
||||
// 获取全局数据
|
||||
const userInfo = getApp().globalData.userInfo
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
### Vue组件规范
|
||||
|
||||
1. 使用 Composition API
|
||||
2. 组件命名使用 PascalCase
|
||||
3. 单文件组件结构:template -> script -> style
|
||||
4. 使用 SCSS 编写样式
|
||||
|
||||
### JavaScript规范
|
||||
|
||||
1. 使用 ES6+ 语法
|
||||
2. 使用 async/await 处理异步
|
||||
3. 使用 const/let 代替 var
|
||||
4. 使用箭头函数
|
||||
|
||||
### 提交规范
|
||||
|
||||
使用 Conventional Commits:
|
||||
|
||||
- feat: 新功能
|
||||
- fix: 修复bug
|
||||
- docs: 文档更新
|
||||
- style: 代码格式
|
||||
- refactor: 代码重构
|
||||
- test: 测试相关
|
||||
- chore: 构建过程或辅助工具变动
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 开发环境部署
|
||||
### 1. 代码审核
|
||||
1. 在微信开发者工具中点击"上传"
|
||||
2. 填写版本号和项目备注
|
||||
3. 上传代码到微信后台
|
||||
|
||||
1. 配置开发环境变量
|
||||
2. 启动开发服务器
|
||||
3. 使用微信开发者工具导入项目
|
||||
4. 配置合法域名
|
||||
### 2. 提交审核
|
||||
1. 登录微信公众平台
|
||||
2. 进入小程序管理后台
|
||||
3. 在"版本管理"中提交审核
|
||||
|
||||
### 生产环境部署
|
||||
### 3. 发布上线
|
||||
审核通过后,在版本管理页面点击"发布"即可上线。
|
||||
|
||||
1. 构建生产版本
|
||||
2. 上传到微信小程序平台
|
||||
3. 提交审核发布
|
||||
## 注意事项
|
||||
|
||||
### 1. 网络请求
|
||||
- 所有网络请求必须使用HTTPS
|
||||
- 需要在微信公众平台配置服务器域名
|
||||
- 请求超时时间建议设置为10秒
|
||||
|
||||
### 2. 图片资源
|
||||
- 图片大小建议不超过2MB
|
||||
- 支持格式:JPG、PNG、GIF
|
||||
- 建议使用CDN加速
|
||||
|
||||
### 3. 性能优化
|
||||
- 合理使用分包加载
|
||||
- 避免频繁的setData操作
|
||||
- 使用图片懒加载
|
||||
- 优化网络请求
|
||||
|
||||
### 4. 兼容性
|
||||
- 支持微信版本7.0.0及以上
|
||||
- 支持iOS 10.0和Android 5.0及以上
|
||||
- 建议在真机上测试功能
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 依赖安装失败
|
||||
### 1. 网络请求失败
|
||||
- 检查服务器域名是否已配置
|
||||
- 确认API地址是否正确
|
||||
- 检查网络连接状态
|
||||
|
||||
如果遇到依赖安装问题,尝试:
|
||||
### 2. 页面显示异常
|
||||
- 检查WXML语法是否正确
|
||||
- 确认数据绑定是否正确
|
||||
- 查看控制台错误信息
|
||||
|
||||
```bash
|
||||
# 清除npm缓存
|
||||
npm cache clean --force
|
||||
|
||||
# 删除node_modules和package-lock.json
|
||||
rm -rf node_modules package-lock.json
|
||||
|
||||
# 重新安装
|
||||
npm install
|
||||
```
|
||||
|
||||
### 微信登录配置
|
||||
|
||||
确保在微信公众平台正确配置:
|
||||
|
||||
1. 小程序AppID
|
||||
2. 服务器域名
|
||||
3. 业务域名
|
||||
4. 开发者权限
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 邮箱: your-email@example.com
|
||||
- 微信: your-wechat-id
|
||||
### 3. 样式问题
|
||||
- 检查WXSS语法是否正确
|
||||
- 确认选择器是否正确
|
||||
- 注意样式优先级
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 项目初始化
|
||||
- 基础框架搭建
|
||||
- 登录认证功能
|
||||
- 首页统计展示
|
||||
- 初始版本发布
|
||||
- 完成基础功能开发
|
||||
- 支持牛只管理、设备监控、预警管理
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系开发团队或查看相关文档:
|
||||
- 微信小程序官方文档:https://developers.weixin.qq.com/miniprogram/dev/
|
||||
- 项目文档:查看项目根目录下的文档文件
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
117
mini_program/farm-monitor-dashboard/app.js
Normal file
117
mini_program/farm-monitor-dashboard/app.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// app.js
|
||||
App({
|
||||
globalData: {
|
||||
version: '1.0.0',
|
||||
platform: 'wechat',
|
||||
isDevelopment: true,
|
||||
baseUrl: 'https://your-backend-url.com/api', // 请替换为实际的后端API地址
|
||||
userInfo: null,
|
||||
token: null
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
console.log('养殖管理系统启动')
|
||||
this.initApp()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('应用显示')
|
||||
},
|
||||
|
||||
onHide() {
|
||||
console.log('应用隐藏')
|
||||
},
|
||||
|
||||
onError(error) {
|
||||
console.error('应用错误:', error)
|
||||
},
|
||||
|
||||
// 应用初始化
|
||||
async initApp() {
|
||||
try {
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
this.globalData.token = token
|
||||
this.globalData.userInfo = userInfo
|
||||
console.log('用户已登录,token:', token)
|
||||
console.log('用户信息:', userInfo)
|
||||
} else {
|
||||
console.log('用户未登录')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
this.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
},
|
||||
|
||||
// 设置token
|
||||
setToken(token) {
|
||||
this.globalData.token = token
|
||||
wx.setStorageSync('token', token)
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo() {
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.token = null
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.removeStorageSync('token')
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
return !!(this.globalData.token && this.globalData.userInfo)
|
||||
},
|
||||
|
||||
// 显示加载提示
|
||||
showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
// 显示成功提示
|
||||
showSuccess(title = '操作成功') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示错误提示
|
||||
showError(title = '操作失败') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示确认对话框
|
||||
showConfirm(content, title = '提示') {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title: title,
|
||||
content: content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
79
mini_program/farm-monitor-dashboard/app.json
Normal file
79
mini_program/farm-monitor-dashboard/app.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/login/login",
|
||||
"pages/home/home",
|
||||
"pages/cattle/cattle",
|
||||
"pages/cattle/add/add",
|
||||
"pages/cattle/detail/detail",
|
||||
"pages/cattle/transfer/transfer",
|
||||
"pages/cattle/exit/exit",
|
||||
"pages/device/device",
|
||||
"pages/device/eartag/eartag",
|
||||
"pages/device/collar/collar",
|
||||
"pages/device/ankle/ankle",
|
||||
"pages/device/host/host",
|
||||
"pages/alert/alert",
|
||||
"pages/alert/eartag/eartag",
|
||||
"pages/alert/collar/collar",
|
||||
"pages/fence/fence",
|
||||
"pages/profile/profile"
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3cc51f",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"iconPath": "images/home.png",
|
||||
"selectedIconPath": "images/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/cattle/cattle",
|
||||
"iconPath": "images/cattle.png",
|
||||
"selectedIconPath": "images/cattle-active.png",
|
||||
"text": "牛只管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/device/device",
|
||||
"iconPath": "images/device.png",
|
||||
"selectedIconPath": "images/device-active.png",
|
||||
"text": "设备管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/alert/alert",
|
||||
"iconPath": "images/alert.png",
|
||||
"selectedIconPath": "images/alert-active.png",
|
||||
"text": "预警中心"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "images/profile.png",
|
||||
"selectedIconPath": "images/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "养殖管理系统",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8f8f8"
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
"downloadFile": 10000
|
||||
},
|
||||
"debug": true,
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于养殖场定位和地图展示"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": ["location"],
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
281
mini_program/farm-monitor-dashboard/app.wxss
Normal file
281
mini_program/farm-monitor-dashboard/app.wxss
Normal file
@@ -0,0 +1,281 @@
|
||||
/* app.wxss - 全局样式 */
|
||||
|
||||
/* 全局重置 */
|
||||
page {
|
||||
background-color: #f6f6f6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
margin: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 通用工具类 */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.mt-8 { margin-top: 8rpx; }
|
||||
.mt-16 { margin-top: 16rpx; }
|
||||
.mt-24 { margin-top: 24rpx; }
|
||||
.mt-32 { margin-top: 32rpx; }
|
||||
.mb-8 { margin-bottom: 8rpx; }
|
||||
.mb-16 { margin-bottom: 16rpx; }
|
||||
.mb-24 { margin-bottom: 24rpx; }
|
||||
.mb-32 { margin-bottom: 32rpx; }
|
||||
|
||||
.pt-8 { padding-top: 8rpx; }
|
||||
.pt-16 { padding-top: 16rpx; }
|
||||
.pt-24 { padding-top: 24rpx; }
|
||||
.pt-32 { padding-top: 32rpx; }
|
||||
.pb-8 { padding-bottom: 8rpx; }
|
||||
.pb-16 { padding-bottom: 16rpx; }
|
||||
.pb-24 { padding-bottom: 24rpx; }
|
||||
.pb-32 { padding-bottom: 32rpx; }
|
||||
|
||||
.p-8 { padding: 8rpx; }
|
||||
.p-16 { padding: 16rpx; }
|
||||
.p-24 { padding: 24rpx; }
|
||||
.p-32 { padding: 32rpx; }
|
||||
|
||||
.m-8 { margin: 8rpx; }
|
||||
.m-16 { margin: 16rpx; }
|
||||
.m-24 { margin: 24rpx; }
|
||||
.m-32 { margin: 32rpx; }
|
||||
|
||||
/* Flex布局 */
|
||||
.flex { display: flex; }
|
||||
.flex-column { flex-direction: column; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.align-center { align-items: center; }
|
||||
.align-start { align-items: flex-start; }
|
||||
.align-end { align-items: flex-end; }
|
||||
|
||||
/* 状态颜色 */
|
||||
.status-normal { color: #52c41a; }
|
||||
.status-pregnant { color: #faad14; }
|
||||
.status-sick { color: #f5222d; }
|
||||
.status-quarantine { color: #909399; }
|
||||
|
||||
/* 主题颜色 */
|
||||
.color-primary { color: #3cc51f; }
|
||||
.color-success { color: #52c41a; }
|
||||
.color-warning { color: #faad14; }
|
||||
.color-danger { color: #f5222d; }
|
||||
.color-info { color: #1890ff; }
|
||||
|
||||
.bg-primary { background-color: #3cc51f; }
|
||||
.bg-success { background-color: #52c41a; }
|
||||
.bg-warning { background-color: #faad14; }
|
||||
.bg-danger { background-color: #f5222d; }
|
||||
.bg-info { background-color: #1890ff; }
|
||||
|
||||
/* 文字颜色 */
|
||||
.text-primary { color: #303133; }
|
||||
.text-regular { color: #606266; }
|
||||
.text-secondary { color: #909399; }
|
||||
.text-placeholder { color: #c0c4cc; }
|
||||
|
||||
/* 背景颜色 */
|
||||
.bg-white { background-color: #ffffff; }
|
||||
.bg-gray { background-color: #f5f5f5; }
|
||||
.bg-light { background-color: #fafafa; }
|
||||
|
||||
/* 边框 */
|
||||
.border { border: 1rpx solid #dcdfe6; }
|
||||
.border-top { border-top: 1rpx solid #dcdfe6; }
|
||||
.border-bottom { border-bottom: 1rpx solid #dcdfe6; }
|
||||
.border-left { border-left: 1rpx solid #dcdfe6; }
|
||||
.border-right { border-right: 1rpx solid #dcdfe6; }
|
||||
|
||||
/* 圆角 */
|
||||
.rounded { border-radius: 4rpx; }
|
||||
.rounded-sm { border-radius: 2rpx; }
|
||||
.rounded-lg { border-radius: 8rpx; }
|
||||
.rounded-xl { border-radius: 12rpx; }
|
||||
.rounded-full { border-radius: 50%; }
|
||||
|
||||
/* 阴影 */
|
||||
.shadow { box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); }
|
||||
.shadow-sm { box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.05); }
|
||||
.shadow-lg { box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.15); }
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #52c41a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-success:active {
|
||||
background-color: #389e0d;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #faad14;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-warning:active {
|
||||
background-color: #d48806;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #f5222d;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:active {
|
||||
background-color: #cf1322;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #ffffff;
|
||||
color: #303133;
|
||||
border: 1rpx solid #dcdfe6;
|
||||
}
|
||||
|
||||
.btn-default:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 24rpx 48rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64rpx 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.container {
|
||||
margin: 8rpx;
|
||||
padding: 12rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.card-header,
|
||||
.card-body,
|
||||
.card-footer {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
padding: 16rpx;
|
||||
}
|
||||
}
|
||||
70
mini_program/farm-monitor-dashboard/debug-console.js
Normal file
70
mini_program/farm-monitor-dashboard/debug-console.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// 在浏览器控制台中运行此脚本来调试API调用
|
||||
|
||||
// 1. 测试登录
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('登录结果:', data);
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
console.log('Token已保存到localStorage');
|
||||
return data.token;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 测试转栏记录API
|
||||
async function testTransferRecords() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
console.log('请先运行 testLogin() 获取token');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/cattle-transfer-records?page=1&pageSize=10', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('转栏记录结果:', data);
|
||||
|
||||
if (data.success) {
|
||||
console.log('记录数量:', data.data.list.length);
|
||||
console.log('第一条记录:', data.data.list[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取转栏记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 完整测试流程
|
||||
async function fullTest() {
|
||||
console.log('开始完整测试...');
|
||||
await testLogin();
|
||||
await testTransferRecords();
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
console.log('调试脚本已加载,请运行以下命令:');
|
||||
console.log('1. testLogin() - 测试登录');
|
||||
console.log('2. testTransferRecords() - 测试转栏记录');
|
||||
console.log('3. fullTest() - 完整测试流程');
|
||||
32
mini_program/farm-monitor-dashboard/images/README.md
Normal file
32
mini_program/farm-monitor-dashboard/images/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 图片资源说明
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
images/
|
||||
├── tabbar/ # 底部导航栏图标
|
||||
│ ├── home.png
|
||||
│ ├── home-active.png
|
||||
│ ├── cattle.png
|
||||
│ ├── cattle-active.png
|
||||
│ ├── device.png
|
||||
│ ├── device-active.png
|
||||
│ ├── alert.png
|
||||
│ ├── alert-active.png
|
||||
│ ├── profile.png
|
||||
│ └── profile-active.png
|
||||
├── common/ # 通用图标
|
||||
│ ├── default-avatar.png
|
||||
│ ├── empty-state.png
|
||||
│ └── loading.gif
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 图标规格
|
||||
- 底部导航栏图标:81x81px
|
||||
- 通用图标:根据实际需要调整尺寸
|
||||
- 格式:PNG(支持透明背景)
|
||||
|
||||
## 注意事项
|
||||
1. 所有图标都应该有对应的激活状态图标
|
||||
2. 图标颜色应该与主题色保持一致
|
||||
3. 建议使用矢量图标,确保在不同分辨率下显示清晰
|
||||
@@ -1,41 +1,38 @@
|
||||
{
|
||||
"name": "farm-mini-program",
|
||||
"name": "farm-monitor-dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "养殖端微信小程序 - 基于Vue.js和Node.js 16.20.2",
|
||||
"main": "main.js",
|
||||
"description": "养殖端微信小程序 - 原生版本",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"serve": "cross-env VUE_APP_BASE_URL=/api vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"dev:h5": "cross-env VUE_APP_BASE_URL=/api vue-cli-service serve --mode development",
|
||||
"build:h5": "vue-cli-service build --mode production"
|
||||
"dev": "echo '请在微信开发者工具中打开项目进行开发'",
|
||||
"build": "echo '请在微信开发者工具中构建项目'",
|
||||
"test": "echo '测试功能'",
|
||||
"lint": "echo '代码检查'"
|
||||
},
|
||||
"keywords": [
|
||||
"微信小程序",
|
||||
"养殖管理",
|
||||
"物联网",
|
||||
"智能设备",
|
||||
"农业科技"
|
||||
],
|
||||
"author": "养殖管理系统开发团队",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/your-org/farm-monitor-dashboard.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/your-org/farm-monitor-dashboard/issues"
|
||||
},
|
||||
"homepage": "https://github.com/your-org/farm-monitor-dashboard#readme",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "^2.0.2-alpha-4080120250905001",
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"axios": "^0.27.2",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.0",
|
||||
"express": "^5.1.0",
|
||||
"pinia": "^2.1.6",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
"dayjs": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
"eslint": "^8.45.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
398
mini_program/farm-monitor-dashboard/pages/alert/alert.js
Normal file
398
mini_program/farm-monitor-dashboard/pages/alert/alert.js
Normal file
@@ -0,0 +1,398 @@
|
||||
// pages/alert/alert.js
|
||||
const { get, post } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
alertList: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
typeFilter: 'all',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
alertTypes: [
|
||||
{ value: 'eartag', label: '耳标预警', icon: '🏷️' },
|
||||
{ value: 'collar', label: '项圈预警', icon: '📱' },
|
||||
{ value: 'fence', label: '围栏预警', icon: '🚧' },
|
||||
{ value: 'health', label: '健康预警', icon: '🏥' }
|
||||
],
|
||||
alertStatuses: [
|
||||
{ value: 'pending', label: '待处理', color: '#faad14' },
|
||||
{ value: 'processing', label: '处理中', color: '#1890ff' },
|
||||
{ value: 'resolved', label: '已解决', color: '#52c41a' },
|
||||
{ value: 'ignored', label: '已忽略', color: '#909399' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreAlerts()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载预警列表
|
||||
async loadAlertList() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
type: this.data.typeFilter === 'all' ? '' : this.data.typeFilter,
|
||||
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
|
||||
}
|
||||
|
||||
if (this.data.searchKeyword) {
|
||||
params.search = this.data.searchKeyword
|
||||
}
|
||||
|
||||
const response = await get('/alerts', params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const alertList = this.data.page === 1 ? newList : [...this.data.alertList, ...newList]
|
||||
|
||||
this.setData({
|
||||
alertList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: alertList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预警列表失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更多预警
|
||||
async loadMoreAlerts() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
|
||||
await this.loadAlertList()
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 类型筛选
|
||||
onTypeFilter(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
typeFilter: type,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 状态筛选
|
||||
onStatusFilter(e) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
this.setData({
|
||||
statusFilter: status,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 查看预警详情
|
||||
viewAlertDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
let url = ''
|
||||
switch (type) {
|
||||
case 'eartag':
|
||||
url = `/pages/alert/eartag/eartag?id=${id}`
|
||||
break
|
||||
case 'collar':
|
||||
url = `/pages/alert/collar/collar?id=${id}`
|
||||
break
|
||||
case 'fence':
|
||||
url = `/pages/alert/fence/fence?id=${id}`
|
||||
break
|
||||
case 'health':
|
||||
url = `/pages/alert/health/health?id=${id}`
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '未知预警类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
|
||||
// 处理预警
|
||||
async handleAlert(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
const result = await wx.showModal({
|
||||
title: '处理预警',
|
||||
content: '确定要处理这个预警吗?',
|
||||
confirmText: '处理',
|
||||
confirmColor: '#3cc51f'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '处理中...' })
|
||||
|
||||
const response = await post(`/alerts/${id}/handle`, {
|
||||
type: type,
|
||||
action: 'process'
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理预警失败:', error)
|
||||
wx.showToast({
|
||||
title: '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 忽略预警
|
||||
async ignoreAlert(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
const result = await wx.showModal({
|
||||
title: '忽略预警',
|
||||
content: '确定要忽略这个预警吗?',
|
||||
confirmText: '忽略',
|
||||
confirmColor: '#909399'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '忽略中...' })
|
||||
|
||||
const response = await post(`/alerts/${id}/handle`, {
|
||||
type: type,
|
||||
action: 'ignore'
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '已忽略',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('忽略预警失败:', error)
|
||||
wx.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 批量处理预警
|
||||
async batchHandleAlerts() {
|
||||
const selectedAlerts = this.data.alertList.filter(alert => alert.selected)
|
||||
|
||||
if (selectedAlerts.length === 0) {
|
||||
wx.showToast({
|
||||
title: '请选择要处理的预警',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await wx.showModal({
|
||||
title: '批量处理',
|
||||
content: `确定要处理选中的${selectedAlerts.length}个预警吗?`,
|
||||
confirmText: '处理',
|
||||
confirmColor: '#3cc51f'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '处理中...' })
|
||||
|
||||
const alertIds = selectedAlerts.map(alert => alert.id)
|
||||
const response = await post('/alerts/batch-handle', {
|
||||
alertIds: alertIds,
|
||||
action: 'process'
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
alertList: []
|
||||
})
|
||||
this.loadAlertList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量处理预警失败:', error)
|
||||
wx.showToast({
|
||||
title: '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取预警类型信息
|
||||
getAlertTypeInfo(type) {
|
||||
return this.data.alertTypes.find(item => item.value === type) || { label: '未知', icon: '❓' }
|
||||
},
|
||||
|
||||
// 获取状态信息
|
||||
getStatusInfo(status) {
|
||||
return this.data.alertStatuses.find(item => item.value === status) || { label: '未知', color: '#909399' }
|
||||
},
|
||||
|
||||
// 获取优先级文本
|
||||
getPriorityText(priority) {
|
||||
const priorityMap = {
|
||||
'low': '低',
|
||||
'medium': '中',
|
||||
'high': '高',
|
||||
'urgent': '紧急'
|
||||
}
|
||||
return priorityMap[priority] || '未知'
|
||||
},
|
||||
|
||||
// 获取优先级颜色
|
||||
getPriorityColor(priority) {
|
||||
const colorMap = {
|
||||
'low': '#52c41a',
|
||||
'medium': '#faad14',
|
||||
'high': '#f5222d',
|
||||
'urgent': '#722ed1'
|
||||
}
|
||||
return colorMap[priority] || '#909399'
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
}
|
||||
})
|
||||
156
mini_program/farm-monitor-dashboard/pages/alert/alert.wxml
Normal file
156
mini_program/farm-monitor-dashboard/pages/alert/alert.wxml
Normal file
@@ -0,0 +1,156 @@
|
||||
<!--pages/alert/alert.wxml-->
|
||||
<view class="alert-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrapper">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索预警内容..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<text class="search-icon" bindtap="onSearch">🔍</text>
|
||||
</view>
|
||||
<text wx:if="{{searchKeyword}}" class="clear-btn" bindtap="onClearSearch">清空</text>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<!-- 预警类型筛选 -->
|
||||
<view class="filter-group">
|
||||
<view class="filter-label">类型:</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{typeFilter === 'all' ? 'active' : ''}}"
|
||||
bindtap="onTypeFilter"
|
||||
data-type="all"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
wx:for="{{alertTypes}}"
|
||||
wx:key="value"
|
||||
class="filter-option {{typeFilter === item.value ? 'active' : ''}}"
|
||||
bindtap="onTypeFilter"
|
||||
data-type="{{item.value}}"
|
||||
>
|
||||
{{item.icon}} {{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<view class="filter-group">
|
||||
<view class="filter-label">状态:</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{statusFilter === 'all' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="all"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
wx:for="{{alertStatuses}}"
|
||||
wx:key="value"
|
||||
class="filter-option {{statusFilter === item.value ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="{{item.value}}"
|
||||
>
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<view wx:if="{{alertList.length > 0}}" class="batch-actions">
|
||||
<button class="batch-btn" bindtap="batchHandleAlerts">批量处理</button>
|
||||
</view>
|
||||
|
||||
<!-- 预警列表 -->
|
||||
<view class="alert-list">
|
||||
<view
|
||||
wx:for="{{alertList}}"
|
||||
wx:key="id"
|
||||
class="alert-item"
|
||||
bindtap="viewAlertDetail"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.type}}"
|
||||
>
|
||||
<view class="alert-icon">
|
||||
<text class="icon">{{getAlertTypeInfo(item.type).icon}}</text>
|
||||
</view>
|
||||
|
||||
<view class="alert-info">
|
||||
<view class="alert-title">{{item.title}}</view>
|
||||
<view class="alert-content">{{item.content}}</view>
|
||||
<view class="alert-meta">
|
||||
<text class="meta-item">设备: {{item.deviceName || '未知'}}</text>
|
||||
<text class="meta-item">时间: {{formatTime(item.createTime)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-status">
|
||||
<view
|
||||
class="priority-badge"
|
||||
style="background-color: {{getPriorityColor(item.priority)}}"
|
||||
>
|
||||
{{getPriorityText(item.priority)}}
|
||||
</view>
|
||||
<view
|
||||
class="status-badge"
|
||||
style="background-color: {{getStatusInfo(item.status).color}}"
|
||||
>
|
||||
{{getStatusInfo(item.status).label}}
|
||||
</view>
|
||||
<view class="alert-actions">
|
||||
<text
|
||||
wx:if="{{item.status === 'pending'}}"
|
||||
class="action-btn handle"
|
||||
bindtap="handleAlert"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.type}}"
|
||||
catchtap="true"
|
||||
>
|
||||
处理
|
||||
</text>
|
||||
<text
|
||||
wx:if="{{item.status === 'pending'}}"
|
||||
class="action-btn ignore"
|
||||
bindtap="ignoreAlert"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.type}}"
|
||||
catchtap="true"
|
||||
>
|
||||
忽略
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{alertList.length === 0 && !loading}}" class="empty-state">
|
||||
<text class="empty-icon">🚨</text>
|
||||
<text class="empty-text">暂无预警数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{hasMore && alertList.length > 0}}" class="load-more">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:else>上拉加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view wx:if="{{!hasMore && alertList.length > 0}}" class="no-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && alertList.length === 0}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
310
mini_program/farm-monitor-dashboard/pages/alert/alert.wxss
Normal file
310
mini_program/farm-monitor-dashboard/pages/alert/alert.wxss
Normal file
@@ -0,0 +1,310 @@
|
||||
/* pages/alert/alert.wxss */
|
||||
.alert-container {
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #3cc51f;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #ffffff;
|
||||
padding: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.filter-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 8rpx 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.batch-btn {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.batch-btn:active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.alert-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
border-left: 6rpx solid #f5222d;
|
||||
}
|
||||
|
||||
.alert-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #fff2f0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alert-icon .icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
font-size: 26rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alert-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.alert-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 18rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 18rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 20rpx;
|
||||
text-align: center;
|
||||
min-width: 50rpx;
|
||||
}
|
||||
|
||||
.action-btn.handle {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-btn.ignore {
|
||||
background-color: #f5f5f5;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.alert-item {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.alert-icon .icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
249
mini_program/farm-monitor-dashboard/pages/cattle/cattle.js
Normal file
249
mini_program/farm-monitor-dashboard/pages/cattle/cattle.js
Normal file
@@ -0,0 +1,249 @@
|
||||
// pages/cattle/cattle.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
cattleList: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 获取筛选参数
|
||||
if (options.status) {
|
||||
this.setData({ statusFilter: options.status })
|
||||
}
|
||||
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
})
|
||||
this.loadCattleList().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreCattle()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载牛只列表
|
||||
async loadCattleList() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
|
||||
}
|
||||
|
||||
if (this.data.searchKeyword) {
|
||||
params.search = this.data.searchKeyword
|
||||
}
|
||||
|
||||
const response = await get('/iot-cattle/public', params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const cattleList = this.data.page === 1 ? newList : [...this.data.cattleList, ...newList]
|
||||
|
||||
this.setData({
|
||||
cattleList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: cattleList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取牛只列表失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更多牛只
|
||||
async loadMoreCattle() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
|
||||
await this.loadCattleList()
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
})
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
})
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 状态筛选
|
||||
onStatusFilter(e) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
this.setData({
|
||||
statusFilter: status,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
})
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 查看牛只详情
|
||||
viewCattleDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/detail/detail?id=${id}`
|
||||
})
|
||||
},
|
||||
|
||||
// 添加牛只
|
||||
addCattle() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/add/add'
|
||||
})
|
||||
},
|
||||
|
||||
// 编辑牛只
|
||||
editCattle(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/edit/edit?id=${id}`
|
||||
})
|
||||
},
|
||||
|
||||
// 删除牛只
|
||||
async deleteCattle(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const name = e.currentTarget.dataset.name
|
||||
|
||||
const confirmed = await wx.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除牛只"${name}"吗?`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#f5222d'
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
wx.showLoading({ title: '删除中...' })
|
||||
|
||||
const response = await del(`/iot-cattle/${id}`)
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
})
|
||||
this.loadCattleList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除牛只失败:', error)
|
||||
wx.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'normal': '正常',
|
||||
'pregnant': '怀孕',
|
||||
'sick': '生病',
|
||||
'quarantine': '隔离',
|
||||
'sold': '已售',
|
||||
'dead': '死亡'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
// 获取状态颜色
|
||||
getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'normal': '#52c41a',
|
||||
'pregnant': '#faad14',
|
||||
'sick': '#f5222d',
|
||||
'quarantine': '#909399',
|
||||
'sold': '#1890ff',
|
||||
'dead': '#666666'
|
||||
}
|
||||
return colorMap[status] || '#909399'
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
}
|
||||
})
|
||||
125
mini_program/farm-monitor-dashboard/pages/cattle/cattle.wxml
Normal file
125
mini_program/farm-monitor-dashboard/pages/cattle/cattle.wxml
Normal file
@@ -0,0 +1,125 @@
|
||||
<!--pages/cattle/cattle.wxml-->
|
||||
<view class="cattle-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrapper">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索牛只耳号、姓名..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<text class="search-icon" bindtap="onSearch">🔍</text>
|
||||
</view>
|
||||
<text wx:if="{{searchKeyword}}" class="clear-btn" bindtap="onClearSearch">清空</text>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<view class="status-filter">
|
||||
<view
|
||||
class="filter-item {{statusFilter === 'all' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="all"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{statusFilter === 'normal' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="normal"
|
||||
>
|
||||
正常
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{statusFilter === 'pregnant' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="pregnant"
|
||||
>
|
||||
怀孕
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{statusFilter === 'sick' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="sick"
|
||||
>
|
||||
生病
|
||||
</view>
|
||||
<view
|
||||
class="filter-item {{statusFilter === 'quarantine' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="quarantine"
|
||||
>
|
||||
隔离
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 牛只列表 -->
|
||||
<view class="cattle-list">
|
||||
<view
|
||||
wx:for="{{cattleList}}"
|
||||
wx:key="id"
|
||||
class="cattle-item"
|
||||
bindtap="viewCattleDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="cattle-avatar">
|
||||
<text class="avatar-icon">🐄</text>
|
||||
</view>
|
||||
|
||||
<view class="cattle-info">
|
||||
<view class="cattle-name">{{item.name || item.earNumber}}</view>
|
||||
<view class="cattle-details">
|
||||
<text class="detail-item">耳号: {{item.earNumber}}</text>
|
||||
<text class="detail-item">品种: {{item.breed || '未知'}}</text>
|
||||
</view>
|
||||
<view class="cattle-meta">
|
||||
<text class="meta-item">年龄: {{item.age || '未知'}}岁</text>
|
||||
<text class="meta-item">体重: {{item.weight || '未知'}}kg</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cattle-status">
|
||||
<view
|
||||
class="status-badge"
|
||||
style="background-color: {{getStatusColor(item.status)}}"
|
||||
>
|
||||
{{getStatusText(item.status)}}
|
||||
</view>
|
||||
<view class="cattle-actions">
|
||||
<text class="action-btn edit" bindtap="editCattle" data-id="{{item.id}}" catchtap="true">编辑</text>
|
||||
<text class="action-btn delete" bindtap="deleteCattle" data-id="{{item.id}}" data-name="{{item.name || item.earNumber}}" catchtap="true">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{cattleList.length === 0 && !loading}}" class="empty-state">
|
||||
<text class="empty-icon">🐄</text>
|
||||
<text class="empty-text">暂无牛只数据</text>
|
||||
<button class="add-btn" bindtap="addCattle">添加牛只</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{hasMore && cattleList.length > 0}}" class="load-more">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:else>上拉加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view wx:if="{{!hasMore && cattleList.length > 0}}" class="no-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="fab" bindtap="addCattle">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && cattleList.length === 0}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
314
mini_program/farm-monitor-dashboard/pages/cattle/cattle.wxss
Normal file
314
mini_program/farm-monitor-dashboard/pages/cattle/cattle.wxss
Normal file
@@ -0,0 +1,314 @@
|
||||
/* pages/cattle/cattle.wxss */
|
||||
.cattle-container {
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #3cc51f;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
padding: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
padding: 12rpx 24rpx;
|
||||
margin-right: 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cattle-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.cattle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.cattle-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.cattle-info {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.cattle-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
.cattle-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.cattle-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
text-align: center;
|
||||
min-width: 60rpx;
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background-color: #fff2f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 32rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.add-btn:active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: 120rpx;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
background-color: #3cc51f;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(60, 197, 31, 0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.cattle-item {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.fab {
|
||||
right: 24rpx;
|
||||
bottom: 100rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
}
|
||||
295
mini_program/farm-monitor-dashboard/pages/device/device.js
Normal file
295
mini_program/farm-monitor-dashboard/pages/device/device.js
Normal file
@@ -0,0 +1,295 @@
|
||||
// pages/device/device.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
deviceList: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
typeFilter: 'all',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
deviceTypes: [
|
||||
{ value: 'eartag', label: '耳标', icon: '🏷️' },
|
||||
{ value: 'collar', label: '项圈', icon: '📱' },
|
||||
{ value: 'ankle', label: '脚环', icon: '⌚' },
|
||||
{ value: 'host', label: '主机', icon: '📡' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreDevices()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载设备列表
|
||||
async loadDeviceList() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
type: this.data.typeFilter === 'all' ? '' : this.data.typeFilter,
|
||||
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
|
||||
}
|
||||
|
||||
if (this.data.searchKeyword) {
|
||||
params.search = this.data.searchKeyword
|
||||
}
|
||||
|
||||
const response = await get('/devices', params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const deviceList = this.data.page === 1 ? newList : [...this.data.deviceList, ...newList]
|
||||
|
||||
this.setData({
|
||||
deviceList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: deviceList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更多设备
|
||||
async loadMoreDevices() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
|
||||
await this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 类型筛选
|
||||
onTypeFilter(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
typeFilter: type,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 状态筛选
|
||||
onStatusFilter(e) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
this.setData({
|
||||
statusFilter: status,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 查看设备详情
|
||||
viewDeviceDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
let url = ''
|
||||
switch (type) {
|
||||
case 'eartag':
|
||||
url = `/pages/device/eartag/eartag?id=${id}`
|
||||
break
|
||||
case 'collar':
|
||||
url = `/pages/device/collar/collar?id=${id}`
|
||||
break
|
||||
case 'ankle':
|
||||
url = `/pages/device/ankle/ankle?id=${id}`
|
||||
break
|
||||
case 'host':
|
||||
url = `/pages/device/host/host?id=${id}`
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '未知设备类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
|
||||
// 添加设备
|
||||
addDevice() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['耳标', '项圈', '脚环', '主机'],
|
||||
success: (res) => {
|
||||
const types = ['eartag', 'collar', 'ankle', 'host']
|
||||
const type = types[res.tapIndex]
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/add/add?type=${type}`
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 编辑设备
|
||||
editDevice(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/edit/edit?id=${id}&type=${type}`
|
||||
})
|
||||
},
|
||||
|
||||
// 删除设备
|
||||
async deleteDevice(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const name = e.currentTarget.dataset.name
|
||||
|
||||
const confirmed = await wx.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除设备"${name}"吗?`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#f5222d'
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
wx.showLoading({ title: '删除中...' })
|
||||
|
||||
const response = await del(`/devices/${id}`)
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除设备失败:', error)
|
||||
wx.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设备类型信息
|
||||
getDeviceTypeInfo(type) {
|
||||
return this.data.deviceTypes.find(item => item.value === type) || { label: '未知', icon: '❓' }
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'online': '在线',
|
||||
'offline': '离线',
|
||||
'error': '故障',
|
||||
'maintenance': '维护中'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
// 获取状态颜色
|
||||
getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'online': '#52c41a',
|
||||
'offline': '#909399',
|
||||
'error': '#f5222d',
|
||||
'maintenance': '#faad14'
|
||||
}
|
||||
return colorMap[status] || '#909399'
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
}
|
||||
})
|
||||
148
mini_program/farm-monitor-dashboard/pages/device/device.wxml
Normal file
148
mini_program/farm-monitor-dashboard/pages/device/device.wxml
Normal file
@@ -0,0 +1,148 @@
|
||||
<!--pages/device/device.wxml-->
|
||||
<view class="device-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrapper">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索设备编号、名称..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<text class="search-icon" bindtap="onSearch">🔍</text>
|
||||
</view>
|
||||
<text wx:if="{{searchKeyword}}" class="clear-btn" bindtap="onClearSearch">清空</text>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<!-- 设备类型筛选 -->
|
||||
<view class="filter-group">
|
||||
<view class="filter-label">类型:</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{typeFilter === 'all' ? 'active' : ''}}"
|
||||
bindtap="onTypeFilter"
|
||||
data-type="all"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
wx:for="{{deviceTypes}}"
|
||||
wx:key="value"
|
||||
class="filter-option {{typeFilter === item.value ? 'active' : ''}}"
|
||||
bindtap="onTypeFilter"
|
||||
data-type="{{item.value}}"
|
||||
>
|
||||
{{item.icon}} {{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<view class="filter-group">
|
||||
<view class="filter-label">状态:</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{statusFilter === 'all' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="all"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{statusFilter === 'online' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="online"
|
||||
>
|
||||
在线
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{statusFilter === 'offline' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="offline"
|
||||
>
|
||||
离线
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{statusFilter === 'error' ? 'active' : ''}}"
|
||||
bindtap="onStatusFilter"
|
||||
data-status="error"
|
||||
>
|
||||
故障
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<view class="device-list">
|
||||
<view
|
||||
wx:for="{{deviceList}}"
|
||||
wx:key="id"
|
||||
class="device-item"
|
||||
bindtap="viewDeviceDetail"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.type}}"
|
||||
>
|
||||
<view class="device-icon">
|
||||
<text class="icon">{{getDeviceTypeInfo(item.type).icon}}</text>
|
||||
</view>
|
||||
|
||||
<view class="device-info">
|
||||
<view class="device-name">{{item.name || item.deviceNumber}}</view>
|
||||
<view class="device-details">
|
||||
<text class="detail-item">编号: {{item.deviceNumber}}</text>
|
||||
<text class="detail-item">类型: {{getDeviceTypeInfo(item.type).label}}</text>
|
||||
</view>
|
||||
<view class="device-meta">
|
||||
<text class="meta-item">位置: {{item.location || '未知'}}</text>
|
||||
<text class="meta-item">最后更新: {{formatTime(item.lastUpdateTime)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="device-status">
|
||||
<view
|
||||
class="status-badge"
|
||||
style="background-color: {{getStatusColor(item.status)}}"
|
||||
>
|
||||
{{getStatusText(item.status)}}
|
||||
</view>
|
||||
<view class="device-actions">
|
||||
<text class="action-btn edit" bindtap="editDevice" data-id="{{item.id}}" data-type="{{item.type}}" catchtap="true">编辑</text>
|
||||
<text class="action-btn delete" bindtap="deleteDevice" data-id="{{item.id}}" data-name="{{item.name || item.deviceNumber}}" catchtap="true">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{deviceList.length === 0 && !loading}}" class="empty-state">
|
||||
<text class="empty-icon">📱</text>
|
||||
<text class="empty-text">暂无设备数据</text>
|
||||
<button class="add-btn" bindtap="addDevice">添加设备</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{hasMore && deviceList.length > 0}}" class="load-more">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:else>上拉加载更多</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view wx:if="{{!hasMore && deviceList.length > 0}}" class="no-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="fab" bindtap="addDevice">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && deviceList.length === 0}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
332
mini_program/farm-monitor-dashboard/pages/device/device.wxss
Normal file
332
mini_program/farm-monitor-dashboard/pages/device/device.wxss
Normal file
@@ -0,0 +1,332 @@
|
||||
/* pages/device/device.wxss */
|
||||
.device-container {
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #3cc51f;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #ffffff;
|
||||
padding: 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.filter-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 8rpx 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.device-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-icon .icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.device-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.device-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
text-align: center;
|
||||
min-width: 60rpx;
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-btn.delete {
|
||||
background-color: #fff2f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 32rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.add-btn:active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: 120rpx;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
background-color: #3cc51f;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(60, 197, 31, 0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.device-item {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.device-icon .icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.fab {
|
||||
right: 24rpx;
|
||||
bottom: 100rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
}
|
||||
114
mini_program/farm-monitor-dashboard/pages/home/home.js
Normal file
114
mini_program/farm-monitor-dashboard/pages/home/home.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// pages/home/home.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
stats: {},
|
||||
recentActivities: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchHomeData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchHomeData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchHomeData().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
// 获取首页数据
|
||||
async fetchHomeData() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const [statsData, activitiesData] = await Promise.all([
|
||||
this.getHomeStats(),
|
||||
this.getRecentActivities()
|
||||
])
|
||||
|
||||
this.setData({
|
||||
stats: statsData,
|
||||
recentActivities: activitiesData
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取首页数据失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 获取首页统计信息
|
||||
async getHomeStats() {
|
||||
try {
|
||||
const data = await get('/home/stats')
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('获取首页统计失败:', error)
|
||||
// 返回默认数据
|
||||
return {
|
||||
totalCattle: 0,
|
||||
pregnantCattle: 0,
|
||||
sickCattle: 0,
|
||||
totalFarms: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取最近活动记录
|
||||
async getRecentActivities() {
|
||||
try {
|
||||
const data = await get('/activities/recent')
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error)
|
||||
// 返回空数组
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// 导航到指定页面
|
||||
navigateTo(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
if (url) {
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
},
|
||||
|
||||
// 获取活动图标
|
||||
getActivityIcon(type) {
|
||||
const icons = {
|
||||
'add_cattle': '🐄',
|
||||
'breed': '👶',
|
||||
'medical': '💊',
|
||||
'feed': '🌾',
|
||||
'vaccine': '💉',
|
||||
'birth': '🎉',
|
||||
'move': '🚚',
|
||||
'sell': '💰'
|
||||
}
|
||||
return icons[type] || '📋'
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
},
|
||||
|
||||
// 查看全部活动
|
||||
viewAllActivities() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/activity/list'
|
||||
})
|
||||
}
|
||||
})
|
||||
102
mini_program/farm-monitor-dashboard/pages/home/home.wxml
Normal file
102
mini_program/farm-monitor-dashboard/pages/home/home.wxml
Normal file
@@ -0,0 +1,102 @@
|
||||
<!--pages/home/home.wxml-->
|
||||
<view class="home-container">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card" bindtap="navigateTo" data-url="/pages/cattle/cattle">
|
||||
<view class="stat-icon">🐄</view>
|
||||
<view class="stat-content">
|
||||
<view class="stat-number">{{stats.totalCattle || 0}}</view>
|
||||
<view class="stat-label">总牛只数</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-card" bindtap="navigateTo" data-url="/pages/cattle/cattle?status=pregnant">
|
||||
<view class="stat-icon pregnant">🤰</view>
|
||||
<view class="stat-content">
|
||||
<view class="stat-number">{{stats.pregnantCattle || 0}}</view>
|
||||
<view class="stat-label">怀孕牛只</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-card" bindtap="navigateTo" data-url="/pages/cattle/cattle?status=sick">
|
||||
<view class="stat-icon sick">🤒</view>
|
||||
<view class="stat-content">
|
||||
<view class="stat-number">{{stats.sickCattle || 0}}</view>
|
||||
<view class="stat-label">生病牛只</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-card" bindtap="navigateTo" data-url="/pages/farm/list">
|
||||
<view class="stat-icon farm">🏡</view>
|
||||
<view class="stat-content">
|
||||
<view class="stat-number">{{stats.totalFarms || 0}}</view>
|
||||
<view class="stat-label">养殖场数</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="section-title">
|
||||
<text>快捷操作</text>
|
||||
</view>
|
||||
|
||||
<view class="actions-grid">
|
||||
<view class="action-item" bindtap="navigateTo" data-url="/pages/cattle/add/add">
|
||||
<view class="action-icon add">➕</view>
|
||||
<text class="action-text">添加牛只</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item" bindtap="navigateTo" data-url="/pages/breed/record">
|
||||
<view class="action-icon breed">👶</view>
|
||||
<text class="action-text">配种记录</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item" bindtap="navigateTo" data-url="/pages/medical/record">
|
||||
<view class="action-icon medical">💊</view>
|
||||
<text class="action-text">医疗记录</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item" bindtap="navigateTo" data-url="/pages/feed/record">
|
||||
<view class="action-icon feed">🌾</view>
|
||||
<text class="action-text">饲喂记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<view class="recent-activities">
|
||||
<view class="section-title">
|
||||
<text>最近活动</text>
|
||||
<text class="view-all" bindtap="viewAllActivities">查看全部</text>
|
||||
</view>
|
||||
|
||||
<view class="activity-list">
|
||||
<view
|
||||
wx:for="{{recentActivities}}"
|
||||
wx:key="index"
|
||||
class="activity-item"
|
||||
>
|
||||
<view class="activity-icon">
|
||||
<text>{{getActivityIcon(item.type)}}</text>
|
||||
</view>
|
||||
<view class="activity-content">
|
||||
<view class="activity-title">{{item.title}}</view>
|
||||
<view class="activity-desc">{{item.description}}</view>
|
||||
<view class="activity-time">{{formatTime(item.time)}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{recentActivities.length === 0}}" class="empty-state">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无活动记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
228
mini_program/farm-monitor-dashboard/pages/home/home.wxss
Normal file
228
mini_program/farm-monitor-dashboard/pages/home/home.wxss
Normal file
@@ -0,0 +1,228 @@
|
||||
/* pages/home/home.wxss */
|
||||
.home-container {
|
||||
padding: 16rpx;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #3cc51f, #2ea617);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(60, 197, 31, 0.3);
|
||||
}
|
||||
|
||||
.stat-card:active {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.stat-card .pregnant {
|
||||
background: linear-gradient(135deg, #faad14, #d48806);
|
||||
}
|
||||
|
||||
.stat-card .sick {
|
||||
background: linear-gradient(135deg, #f5222d, #cf1322);
|
||||
}
|
||||
|
||||
.stat-card .farm {
|
||||
background: linear-gradient(135deg, #52c41a, #389e0d);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 48rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-title .view-all {
|
||||
font-size: 24rpx;
|
||||
color: #3cc51f;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-item:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.action-icon.add { color: #3cc51f; }
|
||||
.action-icon.breed { color: #faad14; }
|
||||
.action-icon.medical { color: #f5222d; }
|
||||
.action-icon.feed { color: #52c41a; }
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.recent-activities {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-item:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 48rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 20rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64rpx 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 64rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.actions-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
padding: 16rpx 12rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
353
mini_program/farm-monitor-dashboard/pages/login/login.js
Normal file
353
mini_program/farm-monitor-dashboard/pages/login/login.js
Normal file
@@ -0,0 +1,353 @@
|
||||
// pages/login/login.js
|
||||
const { post } = require('../../utils/api')
|
||||
const { validatePhone, validateEmail } = require('../../utils/index')
|
||||
const auth = require('../../utils/auth')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loginType: 'password', // password, sms, wechat
|
||||
formData: {
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
smsCode: ''
|
||||
},
|
||||
loading: false,
|
||||
countdown: 0,
|
||||
canSendSms: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 检查是否已经登录
|
||||
if (auth.isLoggedIn()) {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
// 切换登录方式
|
||||
switchLoginType(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
loginType: type,
|
||||
formData: {
|
||||
username: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
smsCode: ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 输入框变化
|
||||
onInputChange(e) {
|
||||
const { field } = e.currentTarget.dataset
|
||||
const { value } = e.detail
|
||||
|
||||
this.setData({
|
||||
[`formData.${field}`]: value
|
||||
})
|
||||
},
|
||||
|
||||
// 密码登录
|
||||
async handlePasswordLogin() {
|
||||
const { username, password } = this.data.formData
|
||||
|
||||
if (!username.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入用户名',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const response = await post('/auth/login', {
|
||||
username: username.trim(),
|
||||
password: password.trim()
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
// 保存登录信息
|
||||
auth.login(response.data.token, response.data.userInfo)
|
||||
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 短信登录
|
||||
async handleSmsLogin() {
|
||||
const { phone, smsCode } = this.data.formData
|
||||
|
||||
if (!phone.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone(phone)) {
|
||||
wx.showToast({
|
||||
title: '请输入正确的手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!smsCode.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入验证码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const response = await post('/auth/sms-login', {
|
||||
phone: phone.trim(),
|
||||
smsCode: smsCode.trim()
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
// 保存登录信息
|
||||
auth.login(response.data.token, response.data.userInfo)
|
||||
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
async handleWechatLogin() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 获取微信授权
|
||||
const { code } = await this.getWechatCode()
|
||||
|
||||
const response = await post('/auth/wechat-login', {
|
||||
code: code
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
// 保存登录信息
|
||||
auth.login(response.data.token, response.data.userInfo)
|
||||
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 获取微信授权码
|
||||
getWechatCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.login({
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res)
|
||||
} else {
|
||||
reject(new Error('获取微信授权码失败'))
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 发送短信验证码
|
||||
async sendSmsCode() {
|
||||
const { phone } = this.data.formData
|
||||
|
||||
if (!phone.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone(phone)) {
|
||||
wx.showToast({
|
||||
title: '请输入正确的手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.data.canSendSms) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await post('/auth/send-sms', {
|
||||
phone: phone.trim()
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '验证码已发送',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 开始倒计时
|
||||
this.startCountdown()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '发送失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送短信失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '发送失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 开始倒计时
|
||||
startCountdown() {
|
||||
this.setData({
|
||||
countdown: 60,
|
||||
canSendSms: false
|
||||
})
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const countdown = this.data.countdown - 1
|
||||
|
||||
if (countdown <= 0) {
|
||||
clearInterval(timer)
|
||||
this.setData({
|
||||
countdown: 0,
|
||||
canSendSms: true
|
||||
})
|
||||
} else {
|
||||
this.setData({ countdown })
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 处理登录
|
||||
handleLogin() {
|
||||
const { loginType } = this.data
|
||||
|
||||
switch (loginType) {
|
||||
case 'password':
|
||||
this.handlePasswordLogin()
|
||||
break
|
||||
case 'sms':
|
||||
this.handleSmsLogin()
|
||||
break
|
||||
case 'wechat':
|
||||
this.handleWechatLogin()
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '不支持的登录方式',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到注册页面
|
||||
goToRegister() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/register/register'
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到忘记密码页面
|
||||
goToForgotPassword() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/forgot-password/forgot-password'
|
||||
})
|
||||
}
|
||||
})
|
||||
128
mini_program/farm-monitor-dashboard/pages/login/login.wxml
Normal file
128
mini_program/farm-monitor-dashboard/pages/login/login.wxml
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--pages/login/login.wxml-->
|
||||
<view class="login-container">
|
||||
<!-- 顶部logo和标题 -->
|
||||
<view class="login-header">
|
||||
<view class="logo">
|
||||
<text class="logo-icon">🐄</text>
|
||||
</view>
|
||||
<view class="title">养殖管理系统</view>
|
||||
<view class="subtitle">智能养殖,科学管理</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录方式切换 -->
|
||||
<view class="login-tabs">
|
||||
<view
|
||||
class="tab-item {{loginType === 'password' ? 'active' : ''}}"
|
||||
bindtap="switchLoginType"
|
||||
data-type="password"
|
||||
>
|
||||
密码登录
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{loginType === 'sms' ? 'active' : ''}}"
|
||||
bindtap="switchLoginType"
|
||||
data-type="sms"
|
||||
>
|
||||
短信登录
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{loginType === 'wechat' ? 'active' : ''}}"
|
||||
bindtap="switchLoginType"
|
||||
data-type="wechat"
|
||||
>
|
||||
微信登录
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="login-form">
|
||||
<!-- 密码登录 -->
|
||||
<view wx:if="{{loginType === 'password'}}" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">用户名</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入用户名"
|
||||
value="{{formData.username}}"
|
||||
bindinput="onInputChange"
|
||||
data-field="username"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">密码</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入密码"
|
||||
password="{{true}}"
|
||||
value="{{formData.password}}"
|
||||
bindinput="onInputChange"
|
||||
data-field="password"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-actions">
|
||||
<text class="forgot-password" bindtap="goToForgotPassword">忘记密码?</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 短信登录 -->
|
||||
<view wx:if="{{loginType === 'sms'}}" class="form-content">
|
||||
<view class="form-item">
|
||||
<view class="form-label">手机号</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入手机号"
|
||||
type="number"
|
||||
value="{{formData.phone}}"
|
||||
bindinput="onInputChange"
|
||||
data-field="phone"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">验证码</view>
|
||||
<view class="sms-input-group">
|
||||
<input
|
||||
class="form-input sms-input"
|
||||
placeholder="请输入验证码"
|
||||
type="number"
|
||||
value="{{formData.smsCode}}"
|
||||
bindinput="onInputChange"
|
||||
data-field="smsCode"
|
||||
/>
|
||||
<button
|
||||
class="sms-btn {{canSendSms ? '' : 'disabled'}}"
|
||||
bindtap="sendSmsCode"
|
||||
disabled="{{!canSendSms}}"
|
||||
>
|
||||
{{canSendSms ? '发送验证码' : countdown + 's'}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 微信登录 -->
|
||||
<view wx:if="{{loginType === 'wechat'}}" class="form-content">
|
||||
<view class="wechat-login-tip">
|
||||
<text class="tip-icon">🔐</text>
|
||||
<text class="tip-text">使用微信授权登录,安全便捷</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn {{loading ? 'loading' : ''}}"
|
||||
bindtap="handleLogin"
|
||||
disabled="{{loading}}"
|
||||
>
|
||||
<text wx:if="{{!loading}}">登录</text>
|
||||
<text wx:else>登录中...</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 底部链接 -->
|
||||
<view class="login-footer">
|
||||
<text class="register-link" bindtap="goToRegister">还没有账号?立即注册</text>
|
||||
</view>
|
||||
</view>
|
||||
224
mini_program/farm-monitor-dashboard/pages/login/login.wxss
Normal file
224
mini_program/farm-monitor-dashboard/pages/login/login.wxss
Normal file
@@ -0,0 +1,224 @@
|
||||
/* pages/login/login.wxss */
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 80rpx;
|
||||
margin-top: 100rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-content {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3cc51f;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.sms-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.sms-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sms-btn {
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
border: none;
|
||||
white-space: nowrap;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.sms-btn.disabled {
|
||||
background-color: #c0c4cc;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sms-btn:not(.disabled):active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 24rpx;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.wechat-login-tip {
|
||||
text-align: center;
|
||||
padding: 48rpx 0;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 64rpx;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
background-color: #2ea617;
|
||||
}
|
||||
|
||||
.login-btn.loading {
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
|
||||
.login-btn.disabled {
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.register-link:active {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.login-container {
|
||||
padding: 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 80rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.sms-btn {
|
||||
height: 80rpx;
|
||||
font-size: 22rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 80rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
248
mini_program/farm-monitor-dashboard/pages/profile/profile.js
Normal file
248
mini_program/farm-monitor-dashboard/pages/profile/profile.js
Normal file
@@ -0,0 +1,248 @@
|
||||
// pages/profile/profile.js
|
||||
const auth = require('../../utils/auth')
|
||||
const { formatDate } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
userInfo: {},
|
||||
menuItems: [
|
||||
{
|
||||
icon: '👤',
|
||||
title: '个人信息',
|
||||
url: '/pages/profile/info/info'
|
||||
},
|
||||
{
|
||||
icon: '🔧',
|
||||
title: '账户设置',
|
||||
url: '/pages/profile/settings/settings'
|
||||
},
|
||||
{
|
||||
icon: '🔔',
|
||||
title: '消息通知',
|
||||
url: '/pages/profile/notifications/notifications'
|
||||
},
|
||||
{
|
||||
icon: '🛡️',
|
||||
title: '隐私安全',
|
||||
url: '/pages/profile/privacy/privacy'
|
||||
},
|
||||
{
|
||||
icon: '❓',
|
||||
title: '帮助中心',
|
||||
url: '/pages/profile/help/help'
|
||||
},
|
||||
{
|
||||
icon: '📞',
|
||||
title: '联系我们',
|
||||
url: '/pages/profile/contact/contact'
|
||||
},
|
||||
{
|
||||
icon: 'ℹ️',
|
||||
title: '关于我们',
|
||||
url: '/pages/profile/about/about'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const userInfo = auth.getUserInfo()
|
||||
if (userInfo) {
|
||||
this.setData({ userInfo })
|
||||
} else {
|
||||
// 如果未登录,跳转到登录页
|
||||
auth.redirectToLogin()
|
||||
}
|
||||
},
|
||||
|
||||
// 点击菜单项
|
||||
onMenuTap(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
if (url) {
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑个人信息
|
||||
editProfile() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/profile/edit/edit'
|
||||
})
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
async logout() {
|
||||
const result = await wx.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmText: '退出',
|
||||
confirmColor: '#f5222d'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '退出中...' })
|
||||
|
||||
// 调用退出登录API
|
||||
// const response = await post('/auth/logout')
|
||||
|
||||
// 清除本地存储
|
||||
auth.logout()
|
||||
|
||||
wx.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
wx.showToast({
|
||||
title: '退出失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
async clearCache() {
|
||||
const result = await wx.showModal({
|
||||
title: '清除缓存',
|
||||
content: '确定要清除应用缓存吗?',
|
||||
confirmText: '清除',
|
||||
confirmColor: '#faad14'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '清除中...' })
|
||||
|
||||
// 清除微信小程序缓存
|
||||
wx.clearStorageSync()
|
||||
|
||||
wx.showToast({
|
||||
title: '缓存已清除',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载用户信息
|
||||
this.loadUserInfo()
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error)
|
||||
wx.showToast({
|
||||
title: '清除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
async checkUpdate() {
|
||||
try {
|
||||
wx.showLoading({ title: '检查中...' })
|
||||
|
||||
// 检查小程序更新
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
if (res.hasUpdate) {
|
||||
wx.showModal({
|
||||
title: '发现新版本',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '已是最新版本',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
wx.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
wx.showToast({
|
||||
title: '检查失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户显示名称
|
||||
getUserDisplayName() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.realName || userInfo.nickname || userInfo.username || '未知用户'
|
||||
},
|
||||
|
||||
// 获取用户头像
|
||||
getUserAvatar() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.avatar || '/images/default-avatar.png'
|
||||
},
|
||||
|
||||
// 获取用户角色
|
||||
getUserRole() {
|
||||
const userInfo = this.data.userInfo
|
||||
const roleMap = {
|
||||
'admin': '管理员',
|
||||
'manager': '经理',
|
||||
'operator': '操作员',
|
||||
'viewer': '观察员'
|
||||
}
|
||||
return roleMap[userInfo.role] || '普通用户'
|
||||
},
|
||||
|
||||
// 获取用户部门
|
||||
getUserDepartment() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.department || '未知部门'
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
<!--pages/profile/profile.wxml-->
|
||||
<view class="profile-container">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-avatar">
|
||||
<image
|
||||
class="avatar-img"
|
||||
src="{{getUserAvatar()}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="user-name">{{getUserDisplayName()}}</view>
|
||||
<view class="user-role">{{getUserRole()}}</view>
|
||||
<view class="user-department">{{getUserDepartment()}}</view>
|
||||
</view>
|
||||
<view class="edit-btn" bindtap="editProfile">
|
||||
<text class="edit-icon">✏️</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-item">
|
||||
<view class="stats-number">{{userInfo.cattleCount || 0}}</view>
|
||||
<view class="stats-label">管理牛只</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-number">{{userInfo.deviceCount || 0}}</view>
|
||||
<view class="stats-label">设备数量</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-number">{{userInfo.alertCount || 0}}</view>
|
||||
<view class="stats-label">预警数量</view>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<view class="stats-number">{{userInfo.farmCount || 0}}</view>
|
||||
<view class="stats-label">养殖场数</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-section">
|
||||
<view
|
||||
wx:for="{{menuItems}}"
|
||||
wx:key="title"
|
||||
class="menu-item"
|
||||
bindtap="onMenuTap"
|
||||
data-url="{{item.url}}"
|
||||
>
|
||||
<view class="menu-icon">{{item.icon}}</view>
|
||||
<view class="menu-title">{{item.title}}</view>
|
||||
<view class="menu-arrow">></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-section">
|
||||
<button class="action-btn cache" bindtap="clearCache">
|
||||
<text class="btn-icon">🗑️</text>
|
||||
<text class="btn-text">清除缓存</text>
|
||||
</button>
|
||||
<button class="action-btn update" bindtap="checkUpdate">
|
||||
<text class="btn-icon">🔄</text>
|
||||
<text class="btn-text">检查更新</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view class="logout-section">
|
||||
<button class="logout-btn" bindtap="logout">
|
||||
<text class="logout-icon">🚪</text>
|
||||
<text class="logout-text">退出登录</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<view class="version-info">
|
||||
<text class="version-text">版本 {{userInfo.appVersion || '1.0.0'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
312
mini_program/farm-monitor-dashboard/pages/profile/profile.wxss
Normal file
312
mini_program/farm-monitor-dashboard/pages/profile/profile.wxss
Normal file
@@ -0,0 +1,312 @@
|
||||
/* pages/profile/profile.wxss */
|
||||
.profile-container {
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 24rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.user-department {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
position: absolute;
|
||||
top: 32rpx;
|
||||
right: 32rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
margin: 0 16rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 32rpx 0;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stats-item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1rpx;
|
||||
height: 60rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #3cc51f;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
background-color: #ffffff;
|
||||
margin: 0 16rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 40rpx;
|
||||
margin-right: 24rpx;
|
||||
width: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin: 0 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
font-size: 28rpx;
|
||||
color: #606266;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.action-btn.cache {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.action-btn.update {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin: 0 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #ff4d4f;
|
||||
font-size: 30rpx;
|
||||
color: #ff4d4f;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
background-color: #fff2f0;
|
||||
}
|
||||
|
||||
.logout-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
text-align: center;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.user-card {
|
||||
padding: 32rpx 24rpx;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.user-department {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.logout-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
116
mini_program/farm-monitor-dashboard/project.config.json
Normal file
116
mini_program/farm-monitor-dashboard/project.config.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"description": "养殖管理系统微信小程序",
|
||||
"packOptions": {
|
||||
"ignore": [
|
||||
{
|
||||
"type": "file",
|
||||
"value": ".eslintrc.js"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "package.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "package-lock.json"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "README.md"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "node_modules"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "src"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "public"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "dist"
|
||||
}
|
||||
]
|
||||
},
|
||||
"setting": {
|
||||
"bundle": false,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"urlCheck": true,
|
||||
"scopeDataCheck": false,
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"autoAudits": false,
|
||||
"newFeature": false,
|
||||
"uglifyFileName": false,
|
||||
"uploadWithSourceMap": true,
|
||||
"useIsolateContext": true,
|
||||
"nodeModules": false,
|
||||
"enhance": true,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmManually": false,
|
||||
"enableEngineNative": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"showES6CompileOption": false,
|
||||
"minifyWXML": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useStaticServer": true,
|
||||
"checkInvalidKey": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"disableUseStrict": false,
|
||||
"useCompilerPlugins": false
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "2.19.4",
|
||||
"appid": "wx363d2520963f1853",
|
||||
"projectname": "farm-monitor-dashboard",
|
||||
"debugOptions": {
|
||||
"hidedInDevtools": []
|
||||
},
|
||||
"scripts": {},
|
||||
"staticServerOptions": {
|
||||
"baseURL": "",
|
||||
"servePath": ""
|
||||
},
|
||||
"isGameTourist": false,
|
||||
"condition": {
|
||||
"search": {
|
||||
"list": []
|
||||
},
|
||||
"conversation": {
|
||||
"list": []
|
||||
},
|
||||
"game": {
|
||||
"list": []
|
||||
},
|
||||
"plugin": {
|
||||
"list": []
|
||||
},
|
||||
"gamePlugin": {
|
||||
"list": []
|
||||
},
|
||||
"miniprogram": {
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"libVersion": "3.10.1",
|
||||
"projectname": "farm-monitor-dashboard",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
"preloadBackgroundData": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
332
mini_program/farm-monitor-dashboard/services/alertService.js
Normal file
332
mini_program/farm-monitor-dashboard/services/alertService.js
Normal file
@@ -0,0 +1,332 @@
|
||||
// services/alertService.js - 预警管理服务
|
||||
const { alertApi } = require('./api')
|
||||
|
||||
// 获取预警列表
|
||||
export const getAlertList = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/alerts', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警详情
|
||||
export const getAlertDetail = async (id) => {
|
||||
try {
|
||||
const response = await get(`/alerts/${id}`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 处理预警
|
||||
export const handleAlert = async (id, data) => {
|
||||
try {
|
||||
const response = await post(`/alerts/${id}/handle`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('处理预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 忽略预警
|
||||
export const ignoreAlert = async (id) => {
|
||||
try {
|
||||
const response = await post(`/alerts/${id}/ignore`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('忽略预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理预警
|
||||
export const batchHandleAlerts = async (alertIds, action) => {
|
||||
try {
|
||||
const response = await post('/alerts/batch-handle', {
|
||||
alertIds,
|
||||
action
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('批量处理预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取耳标预警统计
|
||||
export const getEartagAlertStats = async () => {
|
||||
try {
|
||||
const response = await alertApi.getEartagStats()
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取耳标预警统计失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项圈预警统计
|
||||
export const getCollarAlertStats = async () => {
|
||||
try {
|
||||
const response = await alertApi.getCollarStats()
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取项圈预警统计失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取耳标预警列表
|
||||
export const getEartagAlerts = async (params = {}) => {
|
||||
try {
|
||||
const response = await alertApi.getEartagAlerts(params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取耳标预警列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项圈预警列表
|
||||
export const getCollarAlerts = async (params = {}) => {
|
||||
try {
|
||||
const response = await alertApi.getCollarAlerts(params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取项圈预警列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取耳标预警详情
|
||||
export const getEartagAlertDetail = async (id) => {
|
||||
try {
|
||||
const response = await alertApi.getEartagAlertDetail(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取耳标预警详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项圈预警详情
|
||||
export const getCollarAlertDetail = async (id) => {
|
||||
try {
|
||||
const response = await alertApi.getCollarAlertDetail(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取项圈预警详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 处理耳标预警
|
||||
export const handleEartagAlert = async (id, data) => {
|
||||
try {
|
||||
const response = await alertApi.handleEartagAlert(id, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('处理耳标预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 处理项圈预警
|
||||
export const handleCollarAlert = async (id, data) => {
|
||||
try {
|
||||
const response = await alertApi.handleCollarAlert(id, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('处理项圈预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理耳标预警
|
||||
export const batchHandleEartagAlerts = async (data) => {
|
||||
try {
|
||||
const response = await alertApi.batchHandleEartagAlerts(data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('批量处理耳标预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 批量处理项圈预警
|
||||
export const batchHandleCollarAlerts = async (data) => {
|
||||
try {
|
||||
const response = await alertApi.batchHandleCollarAlerts(data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('批量处理项圈预警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警类型列表
|
||||
export const getAlertTypes = async () => {
|
||||
try {
|
||||
const response = await get('/alerts/types')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警类型失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警优先级列表
|
||||
export const getAlertPriorities = async () => {
|
||||
try {
|
||||
const response = await get('/alerts/priorities')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警优先级失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警状态列表
|
||||
export const getAlertStatuses = async () => {
|
||||
try {
|
||||
const response = await get('/alerts/statuses')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警统计数据
|
||||
export const getAlertStats = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/alerts/stats', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警统计失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警趋势数据
|
||||
export const getAlertTrends = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/alerts/trends', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警趋势失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建预警规则
|
||||
export const createAlertRule = async (data) => {
|
||||
try {
|
||||
const response = await post('/alerts/rules', data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('创建预警规则失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警规则列表
|
||||
export const getAlertRules = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/alerts/rules', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警规则失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新预警规则
|
||||
export const updateAlertRule = async (id, data) => {
|
||||
try {
|
||||
const response = await put(`/alerts/rules/${id}`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新预警规则失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除预警规则
|
||||
export const deleteAlertRule = async (id) => {
|
||||
try {
|
||||
const response = await del(`/alerts/rules/${id}`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除预警规则失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 测试预警规则
|
||||
export const testAlertRule = async (ruleId, testData) => {
|
||||
try {
|
||||
const response = await post(`/alerts/rules/${ruleId}/test`, testData)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('测试预警规则失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警通知设置
|
||||
export const getAlertNotificationSettings = async () => {
|
||||
try {
|
||||
const response = await get('/alerts/notification-settings')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取预警通知设置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新预警通知设置
|
||||
export const updateAlertNotificationSettings = async (settings) => {
|
||||
try {
|
||||
const response = await post('/alerts/notification-settings', settings)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新预警通知设置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getAlertList,
|
||||
getAlertDetail,
|
||||
handleAlert,
|
||||
ignoreAlert,
|
||||
batchHandleAlerts,
|
||||
getEartagAlertStats,
|
||||
getCollarAlertStats,
|
||||
getEartagAlerts,
|
||||
getCollarAlerts,
|
||||
getEartagAlertDetail,
|
||||
getCollarAlertDetail,
|
||||
handleEartagAlert,
|
||||
handleCollarAlert,
|
||||
batchHandleEartagAlerts,
|
||||
batchHandleCollarAlerts,
|
||||
getAlertTypes,
|
||||
getAlertPriorities,
|
||||
getAlertStatuses,
|
||||
getAlertStats,
|
||||
getAlertTrends,
|
||||
createAlertRule,
|
||||
getAlertRules,
|
||||
updateAlertRule,
|
||||
deleteAlertRule,
|
||||
testAlertRule,
|
||||
getAlertNotificationSettings,
|
||||
updateAlertNotificationSettings
|
||||
}
|
||||
447
mini_program/farm-monitor-dashboard/services/api.js
Normal file
447
mini_program/farm-monitor-dashboard/services/api.js
Normal file
@@ -0,0 +1,447 @@
|
||||
// services/api.js - API服务层
|
||||
const { get, post, put, del } = require('../utils/api')
|
||||
|
||||
// 牛只档案相关API
|
||||
export const cattleApi = {
|
||||
// 获取牛只档案列表
|
||||
getCattleList: (params = {}) => {
|
||||
return get('/iot-cattle/public', params)
|
||||
},
|
||||
|
||||
// 根据耳号搜索牛只
|
||||
searchCattleByEarNumber: (earNumber) => {
|
||||
return get('/iot-cattle/public', { search: earNumber })
|
||||
},
|
||||
|
||||
// 获取牛只详情
|
||||
getCattleDetail: (id) => {
|
||||
return get(`/iot-cattle/public/${id}`)
|
||||
},
|
||||
|
||||
// 获取牛只类型列表
|
||||
getCattleTypes: () => {
|
||||
return get('/cattle-type')
|
||||
},
|
||||
|
||||
// 获取栏舍列表
|
||||
getPens: (farmId) => {
|
||||
return get('/iot-cattle/public/pens/list', { farmId })
|
||||
},
|
||||
|
||||
// 获取批次列表
|
||||
getBatches: (farmId) => {
|
||||
return get('/iot-cattle/public/batches/list', { farmId })
|
||||
},
|
||||
|
||||
// 创建牛只档案
|
||||
createCattle: (data) => {
|
||||
return post('/iot-cattle', data)
|
||||
},
|
||||
|
||||
// 更新牛只档案
|
||||
updateCattle: (id, data) => {
|
||||
return put(`/iot-cattle/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除牛只档案
|
||||
deleteCattle: (id) => {
|
||||
return del(`/iot-cattle/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只转栏记录相关API
|
||||
export const cattleTransferApi = {
|
||||
// 获取转栏记录列表
|
||||
getTransferRecords: (params = {}) => {
|
||||
return get('/cattle-transfer-records', params)
|
||||
},
|
||||
|
||||
// 根据耳号搜索转栏记录
|
||||
searchTransferRecordsByEarNumber: (earNumber, params = {}) => {
|
||||
return get('/cattle-transfer-records', { earNumber, ...params })
|
||||
},
|
||||
|
||||
// 获取转栏记录详情
|
||||
getTransferRecordDetail: (id) => {
|
||||
return get(`/cattle-transfer-records/${id}`)
|
||||
},
|
||||
|
||||
// 创建转栏记录
|
||||
createTransferRecord: (data) => {
|
||||
return post('/cattle-transfer-records', data)
|
||||
},
|
||||
|
||||
// 更新转栏记录
|
||||
updateTransferRecord: (id, data) => {
|
||||
return put(`/cattle-transfer-records/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除转栏记录
|
||||
deleteTransferRecord: (id) => {
|
||||
return del(`/cattle-transfer-records/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除转栏记录
|
||||
batchDeleteTransferRecords: (ids) => {
|
||||
return post('/cattle-transfer-records/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取可用的牛只列表
|
||||
getAvailableAnimals: (params = {}) => {
|
||||
return get('/cattle-transfer-records/available-animals', params)
|
||||
},
|
||||
|
||||
// 获取栏舍列表(用于转栏选择)
|
||||
getBarnsForTransfer: (params = {}) => {
|
||||
return get('/cattle-pens', params)
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只离栏记录相关API
|
||||
export const cattleExitApi = {
|
||||
// 获取离栏记录列表
|
||||
getExitRecords: (params = {}) => {
|
||||
return get('/cattle-exit-records', params)
|
||||
},
|
||||
|
||||
// 根据耳号搜索离栏记录
|
||||
searchExitRecordsByEarNumber: (earNumber, params = {}) => {
|
||||
return get('/cattle-exit-records', { earNumber, ...params })
|
||||
},
|
||||
|
||||
// 获取离栏记录详情
|
||||
getExitRecordDetail: (id) => {
|
||||
return get(`/cattle-exit-records/${id}`)
|
||||
},
|
||||
|
||||
// 创建离栏记录
|
||||
createExitRecord: (data) => {
|
||||
return post('/cattle-exit-records', data)
|
||||
},
|
||||
|
||||
// 更新离栏记录
|
||||
updateExitRecord: (id, data) => {
|
||||
return put(`/cattle-exit-records/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除离栏记录
|
||||
deleteExitRecord: (id) => {
|
||||
return del(`/cattle-exit-records/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除离栏记录
|
||||
batchDeleteExitRecords: (ids) => {
|
||||
return post('/cattle-exit-records/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取可用的牛只列表
|
||||
getAvailableAnimals: (params = {}) => {
|
||||
return get('/cattle-exit-records/available-animals', params)
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只栏舍相关API
|
||||
export const cattlePenApi = {
|
||||
// 获取栏舍列表
|
||||
getPens: (params = {}) => {
|
||||
return get('/cattle-pens', params)
|
||||
},
|
||||
|
||||
// 根据名称搜索栏舍
|
||||
searchPensByName: (name, params = {}) => {
|
||||
return get('/cattle-pens', { name, ...params })
|
||||
},
|
||||
|
||||
// 获取栏舍详情
|
||||
getPenDetail: (id) => {
|
||||
return get(`/cattle-pens/${id}`)
|
||||
},
|
||||
|
||||
// 创建栏舍
|
||||
createPen: (data) => {
|
||||
return post('/cattle-pens', data)
|
||||
},
|
||||
|
||||
// 更新栏舍
|
||||
updatePen: (id, data) => {
|
||||
return put(`/cattle-pens/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除栏舍
|
||||
deletePen: (id) => {
|
||||
return del(`/cattle-pens/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除栏舍
|
||||
batchDeletePens: (ids) => {
|
||||
return post('/cattle-pens/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取栏舍类型列表
|
||||
getPenTypes: () => {
|
||||
return get('/cattle-pens/types')
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只批次相关API
|
||||
export const cattleBatchApi = {
|
||||
// 获取批次列表
|
||||
getBatches: (params = {}) => {
|
||||
return get('/cattle-batches', params)
|
||||
},
|
||||
|
||||
// 根据名称搜索批次
|
||||
searchBatchesByName: (name, params = {}) => {
|
||||
return get('/cattle-batches', { name, ...params })
|
||||
},
|
||||
|
||||
// 获取批次详情
|
||||
getBatchDetail: (id) => {
|
||||
return get(`/cattle-batches/${id}`)
|
||||
},
|
||||
|
||||
// 创建批次
|
||||
createBatch: (data) => {
|
||||
return post('/cattle-batches', data)
|
||||
},
|
||||
|
||||
// 更新批次
|
||||
updateBatch: (id, data) => {
|
||||
return put(`/cattle-batches/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除批次
|
||||
deleteBatch: (id) => {
|
||||
return del(`/cattle-batches/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除批次
|
||||
batchDeleteBatches: (ids) => {
|
||||
return post('/cattle-batches/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取批次类型列表
|
||||
getBatchTypes: () => {
|
||||
return get('/cattle-batches/types')
|
||||
}
|
||||
}
|
||||
|
||||
// 智能预警相关API
|
||||
export const alertApi = {
|
||||
// 获取耳标预警统计
|
||||
getEartagStats: () => {
|
||||
return get('/smart-alerts/public/eartag/stats')
|
||||
},
|
||||
|
||||
// 获取项圈预警统计
|
||||
getCollarStats: () => {
|
||||
return get('/smart-alerts/public/collar/stats')
|
||||
},
|
||||
|
||||
// 获取耳标预警列表
|
||||
getEartagAlerts: (params = {}) => {
|
||||
return get('/smart-alerts/public/eartag', params)
|
||||
},
|
||||
|
||||
// 获取项圈预警列表
|
||||
getCollarAlerts: (params = {}) => {
|
||||
return get('/smart-alerts/public/collar', params)
|
||||
},
|
||||
|
||||
// 获取耳标预警详情
|
||||
getEartagAlertDetail: (id) => {
|
||||
return get(`/smart-alerts/public/eartag/${id}`)
|
||||
},
|
||||
|
||||
// 获取项圈预警详情
|
||||
getCollarAlertDetail: (id) => {
|
||||
return get(`/smart-alerts/public/collar/${id}`)
|
||||
},
|
||||
|
||||
// 处理耳标预警
|
||||
handleEartagAlert: (id, data) => {
|
||||
return post(`/smart-alerts/public/eartag/${id}/handle`, data)
|
||||
},
|
||||
|
||||
// 处理项圈预警
|
||||
handleCollarAlert: (id, data) => {
|
||||
return post(`/smart-alerts/public/collar/${id}/handle`, data)
|
||||
},
|
||||
|
||||
// 批量处理耳标预警
|
||||
batchHandleEartagAlerts: (data) => {
|
||||
return post('/smart-alerts/public/eartag/batch-handle', data)
|
||||
},
|
||||
|
||||
// 批量处理项圈预警
|
||||
batchHandleCollarAlerts: (data) => {
|
||||
return post('/smart-alerts/public/collar/batch-handle', data)
|
||||
}
|
||||
}
|
||||
|
||||
// 设备管理相关API
|
||||
export const deviceApi = {
|
||||
// 获取设备列表
|
||||
getDeviceList: (params = {}) => {
|
||||
return get('/devices', params)
|
||||
},
|
||||
|
||||
// 获取设备详情
|
||||
getDeviceDetail: (id) => {
|
||||
return get(`/devices/${id}`)
|
||||
},
|
||||
|
||||
// 创建设备
|
||||
createDevice: (data) => {
|
||||
return post('/devices', data)
|
||||
},
|
||||
|
||||
// 更新设备
|
||||
updateDevice: (id, data) => {
|
||||
return put(`/devices/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除设备
|
||||
deleteDevice: (id) => {
|
||||
return del(`/devices/${id}`)
|
||||
},
|
||||
|
||||
// 获取设备类型列表
|
||||
getDeviceTypes: () => {
|
||||
return get('/devices/types')
|
||||
},
|
||||
|
||||
// 获取设备状态列表
|
||||
getDeviceStatuses: () => {
|
||||
return get('/devices/statuses')
|
||||
}
|
||||
}
|
||||
|
||||
// 首页相关API
|
||||
export const homeApi = {
|
||||
// 获取首页统计信息
|
||||
getHomeStats: () => {
|
||||
return get('/home/stats')
|
||||
},
|
||||
|
||||
// 获取最近活动记录
|
||||
getRecentActivities: () => {
|
||||
return get('/activities/recent')
|
||||
},
|
||||
|
||||
// 获取牛只状态分布
|
||||
getCattleStatusDistribution: () => {
|
||||
return get('/cattle/status-distribution')
|
||||
},
|
||||
|
||||
// 获取养殖场统计
|
||||
getFarmStatistics: () => {
|
||||
return get('/farms/statistics')
|
||||
},
|
||||
|
||||
// 获取预警信息
|
||||
getAlerts: () => {
|
||||
return get('/alerts')
|
||||
},
|
||||
|
||||
// 获取待办事项
|
||||
getTodos: () => {
|
||||
return get('/todos')
|
||||
},
|
||||
|
||||
// 获取天气信息
|
||||
getWeather: (location) => {
|
||||
const params = location ? { location } : {}
|
||||
return get('/weather', params)
|
||||
},
|
||||
|
||||
// 获取市场行情
|
||||
getMarketPrices: () => {
|
||||
return get('/market/prices')
|
||||
},
|
||||
|
||||
// 获取通知消息
|
||||
getNotifications: () => {
|
||||
return get('/notifications')
|
||||
},
|
||||
|
||||
// 标记通知为已读
|
||||
markNotificationAsRead: (notificationId) => {
|
||||
return post(`/notifications/${notificationId}/read`)
|
||||
},
|
||||
|
||||
// 获取系统公告
|
||||
getAnnouncements: () => {
|
||||
return get('/announcements')
|
||||
},
|
||||
|
||||
// 获取用户仪表盘配置
|
||||
getDashboardConfig: () => {
|
||||
return get('/user/dashboard-config')
|
||||
},
|
||||
|
||||
// 更新用户仪表盘配置
|
||||
updateDashboardConfig: (config) => {
|
||||
return post('/user/dashboard-config', config)
|
||||
}
|
||||
}
|
||||
|
||||
// 认证相关API
|
||||
export const authApi = {
|
||||
// 密码登录
|
||||
login: (username, password) => {
|
||||
return post('/auth/login', { username, password })
|
||||
},
|
||||
|
||||
// 短信登录
|
||||
smsLogin: (phone, smsCode) => {
|
||||
return post('/auth/sms-login', { phone, smsCode })
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
wechatLogin: (code) => {
|
||||
return post('/auth/wechat-login', { code })
|
||||
},
|
||||
|
||||
// 发送短信验证码
|
||||
sendSms: (phone) => {
|
||||
return post('/auth/send-sms', { phone })
|
||||
},
|
||||
|
||||
// 刷新token
|
||||
refreshToken: (refreshToken) => {
|
||||
return post('/auth/refresh-token', { refreshToken })
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout: () => {
|
||||
return post('/auth/logout')
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo: () => {
|
||||
return get('/user/info')
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUserInfo: (data) => {
|
||||
return put('/user/info', data)
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
changePassword: (oldPassword, newPassword) => {
|
||||
return post('/user/change-password', { oldPassword, newPassword })
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
cattleApi,
|
||||
cattleTransferApi,
|
||||
cattleExitApi,
|
||||
cattlePenApi,
|
||||
cattleBatchApi,
|
||||
alertApi,
|
||||
deviceApi,
|
||||
homeApi,
|
||||
authApi
|
||||
}
|
||||
245
mini_program/farm-monitor-dashboard/services/cattleService.js
Normal file
245
mini_program/farm-monitor-dashboard/services/cattleService.js
Normal file
@@ -0,0 +1,245 @@
|
||||
// services/cattleService.js - 牛只管理服务
|
||||
const { cattleApi } = require('./api')
|
||||
|
||||
// 获取牛只列表
|
||||
export const getCattleList = async (params = {}) => {
|
||||
try {
|
||||
const response = await cattleApi.getCattleList(params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索牛只
|
||||
export const searchCattle = async (keyword, params = {}) => {
|
||||
try {
|
||||
const response = await cattleApi.searchCattleByEarNumber(keyword)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('搜索牛只失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只详情
|
||||
export const getCattleDetail = async (id) => {
|
||||
try {
|
||||
const response = await cattleApi.getCattleDetail(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建牛只
|
||||
export const createCattle = async (data) => {
|
||||
try {
|
||||
const response = await cattleApi.createCattle(data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('创建牛只失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新牛只
|
||||
export const updateCattle = async (id, data) => {
|
||||
try {
|
||||
const response = await cattleApi.updateCattle(id, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新牛只失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除牛只
|
||||
export const deleteCattle = async (id) => {
|
||||
try {
|
||||
const response = await cattleApi.deleteCattle(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除牛只失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只类型列表
|
||||
export const getCattleTypes = async () => {
|
||||
try {
|
||||
const response = await cattleApi.getCattleTypes()
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只类型失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取栏舍列表
|
||||
export const getPens = async (farmId) => {
|
||||
try {
|
||||
const response = await cattleApi.getPens(farmId)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取栏舍列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取批次列表
|
||||
export const getBatches = async (farmId) => {
|
||||
try {
|
||||
const response = await cattleApi.getBatches(farmId)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取批次列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只状态统计
|
||||
export const getCattleStatusStats = async () => {
|
||||
try {
|
||||
const response = await get('/cattle/status-stats')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只状态统计失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只年龄分布
|
||||
export const getCattleAgeDistribution = async () => {
|
||||
try {
|
||||
const response = await get('/cattle/age-distribution')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只年龄分布失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只品种分布
|
||||
export const getCattleBreedDistribution = async () => {
|
||||
try {
|
||||
const response = await get('/cattle/breed-distribution')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只品种分布失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入牛只
|
||||
export const batchImportCattle = async (filePath) => {
|
||||
try {
|
||||
const response = await upload('/cattle/batch-import', filePath)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('批量导入牛只失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 导出牛只数据
|
||||
export const exportCattleData = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/cattle/export', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('导出牛只数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只健康记录
|
||||
export const getCattleHealthRecords = async (cattleId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/cattle/${cattleId}/health-records`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只健康记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 添加牛只健康记录
|
||||
export const addCattleHealthRecord = async (cattleId, data) => {
|
||||
try {
|
||||
const response = await post(`/cattle/${cattleId}/health-records`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加牛只健康记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只繁殖记录
|
||||
export const getCattleBreedingRecords = async (cattleId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/cattle/${cattleId}/breeding-records`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只繁殖记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 添加牛只繁殖记录
|
||||
export const addCattleBreedingRecord = async (cattleId, data) => {
|
||||
try {
|
||||
const response = await post(`/cattle/${cattleId}/breeding-records`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加牛只繁殖记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只饲喂记录
|
||||
export const getCattleFeedingRecords = async (cattleId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/cattle/${cattleId}/feeding-records`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牛只饲喂记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 添加牛只饲喂记录
|
||||
export const addCattleFeedingRecord = async (cattleId, data) => {
|
||||
try {
|
||||
const response = await post(`/cattle/${cattleId}/feeding-records`, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加牛只饲喂记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getCattleList,
|
||||
searchCattle,
|
||||
getCattleDetail,
|
||||
createCattle,
|
||||
updateCattle,
|
||||
deleteCattle,
|
||||
getCattleTypes,
|
||||
getPens,
|
||||
getBatches,
|
||||
getCattleStatusStats,
|
||||
getCattleAgeDistribution,
|
||||
getCattleBreedDistribution,
|
||||
batchImportCattle,
|
||||
exportCattleData,
|
||||
getCattleHealthRecords,
|
||||
addCattleHealthRecord,
|
||||
getCattleBreedingRecords,
|
||||
addCattleBreedingRecord,
|
||||
getCattleFeedingRecords,
|
||||
addCattleFeedingRecord
|
||||
}
|
||||
274
mini_program/farm-monitor-dashboard/services/deviceService.js
Normal file
274
mini_program/farm-monitor-dashboard/services/deviceService.js
Normal file
@@ -0,0 +1,274 @@
|
||||
// services/deviceService.js - 设备管理服务
|
||||
const { deviceApi } = require('./api')
|
||||
|
||||
// 获取设备列表
|
||||
export const getDeviceList = async (params = {}) => {
|
||||
try {
|
||||
const response = await deviceApi.getDeviceList(params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备详情
|
||||
export const getDeviceDetail = async (id) => {
|
||||
try {
|
||||
const response = await deviceApi.getDeviceDetail(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建设备
|
||||
export const createDevice = async (data) => {
|
||||
try {
|
||||
const response = await deviceApi.createDevice(data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('创建设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备
|
||||
export const updateDevice = async (id, data) => {
|
||||
try {
|
||||
const response = await deviceApi.updateDevice(id, data)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
export const deleteDevice = async (id) => {
|
||||
try {
|
||||
const response = await deviceApi.deleteDevice(id)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备类型列表
|
||||
export const getDeviceTypes = async () => {
|
||||
try {
|
||||
const response = await deviceApi.getDeviceTypes()
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备类型失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备状态列表
|
||||
export const getDeviceStatuses = async () => {
|
||||
try {
|
||||
const response = await deviceApi.getDeviceStatuses()
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备统计数据
|
||||
export const getDeviceStats = async () => {
|
||||
try {
|
||||
const response = await get('/devices/stats')
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备统计失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备在线状态
|
||||
export const getDeviceOnlineStatus = async (deviceId) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/online-status`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备在线状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备位置
|
||||
export const updateDeviceLocation = async (deviceId, location) => {
|
||||
try {
|
||||
const response = await post(`/devices/${deviceId}/location`, location)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新设备位置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备历史数据
|
||||
export const getDeviceHistoryData = async (deviceId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/history`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备历史数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备实时数据
|
||||
export const getDeviceRealtimeData = async (deviceId) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/realtime`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备实时数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 控制设备
|
||||
export const controlDevice = async (deviceId, command) => {
|
||||
try {
|
||||
const response = await post(`/devices/${deviceId}/control`, command)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('控制设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备配置
|
||||
export const getDeviceConfig = async (deviceId) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/config`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备配置
|
||||
export const updateDeviceConfig = async (deviceId, config) => {
|
||||
try {
|
||||
const response = await post(`/devices/${deviceId}/config`, config)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新设备配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重启设备
|
||||
export const restartDevice = async (deviceId) => {
|
||||
try {
|
||||
const response = await post(`/devices/${deviceId}/restart`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('重启设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备日志
|
||||
export const getDeviceLogs = async (deviceId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/logs`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备日志失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备告警
|
||||
export const getDeviceAlerts = async (deviceId, params = {}) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/alerts`, params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备告警失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 批量操作设备
|
||||
export const batchOperateDevices = async (deviceIds, operation) => {
|
||||
try {
|
||||
const response = await post('/devices/batch-operate', {
|
||||
deviceIds,
|
||||
operation
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('批量操作设备失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备地图位置
|
||||
export const getDeviceMapLocations = async (params = {}) => {
|
||||
try {
|
||||
const response = await get('/devices/map-locations', params)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备地图位置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 设备固件更新
|
||||
export const updateDeviceFirmware = async (deviceId, firmwareVersion) => {
|
||||
try {
|
||||
const response = await post(`/devices/${deviceId}/firmware-update`, {
|
||||
firmwareVersion
|
||||
})
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('设备固件更新失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备固件版本
|
||||
export const getDeviceFirmwareVersion = async (deviceId) => {
|
||||
try {
|
||||
const response = await get(`/devices/${deviceId}/firmware-version`)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取设备固件版本失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getDeviceList,
|
||||
getDeviceDetail,
|
||||
createDevice,
|
||||
updateDevice,
|
||||
deleteDevice,
|
||||
getDeviceTypes,
|
||||
getDeviceStatuses,
|
||||
getDeviceStats,
|
||||
getDeviceOnlineStatus,
|
||||
updateDeviceLocation,
|
||||
getDeviceHistoryData,
|
||||
getDeviceRealtimeData,
|
||||
controlDevice,
|
||||
getDeviceConfig,
|
||||
updateDeviceConfig,
|
||||
restartDevice,
|
||||
getDeviceLogs,
|
||||
getDeviceAlerts,
|
||||
batchOperateDevices,
|
||||
getDeviceMapLocations,
|
||||
updateDeviceFirmware,
|
||||
getDeviceFirmwareVersion
|
||||
}
|
||||
7
mini_program/farm-monitor-dashboard/sitemap.json
Normal file
7
mini_program/farm-monitor-dashboard/sitemap.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,829 @@
|
||||
<template>
|
||||
<div class="cattle-batch">
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="back-btn" @click="goBack">
|
||||
<span class="back-icon"><</span>
|
||||
</div>
|
||||
<div class="title">批次设置</div>
|
||||
<div class="status-icons">
|
||||
<span class="icon">...</span>
|
||||
<span class="icon">-</span>
|
||||
<span class="icon">o</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-section">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input
|
||||
v-model="searchName"
|
||||
type="text"
|
||||
placeholder="请输入批次名称(精确匹配)"
|
||||
@input="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<div class="batch-actions" v-if="batches.length > 0">
|
||||
<div class="batch-controls">
|
||||
<label class="batch-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectAll"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
<span>全选</span>
|
||||
</label>
|
||||
<span class="selected-count">已选择 {{ selectedBatches.length }} 项</span>
|
||||
<button
|
||||
class="batch-delete-btn"
|
||||
@click="batchDelete"
|
||||
:disabled="selectedBatches.length === 0"
|
||||
>
|
||||
批量删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次列表 -->
|
||||
<div class="batches-list" v-if="batches.length > 0">
|
||||
<div
|
||||
v-for="(batch, index) in batches"
|
||||
:key="batch.id"
|
||||
class="batch-card"
|
||||
:class="{ selected: selectedBatches.includes(batch.id) }"
|
||||
>
|
||||
<div class="batch-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="batch.id"
|
||||
v-model="selectedBatches"
|
||||
/>
|
||||
</div>
|
||||
<div class="batch-content" @click="selectBatch(batch)">
|
||||
<div class="batch-header">
|
||||
<div class="batch-name">
|
||||
<span class="label">批次名称:</span>
|
||||
<span class="value">{{ batch.name || '--' }}</span>
|
||||
</div>
|
||||
<div class="batch-actions">
|
||||
<button class="edit-btn" @click.stop="editBatch(batch)">编辑</button>
|
||||
<button class="delete-btn" @click.stop="deleteBatch(batch)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">批次编号:</span>
|
||||
<span class="value">{{ batch.code || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">批次类型:</span>
|
||||
<span class="value">{{ getBatchTypeName(batch.type) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">目标数量:</span>
|
||||
<span class="value">{{ batch.targetCount || 0 }} 头</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">当前数量:</span>
|
||||
<span class="value">{{ batch.currentCount || 0 }} 头</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">负责人:</span>
|
||||
<span class="value">{{ batch.manager || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">开始日期:</span>
|
||||
<span class="value">{{ formatDate(batch.startDate) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预计结束:</span>
|
||||
<span class="value">{{ formatDate(batch.expectedEndDate) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">实际结束:</span>
|
||||
<span class="value">{{ formatDate(batch.actualEndDate) || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value status" :class="getStatusClass(batch.status)">
|
||||
{{ getBatchStatusName(batch.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="batch.remark">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ batch.remark }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">创建时间:</span>
|
||||
<span class="value">{{ formatDateTime(batch.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-else-if="!loading">
|
||||
<div class="empty-icon">📦</div>
|
||||
<div class="empty-text">暂无批次数据</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" v-if="loading">
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination" v-if="batches.length > 0">
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToPreviousPage"
|
||||
:disabled="currentPage <= 1"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToNextPage"
|
||||
:disabled="currentPage >= totalPages"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="bottom-actions">
|
||||
<button class="add-btn" @click="addBatch">
|
||||
添加批次
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cattleBatchApi } from '@/services/api'
|
||||
import auth from '@/utils/auth'
|
||||
import { getBatchTypeName, getBatchStatusName } from '@/utils/mapping'
|
||||
|
||||
export default {
|
||||
name: 'CattleBatch',
|
||||
data() {
|
||||
return {
|
||||
searchName: '',
|
||||
currentBatch: null,
|
||||
batches: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
pageSize: 10,
|
||||
searchTimer: null,
|
||||
selectedBatches: [],
|
||||
selectAll: false,
|
||||
showEditDialog: false,
|
||||
editingBatch: null
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 确保有有效的认证token
|
||||
await this.ensureAuthentication()
|
||||
this.loadBatches()
|
||||
},
|
||||
methods: {
|
||||
// 确保认证
|
||||
async ensureAuthentication() {
|
||||
try {
|
||||
// 检查是否已有token
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log('未认证,尝试获取测试token...')
|
||||
await auth.setTestToken()
|
||||
} else {
|
||||
// 验证现有token是否有效
|
||||
const isValid = await auth.validateCurrentToken()
|
||||
if (!isValid) {
|
||||
console.log('Token无效,重新获取...')
|
||||
await auth.setTestToken()
|
||||
}
|
||||
}
|
||||
console.log('认证完成,token:', auth.getToken()?.substring(0, 20) + '...')
|
||||
} catch (error) {
|
||||
console.error('认证失败:', error)
|
||||
// 即使认证失败也继续,让API请求处理错误
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
|
||||
// 获取批次类型中文名称
|
||||
getBatchTypeName(batchType) {
|
||||
return getBatchTypeName(batchType)
|
||||
},
|
||||
|
||||
// 获取批次状态中文名称
|
||||
getBatchStatusName(batchStatus) {
|
||||
return getBatchStatusName(batchStatus)
|
||||
},
|
||||
|
||||
// 获取状态样式类
|
||||
getStatusClass(status) {
|
||||
const statusClassMap = {
|
||||
'进行中': 'status-active',
|
||||
'已完成': 'status-completed',
|
||||
'已暂停': 'status-paused',
|
||||
'已取消': 'status-cancelled',
|
||||
'待开始': 'status-pending'
|
||||
}
|
||||
return statusClassMap[status] || ''
|
||||
},
|
||||
|
||||
// 选择批次
|
||||
selectBatch(batch) {
|
||||
this.currentBatch = batch
|
||||
},
|
||||
|
||||
// 全选/取消全选
|
||||
toggleSelectAll() {
|
||||
if (this.selectAll) {
|
||||
this.selectedBatches = this.batches.map(batch => batch.id)
|
||||
} else {
|
||||
this.selectedBatches = []
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑批次
|
||||
editBatch(batch) {
|
||||
this.editingBatch = batch
|
||||
this.showEditDialog = true
|
||||
},
|
||||
|
||||
// 删除批次
|
||||
async deleteBatch(batch) {
|
||||
if (confirm(`确定要删除批次 "${batch.name}" 吗?`)) {
|
||||
try {
|
||||
await cattleBatchApi.deleteBatch(batch.id)
|
||||
this.$message && this.$message.success('删除成功')
|
||||
this.loadBatches()
|
||||
} catch (error) {
|
||||
console.error('删除批次失败:', error)
|
||||
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加载批次列表
|
||||
async loadBatches() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
}
|
||||
|
||||
if (this.searchName) {
|
||||
params.search = this.searchName
|
||||
}
|
||||
|
||||
const response = await cattleBatchApi.getBatches(params)
|
||||
|
||||
if (response && response.success && response.data && response.data.list) {
|
||||
// 标准API响应格式
|
||||
let batches = response.data.list
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
batches = batches.filter(batch => batch.name === this.searchName)
|
||||
}
|
||||
|
||||
this.batches = batches
|
||||
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
|
||||
|
||||
// 显示第一条记录
|
||||
if (this.batches.length > 0) {
|
||||
this.currentBatch = this.batches[0]
|
||||
} else {
|
||||
this.currentBatch = null
|
||||
}
|
||||
} else if (response && response.data && Array.isArray(response.data)) {
|
||||
// 兼容直接返回数组的格式
|
||||
let batches = response.data
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
batches = batches.filter(batch => batch.name === this.searchName)
|
||||
}
|
||||
|
||||
this.batches = batches
|
||||
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
|
||||
|
||||
if (this.batches.length > 0) {
|
||||
this.currentBatch = this.batches[0]
|
||||
} else {
|
||||
this.currentBatch = null
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
// 如果直接返回数组
|
||||
let batches = response
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
batches = batches.filter(batch => batch.name === this.searchName)
|
||||
}
|
||||
|
||||
this.batches = batches
|
||||
this.totalPages = 1
|
||||
|
||||
if (this.batches.length > 0) {
|
||||
this.currentBatch = this.batches[0]
|
||||
} else {
|
||||
this.currentBatch = null
|
||||
}
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
this.batches = []
|
||||
this.currentBatch = null
|
||||
this.totalPages = 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载批次列表失败:', error)
|
||||
this.$message && this.$message.error('加载批次列表失败')
|
||||
this.batches = []
|
||||
this.currentBatch = null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索处理
|
||||
handleSearch() {
|
||||
// 防抖处理
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.currentPage = 1
|
||||
this.loadBatches()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 批量删除
|
||||
async batchDelete() {
|
||||
if (this.selectedBatches.length === 0) {
|
||||
this.$message && this.$message.warning('请选择要删除的批次')
|
||||
return
|
||||
}
|
||||
|
||||
if (confirm(`确定要删除选中的 ${this.selectedBatches.length} 个批次吗?`)) {
|
||||
try {
|
||||
await cattleBatchApi.batchDeleteBatches(this.selectedBatches)
|
||||
this.$message && this.$message.success('批量删除成功')
|
||||
this.selectedBatches = []
|
||||
this.selectAll = false
|
||||
this.loadBatches()
|
||||
} catch (error) {
|
||||
console.error('批量删除批次失败:', error)
|
||||
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 添加批次
|
||||
addBatch() {
|
||||
// 跳转到添加批次页面
|
||||
this.$router.push('/cattle-batch-add')
|
||||
},
|
||||
|
||||
// 上一页
|
||||
goToPreviousPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--
|
||||
this.loadBatches()
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
goToNextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++
|
||||
this.loadBatches()
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '--'
|
||||
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
})
|
||||
} catch (error) {
|
||||
return dateString
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime(dateTime) {
|
||||
if (!dateTime) return '--'
|
||||
|
||||
try {
|
||||
const date = new Date(dateTime)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
} catch (error) {
|
||||
return dateTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-batch {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
/* 顶部状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-section {
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 批量操作栏 */
|
||||
.batch-actions {
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.batch-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.batch-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.batch-delete-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.batch-delete-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 批次列表 */
|
||||
.batches-list {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.batch-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.batch-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.batch-card.selected {
|
||||
border: 2px solid #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.batch-checkbox {
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.batch-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.batch-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.batch-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-btn, .delete-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.batch-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
color: #666;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 底部操作按钮 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.batch-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.batch-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,781 @@
|
||||
<template>
|
||||
<div class="cattle-exit">
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="back-btn" @click="goBack">
|
||||
<span class="back-icon"><</span>
|
||||
</div>
|
||||
<div class="title">牛只离栏</div>
|
||||
<div class="status-icons">
|
||||
<span class="icon">...</span>
|
||||
<span class="icon">-</span>
|
||||
<span class="icon">o</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-section">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input
|
||||
v-model="searchEarNumber"
|
||||
type="text"
|
||||
placeholder="请输入耳号"
|
||||
@input="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<div class="batch-actions" v-if="records.length > 0">
|
||||
<div class="batch-controls">
|
||||
<label class="batch-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectAll"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
<span>全选</span>
|
||||
</label>
|
||||
<span class="selected-count">已选择 {{ selectedRecords.length }} 项</span>
|
||||
<button
|
||||
class="batch-delete-btn"
|
||||
@click="batchDelete"
|
||||
:disabled="selectedRecords.length === 0"
|
||||
>
|
||||
批量删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<div class="records-list" v-if="records.length > 0">
|
||||
<div
|
||||
v-for="(record, index) in records"
|
||||
:key="record.id"
|
||||
class="record-card"
|
||||
:class="{ selected: selectedRecords.includes(record.id) }"
|
||||
>
|
||||
<div class="record-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="record.id"
|
||||
v-model="selectedRecords"
|
||||
/>
|
||||
</div>
|
||||
<div class="record-content" @click="selectRecord(record)">
|
||||
<div class="record-header">
|
||||
<div class="ear-number">
|
||||
<span class="label">耳号:</span>
|
||||
<span class="value">{{ record.earNumber || '--' }}</span>
|
||||
</div>
|
||||
<div class="record-actions">
|
||||
<button class="edit-btn" @click.stop="editRecord(record)">编辑</button>
|
||||
<button class="delete-btn" @click.stop="deleteRecord(record)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">离栏日期:</span>
|
||||
<span class="value">{{ formatDateTime(record.exitDate) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">原栏舍:</span>
|
||||
<span class="value">{{ getOriginalPenName(record) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">离栏原因:</span>
|
||||
<span class="value">{{ getExitReasonName(record.exitReason) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">登记人:</span>
|
||||
<span class="value">{{ record.handler || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">处理方式:</span>
|
||||
<span class="value">{{ getDisposalMethodName(record.disposalMethod) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">登记日期:</span>
|
||||
<span class="value">{{ formatDateTime(record.created_at) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value status" :class="getStatusClass(record.status)">
|
||||
{{ getExitStatusName(record.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="record.remark">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ record.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-else-if="!loading">
|
||||
<div class="empty-icon">🐄</div>
|
||||
<div class="empty-text">暂无离栏记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" v-if="loading">
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination" v-if="records.length > 0">
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToPreviousPage"
|
||||
:disabled="currentPage <= 1"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToNextPage"
|
||||
:disabled="currentPage >= totalPages"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="bottom-actions">
|
||||
<button class="register-btn" @click="registerExit">
|
||||
离栏登记
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cattleExitApi } from '@/services/api'
|
||||
import auth from '@/utils/auth'
|
||||
import { getExitReasonName, getDisposalMethodName, getExitStatusName } from '@/utils/mapping'
|
||||
|
||||
export default {
|
||||
name: 'CattleExit',
|
||||
data() {
|
||||
return {
|
||||
searchEarNumber: '',
|
||||
currentRecord: null,
|
||||
records: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
pageSize: 10,
|
||||
searchTimer: null,
|
||||
selectedRecords: [],
|
||||
selectAll: false,
|
||||
showEditDialog: false,
|
||||
editingRecord: null
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 确保有有效的认证token
|
||||
await this.ensureAuthentication()
|
||||
this.loadExitRecords()
|
||||
},
|
||||
methods: {
|
||||
// 确保认证
|
||||
async ensureAuthentication() {
|
||||
try {
|
||||
// 检查是否已有token
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log('未认证,尝试获取测试token...')
|
||||
await auth.setTestToken()
|
||||
} else {
|
||||
// 验证现有token是否有效
|
||||
const isValid = await auth.validateCurrentToken()
|
||||
if (!isValid) {
|
||||
console.log('Token无效,重新获取...')
|
||||
await auth.setTestToken()
|
||||
}
|
||||
}
|
||||
console.log('认证完成,token:', auth.getToken()?.substring(0, 20) + '...')
|
||||
} catch (error) {
|
||||
console.error('认证失败:', error)
|
||||
// 即使认证失败也继续,让API请求处理错误
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
|
||||
// 获取原栏舍名称
|
||||
getOriginalPenName(record) {
|
||||
if (record && record.originalPen && record.originalPen.name) {
|
||||
return record.originalPen.name
|
||||
}
|
||||
return '--'
|
||||
},
|
||||
|
||||
// 获取离栏原因中文名称
|
||||
getExitReasonName(exitReason) {
|
||||
return getExitReasonName(exitReason)
|
||||
},
|
||||
|
||||
// 获取处理方式中文名称
|
||||
getDisposalMethodName(disposalMethod) {
|
||||
return getDisposalMethodName(disposalMethod)
|
||||
},
|
||||
|
||||
// 获取离栏状态中文名称
|
||||
getExitStatusName(exitStatus) {
|
||||
return getExitStatusName(exitStatus)
|
||||
},
|
||||
|
||||
// 获取状态样式类
|
||||
getStatusClass(status) {
|
||||
const statusClassMap = {
|
||||
'已确认': 'status-completed',
|
||||
'待确认': 'status-pending',
|
||||
'已取消': 'status-cancelled'
|
||||
}
|
||||
return statusClassMap[status] || ''
|
||||
},
|
||||
|
||||
// 选择记录
|
||||
selectRecord(record) {
|
||||
this.currentRecord = record
|
||||
},
|
||||
|
||||
// 全选/取消全选
|
||||
toggleSelectAll() {
|
||||
if (this.selectAll) {
|
||||
this.selectedRecords = this.records.map(record => record.id)
|
||||
} else {
|
||||
this.selectedRecords = []
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑记录
|
||||
editRecord(record) {
|
||||
this.editingRecord = record
|
||||
this.showEditDialog = true
|
||||
},
|
||||
|
||||
// 删除记录
|
||||
async deleteRecord(record) {
|
||||
if (confirm(`确定要删除耳号为 ${record.earNumber} 的离栏记录吗?`)) {
|
||||
try {
|
||||
await cattleExitApi.deleteExitRecord(record.id)
|
||||
this.$message && this.$message.success('删除成功')
|
||||
this.loadExitRecords()
|
||||
} catch (error) {
|
||||
console.error('删除离栏记录失败:', error)
|
||||
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加载离栏记录
|
||||
async loadExitRecords() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
}
|
||||
|
||||
if (this.searchEarNumber) {
|
||||
params.search = this.searchEarNumber
|
||||
}
|
||||
|
||||
const response = await cattleExitApi.getExitRecords(params)
|
||||
|
||||
if (response && response.success && response.data && response.data.list) {
|
||||
// 标准API响应格式
|
||||
this.records = response.data.list
|
||||
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
|
||||
|
||||
// 显示第一条记录
|
||||
if (this.records.length > 0) {
|
||||
this.currentRecord = this.records[0]
|
||||
} else {
|
||||
this.currentRecord = null
|
||||
}
|
||||
} else if (response && response.data && Array.isArray(response.data)) {
|
||||
// 兼容直接返回数组的格式
|
||||
this.records = response.data
|
||||
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
|
||||
|
||||
if (this.records.length > 0) {
|
||||
this.currentRecord = this.records[0]
|
||||
} else {
|
||||
this.currentRecord = null
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
// 如果直接返回数组
|
||||
this.records = response
|
||||
this.totalPages = 1
|
||||
|
||||
if (this.records.length > 0) {
|
||||
this.currentRecord = this.records[0]
|
||||
} else {
|
||||
this.currentRecord = null
|
||||
}
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
this.records = []
|
||||
this.currentRecord = null
|
||||
this.totalPages = 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载离栏记录失败:', error)
|
||||
this.$message && this.$message.error('加载离栏记录失败')
|
||||
this.records = []
|
||||
this.currentRecord = null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索处理
|
||||
handleSearch() {
|
||||
// 防抖处理
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.currentPage = 1
|
||||
this.loadExitRecords()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 批量删除
|
||||
async batchDelete() {
|
||||
if (this.selectedRecords.length === 0) {
|
||||
this.$message && this.$message.warning('请选择要删除的记录')
|
||||
return
|
||||
}
|
||||
|
||||
if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条离栏记录吗?`)) {
|
||||
try {
|
||||
await cattleExitApi.batchDeleteExitRecords(this.selectedRecords)
|
||||
this.$message && this.$message.success('批量删除成功')
|
||||
this.selectedRecords = []
|
||||
this.selectAll = false
|
||||
this.loadExitRecords()
|
||||
} catch (error) {
|
||||
console.error('批量删除离栏记录失败:', error)
|
||||
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 离栏登记
|
||||
registerExit() {
|
||||
// 跳转到离栏登记页面
|
||||
this.$router.push('/cattle-exit-register')
|
||||
},
|
||||
|
||||
// 上一页
|
||||
goToPreviousPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--
|
||||
this.loadExitRecords()
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
goToNextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++
|
||||
this.loadExitRecords()
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime(dateTime) {
|
||||
if (!dateTime) return '--'
|
||||
|
||||
try {
|
||||
const date = new Date(dateTime)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
} catch (error) {
|
||||
return dateTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-exit {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
/* 顶部状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-section {
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 批量操作栏 */
|
||||
.batch-actions {
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.batch-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.batch-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.batch-delete-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.batch-delete-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 记录列表 */
|
||||
.records-list {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.record-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.record-card.selected {
|
||||
border: 2px solid #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.record-checkbox {
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ear-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-btn, .delete-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
color: #666;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 底部操作按钮 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.register-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.record-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
799
mini_program/farm-monitor-dashboard/src/components/CattlePen.vue
Normal file
799
mini_program/farm-monitor-dashboard/src/components/CattlePen.vue
Normal file
@@ -0,0 +1,799 @@
|
||||
<template>
|
||||
<div class="cattle-pen">
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="back-btn" @click="goBack">
|
||||
<span class="back-icon"><</span>
|
||||
</div>
|
||||
<div class="title">栏舍设置</div>
|
||||
<div class="status-icons">
|
||||
<span class="icon">...</span>
|
||||
<span class="icon">-</span>
|
||||
<span class="icon">o</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-section">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input
|
||||
v-model="searchName"
|
||||
type="text"
|
||||
placeholder="请输入栏舍名称(精确匹配)"
|
||||
@input="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<div class="batch-actions" v-if="pens.length > 0">
|
||||
<div class="batch-controls">
|
||||
<label class="batch-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="selectAll"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
<span>全选</span>
|
||||
</label>
|
||||
<span class="selected-count">已选择 {{ selectedPens.length }} 项</span>
|
||||
<button
|
||||
class="batch-delete-btn"
|
||||
@click="batchDelete"
|
||||
:disabled="selectedPens.length === 0"
|
||||
>
|
||||
批量删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 栏舍列表 -->
|
||||
<div class="pens-list" v-if="pens.length > 0">
|
||||
<div
|
||||
v-for="(pen, index) in pens"
|
||||
:key="pen.id"
|
||||
class="pen-card"
|
||||
:class="{ selected: selectedPens.includes(pen.id) }"
|
||||
>
|
||||
<div class="pen-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="pen.id"
|
||||
v-model="selectedPens"
|
||||
/>
|
||||
</div>
|
||||
<div class="pen-content" @click="selectPen(pen)">
|
||||
<div class="pen-header">
|
||||
<div class="pen-name">
|
||||
<span class="label">栏舍名称:</span>
|
||||
<span class="value">{{ pen.name || '--' }}</span>
|
||||
</div>
|
||||
<div class="pen-actions">
|
||||
<button class="edit-btn" @click.stop="editPen(pen)">编辑</button>
|
||||
<button class="delete-btn" @click.stop="deletePen(pen)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pen-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">栏舍编号:</span>
|
||||
<span class="value">{{ pen.code || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">栏舍类型:</span>
|
||||
<span class="value">{{ getPenTypeName(pen.type) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">容量:</span>
|
||||
<span class="value">{{ pen.capacity || 0 }} 头</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">当前数量:</span>
|
||||
<span class="value">{{ pen.currentCount || 0 }} 头</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">面积:</span>
|
||||
<span class="value">{{ pen.area || '--' }} ㎡</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">位置:</span>
|
||||
<span class="value">{{ pen.location || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value status" :class="getStatusClass(pen.status)">
|
||||
{{ getPenStatusName(pen.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="pen.remark">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ pen.remark }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">创建时间:</span>
|
||||
<span class="value">{{ formatDateTime(pen.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-else-if="!loading">
|
||||
<div class="empty-icon">🏠</div>
|
||||
<div class="empty-text">暂无栏舍数据</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-state" v-if="loading">
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination" v-if="pens.length > 0">
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToPreviousPage"
|
||||
:disabled="currentPage <= 1"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页,共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
@click="goToNextPage"
|
||||
:disabled="currentPage >= totalPages"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="bottom-actions">
|
||||
<button class="add-btn" @click="addPen">
|
||||
添加栏舍
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cattlePenApi } from '@/services/api'
|
||||
import auth from '@/utils/auth'
|
||||
import { getPenTypeName, getPenStatusName } from '@/utils/mapping'
|
||||
|
||||
export default {
|
||||
name: 'CattlePen',
|
||||
data() {
|
||||
return {
|
||||
searchName: '',
|
||||
currentPen: null,
|
||||
pens: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
pageSize: 10,
|
||||
searchTimer: null,
|
||||
selectedPens: [],
|
||||
selectAll: false,
|
||||
showEditDialog: false,
|
||||
editingPen: null
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 确保有有效的认证token
|
||||
await this.ensureAuthentication()
|
||||
this.loadPens()
|
||||
},
|
||||
methods: {
|
||||
// 确保认证
|
||||
async ensureAuthentication() {
|
||||
try {
|
||||
// 检查是否已有token
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log('未认证,尝试获取测试token...')
|
||||
await auth.setTestToken()
|
||||
} else {
|
||||
// 验证现有token是否有效
|
||||
const isValid = await auth.validateCurrentToken()
|
||||
if (!isValid) {
|
||||
console.log('Token无效,重新获取...')
|
||||
await auth.setTestToken()
|
||||
}
|
||||
}
|
||||
console.log('认证完成,token:', auth.getToken()?.substring(0, 20) + '...')
|
||||
} catch (error) {
|
||||
console.error('认证失败:', error)
|
||||
// 即使认证失败也继续,让API请求处理错误
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
|
||||
// 获取栏舍类型中文名称
|
||||
getPenTypeName(penType) {
|
||||
return getPenTypeName(penType)
|
||||
},
|
||||
|
||||
// 获取栏舍状态中文名称
|
||||
getPenStatusName(penStatus) {
|
||||
return getPenStatusName(penStatus)
|
||||
},
|
||||
|
||||
// 获取状态样式类
|
||||
getStatusClass(status) {
|
||||
const statusClassMap = {
|
||||
'启用': 'status-active',
|
||||
'停用': 'status-inactive',
|
||||
'维修': 'status-maintenance',
|
||||
'废弃': 'status-abandoned'
|
||||
}
|
||||
return statusClassMap[status] || ''
|
||||
},
|
||||
|
||||
// 选择栏舍
|
||||
selectPen(pen) {
|
||||
this.currentPen = pen
|
||||
},
|
||||
|
||||
// 全选/取消全选
|
||||
toggleSelectAll() {
|
||||
if (this.selectAll) {
|
||||
this.selectedPens = this.pens.map(pen => pen.id)
|
||||
} else {
|
||||
this.selectedPens = []
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑栏舍
|
||||
editPen(pen) {
|
||||
this.editingPen = pen
|
||||
this.showEditDialog = true
|
||||
},
|
||||
|
||||
// 删除栏舍
|
||||
async deletePen(pen) {
|
||||
if (confirm(`确定要删除栏舍 "${pen.name}" 吗?`)) {
|
||||
try {
|
||||
await cattlePenApi.deletePen(pen.id)
|
||||
this.$message && this.$message.success('删除成功')
|
||||
this.loadPens()
|
||||
} catch (error) {
|
||||
console.error('删除栏舍失败:', error)
|
||||
this.$message && this.$message.error('删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加载栏舍列表
|
||||
async loadPens() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
}
|
||||
|
||||
if (this.searchName) {
|
||||
params.search = this.searchName
|
||||
}
|
||||
|
||||
const response = await cattlePenApi.getPens(params)
|
||||
|
||||
if (response && response.success && response.data && response.data.list) {
|
||||
// 标准API响应格式
|
||||
let pens = response.data.list
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
pens = pens.filter(pen => pen.name === this.searchName)
|
||||
}
|
||||
|
||||
this.pens = pens
|
||||
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
|
||||
|
||||
// 显示第一条记录
|
||||
if (this.pens.length > 0) {
|
||||
this.currentPen = this.pens[0]
|
||||
} else {
|
||||
this.currentPen = null
|
||||
}
|
||||
} else if (response && response.data && Array.isArray(response.data)) {
|
||||
// 兼容直接返回数组的格式
|
||||
let pens = response.data
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
pens = pens.filter(pen => pen.name === this.searchName)
|
||||
}
|
||||
|
||||
this.pens = pens
|
||||
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
|
||||
|
||||
if (this.pens.length > 0) {
|
||||
this.currentPen = this.pens[0]
|
||||
} else {
|
||||
this.currentPen = null
|
||||
}
|
||||
} else if (Array.isArray(response)) {
|
||||
// 如果直接返回数组
|
||||
let pens = response
|
||||
|
||||
// 如果进行了搜索,进行精确匹配过滤
|
||||
if (this.searchName) {
|
||||
pens = pens.filter(pen => pen.name === this.searchName)
|
||||
}
|
||||
|
||||
this.pens = pens
|
||||
this.totalPages = 1
|
||||
|
||||
if (this.pens.length > 0) {
|
||||
this.currentPen = this.pens[0]
|
||||
} else {
|
||||
this.currentPen = null
|
||||
}
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
this.pens = []
|
||||
this.currentPen = null
|
||||
this.totalPages = 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载栏舍列表失败:', error)
|
||||
this.$message && this.$message.error('加载栏舍列表失败')
|
||||
this.pens = []
|
||||
this.currentPen = null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索处理
|
||||
handleSearch() {
|
||||
// 防抖处理
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.currentPage = 1
|
||||
this.loadPens()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 批量删除
|
||||
async batchDelete() {
|
||||
if (this.selectedPens.length === 0) {
|
||||
this.$message && this.$message.warning('请选择要删除的栏舍')
|
||||
return
|
||||
}
|
||||
|
||||
if (confirm(`确定要删除选中的 ${this.selectedPens.length} 个栏舍吗?`)) {
|
||||
try {
|
||||
await cattlePenApi.batchDeletePens(this.selectedPens)
|
||||
this.$message && this.$message.success('批量删除成功')
|
||||
this.selectedPens = []
|
||||
this.selectAll = false
|
||||
this.loadPens()
|
||||
} catch (error) {
|
||||
console.error('批量删除栏舍失败:', error)
|
||||
this.$message && this.$message.error('批量删除失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 添加栏舍
|
||||
addPen() {
|
||||
// 跳转到添加栏舍页面
|
||||
this.$router.push('/cattle-pen-add')
|
||||
},
|
||||
|
||||
// 上一页
|
||||
goToPreviousPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--
|
||||
this.loadPens()
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
goToNextPage() {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.currentPage++
|
||||
this.loadPens()
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime(dateTime) {
|
||||
if (!dateTime) return '--'
|
||||
|
||||
try {
|
||||
const date = new Date(dateTime)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
} catch (error) {
|
||||
return dateTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-pen {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
/* 顶部状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-section {
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 批量操作栏 */
|
||||
.batch-actions {
|
||||
padding: 12px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.batch-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.batch-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.batch-delete-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.batch-delete-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 栏舍列表 */
|
||||
.pens-list {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.pen-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pen-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.pen-card.selected {
|
||||
border: 2px solid #007bff;
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.pen-checkbox {
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pen-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pen-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pen-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-btn, .delete-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pen-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
color: #666;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-maintenance {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-abandoned {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 底部操作按钮 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 20px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.pen-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pen-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pen-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -53,16 +53,16 @@
|
||||
<span class="detail-value">{{ cattle.birthdayFormatted || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">品类:</span>
|
||||
<span class="detail-value">{{ cattle.categoryName || '--' }}</span>
|
||||
<span class="detail-label">品系:</span>
|
||||
<span class="detail-value">{{ cattle.strainName || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">品种:</span>
|
||||
<span class="detail-value">{{ cattle.breedName || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">品系:</span>
|
||||
<span class="detail-value">{{ cattle.strainName || '--' }}</span>
|
||||
<span class="detail-label">品类:</span>
|
||||
<span class="detail-value">{{ cattle.categoryName || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">生理阶段:</span>
|
||||
@@ -135,6 +135,7 @@ import {
|
||||
getSourceName,
|
||||
formatDate
|
||||
} from '@/utils/mapping'
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'CattleProfile',
|
||||
@@ -152,10 +153,34 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
// 确保有有效的认证token
|
||||
await this.ensureAuthentication()
|
||||
this.loadCattleList()
|
||||
},
|
||||
methods: {
|
||||
// 确保认证
|
||||
async ensureAuthentication() {
|
||||
try {
|
||||
// 检查是否已有token
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log('未认证,尝试获取测试token...')
|
||||
await auth.setTestToken()
|
||||
} else {
|
||||
// 验证现有token是否有效
|
||||
const isValid = await auth.validateCurrentToken()
|
||||
if (!isValid) {
|
||||
console.log('Token无效,重新获取...')
|
||||
await auth.setTestToken()
|
||||
}
|
||||
}
|
||||
console.log('认证完成,token:', auth.getToken()?.substring(0, 20) + '...')
|
||||
} catch (error) {
|
||||
console.error('认证失败:', error)
|
||||
// 即使认证失败也继续,让API请求处理错误
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
@@ -229,14 +254,14 @@ export default {
|
||||
birthdayFormatted: formatDate(cattle.birthday),
|
||||
// 性别映射
|
||||
sexName: getSexName(cattle.sex),
|
||||
// 品类映射
|
||||
categoryName: getCategoryName(cattle.cate),
|
||||
// 品系映射(strain字段,显示为品系)
|
||||
strainName: cattle.strain || '--',
|
||||
// 品种名称(从API返回的varieties字段)
|
||||
breedName: getBreedName(cattle.varieties),
|
||||
// 品系映射
|
||||
strainName: getStrainName(cattle.strain),
|
||||
// 生理阶段
|
||||
physiologicalStage: getPhysiologicalStage(cattle.level),
|
||||
breedName: cattle.varieties || '--',
|
||||
// 品类映射(cate字段,显示为品类)
|
||||
categoryName: getCategoryName(cattle.cate),
|
||||
// 生理阶段(parity字段)
|
||||
physiologicalStage: getPhysiologicalStage(cattle.parity),
|
||||
// 来源映射
|
||||
sourceName: getSourceName(cattle.source),
|
||||
// 设备编号(如果有的话)
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
|
||||
<script>
|
||||
import { cattleTransferApi } from '@/services/api'
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'CattleTransfer',
|
||||
@@ -225,10 +226,34 @@ export default {
|
||||
editingRecord: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
// 确保有有效的认证token
|
||||
await this.ensureAuthentication()
|
||||
this.loadTransferRecords()
|
||||
},
|
||||
methods: {
|
||||
// 确保认证
|
||||
async ensureAuthentication() {
|
||||
try {
|
||||
// 检查是否已有token
|
||||
if (!auth.isAuthenticated()) {
|
||||
console.log('未认证,尝试获取测试token...')
|
||||
await auth.setTestToken()
|
||||
} else {
|
||||
// 验证现有token是否有效
|
||||
const isValid = await auth.validateCurrentToken()
|
||||
if (!isValid) {
|
||||
console.log('Token无效,重新获取...')
|
||||
await auth.setTestToken()
|
||||
}
|
||||
}
|
||||
console.log('认证完成,token:', auth.getToken()?.substring(0, 20) + '...')
|
||||
} catch (error) {
|
||||
console.error('认证失败:', error)
|
||||
// 即使认证失败也继续,让API请求处理错误
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
@@ -267,11 +292,22 @@ export default {
|
||||
|
||||
const response = await cattleTransferApi.getTransferRecords(params)
|
||||
|
||||
if (response && response.data) {
|
||||
if (response && response.success && response.data && response.data.list) {
|
||||
// 标准API响应格式
|
||||
this.records = response.data.list
|
||||
this.totalPages = response.data.totalPages || Math.ceil(response.data.total / this.pageSize) || 1
|
||||
|
||||
// 显示第一条记录
|
||||
if (this.records.length > 0) {
|
||||
this.currentRecord = this.records[0]
|
||||
} else {
|
||||
this.currentRecord = null
|
||||
}
|
||||
} else if (response && response.data && Array.isArray(response.data)) {
|
||||
// 兼容直接返回数组的格式
|
||||
this.records = response.data
|
||||
this.totalPages = response.totalPages || Math.ceil(response.total / this.pageSize) || 1
|
||||
|
||||
// 显示第一条记录
|
||||
if (this.records.length > 0) {
|
||||
this.currentRecord = this.records[0]
|
||||
} else {
|
||||
@@ -288,6 +324,7 @@ export default {
|
||||
this.currentRecord = null
|
||||
}
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
this.records = []
|
||||
this.currentRecord = null
|
||||
this.totalPages = 1
|
||||
|
||||
@@ -123,12 +123,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { alertApi } from '@/services/api'
|
||||
import { getAlertTypeName } from '@/utils/mapping'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
activeAlertTab: 'collar',
|
||||
activeNav: 'home',
|
||||
alertStats: {
|
||||
collar: {},
|
||||
ear: {},
|
||||
ankle: {},
|
||||
host: {}
|
||||
},
|
||||
loading: false,
|
||||
alertTabs: [
|
||||
{ key: 'collar', name: '项圈预警' },
|
||||
{ key: 'ear', name: '耳标预警' },
|
||||
@@ -145,6 +155,7 @@ export default {
|
||||
smartTools: [
|
||||
{ key: 'fence', icon: '🎯', label: '电子围栏', color: '#ff9500' },
|
||||
{ key: 'smart-eartag-alert', icon: '⚠️', label: '智能耳标预警', color: '#ff3b30' },
|
||||
{ key: 'smart-collar-alert', icon: '⚠️', label: '智能项圈预警', color: '#ff3b30' },
|
||||
{ key: 'scan', icon: '🛡️', label: '扫码溯源', color: '#007aff' },
|
||||
{ key: 'photo', icon: '📷', label: '档案拍照', color: '#ff3b30' },
|
||||
{ key: 'detect', icon: '📊', label: '检测工具', color: '#af52de' },
|
||||
@@ -164,41 +175,85 @@ export default {
|
||||
},
|
||||
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: 'low_battery', icon: '🔋', label: '电量偏低', value: '2', urgent: false }
|
||||
],
|
||||
ear: [
|
||||
{ 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: 'low_battery', icon: '🔋', label: '电量偏低', value: '2', urgent: false }
|
||||
],
|
||||
ankle: [
|
||||
{ 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: 'low_battery', icon: '🔋', label: '电量偏低', value: '2', urgent: false }
|
||||
],
|
||||
host: [
|
||||
const stats = this.alertStats[this.activeAlertTab] || {}
|
||||
|
||||
// 根据不同的预警类型生成预警卡片 - 与PC端保持一致
|
||||
if (this.activeAlertTab === 'collar') {
|
||||
return [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
|
||||
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
|
||||
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
|
||||
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
|
||||
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true },
|
||||
{ key: 'wear', icon: '✂️', label: '佩戴异常预警', value: stats.wearOff || 0, urgent: false }
|
||||
]
|
||||
} else if (this.activeAlertTab === 'ear') {
|
||||
return [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
|
||||
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
|
||||
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
|
||||
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
|
||||
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true }
|
||||
]
|
||||
} else if (this.activeAlertTab === 'ankle') {
|
||||
// 脚环预警暂时使用耳标数据,因为API中没有单独的脚环统计
|
||||
return [
|
||||
{ key: 'not_collected', icon: '📊', label: '今日未被采集', value: stats.totalDevices - stats.totalAlerts || 0, urgent: false },
|
||||
{ key: 'battery', icon: '🔋', label: '低电量预警', value: stats.lowBattery || 0, urgent: false },
|
||||
{ key: 'offline', icon: '📴', label: '离线预警', value: stats.offline || 0, urgent: false },
|
||||
{ key: 'temperature', icon: '🌡️', label: '温度预警', value: (stats.highTemperature || 0) + (stats.lowTemperature || 0), urgent: false },
|
||||
{ key: 'movement', icon: '📉', label: '异常运动预警', value: stats.abnormalMovement || 0, urgent: true }
|
||||
]
|
||||
} else if (this.activeAlertTab === 'host') {
|
||||
// 主机预警暂时使用固定数据,因为API中没有主机统计
|
||||
return [
|
||||
{ key: 'offline', icon: '📴', label: '主机离线', value: '0', urgent: false },
|
||||
{ key: 'low_storage', icon: '💾', label: '存储空间不足', value: '1', urgent: true },
|
||||
{ key: 'network_error', icon: '🌐', label: '网络异常', value: '0', urgent: false }
|
||||
]
|
||||
}
|
||||
return alertData[this.activeAlertTab] || []
|
||||
|
||||
return []
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadAlertStats()
|
||||
},
|
||||
methods: {
|
||||
// 加载预警统计数据
|
||||
async loadAlertStats() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 并行加载项圈和耳标预警数据
|
||||
const [collarResponse, eartagResponse] = await Promise.all([
|
||||
alertApi.getCollarStats(),
|
||||
alertApi.getEartagStats()
|
||||
])
|
||||
|
||||
if (collarResponse && collarResponse.success) {
|
||||
this.alertStats.collar = collarResponse.data
|
||||
}
|
||||
|
||||
if (eartagResponse && eartagResponse.success) {
|
||||
this.alertStats.ear = eartagResponse.data
|
||||
// 脚环预警暂时使用耳标数据
|
||||
this.alertStats.ankle = eartagResponse.data
|
||||
}
|
||||
|
||||
console.log('预警统计数据加载成功:', this.alertStats)
|
||||
} catch (error) {
|
||||
console.error('加载预警统计数据失败:', error)
|
||||
// 如果API调用失败,使用默认值
|
||||
this.alertStats = {
|
||||
collar: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0, wearOff: 0 },
|
||||
ear: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0 },
|
||||
ankle: { totalDevices: 0, totalAlerts: 0, lowBattery: 0, offline: 0, highTemperature: 0, lowTemperature: 0, abnormalMovement: 0 },
|
||||
host: {}
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleDeviceClick(device) {
|
||||
console.log('点击设备:', device.label)
|
||||
// 根据设备类型跳转到不同页面
|
||||
@@ -241,6 +296,12 @@ export default {
|
||||
case 'api-test':
|
||||
this.$router.push('/api-test')
|
||||
break
|
||||
case 'smart-eartag-alert':
|
||||
this.$router.push('/smart-eartag-alert')
|
||||
break
|
||||
case 'smart-collar-alert':
|
||||
this.$router.push('/smart-collar-alert')
|
||||
break
|
||||
default:
|
||||
console.log('未知工具类型')
|
||||
}
|
||||
|
||||
@@ -188,6 +188,15 @@ export default {
|
||||
} else if (animalType === 'cattle' && func.key === 'transfer') {
|
||||
// 牛只转栏记录跳转
|
||||
this.$router.push('/cattle-transfer')
|
||||
} else if (animalType === 'cattle' && func.key === 'departure') {
|
||||
// 牛只离栏记录跳转
|
||||
this.$router.push('/cattle-exit')
|
||||
} else if (animalType === 'cattle' && func.key === 'pen_setting') {
|
||||
// 牛只栏舍设置跳转
|
||||
this.$router.push('/cattle-pen')
|
||||
} else if (animalType === 'cattle' && func.key === 'batch_setting') {
|
||||
// 牛只批次设置跳转
|
||||
this.$router.push('/cattle-batch')
|
||||
} else if (animalType === 'pig' && func.key === 'archive') {
|
||||
// 猪档案跳转(待实现)
|
||||
console.log('跳转到猪档案页面')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -104,8 +104,18 @@
|
||||
<div class="alert-title">{{ alert.title }}</div>
|
||||
<div class="alert-description">{{ alert.description }}</div>
|
||||
<div class="alert-device">
|
||||
<span class="device-label">设备ID:</span>
|
||||
<span class="device-id">{{ alert.deviceId }}</span>
|
||||
<span class="device-label">耳标编号:</span>
|
||||
<span class="device-id">{{ alert.eartagNumber || alert.deviceId }}</span>
|
||||
</div>
|
||||
<div class="alert-details">
|
||||
<span class="alert-type">{{ getAlertTypeName(alert.alertType) }}</span>
|
||||
<span class="alert-level">{{ getAlertSeverityName(alert.alertLevel) }}</span>
|
||||
<span class="alert-time">{{ alert.alertTime || formatTime(alert.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="alert-metrics">
|
||||
<span v-if="alert.battery" class="metric">电量: {{ alert.battery }}%</span>
|
||||
<span v-if="alert.temperature" class="metric">温度: {{ alert.temperature }}°C</span>
|
||||
<span v-if="alert.dailySteps" class="metric">步数: {{ alert.dailySteps }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -151,19 +161,23 @@
|
||||
<div class="modal-body">
|
||||
<div class="detail-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="detail-item">
|
||||
<span class="label">耳标编号:</span>
|
||||
<span class="value">{{ selectedAlert.eartagNumber || selectedAlert.deviceId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警类型:</span>
|
||||
<span class="value">{{ getAlertTypeName(selectedAlert.alertType) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警级别:</span>
|
||||
<span class="value" :class="`severity-${selectedAlert.severity}`">
|
||||
{{ getSeverityText(selectedAlert.severity) }}
|
||||
{{ getAlertSeverityName(selectedAlert.alertLevel) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">设备ID:</span>
|
||||
<span class="value">{{ selectedAlert.deviceId }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警时间:</span>
|
||||
<span class="value">{{ formatTime(selectedAlert.createdAt) }}</span>
|
||||
<span class="value">{{ selectedAlert.alertTime || formatTime(selectedAlert.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">处理状态:</span>
|
||||
@@ -171,6 +185,30 @@
|
||||
{{ getStatusText(selectedAlert.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">处理人:</span>
|
||||
<span class="value">{{ selectedAlert.handler || '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>设备信息</h4>
|
||||
<div class="detail-item">
|
||||
<span class="label">设备电量:</span>
|
||||
<span class="value">{{ selectedAlert.battery || '--' }}%</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">设备温度:</span>
|
||||
<span class="value">{{ selectedAlert.temperature || '--' }}°C</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">当日步数:</span>
|
||||
<span class="value">{{ selectedAlert.dailySteps || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">位置坐标:</span>
|
||||
<span class="value">{{ selectedAlert.longitude && selectedAlert.latitude ? `${selectedAlert.longitude}, ${selectedAlert.latitude}` : '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
@@ -208,7 +246,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { alertService } from '@/services/alertService'
|
||||
import { alertApi } from '@/services/api'
|
||||
import { getAlertSeverityName, getAlertStatusName, getAlertTypeName } from '@/utils/mapping'
|
||||
|
||||
export default {
|
||||
name: 'SmartEartagAlert',
|
||||
@@ -285,67 +324,58 @@ export default {
|
||||
async loadAlerts() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 暂时使用模拟数据,避免API连接问题
|
||||
this.alerts = [
|
||||
{
|
||||
id: 1,
|
||||
deviceId: 'EARTAG001',
|
||||
title: '体温异常预警',
|
||||
description: '设备EARTAG001检测到体温异常,当前体温39.2°C,超过正常范围',
|
||||
severity: 'critical',
|
||||
status: 'unresolved',
|
||||
createdAt: new Date().toISOString(),
|
||||
data: {
|
||||
temperature: '39.2°C',
|
||||
normalRange: '36.5-38.5°C',
|
||||
location: '牛舍A区',
|
||||
battery: '85%'
|
||||
const response = await alertApi.getEartagAlerts({
|
||||
page: 1,
|
||||
limit: 50
|
||||
})
|
||||
|
||||
if (response && response.success && response.data) {
|
||||
// 按照PC端的数据格式进行转换
|
||||
this.alerts = response.data.map(alert => {
|
||||
// 格式化时间 - 与PC端保持一致
|
||||
let alertTime = ''
|
||||
if (alert.alertTime || alert.alert_time || alert.created_at) {
|
||||
const timeValue = alert.alertTime || alert.alert_time || alert.created_at
|
||||
if (typeof timeValue === 'number') {
|
||||
// Unix时间戳转换
|
||||
alertTime = new Date(timeValue * 1000).toLocaleString('zh-CN')
|
||||
} else if (typeof timeValue === 'string') {
|
||||
// 字符串时间转换
|
||||
const date = new Date(timeValue)
|
||||
if (!isNaN(date.getTime())) {
|
||||
alertTime = date.toLocaleString('zh-CN')
|
||||
} else {
|
||||
alertTime = timeValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
deviceId: 'EARTAG002',
|
||||
title: '活动量异常',
|
||||
description: '设备EARTAG002检测到活动量异常,24小时内活动量仅为平时的30%',
|
||||
severity: 'warning',
|
||||
status: 'unresolved',
|
||||
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||
data: {
|
||||
activityLevel: '30%',
|
||||
normalLevel: '100%',
|
||||
location: '牛舍B区',
|
||||
battery: '92%'
|
||||
|
||||
return {
|
||||
id: alert.id || `${alert.deviceId || alert.eartagNumber || alert.sn}_${alert.alertType || 'unknown'}`,
|
||||
deviceId: alert.deviceId || alert.eartagNumber || alert.sn || '',
|
||||
eartagNumber: alert.eartagNumber || alert.deviceId || alert.sn || '',
|
||||
title: alert.title || alert.alertContent || alert.message || alert.description || '系统预警',
|
||||
description: alert.description || alert.alertContent || alert.message || '系统预警',
|
||||
alertType: alert.alertType || alert.alert_type || 'unknown',
|
||||
alertLevel: alert.alertLevel || alert.alert_level || 'high',
|
||||
severity: alert.severity || alert.alertLevel || 'warning',
|
||||
status: alert.status || (alert.processed ? 'resolved' : 'unresolved'),
|
||||
createdAt: alert.createdAt || alert.created_at,
|
||||
alertTime: alertTime,
|
||||
battery: alert.battery || alert.batteryLevel || '',
|
||||
temperature: alert.temperature || alert.temp || '',
|
||||
dailySteps: alert.dailySteps || alert.steps || '',
|
||||
longitude: alert.longitude || 0,
|
||||
latitude: alert.latitude || 0,
|
||||
handler: alert.handler || alert.processor || alert.handler_name || alert.operator || '',
|
||||
data: alert.data || {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
deviceId: 'EARTAG003',
|
||||
title: '设备离线预警',
|
||||
description: '设备EARTAG003已离线超过2小时,请检查设备状态',
|
||||
severity: 'critical',
|
||||
status: 'resolved',
|
||||
createdAt: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
|
||||
data: {
|
||||
lastSeen: '2小时前',
|
||||
location: '牛舍C区',
|
||||
battery: '15%'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
deviceId: 'EARTAG004',
|
||||
title: '位置异常',
|
||||
description: '设备EARTAG004检测到位置异常,可能已离开指定区域',
|
||||
severity: 'warning',
|
||||
status: 'unresolved',
|
||||
createdAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
|
||||
data: {
|
||||
currentLocation: '牧场外围',
|
||||
allowedArea: '牛舍区域',
|
||||
distance: '500米'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
this.alerts = []
|
||||
}
|
||||
|
||||
console.log('预警数据加载成功:', this.alerts)
|
||||
} catch (error) {
|
||||
console.error('加载预警数据失败:', error)
|
||||
@@ -367,38 +397,46 @@ export default {
|
||||
if (!this.selectedAlert) return
|
||||
|
||||
try {
|
||||
// 暂时模拟API调用
|
||||
console.log('处理预警:', this.selectedAlert.id)
|
||||
this.selectedAlert.status = 'resolved'
|
||||
|
||||
// 更新列表中的状态
|
||||
const alertIndex = this.alerts.findIndex(alert => alert.id === this.selectedAlert.id)
|
||||
if (alertIndex !== -1) {
|
||||
const response = await alertApi.handleEartagAlert(this.selectedAlert.id, {
|
||||
status: 'resolved',
|
||||
handledAt: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (response && response.success) {
|
||||
this.selectedAlert.status = 'resolved'
|
||||
|
||||
// 更新列表中的状态
|
||||
const alertIndex = this.alerts.findIndex(alert => alert.id === this.selectedAlert.id)
|
||||
if (alertIndex !== -1) {
|
||||
this.$set(this.alerts, alertIndex, { ...this.selectedAlert })
|
||||
}
|
||||
|
||||
this.closeModal()
|
||||
console.log('预警已标记为已处理')
|
||||
} else {
|
||||
console.error('处理预警失败:', response?.message || '未知错误')
|
||||
}
|
||||
|
||||
this.closeModal()
|
||||
console.log('预警已标记为已处理')
|
||||
} catch (error) {
|
||||
console.error('处理预警失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
getSeverityText(severity) {
|
||||
const severityMap = {
|
||||
critical: '严重',
|
||||
warning: '一般',
|
||||
info: '信息'
|
||||
}
|
||||
return severityMap[severity] || severity
|
||||
return getAlertSeverityName(severity)
|
||||
},
|
||||
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
unresolved: '未处理',
|
||||
resolved: '已处理'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
return getAlertStatusName(status)
|
||||
},
|
||||
|
||||
getAlertTypeName(alertType) {
|
||||
return getAlertTypeName(alertType)
|
||||
},
|
||||
|
||||
getAlertSeverityName(alertLevel) {
|
||||
return getAlertSeverityName(alertLevel)
|
||||
},
|
||||
|
||||
formatTime(timestamp) {
|
||||
@@ -694,6 +732,44 @@ export default {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.alert-type, .alert-level, .alert-time {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-type {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
background: #fff2e8;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.alert-metrics {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -23,6 +23,10 @@ import CattleAdd from '@/components/CattleAdd.vue'
|
||||
import CattleTest from '@/components/CattleTest.vue'
|
||||
import CattleTransfer from '@/components/CattleTransfer.vue'
|
||||
import CattleTransferRegister from '@/components/CattleTransferRegister.vue'
|
||||
import CattleExit from '@/components/CattleExit.vue'
|
||||
import CattlePen from '@/components/CattlePen.vue'
|
||||
import CattleBatch from '@/components/CattleBatch.vue'
|
||||
import SmartCollarAlert from '@/components/SmartCollarAlert.vue'
|
||||
import ApiTestPage from '@/components/ApiTestPage.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
@@ -143,6 +147,26 @@ const routes = [
|
||||
name: 'CattleTransferRegister',
|
||||
component: CattleTransferRegister
|
||||
},
|
||||
{
|
||||
path: '/cattle-exit',
|
||||
name: 'CattleExit',
|
||||
component: CattleExit
|
||||
},
|
||||
{
|
||||
path: '/cattle-pen',
|
||||
name: 'CattlePen',
|
||||
component: CattlePen
|
||||
},
|
||||
{
|
||||
path: '/cattle-batch',
|
||||
name: 'CattleBatch',
|
||||
component: CattleBatch
|
||||
},
|
||||
{
|
||||
path: '/smart-collar-alert',
|
||||
name: 'SmartCollarAlert',
|
||||
component: SmartCollarAlert
|
||||
},
|
||||
{
|
||||
path: '/api-test-page',
|
||||
name: 'ApiTestPage',
|
||||
|
||||
@@ -3,7 +3,7 @@ import auth from '@/utils/auth'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:5300/api',
|
||||
baseURL: process.env.VUE_APP_BASE_URL || '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -278,4 +278,186 @@ export const cattleTransferApi = {
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只离栏记录相关API
|
||||
export const cattleExitApi = {
|
||||
// 获取离栏记录列表
|
||||
getExitRecords: (params = {}) => {
|
||||
return get('/cattle-exit-records', params)
|
||||
},
|
||||
|
||||
// 根据耳号搜索离栏记录
|
||||
searchExitRecordsByEarNumber: (earNumber, params = {}) => {
|
||||
return get('/cattle-exit-records', { earNumber, ...params })
|
||||
},
|
||||
|
||||
// 获取离栏记录详情
|
||||
getExitRecordDetail: (id) => {
|
||||
return get(`/cattle-exit-records/${id}`)
|
||||
},
|
||||
|
||||
// 创建离栏记录
|
||||
createExitRecord: (data) => {
|
||||
return post('/cattle-exit-records', data)
|
||||
},
|
||||
|
||||
// 更新离栏记录
|
||||
updateExitRecord: (id, data) => {
|
||||
return put(`/cattle-exit-records/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除离栏记录
|
||||
deleteExitRecord: (id) => {
|
||||
return del(`/cattle-exit-records/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除离栏记录
|
||||
batchDeleteExitRecords: (ids) => {
|
||||
return post('/cattle-exit-records/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取可用的牛只列表
|
||||
getAvailableAnimals: (params = {}) => {
|
||||
return get('/cattle-exit-records/available-animals', params)
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只栏舍相关API
|
||||
export const cattlePenApi = {
|
||||
// 获取栏舍列表
|
||||
getPens: (params = {}) => {
|
||||
return get('/cattle-pens', params)
|
||||
},
|
||||
|
||||
// 根据名称搜索栏舍
|
||||
searchPensByName: (name, params = {}) => {
|
||||
return get('/cattle-pens', { name, ...params })
|
||||
},
|
||||
|
||||
// 获取栏舍详情
|
||||
getPenDetail: (id) => {
|
||||
return get(`/cattle-pens/${id}`)
|
||||
},
|
||||
|
||||
// 创建栏舍
|
||||
createPen: (data) => {
|
||||
return post('/cattle-pens', data)
|
||||
},
|
||||
|
||||
// 更新栏舍
|
||||
updatePen: (id, data) => {
|
||||
return put(`/cattle-pens/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除栏舍
|
||||
deletePen: (id) => {
|
||||
return del(`/cattle-pens/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除栏舍
|
||||
batchDeletePens: (ids) => {
|
||||
return post('/cattle-pens/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取栏舍类型列表
|
||||
getPenTypes: () => {
|
||||
return get('/cattle-pens/types')
|
||||
}
|
||||
}
|
||||
|
||||
// 牛只批次相关API
|
||||
export const cattleBatchApi = {
|
||||
// 获取批次列表
|
||||
getBatches: (params = {}) => {
|
||||
return get('/cattle-batches', params)
|
||||
},
|
||||
|
||||
// 根据名称搜索批次
|
||||
searchBatchesByName: (name, params = {}) => {
|
||||
return get('/cattle-batches', { name, ...params })
|
||||
},
|
||||
|
||||
// 获取批次详情
|
||||
getBatchDetail: (id) => {
|
||||
return get(`/cattle-batches/${id}`)
|
||||
},
|
||||
|
||||
// 创建批次
|
||||
createBatch: (data) => {
|
||||
return post('/cattle-batches', data)
|
||||
},
|
||||
|
||||
// 更新批次
|
||||
updateBatch: (id, data) => {
|
||||
return put(`/cattle-batches/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除批次
|
||||
deleteBatch: (id) => {
|
||||
return del(`/cattle-batches/${id}`)
|
||||
},
|
||||
|
||||
// 批量删除批次
|
||||
batchDeleteBatches: (ids) => {
|
||||
return post('/cattle-batches/batch-delete', { ids })
|
||||
},
|
||||
|
||||
// 获取批次类型列表
|
||||
getBatchTypes: () => {
|
||||
return get('/cattle-batches/types')
|
||||
}
|
||||
}
|
||||
|
||||
// 智能预警相关API
|
||||
export const alertApi = {
|
||||
// 获取耳标预警统计
|
||||
getEartagStats: () => {
|
||||
return get('/smart-alerts/public/eartag/stats')
|
||||
},
|
||||
|
||||
// 获取项圈预警统计
|
||||
getCollarStats: () => {
|
||||
return get('/smart-alerts/public/collar/stats')
|
||||
},
|
||||
|
||||
// 获取耳标预警列表
|
||||
getEartagAlerts: (params = {}) => {
|
||||
return get('/smart-alerts/public/eartag', params)
|
||||
},
|
||||
|
||||
// 获取项圈预警列表
|
||||
getCollarAlerts: (params = {}) => {
|
||||
return get('/smart-alerts/public/collar', params)
|
||||
},
|
||||
|
||||
// 获取耳标预警详情
|
||||
getEartagAlertDetail: (id) => {
|
||||
return get(`/smart-alerts/public/eartag/${id}`)
|
||||
},
|
||||
|
||||
// 获取项圈预警详情
|
||||
getCollarAlertDetail: (id) => {
|
||||
return get(`/smart-alerts/public/collar/${id}`)
|
||||
},
|
||||
|
||||
// 处理耳标预警
|
||||
handleEartagAlert: (id, data) => {
|
||||
return post(`/smart-alerts/public/eartag/${id}/handle`, data)
|
||||
},
|
||||
|
||||
// 处理项圈预警
|
||||
handleCollarAlert: (id, data) => {
|
||||
return post(`/smart-alerts/public/collar/${id}/handle`, data)
|
||||
},
|
||||
|
||||
// 批量处理耳标预警
|
||||
batchHandleEartagAlerts: (data) => {
|
||||
return post('/smart-alerts/public/eartag/batch-handle', data)
|
||||
},
|
||||
|
||||
// 批量处理项圈预警
|
||||
batchHandleCollarAlerts: (data) => {
|
||||
return post('/smart-alerts/public/collar/batch-handle', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default service
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:5350',
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -18,7 +18,7 @@ const api = axios.create({
|
||||
export const login = async (username, password) => {
|
||||
try {
|
||||
console.log('正在登录...', username)
|
||||
const response = await api.post('/api/auth/login', {
|
||||
const response = await api.post('/auth/login', {
|
||||
username,
|
||||
password
|
||||
})
|
||||
@@ -38,7 +38,7 @@ export const login = async (username, password) => {
|
||||
export const register = async (userData) => {
|
||||
try {
|
||||
console.log('正在注册...', userData.username)
|
||||
const response = await api.post('/api/auth/register', userData)
|
||||
const response = await api.post('/auth/register', userData)
|
||||
console.log('注册成功:', response.data)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
@@ -54,7 +54,7 @@ export const register = async (userData) => {
|
||||
*/
|
||||
export const validateToken = async (token) => {
|
||||
try {
|
||||
const response = await api.get('/api/auth/validate', {
|
||||
const response = await api.get('/auth/validate', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export const validateToken = async (token) => {
|
||||
*/
|
||||
export const getUserInfo = async (token) => {
|
||||
try {
|
||||
const response = await api.get('/api/auth/me', {
|
||||
const response = await api.get('/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
|
||||
@@ -4,32 +4,32 @@ import { get, post, put, del } from './api'
|
||||
export const fenceService = {
|
||||
// 获取围栏列表
|
||||
getFences(params = {}) {
|
||||
return get('/electronic-fences', params)
|
||||
return get('/electronic-fence', params)
|
||||
},
|
||||
|
||||
// 获取单个围栏详情
|
||||
getFenceById(id) {
|
||||
return get(`/electronic-fences/${id}`)
|
||||
return get(`/electronic-fence/${id}`)
|
||||
},
|
||||
|
||||
// 创建围栏
|
||||
createFence(data) {
|
||||
return post('/electronic-fences', data)
|
||||
return post('/electronic-fence', data)
|
||||
},
|
||||
|
||||
// 更新围栏
|
||||
updateFence(id, data) {
|
||||
return put(`/electronic-fences/${id}`, data)
|
||||
return put(`/electronic-fence/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除围栏
|
||||
deleteFence(id) {
|
||||
return del(`/electronic-fences/${id}`)
|
||||
return del(`/electronic-fence/${id}`)
|
||||
},
|
||||
|
||||
// 搜索围栏
|
||||
searchFences(params) {
|
||||
return get('/electronic-fences/search', params)
|
||||
return get('/electronic-fence/search', params)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,17 +44,13 @@ export const auth = {
|
||||
console.log('成功获取真实token:', response.token.substring(0, 20) + '...')
|
||||
return response.token
|
||||
} else {
|
||||
console.warn('API响应格式不正确:', response)
|
||||
console.error('API响应格式不正确:', response)
|
||||
throw new Error('API响应格式不正确')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法通过API获取测试token,使用模拟token:', error.message)
|
||||
console.error('无法通过API获取测试token:', error.message)
|
||||
throw error
|
||||
}
|
||||
|
||||
// 如果API登录失败,使用模拟token
|
||||
const mockToken = 'mock-token-' + Date.now()
|
||||
this.setToken(mockToken)
|
||||
console.log('使用模拟token:', mockToken)
|
||||
return mockToken
|
||||
},
|
||||
|
||||
// 验证当前token是否有效
|
||||
|
||||
@@ -9,7 +9,7 @@ export const sexMap = {
|
||||
2: '母'
|
||||
}
|
||||
|
||||
// 品类映射
|
||||
// 品类映射(与后端保持一致)
|
||||
export const categoryMap = {
|
||||
1: '犊牛',
|
||||
2: '育成母牛',
|
||||
@@ -37,13 +37,15 @@ export const strainMap = {
|
||||
4: '兼用型'
|
||||
}
|
||||
|
||||
// 生理阶段映射
|
||||
// 生理阶段映射(与后端parity字段对应)
|
||||
export const physiologicalStageMap = {
|
||||
1: '犊牛',
|
||||
2: '育成期',
|
||||
3: '青年期',
|
||||
4: '成年期',
|
||||
5: '老年期'
|
||||
5: '老年期',
|
||||
6: '怀孕期',
|
||||
7: '哺乳期'
|
||||
}
|
||||
|
||||
// 来源映射
|
||||
@@ -128,6 +130,111 @@ export const sellStatusMap = {
|
||||
400: '淘汰'
|
||||
}
|
||||
|
||||
// 离栏原因映射
|
||||
export const exitReasonMap = {
|
||||
'出售': '出售',
|
||||
'死亡': '死亡',
|
||||
'淘汰': '淘汰',
|
||||
'转场': '转场',
|
||||
'其他': '其他'
|
||||
}
|
||||
|
||||
// 处理方式映射
|
||||
export const disposalMethodMap = {
|
||||
'屠宰': '屠宰',
|
||||
'转售': '转售',
|
||||
'掩埋': '掩埋',
|
||||
'焚烧': '焚烧',
|
||||
'其他': '其他'
|
||||
}
|
||||
|
||||
// 离栏状态映射
|
||||
export const exitStatusMap = {
|
||||
'已确认': '已确认',
|
||||
'待确认': '待确认',
|
||||
'已取消': '已取消'
|
||||
}
|
||||
|
||||
// 栏舍类型映射
|
||||
export const penTypeMap = {
|
||||
'产房': '产房',
|
||||
'配种栏': '配种栏',
|
||||
'隔离栏': '隔离栏',
|
||||
'育成栏': '育成栏',
|
||||
'育肥栏': '育肥栏',
|
||||
'犊牛栏': '犊牛栏',
|
||||
'母牛栏': '母牛栏',
|
||||
'公牛栏': '公牛栏',
|
||||
'病牛栏': '病牛栏',
|
||||
'观察栏': '观察栏'
|
||||
}
|
||||
|
||||
// 栏舍状态映射
|
||||
export const penStatusMap = {
|
||||
'启用': '启用',
|
||||
'停用': '停用',
|
||||
'维修': '维修',
|
||||
'废弃': '废弃'
|
||||
}
|
||||
|
||||
// 批次类型映射
|
||||
export const batchTypeMap = {
|
||||
'繁殖批次': '繁殖批次',
|
||||
'育肥批次': '育肥批次',
|
||||
'隔离批次': '隔离批次',
|
||||
'育成批次': '育成批次',
|
||||
'配种批次': '配种批次',
|
||||
'分娩批次': '分娩批次',
|
||||
'断奶批次': '断奶批次',
|
||||
'观察批次': '观察批次'
|
||||
}
|
||||
|
||||
// 批次状态映射
|
||||
export const batchStatusMap = {
|
||||
'进行中': '进行中',
|
||||
'已完成': '已完成',
|
||||
'已暂停': '已暂停',
|
||||
'已取消': '已取消',
|
||||
'待开始': '待开始'
|
||||
}
|
||||
|
||||
// 预警类型映射 - 与PC端保持一致
|
||||
export const alertTypeMap = {
|
||||
'battery': '低电量预警',
|
||||
'offline': '离线预警',
|
||||
'temperature': '温度预警',
|
||||
'movement': '异常运动预警',
|
||||
'wear': '佩戴异常预警',
|
||||
'location': '位置异常预警',
|
||||
// 兼容旧字段
|
||||
'lowBattery': '电量偏低',
|
||||
'highTemperature': '温度过高',
|
||||
'lowTemperature': '温度过低',
|
||||
'abnormalMovement': '运动异常',
|
||||
'wearOff': '项圈脱落',
|
||||
'notCollected': '今日未被采集',
|
||||
'highActivity': '运动量偏高',
|
||||
'lowActivity': '运动量偏低'
|
||||
}
|
||||
|
||||
// 预警级别映射 - 与PC端保持一致
|
||||
export const alertSeverityMap = {
|
||||
'high': '高级',
|
||||
'medium': '中级',
|
||||
'low': '低级',
|
||||
'critical': '紧急',
|
||||
// 兼容旧字段
|
||||
'warning': '一般',
|
||||
'info': '信息'
|
||||
}
|
||||
|
||||
// 预警状态映射
|
||||
export const alertStatusMap = {
|
||||
'unresolved': '未处理',
|
||||
'resolved': '已处理',
|
||||
'processing': '处理中'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性别中文名称
|
||||
* @param {number} sex 性别代码
|
||||
@@ -209,6 +316,96 @@ export function getSellStatusName(sellStatus) {
|
||||
return sellStatusMap[sellStatus] || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离栏原因中文名称
|
||||
* @param {string} exitReason 离栏原因
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getExitReasonName(exitReason) {
|
||||
return exitReasonMap[exitReason] || exitReason || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理方式中文名称
|
||||
* @param {string} disposalMethod 处理方式
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getDisposalMethodName(disposalMethod) {
|
||||
return disposalMethodMap[disposalMethod] || disposalMethod || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离栏状态中文名称
|
||||
* @param {string} exitStatus 离栏状态
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getExitStatusName(exitStatus) {
|
||||
return exitStatusMap[exitStatus] || exitStatus || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栏舍类型中文名称
|
||||
* @param {string} penType 栏舍类型
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getPenTypeName(penType) {
|
||||
return penTypeMap[penType] || penType || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栏舍状态中文名称
|
||||
* @param {string} penStatus 栏舍状态
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getPenStatusName(penStatus) {
|
||||
return penStatusMap[penStatus] || penStatus || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批次类型中文名称
|
||||
* @param {string} batchType 批次类型
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getBatchTypeName(batchType) {
|
||||
return batchTypeMap[batchType] || batchType || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批次状态中文名称
|
||||
* @param {string} batchStatus 批次状态
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getBatchStatusName(batchStatus) {
|
||||
return batchStatusMap[batchStatus] || batchStatus || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预警类型中文名称
|
||||
* @param {string} alertType 预警类型
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getAlertTypeName(alertType) {
|
||||
return alertTypeMap[alertType] || alertType || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预警级别中文名称
|
||||
* @param {string} severity 预警级别
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getAlertSeverityName(severity) {
|
||||
return alertSeverityMap[severity] || severity || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预警状态中文名称
|
||||
* @param {string} status 预警状态
|
||||
* @returns {string} 中文名称
|
||||
*/
|
||||
export function getAlertStatusName(status) {
|
||||
return alertStatusMap[status] || status || '--'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {number} timestamp 时间戳(秒)
|
||||
@@ -251,6 +448,16 @@ export default {
|
||||
insureMap,
|
||||
mortgageMap,
|
||||
sellStatusMap,
|
||||
exitReasonMap,
|
||||
disposalMethodMap,
|
||||
exitStatusMap,
|
||||
penTypeMap,
|
||||
penStatusMap,
|
||||
batchTypeMap,
|
||||
batchStatusMap,
|
||||
alertTypeMap,
|
||||
alertSeverityMap,
|
||||
alertStatusMap,
|
||||
getSexName,
|
||||
getCategoryName,
|
||||
getBreedName,
|
||||
@@ -260,6 +467,16 @@ export default {
|
||||
getEventName,
|
||||
getWearName,
|
||||
getSellStatusName,
|
||||
getExitReasonName,
|
||||
getDisposalMethodName,
|
||||
getExitStatusName,
|
||||
getPenTypeName,
|
||||
getPenStatusName,
|
||||
getBatchTypeName,
|
||||
getBatchStatusName,
|
||||
getAlertTypeName,
|
||||
getAlertSeverityName,
|
||||
getAlertStatusName,
|
||||
formatDate,
|
||||
formatDateToTimestamp
|
||||
}
|
||||
|
||||
308
mini_program/farm-monitor-dashboard/test-alert.html
Normal file
308
mini_program/farm-monitor-dashboard/test-alert.html
Normal file
@@ -0,0 +1,308 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>预警API测试</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
button { padding: 8px 15px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
.alert-card { display: inline-block; margin: 10px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; text-align: center; min-width: 120px; }
|
||||
.alert-value { font-size: 24px; font-weight: bold; color: #ff4757; }
|
||||
.alert-label { font-size: 14px; color: #666; margin-top: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>预警API测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>1. 登录测试</h3>
|
||||
<button onclick="testLogin()">登录</button>
|
||||
<div id="loginResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>2. 项圈预警统计</h3>
|
||||
<button onclick="testCollarStats()">获取项圈预警统计</button>
|
||||
<div id="collarResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>3. 耳标预警统计</h3>
|
||||
<button onclick="testEartagStats()">获取耳标预警统计</button>
|
||||
<div id="eartagResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>4. 预警卡片展示</h3>
|
||||
<button onclick="showAlertCards()">显示预警卡片</button>
|
||||
<div id="alertCards" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>5. 预警列表数据</h3>
|
||||
<button onclick="testAlertList()">获取预警列表</button>
|
||||
<div id="alertList" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>6. 智能项圈预警测试</h3>
|
||||
<button onclick="testCollarAlertPage()">测试项圈预警页面</button>
|
||||
<div id="collarAlertTest" class="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录成功!</strong><br>Token: ' + token.substring(0, 50) + '...';
|
||||
} else {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCollarStats() {
|
||||
if (!token) {
|
||||
document.getElementById('collarResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/smart-alerts/public/collar/stats', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const stats = response.data.data;
|
||||
let html = '<strong>项圈预警统计:</strong><br>';
|
||||
html += `总设备数: ${stats.totalDevices}<br>`;
|
||||
html += `总预警数: ${stats.totalAlerts}<br>`;
|
||||
html += `电量偏低: ${stats.lowBattery}<br>`;
|
||||
html += `设备离线: ${stats.offline}<br>`;
|
||||
html += `温度过高: ${stats.highTemperature}<br>`;
|
||||
html += `温度过低: ${stats.lowTemperature}<br>`;
|
||||
html += `运动异常: ${stats.abnormalMovement}<br>`;
|
||||
html += `项圈脱落: ${stats.wearOff}<br>`;
|
||||
|
||||
document.getElementById('collarResult').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('collarResult').innerHTML =
|
||||
'<strong>获取失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('collarResult').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testEartagStats() {
|
||||
if (!token) {
|
||||
document.getElementById('eartagResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/smart-alerts/public/eartag/stats', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const stats = response.data.data;
|
||||
let html = '<strong>耳标预警统计:</strong><br>';
|
||||
html += `总设备数: ${stats.totalDevices}<br>`;
|
||||
html += `总预警数: ${stats.totalAlerts}<br>`;
|
||||
html += `电量偏低: ${stats.lowBattery}<br>`;
|
||||
html += `设备离线: ${stats.offline}<br>`;
|
||||
html += `温度过高: ${stats.highTemperature}<br>`;
|
||||
html += `温度过低: ${stats.lowTemperature}<br>`;
|
||||
html += `运动异常: ${stats.abnormalMovement}<br>`;
|
||||
|
||||
document.getElementById('eartagResult').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('eartagResult').innerHTML =
|
||||
'<strong>获取失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('eartagResult').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function showAlertCards() {
|
||||
if (!token) {
|
||||
document.getElementById('alertCards').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [collarResponse, eartagResponse] = await Promise.all([
|
||||
axios.get('/api/smart-alerts/public/collar/stats', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}),
|
||||
axios.get('/api/smart-alerts/public/eartag/stats', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
]);
|
||||
|
||||
let html = '<strong>预警卡片展示:</strong><br><br>';
|
||||
|
||||
if (collarResponse.data.success) {
|
||||
const stats = collarResponse.data.data;
|
||||
html += '<h4>项圈预警:</h4>';
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.totalDevices - stats.totalAlerts}</div><div class="alert-label">今日未被采集</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.highTemperature}</div><div class="alert-label">温度过高</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.lowTemperature}</div><div class="alert-label">温度过低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.abnormalMovement}</div><div class="alert-label">运动量偏低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.lowBattery}</div><div class="alert-label">电量偏低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.wearOff}</div><div class="alert-label">项圈脱落</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.offline}</div><div class="alert-label">设备离线</div></div>`;
|
||||
}
|
||||
|
||||
if (eartagResponse.data.success) {
|
||||
const stats = eartagResponse.data.data;
|
||||
html += '<br><h4>耳标预警:</h4>';
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.totalDevices - stats.totalAlerts}</div><div class="alert-label">今日未被采集</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.highTemperature}</div><div class="alert-label">温度过高</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.lowTemperature}</div><div class="alert-label">温度过低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.abnormalMovement}</div><div class="alert-label">运动量偏低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.lowBattery}</div><div class="alert-label">电量偏低</div></div>`;
|
||||
html += `<div class="alert-card"><div class="alert-value">${stats.offline}</div><div class="alert-label">设备离线</div></div>`;
|
||||
}
|
||||
|
||||
document.getElementById('alertCards').innerHTML = html;
|
||||
} catch (error) {
|
||||
document.getElementById('alertCards').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAlertList() {
|
||||
if (!token) {
|
||||
document.getElementById('alertList').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [eartagResponse, collarResponse] = await Promise.all([
|
||||
axios.get('/api/smart-alerts/public/eartag?page=1&limit=5', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}),
|
||||
axios.get('/api/smart-alerts/public/collar?page=1&limit=5', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
]);
|
||||
|
||||
let html = '<strong>预警列表数据:</strong><br><br>';
|
||||
|
||||
if (eartagResponse.data.success) {
|
||||
html += '<h4>耳标预警列表:</h4>';
|
||||
const alerts = eartagResponse.data.data || [];
|
||||
alerts.forEach((alert, index) => {
|
||||
html += `<div class="alert-card">
|
||||
<div class="alert-value">${alert.id || 'N/A'}</div>
|
||||
<div class="alert-label">${alert.alertType || '未知类型'}</div>
|
||||
<div class="alert-label">${alert.alertLevel || '未知级别'}</div>
|
||||
<div class="alert-label">${alert.deviceId || alert.eartagNumber || 'N/A'}</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (collarResponse.data.success) {
|
||||
html += '<br><h4>项圈预警列表:</h4>';
|
||||
const alerts = collarResponse.data.data || [];
|
||||
alerts.forEach((alert, index) => {
|
||||
html += `<div class="alert-card">
|
||||
<div class="alert-value">${alert.id || 'N/A'}</div>
|
||||
<div class="alert-label">${alert.alertType || '未知类型'}</div>
|
||||
<div class="alert-label">${alert.alertLevel || '未知级别'}</div>
|
||||
<div class="alert-label">${alert.deviceId || alert.collarNumber || 'N/A'}</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('alertList').innerHTML = html;
|
||||
} catch (error) {
|
||||
document.getElementById('alertList').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCollarAlertPage() {
|
||||
if (!token) {
|
||||
document.getElementById('collarAlertTest').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const collarResponse = await axios.get('/api/smart-alerts/public/collar?page=1&limit=3', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
|
||||
let html = '<strong>智能项圈预警测试:</strong><br><br>';
|
||||
|
||||
if (collarResponse.data.success) {
|
||||
const alerts = collarResponse.data.data || [];
|
||||
html += `<h4>项圈预警列表 (${alerts.length}条):</h4>`;
|
||||
|
||||
alerts.forEach((alert, index) => {
|
||||
html += `<div class="alert-card">
|
||||
<div class="alert-value">${alert.id || 'N/A'}</div>
|
||||
<div class="alert-label">类型: ${alert.alertType || '未知'}</div>
|
||||
<div class="alert-label">级别: ${alert.alertLevel || '未知'}</div>
|
||||
<div class="alert-label">项圈: ${alert.collarNumber || alert.deviceId || 'N/A'}</div>
|
||||
<div class="alert-label">电量: ${alert.battery || 'N/A'}%</div>
|
||||
<div class="alert-label">温度: ${alert.temperature || 'N/A'}°C</div>
|
||||
<div class="alert-label">步数: ${alert.dailySteps || 'N/A'}</div>
|
||||
<div class="alert-label">GPS: ${alert.gpsSignal || 'N/A'}</div>
|
||||
<div class="alert-label">佩戴: ${alert.wearStatus || 'N/A'}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += '<br><h4>数据字段对比 (与PC端一致):</h4>';
|
||||
html += '<ul>';
|
||||
html += '<li>✅ collarNumber - 项圈编号</li>';
|
||||
html += '<li>✅ alertType - 预警类型 (battery, offline, temperature, movement, wear)</li>';
|
||||
html += '<li>✅ alertLevel - 预警级别 (high, medium, low, critical)</li>';
|
||||
html += '<li>✅ alertTime - 预警时间</li>';
|
||||
html += '<li>✅ battery - 设备电量</li>';
|
||||
html += '<li>✅ temperature - 设备温度</li>';
|
||||
html += '<li>✅ dailySteps - 当日步数</li>';
|
||||
html += '<li>✅ longitude/latitude - 位置坐标</li>';
|
||||
html += '<li>✅ gpsSignal - GPS信号</li>';
|
||||
html += '<li>✅ wearStatus - 佩戴状态</li>';
|
||||
html += '</ul>';
|
||||
} else {
|
||||
html += '<strong>获取项圈预警数据失败!</strong><br>' + collarResponse.data.message;
|
||||
}
|
||||
|
||||
document.getElementById('collarAlertTest').innerHTML = html;
|
||||
} catch (error) {
|
||||
document.getElementById('collarAlertTest').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,106 +3,67 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API测试</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
button { padding: 10px 20px; margin: 5px; background: #007aff; color: white; border: none; border-radius: 3px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 3px; white-space: pre-wrap; font-family: monospace; }
|
||||
.error { background: #ffebee; color: #c62828; }
|
||||
.success { background: #e8f5e8; color: #2e7d32; }
|
||||
</style>
|
||||
<title>API测试页面</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>API连接测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>1. 测试基础连接</h3>
|
||||
<button onclick="testBasicConnection()">测试基础连接</button>
|
||||
<div id="basicResult" class="result">点击按钮开始测试</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>2. 测试牛只档案API</h3>
|
||||
<button onclick="testCattleApi()">测试牛只档案API</button>
|
||||
<div id="cattleResult" class="result">点击按钮开始测试</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>3. 测试牛只类型API</h3>
|
||||
<button onclick="testCattleTypesApi()">测试牛只类型API</button>
|
||||
<div id="typesResult" class="result">点击按钮开始测试</div>
|
||||
</div>
|
||||
<h1>转栏记录API测试</h1>
|
||||
<button onclick="testLogin()">1. 测试登录</button>
|
||||
<button onclick="testTransferRecords()">2. 测试转栏记录</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const baseURL = 'http://localhost:5350/api';
|
||||
|
||||
async function testBasicConnection() {
|
||||
const resultDiv = document.getElementById('basicResult');
|
||||
resultDiv.textContent = '测试中...';
|
||||
resultDiv.className = 'result';
|
||||
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await fetch(`${baseURL}/cattle-type`);
|
||||
const data = await response.json();
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.textContent = `✅ 连接成功!\n状态码: ${response.status}\n数据: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result success';
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录成功!</h3><p>Token: ' + token.substring(0, 50) + '...</p>';
|
||||
} else {
|
||||
resultDiv.textContent = `❌ 连接失败!\n状态码: ${response.status}\n错误: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result error';
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `❌ 连接失败!\n错误: ${error.message}`;
|
||||
resultDiv.className = 'result error';
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testCattleApi() {
|
||||
const resultDiv = document.getElementById('cattleResult');
|
||||
resultDiv.textContent = '测试中...';
|
||||
resultDiv.className = 'result';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseURL}/iot-cattle/public?page=1&pageSize=5`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.textContent = `✅ 牛只档案API成功!\n状态码: ${response.status}\n数据: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result success';
|
||||
} else {
|
||||
resultDiv.textContent = `❌ 牛只档案API失败!\n状态码: ${response.status}\n错误: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result error';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `❌ 牛只档案API失败!\n错误: ${error.message}`;
|
||||
resultDiv.className = 'result error';
|
||||
|
||||
async function testTransferRecords() {
|
||||
if (!token) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>请先登录!</h3>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCattleTypesApi() {
|
||||
const resultDiv = document.getElementById('typesResult');
|
||||
resultDiv.textContent = '测试中...';
|
||||
resultDiv.className = 'result';
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseURL}/cattle-type`);
|
||||
const data = await response.json();
|
||||
const response = await axios.get('/api/cattle-transfer-records?page=1&pageSize=10', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.textContent = `✅ 牛只类型API成功!\n状态码: ${response.status}\n数据: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result success';
|
||||
if (response.data.success) {
|
||||
const records = response.data.data.list;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>转栏记录获取成功!</h3><p>记录数量: ' + records.length + '</p>' +
|
||||
'<pre>' + JSON.stringify(records, null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.textContent = `❌ 牛只类型API失败!\n状态码: ${response.status}\n错误: ${JSON.stringify(data, null, 2)}`;
|
||||
resultDiv.className = 'result error';
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `❌ 牛只类型API失败!\n错误: ${error.message}`;
|
||||
resultDiv.className = 'result error';
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
140
mini_program/farm-monitor-dashboard/test-batch.html
Normal file
140
mini_program/farm-monitor-dashboard/test-batch.html
Normal file
@@ -0,0 +1,140 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>批次设置API测试</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
input { padding: 8px; margin: 5px; width: 200px; }
|
||||
button { padding: 8px 15px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>批次设置API测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>1. 登录测试</h3>
|
||||
<button onclick="testLogin()">登录</button>
|
||||
<div id="loginResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>2. 批次列表测试</h3>
|
||||
<button onclick="testBatches()">获取批次列表</button>
|
||||
<div id="batchResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>3. 精确搜索测试</h3>
|
||||
<input type="text" id="searchInput" placeholder="输入批次名称进行精确搜索" value="333">
|
||||
<button onclick="testExactSearch()">精确搜索</button>
|
||||
<div id="searchResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录成功!</strong><br>Token: ' + token.substring(0, 50) + '...';
|
||||
} else {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testBatches() {
|
||||
if (!token) {
|
||||
document.getElementById('batchResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-batches?page=1&pageSize=5', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const batches = response.data.data.list;
|
||||
document.getElementById('batchResult').innerHTML =
|
||||
'<strong>批次列表获取成功!</strong><br>批次数量: ' + batches.length + '<br><br>' +
|
||||
'<pre>' + JSON.stringify(batches, null, 2) + '</pre>';
|
||||
} else {
|
||||
document.getElementById('batchResult').innerHTML =
|
||||
'<strong>获取失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('batchResult').innerHTML =
|
||||
'<strong>获取错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testExactSearch() {
|
||||
if (!token) {
|
||||
document.getElementById('searchResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
const searchName = document.getElementById('searchInput').value;
|
||||
if (!searchName) {
|
||||
document.getElementById('searchResult').innerHTML = '<strong>请输入搜索名称!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-batches?search=' + encodeURIComponent(searchName) + '&page=1&pageSize=10', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const allBatches = response.data.data.list;
|
||||
// 前端精确过滤
|
||||
const exactBatches = allBatches.filter(batch => batch.name === searchName);
|
||||
|
||||
let html = `<strong>精确搜索结果 (搜索: "${searchName}")</strong><br>`;
|
||||
html += `后端返回总数: ${allBatches.length}<br>`;
|
||||
html += `精确匹配数量: ${exactBatches.length}<br><br>`;
|
||||
|
||||
if (exactBatches.length > 0) {
|
||||
html += '<strong>精确匹配的批次:</strong><br>';
|
||||
exactBatches.forEach(batch => {
|
||||
html += `• 名称: "${batch.name}", 编号: ${batch.code}, 类型: ${batch.type}, 状态: ${batch.status}<br>`;
|
||||
});
|
||||
} else {
|
||||
html += '<em>没有找到精确匹配的批次</em>';
|
||||
}
|
||||
|
||||
document.getElementById('searchResult').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('searchResult').innerHTML =
|
||||
'<strong>搜索失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('searchResult').innerHTML =
|
||||
'<strong>搜索错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
159
mini_program/farm-monitor-dashboard/test-exact-search.html
Normal file
159
mini_program/farm-monitor-dashboard/test-exact-search.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>栏舍精确搜索测试</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.result { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 3px; }
|
||||
input { padding: 8px; margin: 5px; width: 200px; }
|
||||
button { padding: 8px 15px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>栏舍精确搜索功能测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>1. 登录测试</h3>
|
||||
<button onclick="testLogin()">登录</button>
|
||||
<div id="loginResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>2. 精确搜索测试</h3>
|
||||
<input type="text" id="searchInput" placeholder="输入栏舍名称进行精确搜索" value="3">
|
||||
<button onclick="testExactSearch()">精确搜索</button>
|
||||
<div id="searchResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>3. 模糊搜索对比</h3>
|
||||
<input type="text" id="fuzzySearchInput" placeholder="输入栏舍名称进行模糊搜索" value="3">
|
||||
<button onclick="testFuzzySearch()">模糊搜索</button>
|
||||
<div id="fuzzyResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录成功!</strong><br>Token: ' + token.substring(0, 50) + '...';
|
||||
} else {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('loginResult').innerHTML =
|
||||
'<strong>登录错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testExactSearch() {
|
||||
if (!token) {
|
||||
document.getElementById('searchResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
const searchName = document.getElementById('searchInput').value;
|
||||
if (!searchName) {
|
||||
document.getElementById('searchResult').innerHTML = '<strong>请输入搜索名称!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-pens?search=' + encodeURIComponent(searchName) + '&page=1&pageSize=10', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const allPens = response.data.data.list;
|
||||
// 前端精确过滤
|
||||
const exactPens = allPens.filter(pen => pen.name === searchName);
|
||||
|
||||
let html = `<strong>精确搜索结果 (搜索: "${searchName}")</strong><br>`;
|
||||
html += `后端返回总数: ${allPens.length}<br>`;
|
||||
html += `精确匹配数量: ${exactPens.length}<br><br>`;
|
||||
|
||||
if (exactPens.length > 0) {
|
||||
html += '<strong>精确匹配的栏舍:</strong><br>';
|
||||
exactPens.forEach(pen => {
|
||||
html += `• 名称: "${pen.name}", 编号: ${pen.code}<br>`;
|
||||
});
|
||||
} else {
|
||||
html += '<em>没有找到精确匹配的栏舍</em>';
|
||||
}
|
||||
|
||||
document.getElementById('searchResult').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('searchResult').innerHTML =
|
||||
'<strong>搜索失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('searchResult').innerHTML =
|
||||
'<strong>搜索错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testFuzzySearch() {
|
||||
if (!token) {
|
||||
document.getElementById('fuzzyResult').innerHTML = '<strong>请先登录!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
const searchName = document.getElementById('fuzzySearchInput').value;
|
||||
if (!searchName) {
|
||||
document.getElementById('fuzzyResult').innerHTML = '<strong>请输入搜索名称!</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-pens?search=' + encodeURIComponent(searchName) + '&page=1&pageSize=10', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const allPens = response.data.data.list;
|
||||
|
||||
let html = `<strong>模糊搜索结果 (搜索: "${searchName}")</strong><br>`;
|
||||
html += `后端返回总数: ${allPens.length}<br><br>`;
|
||||
|
||||
if (allPens.length > 0) {
|
||||
html += '<strong>所有匹配的栏舍:</strong><br>';
|
||||
allPens.forEach(pen => {
|
||||
const isExact = pen.name === searchName;
|
||||
html += `• 名称: "${pen.name}", 编号: ${pen.code} ${isExact ? '(精确匹配)' : '(模糊匹配)'}<br>`;
|
||||
});
|
||||
} else {
|
||||
html += '<em>没有找到匹配的栏舍</em>';
|
||||
}
|
||||
|
||||
document.getElementById('fuzzyResult').innerHTML = html;
|
||||
} else {
|
||||
document.getElementById('fuzzyResult').innerHTML =
|
||||
'<strong>搜索失败!</strong><br>' + response.data.message;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('fuzzyResult').innerHTML =
|
||||
'<strong>搜索错误!</strong><br>' + error.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
69
mini_program/farm-monitor-dashboard/test-exit.html
Normal file
69
mini_program/farm-monitor-dashboard/test-exit.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>离栏记录API测试</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>离栏记录API测试</h1>
|
||||
<button onclick="testLogin()">1. 测试登录</button>
|
||||
<button onclick="testExitRecords()">2. 测试离栏记录</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录成功!</h3><p>Token: ' + token.substring(0, 50) + '...</p>';
|
||||
} else {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testExitRecords() {
|
||||
if (!token) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>请先登录!</h3>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-exit-records?page=1&pageSize=10', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const records = response.data.data.list;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>离栏记录获取成功!</h3><p>记录数量: ' + records.length + '</p>' +
|
||||
'<pre>' + JSON.stringify(records, null, 2) + '</pre>';
|
||||
} else {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
mini_program/farm-monitor-dashboard/test-navigation.html
Normal file
64
mini_program/farm-monitor-dashboard/test-navigation.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>页面导航测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.nav-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.nav-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>页面导航测试</h1>
|
||||
|
||||
<a href="/cattle-transfer" class="nav-button">
|
||||
转栏记录页面
|
||||
<div class="description">查看和管理牛只转栏记录</div>
|
||||
</a>
|
||||
|
||||
<a href="/cattle-exit" class="nav-button">
|
||||
离栏记录页面
|
||||
<div class="description">查看和管理牛只离栏记录</div>
|
||||
</a>
|
||||
|
||||
<a href="/cattle-profile" class="nav-button">
|
||||
牛只档案页面
|
||||
<div class="description">查看牛只档案信息</div>
|
||||
</a>
|
||||
|
||||
<a href="/test-exit.html" class="nav-button">
|
||||
离栏记录API测试
|
||||
<div class="description">测试离栏记录API接口</div>
|
||||
</a>
|
||||
|
||||
<a href="/test-api.html" class="nav-button">
|
||||
转栏记录API测试
|
||||
<div class="description">测试转栏记录API接口</div>
|
||||
</a>
|
||||
</body>
|
||||
</html>
|
||||
99
mini_program/farm-monitor-dashboard/test-pen.html
Normal file
99
mini_program/farm-monitor-dashboard/test-pen.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>栏舍设置API测试</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>栏舍设置API测试</h1>
|
||||
<button onclick="testLogin()">1. 测试登录</button>
|
||||
<button onclick="testPens()">2. 测试栏舍列表</button>
|
||||
<button onclick="testPenTypes()">3. 测试栏舍类型</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
let token = '';
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
token = response.data.token;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录成功!</h3><p>Token: ' + token.substring(0, 50) + '...</p>';
|
||||
} else {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>登录错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testPens() {
|
||||
if (!token) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>请先登录!</h3>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-pens?page=1&pageSize=5', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const pens = response.data.data.list;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>栏舍列表获取成功!</h3><p>栏舍数量: ' + pens.length + '</p>' +
|
||||
'<pre>' + JSON.stringify(pens, null, 2) + '</pre>';
|
||||
} else {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testPenTypes() {
|
||||
if (!token) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>请先登录!</h3>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/cattle-pens/types', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const types = response.data.data;
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>栏舍类型获取成功!</h3><p>类型数量: ' + types.length + '</p>' +
|
||||
'<pre>' + JSON.stringify(types, null, 2) + '</pre>';
|
||||
} else {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取失败!</h3><p>' + response.data.message + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('result').innerHTML =
|
||||
'<h3>获取错误!</h3><p>' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
269
mini_program/farm-monitor-dashboard/utils/api.js
Normal file
269
mini_program/farm-monitor-dashboard/utils/api.js
Normal file
@@ -0,0 +1,269 @@
|
||||
// utils/api.js - API请求工具
|
||||
|
||||
const app = getApp()
|
||||
|
||||
// 基础配置
|
||||
const config = {
|
||||
baseUrl: 'https://your-backend-url.com/api', // 请替换为实际的后端API地址
|
||||
timeout: 10000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
const requestInterceptor = (options) => {
|
||||
// 添加token到请求头
|
||||
const token = wx.getStorageSync('token')
|
||||
if (token) {
|
||||
options.header = {
|
||||
...options.header,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 添加时间戳防止缓存
|
||||
if (options.method === 'GET') {
|
||||
options.data = {
|
||||
...options.data,
|
||||
_t: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
const responseInterceptor = (response) => {
|
||||
const { statusCode, data } = response
|
||||
|
||||
console.log('API响应:', data)
|
||||
console.log('状态码:', statusCode)
|
||||
|
||||
// 处理HTTP状态码
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
// 统一处理响应格式
|
||||
if (data.code === 200) {
|
||||
console.log('处理code=200格式')
|
||||
return data.data
|
||||
} else if (data.success === true) {
|
||||
// 处理 {success: true, data: ...} 格式
|
||||
console.log('处理success=true格式')
|
||||
return data
|
||||
} else if (data.success === false) {
|
||||
// 处理 {success: false, message: ...} 格式
|
||||
console.log('处理success=false格式')
|
||||
return data
|
||||
} else if (data.code === undefined && data.success === undefined) {
|
||||
// 直接返回数据的情况
|
||||
console.log('处理直接数据格式')
|
||||
return data
|
||||
} else {
|
||||
// 业务错误
|
||||
console.error('请求失败:', data.message || '请求失败')
|
||||
console.error('完整响应:', data)
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
} else {
|
||||
// HTTP错误
|
||||
let message = '网络错误'
|
||||
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
message = '未授权,请重新登录'
|
||||
// 清除token并跳转到登录页
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问'
|
||||
break
|
||||
case 404:
|
||||
message = '请求的资源不存在'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
default:
|
||||
message = `连接错误${statusCode}`
|
||||
}
|
||||
|
||||
console.error('网络错误:', message)
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 应用请求拦截器
|
||||
const processedOptions = requestInterceptor({
|
||||
url: config.baseUrl + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
...config.header,
|
||||
...options.header
|
||||
},
|
||||
timeout: options.timeout || config.timeout
|
||||
})
|
||||
|
||||
wx.request({
|
||||
...processedOptions,
|
||||
success: (response) => {
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('请求失败:', error)
|
||||
let message = '网络连接异常'
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
reject(new Error(message))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// GET请求
|
||||
export const get = (url, data = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// POST请求
|
||||
export const post = (url, data = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
export const put = (url, data = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
export const del = (url, data = {}) => {
|
||||
return request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export const upload = (url, filePath, formData = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = wx.getStorageSync('token')
|
||||
const header = {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
if (token) {
|
||||
header['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
wx.uploadFile({
|
||||
url: config.baseUrl + url,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
formData: formData,
|
||||
header: header,
|
||||
success: (response) => {
|
||||
try {
|
||||
const data = JSON.parse(response.data)
|
||||
if (data.code === 200) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.message))
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('上传失败:', error)
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
export const download = (url, filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.downloadFile({
|
||||
url: config.baseUrl + url,
|
||||
filePath: filePath,
|
||||
success: (response) => {
|
||||
if (response.statusCode === 200) {
|
||||
resolve(response.filePath)
|
||||
} else {
|
||||
reject(new Error('下载失败'))
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('下载失败:', error)
|
||||
reject(new Error('下载失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 设置基础URL
|
||||
export const setBaseUrl = (baseUrl) => {
|
||||
config.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
// 获取基础URL
|
||||
export const getBaseUrl = () => {
|
||||
return config.baseUrl
|
||||
}
|
||||
|
||||
// 设置超时时间
|
||||
export const setTimeout = (timeout) => {
|
||||
config.timeout = timeout
|
||||
}
|
||||
|
||||
// 获取超时时间
|
||||
export const getTimeout = () => {
|
||||
return config.timeout
|
||||
}
|
||||
|
||||
export default {
|
||||
request,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del,
|
||||
upload,
|
||||
download,
|
||||
setBaseUrl,
|
||||
getBaseUrl,
|
||||
setTimeout,
|
||||
getTimeout
|
||||
}
|
||||
308
mini_program/farm-monitor-dashboard/utils/auth.js
Normal file
308
mini_program/farm-monitor-dashboard/utils/auth.js
Normal file
@@ -0,0 +1,308 @@
|
||||
// utils/auth.js - 认证相关工具
|
||||
|
||||
// 获取token
|
||||
export const getToken = () => {
|
||||
return wx.getStorageSync('token')
|
||||
}
|
||||
|
||||
// 设置token
|
||||
export const setToken = (token) => {
|
||||
wx.setStorageSync('token', token)
|
||||
}
|
||||
|
||||
// 清除token
|
||||
export const clearToken = () => {
|
||||
wx.removeStorageSync('token')
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = () => {
|
||||
return wx.getStorageSync('userInfo')
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
export const setUserInfo = (userInfo) => {
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
// 清除用户信息
|
||||
export const clearUserInfo = () => {
|
||||
wx.removeStorageSync('userInfo')
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
export const isLoggedIn = () => {
|
||||
const token = getToken()
|
||||
const userInfo = getUserInfo()
|
||||
return !!(token && userInfo)
|
||||
}
|
||||
|
||||
// 清除所有认证信息
|
||||
export const clearAuth = () => {
|
||||
clearToken()
|
||||
clearUserInfo()
|
||||
}
|
||||
|
||||
// 跳转到登录页
|
||||
export const redirectToLogin = () => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查登录状态,未登录则跳转到登录页
|
||||
export const checkAuth = () => {
|
||||
if (!isLoggedIn()) {
|
||||
redirectToLogin()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
export const getUserPermissions = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.permissions || [] : []
|
||||
}
|
||||
|
||||
// 检查用户权限
|
||||
export const hasPermission = (permission) => {
|
||||
const permissions = getUserPermissions()
|
||||
return permissions.includes(permission)
|
||||
}
|
||||
|
||||
// 检查用户角色
|
||||
export const hasRole = (role) => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.role === role : false
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
export const getUserId = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.id : null
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
export const getUsername = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.username : ''
|
||||
}
|
||||
|
||||
// 获取用户手机号
|
||||
export const getUserPhone = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.phone : ''
|
||||
}
|
||||
|
||||
// 获取用户邮箱
|
||||
export const getUserEmail = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.email : ''
|
||||
}
|
||||
|
||||
// 获取用户头像
|
||||
export const getUserAvatar = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.avatar : ''
|
||||
}
|
||||
|
||||
// 获取用户昵称
|
||||
export const getUserNickname = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.nickname : ''
|
||||
}
|
||||
|
||||
// 获取用户真实姓名
|
||||
export const getUserRealName = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.realName : ''
|
||||
}
|
||||
|
||||
// 获取用户部门
|
||||
export const getUserDepartment = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.department : ''
|
||||
}
|
||||
|
||||
// 获取用户职位
|
||||
export const getUserPosition = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.position : ''
|
||||
}
|
||||
|
||||
// 获取用户创建时间
|
||||
export const getUserCreateTime = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.createTime : ''
|
||||
}
|
||||
|
||||
// 获取用户最后登录时间
|
||||
export const getUserLastLoginTime = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.lastLoginTime : ''
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export const updateUserInfo = (updates) => {
|
||||
const userInfo = getUserInfo()
|
||||
if (userInfo) {
|
||||
const newUserInfo = {
|
||||
...userInfo,
|
||||
...updates
|
||||
}
|
||||
setUserInfo(newUserInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
export const login = (token, userInfo) => {
|
||||
setToken(token)
|
||||
setUserInfo(userInfo)
|
||||
}
|
||||
|
||||
// 登出
|
||||
export const logout = () => {
|
||||
clearAuth()
|
||||
redirectToLogin()
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
export const refreshToken = async (newToken) => {
|
||||
setToken(newToken)
|
||||
}
|
||||
|
||||
// 检查token是否过期
|
||||
export const isTokenExpired = () => {
|
||||
const token = getToken()
|
||||
if (!token) return true
|
||||
|
||||
try {
|
||||
// 简单的token过期检查,实际项目中应该解析JWT token
|
||||
// 这里假设token包含过期时间信息
|
||||
const tokenData = JSON.parse(atob(token.split('.')[1]))
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
return tokenData.exp < currentTime
|
||||
} catch (error) {
|
||||
console.error('解析token失败:', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 自动刷新token
|
||||
export const autoRefreshToken = async () => {
|
||||
if (isTokenExpired()) {
|
||||
// 这里应该调用后端API刷新token
|
||||
// 暂时直接清除认证信息
|
||||
clearAuth()
|
||||
redirectToLogin()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取认证头
|
||||
export const getAuthHeader = () => {
|
||||
const token = getToken()
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
// 保存登录状态
|
||||
export const saveLoginState = (loginData) => {
|
||||
const { token, userInfo, rememberMe = false } = loginData
|
||||
|
||||
if (rememberMe) {
|
||||
// 记住登录状态
|
||||
setToken(token)
|
||||
setUserInfo(userInfo)
|
||||
} else {
|
||||
// 临时登录状态
|
||||
setToken(token)
|
||||
setUserInfo(userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取登录状态
|
||||
export const getLoginState = () => {
|
||||
return {
|
||||
isLoggedIn: isLoggedIn(),
|
||||
token: getToken(),
|
||||
userInfo: getUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
// 验证用户信息完整性
|
||||
export const validateUserInfo = () => {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo) return false
|
||||
|
||||
const requiredFields = ['id', 'username', 'phone']
|
||||
return requiredFields.every(field => userInfo[field])
|
||||
}
|
||||
|
||||
// 获取用户显示名称
|
||||
export const getUserDisplayName = () => {
|
||||
const userInfo = getUserInfo()
|
||||
if (!userInfo) return '未登录'
|
||||
|
||||
return userInfo.realName || userInfo.nickname || userInfo.username || '未知用户'
|
||||
}
|
||||
|
||||
// 获取用户状态
|
||||
export const getUserStatus = () => {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo ? userInfo.status : 'unknown'
|
||||
}
|
||||
|
||||
// 检查用户是否激活
|
||||
export const isUserActive = () => {
|
||||
const status = getUserStatus()
|
||||
return status === 'active'
|
||||
}
|
||||
|
||||
// 检查用户是否被禁用
|
||||
export const isUserDisabled = () => {
|
||||
const status = getUserStatus()
|
||||
return status === 'disabled'
|
||||
}
|
||||
|
||||
export default {
|
||||
getToken,
|
||||
setToken,
|
||||
clearToken,
|
||||
getUserInfo,
|
||||
setUserInfo,
|
||||
clearUserInfo,
|
||||
isLoggedIn,
|
||||
clearAuth,
|
||||
redirectToLogin,
|
||||
checkAuth,
|
||||
getUserPermissions,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
getUserId,
|
||||
getUsername,
|
||||
getUserPhone,
|
||||
getUserEmail,
|
||||
getUserAvatar,
|
||||
getUserNickname,
|
||||
getUserRealName,
|
||||
getUserDepartment,
|
||||
getUserPosition,
|
||||
getUserCreateTime,
|
||||
getUserLastLoginTime,
|
||||
updateUserInfo,
|
||||
login,
|
||||
logout,
|
||||
refreshToken,
|
||||
isTokenExpired,
|
||||
autoRefreshToken,
|
||||
getAuthHeader,
|
||||
saveLoginState,
|
||||
getLoginState,
|
||||
validateUserInfo,
|
||||
getUserDisplayName,
|
||||
getUserStatus,
|
||||
isUserActive,
|
||||
isUserDisabled
|
||||
}
|
||||
357
mini_program/farm-monitor-dashboard/utils/index.js
Normal file
357
mini_program/farm-monitor-dashboard/utils/index.js
Normal file
@@ -0,0 +1,357 @@
|
||||
// utils/index.js - 工具函数
|
||||
|
||||
// 格式化时间
|
||||
export const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
|
||||
const now = new Date()
|
||||
const targetTime = new Date(time)
|
||||
const diff = now - targetTime
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
if (days < 7) return `${days}天前`
|
||||
|
||||
return targetTime.toLocaleDateString()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
export const formatDate = (date, format = 'YYYY-MM-DD') => {
|
||||
if (!date) return ''
|
||||
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
// 防抖函数
|
||||
export const debounce = (func, wait) => {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export const throttle = (func, limit) => {
|
||||
let inThrottle
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
export const deepClone = (obj) => {
|
||||
if (obj === null || typeof obj !== 'object') return obj
|
||||
if (obj instanceof Date) return new Date(obj.getTime())
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
return clonedObj
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
export const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
export const validatePhone = (phone) => {
|
||||
const reg = /^1[3-9]\d{9}$/
|
||||
return reg.test(phone)
|
||||
}
|
||||
|
||||
// 验证邮箱
|
||||
export const validateEmail = (email) => {
|
||||
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return reg.test(email)
|
||||
}
|
||||
|
||||
// 验证身份证号
|
||||
export const validateIdCard = (idCard) => {
|
||||
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return reg.test(idCard)
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
export const getFileExtension = (filename) => {
|
||||
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
export const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 获取URL参数
|
||||
export const getUrlParams = (url) => {
|
||||
const params = {}
|
||||
const urlObj = new URL(url)
|
||||
urlObj.searchParams.forEach((value, key) => {
|
||||
params[key] = value
|
||||
})
|
||||
return params
|
||||
}
|
||||
|
||||
// 数组去重
|
||||
export const uniqueArray = (arr) => {
|
||||
return [...new Set(arr)]
|
||||
}
|
||||
|
||||
// 对象数组去重
|
||||
export const uniqueObjectArray = (arr, key) => {
|
||||
const seen = new Set()
|
||||
return arr.filter(item => {
|
||||
const value = item[key]
|
||||
if (seen.has(value)) {
|
||||
return false
|
||||
}
|
||||
seen.add(value)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 计算两个日期之间的天数
|
||||
export const daysBetween = (date1, date2) => {
|
||||
const oneDay = 24 * 60 * 60 * 1000
|
||||
const firstDate = new Date(date1)
|
||||
const secondDate = new Date(date2)
|
||||
return Math.round(Math.abs((firstDate - secondDate) / oneDay))
|
||||
}
|
||||
|
||||
// 获取当前时间戳
|
||||
export const getCurrentTimestamp = () => {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
// 获取当前日期字符串
|
||||
export const getCurrentDateString = () => {
|
||||
return new Date().toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// 获取当前时间字符串
|
||||
export const getCurrentTimeString = () => {
|
||||
return new Date().toTimeString().split(' ')[0]
|
||||
}
|
||||
|
||||
// 检查是否为移动设备
|
||||
export const isMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
export const getRandomColor = () => {
|
||||
const colors = ['#3cc51f', '#52c41a', '#faad14', '#f5222d', '#1890ff', '#722ed1', '#13c2c2', '#eb2f96']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 数字千分位格式化
|
||||
export const formatNumber = (num) => {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
// 金额格式化
|
||||
export const formatMoney = (amount, currency = '¥') => {
|
||||
return currency + formatNumber(amount.toFixed(2))
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
export const getStatusText = (status, statusMap) => {
|
||||
return statusMap[status] || '未知状态'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
export const getStatusColor = (status, colorMap) => {
|
||||
return colorMap[status] || '#909399'
|
||||
}
|
||||
|
||||
// 检查对象是否为空
|
||||
export const isEmpty = (obj) => {
|
||||
if (obj === null || obj === undefined) return true
|
||||
if (typeof obj === 'string') return obj.trim() === ''
|
||||
if (Array.isArray(obj)) return obj.length === 0
|
||||
if (typeof obj === 'object') return Object.keys(obj).length === 0
|
||||
return false
|
||||
}
|
||||
|
||||
// 安全的JSON解析
|
||||
export const safeJsonParse = (str, defaultValue = null) => {
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 安全的JSON字符串化
|
||||
export const safeJsonStringify = (obj, defaultValue = '{}') => {
|
||||
try {
|
||||
return JSON.stringify(obj)
|
||||
} catch (e) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 获取页面参数
|
||||
export const getPageParams = (options) => {
|
||||
const params = {}
|
||||
if (options && options.query) {
|
||||
Object.keys(options.query).forEach(key => {
|
||||
params[key] = options.query[key]
|
||||
})
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// 设置页面标题
|
||||
export const setPageTitle = (title) => {
|
||||
wx.setNavigationBarTitle({
|
||||
title: title
|
||||
})
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
export const showLoading = (title = '加载中...') => {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
// 隐藏加载提示
|
||||
export const hideLoading = () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
export const showSuccess = (title = '操作成功') => {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
export const showError = (title = '操作失败') => {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
export const showConfirm = (content, title = '提示') => {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title: title,
|
||||
content: content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
export const navigateTo = (url) => {
|
||||
wx.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 页面重定向
|
||||
export const redirectTo = (url) => {
|
||||
wx.redirectTo({ url })
|
||||
}
|
||||
|
||||
// 页面返回
|
||||
export const navigateBack = (delta = 1) => {
|
||||
wx.navigateBack({ delta })
|
||||
}
|
||||
|
||||
// 切换到tabBar页面
|
||||
export const switchTab = (url) => {
|
||||
wx.switchTab({ url })
|
||||
}
|
||||
|
||||
// 重新启动到指定页面
|
||||
export const reLaunch = (url) => {
|
||||
wx.reLaunch({ url })
|
||||
}
|
||||
|
||||
export default {
|
||||
formatTime,
|
||||
formatDate,
|
||||
debounce,
|
||||
throttle,
|
||||
deepClone,
|
||||
generateId,
|
||||
validatePhone,
|
||||
validateEmail,
|
||||
validateIdCard,
|
||||
getFileExtension,
|
||||
formatFileSize,
|
||||
getUrlParams,
|
||||
uniqueArray,
|
||||
uniqueObjectArray,
|
||||
daysBetween,
|
||||
getCurrentTimestamp,
|
||||
getCurrentDateString,
|
||||
getCurrentTimeString,
|
||||
isMobile,
|
||||
getRandomColor,
|
||||
formatNumber,
|
||||
formatMoney,
|
||||
getStatusText,
|
||||
getStatusColor,
|
||||
isEmpty,
|
||||
safeJsonParse,
|
||||
safeJsonStringify,
|
||||
getPageParams,
|
||||
setPageTitle,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showSuccess,
|
||||
showError,
|
||||
showConfirm,
|
||||
navigateTo,
|
||||
redirectTo,
|
||||
navigateBack,
|
||||
switchTab,
|
||||
reLaunch
|
||||
}
|
||||
110
mini_program/farm-monitor-dashboard/智能耳标预警功能完善说明.md
Normal file
110
mini_program/farm-monitor-dashboard/智能耳标预警功能完善说明.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 智能耳标预警功能完善说明
|
||||
|
||||
## 概述
|
||||
基于PC养殖端智能耳标预警的数据结构,完善了小程序端的智能耳标预警功能,确保数据格式和字段映射与PC端保持一致。
|
||||
|
||||
## 主要改进
|
||||
|
||||
### 1. 数据字段映射统一
|
||||
- **预警类型映射**:与PC端保持一致
|
||||
- `battery` → '低电量预警'
|
||||
- `offline` → '离线预警'
|
||||
- `temperature` → '温度预警'
|
||||
- `movement` → '异常运动预警'
|
||||
- `wear` → '佩戴异常预警'
|
||||
- `location` → '位置异常预警'
|
||||
|
||||
- **预警级别映射**:与PC端保持一致
|
||||
- `high` → '高级'
|
||||
- `medium` → '中级'
|
||||
- `low` → '低级'
|
||||
- `critical` → '紧急'
|
||||
|
||||
### 2. 数据结构完善
|
||||
小程序端现在支持以下字段,与PC端完全一致:
|
||||
|
||||
#### 基本信息
|
||||
- `id` - 预警ID
|
||||
- `eartagNumber` - 耳标编号
|
||||
- `deviceId` - 设备ID
|
||||
- `alertType` - 预警类型
|
||||
- `alertLevel` - 预警级别
|
||||
- `alertTime` - 预警时间
|
||||
- `title` - 预警标题
|
||||
- `description` - 预警描述
|
||||
- `status` - 处理状态
|
||||
|
||||
#### 设备信息
|
||||
- `battery` - 设备电量
|
||||
- `temperature` - 设备温度
|
||||
- `dailySteps` - 当日步数
|
||||
- `longitude` - 经度
|
||||
- `latitude` - 纬度
|
||||
- `handler` - 处理人
|
||||
|
||||
### 3. 时间格式化
|
||||
- 支持Unix时间戳转换
|
||||
- 支持字符串时间转换
|
||||
- 统一使用中文本地化格式
|
||||
|
||||
### 4. 预警卡片展示
|
||||
首页预警卡片现在显示:
|
||||
- 今日未被采集
|
||||
- 低电量预警
|
||||
- 离线预警
|
||||
- 温度预警(合并高温和低温)
|
||||
- 异常运动预警
|
||||
- 佩戴异常预警(仅项圈)
|
||||
|
||||
### 5. 预警列表增强
|
||||
- 显示耳标编号而非设备ID
|
||||
- 显示预警类型和级别
|
||||
- 显示设备电量、温度、步数等指标
|
||||
- 支持预警详情查看
|
||||
|
||||
### 6. 预警详情模态框
|
||||
- 基本信息:耳标编号、预警类型、预警级别、预警时间、处理状态、处理人
|
||||
- 设备信息:设备电量、设备温度、当日步数、位置坐标
|
||||
- 预警内容:标题和描述
|
||||
|
||||
## API接口
|
||||
- 获取耳标预警统计:`/api/smart-alerts/public/eartag/stats`
|
||||
- 获取项圈预警统计:`/api/smart-alerts/public/collar/stats`
|
||||
- 获取耳标预警列表:`/api/smart-alerts/public/eartag`
|
||||
- 获取项圈预警列表:`/api/smart-alerts/public/collar`
|
||||
- 处理耳标预警:`/api/smart-alerts/public/eartag/{id}/handle`
|
||||
- 处理项圈预警:`/api/smart-alerts/public/collar/{id}/handle`
|
||||
|
||||
## 测试验证
|
||||
创建了 `test-alert.html` 测试页面,包含:
|
||||
1. 登录测试
|
||||
2. 项圈预警统计测试
|
||||
3. 耳标预警统计测试
|
||||
4. 预警卡片展示测试
|
||||
5. 预警列表数据测试
|
||||
|
||||
## 文件修改清单
|
||||
1. `src/services/api.js` - 添加预警API服务
|
||||
2. `src/utils/mapping.js` - 添加预警字段映射
|
||||
3. `src/components/Home.vue` - 更新首页预警数据
|
||||
4. `src/components/SmartEartagAlert.vue` - 完善耳标预警组件
|
||||
5. `test-alert.html` - 创建测试页面
|
||||
|
||||
## 数据同步保证
|
||||
- 字段名称与PC端完全一致
|
||||
- 数据转换逻辑与PC端保持一致
|
||||
- 中文映射与PC端完全统一
|
||||
- API调用方式与PC端保持一致
|
||||
|
||||
## 使用说明
|
||||
1. 确保后端服务运行在 `http://localhost:5350`
|
||||
2. 前端服务运行在 `http://localhost:8080`
|
||||
3. 访问 `test-alert.html` 进行功能测试
|
||||
4. 在小程序中访问智能耳标预警页面查看实时数据
|
||||
|
||||
## 注意事项
|
||||
- 脚环预警暂时使用耳标数据(API中无独立脚环统计)
|
||||
- 主机预警使用固定数据(API中无主机统计)
|
||||
- 所有预警数据均为动态获取,无硬编码数据
|
||||
- 支持实时刷新和自动更新
|
||||
|
||||
263
mini_program/farm-monitor-wechat/README.md
Normal file
263
mini_program/farm-monitor-wechat/README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# 养殖端微信小程序 - 原生版本
|
||||
|
||||
## 项目简介
|
||||
|
||||
这是养殖管理系统的微信小程序原生版本,使用微信小程序原生开发框架,不依赖uniapp等第三方框架。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🐄 牛只管理
|
||||
- 牛只信息录入和管理
|
||||
- 牛只转移和出栏
|
||||
- 牛只统计和查询
|
||||
- 批量操作支持
|
||||
|
||||
### 📱 设备管理
|
||||
- 智能耳标管理
|
||||
- 智能项圈管理
|
||||
- 智能脚环管理
|
||||
- 智能主机管理
|
||||
- 设备实时监控
|
||||
|
||||
### ⚠️ 预警中心
|
||||
- 智能耳标预警
|
||||
- 智能项圈预警
|
||||
- 预警统计和分析
|
||||
- 预警处理和管理
|
||||
|
||||
### 📍 电子围栏
|
||||
- 围栏设置和管理
|
||||
- 越界预警
|
||||
- 地图显示
|
||||
|
||||
### 👤 个人中心
|
||||
- 用户信息管理
|
||||
- 系统设置
|
||||
- 帮助和支持
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: 微信小程序原生开发
|
||||
- **语言**: JavaScript ES6+
|
||||
- **样式**: WXSS
|
||||
- **模板**: WXML
|
||||
- **状态管理**: 小程序全局数据
|
||||
- **网络请求**: wx.request
|
||||
- **UI组件**: 微信小程序原生组件
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
farm-monitor-wechat/
|
||||
├── app.js # 小程序入口文件
|
||||
├── app.json # 小程序全局配置
|
||||
├── app.wxss # 小程序全局样式
|
||||
├── sitemap.json # 站点地图配置
|
||||
├── project.config.json # 项目配置文件
|
||||
├── project.private.config.json # 私有配置文件
|
||||
├── pages/ # 页面目录
|
||||
│ ├── index/ # 首页
|
||||
│ ├── login/ # 登录页
|
||||
│ ├── home/ # 首页Tab
|
||||
│ ├── cattle/ # 牛只管理
|
||||
│ ├── device/ # 设备管理
|
||||
│ ├── alert/ # 预警中心
|
||||
│ └── profile/ # 个人中心
|
||||
├── services/ # 服务层
|
||||
│ ├── api.js # API请求服务
|
||||
│ ├── authService.js # 认证服务
|
||||
│ ├── cattleService.js # 牛只管理服务
|
||||
│ ├── deviceService.js # 设备管理服务
|
||||
│ └── alertService.js # 预警服务
|
||||
├── components/ # 自定义组件
|
||||
├── utils/ # 工具函数
|
||||
├── images/ # 图片资源
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 环境要求
|
||||
- 微信开发者工具 1.06.0+
|
||||
- Node.js 16.0+
|
||||
- 微信小程序账号
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd farm-monitor-wechat
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **配置项目**
|
||||
- 在微信开发者工具中打开项目
|
||||
- 修改 `app.json` 中的 `appid`
|
||||
- 修改 `services/api.js` 中的 `baseUrl`
|
||||
|
||||
4. **启动开发**
|
||||
- 在微信开发者工具中点击"编译"
|
||||
- 在模拟器中预览效果
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 1. 小程序配置 (app.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [...],
|
||||
"tabBar": {...},
|
||||
"window": {...},
|
||||
"permission": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. API配置 (services/api.js)
|
||||
|
||||
```javascript
|
||||
const apiService = new ApiService()
|
||||
apiService.baseUrl = 'http://your-api-domain.com/api'
|
||||
```
|
||||
|
||||
### 3. 项目配置 (project.config.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"appid": "wx-your-appid-here",
|
||||
"projectname": "farm-monitor-wechat"
|
||||
}
|
||||
```
|
||||
|
||||
## 页面说明
|
||||
|
||||
### 主要页面
|
||||
|
||||
1. **首页 (pages/index)**
|
||||
- 应用启动页
|
||||
- 用户登录引导
|
||||
|
||||
2. **登录页 (pages/login)**
|
||||
- 密码登录
|
||||
- 短信验证码登录
|
||||
- 用户注册
|
||||
|
||||
3. **首页Tab (pages/home)**
|
||||
- 数据统计展示
|
||||
- 快捷操作入口
|
||||
- 最近预警信息
|
||||
|
||||
4. **牛只管理 (pages/cattle)**
|
||||
- 牛只列表
|
||||
- 牛只详情
|
||||
- 添加/编辑牛只
|
||||
|
||||
5. **设备管理 (pages/device)**
|
||||
- 设备列表
|
||||
- 设备详情
|
||||
- 设备控制
|
||||
|
||||
6. **预警中心 (pages/alert)**
|
||||
- 预警列表
|
||||
- 预警详情
|
||||
- 预警处理
|
||||
|
||||
7. **个人中心 (pages/profile)**
|
||||
- 用户信息
|
||||
- 系统设置
|
||||
- 帮助支持
|
||||
|
||||
## API接口
|
||||
|
||||
### 认证接口
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `POST /auth/sms-login` - 短信登录
|
||||
- `POST /auth/register` - 用户注册
|
||||
- `GET /auth/user-info` - 获取用户信息
|
||||
|
||||
### 牛只管理接口
|
||||
- `GET /cattle` - 获取牛只列表
|
||||
- `POST /cattle` - 添加牛只
|
||||
- `PUT /cattle/:id` - 更新牛只信息
|
||||
- `DELETE /cattle/:id` - 删除牛只
|
||||
|
||||
### 设备管理接口
|
||||
- `GET /device` - 获取设备列表
|
||||
- `GET /device/:id` - 获取设备详情
|
||||
- `POST /device` - 添加设备
|
||||
- `PUT /device/:id` - 更新设备信息
|
||||
|
||||
### 预警管理接口
|
||||
- `GET /smart-alerts/public` - 获取预警列表
|
||||
- `GET /smart-alerts/public/stats` - 获取预警统计
|
||||
- `POST /smart-alerts/public/:id/handle` - 处理预警
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 1. 代码规范
|
||||
- 使用ES6+语法
|
||||
- 遵循微信小程序开发规范
|
||||
- 统一使用2空格缩进
|
||||
- 使用有意义的变量和函数名
|
||||
|
||||
### 2. 文件命名
|
||||
- 页面文件使用小写字母和连字符
|
||||
- 组件文件使用PascalCase
|
||||
- 工具文件使用camelCase
|
||||
|
||||
### 3. 注释规范
|
||||
- 文件头部添加文件说明
|
||||
- 函数添加JSDoc注释
|
||||
- 复杂逻辑添加行内注释
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 1. 开发环境
|
||||
- 在微信开发者工具中直接运行
|
||||
- 使用测试API接口
|
||||
|
||||
### 2. 生产环境
|
||||
- 上传代码到微信小程序后台
|
||||
- 配置生产环境API接口
|
||||
- 提交审核发布
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 网络请求失败
|
||||
- 检查API接口地址是否正确
|
||||
- 确认服务器是否正常运行
|
||||
- 检查网络连接状态
|
||||
|
||||
### 2. 登录失败
|
||||
- 检查用户名密码是否正确
|
||||
- 确认API接口返回格式
|
||||
- 查看控制台错误信息
|
||||
|
||||
### 3. 页面显示异常
|
||||
- 检查数据绑定是否正确
|
||||
- 确认样式文件是否引入
|
||||
- 查看控制台错误信息
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 初始版本发布
|
||||
- 实现基础功能模块
|
||||
- 完成用户认证系统
|
||||
- 完成牛只管理功能
|
||||
- 完成设备管理功能
|
||||
- 完成预警中心功能
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 项目负责人: [姓名]
|
||||
- 邮箱: [email]
|
||||
- 电话: [phone]
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证,详情请查看 [LICENSE](LICENSE) 文件。
|
||||
196
mini_program/farm-monitor-wechat/app.js
Normal file
196
mini_program/farm-monitor-wechat/app.js
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 养殖端微信小程序 - 原生版本
|
||||
* @file app.js
|
||||
* @description 小程序入口文件
|
||||
*/
|
||||
|
||||
// 引入API服务
|
||||
const apiService = require('./services/api.js')
|
||||
|
||||
App({
|
||||
/**
|
||||
* 全局数据
|
||||
*/
|
||||
globalData: {
|
||||
version: '1.0.0',
|
||||
platform: 'wechat',
|
||||
isDevelopment: true,
|
||||
userInfo: null,
|
||||
token: null,
|
||||
baseUrl: 'http://localhost:5350/api'
|
||||
},
|
||||
|
||||
/**
|
||||
* 小程序初始化
|
||||
*/
|
||||
onLaunch: function(options) {
|
||||
console.log('小程序启动', options)
|
||||
|
||||
// 检查更新
|
||||
this.checkForUpdate()
|
||||
|
||||
// 初始化用户信息
|
||||
this.initUserInfo()
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
},
|
||||
|
||||
/**
|
||||
* 小程序显示
|
||||
*/
|
||||
onShow: function(options) {
|
||||
console.log('小程序显示', options)
|
||||
},
|
||||
|
||||
/**
|
||||
* 小程序隐藏
|
||||
*/
|
||||
onHide: function() {
|
||||
console.log('小程序隐藏')
|
||||
},
|
||||
|
||||
/**
|
||||
* 小程序错误
|
||||
*/
|
||||
onError: function(msg) {
|
||||
console.error('小程序错误:', msg)
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*/
|
||||
checkForUpdate: function() {
|
||||
if (wx.canIUse('getUpdateManager')) {
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate(function(res) {
|
||||
console.log('检查更新结果:', res.hasUpdate)
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(function() {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success: function(res) {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(function() {
|
||||
console.log('新版本下载失败')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化用户信息
|
||||
*/
|
||||
initUserInfo: function() {
|
||||
try {
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
if (userInfo) {
|
||||
this.globalData.userInfo = userInfo
|
||||
}
|
||||
|
||||
if (token) {
|
||||
this.globalData.token = token
|
||||
}
|
||||
|
||||
console.log('用户信息初始化完成:', userInfo)
|
||||
} catch (error) {
|
||||
console.error('初始化用户信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
checkLoginStatus: function() {
|
||||
const token = this.globalData.token
|
||||
const userInfo = this.globalData.userInfo
|
||||
|
||||
if (token && userInfo) {
|
||||
console.log('用户已登录')
|
||||
// 验证token有效性
|
||||
this.validateToken()
|
||||
} else {
|
||||
console.log('用户未登录')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证token有效性
|
||||
*/
|
||||
validateToken: function() {
|
||||
apiService.validateToken()
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
console.log('Token验证成功')
|
||||
} else {
|
||||
console.log('Token已过期,清除登录信息')
|
||||
this.clearLoginInfo()
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Token验证失败:', error)
|
||||
this.clearLoginInfo()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除登录信息
|
||||
*/
|
||||
clearLoginInfo: function() {
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.token = null
|
||||
|
||||
try {
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.removeStorageSync('token')
|
||||
} catch (error) {
|
||||
console.error('清除登录信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
setUserInfo: function(userInfo, token) {
|
||||
this.globalData.userInfo = userInfo
|
||||
this.globalData.token = token
|
||||
|
||||
try {
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
wx.setStorageSync('token', token)
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserInfo: function() {
|
||||
return this.globalData.userInfo
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
*/
|
||||
getToken: function() {
|
||||
return this.globalData.token
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否已登录
|
||||
*/
|
||||
isLoggedIn: function() {
|
||||
return !!(this.globalData.userInfo && this.globalData.token)
|
||||
}
|
||||
})
|
||||
79
mini_program/farm-monitor-wechat/app.json
Normal file
79
mini_program/farm-monitor-wechat/app.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/login/login",
|
||||
"pages/home/home",
|
||||
"pages/cattle/cattle",
|
||||
"pages/cattle/add/add",
|
||||
"pages/cattle/detail/detail",
|
||||
"pages/cattle/transfer/transfer",
|
||||
"pages/cattle/exit/exit",
|
||||
"pages/device/device",
|
||||
"pages/device/eartag/eartag",
|
||||
"pages/device/collar/collar",
|
||||
"pages/device/ankle/ankle",
|
||||
"pages/device/host/host",
|
||||
"pages/alert/alert",
|
||||
"pages/alert/eartag/eartag",
|
||||
"pages/alert/collar/collar",
|
||||
"pages/fence/fence",
|
||||
"pages/profile/profile"
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3cc51f",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"iconPath": "images/home.png",
|
||||
"selectedIconPath": "images/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/cattle/cattle",
|
||||
"iconPath": "images/cattle.png",
|
||||
"selectedIconPath": "images/cattle-active.png",
|
||||
"text": "牛只管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/device/device",
|
||||
"iconPath": "images/device.png",
|
||||
"selectedIconPath": "images/device-active.png",
|
||||
"text": "设备管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/alert/alert",
|
||||
"iconPath": "images/alert.png",
|
||||
"selectedIconPath": "images/alert-active.png",
|
||||
"text": "预警中心"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "images/profile.png",
|
||||
"selectedIconPath": "images/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "养殖管理系统",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8f8f8"
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
"downloadFile": 10000
|
||||
},
|
||||
"debug": true,
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于养殖场定位和地图展示"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": ["location"],
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
309
mini_program/farm-monitor-wechat/app.wxss
Normal file
309
mini_program/farm-monitor-wechat/app.wxss
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 养殖端微信小程序 - 全局样式
|
||||
* @file app.wxss
|
||||
* @description 全局样式文件
|
||||
*/
|
||||
|
||||
/* 全局样式重置 */
|
||||
page {
|
||||
background-color: #f8f8f8;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 40rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2db815;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4757;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffa502;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 30rpx 60rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #3cc51f;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.list {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-item-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.list-item-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.list-item-arrow {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.status-normal {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
flex: 1;
|
||||
min-width: 140rpx;
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 工具类 */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.text-primary { color: #3cc51f; }
|
||||
.text-secondary { color: #666; }
|
||||
.text-danger { color: #ff4757; }
|
||||
.text-warning { color: #ffa502; }
|
||||
.text-muted { color: #999; }
|
||||
|
||||
.bg-primary { background-color: #3cc51f; }
|
||||
.bg-secondary { background-color: #f0f0f0; }
|
||||
.bg-danger { background-color: #ff4757; }
|
||||
.bg-warning { background-color: #ffa502; }
|
||||
|
||||
.mt-10 { margin-top: 10rpx; }
|
||||
.mt-20 { margin-top: 20rpx; }
|
||||
.mt-30 { margin-top: 30rpx; }
|
||||
|
||||
.mb-10 { margin-bottom: 10rpx; }
|
||||
.mb-20 { margin-bottom: 20rpx; }
|
||||
.mb-30 { margin-bottom: 30rpx; }
|
||||
|
||||
.p-10 { padding: 10rpx; }
|
||||
.p-20 { padding: 20rpx; }
|
||||
.p-30 { padding: 30rpx; }
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||
.flex-between { display: flex; align-items: center; justify-content: space-between; }
|
||||
.flex-column { display: flex; flex-direction: column; }
|
||||
|
||||
.flex-1 { flex: 1; }
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.container {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
min-width: 120rpx;
|
||||
padding: 15rpx;
|
||||
}
|
||||
}
|
||||
94
mini_program/farm-monitor-wechat/create-missing-pages.js
Normal file
94
mini_program/farm-monitor-wechat/create-missing-pages.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 创建缺失页面的脚本
|
||||
* @file create-missing-pages.js
|
||||
* @description 批量创建缺失的页面文件
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// 需要创建的页面列表
|
||||
const pages = [
|
||||
'pages/cattle/exit/exit',
|
||||
'pages/device/device',
|
||||
'pages/device/eartag/eartag',
|
||||
'pages/device/collar/collar',
|
||||
'pages/device/ankle/ankle',
|
||||
'pages/device/host/host',
|
||||
'pages/alert/eartag/eartag',
|
||||
'pages/alert/collar/collar',
|
||||
'pages/fence/fence'
|
||||
]
|
||||
|
||||
// 基础JS模板
|
||||
const jsTemplate = `/**
|
||||
* 页面
|
||||
* @file {filename}.js
|
||||
* @description 页面描述
|
||||
*/
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
console.log('页面加载', options)
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
console.log('页面显示')
|
||||
}
|
||||
})`
|
||||
|
||||
// 基础WXML模板
|
||||
const wxmlTemplate = `<!--页面-->
|
||||
<view class="container">
|
||||
<view class="content">
|
||||
<text>页面内容</text>
|
||||
</view>
|
||||
</view>`
|
||||
|
||||
// 基础WXSS模板
|
||||
const wxssTemplate = `/* 页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
text-align: center;
|
||||
}`
|
||||
|
||||
// 创建页面的函数
|
||||
function createPage(pagePath) {
|
||||
const dir = path.dirname(pagePath)
|
||||
const filename = path.basename(pagePath)
|
||||
|
||||
// 创建目录
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
// 创建JS文件
|
||||
const jsContent = jsTemplate.replace('{filename}', filename)
|
||||
fs.writeFileSync(`${pagePath}.js`, jsContent)
|
||||
|
||||
// 创建WXML文件
|
||||
fs.writeFileSync(`${pagePath}.wxml`, wxmlTemplate)
|
||||
|
||||
// 创建WXSS文件
|
||||
fs.writeFileSync(`${pagePath}.wxss`, wxssTemplate)
|
||||
|
||||
console.log(`创建页面: ${pagePath}`)
|
||||
}
|
||||
|
||||
// 创建所有页面
|
||||
pages.forEach(createPage)
|
||||
|
||||
console.log('所有页面创建完成!')
|
||||
|
||||
118
mini_program/farm-monitor-wechat/create-remaining-pages.js
Normal file
118
mini_program/farm-monitor-wechat/create-remaining-pages.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 创建剩余页面的脚本
|
||||
* @file create-remaining-pages.js
|
||||
* @description 批量创建剩余的页面文件
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// 需要创建的页面列表
|
||||
const pages = [
|
||||
{ path: 'pages/device/ankle/ankle', title: '智能脚环管理', icon: '🦶' },
|
||||
{ path: 'pages/device/host/host', title: '智能主机管理', icon: '📡' },
|
||||
{ path: 'pages/alert/eartag/eartag', title: '智能耳标预警', icon: '🏷️' },
|
||||
{ path: 'pages/alert/collar/collar', title: '智能项圈预警', icon: '📿' },
|
||||
{ path: 'pages/fence/fence', title: '电子围栏管理', icon: '📍' }
|
||||
]
|
||||
|
||||
// 基础JS模板
|
||||
const jsTemplate = `/**
|
||||
* 页面
|
||||
* @file {filename}.js
|
||||
* @description {title}
|
||||
*/
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad: function (options) {
|
||||
console.log('{title}页面加载', options)
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
console.log('{title}页面显示')
|
||||
}
|
||||
})`
|
||||
|
||||
// 基础WXML模板
|
||||
const wxmlTemplate = `<!--{title}页面-->
|
||||
<view class="container">
|
||||
<view class="content">
|
||||
<view class="icon">{icon}</view>
|
||||
<view class="title">{title}</view>
|
||||
<view class="desc">功能开发中...</view>
|
||||
</view>
|
||||
</view>`
|
||||
|
||||
// 基础WXSS模板
|
||||
const wxssTemplate = `/* {title}页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 80rpx 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}`
|
||||
|
||||
// 创建页面的函数
|
||||
function createPage(pageInfo) {
|
||||
const { path: pagePath, title, icon } = pageInfo
|
||||
const dir = path.dirname(pagePath)
|
||||
const filename = path.basename(pagePath)
|
||||
|
||||
// 创建目录
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
// 创建JS文件
|
||||
const jsContent = jsTemplate
|
||||
.replace(/{filename}/g, filename)
|
||||
.replace(/{title}/g, title)
|
||||
fs.writeFileSync(`${pagePath}.js`, jsContent)
|
||||
|
||||
// 创建WXML文件
|
||||
const wxmlContent = wxmlTemplate
|
||||
.replace(/{title}/g, title)
|
||||
.replace(/{icon}/g, icon)
|
||||
fs.writeFileSync(`${pagePath}.wxml`, wxmlContent)
|
||||
|
||||
// 创建WXSS文件
|
||||
const wxssContent = wxssTemplate
|
||||
.replace(/{title}/g, title)
|
||||
fs.writeFileSync(`${pagePath}.wxss`, wxssContent)
|
||||
|
||||
console.log(`创建页面: ${pagePath}`)
|
||||
}
|
||||
|
||||
// 创建所有页面
|
||||
pages.forEach(createPage)
|
||||
|
||||
console.log('所有剩余页面创建完成!')
|
||||
|
||||
53
mini_program/farm-monitor-wechat/images/README.md
Normal file
53
mini_program/farm-monitor-wechat/images/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 图片资源说明
|
||||
|
||||
## 图片文件列表
|
||||
|
||||
### Tab图标
|
||||
- `home.png` - 首页图标(未选中)
|
||||
- `home-active.png` - 首页图标(选中)
|
||||
- `cattle.png` - 牛只管理图标(未选中)
|
||||
- `cattle-active.png` - 牛只管理图标(选中)
|
||||
- `device.png` - 设备管理图标(未选中)
|
||||
- `device-active.png` - 设备管理图标(选中)
|
||||
- `alert.png` - 预警中心图标(未选中)
|
||||
- `alert-active.png` - 预警中心图标(选中)
|
||||
- `profile.png` - 个人中心图标(未选中)
|
||||
- `profile-active.png` - 个人中心图标(选中)
|
||||
|
||||
### 应用图标
|
||||
- `logo.png` - 应用Logo
|
||||
- `default-avatar.png` - 默认头像
|
||||
|
||||
## 图片规格要求
|
||||
|
||||
### Tab图标
|
||||
- 尺寸: 81px × 81px
|
||||
- 格式: PNG
|
||||
- 背景: 透明
|
||||
- 颜色: 灰色(未选中)/ 主题色(选中)
|
||||
|
||||
### 应用Logo
|
||||
- 尺寸: 1024px × 1024px
|
||||
- 格式: PNG
|
||||
- 背景: 透明或白色
|
||||
- 设计: 简洁明了,符合养殖主题
|
||||
|
||||
### 默认头像
|
||||
- 尺寸: 200px × 200px
|
||||
- 格式: PNG
|
||||
- 背景: 透明
|
||||
- 设计: 通用用户头像
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 将图片文件放置在 `images/` 目录下
|
||||
2. 确保文件名与代码中的引用一致
|
||||
3. 图片大小控制在合理范围内,避免影响加载速度
|
||||
4. 建议使用WebP格式以减小文件大小
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有图片都需要适配不同分辨率的设备
|
||||
- 建议提供2x和3x的高清版本
|
||||
- 图片命名使用小写字母和连字符
|
||||
- 定期优化图片大小,提升加载性能
|
||||
1
mini_program/farm-monitor-wechat/images/alert-active.png
Normal file
1
mini_program/farm-monitor-wechat/images/alert-active.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
1
mini_program/farm-monitor-wechat/images/alert.png
Normal file
1
mini_program/farm-monitor-wechat/images/alert.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
1
mini_program/farm-monitor-wechat/images/cattle.png
Normal file
1
mini_program/farm-monitor-wechat/images/cattle.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
1
mini_program/farm-monitor-wechat/images/device.png
Normal file
1
mini_program/farm-monitor-wechat/images/device.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
1
mini_program/farm-monitor-wechat/images/home.png
Normal file
1
mini_program/farm-monitor-wechat/images/home.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
1
mini_program/farm-monitor-wechat/images/profile.png
Normal file
1
mini_program/farm-monitor-wechat/images/profile.png
Normal file
@@ -0,0 +1 @@
|
||||
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
30
mini_program/farm-monitor-wechat/package.json
Normal file
30
mini_program/farm-monitor-wechat/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "farm-monitor-wechat",
|
||||
"version": "1.0.0",
|
||||
"description": "养殖端微信小程序 - 原生版本",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"dev": "echo '请在微信开发者工具中打开项目进行开发'",
|
||||
"build": "echo '请在微信开发者工具中构建项目'",
|
||||
"test": "echo '测试功能'"
|
||||
},
|
||||
"keywords": [
|
||||
"微信小程序",
|
||||
"养殖管理",
|
||||
"物联网",
|
||||
"智能设备"
|
||||
],
|
||||
"author": "养殖管理系统开发团队",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/your-org/farm-monitor-wechat.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/your-org/farm-monitor-wechat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/your-org/farm-monitor-wechat#readme"
|
||||
}
|
||||
379
mini_program/farm-monitor-wechat/pages/alert/alert.js
Normal file
379
mini_program/farm-monitor-wechat/pages/alert/alert.js
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* 预警中心页面
|
||||
* @file alert.js
|
||||
* @description 预警中心列表页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const alertService = require('../../services/alertService.js')
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
alertList: [],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
searchKeyword: '',
|
||||
filterType: '',
|
||||
filterLevel: '',
|
||||
total: 0,
|
||||
showFilter: false,
|
||||
searchInput: '',
|
||||
stats: {
|
||||
total: 0,
|
||||
unread: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('预警中心页面加载', options)
|
||||
this.loadAlertData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow: function () {
|
||||
console.log('预警中心页面显示')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh: function () {
|
||||
console.log('下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom: function () {
|
||||
console.log('上拉触底')
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage: function () {
|
||||
return {
|
||||
title: '预警中心',
|
||||
path: '/pages/alert/alert'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警数据
|
||||
*/
|
||||
loadAlertData: function(reset = true) {
|
||||
if (reset) {
|
||||
this.setData({
|
||||
loading: true,
|
||||
page: 1,
|
||||
hasMore: true
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
// 并行加载统计数据和列表数据
|
||||
Promise.all([
|
||||
this.loadAlertStats(),
|
||||
this.loadAlertList(reset)
|
||||
]).then(() => {
|
||||
console.log('预警数据加载完成')
|
||||
}).catch(error => {
|
||||
console.error('预警数据加载失败:', error)
|
||||
}).finally(() => {
|
||||
this.setData({
|
||||
loading: false,
|
||||
refreshing: false
|
||||
})
|
||||
if (reset) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警统计
|
||||
*/
|
||||
loadAlertStats: function() {
|
||||
return alertService.getAlertStats()
|
||||
.then(res => {
|
||||
console.log('预警统计加载成功', res)
|
||||
this.setData({
|
||||
stats: {
|
||||
total: res.data.totalAlerts || 0,
|
||||
unread: res.data.unreadAlerts || 0,
|
||||
high: res.data.highLevel || 0,
|
||||
medium: res.data.mediumLevel || 0,
|
||||
low: res.data.lowLevel || 0
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('预警统计加载失败:', error)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警列表
|
||||
*/
|
||||
loadAlertList: function(reset = true) {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
limit: this.data.limit,
|
||||
search: this.data.searchKeyword,
|
||||
alertType: this.data.filterType,
|
||||
alertLevel: this.data.filterLevel
|
||||
}
|
||||
|
||||
return alertService.getAlertList(params)
|
||||
.then(res => {
|
||||
console.log('预警列表加载成功', res)
|
||||
|
||||
const newList = res.data || []
|
||||
const alertList = reset ? newList : [...this.data.alertList, ...newList]
|
||||
|
||||
this.setData({
|
||||
alertList: alertList,
|
||||
total: res.total || 0,
|
||||
hasMore: newList.length >= this.data.limit
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('预警列表加载失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
refreshData: function() {
|
||||
this.setData({
|
||||
refreshing: true
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载更多数据
|
||||
*/
|
||||
loadMoreData: function() {
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
this.loadAlertData(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换筛选显示
|
||||
*/
|
||||
toggleFilter: function() {
|
||||
this.setData({
|
||||
showFilter: !this.data.showFilter,
|
||||
searchInput: this.data.searchKeyword
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索输入
|
||||
*/
|
||||
onSearchInput: function(e) {
|
||||
this.setData({
|
||||
searchInput: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
doSearch: function() {
|
||||
this.setData({
|
||||
searchKeyword: this.data.searchInput,
|
||||
showFilter: false
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消搜索
|
||||
*/
|
||||
cancelSearch: function() {
|
||||
this.setData({
|
||||
showFilter: false,
|
||||
searchInput: this.data.searchKeyword
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空搜索
|
||||
*/
|
||||
clearSearch: function() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
searchInput: '',
|
||||
filterType: '',
|
||||
filterLevel: ''
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 类型筛选
|
||||
*/
|
||||
onTypeFilter: function(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
filterType: type
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 级别筛选
|
||||
*/
|
||||
onLevelFilter: function(e) {
|
||||
const level = e.currentTarget.dataset.level
|
||||
this.setData({
|
||||
filterLevel: level
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看预警详情
|
||||
*/
|
||||
viewAlertDetail: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/alert/detail/detail?id=${alertId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理预警
|
||||
*/
|
||||
handleAlert: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
const alertType = e.currentTarget.dataset.type
|
||||
|
||||
wx.showModal({
|
||||
title: '处理预警',
|
||||
content: '确定要处理此预警吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.confirmHandleAlert(alertId, alertType)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认处理预警
|
||||
*/
|
||||
confirmHandleAlert: function(alertId, alertType) {
|
||||
wx.showLoading({
|
||||
title: '处理中...'
|
||||
})
|
||||
|
||||
const handleData = {
|
||||
action: 'acknowledged',
|
||||
notes: '通过小程序处理',
|
||||
handler: app.getUserInfo()?.nickName || '用户'
|
||||
}
|
||||
|
||||
alertService.handleAlert(alertId, handleData)
|
||||
.then(res => {
|
||||
console.log('处理预警成功', res)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('处理预警失败:', error)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: error.message || '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警类型文本
|
||||
*/
|
||||
getAlertTypeText: function(type) {
|
||||
const typeMap = {
|
||||
'battery': '低电量',
|
||||
'offline': '离线',
|
||||
'temperature': '温度异常',
|
||||
'movement': '运动异常',
|
||||
'wear': '脱落'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别文本
|
||||
*/
|
||||
getAlertLevelText: function(level) {
|
||||
const levelMap = {
|
||||
'high': '高',
|
||||
'medium': '中',
|
||||
'low': '低'
|
||||
}
|
||||
return levelMap[level] || level
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别颜色
|
||||
*/
|
||||
getAlertLevelColor: function(level) {
|
||||
const colorMap = {
|
||||
'high': 'red',
|
||||
'medium': 'orange',
|
||||
'low': 'green'
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警类型图标
|
||||
*/
|
||||
getAlertTypeIcon: function(type) {
|
||||
const iconMap = {
|
||||
'battery': '🔋',
|
||||
'offline': '📵',
|
||||
'temperature': '🌡️',
|
||||
'movement': '🏃',
|
||||
'wear': '⚠️'
|
||||
}
|
||||
return iconMap[type] || '⚠️'
|
||||
}
|
||||
})
|
||||
221
mini_program/farm-monitor-wechat/pages/alert/alert.wxml
Normal file
221
mini_program/farm-monitor-wechat/pages/alert/alert.wxml
Normal file
@@ -0,0 +1,221 @@
|
||||
<!--预警中心页面-->
|
||||
<view class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">⚠️</view>
|
||||
<view class="stats-number">{{stats.total}}</view>
|
||||
<view class="stats-label">总预警</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🔴</view>
|
||||
<view class="stats-number">{{stats.high}}</view>
|
||||
<view class="stats-label">高级</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🟡</view>
|
||||
<view class="stats-number">{{stats.medium}}</view>
|
||||
<view class="stats-label">中级</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🟢</view>
|
||||
<view class="stats-number">{{stats.low}}</view>
|
||||
<view class="stats-label">低级</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="搜索设备编号、预警类型..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="doSearch"
|
||||
/>
|
||||
<view class="search-icon" bindtap="doSearch">🔍</view>
|
||||
</view>
|
||||
<view class="search-actions">
|
||||
<view class="action-btn" bindtap="toggleFilter">筛选</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="filter-bar" wx:if="{{showFilter}}">
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警类型</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterType === '' ? 'active' : ''}}"
|
||||
data-type=""
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'battery' ? 'active' : ''}}"
|
||||
data-type="battery"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
低电量
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'offline' ? 'active' : ''}}"
|
||||
data-type="offline"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
离线
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'temperature' ? 'active' : ''}}"
|
||||
data-type="temperature"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
温度异常
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'movement' ? 'active' : ''}}"
|
||||
data-type="movement"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
运动异常
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'wear' ? 'active' : ''}}"
|
||||
data-type="wear"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
脱落
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警级别</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterLevel === '' ? 'active' : ''}}"
|
||||
data-level=""
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'high' ? 'active' : ''}}"
|
||||
data-level="high"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
高级
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'medium' ? 'active' : ''}}"
|
||||
data-level="medium"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
中级
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'low' ? 'active' : ''}}"
|
||||
data-level="low"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
低级
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-actions">
|
||||
<button class="btn btn-secondary btn-small" bindtap="clearSearch">清空</button>
|
||||
<button class="btn btn-primary btn-small" bindtap="doSearch">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-info">
|
||||
<text class="stats-text">共 {{total}} 条预警</text>
|
||||
<text class="stats-text">{{alertList.length}} 条记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && alertList.length === 0}}" class="loading">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 预警列表 -->
|
||||
<view wx:elif="{{alertList.length > 0}}" class="alert-list">
|
||||
<view
|
||||
wx:for="{{alertList}}"
|
||||
wx:key="id"
|
||||
class="alert-item"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="viewAlertDetail"
|
||||
>
|
||||
<view class="alert-header">
|
||||
<view class="alert-icon {{getAlertLevelColor(item.alertLevel)}}">
|
||||
<text>{{getAlertTypeIcon(item.alertType)}}</text>
|
||||
</view>
|
||||
<view class="alert-info">
|
||||
<view class="alert-title">{{item.description}}</view>
|
||||
<view class="alert-device">{{item.deviceName || item.collarNumber || item.eartagNumber}}</view>
|
||||
</view>
|
||||
<view class="alert-level {{getAlertLevelColor(item.alertLevel)}}">
|
||||
<text>{{getAlertLevelText(item.alertLevel)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">类型:</text>
|
||||
<text class="detail-value">{{getAlertTypeText(item.alertType)}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">时间:</text>
|
||||
<text class="detail-value">{{item.alertTime}}</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.battery !== undefined}}">
|
||||
<text class="detail-label">电量:</text>
|
||||
<text class="detail-value">{{item.battery}}%</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.temperature !== undefined}}">
|
||||
<text class="detail-label">温度:</text>
|
||||
<text class="detail-value">{{item.temperature}}°C</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-actions">
|
||||
<button
|
||||
class="action-btn handle"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.alertType}}"
|
||||
catchtap="handleAlert"
|
||||
>
|
||||
处理预警
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">📭</view>
|
||||
<view class="empty-text">暂无预警信息</view>
|
||||
<view class="empty-desc">系统运行正常,无预警数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading && alertList.length > 0}}" class="load-more">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!hasMore && alertList.length > 0}}" class="load-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
473
mini_program/farm-monitor-wechat/pages/alert/alert.wxss
Normal file
473
mini_program/farm-monitor-wechat/pages/alert/alert.wxss
Normal file
@@ -0,0 +1,473 @@
|
||||
/* 预警中心页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-section {
|
||||
padding: 30rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 35rpx;
|
||||
padding: 0 50rpx 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input .input:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 15rpx 25rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background: #2db815;
|
||||
}
|
||||
|
||||
/* 筛选条件 */
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.filter-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-info {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e0e0e0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 预警列表 */
|
||||
.alert-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.alert-icon.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.alert-icon.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.alert-icon.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.alert-icon.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-level.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.alert-level.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.alert-level.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.alert-level.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-bottom: 25rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 15rpx 30rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn.handle {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 40rpx;
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.load-more .loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.stats-section {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 25rpx 15rpx;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 15rpx 20rpx;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 15rpx 20rpx;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.detail-label,
|
||||
.detail-value {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 12rpx 25rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
330
mini_program/farm-monitor-wechat/pages/alert/collar/collar.js
Normal file
330
mini_program/farm-monitor-wechat/pages/alert/collar/collar.js
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 智能项圈预警页面
|
||||
* @file collar.js
|
||||
* @description 智能项圈预警管理页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const alertService = require('../../../services/alertService.js')
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
alertList: [],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
searchKeyword: '',
|
||||
filterType: '',
|
||||
filterLevel: '',
|
||||
total: 0,
|
||||
stats: {
|
||||
total: 0,
|
||||
unread: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('智能项圈预警页面加载', options)
|
||||
this.loadAlertData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow: function () {
|
||||
console.log('智能项圈预警页面显示')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh: function () {
|
||||
console.log('下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom: function () {
|
||||
console.log('上拉触底')
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警数据
|
||||
*/
|
||||
loadAlertData: function(reset = true) {
|
||||
if (reset) {
|
||||
this.setData({
|
||||
loading: true,
|
||||
page: 1,
|
||||
hasMore: true
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
// 并行加载统计数据和列表数据
|
||||
Promise.all([
|
||||
this.loadAlertStats(),
|
||||
this.loadAlertList(reset)
|
||||
]).then(() => {
|
||||
console.log('项圈预警数据加载完成')
|
||||
}).catch(error => {
|
||||
console.error('项圈预警数据加载失败:', error)
|
||||
}).finally(() => {
|
||||
this.setData({
|
||||
loading: false,
|
||||
refreshing: false
|
||||
})
|
||||
if (reset) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警统计
|
||||
*/
|
||||
loadAlertStats: function() {
|
||||
return alertService.getCollarAlertStats()
|
||||
.then(res => {
|
||||
console.log('项圈预警统计加载成功', res)
|
||||
this.setData({
|
||||
stats: {
|
||||
total: res.data.totalAlerts || 0,
|
||||
unread: res.data.unreadAlerts || 0,
|
||||
high: res.data.highLevel || 0,
|
||||
medium: res.data.mediumLevel || 0,
|
||||
low: res.data.lowLevel || 0
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('项圈预警统计加载失败:', error)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警列表
|
||||
*/
|
||||
loadAlertList: function(reset = true) {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
limit: this.data.limit,
|
||||
search: this.data.searchKeyword,
|
||||
alertType: this.data.filterType,
|
||||
alertLevel: this.data.filterLevel
|
||||
}
|
||||
|
||||
return alertService.getCollarAlerts(params)
|
||||
.then(res => {
|
||||
console.log('项圈预警列表加载成功', res)
|
||||
|
||||
const newList = res.data || []
|
||||
const alertList = reset ? newList : [...this.data.alertList, ...newList]
|
||||
|
||||
this.setData({
|
||||
alertList: alertList,
|
||||
total: res.total || 0,
|
||||
hasMore: newList.length >= this.data.limit
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('项圈预警列表加载失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
refreshData: function() {
|
||||
this.setData({
|
||||
refreshing: true
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载更多数据
|
||||
*/
|
||||
loadMoreData: function() {
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
this.loadAlertData(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索输入
|
||||
*/
|
||||
onSearchInput: function(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
onSearch: function() {
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 类型筛选
|
||||
*/
|
||||
onTypeFilter: function(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
filterType: type
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 级别筛选
|
||||
*/
|
||||
onLevelFilter: function(e) {
|
||||
const level = e.currentTarget.dataset.level
|
||||
this.setData({
|
||||
filterLevel: level
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看预警详情
|
||||
*/
|
||||
viewAlertDetail: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/alert/collar-detail/detail?id=${alertId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理预警
|
||||
*/
|
||||
handleAlert: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
const alertType = e.currentTarget.dataset.type
|
||||
|
||||
wx.showModal({
|
||||
title: '处理预警',
|
||||
content: '确定要处理此预警吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.confirmHandleAlert(alertId, alertType)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认处理预警
|
||||
*/
|
||||
confirmHandleAlert: function(alertId, alertType) {
|
||||
wx.showLoading({
|
||||
title: '处理中...'
|
||||
})
|
||||
|
||||
const handleData = {
|
||||
action: 'acknowledged',
|
||||
notes: '通过小程序处理',
|
||||
handler: app.getUserInfo()?.nickName || '用户'
|
||||
}
|
||||
|
||||
alertService.handleAlert(alertId, handleData)
|
||||
.then(res => {
|
||||
console.log('处理预警成功', res)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadAlertData(true)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('处理预警失败:', error)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: error.message || '处理失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警类型文本
|
||||
*/
|
||||
getAlertTypeText: function(type) {
|
||||
const typeMap = {
|
||||
'battery': '低电量',
|
||||
'offline': '离线',
|
||||
'temperature': '温度异常',
|
||||
'movement': '运动异常',
|
||||
'wear': '脱落'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别文本
|
||||
*/
|
||||
getAlertLevelText: function(level) {
|
||||
const levelMap = {
|
||||
'high': '高',
|
||||
'medium': '中',
|
||||
'low': '低'
|
||||
}
|
||||
return levelMap[level] || level
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别颜色
|
||||
*/
|
||||
getAlertLevelColor: function(level) {
|
||||
const colorMap = {
|
||||
'high': 'red',
|
||||
'medium': 'orange',
|
||||
'low': 'green'
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警类型图标
|
||||
*/
|
||||
getAlertTypeIcon: function(type) {
|
||||
const iconMap = {
|
||||
'battery': '🔋',
|
||||
'offline': '📵',
|
||||
'temperature': '🌡️',
|
||||
'movement': '🏃',
|
||||
'wear': '⚠️'
|
||||
}
|
||||
return iconMap[type] || '⚠️'
|
||||
}
|
||||
})
|
||||
213
mini_program/farm-monitor-wechat/pages/alert/collar/collar.wxml
Normal file
213
mini_program/farm-monitor-wechat/pages/alert/collar/collar.wxml
Normal file
@@ -0,0 +1,213 @@
|
||||
<!--智能项圈预警页面-->
|
||||
<view class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">⚠️</view>
|
||||
<view class="stats-number">{{stats.total}}</view>
|
||||
<view class="stats-label">总预警</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🔴</view>
|
||||
<view class="stats-number">{{stats.high}}</view>
|
||||
<view class="stats-label">高级</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🟡</view>
|
||||
<view class="stats-number">{{stats.medium}}</view>
|
||||
<view class="stats-label">中级</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🟢</view>
|
||||
<view class="stats-number">{{stats.low}}</view>
|
||||
<view class="stats-label">低级</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="搜索项圈编号、预警类型..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<view class="search-icon" bindtap="onSearch">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="filter-bar">
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警类型</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterType === '' ? 'active' : ''}}"
|
||||
data-type=""
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'battery' ? 'active' : ''}}"
|
||||
data-type="battery"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
低电量
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'offline' ? 'active' : ''}}"
|
||||
data-type="offline"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
离线
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'temperature' ? 'active' : ''}}"
|
||||
data-type="temperature"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
温度异常
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'movement' ? 'active' : ''}}"
|
||||
data-type="movement"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
运动异常
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'wear' ? 'active' : ''}}"
|
||||
data-type="wear"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
脱落
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警级别</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterLevel === '' ? 'active' : ''}}"
|
||||
data-level=""
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'high' ? 'active' : ''}}"
|
||||
data-level="high"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
高级
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'medium' ? 'active' : ''}}"
|
||||
data-level="medium"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
中级
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'low' ? 'active' : ''}}"
|
||||
data-level="low"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
低级
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-info">
|
||||
<text class="stats-text">共 {{total}} 条预警</text>
|
||||
<text class="stats-text">{{alertList.length}} 条记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && alertList.length === 0}}" class="loading">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 预警列表 -->
|
||||
<view wx:elif="{{alertList.length > 0}}" class="alert-list">
|
||||
<view
|
||||
wx:for="{{alertList}}"
|
||||
wx:key="id"
|
||||
class="alert-item"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="viewAlertDetail"
|
||||
>
|
||||
<view class="alert-header">
|
||||
<view class="alert-icon {{getAlertLevelColor(item.alertLevel)}}">
|
||||
<text>{{getAlertTypeIcon(item.alertType)}}</text>
|
||||
</view>
|
||||
<view class="alert-info">
|
||||
<view class="alert-title">{{item.description}}</view>
|
||||
<view class="alert-device">{{item.collarNumber || item.deviceName}}</view>
|
||||
</view>
|
||||
<view class="alert-level {{getAlertLevelColor(item.alertLevel)}}">
|
||||
<text>{{getAlertLevelText(item.alertLevel)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">类型:</text>
|
||||
<text class="detail-value">{{getAlertTypeText(item.alertType)}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">时间:</text>
|
||||
<text class="detail-value">{{item.alertTime}}</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.battery !== undefined}}">
|
||||
<text class="detail-label">电量:</text>
|
||||
<text class="detail-value">{{item.battery}}%</text>
|
||||
</view>
|
||||
<view class="detail-item" wx:if="{{item.temperature !== undefined}}">
|
||||
<text class="detail-label">温度:</text>
|
||||
<text class="detail-value">{{item.temperature}}°C</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-actions">
|
||||
<button
|
||||
class="action-btn handle"
|
||||
data-id="{{item.id}}"
|
||||
data-type="{{item.alertType}}"
|
||||
catchtap="handleAlert"
|
||||
>
|
||||
处理预警
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">📭</view>
|
||||
<view class="empty-text">暂无预警信息</view>
|
||||
<view class="empty-desc">系统运行正常,无预警数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading && alertList.length > 0}}" class="load-more">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!hasMore && alertList.length > 0}}" class="load-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
441
mini_program/farm-monitor-wechat/pages/alert/collar/collar.wxss
Normal file
441
mini_program/farm-monitor-wechat/pages/alert/collar/collar.wxss
Normal file
@@ -0,0 +1,441 @@
|
||||
/* 智能项圈预警页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-section {
|
||||
padding: 30rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 35rpx;
|
||||
padding: 0 50rpx 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input .input:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 筛选条件 */
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
margin: 0 20rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.filter-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-info {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
margin: 0 20rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e0e0e0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 预警列表 */
|
||||
.alert-list {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.alert-icon.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.alert-icon.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.alert-icon.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.alert-icon.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-level.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.alert-level.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.alert-level.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.alert-level.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-bottom: 25rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 15rpx 30rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn.handle {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 40rpx;
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.load-more .loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.stats-section {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 25rpx 15rpx;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.search-bar,
|
||||
.filter-bar,
|
||||
.stats-info {
|
||||
margin: 0 15rpx 15rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.detail-label,
|
||||
.detail-value {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 12rpx 25rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
359
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.js
Normal file
359
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.js
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 智能耳标预警页面
|
||||
* @file eartag.js
|
||||
* @description 智能耳标预警管理页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
alertList: [],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
searchKeyword: '',
|
||||
filterType: '',
|
||||
filterLevel: '',
|
||||
total: 0,
|
||||
stats: {
|
||||
total: 0,
|
||||
battery: 0,
|
||||
offline: 0,
|
||||
temperature: 0,
|
||||
movement: 0
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('智能耳标预警页面加载', options)
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow: function () {
|
||||
console.log('智能耳标预警页面显示')
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh: function () {
|
||||
console.log('下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom: function () {
|
||||
console.log('上拉触底')
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预警列表
|
||||
*/
|
||||
loadAlertList: function(reset = true) {
|
||||
if (reset) {
|
||||
this.setData({
|
||||
loading: true,
|
||||
page: 1,
|
||||
hasMore: true
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockData = [
|
||||
{
|
||||
id: '1',
|
||||
deviceId: 'ET001',
|
||||
deviceName: '耳标-001',
|
||||
cattleId: 'C001',
|
||||
cattleName: '牛只-001',
|
||||
alertType: 'battery',
|
||||
alertLevel: 'high',
|
||||
message: '电量低于20%,需要充电',
|
||||
battery: 15,
|
||||
temperature: 38.5,
|
||||
steps: 1250,
|
||||
ySteps: 1200,
|
||||
createTime: '2024-01-15 14:30:00',
|
||||
status: 'unhandled',
|
||||
location: 'A区-1号栏'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
deviceId: 'ET002',
|
||||
deviceName: '耳标-002',
|
||||
cattleId: 'C002',
|
||||
cattleName: '牛只-002',
|
||||
alertType: 'offline',
|
||||
alertLevel: 'high',
|
||||
message: '设备离线超过2小时',
|
||||
battery: 0,
|
||||
temperature: 0,
|
||||
steps: 0,
|
||||
ySteps: 0,
|
||||
createTime: '2024-01-15 12:15:00',
|
||||
status: 'handled',
|
||||
location: 'A区-2号栏'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
deviceId: 'ET003',
|
||||
deviceName: '耳标-003',
|
||||
cattleId: 'C003',
|
||||
cattleName: '牛只-003',
|
||||
alertType: 'temperature',
|
||||
alertLevel: 'medium',
|
||||
message: '温度异常,超过40°C',
|
||||
battery: 85,
|
||||
temperature: 42.1,
|
||||
steps: 2100,
|
||||
ySteps: 2100,
|
||||
createTime: '2024-01-15 16:45:00',
|
||||
status: 'unhandled',
|
||||
location: 'B区-1号栏'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
deviceId: 'ET004',
|
||||
deviceName: '耳标-004',
|
||||
cattleId: 'C004',
|
||||
cattleName: '牛只-004',
|
||||
alertType: 'movement',
|
||||
alertLevel: 'low',
|
||||
message: '运动异常,步数为0',
|
||||
battery: 72,
|
||||
temperature: 37.8,
|
||||
steps: 0,
|
||||
ySteps: 0,
|
||||
createTime: '2024-01-15 18:20:00',
|
||||
status: 'unhandled',
|
||||
location: 'B区-2号栏'
|
||||
}
|
||||
]
|
||||
|
||||
const mockStats = {
|
||||
total: 12,
|
||||
battery: 3,
|
||||
offline: 2,
|
||||
temperature: 4,
|
||||
movement: 3
|
||||
}
|
||||
|
||||
const newList = reset ? mockData : [...this.data.alertList, ...mockData]
|
||||
|
||||
this.setData({
|
||||
alertList: newList,
|
||||
stats: mockStats,
|
||||
total: mockData.length,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
refreshing: false
|
||||
})
|
||||
|
||||
if (reset) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
refreshData: function() {
|
||||
this.setData({
|
||||
refreshing: true
|
||||
})
|
||||
this.loadAlertList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载更多数据
|
||||
*/
|
||||
loadMoreData: function() {
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
this.loadAlertList(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索输入
|
||||
*/
|
||||
onSearchInput: function(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
onSearch: function() {
|
||||
this.loadAlertList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 预警类型筛选
|
||||
*/
|
||||
onTypeFilter: function(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
filterType: type
|
||||
})
|
||||
this.loadAlertList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 预警级别筛选
|
||||
*/
|
||||
onLevelFilter: function(e) {
|
||||
const level = e.currentTarget.dataset.level
|
||||
this.setData({
|
||||
filterLevel: level
|
||||
})
|
||||
this.loadAlertList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看预警详情
|
||||
*/
|
||||
viewAlertDetail: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/alert/detail/detail?id=${alertId}&type=eartag`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理预警
|
||||
*/
|
||||
handleAlert: function(e) {
|
||||
const alertId = e.currentTarget.dataset.id
|
||||
const alertMessage = e.currentTarget.dataset.message
|
||||
|
||||
wx.showModal({
|
||||
title: '处理预警',
|
||||
content: `确定要处理预警"${alertMessage}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.confirmHandleAlert(alertId)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认处理预警
|
||||
*/
|
||||
confirmHandleAlert: function(alertId) {
|
||||
wx.showLoading({
|
||||
title: '处理中...'
|
||||
})
|
||||
|
||||
// 模拟处理
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: '处理成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadAlertList(true)
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警类型文本
|
||||
*/
|
||||
getAlertTypeText: function(type) {
|
||||
const typeMap = {
|
||||
'battery': '低电量',
|
||||
'offline': '离线',
|
||||
'temperature': '温度异常',
|
||||
'movement': '运动异常'
|
||||
}
|
||||
return typeMap[type] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别文本
|
||||
*/
|
||||
getAlertLevelText: function(level) {
|
||||
const levelMap = {
|
||||
'high': '高',
|
||||
'medium': '中',
|
||||
'low': '低'
|
||||
}
|
||||
return levelMap[level] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警级别颜色
|
||||
*/
|
||||
getAlertLevelColor: function(level) {
|
||||
const colorMap = {
|
||||
'high': 'red',
|
||||
'medium': 'orange',
|
||||
'low': 'yellow'
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
getStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'unhandled': '未处理',
|
||||
'handled': '已处理',
|
||||
'ignored': '已忽略'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态颜色
|
||||
*/
|
||||
getStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'unhandled': 'red',
|
||||
'handled': 'green',
|
||||
'ignored': 'gray'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取预警图标
|
||||
*/
|
||||
getAlertIcon: function(type) {
|
||||
const iconMap = {
|
||||
'battery': '🔋',
|
||||
'offline': '📶',
|
||||
'temperature': '🌡️',
|
||||
'movement': '🏃'
|
||||
}
|
||||
return iconMap[type] || '⚠️'
|
||||
}
|
||||
})
|
||||
221
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.wxml
Normal file
221
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.wxml
Normal file
@@ -0,0 +1,221 @@
|
||||
<!--智能耳标预警页面-->
|
||||
<view class="container">
|
||||
<!-- 统计概览 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-title">智能耳标预警概览</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">⚠️</view>
|
||||
<view class="stats-number">{{stats.total}}</view>
|
||||
<view class="stats-label">总预警</view>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🔋</view>
|
||||
<view class="stats-number">{{stats.battery}}</view>
|
||||
<view class="stats-label">低电量</view>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">📶</view>
|
||||
<view class="stats-number">{{stats.offline}}</view>
|
||||
<view class="stats-label">离线</view>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🌡️</view>
|
||||
<view class="stats-number">{{stats.temperature}}</view>
|
||||
<view class="stats-label">温度异常</view>
|
||||
</view>
|
||||
<view class="stats-card">
|
||||
<view class="stats-icon">🏃</view>
|
||||
<view class="stats-number">{{stats.movement}}</view>
|
||||
<view class="stats-label">运动异常</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="搜索设备ID或牛只名称..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<view class="search-icon" bindtap="onSearch">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="filter-bar">
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警类型</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterType === '' ? 'active' : ''}}"
|
||||
data-type=""
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'battery' ? 'active' : ''}}"
|
||||
data-type="battery"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
低电量
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'offline' ? 'active' : ''}}"
|
||||
data-type="offline"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
离线
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'temperature' ? 'active' : ''}}"
|
||||
data-type="temperature"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
温度异常
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterType === 'movement' ? 'active' : ''}}"
|
||||
data-type="movement"
|
||||
bindtap="onTypeFilter"
|
||||
>
|
||||
运动异常
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<view class="filter-title">预警级别</view>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterLevel === '' ? 'active' : ''}}"
|
||||
data-level=""
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'high' ? 'active' : ''}}"
|
||||
data-level="high"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
高
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'medium' ? 'active' : ''}}"
|
||||
data-level="medium"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
中
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterLevel === 'low' ? 'active' : ''}}"
|
||||
data-level="low"
|
||||
bindtap="onLevelFilter"
|
||||
>
|
||||
低
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-info">
|
||||
<text class="stats-text">共 {{total}} 条预警</text>
|
||||
<text class="stats-text">{{alertList.length}} 条记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && alertList.length === 0}}" class="loading">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 预警列表 -->
|
||||
<view wx:elif="{{alertList.length > 0}}" class="alert-list">
|
||||
<view
|
||||
wx:for="{{alertList}}"
|
||||
wx:key="id"
|
||||
class="alert-item"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="viewAlertDetail"
|
||||
>
|
||||
<view class="alert-header">
|
||||
<view class="alert-icon">{{getAlertIcon(item.alertType)}}</view>
|
||||
<view class="alert-info">
|
||||
<view class="alert-title">{{item.message}}</view>
|
||||
<view class="alert-device">{{item.deviceName}} - {{item.cattleName}}</view>
|
||||
</view>
|
||||
<view class="alert-level {{getAlertLevelColor(item.alertLevel)}}">
|
||||
<text>{{getAlertLevelText(item.alertLevel)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-details">
|
||||
<view class="detail-row">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">预警类型:</text>
|
||||
<text class="detail-value">{{getAlertTypeText(item.alertType)}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">处理状态:</text>
|
||||
<text class="detail-value {{getStatusColor(item.status)}}">{{getStatusText(item.status)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">设备电量:</text>
|
||||
<text class="detail-value">{{item.battery}}%</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">设备温度:</text>
|
||||
<text class="detail-value">{{item.temperature}}°C</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">当前位置:</text>
|
||||
<text class="detail-value">{{item.location}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">预警时间:</text>
|
||||
<text class="detail-value">{{item.createTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="alert-actions">
|
||||
<button
|
||||
class="action-btn handle"
|
||||
data-id="{{item.id}}"
|
||||
data-message="{{item.message}}"
|
||||
catchtap="handleAlert"
|
||||
>
|
||||
处理预警
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">✅</view>
|
||||
<view class="empty-text">暂无预警数据</view>
|
||||
<view class="empty-desc">当前没有智能耳标预警信息</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading && alertList.length > 0}}" class="load-more">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!hasMore && alertList.length > 0}}" class="load-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
456
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.wxss
Normal file
456
mini_program/farm-monitor-wechat/pages/alert/eartag/eartag.wxss
Normal file
@@ -0,0 +1,456 @@
|
||||
/* 智能耳标预警页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 统计概览 */
|
||||
.stats-section {
|
||||
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||
padding: 40rpx 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
flex: 1;
|
||||
min-width: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 25rpx 15rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 40rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 35rpx;
|
||||
padding: 0 50rpx 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input .input:focus {
|
||||
border-color: #ff4d4f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 筛选条件 */
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 30rpx;
|
||||
margin: 0 20rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.filter-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-info {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e0e0e0;
|
||||
border-top: 4rpx solid #ff4d4f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 预警列表 */
|
||||
.alert-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
font-size: 28rpx;
|
||||
background: #fff2f0;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-level.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.alert-level.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.alert-level.yellow {
|
||||
background: #fffbe6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.alert-level.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-bottom: 25rpx;
|
||||
padding: 20rpx;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-value.red {
|
||||
color: #ff4757;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value.green {
|
||||
color: #3cc51f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value.gray {
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 15rpx 30rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn.handle {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 40rpx;
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.load-more .loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.stats-section {
|
||||
padding: 30rpx 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 20rpx 12rpx;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 15rpx 20rpx;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin: 0 15rpx 15rpx;
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.alert-device {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.detail-label,
|
||||
.detail-value {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.alert-actions .action-btn {
|
||||
padding: 12rpx 25rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
370
mini_program/farm-monitor-wechat/pages/cattle/add/add.js
Normal file
370
mini_program/farm-monitor-wechat/pages/cattle/add/add.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 牛只添加页面
|
||||
* @file add.js
|
||||
* @description 添加新牛只页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
formData: {
|
||||
cattleId: '',
|
||||
name: '',
|
||||
breed: '',
|
||||
gender: '',
|
||||
birthDate: '',
|
||||
weight: '',
|
||||
healthStatus: 'healthy',
|
||||
location: '',
|
||||
description: '',
|
||||
parentId: '',
|
||||
parentName: ''
|
||||
},
|
||||
// 计算后的索引值
|
||||
breedIndex: 0,
|
||||
genderIndex: 0,
|
||||
healthStatusIndex: 0,
|
||||
locationIndex: 0,
|
||||
parentIndex: 0,
|
||||
breedOptions: [
|
||||
'荷斯坦牛',
|
||||
'西门塔尔牛',
|
||||
'夏洛莱牛',
|
||||
'利木赞牛',
|
||||
'安格斯牛',
|
||||
'其他'
|
||||
],
|
||||
genderOptions: [
|
||||
{ value: 'male', label: '公牛' },
|
||||
{ value: 'female', label: '母牛' },
|
||||
{ value: 'calf', label: '犊牛' }
|
||||
],
|
||||
healthStatusOptions: [
|
||||
{ value: 'healthy', label: '健康' },
|
||||
{ value: 'sick', label: '生病' },
|
||||
{ value: 'pregnant', label: '怀孕' },
|
||||
{ value: 'lactating', label: '泌乳期' }
|
||||
],
|
||||
locationOptions: [
|
||||
'A区-1号栏',
|
||||
'A区-2号栏',
|
||||
'A区-3号栏',
|
||||
'B区-1号栏',
|
||||
'B区-2号栏',
|
||||
'B区-3号栏',
|
||||
'C区-1号栏',
|
||||
'C区-2号栏',
|
||||
'C区-3号栏'
|
||||
],
|
||||
submitting: false,
|
||||
showParentPicker: false,
|
||||
parentList: []
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('牛只添加页面加载', options)
|
||||
this.loadParentList()
|
||||
this.generateCattleId()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成牛只ID
|
||||
*/
|
||||
generateCattleId: function() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const random = String(Math.floor(Math.random() * 1000)).padStart(3, '0')
|
||||
const cattleId = `C${year}${month}${day}${random}`
|
||||
|
||||
this.setData({
|
||||
'formData.cattleId': cattleId
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算索引值
|
||||
*/
|
||||
calculateIndexes: function() {
|
||||
const { formData, breedOptions, genderOptions, healthStatusOptions, locationOptions, parentList } = this.data
|
||||
|
||||
const breedIndex = breedOptions.indexOf(formData.breed)
|
||||
const genderIndex = genderOptions.findIndex(item => item.value === formData.gender)
|
||||
const healthStatusIndex = healthStatusOptions.findIndex(item => item.value === formData.healthStatus)
|
||||
const locationIndex = locationOptions.indexOf(formData.location)
|
||||
const parentIndex = parentList.findIndex(item => item.id === formData.parentId)
|
||||
|
||||
this.setData({
|
||||
breedIndex: breedIndex >= 0 ? breedIndex : 0,
|
||||
genderIndex: genderIndex >= 0 ? genderIndex : 0,
|
||||
healthStatusIndex: healthStatusIndex >= 0 ? healthStatusIndex : 0,
|
||||
locationIndex: locationIndex >= 0 ? locationIndex : 0,
|
||||
parentIndex: parentIndex >= 0 ? parentIndex : 0
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载父代牛只列表
|
||||
*/
|
||||
loadParentList: function() {
|
||||
// 模拟数据
|
||||
const mockParents = [
|
||||
{ id: 'C001', name: '牛只-001', breed: '荷斯坦牛', gender: 'female' },
|
||||
{ id: 'C002', name: '牛只-002', breed: '西门塔尔牛', gender: 'male' },
|
||||
{ id: 'C003', name: '牛只-003', breed: '夏洛莱牛', gender: 'female' }
|
||||
]
|
||||
|
||||
this.setData({
|
||||
parentList: mockParents
|
||||
}, () => {
|
||||
// 数据加载完成后计算索引
|
||||
this.calculateIndexes()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 输入框变化
|
||||
*/
|
||||
onInputChange: function(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
const value = e.detail.value
|
||||
|
||||
this.setData({
|
||||
[`formData.${field}`]: value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择品种
|
||||
*/
|
||||
onBreedChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
'formData.breed': this.data.breedOptions[index],
|
||||
breedIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择性别
|
||||
*/
|
||||
onGenderChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
'formData.gender': this.data.genderOptions[index].value,
|
||||
genderIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择健康状态
|
||||
*/
|
||||
onHealthStatusChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
'formData.healthStatus': this.data.healthStatusOptions[index].value,
|
||||
healthStatusIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择位置
|
||||
*/
|
||||
onLocationChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
'formData.location': this.data.locationOptions[index],
|
||||
locationIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择出生日期
|
||||
*/
|
||||
onBirthDateChange: function(e) {
|
||||
this.setData({
|
||||
'formData.birthDate': e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择父代
|
||||
*/
|
||||
onParentChange: function(e) {
|
||||
const index = e.detail.value
|
||||
const parent = this.data.parentList[index]
|
||||
|
||||
this.setData({
|
||||
'formData.parentId': parent.id,
|
||||
'formData.parentName': parent.name,
|
||||
parentIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示父代选择器
|
||||
*/
|
||||
showParentPicker: function() {
|
||||
this.setData({
|
||||
showParentPicker: true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 隐藏父代选择器
|
||||
*/
|
||||
hideParentPicker: function() {
|
||||
this.setData({
|
||||
showParentPicker: false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 表单验证
|
||||
*/
|
||||
validateForm: function() {
|
||||
const { formData } = this.data
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入牛只名称',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.breed) {
|
||||
wx.showToast({
|
||||
title: '请选择品种',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.gender) {
|
||||
wx.showToast({
|
||||
title: '请选择性别',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.birthDate) {
|
||||
wx.showToast({
|
||||
title: '请选择出生日期',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.weight || isNaN(formData.weight) || formData.weight <= 0) {
|
||||
wx.showToast({
|
||||
title: '请输入正确的体重',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.location) {
|
||||
wx.showToast({
|
||||
title: '请选择位置',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
onSubmit: function() {
|
||||
if (!this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
submitting: true
|
||||
})
|
||||
|
||||
// 模拟提交
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
submitting: false
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
onReset: function() {
|
||||
wx.showModal({
|
||||
title: '确认重置',
|
||||
content: '确定要重置表单吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({
|
||||
formData: {
|
||||
cattleId: '',
|
||||
name: '',
|
||||
breed: '',
|
||||
gender: '',
|
||||
birthDate: '',
|
||||
weight: '',
|
||||
healthStatus: 'healthy',
|
||||
location: '',
|
||||
description: '',
|
||||
parentId: '',
|
||||
parentName: ''
|
||||
}
|
||||
})
|
||||
this.generateCattleId()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取性别文本
|
||||
*/
|
||||
getGenderText: function(gender) {
|
||||
const genderMap = {
|
||||
'male': '公牛',
|
||||
'female': '母牛',
|
||||
'calf': '犊牛'
|
||||
}
|
||||
return genderMap[gender] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态文本
|
||||
*/
|
||||
getHealthStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'healthy': '健康',
|
||||
'sick': '生病',
|
||||
'pregnant': '怀孕',
|
||||
'lactating': '泌乳期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
})
|
||||
164
mini_program/farm-monitor-wechat/pages/cattle/add/add.wxml
Normal file
164
mini_program/farm-monitor-wechat/pages/cattle/add/add.wxml
Normal file
@@ -0,0 +1,164 @@
|
||||
<!--牛只添加页面-->
|
||||
<view class="container">
|
||||
<view class="form-container">
|
||||
<view class="form-header">
|
||||
<view class="form-title">添加新牛只</view>
|
||||
<view class="form-subtitle">请填写牛只基本信息</view>
|
||||
</view>
|
||||
|
||||
<form bindsubmit="onSubmit">
|
||||
<!-- 基本信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">牛只ID</view>
|
||||
<view class="form-value">{{formData.cattleId}}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">牛只名称</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入牛只名称"
|
||||
value="{{formData.name}}"
|
||||
data-field="name"
|
||||
bindinput="onInputChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">品种</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{breedOptions}}"
|
||||
value="{{breedIndex}}"
|
||||
bindchange="onBreedChange"
|
||||
>
|
||||
<view class="picker-text">{{formData.breed || '请选择品种'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">性别</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{genderOptions}}"
|
||||
range-key="label"
|
||||
value="{{genderIndex}}"
|
||||
bindchange="onGenderChange"
|
||||
>
|
||||
<view class="picker-text">{{getGenderText(formData.gender) || '请选择性别'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">出生日期</view>
|
||||
<picker
|
||||
mode="date"
|
||||
value="{{formData.birthDate}}"
|
||||
bindchange="onBirthDateChange"
|
||||
>
|
||||
<view class="picker-text">{{formData.birthDate || '请选择出生日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">体重(kg)</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入体重"
|
||||
type="digit"
|
||||
value="{{formData.weight}}"
|
||||
data-field="weight"
|
||||
bindinput="onInputChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康状态 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">健康状态</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">健康状态</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{healthStatusOptions}}"
|
||||
range-key="label"
|
||||
value="{{healthStatusIndex}}"
|
||||
bindchange="onHealthStatusChange"
|
||||
>
|
||||
<view class="picker-text">{{getHealthStatusText(formData.healthStatus)}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">位置</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{locationOptions}}"
|
||||
value="{{locationIndex}}"
|
||||
bindchange="onLocationChange"
|
||||
>
|
||||
<view class="picker-text">{{formData.location || '请选择位置'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 父代信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">父代信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">父代牛只</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{parentList}}"
|
||||
range-key="name"
|
||||
value="{{parentIndex}}"
|
||||
bindchange="onParentChange"
|
||||
>
|
||||
<view class="picker-text">{{formData.parentName || '请选择父代牛只'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">备注描述</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
placeholder="请输入备注描述"
|
||||
value="{{formData.description}}"
|
||||
data-field="description"
|
||||
bindinput="onInputChange"
|
||||
maxlength="200"
|
||||
/>
|
||||
<view class="textarea-count">{{formData.description.length}}/200</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="form-actions">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
bindtap="onReset"
|
||||
disabled="{{submitting}}"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
formType="submit"
|
||||
loading="{{submitting}}"
|
||||
>
|
||||
{{submitting ? '提交中...' : '提交'}}
|
||||
</button>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
225
mini_program/farm-monitor-wechat/pages/cattle/add/add.wxss
Normal file
225
mini_program/farm-monitor-wechat/pages/cattle/add/add.wxss
Normal file
@@ -0,0 +1,225 @@
|
||||
/* 牛只添加页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
padding-bottom: 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 25rpx;
|
||||
padding-left: 15rpx;
|
||||
border-left: 6rpx solid #3cc51f;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-text:active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.textarea-count {
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #f0f0f0;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
padding-top: 30rpx;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
background: #2db815;
|
||||
}
|
||||
|
||||
.btn-primary[disabled] {
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.btn-secondary[disabled] {
|
||||
background: #f0f0f0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.container {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.picker-text,
|
||||
.form-value {
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 70rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
334
mini_program/farm-monitor-wechat/pages/cattle/cattle.js
Normal file
334
mini_program/farm-monitor-wechat/pages/cattle/cattle.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 牛只管理页面
|
||||
* @file cattle.js
|
||||
* @description 牛只管理列表页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const cattleService = require('../../services/cattleService.js')
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
cattleList: [],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
searchKeyword: '',
|
||||
filterStatus: '',
|
||||
total: 0,
|
||||
showSearch: false,
|
||||
searchInput: ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('牛只管理页面加载', options)
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow: function () {
|
||||
console.log('牛只管理页面显示')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh: function () {
|
||||
console.log('下拉刷新')
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom: function () {
|
||||
console.log('上拉触底')
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage: function () {
|
||||
return {
|
||||
title: '牛只管理',
|
||||
path: '/pages/cattle/cattle'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载牛只列表
|
||||
*/
|
||||
loadCattleList: function(reset = true) {
|
||||
if (reset) {
|
||||
this.setData({
|
||||
loading: true,
|
||||
page: 1,
|
||||
hasMore: true
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
limit: this.data.limit,
|
||||
search: this.data.searchKeyword,
|
||||
status: this.data.filterStatus
|
||||
}
|
||||
|
||||
cattleService.getCattleList(params)
|
||||
.then(res => {
|
||||
console.log('牛只列表加载成功', res)
|
||||
|
||||
const newList = res.data || []
|
||||
const cattleList = reset ? newList : [...this.data.cattleList, ...newList]
|
||||
|
||||
this.setData({
|
||||
cattleList: cattleList,
|
||||
total: res.total || 0,
|
||||
hasMore: newList.length >= this.data.limit,
|
||||
loading: false,
|
||||
refreshing: false
|
||||
})
|
||||
|
||||
if (reset) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('牛只列表加载失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
this.setData({
|
||||
loading: false,
|
||||
refreshing: false
|
||||
})
|
||||
if (reset) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
refreshData: function() {
|
||||
this.setData({
|
||||
refreshing: true
|
||||
})
|
||||
this.loadCattleList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载更多数据
|
||||
*/
|
||||
loadMoreData: function() {
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
this.loadCattleList(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换搜索显示
|
||||
*/
|
||||
toggleSearch: function() {
|
||||
this.setData({
|
||||
showSearch: !this.data.showSearch,
|
||||
searchInput: this.data.searchKeyword
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索输入
|
||||
*/
|
||||
onSearchInput: function(e) {
|
||||
this.setData({
|
||||
searchInput: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
doSearch: function() {
|
||||
this.setData({
|
||||
searchKeyword: this.data.searchInput,
|
||||
showSearch: false
|
||||
})
|
||||
this.loadCattleList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消搜索
|
||||
*/
|
||||
cancelSearch: function() {
|
||||
this.setData({
|
||||
showSearch: false,
|
||||
searchInput: this.data.searchKeyword
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空搜索
|
||||
*/
|
||||
clearSearch: function() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
searchInput: ''
|
||||
})
|
||||
this.loadCattleList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 状态筛选
|
||||
*/
|
||||
onStatusFilter: function(e) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
this.setData({
|
||||
filterStatus: status
|
||||
})
|
||||
this.loadCattleList(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看牛只详情
|
||||
*/
|
||||
viewCattleDetail: function(e) {
|
||||
const cattleId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/detail/detail?id=${cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加牛只
|
||||
*/
|
||||
addCattle: function() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/add/add'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 编辑牛只
|
||||
*/
|
||||
editCattle: function(e) {
|
||||
const cattleId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/edit/edit?id=${cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除牛只
|
||||
*/
|
||||
deleteCattle: function(e) {
|
||||
const cattleId = e.currentTarget.dataset.id
|
||||
const cattleName = e.currentTarget.dataset.name
|
||||
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除牛只"${cattleName}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.confirmDeleteCattle(cattleId)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认删除牛只
|
||||
*/
|
||||
confirmDeleteCattle: function(cattleId) {
|
||||
wx.showLoading({
|
||||
title: '删除中...'
|
||||
})
|
||||
|
||||
cattleService.deleteCattle(cattleId)
|
||||
.then(res => {
|
||||
console.log('删除牛只成功', res)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
this.loadCattleList(true)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('删除牛只失败:', error)
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: error.message || '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 牛只转移
|
||||
*/
|
||||
transferCattle: function(e) {
|
||||
const cattleId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/transfer/transfer?id=${cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 牛只出栏
|
||||
*/
|
||||
exitCattle: function(e) {
|
||||
const cattleId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/exit/exit?id=${cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
getStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'active': '在栏',
|
||||
'transferred': '已转移',
|
||||
'exited': '已出栏',
|
||||
'sick': '生病',
|
||||
'pregnant': '怀孕'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态颜色
|
||||
*/
|
||||
getStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'active': 'green',
|
||||
'transferred': 'blue',
|
||||
'exited': 'gray',
|
||||
'sick': 'red',
|
||||
'pregnant': 'orange'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
})
|
||||
163
mini_program/farm-monitor-wechat/pages/cattle/cattle.wxml
Normal file
163
mini_program/farm-monitor-wechat/pages/cattle/cattle.wxml
Normal file
@@ -0,0 +1,163 @@
|
||||
<!--牛只管理页面-->
|
||||
<view class="container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="搜索牛只编号、耳标号..."
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="doSearch"
|
||||
/>
|
||||
<view class="search-icon" bindtap="doSearch">🔍</view>
|
||||
</view>
|
||||
<view class="search-actions">
|
||||
<view class="action-btn" bindtap="toggleSearch">筛选</view>
|
||||
<view class="action-btn" bindtap="addCattle">添加</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="filter-bar" wx:if="{{showSearch}}">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">状态:</text>
|
||||
<view class="filter-options">
|
||||
<view
|
||||
class="filter-option {{filterStatus === '' ? 'active' : ''}}"
|
||||
data-status=""
|
||||
bindtap="onStatusFilter"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterStatus === 'active' ? 'active' : ''}}"
|
||||
data-status="active"
|
||||
bindtap="onStatusFilter"
|
||||
>
|
||||
在栏
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterStatus === 'transferred' ? 'active' : ''}}"
|
||||
data-status="transferred"
|
||||
bindtap="onStatusFilter"
|
||||
>
|
||||
已转移
|
||||
</view>
|
||||
<view
|
||||
class="filter-option {{filterStatus === 'exited' ? 'active' : ''}}"
|
||||
data-status="exited"
|
||||
bindtap="onStatusFilter"
|
||||
>
|
||||
已出栏
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-actions">
|
||||
<button class="btn btn-secondary btn-small" bindtap="clearSearch">清空</button>
|
||||
<button class="btn btn-primary btn-small" bindtap="doSearch">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-bar">
|
||||
<text class="stats-text">共 {{total}} 头牛只</text>
|
||||
<text class="stats-text">{{cattleList.length}} 条记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && cattleList.length === 0}}" class="loading">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 牛只列表 -->
|
||||
<view wx:elif="{{cattleList.length > 0}}" class="cattle-list">
|
||||
<view
|
||||
wx:for="{{cattleList}}"
|
||||
wx:key="id"
|
||||
class="cattle-item"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="viewCattleDetail"
|
||||
>
|
||||
<view class="cattle-header">
|
||||
<view class="cattle-info">
|
||||
<view class="cattle-name">{{item.name || item.earTagNumber}}</view>
|
||||
<view class="cattle-id">编号: {{item.earTagNumber}}</view>
|
||||
</view>
|
||||
<view class="cattle-status {{getStatusColor(item.status)}}">
|
||||
<text>{{getStatusText(item.status)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cattle-details">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">品种:</text>
|
||||
<text class="detail-value">{{item.breed || '未知'}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">性别:</text>
|
||||
<text class="detail-value">{{item.gender === 'male' ? '公牛' : item.gender === 'female' ? '母牛' : '未知'}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">年龄:</text>
|
||||
<text class="detail-value">{{item.age || 0}}岁</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">圈舍:</text>
|
||||
<text class="detail-value">{{item.penName || '未分配'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cattle-actions">
|
||||
<button
|
||||
class="action-btn edit"
|
||||
data-id="{{item.id}}"
|
||||
catchtap="editCattle"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="action-btn transfer"
|
||||
data-id="{{item.id}}"
|
||||
catchtap="transferCattle"
|
||||
>
|
||||
转移
|
||||
</button>
|
||||
<button
|
||||
class="action-btn exit"
|
||||
data-id="{{item.id}}"
|
||||
catchtap="exitCattle"
|
||||
>
|
||||
出栏
|
||||
</button>
|
||||
<button
|
||||
class="action-btn delete"
|
||||
data-id="{{item.id}}"
|
||||
data-name="{{item.name || item.earTagNumber}}"
|
||||
catchtap="deleteCattle"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">🐄</view>
|
||||
<view class="empty-text">暂无牛只数据</view>
|
||||
<view class="empty-desc">点击右上角"添加"按钮添加牛只</view>
|
||||
<button class="btn btn-primary" bindtap="addCattle">添加牛只</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view wx:if="{{loading && cattleList.length > 0}}" class="load-more">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{!hasMore && cattleList.length > 0}}" class="load-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
398
mini_program/farm-monitor-wechat/pages/cattle/cattle.wxss
Normal file
398
mini_program/farm-monitor-wechat/pages/cattle/cattle.wxss
Normal file
@@ -0,0 +1,398 @@
|
||||
/* 牛只管理页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 35rpx;
|
||||
padding: 0 50rpx 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input .input:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 15rpx 25rpx;
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background: #2db815;
|
||||
}
|
||||
|
||||
/* 筛选条件 */
|
||||
.filter-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-right: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-option.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 15rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.stats-bar {
|
||||
background: #fff;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e0e0e0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 牛只列表 */
|
||||
.cattle-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cattle-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.cattle-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-id {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cattle-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cattle-status.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.cattle-status.blue {
|
||||
background: #e8f4fd;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.cattle-status.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.cattle-status.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.cattle-status.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.cattle-details {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cattle-actions {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn {
|
||||
flex: 1;
|
||||
min-width: 120rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn.edit {
|
||||
background: #e8f4fd;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn.transfer {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn.exit {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn.delete {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 40rpx;
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 30rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.load-more .loading-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.search-bar {
|
||||
padding: 15rpx 20rpx;
|
||||
}
|
||||
|
||||
.search-input .input {
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 15rpx 20rpx;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.cattle-item {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.cattle-id {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.detail-label,
|
||||
.detail-value {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.cattle-actions .action-btn {
|
||||
padding: 12rpx 15rpx;
|
||||
font-size: 20rpx;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
}
|
||||
315
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.js
Normal file
315
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* 牛只详情页面
|
||||
* @file detail.js
|
||||
* @description 牛只详细信息页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
cattleId: '',
|
||||
cattleInfo: {},
|
||||
deviceInfo: {},
|
||||
healthRecords: [],
|
||||
movementRecords: [],
|
||||
loading: true,
|
||||
activeTab: 'info'
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('牛只详情页面加载', options)
|
||||
if (options.id) {
|
||||
this.setData({
|
||||
cattleId: options.id
|
||||
})
|
||||
this.loadCattleDetail()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow: function () {
|
||||
console.log('牛只详情页面显示')
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载牛只详情
|
||||
*/
|
||||
loadCattleDetail: function() {
|
||||
this.setData({
|
||||
loading: true
|
||||
})
|
||||
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockCattleInfo = {
|
||||
id: this.data.cattleId,
|
||||
name: '牛只-001',
|
||||
breed: '荷斯坦牛',
|
||||
gender: 'female',
|
||||
birthDate: '2020-03-15',
|
||||
age: '3岁8个月',
|
||||
weight: 580,
|
||||
healthStatus: 'healthy',
|
||||
location: 'A区-1号栏',
|
||||
parentId: 'C000',
|
||||
parentName: '牛只-000',
|
||||
description: '这是一头健康的荷斯坦母牛,产奶量稳定。',
|
||||
createTime: '2020-03-15 10:30:00',
|
||||
updateTime: '2024-01-15 14:30:00'
|
||||
}
|
||||
|
||||
const mockDeviceInfo = {
|
||||
deviceId: 'ET001',
|
||||
deviceType: 'eartag',
|
||||
deviceName: '智能耳标-001',
|
||||
status: 'online',
|
||||
battery: 85,
|
||||
temperature: 38.5,
|
||||
lastUpdate: '2024-01-15 14:30:00'
|
||||
}
|
||||
|
||||
const mockHealthRecords = [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-01-15',
|
||||
type: 'vaccination',
|
||||
description: '接种疫苗',
|
||||
veterinarian: '张医生',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2024-01-10',
|
||||
type: 'checkup',
|
||||
description: '定期体检',
|
||||
veterinarian: '李医生',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
date: '2024-01-05',
|
||||
type: 'treatment',
|
||||
description: '治疗感冒',
|
||||
veterinarian: '王医生',
|
||||
status: 'completed'
|
||||
}
|
||||
]
|
||||
|
||||
const mockMovementRecords = [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-01-15',
|
||||
steps: 1250,
|
||||
distance: 2.5,
|
||||
location: 'A区-1号栏',
|
||||
duration: '8小时'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2024-01-14',
|
||||
steps: 1180,
|
||||
distance: 2.3,
|
||||
location: 'A区-1号栏',
|
||||
duration: '8小时'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
date: '2024-01-13',
|
||||
steps: 1320,
|
||||
distance: 2.7,
|
||||
location: 'A区-1号栏',
|
||||
duration: '8小时'
|
||||
}
|
||||
]
|
||||
|
||||
this.setData({
|
||||
cattleInfo: mockCattleInfo,
|
||||
deviceInfo: mockDeviceInfo,
|
||||
healthRecords: mockHealthRecords,
|
||||
movementRecords: mockMovementRecords,
|
||||
loading: false
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换标签页
|
||||
*/
|
||||
onTabChange: function(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
this.setData({
|
||||
activeTab: tab
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 编辑牛只信息
|
||||
*/
|
||||
editCattle: function() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/edit/edit?id=${this.data.cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 转移牛只
|
||||
*/
|
||||
transferCattle: function() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/transfer/transfer?id=${this.data.cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 退出牛只
|
||||
*/
|
||||
exitCattle: function() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/exit/exit?id=${this.data.cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看设备详情
|
||||
*/
|
||||
viewDeviceDetail: function() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/detail/detail?id=${this.data.deviceInfo.deviceId}&type=${this.data.deviceInfo.deviceType}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加健康记录
|
||||
*/
|
||||
addHealthRecord: function() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/health/add/add?cattleId=${this.data.cattleId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看健康记录详情
|
||||
*/
|
||||
viewHealthRecord: function(e) {
|
||||
const recordId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/health/detail/detail?id=${recordId}`
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取性别文本
|
||||
*/
|
||||
getGenderText: function(gender) {
|
||||
const genderMap = {
|
||||
'male': '公牛',
|
||||
'female': '母牛',
|
||||
'calf': '犊牛'
|
||||
}
|
||||
return genderMap[gender] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态文本
|
||||
*/
|
||||
getHealthStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'healthy': '健康',
|
||||
'sick': '生病',
|
||||
'pregnant': '怀孕',
|
||||
'lactating': '泌乳期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态颜色
|
||||
*/
|
||||
getHealthStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'healthy': 'green',
|
||||
'sick': 'red',
|
||||
'pregnant': 'orange',
|
||||
'lactating': 'blue'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取设备状态文本
|
||||
*/
|
||||
getDeviceStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'online': '在线',
|
||||
'offline': '离线',
|
||||
'maintenance': '维护中'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取设备状态颜色
|
||||
*/
|
||||
getDeviceStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'online': 'green',
|
||||
'offline': 'red',
|
||||
'maintenance': 'orange'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康记录类型文本
|
||||
*/
|
||||
getHealthRecordTypeText: function(type) {
|
||||
const typeMap = {
|
||||
'vaccination': '疫苗接种',
|
||||
'checkup': '体检',
|
||||
'treatment': '治疗',
|
||||
'surgery': '手术'
|
||||
}
|
||||
return typeMap[type] || '其他'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康记录状态文本
|
||||
*/
|
||||
getHealthRecordStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'completed': '已完成',
|
||||
'pending': '待处理',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康记录状态颜色
|
||||
*/
|
||||
getHealthRecordStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'completed': 'green',
|
||||
'pending': 'orange',
|
||||
'cancelled': 'red'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
})
|
||||
230
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.wxml
Normal file
230
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.wxml
Normal file
@@ -0,0 +1,230 @@
|
||||
<!--牛只详情页面-->
|
||||
<view class="container">
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading}}" class="loading">
|
||||
<view class="loading-icon"></view>
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 牛只详情 -->
|
||||
<view wx:else class="cattle-detail">
|
||||
<!-- 基本信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<view class="cattle-avatar">🐄</view>
|
||||
<view class="cattle-basic">
|
||||
<view class="cattle-name">{{cattleInfo.name}}</view>
|
||||
<view class="cattle-id">ID: {{cattleInfo.id}}</view>
|
||||
<view class="cattle-status {{getHealthStatusColor(cattleInfo.healthStatus)}}">
|
||||
{{getHealthStatusText(cattleInfo.healthStatus)}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-actions">
|
||||
<button class="action-btn edit" bindtap="editCattle">编辑</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<view class="info-grid">
|
||||
<view class="info-item">
|
||||
<text class="info-label">品种</text>
|
||||
<text class="info-value">{{cattleInfo.breed}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">性别</text>
|
||||
<text class="info-value">{{getGenderText(cattleInfo.gender)}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">年龄</text>
|
||||
<text class="info-value">{{cattleInfo.age}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">体重</text>
|
||||
<text class="info-value">{{cattleInfo.weight}}kg</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">位置</text>
|
||||
<text class="info-value">{{cattleInfo.location}}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">父代</text>
|
||||
<text class="info-value">{{cattleInfo.parentName}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<view class="card-title">设备信息</view>
|
||||
<button class="action-btn view" bindtap="viewDeviceDetail">查看详情</button>
|
||||
</view>
|
||||
|
||||
<view class="card-content">
|
||||
<view class="device-info">
|
||||
<view class="device-item">
|
||||
<text class="device-label">设备ID</text>
|
||||
<text class="device-value">{{deviceInfo.deviceId}}</text>
|
||||
</view>
|
||||
<view class="device-item">
|
||||
<text class="device-label">设备类型</text>
|
||||
<text class="device-value">{{deviceInfo.deviceName}}</text>
|
||||
</view>
|
||||
<view class="device-item">
|
||||
<text class="device-label">状态</text>
|
||||
<text class="device-value {{getDeviceStatusColor(deviceInfo.status)}}">{{getDeviceStatusText(deviceInfo.status)}}</text>
|
||||
</view>
|
||||
<view class="device-item">
|
||||
<text class="device-label">电量</text>
|
||||
<text class="device-value">{{deviceInfo.battery}}%</text>
|
||||
</view>
|
||||
<view class="device-item">
|
||||
<text class="device-label">温度</text>
|
||||
<text class="device-value">{{deviceInfo.temperature}}°C</text>
|
||||
</view>
|
||||
<view class="device-item">
|
||||
<text class="device-label">最后更新</text>
|
||||
<text class="device-value">{{deviceInfo.lastUpdate}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<view class="tab-container">
|
||||
<view class="tab-header">
|
||||
<view
|
||||
class="tab-item {{activeTab === 'info' ? 'active' : ''}}"
|
||||
data-tab="info"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
基本信息
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'health' ? 'active' : ''}}"
|
||||
data-tab="health"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
健康记录
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'movement' ? 'active' : ''}}"
|
||||
data-tab="movement"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
运动记录
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tab-content">
|
||||
<!-- 基本信息标签页 -->
|
||||
<view wx:if="{{activeTab === 'info'}}" class="tab-panel">
|
||||
<view class="detail-section">
|
||||
<view class="section-title">详细信息</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">出生日期</text>
|
||||
<text class="detail-value">{{cattleInfo.birthDate}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">创建时间</text>
|
||||
<text class="detail-value">{{cattleInfo.createTime}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">更新时间</text>
|
||||
<text class="detail-value">{{cattleInfo.updateTime}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">备注描述</text>
|
||||
<text class="detail-value">{{cattleInfo.description}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康记录标签页 -->
|
||||
<view wx:elif="{{activeTab === 'health'}}" class="tab-panel">
|
||||
<view class="section-header">
|
||||
<view class="section-title">健康记录</view>
|
||||
<button class="action-btn add" bindtap="addHealthRecord">添加记录</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{healthRecords.length > 0}}" class="record-list">
|
||||
<view
|
||||
wx:for="{{healthRecords}}"
|
||||
wx:key="id"
|
||||
class="record-item"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="viewHealthRecord"
|
||||
>
|
||||
<view class="record-header">
|
||||
<view class="record-type">{{getHealthRecordTypeText(item.type)}}</view>
|
||||
<view class="record-status {{getHealthRecordStatusColor(item.status)}}">
|
||||
{{getHealthRecordStatusText(item.status)}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="record-description">{{item.description}}</view>
|
||||
<view class="record-meta">
|
||||
<text class="record-date">{{item.date}}</text>
|
||||
<text class="record-vet">{{item.veterinarian}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">📋</view>
|
||||
<view class="empty-text">暂无健康记录</view>
|
||||
<view class="empty-desc">点击"添加记录"按钮添加健康记录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 运动记录标签页 -->
|
||||
<view wx:elif="{{activeTab === 'movement'}}" class="tab-panel">
|
||||
<view class="section-title">运动记录</view>
|
||||
|
||||
<view wx:if="{{movementRecords.length > 0}}" class="record-list">
|
||||
<view
|
||||
wx:for="{{movementRecords}}"
|
||||
wx:key="id"
|
||||
class="record-item"
|
||||
>
|
||||
<view class="record-header">
|
||||
<view class="record-date">{{item.date}}</view>
|
||||
<view class="record-location">{{item.location}}</view>
|
||||
</view>
|
||||
<view class="record-content">
|
||||
<view class="movement-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">步数</text>
|
||||
<text class="stat-value">{{item.steps}}</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">距离</text>
|
||||
<text class="stat-value">{{item.distance}}km</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-label">时长</text>
|
||||
<text class="stat-value">{{item.duration}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="empty-state">
|
||||
<view class="empty-icon">🏃</view>
|
||||
<view class="empty-text">暂无运动记录</view>
|
||||
<view class="empty-desc">设备将自动记录牛只运动数据</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<button class="btn btn-secondary" bindtap="transferCattle">转移</button>
|
||||
<button class="btn btn-warning" bindtap="exitCattle">退出</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
533
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.wxss
Normal file
533
mini_program/farm-monitor-wechat/pages/cattle/detail/detail.wxss
Normal file
@@ -0,0 +1,533 @@
|
||||
/* 牛只详情页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 20rpx;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #e0e0e0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 信息卡片 */
|
||||
.info-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
background: #f0f8ff;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-basic {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-id {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-status {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cattle-status.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.cattle-status.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.cattle-status.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.cattle-status.blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.cattle-status.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn.edit {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.action-btn.view {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.action-btn.add {
|
||||
background: #fff2e8;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.device-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.device-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-value.green {
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.device-value.red {
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.device-value.orange {
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.device-value.gray {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.tab-container {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
background: #f8f8f8;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
padding: 25rpx 20rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #3cc51f;
|
||||
border-bottom-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 400rpx;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 记录列表 */
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.record-item:active {
|
||||
background: #f0f0f0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.record-type {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-status.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.record-status.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.record-status.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.record-status.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.record-description {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-date {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.record-vet {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.movement-stats {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #fff2e8;
|
||||
color: #fa8c16;
|
||||
border: 2rpx solid #ffd591;
|
||||
}
|
||||
|
||||
.btn-warning:active {
|
||||
background: #ffe7ba;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.container {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.cattle-id {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 20rpx 15rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.movement-stats {
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 70rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
270
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.js
Normal file
270
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* 牛只退出页面
|
||||
* @file exit.js
|
||||
* @description 牛只退出管理页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
cattleId: '',
|
||||
cattleInfo: {},
|
||||
exitReason: '',
|
||||
exitDate: '',
|
||||
exitType: '',
|
||||
exitNotes: '',
|
||||
reasonOptions: [
|
||||
'出售',
|
||||
'屠宰',
|
||||
'死亡',
|
||||
'转移',
|
||||
'其他'
|
||||
],
|
||||
typeOptions: [
|
||||
{ value: 'sale', label: '出售' },
|
||||
{ value: 'slaughter', label: '屠宰' },
|
||||
{ value: 'death', label: '死亡' },
|
||||
{ value: 'transfer', label: '转移' },
|
||||
{ value: 'other', label: '其他' }
|
||||
],
|
||||
submitting: false,
|
||||
// 计算后的索引值
|
||||
reasonIndex: 0,
|
||||
typeIndex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('牛只退出页面加载', options)
|
||||
if (options.id) {
|
||||
this.setData({
|
||||
cattleId: options.id
|
||||
})
|
||||
this.loadCattleInfo()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载牛只信息
|
||||
*/
|
||||
loadCattleInfo: function() {
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockCattleInfo = {
|
||||
id: this.data.cattleId,
|
||||
name: '牛只-001',
|
||||
breed: '荷斯坦牛',
|
||||
gender: 'female',
|
||||
age: '3岁8个月',
|
||||
weight: 580,
|
||||
healthStatus: 'healthy',
|
||||
currentLocation: 'A区-1号栏'
|
||||
}
|
||||
|
||||
this.setData({
|
||||
cattleInfo: mockCattleInfo
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择退出原因
|
||||
*/
|
||||
onReasonChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
exitReason: this.data.reasonOptions[index],
|
||||
reasonIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择退出类型
|
||||
*/
|
||||
onTypeChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
exitType: this.data.typeOptions[index].value,
|
||||
typeIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择退出日期
|
||||
*/
|
||||
onDateChange: function(e) {
|
||||
this.setData({
|
||||
exitDate: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 输入框变化
|
||||
*/
|
||||
onInputChange: function(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
const value = e.detail.value
|
||||
|
||||
this.setData({
|
||||
[field]: value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 表单验证
|
||||
*/
|
||||
validateForm: function() {
|
||||
const { exitReason, exitType, exitDate } = this.data
|
||||
|
||||
if (!exitReason) {
|
||||
wx.showToast({
|
||||
title: '请选择退出原因',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!exitType) {
|
||||
wx.showToast({
|
||||
title: '请选择退出类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!exitDate) {
|
||||
wx.showToast({
|
||||
title: '请选择退出日期',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交退出申请
|
||||
*/
|
||||
onSubmit: function() {
|
||||
if (!this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要将此牛只退出系统吗?此操作不可撤销。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.confirmSubmit()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认提交
|
||||
*/
|
||||
confirmSubmit: function() {
|
||||
this.setData({
|
||||
submitting: true
|
||||
})
|
||||
|
||||
// 模拟提交
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
submitting: false
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '退出申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消退出
|
||||
*/
|
||||
onCancel: function() {
|
||||
wx.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消退出申请吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取性别文本
|
||||
*/
|
||||
getGenderText: function(gender) {
|
||||
const genderMap = {
|
||||
'male': '公牛',
|
||||
'female': '母牛',
|
||||
'calf': '犊牛'
|
||||
}
|
||||
return genderMap[gender] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态文本
|
||||
*/
|
||||
getHealthStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'healthy': '健康',
|
||||
'sick': '生病',
|
||||
'pregnant': '怀孕',
|
||||
'lactating': '泌乳期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态颜色
|
||||
*/
|
||||
getHealthStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'healthy': 'green',
|
||||
'sick': 'red',
|
||||
'pregnant': 'orange',
|
||||
'lactating': 'blue'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取退出类型文本
|
||||
*/
|
||||
getExitTypeText: function(type) {
|
||||
const typeMap = {
|
||||
'sale': '出售',
|
||||
'slaughter': '屠宰',
|
||||
'death': '死亡',
|
||||
'transfer': '转移',
|
||||
'other': '其他'
|
||||
}
|
||||
return typeMap[type] || '未知'
|
||||
}
|
||||
})
|
||||
117
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.wxml
Normal file
117
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.wxml
Normal file
@@ -0,0 +1,117 @@
|
||||
<!--牛只退出页面-->
|
||||
<view class="container">
|
||||
<view class="form-container">
|
||||
<view class="form-header">
|
||||
<view class="form-title">牛只退出</view>
|
||||
<view class="form-subtitle">将牛只从系统中移除</view>
|
||||
</view>
|
||||
|
||||
<!-- 牛只信息 -->
|
||||
<view class="cattle-info">
|
||||
<view class="cattle-avatar">🐄</view>
|
||||
<view class="cattle-details">
|
||||
<view class="cattle-name">{{cattleInfo.name}}</view>
|
||||
<view class="cattle-meta">
|
||||
<text class="cattle-breed">{{cattleInfo.breed}}</text>
|
||||
<text class="cattle-gender">{{getGenderText(cattleInfo.gender)}}</text>
|
||||
<text class="cattle-age">{{cattleInfo.age}}</text>
|
||||
<text class="cattle-weight">{{cattleInfo.weight}}kg</text>
|
||||
</view>
|
||||
<view class="cattle-status">
|
||||
<text class="status-item {{getHealthStatusColor(cattleInfo.healthStatus)}}">{{getHealthStatusText(cattleInfo.healthStatus)}}</text>
|
||||
<text class="status-item location">{{cattleInfo.currentLocation}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<form bindsubmit="onSubmit">
|
||||
<!-- 退出信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">退出信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">退出原因</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{reasonOptions}}"
|
||||
value="{{reasonIndex}}"
|
||||
bindchange="onReasonChange"
|
||||
>
|
||||
<view class="picker-text">{{exitReason || '请选择退出原因'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">退出类型</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{typeOptions}}"
|
||||
range-key="label"
|
||||
value="{{typeIndex}}"
|
||||
bindchange="onTypeChange"
|
||||
>
|
||||
<view class="picker-text">{{getExitTypeText(exitType) || '请选择退出类型'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">退出日期</view>
|
||||
<picker
|
||||
mode="date"
|
||||
value="{{exitDate}}"
|
||||
bindchange="onDateChange"
|
||||
>
|
||||
<view class="picker-text">{{exitDate || '请选择退出日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">退出备注</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
placeholder="请输入退出备注信息"
|
||||
value="{{exitNotes}}"
|
||||
data-field="exitNotes"
|
||||
bindinput="onInputChange"
|
||||
maxlength="200"
|
||||
/>
|
||||
<view class="textarea-count">{{exitNotes.length}}/200</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 警告信息 -->
|
||||
<view class="warning-section">
|
||||
<view class="warning-icon">⚠️</view>
|
||||
<view class="warning-content">
|
||||
<view class="warning-title">重要提醒</view>
|
||||
<view class="warning-text">
|
||||
牛只退出后将从系统中永久移除,相关数据将被归档。此操作不可撤销,请谨慎操作。
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="form-actions">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
bindtap="onCancel"
|
||||
disabled="{{submitting}}"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
formType="submit"
|
||||
loading="{{submitting}}"
|
||||
>
|
||||
{{submitting ? '提交中...' : '确认退出'}}
|
||||
</button>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
381
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.wxss
Normal file
381
mini_program/farm-monitor-wechat/pages/cattle/exit/exit.wxss
Normal file
@@ -0,0 +1,381 @@
|
||||
/* 牛只退出页面样式 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
padding-bottom: 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 牛只信息 */
|
||||
.cattle-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-radius: 12rpx;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
background: #e6f7ff;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-meta {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-breed,
|
||||
.cattle-gender,
|
||||
.cattle-age,
|
||||
.cattle-weight {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
padding: 4rpx 8rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.cattle-status {
|
||||
display: flex;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-item.green {
|
||||
background: #e8f5e8;
|
||||
color: #3cc51f;
|
||||
}
|
||||
|
||||
.status-item.red {
|
||||
background: #ffe8e8;
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.status-item.orange {
|
||||
background: #fff3e0;
|
||||
color: #ffa502;
|
||||
}
|
||||
|
||||
.status-item.blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-item.gray {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-item.location {
|
||||
background: #f0f8ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 25rpx;
|
||||
padding-left: 15rpx;
|
||||
border-left: 6rpx solid #3cc51f;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-text:active {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
background: #f8f8f8;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
border-color: #3cc51f;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.textarea-count {
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 警告信息 */
|
||||
.warning-section {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #fff2e8;
|
||||
border: 2rpx solid #ffd591;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 15rpx;
|
||||
margin-top: 5rpx;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #fa8c16;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 24rpx;
|
||||
color: #d46b08;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
padding-top: 30rpx;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:active {
|
||||
background: #d9363e;
|
||||
}
|
||||
|
||||
.btn-danger[disabled] {
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.btn-secondary[disabled] {
|
||||
background: #f0f0f0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 750rpx) {
|
||||
.container {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.cattle-info {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.cattle-avatar {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.cattle-name {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.cattle-breed,
|
||||
.cattle-gender,
|
||||
.cattle-age,
|
||||
.cattle-weight {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.picker-text {
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 70rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 牛只转移页面
|
||||
* @file transfer.js
|
||||
* @description 牛只转移管理页面
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
cattleId: '',
|
||||
cattleInfo: {},
|
||||
fromLocation: '',
|
||||
toLocation: '',
|
||||
transferReason: '',
|
||||
transferDate: '',
|
||||
transferNotes: '',
|
||||
locationOptions: [
|
||||
'A区-1号栏',
|
||||
'A区-2号栏',
|
||||
'A区-3号栏',
|
||||
'B区-1号栏',
|
||||
'B区-2号栏',
|
||||
'B区-3号栏',
|
||||
'C区-1号栏',
|
||||
'C区-2号栏',
|
||||
'C区-3号栏'
|
||||
],
|
||||
reasonOptions: [
|
||||
'健康检查',
|
||||
'繁殖管理',
|
||||
'饲料调整',
|
||||
'隔离治疗',
|
||||
'生产管理',
|
||||
'其他'
|
||||
],
|
||||
submitting: false,
|
||||
// 计算后的索引值
|
||||
toLocationIndex: 0,
|
||||
reasonIndex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad: function (options) {
|
||||
console.log('牛只转移页面加载', options)
|
||||
if (options.id) {
|
||||
this.setData({
|
||||
cattleId: options.id
|
||||
})
|
||||
this.loadCattleInfo()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载牛只信息
|
||||
*/
|
||||
loadCattleInfo: function() {
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockCattleInfo = {
|
||||
id: this.data.cattleId,
|
||||
name: '牛只-001',
|
||||
breed: '荷斯坦牛',
|
||||
gender: 'female',
|
||||
currentLocation: 'A区-1号栏',
|
||||
healthStatus: 'healthy'
|
||||
}
|
||||
|
||||
this.setData({
|
||||
cattleInfo: mockCattleInfo,
|
||||
fromLocation: mockCattleInfo.currentLocation
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择目标位置
|
||||
*/
|
||||
onToLocationChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
toLocation: this.data.locationOptions[index],
|
||||
toLocationIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择转移原因
|
||||
*/
|
||||
onReasonChange: function(e) {
|
||||
const index = e.detail.value
|
||||
this.setData({
|
||||
transferReason: this.data.reasonOptions[index],
|
||||
reasonIndex: index
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择转移日期
|
||||
*/
|
||||
onDateChange: function(e) {
|
||||
this.setData({
|
||||
transferDate: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 输入框变化
|
||||
*/
|
||||
onInputChange: function(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
const value = e.detail.value
|
||||
|
||||
this.setData({
|
||||
[field]: value
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 表单验证
|
||||
*/
|
||||
validateForm: function() {
|
||||
const { toLocation, transferReason, transferDate } = this.data
|
||||
|
||||
if (!toLocation) {
|
||||
wx.showToast({
|
||||
title: '请选择目标位置',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (toLocation === this.data.fromLocation) {
|
||||
wx.showToast({
|
||||
title: '目标位置不能与当前位置相同',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!transferReason) {
|
||||
wx.showToast({
|
||||
title: '请选择转移原因',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!transferDate) {
|
||||
wx.showToast({
|
||||
title: '请选择转移日期',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交转移申请
|
||||
*/
|
||||
onSubmit: function() {
|
||||
if (!this.validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
submitting: true
|
||||
})
|
||||
|
||||
// 模拟提交
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
submitting: false
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: '转移申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消转移
|
||||
*/
|
||||
onCancel: function() {
|
||||
wx.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消转移申请吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取性别文本
|
||||
*/
|
||||
getGenderText: function(gender) {
|
||||
const genderMap = {
|
||||
'male': '公牛',
|
||||
'female': '母牛',
|
||||
'calf': '犊牛'
|
||||
}
|
||||
return genderMap[gender] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态文本
|
||||
*/
|
||||
getHealthStatusText: function(status) {
|
||||
const statusMap = {
|
||||
'healthy': '健康',
|
||||
'sick': '生病',
|
||||
'pregnant': '怀孕',
|
||||
'lactating': '泌乳期'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康状态颜色
|
||||
*/
|
||||
getHealthStatusColor: function(status) {
|
||||
const colorMap = {
|
||||
'healthy': 'green',
|
||||
'sick': 'red',
|
||||
'pregnant': 'orange',
|
||||
'lactating': 'blue'
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
<!--牛只转移页面-->
|
||||
<view class="container">
|
||||
<view class="form-container">
|
||||
<view class="form-header">
|
||||
<view class="form-title">牛只转移</view>
|
||||
<view class="form-subtitle">将牛只转移到新的位置</view>
|
||||
</view>
|
||||
|
||||
<!-- 牛只信息 -->
|
||||
<view class="cattle-info">
|
||||
<view class="cattle-avatar">🐄</view>
|
||||
<view class="cattle-details">
|
||||
<view class="cattle-name">{{cattleInfo.name}}</view>
|
||||
<view class="cattle-meta">
|
||||
<text class="cattle-breed">{{cattleInfo.breed}}</text>
|
||||
<text class="cattle-gender">{{getGenderText(cattleInfo.gender)}}</text>
|
||||
<text class="cattle-status {{getHealthStatusColor(cattleInfo.healthStatus)}}">{{getHealthStatusText(cattleInfo.healthStatus)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<form bindsubmit="onSubmit">
|
||||
<!-- 转移信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">转移信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">当前位置</view>
|
||||
<view class="form-value">{{fromLocation}}</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">目标位置</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{locationOptions}}"
|
||||
value="{{toLocationIndex}}"
|
||||
bindchange="onToLocationChange"
|
||||
>
|
||||
<view class="picker-text">{{toLocation || '请选择目标位置'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">转移原因</view>
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{reasonOptions}}"
|
||||
value="{{reasonIndex}}"
|
||||
bindchange="onReasonChange"
|
||||
>
|
||||
<view class="picker-text">{{transferReason || '请选择转移原因'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label required">转移日期</view>
|
||||
<picker
|
||||
mode="date"
|
||||
value="{{transferDate}}"
|
||||
bindchange="onDateChange"
|
||||
>
|
||||
<view class="picker-text">{{transferDate || '请选择转移日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">转移备注</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
placeholder="请输入转移备注信息"
|
||||
value="{{transferNotes}}"
|
||||
data-field="transferNotes"
|
||||
bindinput="onInputChange"
|
||||
maxlength="200"
|
||||
/>
|
||||
<view class="textarea-count">{{transferNotes.length}}/200</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="form-actions">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
bindtap="onCancel"
|
||||
disabled="{{submitting}}"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
formType="submit"
|
||||
loading="{{submitting}}"
|
||||
>
|
||||
{{submitting ? '提交中...' : '提交转移'}}
|
||||
</button>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user