完善保险端项目

This commit is contained in:
shenquanyi
2025-10-09 17:56:19 +08:00
parent deb005b88e
commit 74b2edb510
129 changed files with 17064 additions and 8802 deletions

View File

@@ -1,6 +1,6 @@
# 开发环境配置
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=development
VUE_APP_BASE_URL=http://localhost:3000/api
VUE_APP_WEIXIN_APPID=wx-your-dev-appid
VUE_APP_DEBUG=true
VUE_APP_VERSION=1.0.0-dev

View File

@@ -1,6 +1,6 @@
# 生产环境配置
NODE_ENV=production
VUE_APP_BASE_URL=https://your-production-domain.com/api
VUE_APP_WEIXIN_APPID=wx-your-prod-appid
VUE_APP_DEBUG=false
# 生产环境配置
NODE_ENV=production
VUE_APP_BASE_URL=https://your-production-domain.com/api
VUE_APP_WEIXIN_APPID=wx-your-prod-appid
VUE_APP_DEBUG=false
VUE_APP_VERSION=1.0.0

View File

@@ -0,0 +1,55 @@
// app.js - 养殖管理系统小程序入口文件
App({
onLaunch() {
// 小程序启动
console.log('养殖管理系统小程序启动')
// 获取系统信息
const systemInfo = wx.getSystemInfoSync()
this.globalData.systemInfo = systemInfo
// 检查登录状态
this.checkLoginStatus()
},
onShow() {
// 小程序显示
console.log('小程序显示')
},
onHide() {
// 小程序隐藏
console.log('小程序隐藏')
},
onError(err) {
// 小程序错误
console.error('小程序错误:', err)
},
// 检查登录状态
checkLoginStatus() {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
if (token && userInfo) {
this.globalData.isLogin = true
this.globalData.userInfo = userInfo
this.globalData.token = token
} else {
this.globalData.isLogin = false
this.globalData.userInfo = null
this.globalData.token = ''
}
},
// 全局数据
globalData: {
isLogin: false,
userInfo: null,
token: '',
systemInfo: null,
apiBaseUrl: 'https://ad.ningmuyun.com/farm/api'
}
})

View File

@@ -0,0 +1,64 @@
{
"pages": [
"pages/home/home",
"pages/login/login",
"pages/cattle/cattle",
"pages/cattle/archive/archive",
"pages/cattle/detail/detail",
"pages/cattle/transfer/transfer",
"pages/cattle/transfer-detail/transfer-detail",
"pages/cattle/exit/exit",
"pages/cattle/exit-detail/exit-detail",
"pages/cattle/pen/pen",
"pages/cattle/pen-detail/pen-detail",
"pages/cattle/batch/batch",
"pages/cattle/batch-detail/batch-detail",
"pages/device/device",
"pages/device/collar/collar",
"pages/device/eartag/eartag",
"pages/device/host/host",
"pages/device/fence/fence",
"pages/alert/alert",
"pages/alert/collar-alert/collar-alert",
"pages/alert/collar-alert-detail/collar-alert-detail",
"pages/alert/eartag-alert/eartag-alert",
"pages/alert/eartag-alert-detail/eartag-alert-detail",
"pages/production/production",
"pages/profile/profile"
],
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "养殖管理系统",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f6f6f6"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#52c41a",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页"
},
{
"pagePath": "pages/cattle/cattle",
"text": "生产管理"
},
{
"pagePath": "pages/profile/profile",
"text": "我的"
}
]
},
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于养殖场定位和地图展示"
}
},
"sitemapLocation": "sitemap.json",
"style": "v2",
"lazyCodeLoading": "requiredComponents"
}

View File

@@ -1,281 +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;
}
}
/* 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,48 @@
// config/env.js - 环境配置文件
/**
* 环境配置
* 可以根据不同环境切换API地址
*/
// 开发环境
const development = {
apiBaseUrl: 'https://ad.ningmuyun.com/farm/api',
uploadUrl: 'https://ad.ningmuyun.com/farm/api/upload',
imageBaseUrl: 'https://ad.ningmuyun.com/farm',
timeout: 10000,
debug: true
}
// 测试环境
const testing = {
apiBaseUrl: 'https://test.ningmuyun.com/farm/api',
uploadUrl: 'https://test.ningmuyun.com/farm/api/upload',
imageBaseUrl: 'https://test.ningmuyun.com/farm',
timeout: 10000,
debug: true
}
// 生产环境
const production = {
apiBaseUrl: 'https://ad.ningmuyun.com/farm/api',
uploadUrl: 'https://ad.ningmuyun.com/farm/api/upload',
imageBaseUrl: 'https://ad.ningmuyun.com/farm',
timeout: 10000,
debug: false
}
// 当前环境配置
// 可以通过修改这里来切换环境
const currentEnv = 'development' // development | testing | production
// 环境映射
const envMap = {
development,
testing,
production
}
// 导出当前环境配置
module.exports = envMap[currentEnv]

View File

@@ -1,21 +1,21 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 背景圆形 -->
<circle cx="15" cy="15" r="14" fill="#7CB342" stroke="#ffffff" stroke-width="2"/>
<!-- 围栏图标 -->
<g transform="translate(7, 7)">
<!-- 围栏柱子 -->
<rect x="2" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="6" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="10" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="14" y="4" width="1.5" height="12" fill="#ffffff"/>
<!-- 围栏横条 -->
<rect x="1" y="7" width="15" height="1" fill="#ffffff"/>
<rect x="1" y="10" width="15" height="1" fill="#ffffff"/>
<rect x="1" y="13" width="15" height="1" fill="#ffffff"/>
</g>
<!-- 阴影效果 -->
<circle cx="15" cy="16" r="12" fill="rgba(0,0,0,0.1)" opacity="0.3"/>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 背景圆形 -->
<circle cx="15" cy="15" r="14" fill="#7CB342" stroke="#ffffff" stroke-width="2"/>
<!-- 围栏图标 -->
<g transform="translate(7, 7)">
<!-- 围栏柱子 -->
<rect x="2" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="6" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="10" y="4" width="1.5" height="12" fill="#ffffff"/>
<rect x="14" y="4" width="1.5" height="12" fill="#ffffff"/>
<!-- 围栏横条 -->
<rect x="1" y="7" width="15" height="1" fill="#ffffff"/>
<rect x="1" y="10" width="15" height="1" fill="#ffffff"/>
<rect x="1" y="13" width="15" height="1" fill="#ffffff"/>
</g>
<!-- 阴影效果 -->
<circle cx="15" cy="16" r="12" fill="rgba(0,0,0,0.1)" opacity="0.3"/>
</svg>

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 893 B

View File

@@ -0,0 +1,234 @@
# 首页图标资源获取指南
## 📌 需要的图标列表
根据首页UI设计需要准备以下13个图标文件
### 智能设备图标5个
1.`icon-collar.png` - 智能项圈图标
2.`icon-eartag.png` - 智能耳标图标
3.`icon-anklet.png` - 智能脚环图标
4.`icon-host.png` - 智能主机图标
5.`icon-monitor.png` - 视频监控图标
### 智能工具图标4个
6.`icon-fence.png` - 电子围栏图标
7.`icon-scan.png` - 扫码溯源图标
8.`icon-check.png` - 检测工具图标
9.`icon-photo.png` - 档案拍照图标
### 业务办理图标3个
10.`icon-quarantine.png` - 电子检疫图标
11.`icon-rights.png` - 电子确权图标
12.`icon-disposal.png` - 无害化处理申报图标
---
## 🎨 图标规格要求
### 尺寸
- **推荐尺寸**120x120 像素
- **最小尺寸**60x60 像素
- **最大尺寸**200x200 像素
### 格式
- **格式**PNG推荐支持透明
- **备选**SVG矢量图可缩放
### 风格
- **图标风格**:扁平化、线性或面性图标
- **颜色**:单色或双色(与背景渐变色搭配)
- **背景**:透明背景
### 文件大小
- 每个图标文件不超过50KB
---
## 🔍 图标获取途径
### 方法一:使用在线图标库(推荐)
#### 1. 阿里巴巴矢量图标库iconfont
🔗 https://www.iconfont.cn/
**步骤:**
1. 注册/登录账号
2. 搜索对应的图标关键词
3. 选择合适的图标
4. 下载PNG格式120x120
5. 重命名为对应的文件名
**推荐搜索关键词:**
- 项圈collar, necklace, tag
- 耳标tag, label, eartag
- 脚环anklet, tracker
- 主机server, host, gateway
- 监控monitor, camera, video
- 围栏fence, boundary
- 扫码scan, qrcode, barcode
- 检测check, test, detect
- 拍照camera, photo
- 检疫quarantine, inspection
- 确权certificate, rights
- 处理recycle, disposal
#### 2. IconPark字节跳动
🔗 https://iconpark.oceanengine.com/
**特点:**
- 高质量图标
- 可自定义颜色
- 支持多种格式下载
#### 3. Flaticon
🔗 https://www.flaticon.com/
**特点:**
- 海量免费图标
- 支持PNG、SVG格式
- 需要注明来源
#### 4. IconFinder
🔗 https://www.iconfinder.com/
**特点:**
- 免费+付费图标
- 质量高
- 可按风格筛选
---
### 方法二:设计师制作
如果您有设计师,可以根据以下参考制作图标:
**智能项圈**:圆形项圈 + 信号图标
**智能耳标**:标签形状 + 数字/信号
**智能脚环**:圆环 + 定位图标
**智能主机**:服务器/主机图标
**视频监控**:摄像头图标
**电子围栏**:围栏/边界图标
**扫码溯源**:二维码/扫描图标
**检测工具**:检查/测试图标
**档案拍照**:相机图标
**电子检疫**:文档+检查图标
**电子确权**:证书/文档图标
**无害化处理**:回收/处理图标
---
### 方法三使用Emoji临时方案
如果暂时没有图标资源可以先用Emoji代替已在代码中实现
当前使用的Emoji
- 🎯 智能项圈
- 🏷️ 智能耳标
- 📍 智能脚环
- 📡 智能主机
- 📹 视频监控
- ⭕ 电子围栏
- ✓ 扫码溯源
- 📊 检测工具
- 📷 档案拍照
- 📋 电子检疫
- 📝 电子确权
- ♻️ 无害化处理申报
**如需使用Emoji请修改代码**
`<image>` 标签改为 `<text>` 标签即可。
---
## 📦 下载后的操作步骤
### 1. 重命名文件
按照上述文件名列表,将下载的图标重命名。
### 2. 放置图标
将所有图标文件复制到:
```
mini_program/farm-monitor-dashboard/images/
```
### 3. 验证图标
在微信开发者工具中:
1. 打开首页
2. 检查所有图标是否正常显示
3. 如有缺失,检查文件名是否正确
---
## 🎨 配色建议
虽然图标可以是单色的(代码中已设置背景色),但如果想要更好的视觉效果,建议图标颜色与背景色协调:
| 功能 | 背景色 | 建议图标色 |
|------|--------|-----------|
| 智能项圈 | 黄色渐变 | 深橙色 #FF9800 |
| 智能耳标 | 蓝色渐变 | 深蓝色 #2196F3 |
| 智能脚环 | 紫色渐变 | 深紫色 #9C27B0 |
| 智能主机 | 浅蓝渐变 | 蓝色 #03A9F4 |
| 视频监控 | 橙色渐变 | 深橙色 #FF9800 |
| 电子围栏 | 黄橙渐变 | 橙色 #FFA726 |
| 扫码溯源 | 蓝色渐变 | 深蓝色 #5C6BC0 |
| 检测工具 | 紫色渐变 | 紫色 #7E57C2 |
| 档案拍照 | 红色渐变 | 红色 #EF5350 |
| 电子检疫 | 黄色渐变 | 金色 #FDD835 |
| 电子确权 | 青色渐变 | 青色 #26A69A |
| 无害化处理 | 绿紫渐变 | 绿色 #66BB6A |
---
## 🚀 快速开始建议
**如果急于上线,建议:**
1. **方案A**先用Emoji已实现
- 优点:立即可用
- 缺点:视觉效果一般
2. **方案B**快速下载iconfont图标
- 时间约30分钟
- 质量:中等
- 适合:快速原型
3. **方案C**:请设计师定制
- 时间1-3天
- 质量:高
- 适合:正式发布
---
## ⚠️ 注意事项
1. **版权问题**
- 使用免费图标库时,注意查看授权协议
- 商用项目建议购买商业授权或定制
2. **文件命名**
- 必须与代码中的文件名完全一致
- 区分大小写
3. **图片优化**
- 使用TinyPNG等工具压缩图片
- 减少小程序包体积
4. **测试**
- 在不同设备上测试显示效果
- 确保图标清晰不模糊
---
## 📞 需要帮助?
如果您在获取或使用图标时遇到问题,可以:
1. 联系设计团队
2. 查看微信小程序官方文档
3. 在项目群中询问
---
**更新日期**2025-01-09

View File

@@ -0,0 +1,70 @@
# 地图标记图标说明
## 📍 所需图标
电子围栏地图功能需要以下图标文件:
### marker.png
**路径:** `/images/marker.png`
**用途:** 地图上的围栏中心点标记
**建议规格:**
- 尺寸60x60 像素或更大保持1:1比例
- 格式PNG支持透明背景
- 颜色:建议使用绿色或红色(匹配围栏状态)
**图标样式建议:**
- 📍 红色定位针图标
- 🔴 实心圆点标记
- 🟢 绿色标记点
---
## 🎨 获取图标的方法
### 方法1使用在线图标库
1. 访问 [Iconfont](https://www.iconfont.cn/) 或 [IconPark](https://iconpark.oceanengine.com/)
2. 搜索 "地图标记" 或 "定位"
3. 选择喜欢的图标
4. 下载PNG格式60x60或更大
5. 重命名为 `marker.png`
6. 放置到 `images/` 目录
### 方法2使用系统Emoji临时方案
代码中已使用Emoji作为临时标记无需额外图标文件。
如果图标文件不存在,将使用默认的地图气泡标记。
---
## 🔄 替代方案
如果没有图标文件,可以修改代码使用默认标记:
```javascript
// 在 fence.js 中修改 marker 配置
const marker = {
id: fence.id,
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng),
title: fence.name,
// iconPath: '/images/marker.png', // 注释掉这行
width: 30,
height: 30,
callout: {
content: `${fence.name}\n${fence.type}`,
fontSize: 12,
borderRadius: 5,
padding: 8,
display: 'ALWAYS'
}
}
```
这样将使用微信地图的默认红色气泡标记。
---
**说明:** 当前代码使用了 `/images/marker.png`,如果该文件不存在,地图标记仍然会显示,只是使用默认样式。

View File

@@ -1,80 +1,80 @@
{
"name": "养殖端小程序",
"appid": "wx-your-appid-here",
"description": "养殖管理系统微信小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"mp-weixin": {
"appid": "wx-your-appid-here",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于养殖场定位和地图展示"
}
},
"optimization": {
"subPackages": true
}
},
"vueVersion": "3",
"splashscreen": {
"alwaysShowBeforeRender": true,
"autoclose": false,
"waiting": true
},
"app-plus": {
"usingComponents": true,
"nvueStyle": "flex",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"autoclose": false,
"waiting": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_LOCATION_EXTRA_COMMANDS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>"
]
},
"ios": {}
}
},
"quickapp": {},
"h5": {
"router": {
"mode": "hash"
},
"optimization": {
"treeShaking": {
"enable": true
}
}
}
{
"name": "养殖端小程序",
"appid": "wx-your-appid-here",
"description": "养殖管理系统微信小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"mp-weixin": {
"appid": "wx-your-appid-here",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于养殖场定位和地图展示"
}
},
"optimization": {
"subPackages": true
}
},
"vueVersion": "3",
"splashscreen": {
"alwaysShowBeforeRender": true,
"autoclose": false,
"waiting": true
},
"app-plus": {
"usingComponents": true,
"nvueStyle": "flex",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"autoclose": false,
"waiting": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_LOCATION_EXTRA_COMMANDS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>"
]
},
"ios": {}
}
},
"quickapp": {},
"h5": {
"router": {
"mode": "hash"
},
"optimization": {
"treeShaking": {
"enable": true
}
}
}
}

View File

