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

This commit is contained in:
xuqiuyun
2025-09-22 19:09:45 +08:00
parent 02a25515a9
commit 325c114c38
256 changed files with 48348 additions and 4444 deletions

View 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文档
- 代码注释和说明

View File

@@ -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

View 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)
}
})
})
}
})

View 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"
}

View 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;
}
}

View 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() - 完整测试流程');

View 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. 建议使用矢量图标,确保在不同分辨率下显示清晰

View File

@@ -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"
}
}
}

View 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)
}
})

View 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>

View 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;
}
}

View 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)
}
})

View 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>

View 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;
}
}

View 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)
}
})

View 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>

View 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;
}
}

View 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'
})
}
})

View 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>

View 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;
}
}

View 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'
})
}
})

View 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>

View 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;
}
}

View 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)
}
})

View File

@@ -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>

View 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;
}
}

View 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": []
}
}
}

View File

@@ -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
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

View File

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

View File

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

View File

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

View File

@@ -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),
// 设备编号(如果有的话)

View File

@@ -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

View File

@@ -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('未知工具类型')
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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',

View File

@@ -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

View File

@@ -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}`
}

View File

@@ -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)
}
}

View File

@@ -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是否有效

View File

@@ -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
}

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
}

View 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
}

View 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
}

View 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中无主机统计
- 所有预警数据均为动态获取,无硬编码数据
- 支持实时刷新和自动更新

View 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) 文件。

View 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)
}
})

View 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"
}

View 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;
}
}

View 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('所有页面创建完成!')

View 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('所有剩余页面创建完成!')

View 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的高清版本
- 图片命名使用小写字母和连字符
- 定期优化图片大小,提升加载性能

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View File

@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==

View 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"
}

View 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] || '⚠️'
}
})

View 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>

View 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;
}
}

View 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] || '⚠️'
}
})

View 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>

View 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;
}
}

View 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] || '⚠️'
}
})

View 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>

View 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;
}
}

View 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] || '未知'
}
})

View 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>

View 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;
}
}

View 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'
}
})

View 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>

View 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;
}
}

View 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'
}
})

View 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>

View 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;
}
}

View 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] || '未知'
}
})

View 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>

View 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;
}
}

View File

@@ -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'
}
})

View File

@@ -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