@@ -1,141 +1,141 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "养殖管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/cattle/cattle",
"style": {
"navigationBarTitleText": "牛只管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6"
}
},
{
"path": "pages/cattle-detail/cattle-detail",
"style": {
"navigationBarTitleText": "牛只详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/farms/farms",
"style": {
"navigationBarTitleText": "养殖场管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/farm-detail/farm-detail",
"style": {
"navigationBarTitleText": "养殖场详情",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/growth/growth",
"style": {
"navigationBarTitleText": "生长数据",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/events/events",
"style": {
"navigationBarTitleText": "事件记录",
"enablePullDownRefresh": true
}
},
{
"path": "pages/stats/stats",
"style": {
"navigationBarTitleText": "统计分析",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "个人中心",
"navigationBarBackgroundColor": "#ffffff"
}
}
],
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#1890ff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/cattle/cattle",
"iconPath": "static/tabbar/cattle.png",
"selectedIconPath": "static/tabbar/cattle-active.png",
"text": "牛只"
},
{
"pagePath": "pages/farms/farms",
"iconPath": "static/tabbar/farm.png",
"selectedIconPath": "static/tabbar/farm-active.png",
"text": "养殖场"
},
{
"pagePath": "pages/my/my",
"iconPath": "static/tabbar/my.png",
"selectedIconPath": "static/tabbar/my-active.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "养殖管理系统",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f6f6f6",
"app-plus": {
"background": "#efeff4"
}
},
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^van-(.*)": "@vant/weapp/dist/$1/index"
}
},
"condition": {
"current": 0,
"list": [
{
"name": "牛只详情",
"path": "pages/cattle-detail/cattle-detail",
"query": "id=1"
},
{
"name": "设备详情",
"path": "pages/device-detail/device-detail",
"query": "id=1"
}
]
}
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "养殖管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/cattle/cattle",
"style": {
"navigationBarTitleText": "牛只管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6"
}
},
{
"path": "pages/cattle-detail/cattle-detail",
"style": {
"navigationBarTitleText": "牛只详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/farms/farms",
"style": {
"navigationBarTitleText": "养殖场管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/farm-detail/farm-detail",
"style": {
"navigationBarTitleText": "养殖场详情",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/growth/growth",
"style": {
"navigationBarTitleText": "生长数据",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/events/events",
"style": {
"navigationBarTitleText": "事件记录",
"enablePullDownRefresh": true
}
},
{
"path": "pages/stats/stats",
"style": {
"navigationBarTitleText": "统计分析",
"navigationBarBackgroundColor": "#ffffff"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "个人中心",
"navigationBarBackgroundColor": "#ffffff"
}
}
],
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#1890ff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/cattle/cattle",
"iconPath": "static/tabbar/cattle.png",
"selectedIconPath": "static/tabbar/cattle-active.png",
"text": "牛只"
},
{
"pagePath": "pages/farms/farms",
"iconPath": "static/tabbar/farm.png",
"selectedIconPath": "static/tabbar/farm-active.png",
"text": "养殖场"
},
{
"pagePath": "pages/my/my",
"iconPath": "static/tabbar/my.png",
"selectedIconPath": "static/tabbar/my-active.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "养殖管理系统",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f6f6f6",
"app-plus": {
"background": "#efeff4"
}
},
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^van-(.*)": "@vant/weapp/dist/$1/index"
}
},
"condition": {
"current": 0,
"list": [
{
"name": "牛只详情",
"path": "pages/cattle-detail/cattle-detail",
"query": "id=1"
},
{
"name": "设备详情",
"path": "pages/device-detail/device-detail",
"query": "id=1"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "告警信息",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6",
"usingComponents": {}
}

View File

@@ -1,176 +1,176 @@
<!--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 wx:if="{{detailVisible}}" class="detail-mask" catchtouchmove="true">
<view class="detail-panel">
<view class="detail-header">
<text class="detail-title">预警详情</text>
<text class="detail-close" bindtap="closeDetail">✖</text>
</view>
<view class="detail-body">
<view class="detail-row" wx:for="{{detailPairs}}" wx:key="label">
<text class="detail-label">{{item.label}}</text>
<text class="detail-value">{{item.value}}</text>
</view>
</view>
<view class="detail-footer">
<button class="primary" bindtap="handleAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">处理</button>
<button class="plain" bindtap="ignoreAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">忽略</button>
</view>
</view>
</view>
</view>
<!--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 wx:if="{{detailVisible}}" class="detail-mask" catchtouchmove="true">
<view class="detail-panel">
<view class="detail-header">
<text class="detail-title">预警详情</text>
<text class="detail-close" bindtap="closeDetail">✖</text>
</view>
<view class="detail-body">
<view class="detail-row" wx:for="{{detailPairs}}" wx:key="label">
<text class="detail-label">{{item.label}}</text>
<text class="detail-value">{{item.value}}</text>
</view>
</view>
<view class="detail-footer">
<button class="primary" bindtap="handleAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">处理</button>
<button class="plain" bindtap="ignoreAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">忽略</button>
</view>
</view>
</view>
</view>

View File

@@ -1,380 +1,380 @@
/* 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;
}
/* 详情弹窗样式 */
.detail-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999;
}
.detail-panel {
width: 100%;
max-height: 70vh;
background: #fff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
overflow: hidden;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-title { font-size: 32rpx; font-weight: 600; color: #303133; }
.detail-close { font-size: 32rpx; color: #909399; }
.detail-body {
padding: 16rpx 24rpx;
max-height: 50vh;
overflow-y: auto;
}
.detail-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx dashed #f0f0f0;
}
.detail-label { font-size: 26rpx; color: #606266; }
.detail-value { font-size: 26rpx; color: #303133; }
.detail-footer {
display: flex;
gap: 16rpx;
padding: 24rpx;
border-top: 1rpx solid #f0f0f0;
}
.detail-footer .primary {
flex: 1;
background-color: #3cc51f;
color: #fff;
}
.detail-footer .plain {
flex: 1;
background-color: #f5f5f5;
color: #606266;
}
.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;
}
}
/* 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;
}
/* 详情弹窗样式 */
.detail-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999;
}
.detail-panel {
width: 100%;
max-height: 70vh;
background: #fff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
overflow: hidden;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-title { font-size: 32rpx; font-weight: 600; color: #303133; }
.detail-close { font-size: 32rpx; color: #909399; }
.detail-body {
padding: 16rpx 24rpx;
max-height: 50vh;
overflow-y: auto;
}
.detail-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx dashed #f0f0f0;
}
.detail-label { font-size: 26rpx; color: #606266; }
.detail-value { font-size: 26rpx; color: #303133; }
.detail-footer {
display: flex;
gap: 16rpx;
padding: 24rpx;
border-top: 1rpx solid #f0f0f0;
}
.detail-footer .primary {
flex: 1;
background-color: #3cc51f;
color: #fff;
}
.detail-footer .plain {
flex: 1;
background-color: #f5f5f5;
color: #606266;
}
.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,152 @@
// pages/alert/collar-alert-detail/collar-alert-detail.js
const API = require('../../../utils/api').API
Page({
data: {
alertId: null,
alertData: [],
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('预警详情页面加载,参数:', options)
if (options.id) {
this.setData({ alertId: options.id })
this.loadAlertDetail(options.id)
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载预警详情
*/
async loadAlertDetail(id) {
try {
this.setData({ loading: true })
console.log('获取预警详情ID:', id)
const res = await API.getCollarAlertDetail(id)
console.log('预警详情数据:', res)
// 根据实际返回的数据结构调整
const alertInfo = res.data || res
// 格式化数据为展示格式
const formattedData = this.formatAlertData(alertInfo)
this.setData({
alertData: formattedData,
loading: false
})
} catch (error) {
console.error('加载预警详情失败:', error)
this.setData({ loading: false })
wx.showToast({
title: '加载失败',
icon: 'none'
})
}
},
/**
* 格式化预警数据
*/
formatAlertData(data) {
if (!data) return []
const sections = [
{
title: '预警信息',
items: [
{ label: '预警ID', value: data.id || '-' },
{ label: '预警类型', value: this.getAlertTypeText(data.alertType) },
{ label: '预警等级', value: this.getAlertLevelText(data.alertLevel) },
{ label: '预警时间', value: data.alertTime || '-' },
{ label: '预警描述', value: data.description || '-' }
]
},
{
title: '设备信息',
items: [
{ label: '设备ID', value: data.deviceId || '-' },
{ label: '设备名称', value: data.deviceName || '-' },
{ label: '项圈编号', value: data.collarNumber || '-' },
{ label: '设备状态', value: data.deviceStatus || '-' },
{ label: '佩戴状态', value: data.wearStatus || '-' }
]
},
{
title: '监测数据',
items: [
{ label: '当前温度', value: data.temperature ? `${data.temperature}°C` : '-' },
{ label: '电池电量', value: data.battery ? `${data.battery}%` : '-' },
{ label: 'GPS信号', value: data.gpsSignal || '-' },
{ label: '运动状态', value: data.movementStatus || '-' }
]
},
{
title: '位置信息',
items: [
{ label: '经度', value: data.longitude || '-' },
{ label: '纬度', value: data.latitude || '-' }
]
},
{
title: '运动数据',
items: [
{ label: '今日步数', value: data.dailySteps || 0 },
{ label: '昨日步数', value: data.yesterdaySteps || 0 },
{ label: '累计步数', value: data.totalSteps || 0 }
]
}
]
return sections
},
/**
* 获取预警等级文本
*/
getAlertLevelText(level) {
const levelMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '紧急'
}
return levelMap[level] || level || '-'
},
/**
* 获取预警类型文本
*/
getAlertTypeText(type) {
const typeMap = {
'temperature': '温度异常',
'battery': '电量异常',
'not_collected': '未采集',
'collar_cut': '项圈剪断',
'fence': '围栏异常',
'activity_high': '运动量偏高',
'activity_low': '运动量偏低',
'transfer_slow': '传输过慢'
}
return typeMap[type] || type || '-'
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "预警详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,34 @@
<!--pages/alert/collar-alert-detail/collar-alert-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 预警详情 -->
<view class="detail-content" wx:else>
<!-- 头部 -->
<view class="detail-header">
<text class="header-icon">🔔</text>
<text class="header-title">项圈预警详情</text>
</view>
<!-- 详细信息 -->
<view class="section" wx:for="{{alertData}}" wx:key="title">
<view class="section-title">{{item.title}}</view>
<view class="section-content">
<view class="info-item" wx:for="{{item.items}}" wx:key="label" wx:for-item="field">
<view class="info-label">{{field.label}}</view>
<view class="info-value">{{field.value}}</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="action-btn resolve">标记已处理</button>
<button class="action-btn ignore">忽略预警</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,124 @@
/* pages/alert/collar-alert-detail/collar-alert-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 0 30rpx;
}
/* 头部 */
.detail-header {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c5a 100%);
padding: 40rpx;
margin: 0 -30rpx 30rpx;
text-align: center;
}
.header-icon {
font-size: 80rpx;
display: block;
margin-bottom: 16rpx;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
/* 信息块 */
.section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #fff8f5;
border-bottom: 1rpx solid #ffe5d9;
border-left: 6rpx solid #ff6b35;
}
.section-content {
padding: 20rpx;
}
.info-item {
display: flex;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
align-items: flex-start;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
width: 220rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
font-weight: 500;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 20rpx;
padding: 0 30rpx;
margin-top: 40rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
padding: 0;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
border: none;
}
.action-btn::after {
border: none;
}
.action-btn.resolve {
background-color: #07c160;
color: #fff;
}
.action-btn.ignore {
background-color: #999;
color: #fff;
}

View File

@@ -0,0 +1,255 @@
// pages/alert/collar-alert/collar-alert.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: '',
alertType: '' // 预警类型筛选
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
limit: this.data.limit,
search: this.data.searchValue,
alertType: this.data.alertType,
_t: Date.now()
}
console.log('请求参数:', params)
const res = await API.getCollarAlerts(params)
console.log('项圈预警数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化数据
list = list.map(item => {
return {
...item,
// 格式化预警时间
alertTimeText: item.alertTime ? this.formatDateTime(item.alertTime) : '-',
// 预警等级文本
alertLevelText: this.getAlertLevelText(item.alertLevel),
// 预警类型文本
alertTypeText: this.getAlertTypeText(item.alertType),
// 设备状态文本
deviceStatusText: item.deviceStatus || '未知',
// 佩戴状态文本
wearStatusText: item.wearStatus || '未知',
// 运动状态文本
movementStatusText: item.movementStatus || '未知'
}
})
const totalPages = pagination.pages || Math.ceil(total / this.data.limit)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
// 如果已经是格式化好的,直接返回
if (typeof dateString === 'string' && dateString.includes('-')) {
return dateString
}
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 获取预警等级文本
*/
getAlertLevelText(level) {
const levelMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '紧急'
}
return levelMap[level] || level || '-'
},
/**
* 获取预警类型文本
*/
getAlertTypeText(type) {
const typeMap = {
'temperature': '温度异常',
'battery': '电量异常',
'not_collected': '未采集',
'collar_cut': '项圈剪断',
'fence': '围栏异常',
'activity_high': '运动量偏高',
'activity_low': '运动量偏低',
'transfer_slow': '传输过慢'
}
return typeMap[type] || type || '-'
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看预警详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看预警详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/alert/collar-alert-detail/collar-alert-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "项圈预警",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,93 @@
<!--pages/alert/collar-alert/collar-alert.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入项圈编号或设备名称"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 预警列表 -->
<view class="alert-list" wx:else>
<view class="alert-card {{item.alertLevel}}" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 预警头部 -->
<view class="card-header">
<view class="header-left">
<text class="alert-type">{{item.alertTypeText}}</text>
<text class="alert-level level-{{item.alertLevel}}">{{item.alertLevelText}}</text>
</view>
<text class="collar-number">{{item.collarNumber}}</text>
</view>
<!-- 预警描述 -->
<view class="alert-description">
<text>{{item.description}}</text>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">设备名称:</text>
<text class="value">{{item.deviceName}}</text>
</view>
<view class="info-row">
<text class="label">设备状态:</text>
<text class="value">{{item.deviceStatusText}}</text>
</view>
<view class="info-row">
<text class="label">预警时间:</text>
<text class="value">{{item.alertTimeText}}</text>
</view>
<view class="info-row">
<text class="label">温度:</text>
<text class="value">{{item.temperature}}°C</text>
</view>
<view class="info-row">
<text class="label">电量:</text>
<text class="value">{{item.battery}}%</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="view-detail">点击查看详情 ></text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">🔔</text>
<text class="empty-text">暂无预警数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
</view>

View File

@@ -0,0 +1,285 @@
/* pages/alert/collar-alert/collar-alert.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 100rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #ff6b35;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #999;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 预警列表 */
.alert-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 预警卡片 */
.alert-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
border-left: 6rpx solid #ff6b35;
}
.alert-card.high {
border-left-color: #ff4444;
}
.alert-card.medium {
border-left-color: #ff9800;
}
.alert-card.low {
border-left-color: #ffeb3b;
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.header-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.alert-type {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.alert-level {
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #fff;
}
.alert-level.level-high {
background-color: #ff4444;
}
.alert-level.level-medium {
background-color: #ff9800;
}
.alert-level.level-low {
background-color: #ffeb3b;
color: #333;
}
.alert-level.level-critical {
background-color: #d32f2f;
}
.collar-number {
font-size: 28rpx;
font-weight: bold;
color: #ff6b35;
}
/* 预警描述 */
.alert-description {
padding: 24rpx;
background-color: #fff8f5;
border-bottom: 1rpx solid #f0f0f0;
}
.alert-description text {
font-size: 28rpx;
color: #ff6b35;
line-height: 1.6;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 180rpx;
flex-shrink: 0;
}
.info-row .value {
color: #333;
flex: 1;
word-break: break-all;
}
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: right;
border-top: 1rpx solid #f0f0f0;
}
.view-detail {
font-size: 26rpx;
color: #ff6b35;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #ff6b35;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #ff6b35;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 53, 0.3);
}

View File

@@ -0,0 +1,151 @@
// pages/alert/eartag-alert-detail/eartag-alert-detail.js
const API = require('../../../utils/api').API
Page({
data: {
alertId: null,
alertData: [],
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('预警详情页面加载,参数:', options)
if (options.id) {
this.setData({ alertId: options.id })
this.loadAlertDetail(options.id)
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载预警详情
*/
async loadAlertDetail(id) {
try {
this.setData({ loading: true })
console.log('获取预警详情ID:', id)
const res = await API.getEartagAlertDetail(id)
console.log('预警详情数据:', res)
// 根据实际返回的数据结构调整
const alertInfo = res.data || res
// 格式化数据为展示格式
const formattedData = this.formatAlertData(alertInfo)
this.setData({
alertData: formattedData,
loading: false
})
} catch (error) {
console.error('加载预警详情失败:', error)
this.setData({ loading: false })
wx.showToast({
title: '加载失败',
icon: 'none'
})
}
},
/**
* 格式化预警数据
*/
formatAlertData(data) {
if (!data) return []
const sections = [
{
title: '预警信息',
items: [
{ label: '预警ID', value: data.id || '-' },
{ label: '预警类型', value: this.getAlertTypeText(data.alertType) },
{ label: '预警等级', value: this.getAlertLevelText(data.alertLevel) },
{ label: '预警时间', value: data.alertTime || '-' },
{ label: '预警描述', value: data.description || '-' }
]
},
{
title: '设备信息',
items: [
{ label: '设备ID', value: data.deviceId || '-' },
{ label: '设备名称', value: data.deviceName || '-' },
{ label: '耳标编号', value: data.eartagNumber || '-' },
{ label: '设备状态', value: data.deviceStatus || '-' }
]
},
{
title: '监测数据',
items: [
{ label: '当前温度', value: data.temperature ? `${data.temperature}°C` : '-' },
{ label: '电池电量', value: data.battery ? `${data.battery}%` : '-' },
{ label: 'GPS信号', value: data.gpsSignal || '-' },
{ label: '运动状态', value: data.movementStatus || '-' }
]
},
{
title: '位置信息',
items: [
{ label: '经度', value: data.longitude || '-' },
{ label: '纬度', value: data.latitude || '-' }
]
},
{
title: '运动数据',
items: [
{ label: '今日步数', value: data.dailySteps || 0 },
{ label: '昨日步数', value: data.yesterdaySteps || 0 },
{ label: '累计步数', value: data.totalSteps || 0 }
]
}
]
return sections
},
/**
* 获取预警等级文本
*/
getAlertLevelText(level) {
const levelMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '紧急'
}
return levelMap[level] || level || '-'
},
/**
* 获取预警类型文本
*/
getAlertTypeText(type) {
const typeMap = {
'temperature': '温度异常',
'battery': '电量异常',
'not_collected': '未采集',
'movement': '运动异常',
'fence': '围栏异常',
'activity_high': '运动量偏高',
'activity_low': '运动量偏低',
'transfer_slow': '传输过慢'
}
return typeMap[type] || type || '-'
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "预警详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,34 @@
<!--pages/alert/eartag-alert-detail/eartag-alert-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 预警详情 -->
<view class="detail-content" wx:else>
<!-- 头部 -->
<view class="detail-header">
<text class="header-icon">🔔</text>
<text class="header-title">耳标预警详情</text>
</view>
<!-- 详细信息 -->
<view class="section" wx:for="{{alertData}}" wx:key="title">
<view class="section-title">{{item.title}}</view>
<view class="section-content">
<view class="info-item" wx:for="{{item.items}}" wx:key="label" wx:for-item="field">
<view class="info-label">{{field.label}}</view>
<view class="info-value">{{field.value}}</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="action-btn resolve">标记已处理</button>
<button class="action-btn ignore">忽略预警</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,124 @@
/* pages/alert/eartag-alert-detail/eartag-alert-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 0 30rpx;
}
/* 头部 */
.detail-header {
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
padding: 40rpx;
margin: 0 -30rpx 30rpx;
text-align: center;
}
.header-icon {
font-size: 80rpx;
display: block;
margin-bottom: 16rpx;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
/* 信息块 */
.section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f1f8f4;
border-bottom: 1rpx solid #c8e6c9;
border-left: 6rpx solid #4caf50;
}
.section-content {
padding: 20rpx;
}
.info-item {
display: flex;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
align-items: flex-start;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
width: 220rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
font-weight: 500;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 20rpx;
padding: 0 30rpx;
margin-top: 40rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
padding: 0;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
border: none;
}
.action-btn::after {
border: none;
}
.action-btn.resolve {
background-color: #4caf50;
color: #fff;
}
.action-btn.ignore {
background-color: #999;
color: #fff;
}

View File

@@ -0,0 +1,253 @@
// pages/alert/eartag-alert/eartag-alert.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: '',
alertType: '' // 预警类型筛选
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
limit: this.data.limit,
search: this.data.searchValue,
alertType: this.data.alertType,
_t: Date.now()
}
console.log('请求参数:', params)
const res = await API.getEartagAlerts(params)
console.log('耳标预警数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化数据
list = list.map(item => {
return {
...item,
// 格式化预警时间
alertTimeText: item.alertTime ? this.formatDateTime(item.alertTime) : '-',
// 预警等级文本
alertLevelText: this.getAlertLevelText(item.alertLevel),
// 预警类型文本
alertTypeText: this.getAlertTypeText(item.alertType),
// 设备状态文本
deviceStatusText: item.deviceStatus || '未知',
// 运动状态文本
movementStatusText: item.movementStatus || '未知'
}
})
const totalPages = pagination.pages || Math.ceil(total / this.data.limit)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
// 如果已经是格式化好的,直接返回
if (typeof dateString === 'string' && dateString.includes('-')) {
return dateString
}
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 获取预警等级文本
*/
getAlertLevelText(level) {
const levelMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '紧急'
}
return levelMap[level] || level || '-'
},
/**
* 获取预警类型文本
*/
getAlertTypeText(type) {
const typeMap = {
'temperature': '温度异常',
'battery': '电量异常',
'not_collected': '未采集',
'movement': '运动异常',
'fence': '围栏异常',
'activity_high': '运动量偏高',
'activity_low': '运动量偏低',
'transfer_slow': '传输过慢'
}
return typeMap[type] || type || '-'
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看预警详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看预警详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/alert/eartag-alert-detail/eartag-alert-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "耳标预警",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,93 @@
<!--pages/alert/eartag-alert/eartag-alert.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入耳标编号或设备名称"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 预警列表 -->
<view class="alert-list" wx:else>
<view class="alert-card {{item.alertLevel}}" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 预警头部 -->
<view class="card-header">
<view class="header-left">
<text class="alert-type">{{item.alertTypeText}}</text>
<text class="alert-level level-{{item.alertLevel}}">{{item.alertLevelText}}</text>
</view>
<text class="eartag-number">{{item.eartagNumber}}</text>
</view>
<!-- 预警描述 -->
<view class="alert-description">
<text>{{item.description}}</text>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">设备名称:</text>
<text class="value">{{item.deviceName}}</text>
</view>
<view class="info-row">
<text class="label">设备状态:</text>
<text class="value">{{item.deviceStatusText}}</text>
</view>
<view class="info-row">
<text class="label">预警时间:</text>
<text class="value">{{item.alertTimeText}}</text>
</view>
<view class="info-row">
<text class="label">温度:</text>
<text class="value">{{item.temperature}}°C</text>
</view>
<view class="info-row">
<text class="label">电量:</text>
<text class="value">{{item.battery}}%</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="view-detail">点击查看详情 ></text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">🔔</text>
<text class="empty-text">暂无预警数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
</view>

View File

@@ -0,0 +1,285 @@
/* pages/alert/eartag-alert/eartag-alert.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 100rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #4caf50;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #999;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 预警列表 */
.alert-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 预警卡片 */
.alert-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
border-left: 6rpx solid #4caf50;
}
.alert-card.high {
border-left-color: #ff4444;
}
.alert-card.medium {
border-left-color: #ff9800;
}
.alert-card.low {
border-left-color: #ffeb3b;
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.header-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.alert-type {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.alert-level {
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #fff;
}
.alert-level.level-high {
background-color: #ff4444;
}
.alert-level.level-medium {
background-color: #ff9800;
}
.alert-level.level-low {
background-color: #ffeb3b;
color: #333;
}
.alert-level.level-critical {
background-color: #d32f2f;
}
.eartag-number {
font-size: 28rpx;
font-weight: bold;
color: #4caf50;
}
/* 预警描述 */
.alert-description {
padding: 24rpx;
background-color: #f1f8f4;
border-bottom: 1rpx solid #f0f0f0;
}
.alert-description text {
font-size: 28rpx;
color: #2e7d32;
line-height: 1.6;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 180rpx;
flex-shrink: 0;
}
.info-row .value {
color: #333;
flex: 1;
word-break: break-all;
}
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: right;
border-top: 1rpx solid #f0f0f0;
}
.view-detail {
font-size: 26rpx;
color: #4caf50;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #4caf50;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #4caf50;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(76, 175, 80, 0.3);
}

View File

@@ -1,3 +1,3 @@
{
"usingComponents": {}
{
"usingComponents": {}
}

View File

@@ -0,0 +1,282 @@
// pages/cattle/archive/archive.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 可选:每次显示页面时刷新数据
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
pageSize: this.data.pageSize,
_t: Date.now()
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.earNumber = this.data.searchValue.trim() // 按耳号精确查询
}
console.log('请求参数:', params)
const res = await API.getCattleList(params)
console.log('牛只档案数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化时间戳和数据
list = list.map(item => {
return {
...item,
// 格式化出生日期
birthdayText: item.birthday ? this.formatDate(item.birthday) : '-',
// 格式化入场时间
intoTimeText: item.intoTime ? this.formatDate(item.intoTime) : '-',
// 格式化性别
sexText: item.sex === 1 ? '公' : item.sex === 2 ? '母' : '未知',
// 格式化生理阶段
physiologicalStageText: this.getPhysiologicalStage(item.physiologicalStage),
// 格式化来源
sourceText: item.source === 1 ? '自繁' : item.source === 2 ? '外购' : '未知',
// 格式化体重计算时间
weightCalculateTimeText: item.weightCalculateTime ? this.formatDateTime(item.weightCalculateTime) : '-'
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const earNumber = String(item.earNumber || '')
// 精确匹配:完全相等
return earNumber === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的牛只',
icon: 'none',
duration: 2000
})
}
}
const totalPages = pagination.pages || Math.ceil((this.data.searchValue ? list.length : total) / this.data.pageSize)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期(时间戳转日期)
*/
formatDate(timestamp) {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
/**
* 获取生理阶段文本
*/
getPhysiologicalStage(stage) {
const stages = {
0: '犊牛期',
1: '育成期',
2: '成年期',
3: '老年期'
}
return stages[stage] || '未知'
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看牛只详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看牛只详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/cattle/detail/detail?id=${id}`
})
},
/**
* 新增档案
*/
addCattle() {
wx.showToast({
title: '新增功能开发中',
icon: 'none'
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "牛只档案",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f6f6f6"
}

View File

@@ -0,0 +1,97 @@
<!--pages/cattle/archive/archive.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入耳号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="number"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 牛只列表 -->
<view class="cattle-list" wx:else>
<view class="cattle-card" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 耳号 -->
<view class="card-header">
<text class="ear-number">耳号:{{item.earNumber}}</text>
<text class="arrow"></text>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">佩戴设备:</text>
<text class="value">{{item.earNumber}}</text>
</view>
<view class="info-row">
<text class="label">出生日期:</text>
<text class="value">{{item.birthdayText}}</text>
</view>
<view class="info-row">
<text class="label">品类:</text>
<text class="value">{{item.cate || '-'}}</text>
</view>
<view class="info-row">
<text class="label">品种:</text>
<text class="value">{{item.varieties || '-'}}</text>
</view>
<view class="info-row">
<text class="label">生理阶段:</text>
<text class="value">{{item.physiologicalStageText}}</text>
</view>
<view class="info-row">
<text class="label">性别:</text>
<text class="value">{{item.sexText}}</text>
</view>
<view class="info-row">
<text class="label">栏舍:</text>
<text class="value">{{item.penName || '-'}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">🐄</text>
<text class="empty-text">暂无牛只档案数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
<!-- 数据统计 -->
<view class="stats-bar" wx:if="{{!loading}}">
<text class="stats-text">共 {{total}} 头牛,当前第 {{page}}/{{totalPages}} 页</text>
</view>
<!-- 新增档案按钮 -->
<view class="add-btn" bindtap="addCattle">
<text>新增档案</text>
</view>
</view>

View File

@@ -0,0 +1,246 @@
/* pages/cattle/archive/archive.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
/* 搜索栏样式 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #fa5151;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 牛只列表 */
.cattle-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 牛只卡片 */
.cattle-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.ear-number {
font-size: 32rpx;
font-weight: bold;
color: #07c160;
}
.arrow {
font-size: 48rpx;
color: #ccc;
font-weight: 300;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: center;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 160rpx;
}
.info-row .value {
color: #333;
flex: 1;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页样式 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #07c160;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #07c160;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
/* 数据统计 */
.stats-bar {
text-align: center;
padding: 20rpx;
margin-top: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.stats-text {
font-size: 26rpx;
color: #666;
}
/* 新增按钮 */
.add-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
width: 90%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}

View File

@@ -0,0 +1,146 @@
// pages/cattle/batch-detail/batch-detail.js
const API = require('../../../utils/api').API
Page({
data: {
batchId: null,
batchData: [],
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
console.log('批次详情页面加载,参数:', options)
if (options.id) {
this.setData({ batchId: options.id })
this.loadBatchDetail(options.id)
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载批次详情
*/
async loadBatchDetail(id) {
try {
this.setData({ loading: true })
console.log('获取批次详情ID:', id)
const res = await API.getBatchDetail(id)
console.log('批次详情数据:', res)
// 根据实际返回的数据结构调整
const batchInfo = res.data || res
// 格式化数据为展示格式
const formattedData = this.formatBatchData(batchInfo)
this.setData({
batchData: formattedData,
loading: false
})
} catch (error) {
console.error('加载批次详情失败:', error)
this.setData({ loading: false })
wx.showToast({
title: '加载失败',
icon: 'none'
})
}
},
/**
* 格式化批次数据
*/
formatBatchData(data) {
if (!data) return []
const sections = [
{
title: '基本信息',
items: [
{ label: 'ID', value: data.id || '-' },
{ label: '批次名称', value: data.name || '-' },
{ label: '批次编码', value: data.code || '-' },
{ label: '批次类型', value: data.type || '-' },
{ label: '状态', value: data.status || '-' }
]
},
{
title: '数量信息',
items: [
{ label: '当前数量', value: data.currentCount || 0 },
{ label: '目标数量', value: data.targetCount || 0 }
]
},
{
title: '时间信息',
items: [
{ label: '开始日期', value: this.formatDate(data.startDate) },
{ label: '预期结束日期', value: this.formatDate(data.expectedEndDate) },
{ label: '实际结束日期', value: this.formatDate(data.actualEndDate) }
]
},
{
title: '管理信息',
items: [
{ label: '养殖场', value: data.farm?.name || data.farmName || '-' },
{ label: '养殖场ID', value: data.farmId || data.farm_id || '-' },
{ label: '管理人员', value: data.manager || '-' }
]
},
{
title: '其他信息',
items: [
{ label: '备注', value: data.remark || '-' },
{ label: '创建时间', value: this.formatDateTime(data.created_at) },
{ label: '更新时间', value: this.formatDateTime(data.updated_at) }
]
}
]
return sections
},
/**
* 格式化日期
*/
formatDate(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "批次详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,27 @@
<!--pages/cattle/batch-detail/batch-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 批次详情 -->
<view class="detail-content" wx:else>
<!-- 头部 -->
<view class="detail-header">
<text class="header-title">批次设置详情</text>
</view>
<!-- 详细信息 -->
<view class="section" wx:for="{{batchData}}" wx:key="title">
<view class="section-title">{{item.title}}</view>
<view class="section-content">
<view class="info-item" wx:for="{{item.items}}" wx:key="label" wx:for-item="field">
<view class="info-label">{{field.label}}</view>
<view class="info-value">{{field.value}}</view>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,84 @@
/* pages/cattle/batch-detail/batch-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 0 30rpx;
}
/* 头部 */
.detail-header {
background: linear-gradient(135deg, #07c160 0%, #09a856 100%);
padding: 40rpx;
margin: 0 -30rpx 30rpx;
text-align: center;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
/* 信息块 */
.section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f8f9fa;
border-bottom: 1rpx solid #eee;
}
.section-content {
padding: 20rpx;
}
.info-item {
display: flex;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
align-items: flex-start;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
width: 220rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
font-weight: 500;
}

View File

@@ -0,0 +1,244 @@
// pages/cattle/batch/batch.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
pageSize: this.data.pageSize,
exactMatch: true,
strictMatch: true,
_t: Date.now()
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.name = this.data.searchValue.trim() // 按批次名称精确查询
params.search = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getBatchList(params)
console.log('批次设置数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化时间和数据
list = list.map(item => {
return {
...item,
// 格式化开始日期
startDateText: item.startDate ? this.formatDateTime(item.startDate) : '-',
// 格式化预期结束日期
expectedEndDateText: item.expectedEndDate ? this.formatDateTime(item.expectedEndDate) : '-',
// 格式化实际结束日期
actualEndDateText: item.actualEndDate ? this.formatDateTime(item.actualEndDate) : '-',
// 格式化创建时间
createdAtText: item.created_at ? this.formatDateTime(item.created_at) : '-',
// 格式化更新时间
updatedAtText: item.updated_at ? this.formatDateTime(item.updated_at) : '-',
// 养殖场名称
farmName: item.farm?.name || '-'
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const name = String(item.name || '')
// 精确匹配:完全相等
return name === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的批次',
icon: 'none',
duration: 2000
})
}
}
const totalPages = pagination.pages || Math.ceil((this.data.searchValue ? list.length : total) / this.data.pageSize)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看批次详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看批次详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/cattle/batch-detail/batch-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "批次设置",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,90 @@
<!--pages/cattle/batch/batch.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入批次号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 批次列表 -->
<view class="batch-list" wx:else>
<view class="batch-card" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 批次号 -->
<view class="card-header">
<text class="batch-number">批次号:{{item.name}}</text>
<button class="edit-btn" size="mini" catchtap="editBatch" data-id="{{item.id}}">编辑</button>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">种类:</text>
<text class="value">{{item.type || '牛'}}</text>
</view>
<view class="info-row">
<text class="label">批次内数量:</text>
<text class="value">{{item.currentCount || 0}}</text>
</view>
<view class="info-row">
<text class="label">最后操作时间:</text>
<text class="value">{{item.updatedAtText}}</text>
</view>
<view class="info-row">
<text class="label">创建人:</text>
<text class="value">{{item.manager || 'admin'}}</text>
</view>
<view class="info-row">
<text class="label">备注:</text>
<text class="value">{{item.remark || '--'}}</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="page-indicator">{{page}}/{{totalPages}}</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无批次设置数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
<!-- 新增批次按钮 -->
<view class="add-btn">
<text>新增批次</text>
</view>
</view>

View File

@@ -0,0 +1,254 @@
/* pages/cattle/batch/batch.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #fa5151;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 批次列表 */
.batch-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 批次卡片 */
.batch-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.batch-number {
font-size: 32rpx;
font-weight: bold;
color: #07c160;
}
.edit-btn {
padding: 8rpx 24rpx;
background-color: #07c160;
color: #fff;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
}
.edit-btn::after {
border: none;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 220rpx;
flex-shrink: 0;
}
.info-row .value {
color: #333;
flex: 1;
word-break: break-all;
}
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: center;
border-top: 1rpx solid #f0f0f0;
}
.page-indicator {
font-size: 24rpx;
color: #999;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #07c160;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #07c160;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
/* 新增批次按钮 */
.add-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
width: 90%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}

View File

@@ -1,66 +1,66 @@
<!-- 牛只批次设置页面 -->
<view class="page-container">
<!-- 顶部搜索区域 -->
<view class="search-bar">
<input class="search-input" placeholder="请输入批次名称或编号(精确匹配)" value="{{search}}" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" />
<button class="search-btn" bindtap="onSearch">查询</button>
</view>
<!-- 统计与分页信息 -->
<view class="summary-bar">
<text>总数:{{total}}</text>
<text>第 {{page}} / {{totalPages}} 页</text>
</view>
<!-- 批次列表 -->
<scroll-view scroll-y class="list-scroll">
<block wx:if="{{loading}}">
<view class="loading">加载中...</view>
</block>
<block wx:if="{{!loading && records.length === 0}}">
<view class="empty">暂无数据</view>
</block>
<block wx:for="{{records}}" wx:key="id">
<view class="record-card">
<!-- 头部主信息 -->
<view class="record-header">
<view class="title-line">
<text class="name">{{item.name || item.batch_name || item.batchName || '-'}}</text>
<text class="code">编号:{{item.code || item.batch_number || item.batchNumber || '-'}}</text>
</view>
<view class="meta-line">
<text class="status" wx:if="{{item.statusStr}}">状态:{{item.statusStr}}</text>
<text class="time" wx:if="{{item.createdAtStr}}">创建时间:{{item.createdAtStr}}</text>
</view>
</view>
<!-- 详情字段(显示全部返回字段,中文映射) -->
<view class="record-body">
<block wx:for="{{item.displayPairs}}" wx:key="key" wx:for-item="pair">
<view class="pair-row">
<text class="pair-key">{{pair.keyZh}}</text>
<text class="pair-val">{{pair.val}}</text>
</view>
</block>
</view>
</view>
</block>
</scroll-view>
<!-- 分页 -->
<view class="pagination">
<button class="page-btn" bindtap="prevPage" disabled="{{page <= 1}}">上一页</button>
<scroll-view scroll-x class="page-numbers">
<view class="page-items">
<block wx:for="{{pages}}" wx:key="index">
<view class="page-item {{current ? 'active' : ''}}" data-page="{{num}}" bindtap="goToPage">
<text>{{num}}</text>
</view>
</block>
</view>
</scroll-view>
<button class="page-btn" bindtap="nextPage" disabled="{{page >= totalPages}}">下一页</button>
</view>
<!-- 牛只批次设置页面 -->
<view class="page-container">
<!-- 顶部搜索区域 -->
<view class="search-bar">
<input class="search-input" placeholder="请输入批次名称或编号(精确匹配)" value="{{search}}" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" />
<button class="search-btn" bindtap="onSearch">查询</button>
</view>
<!-- 统计与分页信息 -->
<view class="summary-bar">
<text>总数:{{total}}</text>
<text>第 {{page}} / {{totalPages}} 页</text>
</view>
<!-- 批次列表 -->
<scroll-view scroll-y class="list-scroll">
<block wx:if="{{loading}}">
<view class="loading">加载中...</view>
</block>
<block wx:if="{{!loading && records.length === 0}}">
<view class="empty">暂无数据</view>
</block>
<block wx:for="{{records}}" wx:key="id">
<view class="record-card">
<!-- 头部主信息 -->
<view class="record-header">
<view class="title-line">
<text class="name">{{item.name || item.batch_name || item.batchName || '-'}}</text>
<text class="code">编号:{{item.code || item.batch_number || item.batchNumber || '-'}}</text>
</view>
<view class="meta-line">
<text class="status" wx:if="{{item.statusStr}}">状态:{{item.statusStr}}</text>
<text class="time" wx:if="{{item.createdAtStr}}">创建时间:{{item.createdAtStr}}</text>
</view>
</view>
<!-- 详情字段(显示全部返回字段,中文映射) -->
<view class="record-body">
<block wx:for="{{item.displayPairs}}" wx:key="key" wx:for-item="pair">
<view class="pair-row">
<text class="pair-key">{{pair.keyZh}}</text>
<text class="pair-val">{{pair.val}}</text>
</view>
</block>
</view>
</view>
</block>
</scroll-view>
<!-- 分页 -->
<view class="pagination">
<button class="page-btn" bindtap="prevPage" disabled="{{page <= 1}}">上一页</button>
<scroll-view scroll-x class="page-numbers">
<view class="page-items">
<block wx:for="{{pages}}" wx:key="index">
<view class="page-item {{current ? 'active' : ''}}" data-page="{{num}}" bindtap="goToPage">
<text>{{num}}</text>
</view>
</block>
</view>
</scroll-view>
<button class="page-btn" bindtap="nextPage" disabled="{{page >= totalPages}}">下一页</button>
</view>
</view>

View File

@@ -1,119 +1,119 @@
/* 页面容器 */
.page-container {
padding: 12rpx 16rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.search-input {
flex: 1;
height: 64rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 0 12rpx;
background: #fff;
}
.search-btn {
height: 64rpx;
padding: 0 20rpx;
}
/* 概览栏 */
.summary-bar {
display: flex;
justify-content: space-between;
color: #666;
font-size: 26rpx;
margin-bottom: 12rpx;
}
/* 列表滚动区 */
.list-scroll {
max-height: calc(100vh - 280rpx);
}
.loading, .empty {
text-align: center;
color: #999;
padding: 24rpx 0;
}
/* 卡片 */
.record-card {
background: #fff;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.06);
}
.record-header .title-line {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.record-header .name {
font-weight: 600;
font-size: 30rpx;
}
.record-header .code {
color: #333;
}
.record-header .meta-line {
display: flex;
gap: 20rpx;
color: #666;
font-size: 26rpx;
}
.record-body .pair-row {
display: flex;
justify-content: space-between;
padding: 8rpx 0;
border-bottom: 1px dashed #eee;
}
.pair-key {
color: #888;
}
.pair-val {
color: #333;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12rpx;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
}
.page-numbers {
width: 60%;
}
.page-items {
display: flex;
gap: 12rpx;
}
.page-item {
min-width: 56rpx;
height: 56rpx;
line-height: 56rpx;
text-align: center;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 0 12rpx;
color: #333;
}
.page-item.active {
background: #3cc51f;
color: #fff;
border-color: #3cc51f;
}
.page-btn[disabled] {
opacity: 0.5;
/* 页面容器 */
.page-container {
padding: 12rpx 16rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.search-input {
flex: 1;
height: 64rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 0 12rpx;
background: #fff;
}
.search-btn {
height: 64rpx;
padding: 0 20rpx;
}
/* 概览栏 */
.summary-bar {
display: flex;
justify-content: space-between;
color: #666;
font-size: 26rpx;
margin-bottom: 12rpx;
}
/* 列表滚动区 */
.list-scroll {
max-height: calc(100vh - 280rpx);
}
.loading, .empty {
text-align: center;
color: #999;
padding: 24rpx 0;
}
/* 卡片 */
.record-card {
background: #fff;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.06);
}
.record-header .title-line {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.record-header .name {
font-weight: 600;
font-size: 30rpx;
}
.record-header .code {
color: #333;
}
.record-header .meta-line {
display: flex;
gap: 20rpx;
color: #666;
font-size: 26rpx;
}
.record-body .pair-row {
display: flex;
justify-content: space-between;
padding: 8rpx 0;
border-bottom: 1px dashed #eee;
}
.pair-key {
color: #888;
}
.pair-val {
color: #333;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12rpx;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
}
.page-numbers {
width: 60%;
}
.page-items {
display: flex;
gap: 12rpx;
}
.page-item {
min-width: 56rpx;
height: 56rpx;
line-height: 56rpx;
text-align: center;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 0 12rpx;
color: #333;
}
.page-item.active {
background: #3cc51f;
color: #fff;
border-color: #3cc51f;
}
.page-btn[disabled] {
opacity: 0.5;
}

View File

@@ -1,421 +1,73 @@
// pages/cattle/cattle.js
const { get, del } = require('../../utils/api')
const { formatDate, formatTime } = require('../../utils/index')
// pages/cattle/cattle.js - 生产管理/牛羊管理页面
Page({
data: {
cattleList: [],
loading: false,
refreshing: false,
searchKeyword: '',
// 设备编号精确查询
deviceNumber: '',
statusFilter: 'all',
page: 1,
// 按需求使用每页10条
pageSize: 10,
hasMore: true,
total: 0,
// 分页页码集合
pages: [],
lastPage: 1
// 牛只管理功能列表
cattleFunctions: [
{ id: 1, name: '牛档案', iconText: '📁', color: 'icon-cyan', url: '/pages/cattle/archive/archive' },
{ id: 2, name: '发情记录', iconText: '💗', color: 'icon-orange', url: '/pages/cattle/estrus/estrus' },
{ id: 3, name: '配种记录', iconText: '🔬', color: 'icon-blue', url: '/pages/cattle/breeding/breeding' },
{ id: 4, name: '妊娠记录', iconText: '🤰', color: 'icon-yellow', url: '/pages/cattle/pregnancy/pregnancy' },
{ id: 5, name: '分娩记录', iconText: '👶', color: 'icon-green', url: '/pages/cattle/birth/birth' },
{ id: 6, name: '断奶记录', iconText: '🍼', color: 'icon-purple', url: '/pages/cattle/weaning/weaning' },
{ id: 7, name: '转栏记录', iconText: '🔄', color: 'icon-red', url: '/pages/cattle/transfer/transfer' },
{ id: 8, name: '离栏记录', iconText: '🚪', color: 'icon-teal', url: '/pages/cattle/exit/exit' },
{ id: 9, name: '栏舍设置', iconText: '🏠', color: 'icon-indigo', url: '/pages/cattle/pen/pen' },
{ id: 10, name: '批次设置', iconText: '📦', color: 'icon-pink', url: '/pages/cattle/batch/batch' },
{ id: 11, name: '防疫预警', iconText: '💉', color: 'icon-lime', url: '/pages/cattle/vaccination/vaccination' }
],
// 猪只管理功能列表
pigFunctions: [
{ id: 1, name: '猪档案', iconText: '📁', color: 'icon-cyan', url: '/pages/pig/archive/archive' },
{ id: 2, name: '发情记录', iconText: '💗', color: 'icon-orange', url: '/pages/pig/estrus/estrus' },
{ id: 3, name: '配种记录', iconText: '🔬', color: 'icon-blue', url: '/pages/pig/breeding/breeding' },
{ id: 4, name: '妊娠记录', iconText: '🤰', color: 'icon-yellow', url: '/pages/pig/pregnancy/pregnancy' },
{ id: 5, name: '分娩记录', iconText: '👶', color: 'icon-green', url: '/pages/pig/birth/birth' },
{ id: 6, name: '断奶记录', iconText: '🍼', color: 'icon-purple', url: '/pages/pig/weaning/weaning' },
{ id: 7, name: '转栏记录', iconText: '🔄', color: 'icon-red', url: '/pages/pig/transfer/transfer' },
{ id: 8, name: '离栏记录', iconText: '🚪', color: 'icon-teal', url: '/pages/pig/exit/exit' },
{ id: 9, name: '栏舍设置', iconText: '🏠', color: 'icon-indigo', url: '/pages/pig/pens/pens' },
{ id: 10, name: '批次设置', iconText: '📦', color: 'icon-pink', url: '/pages/pig/batches/batches' },
{ id: 11, name: '防疫预警', iconText: '💉', color: 'icon-lime', url: '/pages/pig/vaccination/vaccination' }
]
},
onLoad(options) {
// 获取筛选参数
if (options.status) {
this.setData({ statusFilter: options.status })
}
this.loadCattleList()
onLoad() {
console.log('牛羊管理页面加载')
},
onShow() {
this.loadCattleList()
console.log('牛羊管理页面显示')
},
onPullDownRefresh() {
this.setData({
page: 1,
hasMore: true,
cattleList: []
})
this.loadCattleList().then(() => {
// 下拉刷新
setTimeout(() => {
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,
pageNo: this.data.page,
pageIndex: this.data.page,
current: this.data.page,
pageSize: this.data.pageSize,
size: this.data.pageSize,
limit: this.data.pageSize,
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
}
if (this.data.searchKeyword) {
params.search = this.data.searchKeyword
}
// 设备编号精确查询参数(尽可能兼容后端不同命名)
if (this.data.deviceNumber) {
params.deviceNumber = this.data.deviceNumber
params.deviceSn = this.data.deviceNumber
params.deviceId = this.data.deviceNumber
// 部分接口可能支持exact开关
params.exact = true
}
const response = await get('/iot-cattle/public', params)
// 统一兼容响应结构(尽可能兼容不同后端命名)
const list = (response && response.data && (response.data.list || response.data.records || response.data.items))
|| (response && (response.list || response.records || response.items))
|| []
const totalRaw = (response && response.data && (response.data.total ?? response.data.totalCount ?? response.data.count))
|| (response && (response.total ?? response.totalCount ?? response.count))
|| (response && response.page && response.page.total)
|| 0
const totalPagesOverride = (response && response.data && (response.data.totalPages ?? response.data.pageCount))
|| (response && (response.totalPages ?? response.pageCount))
|| (response && response.page && (response.page.totalPages ?? response.page.pageCount))
|| 0
const total = Number(totalRaw) || 0
const isUnknownTotal = !(Number(totalPagesOverride) > 0) && !(Number(totalRaw) > 0)
console.log('牛只档案接口原始响应:', response)
const mappedList = list.map(this.mapCattleRecord)
console.log('牛只档案字段映射结果(当前页):', mappedList)
const cattleList = this.data.page === 1 ? mappedList : [...this.data.cattleList, ...mappedList]
// 根据总页数判断是否还有更多兼容后端直接返回totalPages/pageCount
// 当后端未返回 total/totalPages 时,使用“本页数据条数 == pageSize”作为是否还有更多的兜底策略。
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / this.data.pageSize))
const hasMore = isUnknownTotal ? (mappedList.length >= this.data.pageSize) : (this.data.page < totalPages)
this.setData({ cattleList, total, hasMore })
console.log('分页计算:', { total, pageSize: this.data.pageSize, totalPages, currentPage: this.data.page, isUnknownTotal })
// 生成分页页码
this.buildPagination(total, totalPages, isUnknownTotal)
} catch (error) {
console.error('获取牛只列表失败:', error)
wx.showToast({
title: '获取数据失败',
icon: 'none'
title: '刷新成功',
icon: 'success'
})
} finally {
this.setData({ loading: false })
}
}, 1000)
},
// 加载更多牛只
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
})
},
// 设备编号输入(精确查询)
onDeviceInput(e) {
this.setData({
deviceNumber: e.detail.value.trim()
})
},
// 执行搜索
onSearch() {
this.setData({
page: 1,
hasMore: true,
cattleList: []
})
this.loadCattleList()
},
// 执行设备编号精确查询
onDeviceSearch() {
if (!this.data.deviceNumber) {
wx.showToast({ title: '请输入设备编号', icon: 'none' })
return
}
this.setData({
page: 1,
hasMore: true,
cattleList: [],
// 精确查询时不使用模糊关键词
searchKeyword: ''
})
this.loadCattleList()
},
// 清空搜索
onClearSearch() {
this.setData({
searchKeyword: '',
deviceNumber: '',
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) {
// 导航到功能页面
navigateTo(e) {
const url = e.currentTarget.dataset.url
if (url) {
wx.navigateTo({
url,
fail: () => {
wx.showToast({
title: '删除成功',
icon: 'success'
})
// 刷新列表
this.setData({
page: 1,
hasMore: true,
cattleList: []
})
this.loadCattleList()
} else {
wx.showToast({
title: response.message || '删除失败',
title: '功能开发中',
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)
},
// 字段中文映射与安全处理
mapCattleRecord(item = {}) {
// 统一时间戳(后端可能返回秒级或毫秒级)
const normalizeTs = (ts) => {
if (ts === null || ts === undefined || ts === '') return ''
if (typeof ts === 'number') {
return ts < 1000000000000 ? ts * 1000 : ts
}
// 字符串数字
const n = Number(ts)
if (!Number.isNaN(n)) return n < 1000000000000 ? n * 1000 : n
return ts
}
// 性别映射(仅做友好展示,保留原值)
const rawSex = item.sex ?? item.gender
const sexText = rawSex === 1 || rawSex === '1' ? '公' : (rawSex === 2 || rawSex === '2' ? '母' : (rawSex ?? '-'))
return {
// 基本标识
id: item.id ?? item.cattleId ?? item._id ?? '',
name: item.name ?? item.cattleName ?? '',
earNumber: item.earNumber ?? item.earNo ?? item.earTag ?? '-',
// 基本属性
breed: item.breed ?? item.breedName ?? item.varieties ?? '-',
strain: item.strain ?? '-',
varieties: item.varieties ?? '-',
cate: item.cate ?? '-',
gender: rawSex ?? '-',
genderText: sexText,
age: item.age ?? item.ageYear ?? '-',
ageInMonths: item.ageInMonths ?? item.ageMonth ?? '-',
// 体重与计算
weight: item.weight ?? item.currentWeight ?? '-',
currentWeight: item.currentWeight ?? '-',
birthWeight: item.birthWeight ?? '-',
sourceWeight: item.sourceWeight ?? '-',
weightCalculateTime: item.weightCalculateTime ? formatDate(normalizeTs(item.weightCalculateTime), 'YYYY-MM-DD HH:mm:ss') : '',
// 来源信息
source: item.source ?? '-',
sourceDay: item.sourceDay ?? '-',
// 关联位置与组织
deviceNumber: item.deviceNumber ?? item.deviceSn ?? item.deviceId ?? '-',
penId: item.penId ?? '-',
penName: item.penName ?? item.barnName ?? '-',
batchId: item.batchId ?? '-',
batchName: item.batchName ?? '-',
farmId: item.farmId ?? '-',
farmName: item.farmName ?? '-',
// 生育与阶段
parity: item.parity ?? '-',
physiologicalStage: item.physiologicalStage ?? '-',
status: item.status ?? item.cattleStatus ?? 'normal',
// 重要日期
birthday: item.birthday ?? item.birthDate ?? item.bornDate ?? item.birthTime ?? '',
birthdayStr: item.birthday || item.birthDate || item.bornDate || item.birthTime
? formatDate(normalizeTs(item.birthday ?? item.birthDate ?? item.bornDate ?? item.birthTime))
: '',
dayOfBirthday: item.dayOfBirthday ?? '-',
intoTime: item.intoTime ?? '',
intoTimeStr: item.intoTime ? formatDate(normalizeTs(item.intoTime)) : ''
}
},
// 生成分页页码,控制展示范围并高亮当前页
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
const pageSize = this.data.pageSize
const current = this.data.page
// 当总数未知时按已加载的当前页生成页码1..current允许继续“下一页”。
if (isUnknownTotal) {
const pages = Array.from({ length: Math.max(1, current) }, (_, i) => i + 1)
this.setData({ pages, lastPage: current })
return
}
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / pageSize))
let pages = []
const maxVisible = 9
if (totalPages <= maxVisible) {
pages = Array.from({ length: totalPages }, (_, i) => i + 1)
})
} else {
// 滑动窗口
let start = Math.max(1, current - 4)
let end = Math.min(totalPages, start + maxVisible - 1)
// 保证区间长度
start = Math.max(1, end - maxVisible + 1)
pages = Array.from({ length: end - start + 1 }, (_, i) => start + i)
wx.showToast({
title: '功能开发中',
icon: 'none'
})
}
this.setData({ pages, lastPage: totalPages })
},
// 上一页
onPrevPage() {
if (this.data.page <= 1) return
this.setData({ page: this.data.page - 1, cattleList: [] })
this.loadCattleList()
},
// 下一页
onNextPage() {
if (!this.data.hasMore) return
this.setData({ page: this.data.page + 1, cattleList: [] })
this.loadCattleList()
},
// 切换页码
onPageTap(e) {
const targetPage = Number(e.currentTarget.dataset.page)
if (!targetPage || targetPage === this.data.page) return
this.setData({ page: targetPage, cattleList: [] })
this.loadCattleList()
}
})

View File

@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "生产管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

View File

@@ -1,159 +1,39 @@
<!--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>
<!-- pages/cattle/cattle.wxml - 生产管理/牛羊管理页面 -->
<view class="page-container">
<scroll-view class="scroll-content" scroll-y>
<!-- 牛只管理区块 -->
<view class="section">
<view class="section-header">
<view class="header-line"></view>
<text class="section-title">牛只管理</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-extra">
<view class="extra-row"><text class="extra-label">性别:</text><text class="extra-value">{{item.genderText}}</text></view>
<view class="extra-row"><text class="extra-label">血统:</text><text class="extra-value">{{item.strain || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">品系:</text><text class="extra-value">{{item.varieties || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">品类:</text><text class="extra-value">{{item.cate || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">年龄(月):</text><text class="extra-value">{{item.ageInMonths || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">出生日期:</text><text class="extra-value">{{item.birthdayStr || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">出生体重(kg):</text><text class="extra-value">{{item.birthWeight || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">当前体重(kg):</text><text class="extra-value">{{item.currentWeight || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">来源:</text><text class="extra-value">{{item.source || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">来源天数:</text><text class="extra-value">{{item.sourceDay || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">来源体重(kg):</text><text class="extra-value">{{item.sourceWeight || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">批次:</text><text class="extra-value">{{item.batchName || '-'}}(ID:{{item.batchId || '-'}})</text></view>
<view class="extra-row"><text class="extra-label">农场:</text><text class="extra-value">{{item.farmName || '-'}}(ID:{{item.farmId || '-'}})</text></view>
<view class="extra-row"><text class="extra-label">栏舍:</text><text class="extra-value">{{item.penName || '-'}}(ID:{{item.penId || '-'}})</text></view>
<view class="extra-row"><text class="extra-label">进场日期:</text><text class="extra-value">{{item.intoTimeStr || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">胎次:</text><text class="extra-value">{{item.parity || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">生理阶段:</text><text class="extra-value">{{item.physiologicalStage || '-'}} </text></view>
<view class="extra-row"><text class="extra-label">体重计算时间:</text><text class="extra-value">{{item.weightCalculateTime || '-'}} </text></view>
</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 class="function-grid">
<view class="function-item" wx:for="{{cattleFunctions}}" wx:key="id" bindtap="navigateTo" data-url="{{item.url}}">
<view class="icon-box {{item.color}}">
<text class="item-icon-text">{{item.iconText}}</text>
</view>
<text class="item-label">{{item.name}}</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 class="section">
<view class="section-header">
<view class="header-line"></view>
<text class="section-title">猪只管理</text>
</view>
<view class="function-grid">
<view class="function-item" wx:for="{{pigFunctions}}" wx:key="id" bindtap="navigateTo" data-url="{{item.url}}">
<view class="icon-box {{item.color}}">
<text class="item-icon-text">{{item.iconText}}</text>
</view>
<text class="item-label">{{item.name}}</text>
</view>
</view>
</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 wx:if="{{cattleList.length > 0}}" class="pagination">
<view class="page-item {{page <= 1 ? 'disabled' : ''}}" bindtap="onPrevPage">上一页</view>
<view class="page-item" data-page="1" bindtap="onPageTap">首页</view>
<view
wx:for="{{pages}}"
wx:key="*this"
class="page-item {{item === page ? 'active' : ''}}"
data-page="{{item}}"
bindtap="onPageTap"
>{{item}}</view>
<view class="page-item" data-page="{{lastPage}}" bindtap="onPageTap">末页</view>
<view class="page-item {{page >= lastPage ? 'disabled' : ''}}" bindtap="onNextPage">下一页</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 class="bottom-space"></view>
</scroll-view>
</view>

View File

@@ -1,365 +1,138 @@
/* pages/cattle/cattle.wxss */
.cattle-container {
background-color: #f6f6f6;
min-height: 100vh;
padding-bottom: 120rpx;
/* pages/cattle/cattle.wxss - 生产管理/牛羊管理样式 */
page {
background-color: #f5f5f5;
height: 100%;
}
.search-bar {
.page-container {
display: flex;
align-items: center;
padding: 16rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
flex-direction: column;
height: 100%;
background-color: #f5f5f5;
}
.search-input-wrapper {
.scroll-content {
flex: 1;
position: relative;
margin-right: 16rpx;
padding: 20rpx 0;
}
.search-input {
width: 100%;
height: 72rpx;
background-color: #f5f5f5;
border-radius: 36rpx;
padding: 0 60rpx 0 24rpx;
font-size: 28rpx;
color: #303133;
/* 区块样式 */
.section {
margin-bottom: 30rpx;
}
.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 {
.section-header {
display: flex;
align-items: center;
margin: 0 30rpx 30rpx;
}
.header-line {
width: 8rpx;
height: 32rpx;
background: linear-gradient(180deg, #52c41a 0%, #73d13d 100%);
border-radius: 4rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
}
/* 功能网格 */
.function-grid {
display: flex;
flex-wrap: wrap;
padding: 0 20rpx;
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;
margin: 0 20rpx;
border-radius: 16rpx;
padding: 20rpx 10rpx;
}
.cattle-item:active {
transform: scale(0.98);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
.function-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.cattle-avatar {
width: 80rpx;
height: 80rpx;
background-color: #f0f9ff;
.icon-box {
width: 96rpx;
height: 96rpx;
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-extra {
margin-top: 8rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6rpx 12rpx;
}
.extra-row {
display: flex;
align-items: center;
font-size: 22rpx;
}
.extra-label {
color: #909399;
margin-right: 8rpx;
}
.extra-value {
color: #303133;
}
.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;
}
/* 分页导航 */
.pagination {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
align-items: center;
justify-content: center;
padding: 20rpx 16rpx 40rpx;
}
.page-item {
min-width: 60rpx;
padding: 12rpx 20rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
color: #606266;
font-size: 26rpx;
text-align: center;
}
.page-item.active {
background-color: #3cc51f;
color: #ffffff;
}
/* 分页禁用态 */
.page-item.disabled {
opacity: 0.5;
pointer-events: none;
}
.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 {
.item-icon {
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;
.item-icon-text {
font-size: 48rpx;
line-height: 1;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
.item-label {
font-size: 24rpx;
color: #333333;
text-align: center;
line-height: 1.4;
}
/* 响应式设计 */
@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;
}
/* 图标背景颜色 */
.icon-cyan {
background: linear-gradient(135deg, #E3F9FF 0%, #B3ECFF 100%);
}
.icon-orange {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);
}
.icon-blue {
background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%);
}
.icon-yellow {
background: linear-gradient(135deg, #FFF9E6 0%, #FFF3CC 100%);
}
.icon-green {
background: linear-gradient(135deg, #E8F9F0 0%, #C3F0D3 100%);
}
.icon-purple {
background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
}
.icon-red {
background: linear-gradient(135deg, #FFEBEE 0%, #FFCDD2 100%);
}
.icon-teal {
background: linear-gradient(135deg, #E0F2F1 0%, #B2DFDB 100%);
}
.icon-indigo {
background: linear-gradient(135deg, #E8EAF6 0%, #C5CAE9 100%);
}
.icon-pink {
background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
}
.icon-lime {
background: linear-gradient(135deg, #F9FBE7 0%, #F0F4C3 100%);
}
/* 底部留白 */
.bottom-space {
height: 40rpx;
}

View File

@@ -0,0 +1,148 @@
// pages/cattle/detail/detail.js
const API = require('../../../utils/api').API
Page({
data: {
id: null,
cattle: null,
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options.id) {
this.setData({ id: options.id })
this.loadData()
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载数据
*/
async loadData() {
this.setData({ loading: true })
try {
// 方式1如果有详情接口
// const res = await API.getCattleDetail(this.data.id)
// const cattle = res.data || res
// 方式2从列表中获取临时方案
// 先获取列表找到对应ID的牛只
const res = await API.getCattleList({
page: 1,
pageSize: 1000 // 获取足够多的数据
})
const list = res.data?.list || res.list || []
const cattle = list.find(item => item.id === parseInt(this.data.id))
if (!cattle) {
wx.showToast({
title: '未找到该牛只信息',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
return
}
// 格式化数据
const formattedCattle = {
...cattle,
// 格式化出生日期
birthdayText: cattle.birthday ? this.formatDate(cattle.birthday) : '-',
// 格式化入场时间
intoTimeText: cattle.intoTime ? this.formatDate(cattle.intoTime) : '-',
// 格式化性别
sexText: cattle.sex === 1 ? '公' : cattle.sex === 2 ? '母' : '未知',
// 格式化生理阶段
physiologicalStageText: this.getPhysiologicalStage(cattle.physiologicalStage),
// 格式化来源
sourceText: cattle.source === 1 ? '自繁' : cattle.source === 2 ? '外购' : '未知',
// 格式化体重计算时间
weightCalculateTimeText: cattle.weightCalculateTime ? this.formatDateTime(cattle.weightCalculateTime) : '-'
}
this.setData({
cattle: formattedCattle,
loading: false
})
} catch (error) {
console.error('加载数据失败:', error)
wx.showToast({
title: '加载失败',
icon: 'none'
})
this.setData({ loading: false })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 格式化日期(时间戳转日期)
*/
formatDate(timestamp) {
if (!timestamp) return '-'
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
/**
* 获取生理阶段文本
*/
getPhysiologicalStage(stage) {
const stages = {
0: '犊牛期',
1: '育成期',
2: '成年期',
3: '老年期'
}
return stages[stage] || '未知'
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadData()
setTimeout(() => {
wx.stopPullDownRefresh()
}, 1000)
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "牛只详情",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,163 @@
<!--pages/cattle/detail/detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 详情内容 -->
<view class="detail-content" wx:else>
<!-- 头部:耳号 -->
<view class="detail-header">
<text class="ear-number">耳号:{{cattle.earNumber}}</text>
<text class="sex-badge {{cattle.sex === 1 ? 'male' : 'female'}}">{{cattle.sexText}}</text>
</view>
<!-- 基本信息 -->
<view class="info-section">
<view class="section-title">基本信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">ID</text>
<text class="value">{{cattle.id}}</text>
</view>
<view class="info-item">
<text class="label">耳号:</text>
<text class="value">{{cattle.earNumber}}</text>
</view>
<view class="info-item">
<text class="label">性别:</text>
<text class="value">{{cattle.sexText}}</text>
</view>
<view class="info-item">
<text class="label">品种:</text>
<text class="value">{{cattle.varieties || '-'}}</text>
</view>
<view class="info-item">
<text class="label">品系:</text>
<text class="value">{{cattle.strain || '-'}}</text>
</view>
<view class="info-item">
<text class="label">分类:</text>
<text class="value">{{cattle.cate || '-'}}</text>
</view>
</view>
</view>
<!-- 出生信息 -->
<view class="info-section">
<view class="section-title">出生信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">出生日期:</text>
<text class="value">{{cattle.birthdayText}}</text>
</view>
<view class="info-item">
<text class="label">出生体重:</text>
<text class="value">{{cattle.birthWeight || '-'}} kg</text>
</view>
<view class="info-item">
<text class="label">月龄:</text>
<text class="value">{{cattle.ageInMonths || 0}} 个月</text>
</view>
<view class="info-item">
<text class="label">出生天数:</text>
<text class="value">{{cattle.dayOfBirthday || 0}} 天</text>
</view>
</view>
</view>
<!-- 入场信息 -->
<view class="info-section">
<view class="section-title">入场信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">入场时间:</text>
<text class="value">{{cattle.intoTimeText}}</text>
</view>
<view class="info-item">
<text class="label">来源:</text>
<text class="value">{{cattle.sourceText}}</text>
</view>
<view class="info-item">
<text class="label">来源日龄:</text>
<text class="value">{{cattle.sourceDay || 0}} 天</text>
</view>
<view class="info-item">
<text class="label">来源体重:</text>
<text class="value">{{cattle.sourceWeight || '-'}} kg</text>
</view>
</view>
</view>
<!-- 生理信息 -->
<view class="info-section">
<view class="section-title">生理信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">生理阶段:</text>
<text class="value">{{cattle.physiologicalStageText}}</text>
</view>
<view class="info-item">
<text class="label">胎次:</text>
<text class="value">{{cattle.parity || 0}} 胎</text>
</view>
</view>
</view>
<!-- 体重信息 -->
<view class="info-section">
<view class="section-title">体重信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">当前体重:</text>
<text class="value">{{cattle.currentWeight || 0}} kg</text>
</view>
<view class="info-item">
<text class="label">体重计算时间:</text>
<text class="value">{{cattle.weightCalculateTimeText}}</text>
</view>
</view>
</view>
<!-- 场地信息 -->
<view class="info-section">
<view class="section-title">场地信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">养殖场ID</text>
<text class="value">{{cattle.farmId || '-'}}</text>
</view>
<view class="info-item">
<text class="label">养殖场名称:</text>
<text class="value">{{cattle.farmName || '-'}}</text>
</view>
<view class="info-item">
<text class="label">栏舍ID</text>
<text class="value">{{cattle.penId || '-'}}</text>
</view>
<view class="info-item">
<text class="label">栏舍名称:</text>
<text class="value">{{cattle.penName || '-'}}</text>
</view>
<view class="info-item">
<text class="label">批次ID</text>
<text class="value">{{cattle.batchId || '-'}}</text>
</view>
<view class="info-item">
<text class="label">批次名称:</text>
<text class="value">{{cattle.batchName || '-'}}</text>
</view>
</view>
</view>
<!-- 原始数据(调试用) -->
<!-- <view class="info-section">
<view class="section-title">原始数据</view>
<view class="raw-data">
<text>{{cattle}}</text>
</view>
</view> -->
</view>
</view>

View File

@@ -0,0 +1,120 @@
/* pages/cattle/detail/detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 20rpx;
}
/* 头部 */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 24rpx;
background: linear-gradient(135deg, #07c160 0%, #05a651 100%);
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
.ear-number {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.sex-badge {
padding: 8rpx 24rpx;
font-size: 26rpx;
font-weight: 500;
border-radius: 20rpx;
background-color: rgba(255, 255, 255, 0.3);
color: #fff;
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
.sex-badge.male {
background-color: rgba(33, 150, 243, 0.3);
}
.sex-badge.female {
background-color: rgba(233, 30, 99, 0.3);
}
/* 信息区块 */
.info-section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
}
.info-list {
padding: 12rpx 24rpx;
}
.info-item {
display: flex;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-item .label {
min-width: 220rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-item .value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
word-break: break-all;
}
/* 原始数据 */
.raw-data {
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
margin: 20rpx;
}
.raw-data text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
word-break: break-all;
}

View File

@@ -0,0 +1,122 @@
// pages/cattle/exit-detail/exit-detail.js
const API = require('../../../utils/api').API
Page({
data: {
id: null,
record: null,
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options.id) {
this.setData({ id: options.id })
this.loadData()
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载数据
*/
async loadData() {
this.setData({ loading: true })
try {
// 方式1如果有详情接口
// const res = await API.getExitRecordDetail(this.data.id)
// const record = res.data || res
// 方式2从列表中获取临时方案
const res = await API.getExitRecords({
page: 1,
pageSize: 1000 // 获取足够多的数据
})
const list = res.data?.list || res.data || res.list || []
const record = list.find(item => item.id === parseInt(this.data.id))
if (!record) {
wx.showToast({
title: '未找到该离栏记录',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
return
}
// 格式化数据
const formattedRecord = {
...record,
// 格式化离栏日期
exitDateText: record.exitDate ? this.formatDateTime(record.exitDate) : '-',
// 格式化创建时间
createdAtText: record.created_at ? this.formatDateTime(record.created_at) : '-',
// 格式化更新时间
updatedAtText: record.updated_at ? this.formatDateTime(record.updated_at) : '-',
// 原栏舍信息
originalPenName: record.originalPen?.name || '-',
originalPenCode: record.originalPen?.code || '-',
// 养殖场信息
farmName: record.farm?.name || '-'
}
this.setData({
record: formattedRecord,
loading: false
})
} catch (error) {
console.error('加载数据失败:', error)
wx.showToast({
title: '加载失败',
icon: 'none'
})
this.setData({ loading: false })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadData()
setTimeout(() => {
wx.stopPullDownRefresh()
}, 1000)
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "离栏记录详情",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,134 @@
<!--pages/cattle/exit-detail/exit-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 详情内容 -->
<view class="detail-content" wx:else>
<!-- 头部:记录编号 -->
<view class="detail-header">
<view class="header-left">
<text class="record-label">记录编号</text>
<text class="record-id">{{record.recordId}}</text>
</view>
<text class="status-badge">{{record.status || '已确认'}}</text>
</view>
<!-- 基本信息 -->
<view class="info-section">
<view class="section-title">基本信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">记录ID</text>
<text class="value">{{record.id}}</text>
</view>
<view class="info-item">
<text class="label">记录编号:</text>
<text class="value">{{record.recordId}}</text>
</view>
<view class="info-item">
<text class="label">状态:</text>
<text class="value">{{record.status || '已确认'}}</text>
</view>
</view>
</view>
<!-- 牛只信息 -->
<view class="info-section">
<view class="section-title">牛只信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">牛只ID</text>
<text class="value">{{record.animalId}}</text>
</view>
<view class="info-item">
<text class="label">耳号:</text>
<text class="value">{{record.earNumber}}</text>
</view>
</view>
</view>
<!-- 离栏信息 -->
<view class="info-section">
<view class="section-title">离栏信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">离栏日期:</text>
<text class="value">{{record.exitDateText}}</text>
</view>
<view class="info-item">
<text class="label">离栏原因:</text>
<text class="value">{{record.exitReason || '-'}}</text>
</view>
<view class="info-item">
<text class="label">去向:</text>
<text class="value">{{record.destination || '-'}}</text>
</view>
<view class="info-item">
<text class="label">处置方式:</text>
<text class="value">{{record.disposalMethod || '-'}}</text>
</view>
<view class="info-item">
<text class="label">原栏舍ID</text>
<text class="value">{{record.originalPenId}}</text>
</view>
<view class="info-item">
<text class="label">原栏舍名称:</text>
<text class="value">{{record.originalPenName}}</text>
</view>
<view class="info-item">
<text class="label">原栏舍编码:</text>
<text class="value">{{record.originalPenCode}}</text>
</view>
</view>
</view>
<!-- 养殖场信息 -->
<view class="info-section">
<view class="section-title">养殖场信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">养殖场ID</text>
<text class="value">{{record.farmId}}</text>
</view>
<view class="info-item">
<text class="label">养殖场名称:</text>
<text class="value">{{record.farmName}}</text>
</view>
</view>
</view>
<!-- 操作信息 -->
<view class="info-section">
<view class="section-title">操作信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">操作人:</text>
<text class="value">{{record.handler || '-'}}</text>
</view>
<view class="info-item">
<text class="label">备注:</text>
<text class="value">{{record.remark || '-'}}</text>
</view>
</view>
</view>
<!-- 时间信息 -->
<view class="info-section">
<view class="section-title">时间信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{record.createdAtText}}</text>
</view>
<view class="info-item">
<text class="label">更新时间:</text>
<text class="value">{{record.updatedAtText}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,108 @@
/* pages/cattle/exit-detail/exit-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 20rpx;
}
/* 头部 */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 24rpx;
background: linear-gradient(135deg, #07c160 0%, #05a651 100%);
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
.header-left {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.record-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.record-id {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.status-badge {
padding: 8rpx 24rpx;
font-size: 26rpx;
font-weight: 500;
border-radius: 20rpx;
background-color: rgba(255, 255, 255, 0.3);
color: #fff;
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
/* 信息区块 */
.info-section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
}
.info-list {
padding: 12rpx 24rpx;
}
.info-item {
display: flex;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-item .label {
min-width: 220rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-item .value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
word-break: break-all;
}

View File

@@ -0,0 +1,240 @@
// pages/cattle/exit/exit.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
pageSize: this.data.pageSize,
_t: Date.now()
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.search = this.data.searchValue.trim() // 按耳号精确查询
params.earNumber = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getExitRecords(params)
console.log('离栏记录数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化时间和数据
list = list.map(item => {
return {
...item,
// 格式化离栏日期
exitDateText: item.exitDate ? this.formatDateTime(item.exitDate) : '-',
// 格式化创建时间
createdAtText: item.created_at ? this.formatDateTime(item.created_at) : '-',
// 格式化更新时间
updatedAtText: item.updated_at ? this.formatDateTime(item.updated_at) : '-',
// 原栏舍名称
originalPenName: item.originalPen?.name || '-',
// 养殖场名称
farmName: item.farm?.name || '-'
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const earNumber = String(item.earNumber || '')
// 精确匹配:完全相等
return earNumber === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的记录',
icon: 'none',
duration: 2000
})
}
}
const totalPages = pagination.pages || Math.ceil((this.data.searchValue ? list.length : total) / this.data.pageSize)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看离栏记录详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看离栏记录详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/cattle/exit-detail/exit-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "离栏记录",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -1,51 +1,93 @@
<view class="page">
<view class="header">
<view class="title">牛只管理 - 离栏记录</view>
<view class="search-box">
<input class="search-input" placeholder="输入耳号精确查询" value="{{searchEarNumber}}" bindinput="onSearchInput" />
<button class="search-btn" bindtap="onSearch">查询</button>
<button class="clear-btn" bindtap="onClearSearch">清空</button>
<!--pages/cattle/exit/exit.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入耳号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 离栏记录列表 -->
<view class="exit-list" wx:else>
<view class="exit-card" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 记录编号 -->
<view class="card-header">
<text class="record-id">耳号:{{item.earNumber}}</text>
<button class="edit-btn" size="mini" catchtap="editRecord" data-id="{{item.id}}">编辑</button>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">转舍日期:</text>
<text class="value">{{item.exitDateText}}</text>
</view>
<view class="info-row">
<text class="label">转入栏舍:</text>
<text class="value">{{item.destination || '-'}}</text>
</view>
<view class="info-row">
<text class="label">转出栏舍:</text>
<text class="value">{{item.originalPenName}}</text>
</view>
<view class="info-row">
<text class="label">登记人:</text>
<text class="value">{{item.handler || '-'}}</text>
</view>
<view class="info-row">
<text class="label">登记日期:</text>
<text class="value">{{item.createdAtText}}</text>
</view>
<view class="info-row">
<text class="label">备注:</text>
<text class="value">{{item.remark || '--'}}</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="page-indicator">{{page}}/{{totalPages}}</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无离栏记录数据</text>
</view>
</view>
<view class="list" wx:if="{{records && records.length > 0}}">
<block wx:for="{{records}}" wx:key="id">
<view class="card">
<view class="card-header">
<view class="left">
<view class="main">耳号:{{item.earNumber}}</view>
<view class="sub">离栏栏舍:{{item.pen}}</view>
</view>
<view class="right">
<view class="meta">状态:{{item.status ? item.status : '-'}} | 记录编号:{{item.recordId ? item.recordId : '-'}} | 农场:{{item.farmName ? item.farmName : '-'}} </view>
<view class="time">离栏时间:{{item.exitTimeStr}}</view>
</view>
</view>
<view class="card-body">
<view class="details">
<block wx:for="{{item.details}}" wx:key="key">
<view class="row">
<text class="label">{{item.label}}</text>
<text class="value">{{item.value}}</text>
</view>
</block>
</view>
</view>
</view>
</block>
</view>
<view class="empty" wx:else>暂无数据</view>
<view class="pagination">
<button class="prev" bindtap="onPrevPage" disabled="{{page<=1}}">上一页</button>
<block wx:for="{{pages}}" wx:key="*this">
<view class="page-item {{page==item ? 'active' : ''}}" data-page="{{item}}" bindtap="onPageTap">{{item}}</view>
</block>
<button class="next" bindtap="onNextPage" disabled="{{!hasMore}}">下一页</button>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
<view class="footer">
<view class="tips">总数:{{total}};当前页:{{page}} / {{lastPage}}</view>
<view class="loading" wx:if="{{loading}}">加载中...</view>
<!-- 转栏登记按钮 -->
<view class="add-btn">
<text>转栏登记</text>
</view>
</view>
</view>

View File

@@ -1,32 +1,253 @@
page {
background: #f7f8fa;
/* pages/cattle/exit/exit.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
.header { padding: 12px; background: #fff; border-bottom: 1px solid #eee; }
.title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
.search-box { display: flex; gap: 8px; }
.search-input { flex: 1; border: 1px solid #ddd; padding: 6px 8px; border-radius: 4px; }
.search-btn, .clear-btn { padding: 6px 12px; border-radius: 4px; background: #1677ff; color: #fff; }
.clear-btn { background: #999; }
.list { padding: 12px; }
.card { background: #fff; border-radius: 8px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); margin-bottom: 12px; }
.card-header { display: flex; justify-content: space-between; border-bottom: 1px dashed #eee; padding-bottom: 8px; margin-bottom: 8px; }
.left .main { font-size: 16px; font-weight: bold; }
.left .sub { font-size: 14px; color: #666; margin-top: 4px; }
.right { text-align: right; }
.right .meta { font-size: 12px; color: #333; }
.right .time { font-size: 12px; color: #666; margin-top: 4px; }
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.details .row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px dashed #f0f0f0; }
.details .row:last-child { border-bottom: none; }
.label { color: #666; }
.value { color: #111; font-weight: 500; }
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; }
.page-item { padding: 4px 10px; border-radius: 4px; background: #fff; border: 1px solid #ddd; }
.page-item.active { background: #1677ff; color: #fff; border-color: #1677ff; }
.prev, .next { background: #fff; border: 1px solid #ddd; padding: 6px 12px; border-radius: 4px; }
.prev[disabled], .next[disabled] { opacity: 0.5; }
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.footer { padding: 12px; text-align: center; color: #666; }
.loading { margin-top: 8px; }
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #fa5151;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 离栏记录列表 */
.exit-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 离栏记录卡片 */
.exit-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.record-id {
font-size: 32rpx;
font-weight: bold;
color: #07c160;
}
.edit-btn {
padding: 8rpx 24rpx;
background-color: #07c160;
color: #fff;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
}
.edit-btn::after {
border: none;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 180rpx;
flex-shrink: 0;
}
.info-row .value {
color: #333;
flex: 1;
word-break: break-all;
}
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: center;
border-top: 1rpx solid #f0f0f0;
}
.page-indicator {
font-size: 24rpx;
color: #999;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #07c160;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #07c160;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
/* 转栏登记按钮 */
.add-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
width: 90%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}

View File

@@ -0,0 +1,119 @@
// pages/cattle/pen-detail/pen-detail.js
const API = require('../../../utils/api').API
Page({
data: {
id: null,
pen: null,
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options.id) {
this.setData({ id: options.id })
this.loadData()
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载数据
*/
async loadData() {
this.setData({ loading: true })
try {
// 方式1如果有详情接口
// const res = await API.getPenDetail(this.data.id)
// const pen = res.data || res
// 方式2从列表中获取临时方案
const res = await API.getPenList({
page: 1,
pageSize: 1000 // 获取足够多的数据
})
const list = res.data?.list || res.data || res.list || []
const pen = list.find(item => item.id === parseInt(this.data.id))
if (!pen) {
wx.showToast({
title: '未找到该栏舍',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
return
}
// 格式化数据
const formattedPen = {
...pen,
// 格式化创建时间
createdAtText: pen.created_at ? this.formatDateTime(pen.created_at) : '-',
// 格式化更新时间
updatedAtText: pen.updated_at ? this.formatDateTime(pen.updated_at) : '-',
// 养殖场信息
farmName: pen.farm?.name || '-',
// 状态文本
statusText: pen.status === '启用' || pen.status === 1 ? '启用' : '未启用'
}
this.setData({
pen: formattedPen,
loading: false
})
} catch (error) {
console.error('加载数据失败:', error)
wx.showToast({
title: '加载失败',
icon: 'none'
})
this.setData({ loading: false })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadData()
setTimeout(() => {
wx.stopPullDownRefresh()
}, 1000)
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "栏舍详情",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,115 @@
<!--pages/cattle/pen-detail/pen-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 详情内容 -->
<view class="detail-content" wx:else>
<!-- 头部:栏舍名称 -->
<view class="detail-header">
<view class="header-left">
<text class="pen-label">栏舍名称</text>
<text class="pen-name">{{pen.name}}</text>
</view>
<text class="status-badge">{{pen.statusText}}</text>
</view>
<!-- 基本信息 -->
<view class="info-section">
<view class="section-title">基本信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">栏舍ID</text>
<text class="value">{{pen.id}}</text>
</view>
<view class="info-item">
<text class="label">栏舍名称:</text>
<text class="value">{{pen.name}}</text>
</view>
<view class="info-item">
<text class="label">栏舍编码:</text>
<text class="value">{{pen.code}}</text>
</view>
<view class="info-item">
<text class="label">栏舍类型:</text>
<text class="value">{{pen.type || '-'}}</text>
</view>
<view class="info-item">
<text class="label">状态:</text>
<text class="value">{{pen.statusText}}</text>
</view>
</view>
</view>
<!-- 容量信息 -->
<view class="info-section">
<view class="section-title">容量信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">最大容量:</text>
<text class="value">{{pen.capacity || 0}}</text>
</view>
<view class="info-item">
<text class="label">当前数量:</text>
<text class="value">{{pen.currentCount || 0}}</text>
</view>
<view class="info-item">
<text class="label">剩余容量:</text>
<text class="value">{{(pen.capacity || 0) - (pen.currentCount || 0)}}</text>
</view>
<view class="info-item">
<text class="label">面积:</text>
<text class="value">{{pen.area || 0}}平方米</text>
</view>
</view>
</view>
<!-- 养殖场信息 -->
<view class="info-section">
<view class="section-title">养殖场信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">养殖场ID</text>
<text class="value">{{pen.farmId || pen.farm_id}}</text>
</view>
<view class="info-item">
<text class="label">养殖场名称:</text>
<text class="value">{{pen.farmName}}</text>
</view>
</view>
</view>
<!-- 位置与备注 -->
<view class="info-section">
<view class="section-title">位置与备注</view>
<view class="info-list">
<view class="info-item">
<text class="label">位置:</text>
<text class="value">{{pen.location || '-'}}</text>
</view>
<view class="info-item">
<text class="label">备注:</text>
<text class="value">{{pen.remark || '-'}}</text>
</view>
</view>
</view>
<!-- 时间信息 -->
<view class="info-section">
<view class="section-title">时间信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{pen.createdAtText}}</text>
</view>
<view class="info-item">
<text class="label">更新时间:</text>
<text class="value">{{pen.updatedAtText}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,108 @@
/* pages/cattle/pen-detail/pen-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 20rpx;
}
/* 头部 */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 24rpx;
background: linear-gradient(135deg, #07c160 0%, #05a651 100%);
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
.header-left {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.pen-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.pen-name {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.status-badge {
padding: 8rpx 24rpx;
font-size: 26rpx;
font-weight: 500;
border-radius: 20rpx;
background-color: rgba(255, 255, 255, 0.3);
color: #fff;
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
/* 信息区块 */
.info-section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
}
.info-list {
padding: 12rpx 24rpx;
}
.info-item {
display: flex;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-item .label {
min-width: 200rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-item .value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
word-break: break-all;
}

View File

@@ -0,0 +1,239 @@
// pages/cattle/pen/pen.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
pageSize: this.data.pageSize,
_t: Date.now()
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.name = this.data.searchValue.trim() // 按栏舍名称精确查询
params.search = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getPenList(params)
console.log('栏舍设置数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化时间和数据
list = list.map(item => {
return {
...item,
// 格式化创建时间
createdAtText: item.created_at ? this.formatDateTime(item.created_at) : '-',
// 格式化更新时间
updatedAtText: item.updated_at ? this.formatDateTime(item.updated_at) : '-',
// 养殖场名称
farmName: item.farm?.name || '-',
// 状态文本
statusText: item.status === '启用' || item.status === 1 ? '启用' : '未启用',
statusValue: item.status === '启用' || item.status === 1
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const name = String(item.name || '')
// 精确匹配:完全相等
return name === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的栏舍',
icon: 'none',
duration: 2000
})
}
}
const totalPages = pagination.pages || Math.ceil((this.data.searchValue ? list.length : total) / this.data.pageSize)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看栏舍详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看栏舍详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/cattle/pen-detail/pen-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "栏舍设置",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,98 @@
<!--pages/cattle/pen/pen.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
placeholder="🔍 请输入栏舍名称"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
<!-- 栏舍列表 -->
<view class="pen-list" wx:else>
<view class="pen-card" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 栏舍名称 -->
<view class="card-header">
<text class="pen-name">栏舍名:{{item.name}}</text>
<button class="edit-btn" size="mini" catchtap="editPen" data-id="{{item.id}}">编辑</button>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">动物类型:</text>
<text class="value">牛</text>
</view>
<view class="info-row">
<text class="label">栏舍类型:</text>
<text class="value">{{item.type || '--'}}</text>
</view>
<view class="info-row">
<text class="label">负责人:</text>
<text class="value">admin</text>
</view>
<view class="info-row">
<text class="label">容量:</text>
<text class="value">{{item.capacity || 0}}</text>
</view>
<view class="info-row">
<text class="label">状态:</text>
<switch class="status-switch" checked="{{item.statusValue}}" catchtap="toggleStatus" data-id="{{item.id}}" />
</view>
<view class="info-row">
<text class="label">创建人:</text>
<text class="value">admin</text>
</view>
<view class="info-row">
<text class="label">创建时间:</text>
<text class="value">{{item.createdAtText}}</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="page-indicator">{{page}}/{{totalPages}}</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">🏠</text>
<text class="empty-text">暂无栏舍设置数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
<!-- 新增栏舍按钮 -->
<view class="add-btn">
<text>新增栏舍</text>
</view>
</view>

View File

@@ -0,0 +1,258 @@
/* pages/cattle/pen/pen.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #fa5151;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.clear-btn::after {
border: none;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 栏舍列表 */
.pen-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 栏舍卡片 */
.pen-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.pen-name {
font-size: 32rpx;
font-weight: bold;
color: #07c160;
}
.edit-btn {
padding: 8rpx 24rpx;
background-color: #07c160;
color: #fff;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
}
.edit-btn::after {
border: none;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: center;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 180rpx;
flex-shrink: 0;
}
.info-row .value {
color: #333;
flex: 1;
word-break: break-all;
}
.status-switch {
transform: scale(0.9);
}
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: center;
border-top: 1rpx solid #f0f0f0;
}
.page-indicator {
font-size: 24rpx;
color: #999;
}
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #07c160;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #07c160;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
/* 新增栏舍按钮 */
.add-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
width: 90%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}

View File

@@ -1,51 +1,51 @@
<view class="page">
<!-- 头部与搜索栏 -->
<view class="header">
<text class="title">牛只管理 · 栏舍设置</text>
<view class="search-bar">
<input class="search-input" placeholder="输入栏舍名称(精确)" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" value="{{searchName}}" />
<button class="search-btn" bindtap="onSearchConfirm">查询</button>
</view>
</view>
<!-- 列表 -->
<view class="list">
<block wx:for="{{displayRecords}}" wx:key="index">
<view class="card">
<view class="card-header">
<view class="main">
<text class="name">栏舍:{{item.name || '-'}}</text>
<text class="code">编码:{{item.code || '-'}}</text>
</view>
<view class="meta">
<text>养殖场:{{item.farmName || '-'}} </text>
<text>状态:{{item.status || '-'}} </text>
<text>容量:{{item.capacity || '-'}} </text>
</view>
<view class="time">
<text>创建时间:{{item.createdAtStr || '-'}} </text>
</view>
</view>
<view class="card-body">
<block wx:for="{{item.displayFields}}" wx:key="key">
<view class="row">
<text class="label">{{item.label}}</text>
<text class="value">{{item.value}}</text>
</view>
</block>
</view>
</view>
</block>
<view wx:if="{{!displayRecords || displayRecords.length === 0}}" class="empty">暂无数据</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{pages && pages.length}}">
<button class="pager-btn" bindtap="onPrevPage" disabled="{{currentPage<=1}}">上一页</button>
<block wx:for="{{pages}}" wx:key="page">
<button class="page-item {{currentPage === item ? 'active' : ''}}" data-page="{{item}}" bindtap="onPageTap">{{item}}</button>
</block>
<button class="pager-btn" bindtap="onNextPage" disabled="{{!hasMore}}">下一页</button>
</view>
<view class="page">
<!-- 头部与搜索栏 -->
<view class="header">
<text class="title">牛只管理 · 栏舍设置</text>
<view class="search-bar">
<input class="search-input" placeholder="输入栏舍名称(精确)" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" value="{{searchName}}" />
<button class="search-btn" bindtap="onSearchConfirm">查询</button>
</view>
</view>
<!-- 列表 -->
<view class="list">
<block wx:for="{{displayRecords}}" wx:key="index">
<view class="card">
<view class="card-header">
<view class="main">
<text class="name">栏舍:{{item.name || '-'}}</text>
<text class="code">编码:{{item.code || '-'}}</text>
</view>
<view class="meta">
<text>养殖场:{{item.farmName || '-'}} </text>
<text>状态:{{item.status || '-'}} </text>
<text>容量:{{item.capacity || '-'}} </text>
</view>
<view class="time">
<text>创建时间:{{item.createdAtStr || '-'}} </text>
</view>
</view>
<view class="card-body">
<block wx:for="{{item.displayFields}}" wx:key="key">
<view class="row">
<text class="label">{{item.label}}</text>
<text class="value">{{item.value}}</text>
</view>
</block>
</view>
</view>
</block>
<view wx:if="{{!displayRecords || displayRecords.length === 0}}" class="empty">暂无数据</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{pages && pages.length}}">
<button class="pager-btn" bindtap="onPrevPage" disabled="{{currentPage<=1}}">上一页</button>
<block wx:for="{{pages}}" wx:key="page">
<button class="page-item {{currentPage === item ? 'active' : ''}}" data-page="{{item}}" bindtap="onPageTap">{{item}}</button>
</block>
<button class="pager-btn" bindtap="onNextPage" disabled="{{!hasMore}}">下一页</button>
</view>
</view>

View File

@@ -1,27 +1,27 @@
/* 栏舍设置页面样式 */
.page { padding: 12px; background: #f7f8fa; min-height: 100vh; }
.header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.title { font-size: 16px; font-weight: 600; color: #333; }
.search-bar { display: flex; gap: 8px; }
.search-input { flex: 1; border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; background: #fff; }
.search-btn { padding: 8px 12px; background: #3cc51f; color: #fff; border-radius: 6px; }
.list { display: flex; flex-direction: column; gap: 12px; }
.card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); padding: 12px; }
.card-header { display: flex; flex-direction: column; gap: 6px; border-bottom: 1px dashed #eee; padding-bottom: 8px; }
.main { display: flex; gap: 10px; align-items: baseline; }
.name { font-size: 16px; font-weight: 600; color: #222; }
.code { font-size: 14px; color: #666; }
.meta { display: flex; flex-wrap: wrap; gap: 10px; color: #666; font-size: 13px; }
.time { color: #999; font-size: 12px; }
.card-body { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.row { display: flex; gap: 8px; }
.label { color: #666; min-width: 84px; text-align: right; }
.value { color: #333; flex: 1; }
.pagination { display: flex; gap: 6px; justify-content: center; align-items: center; margin-top: 14px; }
.pager-btn { background: #f0f0f0; color: #333; border-radius: 6px; padding: 6px 10px; }
.page-item { background: #fff; color: #333; border: 1px solid #ddd; border-radius: 6px; padding: 6px 10px; }
.page-item.active { background: #3cc51f; border-color: #3cc51f; color: #fff; }
/* 栏舍设置页面样式 */
.page { padding: 12px; background: #f7f8fa; min-height: 100vh; }
.header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.title { font-size: 16px; font-weight: 600; color: #333; }
.search-bar { display: flex; gap: 8px; }
.search-input { flex: 1; border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; background: #fff; }
.search-btn { padding: 8px 12px; background: #3cc51f; color: #fff; border-radius: 6px; }
.list { display: flex; flex-direction: column; gap: 12px; }
.card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); padding: 12px; }
.card-header { display: flex; flex-direction: column; gap: 6px; border-bottom: 1px dashed #eee; padding-bottom: 8px; }
.main { display: flex; gap: 10px; align-items: baseline; }
.name { font-size: 16px; font-weight: 600; color: #222; }
.code { font-size: 14px; color: #666; }
.meta { display: flex; flex-wrap: wrap; gap: 10px; color: #666; font-size: 13px; }
.time { color: #999; font-size: 12px; }
.card-body { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.row { display: flex; gap: 8px; }
.label { color: #666; min-width: 84px; text-align: right; }
.value { color: #333; flex: 1; }
.pagination { display: flex; gap: 6px; justify-content: center; align-items: center; margin-top: 14px; }
.pager-btn { background: #f0f0f0; color: #333; border-radius: 6px; padding: 6px 10px; }
.page-item { background: #fff; color: #333; border: 1px solid #ddd; border-radius: 6px; padding: 6px 10px; }
.page-item.active { background: #3cc51f; border-color: #3cc51f; color: #fff; }
.empty { text-align: center; color: #999; padding: 20px 0; }

View File

@@ -0,0 +1,125 @@
// pages/cattle/transfer-detail/transfer-detail.js
const API = require('../../../utils/api').API
Page({
data: {
id: null,
record: null,
loading: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options.id) {
this.setData({ id: options.id })
this.loadData()
} else {
wx.showToast({
title: '参数错误',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 加载数据
*/
async loadData() {
this.setData({ loading: true })
try {
// 方式1如果有详情接口
// const res = await API.getTransferRecordDetail(this.data.id)
// const record = res.data || res
// 方式2从列表中获取临时方案
const res = await API.getTransferRecords({
page: 1,
pageSize: 1000 // 获取足够多的数据
})
const list = res.data?.list || res.data || res.list || []
const record = list.find(item => item.id === parseInt(this.data.id))
if (!record) {
wx.showToast({
title: '未找到该转栏记录',
icon: 'none'
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
return
}
// 格式化数据
const formattedRecord = {
...record,
// 格式化转栏日期
transferDateText: record.transferDate ? this.formatDateTime(record.transferDate) : '-',
// 格式化创建时间
createdAtText: record.created_at ? this.formatDateTime(record.created_at) : '-',
// 格式化更新时间
updatedAtText: record.updated_at ? this.formatDateTime(record.updated_at) : '-',
// 转出栏舍信息
fromPenName: record.fromPen?.name || '-',
fromPenCode: record.fromPen?.code || '-',
// 转入栏舍信息
toPenName: record.toPen?.name || '-',
toPenCode: record.toPen?.code || '-',
// 养殖场信息
farmName: record.farm?.name || '-'
}
this.setData({
record: formattedRecord,
loading: false
})
} catch (error) {
console.error('加载数据失败:', error)
wx.showToast({
title: '加载失败',
icon: 'none'
})
this.setData({ loading: false })
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadData()
setTimeout(() => {
wx.stopPullDownRefresh()
}, 1000)
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "转栏记录详情",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,138 @@
<!--pages/cattle/transfer-detail/transfer-detail.wxml-->
<view class="page-container">
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 详情内容 -->
<view class="detail-content" wx:else>
<!-- 头部:记录编号 -->
<view class="detail-header">
<view class="header-left">
<text class="record-label">记录编号</text>
<text class="record-id">{{record.recordId}}</text>
</view>
<text class="status-badge">{{record.status || '已完成'}}</text>
</view>
<!-- 基本信息 -->
<view class="info-section">
<view class="section-title">基本信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">记录ID</text>
<text class="value">{{record.id}}</text>
</view>
<view class="info-item">
<text class="label">记录编号:</text>
<text class="value">{{record.recordId}}</text>
</view>
<view class="info-item">
<text class="label">状态:</text>
<text class="value">{{record.status || '已完成'}}</text>
</view>
</view>
</view>
<!-- 牛只信息 -->
<view class="info-section">
<view class="section-title">牛只信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">牛只ID</text>
<text class="value">{{record.animalId}}</text>
</view>
<view class="info-item">
<text class="label">耳号:</text>
<text class="value">{{record.earNumber}}</text>
</view>
</view>
</view>
<!-- 转栏信息 -->
<view class="info-section">
<view class="section-title">转栏信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">转栏日期:</text>
<text class="value">{{record.transferDateText}}</text>
</view>
<view class="info-item">
<text class="label">转出栏舍ID</text>
<text class="value">{{record.fromPenId}}</text>
</view>
<view class="info-item">
<text class="label">转出栏舍名称:</text>
<text class="value">{{record.fromPenName}}</text>
</view>
<view class="info-item">
<text class="label">转出栏舍编码:</text>
<text class="value">{{record.fromPenCode}}</text>
</view>
<view class="info-item">
<text class="label">转入栏舍ID</text>
<text class="value">{{record.toPenId}}</text>
</view>
<view class="info-item">
<text class="label">转入栏舍名称:</text>
<text class="value">{{record.toPenName}}</text>
</view>
<view class="info-item">
<text class="label">转入栏舍编码:</text>
<text class="value">{{record.toPenCode}}</text>
</view>
</view>
</view>
<!-- 养殖场信息 -->
<view class="info-section">
<view class="section-title">养殖场信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">养殖场ID</text>
<text class="value">{{record.farmId}}</text>
</view>
<view class="info-item">
<text class="label">养殖场名称:</text>
<text class="value">{{record.farmName}}</text>
</view>
</view>
</view>
<!-- 操作信息 -->
<view class="info-section">
<view class="section-title">操作信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">操作人:</text>
<text class="value">{{record.operator || '-'}}</text>
</view>
<view class="info-item">
<text class="label">转栏原因:</text>
<text class="value">{{record.reason || '-'}}</text>
</view>
<view class="info-item">
<text class="label">备注:</text>
<text class="value">{{record.remark || '-'}}</text>
</view>
</view>
</view>
<!-- 时间信息 -->
<view class="info-section">
<view class="section-title">时间信息</view>
<view class="info-list">
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{record.createdAtText}}</text>
</view>
<view class="info-item">
<text class="label">更新时间:</text>
<text class="value">{{record.updatedAtText}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,108 @@
/* pages/cattle/transfer-detail/transfer-detail.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 加载状态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 详情内容 */
.detail-content {
padding: 20rpx;
}
/* 头部 */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 24rpx;
background: linear-gradient(135deg, #07c160 0%, #05a651 100%);
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
.header-left {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.record-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.record-id {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.status-badge {
padding: 8rpx 24rpx;
font-size: 26rpx;
font-weight: 500;
border-radius: 20rpx;
background-color: rgba(255, 255, 255, 0.3);
color: #fff;
border: 2rpx solid rgba(255, 255, 255, 0.5);
}
/* 信息区块 */
.info-section {
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.section-title {
padding: 24rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
}
.info-list {
padding: 12rpx 24rpx;
}
.info-item {
display: flex;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-item .label {
min-width: 240rpx;
font-size: 28rpx;
color: #666;
flex-shrink: 0;
}
.info-item .value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
word-break: break-all;
}

View File

@@ -0,0 +1,242 @@
// pages/cattle/transfer/transfer.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
pageSize: this.data.pageSize,
_t: Date.now()
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.search = this.data.searchValue.trim() // 按耳号精确查询
params.earNumber = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getTransferRecords(params)
console.log('转栏记录数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const pagination = res.data?.pagination || {}
const total = pagination.total || res.total || list.length || 0
// 数据预处理:格式化时间和数据
list = list.map(item => {
return {
...item,
// 格式化转栏日期
transferDateText: item.transferDate ? this.formatDateTime(item.transferDate) : '-',
// 格式化创建时间
createdAtText: item.created_at ? this.formatDateTime(item.created_at) : '-',
// 格式化更新时间
updatedAtText: item.updated_at ? this.formatDateTime(item.updated_at) : '-',
// 转出栏舍名称
fromPenName: item.fromPen?.name || '-',
// 转入栏舍名称
toPenName: item.toPen?.name || '-',
// 养殖场名称
farmName: item.farm?.name || '-'
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const earNumber = String(item.earNumber || '')
// 精确匹配:完全相等
return earNumber === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的记录',
icon: 'none',
duration: 2000
})
}
}
const totalPages = pagination.pages || Math.ceil((this.data.searchValue ? list.length : total) / this.data.pageSize)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 格式化日期时间
*/
formatDateTime(dateString) {
if (!dateString) return '-'
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看转栏记录详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
console.log('查看转栏记录详情:', id)
// 跳转到详情页
wx.navigateTo({
url: `/pages/cattle/transfer-detail/transfer-detail?id=${id}`
})
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "转栏记录",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}

View File

@@ -1,82 +1,93 @@
<!-- pages/cattle/transfer/transfer.wxml -->
<view class="transfer-container">
<!-- 搜索栏:耳号精确查询 -->
<!--pages/cattle/transfer/transfer.wxml-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<input class="search-input" placeholder="请输入耳号,精确查询" value="{{searchEarNumber}}" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearch"/>
<text class="search-icon" bindtap="onSearch">🔍</text>
</view>
<text class="clear-btn" bindtap="onClearSearch" wx:if="{{searchEarNumber}}">清空</text>
</view>
<!-- 列表 -->
<view class="record-list">
<block wx:if="{{records.length > 0}}">
<block wx:for="{{records}}" wx:key="id">
<view class="record-item">
<!-- 左侧头像/图标 -->
<view class="record-avatar"><text class="avatar-icon">🏠</text></view>
<!-- 中间信息 -->
<view class="record-info">
<view class="record-title">
<text class="ear-number">耳号:{{item.earNumber}}</text>
</view>
<!-- 扩展详情(全部字段展示,已去重头部字段) -->
<view class="record-extra">
<block wx:for="{{item.details}}" wx:key="{{item.key}}">
<view class="extra-row">
<text class="extra-label">{{item.label}}</text>
<text class="extra-value">{{item.value}}</text>
</view>
</block>
</view>
</view>
<!-- 右侧元信息 -->
<view class="record-meta">
<text class="meta-item">状态:{{item.status || '-'}} </text>
<text class="meta-item">编号:{{item.recordId || '-'}} </text>
<text class="meta-item">农场:{{item.farmName || '-'}} </text>
</view>
</view>
</block>
<!-- 加载更多/无更多 -->
<view wx:if="{{hasMore}}" class="load-more">
<text wx:if="{{loading}}">加载中...</text>
<text wx:else>上拉或点击页码加载更多</text>
</view>
<view wx:if="{{!hasMore}}" class="no-more">
<text>没有更多数据了</text>
</view>
</block>
<!-- 空态 -->
<view wx:else class="empty-state">
<text class="empty-icon">📄</text>
<text class="empty-text">暂无转栏记录</text>
</view>
</view>
<!-- 分页导航 -->
<view wx:if="{{records.length > 0}}" class="pagination">
<view class="page-item {{page <= 1 ? 'disabled' : ''}}" bindtap="onPrevPage">上一页</view>
<view class="page-item" data-page="1" bindtap="onPageTap">首页</view>
<view
wx:for="{{pages}}"
wx:key="*this"
class="page-item {{item === page ? 'active' : ''}}"
data-page="{{item}}"
bindtap="onPageTap"
>{{item}}</view>
<view class="page-item" data-page="{{lastPage}}" bindtap="onPageTap">末页</view>
<view class="page-item {{page >= lastPage ? 'disabled' : ''}}" bindtap="onNextPage">下一页</view>
<input
class="search-input"
placeholder="🔍 请输入耳号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
type="text"
/>
<button class="search-btn" bindtap="handleSearch" wx:if="{{searchValue}}">搜索</button>
<button class="clear-btn" bindtap="clearSearch" wx:if="{{searchValue}}">清空</button>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading && records.length === 0}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
<view class="loading-container" wx:if="{{loading && !refreshing}}">
<text>加载中...</text>
</view>
</view>
<!-- 转栏记录列表 -->
<view class="transfer-list" wx:else>
<view class="transfer-card" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<!-- 记录编号 -->
<view class="card-header">
<text class="record-id">耳号:{{item.earNumber}}</text>
<button class="edit-btn" size="mini" catchtap="editRecord" data-id="{{item.id}}">编辑</button>
</view>
<!-- 详细信息 -->
<view class="card-body">
<view class="info-row">
<text class="label">转栏日期:</text>
<text class="value">{{item.transferDateText}}</text>
</view>
<view class="info-row">
<text class="label">转入栏舍:</text>
<text class="value">{{item.toPenName}}</text>
</view>
<view class="info-row">
<text class="label">转出栏舍:</text>
<text class="value">{{item.fromPenName}}</text>
</view>
<view class="info-row">
<text class="label">登记人:</text>
<text class="value">{{item.operator || '-'}}</text>
</view>
<view class="info-row">
<text class="label">登记日期:</text>
<text class="value">{{item.createdAtText}}</text>
</view>
<view class="info-row">
<text class="label">备注:</text>
<text class="value">{{item.remark || '--'}}</text>
</view>
</view>
<!-- 分隔线 -->
<view class="card-footer">
<text class="page-indicator">{{page}}/{{totalPages}}</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{!loading && list.length === 0}}">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无转栏记录数据</text>
</view>
</view>
<!-- 分页 -->
<view class="pagination" wx:if="{{totalPages > 1 && list.length > 0}}">
<button class="page-btn" bindtap="prevPage" disabled="{{page === 1}}">上一页</button>
<view class="page-numbers">
<text class="page-number {{page === item ? 'active' : ''}}"
wx:for="{{totalPages}}"
wx:key="index"
wx:for-item="item"
wx:for-index="index"
data-page="{{index + 1}}"
bindtap="changePage">
{{index + 1}}
</text>
</view>
<button class="page-btn" bindtap="nextPage" disabled="{{page === totalPages}}">下一页</button>
</view>
<!-- 转栏登记按钮 -->
<view class="add-btn">
<text>转栏登记</text>
</view>
</view>

View File

@@ -1,211 +1,253 @@
/* pages/cattle/transfer/transfer.wxss */
.transfer-container {
background-color: #f6f6f6;
.page-container {
min-height: 100vh;
padding-bottom: 120rpx;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
/* 搜索栏 */
.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;
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-input {
width: 100%;
height: 72rpx;
flex: 1;
height: 60rpx;
padding: 0 20rpx;
background-color: #f5f5f5;
border-radius: 36rpx;
padding: 0 60rpx 0 24rpx;
border-radius: 8rpx;
font-size: 28rpx;
color: #303133;
}
.search-icon {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: #909399;
.search-btn {
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #07c160;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
}
.search-btn::after {
border: none;
}
.clear-btn {
width: 100rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin-left: 20rpx;
background-color: #fa5151;
color: #fff;
font-size: 28rpx;
color: #3cc51f;
padding: 8rpx 16rpx;
border-radius: 8rpx;
border: none;
}
.record-list {
padding: 16rpx;
.clear-btn::after {
border: none;
}
.record-item {
/* 加载状态 */
.loading-container {
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;
}
.record-item:active {
transform: scale(0.98);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.record-avatar {
width: 80rpx;
height: 80rpx;
background-color: #f0f9ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
align-items: center;
height: 400rpx;
color: #999;
font-size: 28rpx;
}
/* 转栏记录列表 */
.transfer-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 转栏记录卡片 */
.transfer-card {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.record-id {
font-size: 32rpx;
font-weight: bold;
color: #07c160;
}
.edit-btn {
padding: 8rpx 24rpx;
background-color: #07c160;
color: #fff;
border: none;
border-radius: 20rpx;
font-size: 24rpx;
}
.edit-btn::after {
border: none;
}
/* 卡片主体 */
.card-body {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
font-size: 28rpx;
}
.info-row .label {
color: #666;
min-width: 180rpx;
flex-shrink: 0;
}
.avatar-icon {
font-size: 40rpx;
}
.record-info {
.info-row .value {
color: #333;
flex: 1;
margin-right: 16rpx;
word-break: break-all;
}
.record-title {
font-size: 32rpx;
font-weight: 500;
color: #303133;
margin-bottom: 8rpx;
/* 卡片底部 */
.card-footer {
padding: 16rpx 24rpx;
background-color: #f8f9fa;
text-align: center;
border-top: 1rpx solid #f0f0f0;
}
.ear-number {
color: #303133;
.page-indicator {
font-size: 24rpx;
color: #999;
}
.record-details {
/* 空状态 */
.empty-container {
display: flex;
flex-direction: column;
margin-bottom: 8rpx;
}
.detail-item {
font-size: 24rpx;
color: #606266;
margin-bottom: 4rpx;
}
.record-extra {
margin-top: 8rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6rpx 12rpx;
}
.extra-row {
display: flex;
align-items: center;
font-size: 22rpx;
}
.extra-label {
color: #909399;
margin-right: 8rpx;
}
.extra-value {
color: #303133;
}
.record-meta {
display: flex;
flex-direction: column;
}
.meta-item {
font-size: 22rpx;
color: #909399;
margin-bottom: 2rpx;
}
.load-more {
text-align: center;
padding: 32rpx;
font-size: 24rpx;
color: #909399;
}
.no-more {
text-align: center;
padding: 32rpx;
font-size: 24rpx;
color: #c0c4cc;
}
/* 分页导航 */
.pagination {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
align-items: center;
justify-content: center;
padding: 20rpx 16rpx 40rpx;
padding: 100rpx 0;
}
.page-item {
min-width: 60rpx;
padding: 12rpx 20rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
color: #606266;
font-size: 26rpx;
text-align: center;
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.page-item.active {
background-color: #3cc51f;
color: #ffffff;
}
.page-item.disabled {
opacity: 0.5;
pointer-events: none;
}
.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 {
.empty-text {
font-size: 28rpx;
color: #909399;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 分页 */
.pagination {
display: flex;
align-items: center;
justify-content: center;
margin: 30rpx 0;
padding: 20rpx;
background-color: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.page-btn {
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
margin: 0 10rpx;
background-color: #07c160;
color: #fff;
font-size: 26rpx;
border-radius: 8rpx;
border: none;
}
.page-btn::after {
border: none;
}
.page-btn[disabled] {
background-color: #e0e0e0;
color: #999;
}
.page-numbers {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin: 0 20rpx;
max-width: 400rpx;
justify-content: center;
}
.page-number {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 26rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 8rpx;
transition: all 0.3s;
}
.page-number.active {
background-color: #07c160;
color: #fff;
font-weight: bold;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}
/* 转栏登记按钮 */
.add-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
width: 90%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
}

View File

@@ -0,0 +1,57 @@
# 生产管理页面图标说明
## 需要的图标列表
### 牛只管理图标11个
放置目录:`/images/cattle/`
1. `archive.png` - 牛档案(浅蓝色图标)
2. `estrus.png` - 发情记录(橙色图标)
3. `breeding.png` - 配种记录(蓝色图标)
4. `pregnancy.png` - 妊娠记录(黄色图标)
5. `birth.png` - 分娩记录(绿色图标)
6. `weaning.png` - 断奶记录(紫色图标)
7. `transfer.png` - 转栏记录(红色图标)
8. `exit.png` - 离栏记录(青色图标)
9. `pens.png` - 栏舍设置(靛蓝色图标)
10. `batches.png` - 批次设置(粉色图标)
11. `vaccination.png` - 防疫预警(黄绿色图标)
### 猪只管理图标11个
放置目录:`/images/pig/`
1. `archive.png` - 猪档案(浅蓝色图标)
2. `estrus.png` - 发情记录(橙色图标)
3. `breeding.png` - 配种记录(蓝色图标)
4. `pregnancy.png` - 妊娠记录(黄色图标)
5. `birth.png` - 分娩记录(绿色图标)
6. `weaning.png` - 断奶记录(紫色图标)
7. `transfer.png` - 转栏记录(红色图标)
8. `exit.png` - 离栏记录(青色图标)
9. `pens.png` - 栏舍设置(靛蓝色图标)
10. `batches.png` - 批次设置(粉色图标)
11. `vaccination.png` - 防疫预警(黄绿色图标)
## 图标规格
- 尺寸96x96 像素
- 格式PNG透明背景
- 风格:线性图标,简洁风格
## 获取方式
1. 从 iconfont.cn 搜索下载
2. 请设计师制作
3. 暂时可不放置图标文件,页面仍可正常显示背景色
## 推荐搜索关键词
- 档案folder, file, document
- 发情heart, love
- 配种breeding
- 妊娠pregnant
- 分娩birth, baby
- 断奶weaning
- 转栏transfer, move
- 离栏exit, leave
- 栏舍house, pen
- 批次batch, group
- 防疫vaccine, shield

View File

@@ -0,0 +1,201 @@
// pages/device/collar/collar.js - 智能项圈页面
const { API } = require('../../../utils/api.js')
Page({
data: {
list: [], // 数据列表
page: 1, // 当前页码
limit: 10, // 每页数量
total: 0, // 总数量
totalPages: 0, // 总页数
searchValue: '', // 搜索值
loading: false, // 加载状态
refreshing: false // 刷新状态
},
onLoad() {
console.log('智能项圈页面加载')
this.loadData()
},
onShow() {
console.log('智能项圈页面显示')
},
onPullDownRefresh() {
this.setData({ page: 1, refreshing: true })
this.loadData(true)
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (this.data.loading) return
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
limit: this.data.limit,
_t: Date.now(),
refresh: isRefresh
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.sn = this.data.searchValue.trim() // 按项圈SN编号精确查询
params.number = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getCollarList(params)
console.log('智能项圈数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const total = res.data?.total || res.total || 0
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const sn = String(item.sn || item.number || '')
// 精确匹配:完全相等
return sn === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的设备',
icon: 'none',
duration: 2000
})
}
}
const totalPages = Math.ceil((this.data.searchValue ? list.length : total) / this.data.limit)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page === this.data.page || page < 1 || page > this.data.totalPages) {
return
}
this.setData({ page: page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/device/collar-detail/collar-detail?id=${id}`,
fail: () => {
wx.showToast({
title: '详情页面开发中',
icon: 'none'
})
}
})
}
})

View File

@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "智能项圈",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

View File

@@ -1,217 +1,102 @@
<view class="container">
<!-- 搜索区域 -->
<view class="search-box">
<input
placeholder="请输入项圈编号"
bindinput="onSearchInput"
value="{{searchValue}}"
/>
<button bindtap="onSearch" class="search-btn">查询</button>
<button wx:if="{{isSearching}}" bindtap="clearSearch" class="clear-btn">清除</button>
</view>
<!-- 搜索状态提示 -->
<view wx:if="{{isSearching}}" class="search-status">
<text>搜索项圈编号: {{searchValue}}</text>
</view>
<!-- 搜索结果 -->
<view wx:if="{{isSearching && searchResult}}" class="search-result">
<view class="result-header">
<text class="result-title">搜索结果</text>
<text class="result-subtitle">找到匹配的设备</text>
</view>
<view class="item search-item" bindtap="viewCollarDetail" data-id="{{searchResult.id}}">
<!-- 设备基本信息 -->
<view class="item-header">
<text class="device-sn">{{searchResult.snText}}</text>
<text class="device-status {{searchResult.status === '在线' ? 'online' : 'offline'}}">{{searchResult.statusText}}</text>
</view>
<!-- 设备详细信息 -->
<view class="item-content">
<view class="info-row">
<text class="label">项圈编号:</text>
<text class="value">{{searchResult.sn}}</text>
</view>
<view class="info-row">
<text class="label">佩戴状态:</text>
<text class="value {{searchResult.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{searchResult.wearStatusText}}</text>
</view>
<view class="info-row">
<text class="label">连接状态:</text>
<text class="value {{searchResult.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{searchResult.connectStatusText}}</text>
</view>
<view class="info-row">
<text class="label">电池电量:</text>
<text class="value battery-{{searchResult.batteryPercent > 50 ? 'high' : searchResult.batteryPercent > 20 ? 'medium' : 'low'}}">{{searchResult.batteryText}}</text>
</view>
<view class="info-row">
<text class="label">体温:</text>
<text class="value">{{searchResult.temperatureText}}</text>
</view>
<view class="info-row">
<text class="label">步数:</text>
<text class="value">{{searchResult.stepsText}}</text>
</view>
<view class="info-row">
<text class="label">信号强度:</text>
<text class="value">{{searchResult.signalText}}</text>
</view>
<view class="info-row">
<text class="label">GPS信号:</text>
<text class="value">{{searchResult.gpsText}}</text>
</view>
<view class="info-row">
<text class="label">位置状态:</text>
<text class="value">{{searchResult.locationText}}</text>
</view>
<view class="info-row">
<text class="label">最后更新:</text>
<text class="value">{{searchResult.lastUpdateText}}</text>
</view>
<view class="info-row">
<text class="label">更新间隔:</text>
<text class="value">{{searchResult.updateIntervalText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="item-actions">
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{searchResult.id}}" catchtap="true">查看详情</button>
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{searchResult.id}}" catchtap="true">编辑</button>
</view>
</view>
</view>
<!-- 无搜索结果 -->
<view wx:if="{{isSearching && !searchResult}}" class="no-result">
<view class="no-result-icon">🔍</view>
<text class="no-result-text">未找到项圈编号为 "{{searchValue}}" 的设备</text>
<button class="retry-btn" bindtap="clearSearch">重新搜索</button>
</view>
<!-- 数据列表 -->
<view wx:if="{{!isSearching}}" class="list">
<block wx:for="{{list}}" wx:key="id">
<view class="item" bindtap="viewCollarDetail" data-id="{{item.id}}">
<!-- 设备基本信息 -->
<view class="item-header">
<text class="device-sn">{{item.snText}}</text>
<text class="device-status {{item.status === '在线' ? 'online' : 'offline'}}">{{item.statusText}}</text>
</view>
<!-- 设备详细信息 -->
<view class="item-content">
<view class="info-row">
<text class="label">项圈编号:</text>
<text class="value">{{item.sn}}</text>
</view>
<view class="info-row">
<text class="label">佩戴状态:</text>
<text class="value {{item.is_wear === 1 ? 'wear-on' : 'wear-off'}}">{{item.wearStatusText}}</text>
</view>
<view class="info-row">
<text class="label">连接状态:</text>
<text class="value {{item.is_connect === 1 ? 'connect-on' : 'connect-off'}}">{{item.connectStatusText}}</text>
</view>
<view class="info-row">
<text class="label">电池电量:</text>
<text class="value battery-{{item.batteryPercent > 50 ? 'high' : item.batteryPercent > 20 ? 'medium' : 'low'}}">{{item.batteryText}}</text>
</view>
<view class="info-row">
<text class="label">体温:</text>
<text class="value">{{item.temperatureText}}</text>
</view>
<view class="info-row">
<text class="label">步数:</text>
<text class="value">{{item.stepsText}}</text>
</view>
<view class="info-row">
<text class="label">信号强度:</text>
<text class="value">{{item.signalText}}</text>
</view>
<view class="info-row">
<text class="label">GPS信号:</text>
<text class="value">{{item.gpsText}}</text>
</view>
<view class="info-row">
<text class="label">位置状态:</text>
<text class="value">{{item.locationText}}</text>
</view>
<view class="info-row">
<text class="label">最后更新:</text>
<text class="value">{{item.lastUpdateText}}</text>
</view>
<view class="info-row">
<text class="label">更新间隔:</text>
<text class="value">{{item.updateIntervalText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="item-actions">
<button class="btn-detail" size="mini" bindtap="viewCollarDetail" data-id="{{item.id}}" catchtap="true">查看详情</button>
<button class="btn-edit" size="mini" bindtap="editCollar" data-id="{{item.id}}" catchtap="true">编辑</button>
</view>
</view>
</block>
</view>
<!-- 分页控件 -->
<view class="pagination" wx:if="{{!isSearching && totalPages > 1}}">
<view class="pagination-info">
<text>共 {{total}} 条数据,第 {{currentPage}} / {{totalPages}} 页</text>
</view>
<view class="pagination-buttons">
<button
class="page-btn prev-btn {{currentPage <= 1 ? 'disabled' : ''}}"
bindtap="onPrevPage"
disabled="{{currentPage <= 1}}"
>
上一页
</button>
<view class="page-numbers">
<block wx:for="{{pageNumbers}}" wx:key="index">
<text
class="page-number {{currentPage === item ? 'active' : ''}}"
bindtap="onPageChange"
data-page="{{item}}"
>
{{item}}
</text>
</block>
</view>
<button
class="page-btn next-btn {{currentPage >= totalPages ? 'disabled' : ''}}"
bindtap="onNextPage"
disabled="{{currentPage >= totalPages}}"
>
下一页
</button>
</view>
</view>
</view>
<!-- pages/device/collar/collar.wxml - 智能项圈页面 -->
<view class="page-container">
<!-- 搜索 -->
<view class="search-bar">
<view class="search-input-wrapper">
<input
class="search-input"
placeholder="请输入项圈编号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
/>
<view class="search-clear" wx:if="{{searchValue}}" bindtap="clearSearch">✕</view>
</view>
<button class="search-btn" bindtap="handleSearch">搜索</button>
</view>
<!-- 数据统计 -->
<view class="data-stats">
<text class="stats-text">共 {{total}} 条数据</text>
<text class="stats-text">第 {{page}}/{{totalPages}} 页</text>
</view>
<!-- 数据列表 -->
<view class="data-list">
<view class="list-item" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<view class="item-header">
<text class="item-number">{{item.sn || item.number}}</text>
<text class="item-status {{item.status === '在线' ? 'status-online' : 'status-offline'}}">
{{item.status || '离线'}}
</text>
</view>
<view class="item-content">
<view class="item-row">
<text class="item-label">设备状态:</text>
<text class="item-value">{{item.deviceStatus || '-'}}</text>
</view>
<view class="item-row">
<text class="item-label">佩戴状态:</text>
<text class="item-value">{{item.undefinedInfo || (item.is_wear ? '已佩戴' : '未佩戴')}}</text>
</view>
<view class="item-row">
<text class="item-label">电量:</text>
<text class="item-value">{{item.battery || 0}}%</text>
</view>
<view class="item-row">
<text class="item-label">温度:</text>
<text class="item-value">{{item.temperature || '-'}}℃</text>
</view>
<view class="item-row">
<text class="item-label">步数:</text>
<text class="item-value">{{item.steps || 0}}</text>
</view>
<view class="item-row">
<text class="item-label">定位:</text>
<text class="item-value">{{item.location || '-'}}</text>
</view>
<view class="item-row">
<text class="item-label">更新时间:</text>
<text class="item-value">{{item.lastUpdate || item.updateTime || '-'}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{!loading && list.length === 0}}">
<text class="empty-text">暂无数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 分页器 -->
<view class="pagination" wx:if="{{totalPages > 1}}">
<view class="page-btn {{page === 1 ? 'disabled' : ''}}" bindtap="prevPage">
<text>上一页</text>
</view>
<view class="page-numbers">
<block wx:for="{{totalPages}}" wx:key="index">
<view
class="page-number {{page === index + 1 ? 'active' : ''}}"
wx:if="{{index + 1 === 1 || index + 1 === totalPages || (index + 1 >= page - 2 && index + 1 <= page + 2)}}"
bindtap="changePage"
data-page="{{index + 1}}"
>
{{index + 1}}
</view>
<view class="page-ellipsis" wx:if="{{index + 1 === page - 3 || index + 1 === page + 3}}">
...
</view>
</block>
</view>
<view class="page-btn {{page === totalPages ? 'disabled' : ''}}" bindtap="nextPage">
<text>下一页</text>
</view>
</view>
</view>

View File

@@ -1,356 +1,215 @@
.container {
padding: 20rpx;
}
.search-box {
display: flex;
margin-bottom: 20rpx;
gap: 15rpx;
align-items: center;
}
.search-box input {
flex: 1;
border: 2rpx solid #e8e8e8;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
background: #fafafa;
transition: all 0.3s ease;
}
.search-box input:focus {
border-color: #1890ff;
background: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1);
}
.search-btn {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
}
.search-btn:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
.clear-btn {
background: linear-gradient(135deg, #ff4d4f, #ff7875);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 30rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.3);
transition: all 0.3s ease;
}
.clear-btn:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
}
.search-status {
background: linear-gradient(135deg, #e6f7ff, #bae7ff);
border: 2rpx solid #91d5ff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
text-align: center;
}
.search-status text {
color: #1890ff;
font-size: 28rpx;
font-weight: bold;
}
.search-result {
margin-bottom: 30rpx;
}
.result-header {
background: linear-gradient(135deg, #f6ffed, #d9f7be);
border: 2rpx solid #b7eb8f;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
text-align: center;
}
.result-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #52c41a;
margin-bottom: 10rpx;
}
.result-subtitle {
display: block;
font-size: 24rpx;
color: #73d13d;
}
.search-item {
border: 2rpx solid #52c41a;
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.2);
}
.no-result {
text-align: center;
padding: 80rpx 40rpx;
background: #fafafa;
border-radius: 12rpx;
margin-bottom: 30rpx;
}
.no-result-icon {
font-size: 80rpx;
margin-bottom: 30rpx;
}
.no-result-text {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
line-height: 1.5;
}
.retry-btn {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border: none;
border-radius: 25rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
.list {
margin-bottom: 30rpx;
}
.item {
padding: 20rpx;
margin-bottom: 20rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
border: 1rpx solid #eee;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.device-sn {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.device-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: bold;
}
.device-status.online {
background: #e8f5e8;
color: #52c41a;
}
.device-status.offline {
background: #fff2e8;
color: #fa8c16;
}
.item-content {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
padding: 8rpx 0;
}
.info-row .label {
font-size: 28rpx;
color: #666;
min-width: 140rpx;
}
.info-row .value {
font-size: 28rpx;
color: #333;
text-align: right;
flex: 1;
}
/* 状态颜色 */
.wear-on {
color: #52c41a;
font-weight: bold;
}
.wear-off {
color: #ff4d4f;
font-weight: bold;
}
.connect-on {
color: #52c41a;
font-weight: bold;
}
.connect-off {
color: #ff4d4f;
font-weight: bold;
}
.battery-high {
color: #52c41a;
font-weight: bold;
}
.battery-medium {
color: #fa8c16;
font-weight: bold;
}
.battery-low {
color: #ff4d4f;
font-weight: bold;
}
.item-actions {
display: flex;
justify-content: flex-end;
gap: 20rpx;
padding-top: 15rpx;
border-top: 1rpx solid #f0f0f0;
}
.btn-detail {
background: #1890ff;
color: white;
border: none;
border-radius: 6rpx;
}
.btn-edit {
background: #52c41a;
color: white;
border: none;
border-radius: 6rpx;
}
.pagination {
margin-top: 40rpx;
padding: 30rpx;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
border: 2rpx solid #e8e8e8;
}
.pagination-info {
text-align: center;
margin-bottom: 30rpx;
font-size: 28rpx;
color: #666;
font-weight: 500;
background: rgba(255, 255, 255, 0.8);
padding: 15rpx;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
}
.pagination-buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 15rpx;
flex-wrap: wrap;
}
.page-btn {
padding: 16rpx 28rpx;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
border: 2rpx solid #d9d9d9;
background: linear-gradient(135deg, #fff, #f8f9fa);
color: #333;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
min-width: 80rpx;
}
.page-btn:not(.disabled):active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.2);
}
.page-btn.disabled {
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
color: #ccc;
border-color: #d9d9d9;
cursor: not-allowed;
}
.page-numbers {
display: flex;
gap: 8rpx;
margin: 0 20rpx;
}
.page-number {
padding: 16rpx 20rpx;
border: 2rpx solid #d9d9d9;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
color: #333;
background: linear-gradient(135deg, #fff, #f8f9fa);
min-width: 60rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.page-number:active {
transform: translateY(2rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.2);
}
.page-number.active {
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
border-color: #1890ff;
font-weight: bold;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transform: scale(1.05);
}
/* pages/device/collar/collar.wxss - 智能项圈样式 */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
/* 搜索栏 */
.search-bar {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
background-color: #ffffff;
padding: 20rpx;
border-radius: 12rpx;
}
.search-input-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
}
.search-input {
flex: 1;
height: 64rpx;
font-size: 28rpx;
}
.search-clear {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #cccccc;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.search-btn {
width: 120rpx;
height: 64rpx;
line-height: 64rpx;
background-color: #52c41a;
color: #ffffff;
font-size: 28rpx;
border-radius: 8rpx;
padding: 0;
border: none;
}
.search-btn::after {
border: none;
}
/* 数据统计 */
.data-stats {
display: flex;
justify-content: space-between;
padding: 20rpx;
background-color: #ffffff;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.stats-text {
font-size: 26rpx;
color: #666666;
}
/* 数据列表 */
.data-list {
margin-bottom: 20rpx;
}
.list-item {
background-color: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.item-number {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.item-status {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.status-online {
background-color: #e8f9f0;
color: #52c41a;
}
.status-offline {
background-color: #fff1f0;
color: #ff4d4f;
}
.item-content {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.item-row {
display: flex;
font-size: 28rpx;
}
.item-label {
color: #999999;
min-width: 160rpx;
}
.item-value {
color: #333333;
flex: 1;
}
/* 空状态 */
.empty-state {
padding: 120rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
/* 加载状态 */
.loading-state {
padding: 80rpx 0;
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #999999;
}
/* 分页器 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16rpx;
padding: 20rpx;
background-color: #ffffff;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
.page-btn {
padding: 12rpx 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333333;
}
.page-btn.disabled {
opacity: 0.4;
}
.page-numbers {
display: flex;
align-items: center;
gap: 8rpx;
}
.page-number {
min-width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333333;
}
.page-number.active {
background-color: #52c41a;
color: #ffffff;
font-weight: bold;
}
.page-ellipsis {
padding: 0 8rpx;
font-size: 26rpx;
color: #999999;
}

View File

@@ -0,0 +1,84 @@
// pages/device/device.js - 设备管理页面
Page({
data: {
deviceList: [],
activeTab: 'all',
tabs: [
{ key: 'all', name: '全部' },
{ key: 'eartag', name: '耳标' },
{ key: 'collar', name: '项圈' },
{ key: 'host', name: '主机' },
{ key: 'fence', name: '围栏' }
],
loading: false
},
onLoad() {
console.log('设备管理页加载')
this.loadDeviceList()
},
onShow() {
console.log('设备管理页显示')
this.loadDeviceList()
},
onPullDownRefresh() {
this.loadDeviceList()
setTimeout(() => {
wx.stopPullDownRefresh()
}, 1000)
},
// 切换标签
switchTab(e) {
const tab = e.currentTarget.dataset.tab
this.setData({ activeTab: tab })
this.loadDeviceList()
},
// 加载设备列表
loadDeviceList() {
this.setData({ loading: true })
// TODO: 从API加载设备列表
// 模拟数据
setTimeout(() => {
this.setData({
deviceList: [
{ id: 1, name: '耳标-001', type: 'eartag', status: 'online', battery: 85 },
{ id: 2, name: '项圈-001', type: 'collar', status: 'online', battery: 92 },
{ id: 3, name: '主机-001', type: 'host', status: 'online', battery: 100 },
{ id: 4, name: '围栏-001', type: 'fence', status: 'online', battery: 78 }
],
loading: false
})
}, 500)
},
// 查看设备详情
viewDeviceDetail(e) {
const id = e.currentTarget.dataset.id
const type = e.currentTarget.dataset.type
// 根据设备类型跳转到不同的详情页
const urlMap = {
eartag: `/pages/device/eartag/eartag?id=${id}`,
collar: `/pages/device/collar/collar?id=${id}`,
host: `/pages/device/host/host?id=${id}`,
fence: `/pages/device/fence/fence?id=${id}`
}
const url = urlMap[type] || `/pages/device/eartag/eartag?id=${id}`
wx.navigateTo({ url })
},
// 添加设备
addDevice() {
wx.navigateTo({
url: '/pages/device/eartag-add/eartag-add'
})
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "设备管理",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6",
"usingComponents": {}
}

View File

@@ -1,148 +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>
<!--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

@@ -1,332 +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;
}
}
/* 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

@@ -1,68 +1,68 @@
<!--pages/device/eartag-add/eartag-add.wxml-->
<view class="eartag-add-container">
<!-- 顶部导航 -->
<view class="header">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<text class="title">添加耳标</text>
<view class="placeholder"></view>
</view>
<!-- 表单 -->
<view class="form-container">
<view class="form-item">
<text class="label">耳标编号</text>
<input
class="input"
placeholder="请输入耳标编号"
bindinput="onEartagNumberInput"
value="{{formData.eartagNumber}}"
/>
</view>
<view class="form-item">
<text class="label">主机号</text>
<input
class="input"
placeholder="请输入主机号"
bindinput="onHostNumberInput"
value="{{formData.hostNumber}}"
/>
</view>
<view class="form-item">
<text class="label">初始电量</text>
<input
class="input"
placeholder="请输入初始电量"
type="number"
bindinput="onBatteryInput"
value="{{formData.batteryLevel}}"
/>
</view>
<view class="form-item">
<text class="label">备注</text>
<textarea
class="textarea"
placeholder="请输入备注信息"
bindinput="onRemarkInput"
value="{{formData.remark}}"
/>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-container">
<view class="btn submit-btn" bindtap="onSubmit">
<text>添加耳标</text>
</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">添加中...</text>
</view>
</view>
<!--pages/device/eartag-add/eartag-add.wxml-->
<view class="eartag-add-container">
<!-- 顶部导航 -->
<view class="header">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<text class="title">添加耳标</text>
<view class="placeholder"></view>
</view>
<!-- 表单 -->
<view class="form-container">
<view class="form-item">
<text class="label">耳标编号</text>
<input
class="input"
placeholder="请输入耳标编号"
bindinput="onEartagNumberInput"
value="{{formData.eartagNumber}}"
/>
</view>
<view class="form-item">
<text class="label">主机号</text>
<input
class="input"
placeholder="请输入主机号"
bindinput="onHostNumberInput"
value="{{formData.hostNumber}}"
/>
</view>
<view class="form-item">
<text class="label">初始电量</text>
<input
class="input"
placeholder="请输入初始电量"
type="number"
bindinput="onBatteryInput"
value="{{formData.batteryLevel}}"
/>
</view>
<view class="form-item">
<text class="label">备注</text>
<textarea
class="textarea"
placeholder="请输入备注信息"
bindinput="onRemarkInput"
value="{{formData.remark}}"
/>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-container">
<view class="btn submit-btn" bindtap="onSubmit">
<text>添加耳标</text>
</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">添加中...</text>
</view>
</view>

View File

@@ -1,154 +1,154 @@
/* pages/device/eartag-add/eartag-add.wxss */
.eartag-add-container {
background: #f8f9fa;
min-height: 100vh;
}
/* 顶部导航 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #3cc51f;
color: white;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 32rpx;
font-weight: bold;
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.placeholder {
width: 60rpx;
}
/* 表单容器 */
.form-container {
background: #ffffff;
margin: 24rpx 32rpx;
border-radius: 12rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 32rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.input {
width: 100%;
padding: 20rpx 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #ffffff;
}
.input:focus {
border-color: #3cc51f;
}
.textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #ffffff;
resize: none;
}
.textarea:focus {
border-color: #3cc51f;
}
/* 提交按钮 */
.submit-container {
padding: 32rpx;
}
.submit-btn {
width: 100%;
padding: 24rpx;
background: #3cc51f;
color: #ffffff;
border-radius: 12rpx;
text-align: center;
font-size: 32rpx;
font-weight: bold;
}
.submit-btn:active {
background: #2ea617;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 80rpx;
}
.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 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.header {
padding: 20rpx 24rpx;
}
.form-container {
margin: 20rpx 24rpx;
padding: 24rpx;
}
.submit-container {
padding: 24rpx;
}
}
/* pages/device/eartag-add/eartag-add.wxss */
.eartag-add-container {
background: #f8f9fa;
min-height: 100vh;
}
/* 顶部导航 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #3cc51f;
color: white;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 32rpx;
font-weight: bold;
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.placeholder {
width: 60rpx;
}
/* 表单容器 */
.form-container {
background: #ffffff;
margin: 24rpx 32rpx;
border-radius: 12rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 32rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.input {
width: 100%;
padding: 20rpx 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #ffffff;
}
.input:focus {
border-color: #3cc51f;
}
.textarea {
width: 100%;
min-height: 120rpx;
padding: 20rpx 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
background: #ffffff;
resize: none;
}
.textarea:focus {
border-color: #3cc51f;
}
/* 提交按钮 */
.submit-container {
padding: 32rpx;
}
.submit-btn {
width: 100%;
padding: 24rpx;
background: #3cc51f;
color: #ffffff;
border-radius: 12rpx;
text-align: center;
font-size: 32rpx;
font-weight: bold;
}
.submit-btn:active {
background: #2ea617;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 80rpx;
}
.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 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.header {
padding: 20rpx 24rpx;
}
.form-container {
margin: 20rpx 24rpx;
padding: 24rpx;
}
.submit-container {
padding: 24rpx;
}
}

View File

@@ -1,112 +1,112 @@
<!--pages/device/eartag-detail/eartag-detail.wxml-->
<view class="eartag-detail-container">
<!-- 顶部导航 -->
<view class="header">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<text class="title">耳标详情</text>
<view class="placeholder"></view>
</view>
<!-- 耳标信息 -->
<view class="eartag-info">
<view class="info-header">
<text class="eartag-number">{{eartagData.eartagNumber}}</text>
<view class="bind-status {{eartagData.isBound ? 'bound' : 'unbound'}}">
{{eartagData.isBound ? '已绑定' : '未绑定'}}
</view>
</view>
<view class="info-details">
<view class="detail-item">
<text class="label">设备电量</text>
<text class="value">{{eartagData.batteryLevel}}%</text>
</view>
<view class="detail-item">
<text class="label">设备温度</text>
<text class="value">{{eartagData.temperature}}°C</text>
</view>
<view class="detail-item">
<text class="label">被采集主机</text>
<text class="value">{{eartagData.hostNumber}}</text>
</view>
<view class="detail-item">
<text class="label">总运动量</text>
<text class="value">{{eartagData.totalMovement}}</text>
</view>
<view class="detail-item">
<text class="label">今日运动量</text>
<text class="value">{{eartagData.todayMovement}}</text>
</view>
<view class="detail-item">
<text class="label">佩戴状态</text>
<text class="value">{{eartagData.wearStatus}}</text>
</view>
<view class="detail-item">
<text class="label">设备状态</text>
<text class="value">{{eartagData.deviceStatus}}</text>
</view>
<view class="detail-item">
<text class="label">位置信息</text>
<text class="value">{{eartagData.location}}</text>
</view>
<view class="detail-item">
<text class="label">GPS状态</text>
<text class="value">{{eartagData.gpsState}}</text>
</view>
<view class="detail-item">
<text class="label">纬度</text>
<text class="value">{{eartagData.latitude}}</text>
</view>
<view class="detail-item">
<text class="label">经度</text>
<text class="value">{{eartagData.longitude}}</text>
</view>
<view class="detail-item">
<text class="label">绑定状态</text>
<text class="value">{{eartagData.bindingStatus}}</text>
</view>
<view class="detail-item">
<text class="label">电压值</text>
<text class="value">{{eartagData.voltage}}V</text>
</view>
<view class="detail-item">
<text class="label">数据更新时间</text>
<text class="value">{{eartagData.updateTime}}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="btn primary" bindtap="onBind" wx:if="{{!eartagData.isBound}}">
<text>绑定牛只</text>
</view>
<view class="btn secondary" bindtap="onUnbind" wx:if="{{eartagData.isBound}}">
<text>解绑牛只</text>
</view>
<view class="btn default" bindtap="onEdit">
<text>编辑信息</text>
</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
<!--pages/device/eartag-detail/eartag-detail.wxml-->
<view class="eartag-detail-container">
<!-- 顶部导航 -->
<view class="header">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<text class="title">耳标详情</text>
<view class="placeholder"></view>
</view>
<!-- 耳标信息 -->
<view class="eartag-info">
<view class="info-header">
<text class="eartag-number">{{eartagData.eartagNumber}}</text>
<view class="bind-status {{eartagData.isBound ? 'bound' : 'unbound'}}">
{{eartagData.isBound ? '已绑定' : '未绑定'}}
</view>
</view>
<view class="info-details">
<view class="detail-item">
<text class="label">设备电量</text>
<text class="value">{{eartagData.batteryLevel}}%</text>
</view>
<view class="detail-item">
<text class="label">设备温度</text>
<text class="value">{{eartagData.temperature}}°C</text>
</view>
<view class="detail-item">
<text class="label">被采集主机</text>
<text class="value">{{eartagData.hostNumber}}</text>
</view>
<view class="detail-item">
<text class="label">总运动量</text>
<text class="value">{{eartagData.totalMovement}}</text>
</view>
<view class="detail-item">
<text class="label">今日运动量</text>
<text class="value">{{eartagData.todayMovement}}</text>
</view>
<view class="detail-item">
<text class="label">佩戴状态</text>
<text class="value">{{eartagData.wearStatus}}</text>
</view>
<view class="detail-item">
<text class="label">设备状态</text>
<text class="value">{{eartagData.deviceStatus}}</text>
</view>
<view class="detail-item">
<text class="label">位置信息</text>
<text class="value">{{eartagData.location}}</text>
</view>
<view class="detail-item">
<text class="label">GPS状态</text>
<text class="value">{{eartagData.gpsState}}</text>
</view>
<view class="detail-item">
<text class="label">纬度</text>
<text class="value">{{eartagData.latitude}}</text>
</view>
<view class="detail-item">
<text class="label">经度</text>
<text class="value">{{eartagData.longitude}}</text>
</view>
<view class="detail-item">
<text class="label">绑定状态</text>
<text class="value">{{eartagData.bindingStatus}}</text>
</view>
<view class="detail-item">
<text class="label">电压值</text>
<text class="value">{{eartagData.voltage}}V</text>
</view>
<view class="detail-item">
<text class="label">数据更新时间</text>
<text class="value">{{eartagData.updateTime}}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="btn primary" bindtap="onBind" wx:if="{{!eartagData.isBound}}">
<text>绑定牛只</text>
</view>
<view class="btn secondary" bindtap="onUnbind" wx:if="{{eartagData.isBound}}">
<text>解绑牛只</text>
</view>
<view class="btn default" bindtap="onEdit">
<text>编辑信息</text>
</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>

View File

@@ -1,183 +1,183 @@
/* pages/device/eartag-detail/eartag-detail.wxss */
.eartag-detail-container {
background: #f8f9fa;
min-height: 100vh;
}
/* 顶部导航 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #3cc51f;
color: white;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 32rpx;
font-weight: bold;
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.placeholder {
width: 60rpx;
}
/* 耳标信息 */
.eartag-info {
background: #ffffff;
margin: 24rpx 32rpx;
border-radius: 12rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.eartag-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.bind-status {
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
}
.bind-status.bound {
background: #52c41a;
color: #ffffff;
}
.bind-status.unbound {
background: #1890ff;
color: #ffffff;
}
.info-details {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.label {
font-size: 28rpx;
color: #666;
flex: 1;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
text-align: right;
flex: 1;
}
/* 操作按钮 */
.action-buttons {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.btn {
padding: 24rpx;
border-radius: 12rpx;
text-align: center;
font-size: 28rpx;
font-weight: 500;
}
.btn.primary {
background: #3cc51f;
color: #ffffff;
}
.btn.secondary {
background: #f5222d;
color: #ffffff;
}
.btn.default {
background: #ffffff;
color: #333;
border: 1rpx solid #d9d9d9;
}
.btn:active {
opacity: 0.8;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 80rpx;
}
.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 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.header {
padding: 20rpx 24rpx;
}
.eartag-info {
margin: 20rpx 24rpx;
padding: 24rpx;
}
.action-buttons {
padding: 24rpx;
}
}
/* pages/device/eartag-detail/eartag-detail.wxss */
.eartag-detail-container {
background: #f8f9fa;
min-height: 100vh;
}
/* 顶部导航 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #3cc51f;
color: white;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 32rpx;
font-weight: bold;
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.placeholder {
width: 60rpx;
}
/* 耳标信息 */
.eartag-info {
background: #ffffff;
margin: 24rpx 32rpx;
border-radius: 12rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.eartag-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.bind-status {
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
}
.bind-status.bound {
background: #52c41a;
color: #ffffff;
}
.bind-status.unbound {
background: #1890ff;
color: #ffffff;
}
.info-details {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.label {
font-size: 28rpx;
color: #666;
flex: 1;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
text-align: right;
flex: 1;
}
/* 操作按钮 */
.action-buttons {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.btn {
padding: 24rpx;
border-radius: 12rpx;
text-align: center;
font-size: 28rpx;
font-weight: 500;
}
.btn.primary {
background: #3cc51f;
color: #ffffff;
}
.btn.secondary {
background: #f5222d;
color: #ffffff;
}
.btn.default {
background: #ffffff;
color: #333;
border: 1rpx solid #d9d9d9;
}
.btn:active {
opacity: 0.8;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 80rpx;
}
.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 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.header {
padding: 20rpx 24rpx;
}
.eartag-info {
margin: 20rpx 24rpx;
padding: 24rpx;
}
.action-buttons {
padding: 24rpx;
}
}

View File

@@ -0,0 +1,202 @@
// pages/device/eartag/eartag.js - 智能耳标页面
const { API } = require('../../../utils/api.js')
Page({
data: {
list: [], // 数据列表
page: 1, // 当前页码
limit: 10, // 每页数量
total: 0, // 总数量
totalPages: 0, // 总页数
searchValue: '', // 搜索值
loading: false, // 加载状态
refreshing: false // 刷新状态
},
onLoad() {
console.log('智能耳标页面加载')
this.loadData()
},
onShow() {
console.log('智能耳标页面显示')
},
onPullDownRefresh() {
this.setData({ page: 1, refreshing: true })
this.loadData(true)
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (this.data.loading) return
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
limit: this.data.limit,
_t: Date.now(),
refresh: isRefresh
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.eartagNumber = this.data.searchValue.trim() // 按耳标编号精确查询
params.cid = this.data.searchValue.trim() // 兼容参数
params.number = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getEartagList(params)
console.log('智能耳标数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const total = res.data?.total || res.total || 0
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const eartagNumber = String(item.eartagNumber || item.cid || item.number || '')
// 精确匹配:完全相等
return eartagNumber === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的设备',
icon: 'none',
duration: 2000
})
}
}
const totalPages = Math.ceil((this.data.searchValue ? list.length : total) / this.data.limit)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page === this.data.page || page < 1 || page > this.data.totalPages) {
return
}
this.setData({ page: page })
this.loadData()
// 滚动到顶部
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
}
},
/**
* 查看详情
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/device/eartag-detail/eartag-detail?id=${id}`,
fail: () => {
wx.showToast({
title: '详情页面开发中',
icon: 'none'
})
}
})
}
})

View File

@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "智能耳标",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

View File

@@ -1,146 +1,106 @@
<!--pages/device/eartag/eartag.wxml-->
<view class="eartag-container">
<!-- 顶部搜索和添加区域 -->
<view class="header-section">
<view class="search-box">
<text class="search-icon">🔍</text>
<input
class="search-input"
placeholder="搜索"
bindinput="onSearchInput"
value="{{searchKeyword}}"
/>
</view>
<view class="add-btn" bindtap="onAddEartag">
<text class="add-icon">+</text>
</view>
</view>
<!-- 筛选标签 -->
<view class="filter-tabs">
<view
wx:for="{{filterTabs}}"
wx:key="index"
class="filter-tab {{item.active ? 'active' : ''}}"
bindtap="switchFilter"
data-type="{{item.type}}"
>
{{item.name}}:{{item.count}}
</view>
</view>
<!-- 耳标列表 -->
<view class="eartag-list">
<view
wx:for="{{eartagList}}"
wx:key="eartagNumber"
class="eartag-item"
bindtap="onEartagClick"
data-item="{{item}}"
>
<view class="eartag-header">
<text class="eartag-number">耳标编号: {{item.eartagNumber}}</text>
<view class="bind-status {{item.isBound ? 'bound' : 'unbound'}}">
{{item.isBound ? '已绑定' : '未绑定'}}
</view>
</view>
<view class="eartag-details">
<view class="detail-row">
<text class="detail-label">设备电量/%:</text>
<text class="detail-value">{{item.batteryLevel}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备温度/°C:</text>
<text class="detail-value">{{item.temperature}}</text>
</view>
<view class="detail-row">
<text class="detail-label">被采集主机:</text>
<text class="detail-value">{{item.hostNumber}}</text>
</view>
<view class="detail-row">
<text class="detail-label">总运动量:</text>
<text class="detail-value">{{item.totalMovement}}</text>
</view>
<view class="detail-row">
<text class="detail-label">今日运动量:</text>
<text class="detail-value">{{item.todayMovement}}</text>
</view>
<view class="detail-row">
<text class="detail-label">佩戴状态:</text>
<text class="detail-value">{{item.wearStatus}}</text>
</view>
<view class="detail-row">
<text class="detail-label">设备状态:</text>
<text class="detail-value">{{item.deviceStatus}}</text>
</view>
<view class="detail-row">
<text class="detail-label">位置信息:</text>
<text class="detail-value">{{item.location}}</text>
</view>
<view class="detail-row">
<text class="detail-label">数据更新时间:</text>
<text class="detail-value">{{item.updateTime}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view wx:if="{{eartagList.length === 0 && !loading}}" class="empty-state">
<text class="empty-icon">📱</text>
<text class="empty-text">暂无耳标数据</text>
</view>
</view>
<!-- 加载状态 -->
<view wx:if="{{loading}}" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 分页组件 -->
<view wx:if="{{totalPages > 1}}" class="pagination-container">
<view class="pagination-info">
<text>共 {{totalCount}} 条记录,第 {{currentPage}}/{{totalPages}} 页</text>
</view>
<view class="pagination-controls">
<!-- 上一页按钮 -->
<view
class="pagination-btn {{currentPage === 1 ? 'disabled' : ''}}"
bindtap="prevPage"
>
上一页
</view>
<!-- 页码列表 -->
<view class="pagination-pages">
<view
wx:for="{{paginationList}}"
wx:key="index"
class="pagination-page {{item.active ? 'active' : ''}} {{item.page === -1 ? 'ellipsis' : ''}}"
bindtap="goToPage"
data-page="{{item.page}}"
>
{{item.text}}
</view>
</view>
<!-- 下一页按钮 -->
<view
class="pagination-btn {{currentPage === totalPages ? 'disabled' : ''}}"
bindtap="nextPage"
>
下一页
</view>
</view>
</view>
</view>
<!-- pages/device/eartag/eartag.wxml - 智能耳标页面 -->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<input
class="search-input"
placeholder="请输入耳标编号"
value="{{searchValue}}"
bindinput="onSearchInput"
bindconfirm="handleSearch"
/>
<view class="search-clear" wx:if="{{searchValue}}" bindtap="clearSearch">✕</view>
</view>
<button class="search-btn" bindtap="handleSearch">搜索</button>
</view>
<!-- 数据统计 -->
<view class="data-stats">
<text class="stats-text">共 {{total}} 条数据</text>
<text class="stats-text">第 {{page}}/{{totalPages}} 页</text>
</view>
<!-- 数据列表 -->
<view class="data-list">
<view class="list-item" wx:for="{{list}}" wx:key="id" bindtap="viewDetail" data-id="{{item.id}}">
<view class="item-header">
<text class="item-number">{{item.eartagNumber || item.cid || item.number}}</text>
<text class="item-status {{item.deviceStatus === '在线' ? 'status-online' : 'status-offline'}}">
{{item.deviceStatus || '离线'}}
</text>
</view>
<view class="item-content">
<view class="item-row">
<text class="item-label">绑定状态:</text>
<text class="item-value">{{item.bindingStatus || '未绑定'}}</text>
</view>
<view class="item-row">
<text class="item-label">佩戴状态:</text>
<text class="item-value">{{item.wearStatus || (item.is_wear ? '已佩戴' : '未佩戴')}}</text>
</view>
<view class="item-row">
<text class="item-label">采集主机:</text>
<text class="item-value">{{item.collectedHost || item.sid || '-'}}</text>
</view>
<view class="item-row">
<text class="item-label">电量:</text>
<text class="item-value">{{item.battery || item.voltage || 0}}%</text>
</view>
<view class="item-row">
<text class="item-label">温度:</text>
<text class="item-value">{{item.temperature || '-'}}℃</text>
</view>
<view class="item-row">
<text class="item-label">今日运动量:</text>
<text class="item-value">{{item.dailyMovement || 0}}</text>
</view>
<view class="item-row">
<text class="item-label">定位:</text>
<text class="item-value">{{item.location || item.gps_state || '-'}}</text>
</view>
<view class="item-row">
<text class="item-label">更新时间:</text>
<text class="item-value">{{item.lastUpdate || item.updateTime || '-'}}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{!loading && list.length === 0}}">
<text class="empty-text">暂无数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 分页器 -->
<view class="pagination" wx:if="{{totalPages > 1}}">
<view class="page-btn {{page === 1 ? 'disabled' : ''}}" bindtap="prevPage">
<text>上一页</text>
</view>
<view class="page-numbers">
<block wx:for="{{totalPages}}" wx:key="index">
<view
class="page-number {{page === index + 1 ? 'active' : ''}}"
wx:if="{{index + 1 === 1 || index + 1 === totalPages || (index + 1 >= page - 2 && index + 1 <= page + 2)}}"
bindtap="changePage"
data-page="{{index + 1}}"
>
{{index + 1}}
</view>
<view class="page-ellipsis" wx:if="{{index + 1 === page - 3 || index + 1 === page + 3}}">
...
</view>
</block>
</view>
<view class="page-btn {{page === totalPages ? 'disabled' : ''}}" bindtap="nextPage">
<text>下一页</text>
</view>
</view>
</view>

View File

@@ -1,285 +1,2 @@
/* pages/device/eartag/eartag.wxss */
.eartag-container {
background: #ffffff;
min-height: 100vh;
}
/* 顶部搜索和添加区域 */
.header-section {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
background: #3cc51f;
gap: 16rpx;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #ffffff;
border-radius: 24rpx;
padding: 16rpx 20rpx;
gap: 12rpx;
}
.search-icon {
font-size: 28rpx;
color: #999;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.add-btn {
width: 80rpx;
height: 80rpx;
background: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.add-icon {
font-size: 32rpx;
color: #3cc51f;
font-weight: bold;
}
/* 筛选标签 */
.filter-tabs {
display: flex;
background: #ffffff;
padding: 0 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.filter-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
border-bottom: 4rpx solid transparent;
}
.filter-tab.active {
color: #3cc51f;
border-bottom-color: #3cc51f;
font-weight: 500;
}
/* 耳标列表 */
.eartag-list {
padding: 24rpx 32rpx;
}
.eartag-item {
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
border: 1rpx solid #f0f0f0;
}
.eartag-item:active {
background: #f8f9fa;
transform: scale(0.98);
}
.eartag-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.eartag-number {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.bind-status {
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 500;
}
.bind-status.bound {
background: #52c41a;
color: #ffffff;
}
.bind-status.unbound {
background: #1890ff;
color: #ffffff;
}
.eartag-details {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 28rpx;
color: #666;
flex: 1;
}
.detail-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
text-align: right;
flex: 1;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 120rpx 32rpx;
color: #999;
}
.empty-icon {
font-size: 80rpx;
display: block;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 80rpx;
}
.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 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 375px) {
.header-section {
padding: 20rpx 24rpx;
}
.eartag-list {
padding: 20rpx 24rpx;
}
.eartag-item {
padding: 20rpx;
}
.eartag-number {
font-size: 28rpx;
}
.detail-label,
.detail-value {
font-size: 26rpx;
}
}
/* 分页组件样式 */
.pagination-container {
padding: 32rpx;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
.pagination-info {
text-align: center;
margin-bottom: 24rpx;
font-size: 24rpx;
color: #666;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.pagination-btn {
padding: 16rpx 24rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
text-align: center;
min-width: 120rpx;
}
.pagination-btn.disabled {
background: #f0f0f0;
color: #ccc;
}
.pagination-pages {
display: flex;
gap: 8rpx;
}
.pagination-page {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 24rpx;
color: #333;
text-align: center;
}
.pagination-page.active {
background: #3cc51f;
color: #ffffff;
}
.pagination-page.ellipsis {
background: transparent;
color: #999;
font-weight: bold;
}
/* pages/device/eartag/eartag.wxss - 智能耳标样式 */
@import "../collar/collar.wxss";

View File

@@ -1,183 +1,183 @@
<!--pages/device/fence/fence-new.wxml-->
<view class="fence-container">
<!-- 顶部标题栏 -->
<view class="header">
<view class="header-left" bindtap="goBack">
<text class="back-icon"></text>
</view>
<view class="header-title">电子围栏</view>
<view class="header-right">
<text class="menu-icon">⋯</text>
<text class="settings-icon">⚙</text>
<text class="location-icon">⊙</text>
</view>
</view>
<!-- 地图容器 -->
<view class="map-container">
<!-- 显示牧场按钮 -->
<view class="show-farm-btn">
<text>显示牧场</text>
</view>
<!-- 地图组件 -->
<map
id="fenceMap"
class="fence-map"
longitude="{{mapCenter.lng}}"
latitude="{{mapCenter.lat}}"
scale="{{mapScale}}"
markers="{{markers}}"
polygons="{{polygons}}"
bindtap="onMapTap"
bindmarkertap="onMarkerTap"
bindpolygontap="onPolygonTap"
show-location="{{true}}"
enable-3D="{{false}}"
enable-overlooking="{{false}}"
enable-zoom="{{true}}"
enable-scroll="{{true}}"
enable-rotate="{{false}}"
enable-satellite="{{false}}"
enable-traffic="{{false}}"
>
<!-- 地图控件 -->
<cover-view class="map-controls">
<!-- 设备统计 -->
<cover-view class="device-stats">
<cover-view class="stat-item">
<cover-view class="stat-label">智能采集器:</cover-view>
<cover-view class="stat-value">{{deviceStats.collectors}}</cover-view>
</cover-view>
<cover-view class="stat-item">
<cover-view class="stat-label">智能设备:</cover-view>
<cover-view class="stat-value">{{deviceStats.devices}}</cover-view>
</cover-view>
</cover-view>
<!-- 切换地图按钮 -->
<cover-view class="switch-map-btn" bindtap="switchMapType">
<cover-view class="switch-text">切换地图</cover-view>
</cover-view>
</cover-view>
<!-- 地图上的位置标记 -->
<cover-view class="location-markers">
<cover-view
wx:for="{{fenceList}}"
wx:key="id"
class="location-marker"
style="left: {{item.position.x}}px; top: {{item.position.y}}px;"
>
<cover-view class="marker-icon">📍</cover-view>
<cover-view class="marker-label">{{item.name}}</cover-view>
</cover-view>
</cover-view>
</map>
<!-- 底部信息栏 -->
<view class="bottom-info">
<view class="location-info">
<text class="location-name">{{currentLocation.name || '各德'}}</text>
</view>
<view class="coordinates-info">
<text class="coordinates">设备:{{deviceStats.total || '24065000912'}}更多>></text>
</view>
</view>
</view>
<!-- 操作面板 -->
<view class="operation-panel" wx:if="{{showOperationPanel}}">
<view class="panel-header">
<text class="panel-title">{{isCreatingFence ? '新建围栏' : '围栏详情'}}</text>
<text class="panel-close" bindtap="closeOperationPanel">×</text>
</view>
<view class="panel-content">
<view wx:if="{{isCreatingFence}}" class="create-fence-form">
<view class="form-item">
<text class="form-label">围栏名称:</text>
<input class="form-input" placeholder="请输入围栏名称" value="{{newFence.name}}" bindinput="onFenceNameInput"/>
</view>
<view class="form-item">
<text class="form-label">围栏类型:</text>
<picker class="form-picker" range="{{fenceTypes}}" value="{{newFence.typeIndex}}" bindchange="onFenceTypeChange">
<view class="picker-text">{{fenceTypes[newFence.typeIndex] || '请选择类型'}}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">描述:</text>
<textarea class="form-textarea" placeholder="请输入围栏描述" value="{{newFence.description}}" bindinput="onFenceDescInput"/>
</view>
<view class="form-item">
<text class="form-label">坐标点 ({{newFence.coordinates.length}}):</text>
<view class="coordinates-list">
<view wx:for="{{newFence.coordinates}}" wx:key="index" class="coordinate-item">
<text>点{{index + 1}}: {{item.lng}}, {{item.lat}}</text>
<text class="remove-point" bindtap="removeCoordinate" data-index="{{index}}">删除</text>
</view>
</view>
</view>
<view class="form-actions">
<button class="btn-cancel" bindtap="cancelCreateFence">取消</button>
<button class="btn-save" bindtap="saveFence" disabled="{{!canSaveFence}}">保存围栏</button>
</view>
</view>
<view wx:else class="fence-detail">
<view class="detail-item">
<text class="detail-label">围栏名称:</text>
<text class="detail-value">{{selectedFence.name}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏类型:</text>
<text class="detail-value">{{selectedFence.type}}</text>
</view>
<view class="detail-item">
<text class="detail-label">面积:</text>
<text class="detail-value">{{selectedFence.area}}平方米</text>
</view>
<view class="detail-item">
<text class="detail-label">放牧状态:</text>
<text class="detail-value">{{selectedFence.grazingStatus}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏内数量:</text>
<text class="detail-value">{{selectedFence.insideCount}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏外数量:</text>
<text class="detail-value">{{selectedFence.outsideCount}}</text>
</view>
<view class="detail-actions">
<button class="btn-edit" bindtap="editFence">编辑</button>
<button class="btn-delete" bindtap="deleteFence">删除</button>
</view>
</view>
</view>
</view>
<!-- 浮动操作按钮 -->
<view class="fab-container">
<view class="fab-main" bindtap="toggleFab">
<text class="fab-icon">{{fabExpanded ? '×' : '+'}}</text>
</view>
<view class="fab-menu" wx:if="{{fabExpanded}}">
<view class="fab-item" bindtap="startCreateFence">
<text class="fab-item-icon">📍</text>
<text class="fab-item-text">新建围栏</text>
</view>
<view class="fab-item" bindtap="refreshFences">
<text class="fab-item-icon">🔄</text>
<text class="fab-item-text">刷新数据</text>
</view>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-overlay" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">{{loadingText}}</text>
</view>
</view>
<!--pages/device/fence/fence-new.wxml-->
<view class="fence-container">
<!-- 顶部标题栏 -->
<view class="header">
<view class="header-left" bindtap="goBack">
<text class="back-icon"></text>
</view>
<view class="header-title">电子围栏</view>
<view class="header-right">
<text class="menu-icon">⋯</text>
<text class="settings-icon">⚙</text>
<text class="location-icon">⊙</text>
</view>
</view>
<!-- 地图容器 -->
<view class="map-container">
<!-- 显示牧场按钮 -->
<view class="show-farm-btn">
<text>显示牧场</text>
</view>
<!-- 地图组件 -->
<map
id="fenceMap"
class="fence-map"
longitude="{{mapCenter.lng}}"
latitude="{{mapCenter.lat}}"
scale="{{mapScale}}"
markers="{{markers}}"
polygons="{{polygons}}"
bindtap="onMapTap"
bindmarkertap="onMarkerTap"
bindpolygontap="onPolygonTap"
show-location="{{true}}"
enable-3D="{{false}}"
enable-overlooking="{{false}}"
enable-zoom="{{true}}"
enable-scroll="{{true}}"
enable-rotate="{{false}}"
enable-satellite="{{false}}"
enable-traffic="{{false}}"
>
<!-- 地图控件 -->
<cover-view class="map-controls">
<!-- 设备统计 -->
<cover-view class="device-stats">
<cover-view class="stat-item">
<cover-view class="stat-label">智能采集器:</cover-view>
<cover-view class="stat-value">{{deviceStats.collectors}}</cover-view>
</cover-view>
<cover-view class="stat-item">
<cover-view class="stat-label">智能设备:</cover-view>
<cover-view class="stat-value">{{deviceStats.devices}}</cover-view>
</cover-view>
</cover-view>
<!-- 切换地图按钮 -->
<cover-view class="switch-map-btn" bindtap="switchMapType">
<cover-view class="switch-text">切换地图</cover-view>
</cover-view>
</cover-view>
<!-- 地图上的位置标记 -->
<cover-view class="location-markers">
<cover-view
wx:for="{{fenceList}}"
wx:key="id"
class="location-marker"
style="left: {{item.position.x}}px; top: {{item.position.y}}px;"
>
<cover-view class="marker-icon">📍</cover-view>
<cover-view class="marker-label">{{item.name}}</cover-view>
</cover-view>
</cover-view>
</map>
<!-- 底部信息栏 -->
<view class="bottom-info">
<view class="location-info">
<text class="location-name">{{currentLocation.name || '各德'}}</text>
</view>
<view class="coordinates-info">
<text class="coordinates">设备:{{deviceStats.total || '24065000912'}}更多>></text>
</view>
</view>
</view>
<!-- 操作面板 -->
<view class="operation-panel" wx:if="{{showOperationPanel}}">
<view class="panel-header">
<text class="panel-title">{{isCreatingFence ? '新建围栏' : '围栏详情'}}</text>
<text class="panel-close" bindtap="closeOperationPanel">×</text>
</view>
<view class="panel-content">
<view wx:if="{{isCreatingFence}}" class="create-fence-form">
<view class="form-item">
<text class="form-label">围栏名称:</text>
<input class="form-input" placeholder="请输入围栏名称" value="{{newFence.name}}" bindinput="onFenceNameInput"/>
</view>
<view class="form-item">
<text class="form-label">围栏类型:</text>
<picker class="form-picker" range="{{fenceTypes}}" value="{{newFence.typeIndex}}" bindchange="onFenceTypeChange">
<view class="picker-text">{{fenceTypes[newFence.typeIndex] || '请选择类型'}}</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">描述:</text>
<textarea class="form-textarea" placeholder="请输入围栏描述" value="{{newFence.description}}" bindinput="onFenceDescInput"/>
</view>
<view class="form-item">
<text class="form-label">坐标点 ({{newFence.coordinates.length}}):</text>
<view class="coordinates-list">
<view wx:for="{{newFence.coordinates}}" wx:key="index" class="coordinate-item">
<text>点{{index + 1}}: {{item.lng}}, {{item.lat}}</text>
<text class="remove-point" bindtap="removeCoordinate" data-index="{{index}}">删除</text>
</view>
</view>
</view>
<view class="form-actions">
<button class="btn-cancel" bindtap="cancelCreateFence">取消</button>
<button class="btn-save" bindtap="saveFence" disabled="{{!canSaveFence}}">保存围栏</button>
</view>
</view>
<view wx:else class="fence-detail">
<view class="detail-item">
<text class="detail-label">围栏名称:</text>
<text class="detail-value">{{selectedFence.name}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏类型:</text>
<text class="detail-value">{{selectedFence.type}}</text>
</view>
<view class="detail-item">
<text class="detail-label">面积:</text>
<text class="detail-value">{{selectedFence.area}}平方米</text>
</view>
<view class="detail-item">
<text class="detail-label">放牧状态:</text>
<text class="detail-value">{{selectedFence.grazingStatus}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏内数量:</text>
<text class="detail-value">{{selectedFence.insideCount}}</text>
</view>
<view class="detail-item">
<text class="detail-label">围栏外数量:</text>
<text class="detail-value">{{selectedFence.outsideCount}}</text>
</view>
<view class="detail-actions">
<button class="btn-edit" bindtap="editFence">编辑</button>
<button class="btn-delete" bindtap="deleteFence">删除</button>
</view>
</view>
</view>
</view>
<!-- 浮动操作按钮 -->
<view class="fab-container">
<view class="fab-main" bindtap="toggleFab">
<text class="fab-icon">{{fabExpanded ? '×' : '+'}}</text>
</view>
<view class="fab-menu" wx:if="{{fabExpanded}}">
<view class="fab-item" bindtap="startCreateFence">
<text class="fab-item-icon">📍</text>
<text class="fab-item-text">新建围栏</text>
</view>
<view class="fab-item" bindtap="refreshFences">
<text class="fab-item-icon">🔄</text>
<text class="fab-item-text">刷新数据</text>
</view>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-overlay" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">{{loadingText}}</text>
</view>
</view>
</view>

View File

@@ -1,473 +1,473 @@
/* pages/device/fence/fence-new.wxss */
.fence-container {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
background: linear-gradient(135deg, #7CB342, #8BC34A);
color: white;
padding: 0 32rpx;
position: relative;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
}
.back-icon {
font-size: 48rpx;
font-weight: bold;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
}
.header-right {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon, .settings-icon, .location-icon {
font-size: 32rpx;
}
/* 地图容器 */
.map-container {
width: 100%;
height: calc(100vh - 88rpx);
position: relative;
}
.show-farm-btn {
position: absolute;
top: 20rpx;
left: 20rpx;
background: #7CB342;
color: white;
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
z-index: 10;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.fence-map {
width: 100%;
height: 100%;
}
/* 地图控件 */
.map-controls {
position: absolute;
top: 20rpx;
left: 20rpx;
right: 20rpx;
z-index: 5;
}
.device-stats {
position: absolute;
top: 80rpx;
left: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 16rpx 24rpx;
border-radius: 12rpx;
font-size: 24rpx;
}
.stat-item {
display: flex;
margin-bottom: 8rpx;
}
.stat-item:last-child {
margin-bottom: 0;
}
.stat-label {
margin-right: 16rpx;
}
.stat-value {
font-weight: bold;
}
.switch-map-btn {
position: absolute;
top: 0;
right: 0;
background: #7CB342;
color: white;
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
/* 位置标记 */
.location-markers {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.location-marker {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
}
.marker-icon {
font-size: 32rpx;
color: #f5222d;
}
.marker-label {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 20rpx;
margin-top: 4rpx;
}
/* 底部信息栏 */
.bottom-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
padding: 24rpx 32rpx;
border-top: 1rpx solid #e8e8e8;
}
.location-info {
margin-bottom: 12rpx;
}
.location-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.coordinates-info {
display: flex;
align-items: center;
}
.coordinates {
font-size: 24rpx;
color: #1890ff;
}
/* 操作面板 */
.operation-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-radius: 24rpx 24rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
z-index: 200;
max-height: 80vh;
overflow-y: auto;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.panel-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.panel-close {
font-size: 48rpx;
color: #999;
}
.panel-content {
padding: 32rpx;
}
/* 创建围栏表单 */
.create-fence-form {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.form-item {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.form-input, .form-textarea {
padding: 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 12rpx;
font-size: 28rpx;
background: #fafafa;
}
.form-textarea {
min-height: 120rpx;
resize: none;
}
.form-picker {
padding: 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 12rpx;
background: #fafafa;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
.coordinates-list {
display: flex;
flex-direction: column;
gap: 16rpx;
max-height: 200rpx;
overflow-y: auto;
}
.coordinate-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 24rpx;
}
.remove-point {
color: #f5222d;
font-size: 24rpx;
}
.form-actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
}
.btn-cancel, .btn-save {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-save {
background: #7CB342;
color: white;
}
.btn-save:disabled {
background: #d9d9d9;
color: #999;
}
/* 围栏详情 */
.fence-detail {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-label {
font-size: 28rpx;
color: #666;
}
.detail-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.detail-actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
}
.btn-edit, .btn-delete {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.btn-edit {
background: #1890ff;
color: white;
}
.btn-delete {
background: #f5222d;
color: white;
}
/* 浮动操作按钮 */
.fab-container {
position: fixed;
bottom: 120rpx;
right: 32rpx;
z-index: 150;
}
.fab-main {
width: 112rpx;
height: 112rpx;
background: #7CB342;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(124, 179, 66, 0.4);
}
.fab-icon {
font-size: 48rpx;
color: white;
font-weight: bold;
}
.fab-menu {
position: absolute;
bottom: 140rpx;
right: 0;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.fab-item {
display: flex;
align-items: center;
gap: 16rpx;
background: white;
padding: 16rpx 24rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
white-space: nowrap;
}
.fab-item-icon {
font-size: 32rpx;
}
.fab-item-text {
font-size: 24rpx;
color: #333;
}
/* 加载提示 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
}
.loading-content {
background: white;
padding: 48rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 24rpx;
}
.loading-text {
font-size: 28rpx;
color: #333;
}
/* 响应式调整 */
@media (max-width: 750rpx) {
.header {
height: 80rpx;
padding: 0 24rpx;
}
.header-title {
font-size: 32rpx;
}
.map-container {
height: calc(100vh - 80rpx);
}
.fab-container {
bottom: 100rpx;
right: 24rpx;
}
/* pages/device/fence/fence-new.wxss */
.fence-container {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
background: linear-gradient(135deg, #7CB342, #8BC34A);
color: white;
padding: 0 32rpx;
position: relative;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
}
.back-icon {
font-size: 48rpx;
font-weight: bold;
}
.header-title {
font-size: 36rpx;
font-weight: bold;
}
.header-right {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon, .settings-icon, .location-icon {
font-size: 32rpx;
}
/* 地图容器 */
.map-container {
width: 100%;
height: calc(100vh - 88rpx);
position: relative;
}
.show-farm-btn {
position: absolute;
top: 20rpx;
left: 20rpx;
background: #7CB342;
color: white;
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
z-index: 10;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.fence-map {
width: 100%;
height: 100%;
}
/* 地图控件 */
.map-controls {
position: absolute;
top: 20rpx;
left: 20rpx;
right: 20rpx;
z-index: 5;
}
.device-stats {
position: absolute;
top: 80rpx;
left: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 16rpx 24rpx;
border-radius: 12rpx;
font-size: 24rpx;
}
.stat-item {
display: flex;
margin-bottom: 8rpx;
}
.stat-item:last-child {
margin-bottom: 0;
}
.stat-label {
margin-right: 16rpx;
}
.stat-value {
font-weight: bold;
}
.switch-map-btn {
position: absolute;
top: 0;
right: 0;
background: #7CB342;
color: white;
padding: 16rpx 32rpx;
border-radius: 24rpx;
font-size: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
/* 位置标记 */
.location-markers {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.location-marker {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
}
.marker-icon {
font-size: 32rpx;
color: #f5222d;
}
.marker-label {
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 20rpx;
margin-top: 4rpx;
}
/* 底部信息栏 */
.bottom-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
padding: 24rpx 32rpx;
border-top: 1rpx solid #e8e8e8;
}
.location-info {
margin-bottom: 12rpx;
}
.location-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.coordinates-info {
display: flex;
align-items: center;
}
.coordinates {
font-size: 24rpx;
color: #1890ff;
}
/* 操作面板 */
.operation-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-radius: 24rpx 24rpx 0 0;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
z-index: 200;
max-height: 80vh;
overflow-y: auto;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.panel-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.panel-close {
font-size: 48rpx;
color: #999;
}
.panel-content {
padding: 32rpx;
}
/* 创建围栏表单 */
.create-fence-form {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.form-item {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.form-input, .form-textarea {
padding: 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 12rpx;
font-size: 28rpx;
background: #fafafa;
}
.form-textarea {
min-height: 120rpx;
resize: none;
}
.form-picker {
padding: 24rpx;
border: 1rpx solid #d9d9d9;
border-radius: 12rpx;
background: #fafafa;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
.coordinates-list {
display: flex;
flex-direction: column;
gap: 16rpx;
max-height: 200rpx;
overflow-y: auto;
}
.coordinate-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 24rpx;
}
.remove-point {
color: #f5222d;
font-size: 24rpx;
}
.form-actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
}
.btn-cancel, .btn-save {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-save {
background: #7CB342;
color: white;
}
.btn-save:disabled {
background: #d9d9d9;
color: #999;
}
/* 围栏详情 */
.fence-detail {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-label {
font-size: 28rpx;
color: #666;
}
.detail-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.detail-actions {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
}
.btn-edit, .btn-delete {
flex: 1;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.btn-edit {
background: #1890ff;
color: white;
}
.btn-delete {
background: #f5222d;
color: white;
}
/* 浮动操作按钮 */
.fab-container {
position: fixed;
bottom: 120rpx;
right: 32rpx;
z-index: 150;
}
.fab-main {
width: 112rpx;
height: 112rpx;
background: #7CB342;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(124, 179, 66, 0.4);
}
.fab-icon {
font-size: 48rpx;
color: white;
font-weight: bold;
}
.fab-menu {
position: absolute;
bottom: 140rpx;
right: 0;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.fab-item {
display: flex;
align-items: center;
gap: 16rpx;
background: white;
padding: 16rpx 24rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
white-space: nowrap;
}
.fab-item-icon {
font-size: 32rpx;
}
.fab-item-text {
font-size: 24rpx;
color: #333;
}
/* 加载提示 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
}
.loading-content {
background: white;
padding: 48rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 24rpx;
}
.loading-text {
font-size: 28rpx;
color: #333;
}
/* 响应式调整 */
@media (max-width: 750rpx) {
.header {
height: 80rpx;
padding: 0 24rpx;
}
.header-title {
font-size: 32rpx;
}
.map-container {
height: calc(100vh - 80rpx);
}
.fab-container {
bottom: 100rpx;
right: 24rpx;
}
}

View File

@@ -0,0 +1,400 @@
// pages/device/fence/fence.js
const API = require('../../../utils/api').API
Page({
data: {
list: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
loading: false,
refreshing: false,
searchValue: '',
// 地图相关
showMap: false, // 是否显示地图视图
selectedFence: null, // 当前选中的围栏
mapCenter: {
latitude: 38.5248248,
longitude: 106.2267664
},
mapMarkers: [], // 地图标记点
mapPolygons: [], // 地图多边形
mapScale: 12
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadData()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 可选:每次显示页面时刷新数据
// this.loadData()
},
/**
* 加载数据
*/
async loadData(isRefresh = false) {
if (isRefresh) {
this.setData({ refreshing: true })
}
this.setData({ loading: true })
try {
const params = {
page: this.data.page,
limit: this.data.limit,
_t: Date.now(),
refresh: isRefresh
}
// 如果有搜索条件,添加到参数中
if (this.data.searchValue) {
params.name = this.data.searchValue.trim() // 按围栏名称精确查询
params.number = this.data.searchValue.trim() // 兼容参数
}
console.log('请求参数:', params)
const res = await API.getFenceList(params)
console.log('电子围栏数据:', res)
// 根据实际返回的数据结构调整
let list = res.data?.list || res.data || res.list || []
const total = res.data?.total || res.total || list.length || 0
// 数据预处理:确保所有字段都存在
list = list.map(item => {
return {
...item,
// 确保必要字段存在
id: item.id,
name: item.name || '未命名围栏',
type: item.type || '围栏',
description: item.description || '',
isActive: item.isActive !== undefined ? item.isActive : true,
grazingStatus: item.grazingStatus || '未知',
area: item.area || '0',
center: item.center || { lng: '106.2267664', lat: '38.5248248' },
coordinates: item.coordinates || [],
insideCount: item.insideCount || 0,
outsideCount: item.outsideCount || 0,
createdAt: item.createdAt || item.created_at || '',
updatedAt: item.updatedAt || item.updated_at || ''
}
})
// 如果有搜索条件,进行前端精确过滤(确保精确匹配)
if (this.data.searchValue && this.data.searchValue.trim()) {
const searchKey = this.data.searchValue.trim()
list = list.filter(item => {
const name = String(item.name || '')
// 精确匹配:完全相等
return name === searchKey
})
console.log('精确查找结果:', list.length, '条')
if (list.length === 0) {
wx.showToast({
title: '未找到匹配的围栏',
icon: 'none',
duration: 2000
})
}
}
const totalPages = Math.ceil((this.data.searchValue ? list.length : total) / this.data.limit)
this.setData({
list: list,
total: total,
totalPages: totalPages,
loading: false,
refreshing: false
})
// 停止下拉刷新
if (isRefresh) {
wx.stopPullDownRefresh()
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false,
refreshing: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
if (isRefresh) {
wx.stopPullDownRefresh()
}
}
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
searchValue: e.detail.value
})
},
/**
* 执行搜索
*/
handleSearch() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 清空搜索
*/
clearSearch() {
this.setData({
searchValue: '',
page: 1
})
this.loadData(true)
},
/**
* 切换页码
*/
changePage(e) {
const page = e.currentTarget.dataset.page
if (page !== this.data.page && page >= 1 && page <= this.data.totalPages) {
this.setData({ page })
this.loadData()
}
},
/**
* 上一页
*/
prevPage() {
if (this.data.page > 1) {
this.setData({ page: this.data.page - 1 })
this.loadData()
}
},
/**
* 下一页
*/
nextPage() {
if (this.data.page < this.data.totalPages) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
},
/**
* 查看围栏详情(在地图上显示)
*/
viewDetail(e) {
const id = e.currentTarget.dataset.id
const index = e.currentTarget.dataset.index
console.log('查看围栏详情:', id)
const fence = this.data.list[index]
if (!fence) {
wx.showToast({
title: '围栏数据不存在',
icon: 'none'
})
return
}
// 切换到地图视图并显示该围栏
this.showFenceOnMap(fence)
},
/**
* 切换视图:列表/地图
*/
toggleView() {
const showMap = !this.data.showMap
this.setData({ showMap })
// 如果切换到地图视图,加载所有围栏到地图
if (showMap && this.data.list.length > 0) {
this.loadAllFencesToMap()
}
},
/**
* 在地图上显示单个围栏
*/
showFenceOnMap(fence) {
if (!fence || !fence.coordinates || fence.coordinates.length === 0) {
wx.showToast({
title: '围栏坐标数据不完整',
icon: 'none'
})
return
}
// 转换坐标格式
const points = fence.coordinates.map(coord => ({
latitude: parseFloat(coord.lat),
longitude: parseFloat(coord.lng)
}))
// 创建多边形
const polygon = {
points: points,
strokeWidth: 3,
strokeColor: fence.isActive ? '#52c41a' : '#ff4d4f',
fillColor: fence.isActive ? '#52c41a33' : '#ff4d4f33'
}
// 创建中心点标记
const marker = {
id: fence.id,
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng),
title: fence.name,
iconPath: '/images/marker.png',
width: 30,
height: 30,
callout: {
content: `${fence.name}\n${fence.type}\n${fence.grazingStatus}`,
fontSize: 12,
borderRadius: 5,
padding: 8,
display: 'ALWAYS'
}
}
this.setData({
showMap: true,
selectedFence: fence,
mapCenter: {
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng)
},
mapPolygons: [polygon],
mapMarkers: [marker],
mapScale: 14
})
},
/**
* 加载所有围栏到地图
*/
loadAllFencesToMap() {
const polygons = []
const markers = []
this.data.list.forEach(fence => {
if (fence.coordinates && fence.coordinates.length > 0) {
// 添加多边形
const points = fence.coordinates.map(coord => ({
latitude: parseFloat(coord.lat),
longitude: parseFloat(coord.lng)
}))
polygons.push({
points: points,
strokeWidth: 2,
strokeColor: fence.isActive ? '#52c41a' : '#ff4d4f',
fillColor: fence.isActive ? '#52c41a22' : '#ff4d4f22'
})
// 添加中心点标记
markers.push({
id: fence.id,
latitude: parseFloat(fence.center.lat),
longitude: parseFloat(fence.center.lng),
title: fence.name,
iconPath: '/images/marker.png',
width: 25,
height: 25
})
}
})
// 计算所有围栏的中心点
let centerLat = 38.5248248
let centerLng = 106.2267664
if (markers.length > 0) {
centerLat = markers.reduce((sum, m) => sum + m.latitude, 0) / markers.length
centerLng = markers.reduce((sum, m) => sum + m.longitude, 0) / markers.length
}
this.setData({
mapPolygons: polygons,
mapMarkers: markers,
mapCenter: {
latitude: centerLat,
longitude: centerLng
},
mapScale: 10
})
},
/**
* 地图标记点击事件
*/
onMarkerTap(e) {
const markerId = e.detail.markerId || e.markerId
console.log('点击标记:', markerId)
const fence = this.data.list.find(f => f.id === markerId)
if (fence) {
this.setData({ selectedFence: fence })
// 可以显示围栏详细信息
wx.showModal({
title: fence.name,
content: `类型:${fence.type}\n状态:${fence.grazingStatus}\n面积:${fence.area}平方米\n内部数量:${fence.insideCount}\n外部数量:${fence.outsideCount}`,
showCancel: false
})
}
},
/**
* 地图区域变化事件
*/
onRegionChange(e) {
console.log('地图区域变化:', e)
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ page: 1 })
this.loadData(true)
},
/**
* 上拉加载更多
*/
onReachBottom() {
if (this.data.page < this.data.totalPages && !this.data.loading) {
this.setData({ page: this.data.page + 1 })
this.loadData()
}
}
})

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