完善项目
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
"pages/profile/profile",
|
||||
"pages/login/login",
|
||||
"pages/cattle/cattle",
|
||||
"pages/cattle/transfer/transfer",
|
||||
"pages/cattle/exit/exit",
|
||||
"pages/cattle/pens/pens",
|
||||
"pages/cattle/batches/batches",
|
||||
"pages/device/device",
|
||||
"pages/device/eartag/eartag",
|
||||
"pages/device/collar/collar",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// pages/alert/alert.js
|
||||
const { get, post } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
// 引入预警相关真实接口
|
||||
const { alertApi } = require('../../services/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -14,6 +16,10 @@ Page({
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
// 详情弹窗
|
||||
detailVisible: false,
|
||||
detailData: null,
|
||||
detailPairs: [],
|
||||
alertTypes: [
|
||||
{ value: 'eartag', label: '耳标预警', icon: '🏷️' },
|
||||
{ value: 'collar', label: '项圈预警', icon: '📱' },
|
||||
@@ -28,7 +34,19 @@ Page({
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
// 允许通过URL参数设置默认筛选:?type=eartag&status=pending
|
||||
const { type, status } = options || {}
|
||||
const validTypes = this.data.alertTypes.map(t => t.value)
|
||||
const validStatuses = this.data.alertStatuses.map(s => s.value)
|
||||
|
||||
if (type && validTypes.includes(type)) {
|
||||
this.setData({ typeFilter: type })
|
||||
}
|
||||
if (status && validStatuses.includes(status)) {
|
||||
this.setData({ statusFilter: status })
|
||||
}
|
||||
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
@@ -160,34 +178,44 @@ Page({
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 查看预警详情
|
||||
viewAlertDetail(e) {
|
||||
// 查看预警详情(在当前页弹窗展示)
|
||||
async viewAlertDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
let url = ''
|
||||
switch (type) {
|
||||
case 'eartag':
|
||||
url = `/pages/alert/eartag/eartag?id=${id}`
|
||||
break
|
||||
case 'collar':
|
||||
url = `/pages/alert/collar/collar?id=${id}`
|
||||
break
|
||||
case 'fence':
|
||||
url = `/pages/alert/fence/fence?id=${id}`
|
||||
break
|
||||
case 'health':
|
||||
url = `/pages/alert/health/health?id=${id}`
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '未知预警类型',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
try {
|
||||
wx.showLoading({ title: '加载详情...' })
|
||||
|
||||
let res
|
||||
if (type === 'eartag') {
|
||||
res = await alertApi.getEartagAlertDetail(id)
|
||||
} else if (type === 'collar') {
|
||||
res = await alertApi.getCollarAlertDetail(id)
|
||||
} else {
|
||||
// 其他类型暂未实现,给出提示
|
||||
wx.showToast({ title: '该类型详情暂未实现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const detail = res?.data || res
|
||||
const detailPairs = this.mapAlertDetail(type, detail)
|
||||
|
||||
this.setData({
|
||||
detailVisible: true,
|
||||
detailData: detail,
|
||||
detailPairs
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('加载预警详情失败:', err)
|
||||
wx.showToast({ title: '加载详情失败', icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
|
||||
// 关闭详情弹窗
|
||||
closeDetail() {
|
||||
this.setData({ detailVisible: false, detailData: null, detailPairs: [] })
|
||||
},
|
||||
|
||||
// 处理预警
|
||||
@@ -386,6 +414,61 @@ Page({
|
||||
return colorMap[priority] || '#909399'
|
||||
},
|
||||
|
||||
// 映射预警详情字段为键值对用于展示
|
||||
mapAlertDetail(type, d) {
|
||||
if (!d) return []
|
||||
|
||||
// 通用映射函数
|
||||
const levelText = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
urgent: '紧急'
|
||||
}
|
||||
const typeText = {
|
||||
temperature: '温度',
|
||||
battery: '电量',
|
||||
movement: '运动',
|
||||
offline: '离线',
|
||||
gps: '定位',
|
||||
eartag: '耳标',
|
||||
collar: '项圈'
|
||||
}
|
||||
|
||||
const pairs = []
|
||||
// 基本信息
|
||||
pairs.push({ label: '预警ID', value: d.id || '-' })
|
||||
pairs.push({ label: '异常类型', value: typeText[d.alertType] || (d.alertType || '-') })
|
||||
pairs.push({ label: '异常等级', value: levelText[d.alertLevel] || (d.alertLevel || '-') })
|
||||
pairs.push({ label: '预警时间', value: d.alertTime ? this.formatTime(d.alertTime) : '-' })
|
||||
|
||||
if (type === 'eartag') {
|
||||
pairs.push({ label: '耳标编号', value: d.eartagNumber || d.deviceName || '-' })
|
||||
pairs.push({ label: '设备ID', value: d.deviceId || '-' })
|
||||
pairs.push({ label: '设备状态', value: d.deviceStatus || '-' })
|
||||
// 传感数据
|
||||
if (d.temperature !== undefined) pairs.push({ label: '温度(°C)', value: d.temperature })
|
||||
if (d.battery !== undefined) pairs.push({ label: '电量(%)', value: d.battery })
|
||||
if (d.gpsSignal) pairs.push({ label: 'GPS信号', value: d.gpsSignal })
|
||||
if (d.longitude !== undefined) pairs.push({ label: '经度', value: d.longitude })
|
||||
if (d.latitude !== undefined) pairs.push({ label: '纬度', value: d.latitude })
|
||||
if (d.movementStatus) pairs.push({ label: '运动状态', value: d.movementStatus })
|
||||
if (d.dailySteps !== undefined) pairs.push({ label: '今日步数', value: d.dailySteps })
|
||||
if (d.yesterdaySteps !== undefined) pairs.push({ label: '昨日步数', value: d.yesterdaySteps })
|
||||
if (d.totalSteps !== undefined) pairs.push({ label: '总步数', value: d.totalSteps })
|
||||
if (d.description) pairs.push({ label: '描述', value: d.description })
|
||||
} else if (type === 'collar') {
|
||||
// 可按需要加入项圈详情字段
|
||||
pairs.push({ label: '设备ID', value: d.deviceId || '-' })
|
||||
pairs.push({ label: '设备名称', value: d.deviceName || '-' })
|
||||
if (d.battery !== undefined) pairs.push({ label: '电量(%)', value: d.battery })
|
||||
if (d.temperature !== undefined) pairs.push({ label: '温度(°C)', value: d.temperature })
|
||||
if (d.description) pairs.push({ label: '描述', value: d.description })
|
||||
}
|
||||
|
||||
return pairs
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
|
||||
@@ -153,4 +153,24 @@
|
||||
<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>
|
||||
|
||||
@@ -248,6 +248,76 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
// pages/cattle/batches/batches.js
|
||||
const { cattleBatchApi } = require('../../../services/api.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
records: [],
|
||||
search: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
pages: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ search: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onSearchConfirm() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
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()
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(e) {
|
||||
const page = Number(e.currentTarget.dataset.page)
|
||||
if (!isNaN(page) && page >= 1 && page <= this.data.totalPages) {
|
||||
this.setData({ page })
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
async loadData() {
|
||||
const { page, pageSize, search } = this.data
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
// 请求参数,按照需求开启精确匹配
|
||||
const params = {
|
||||
page,
|
||||
pageSize,
|
||||
search: search || '',
|
||||
exactMatch: true,
|
||||
strictMatch: true
|
||||
}
|
||||
|
||||
const res = await cattleBatchApi.getBatches(params)
|
||||
// 统一解析数据结构
|
||||
const { list, total, page: curPage, pageSize: size, totalPages } = this.normalizeResponse(res)
|
||||
|
||||
let safeList = Array.isArray(list) ? list : []
|
||||
// 前端二次严格过滤,保证精确查询(名称或编号一模一样)
|
||||
const kw = (search || '').trim()
|
||||
if (kw) {
|
||||
safeList = safeList.filter(it => {
|
||||
const name = String(it.name || it.batch_name || it.batchName || '').trim()
|
||||
const code = String(it.code || it.batch_number || it.batchNumber || '').trim()
|
||||
return name === kw || code === kw
|
||||
})
|
||||
}
|
||||
const mapped = safeList.map(item => this.mapItem(item))
|
||||
|
||||
const pages = this.buildPages(totalPages, curPage)
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: Number(total) || safeList.length,
|
||||
page: Number(curPage) || page,
|
||||
pageSize: Number(size) || pageSize,
|
||||
totalPages: Number(totalPages) || this.calcTotalPages(Number(total) || safeList.length, Number(size) || pageSize),
|
||||
pages
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取批次列表失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 解析后端响应,兼容不同结构
|
||||
normalizeResponse(res) {
|
||||
// 可能的结构:
|
||||
// 1) { success: true, data: { list: [...], pagination: { total, page, pageSize, totalPages } } }
|
||||
// 2) { data: { items: [...], total, page, pageSize, totalPages } }
|
||||
// 3) 直接返回数组或对象
|
||||
let dataObj = res?.data ?? res
|
||||
let list = []
|
||||
let total = 0
|
||||
let page = this.data.page
|
||||
let pageSize = this.data.pageSize
|
||||
let totalPages = 1
|
||||
|
||||
if (Array.isArray(dataObj)) {
|
||||
list = dataObj
|
||||
total = dataObj.length
|
||||
} else if (dataObj && typeof dataObj === 'object') {
|
||||
// 优先 data.list / data.items
|
||||
list = dataObj.list || dataObj.items || []
|
||||
const pag = dataObj.pagination || {}
|
||||
total = dataObj.total ?? pag.total ?? (Array.isArray(list) ? list.length : 0)
|
||||
page = dataObj.page ?? pag.page ?? page
|
||||
pageSize = dataObj.pageSize ?? pag.pageSize ?? pageSize
|
||||
totalPages = dataObj.totalPages ?? pag.totalPages ?? this.calcTotalPages(total, pageSize)
|
||||
|
||||
// 如果整体包了一层 { success: true, data: {...} }
|
||||
if (!Array.isArray(list) && dataObj.data) {
|
||||
const inner = dataObj.data
|
||||
list = inner.list || inner.items || []
|
||||
const pag2 = inner.pagination || {}
|
||||
total = inner.total ?? pag2.total ?? (Array.isArray(list) ? list.length : 0)
|
||||
page = inner.page ?? pag2.page ?? page
|
||||
pageSize = inner.pageSize ?? pag2.pageSize ?? pageSize
|
||||
totalPages = inner.totalPages ?? pag2.totalPages ?? this.calcTotalPages(total, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
return { list, total, page, pageSize, totalPages }
|
||||
},
|
||||
|
||||
calcTotalPages(total, pageSize) {
|
||||
const t = Number(total) || 0
|
||||
const s = Number(pageSize) || 10
|
||||
return t > 0 ? Math.ceil(t / s) : 1
|
||||
},
|
||||
|
||||
buildPages(totalPages, current) {
|
||||
const tp = Number(totalPages) || 1
|
||||
const cur = Number(current) || 1
|
||||
const arr = []
|
||||
for (let i = 1; i <= tp; i++) {
|
||||
arr.push({ num: i, current: i === cur })
|
||||
}
|
||||
return arr
|
||||
},
|
||||
|
||||
// 字段中文映射与格式化
|
||||
mapItem(item) {
|
||||
const keyMap = {
|
||||
id: '批次ID',
|
||||
batch_id: '批次ID',
|
||||
batchId: '批次ID',
|
||||
name: '批次名称',
|
||||
batch_name: '批次名称',
|
||||
batchName: '批次名称',
|
||||
code: '批次编号',
|
||||
batch_number: '批次编号',
|
||||
batchNumber: '批次编号',
|
||||
type: '类型',
|
||||
status: '状态',
|
||||
enabled: '是否启用',
|
||||
currentCount: '当前数量',
|
||||
targetCount: '目标数量',
|
||||
capacity: '容量',
|
||||
remark: '备注',
|
||||
description: '描述',
|
||||
manager: '负责人',
|
||||
farm_id: '养殖场ID',
|
||||
farmId: '养殖场ID',
|
||||
farmName: '养殖场',
|
||||
farm: '养殖场对象',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
start_date: '开始日期',
|
||||
end_date: '结束日期',
|
||||
startDate: '开始日期',
|
||||
expectedEndDate: '预计结束日期',
|
||||
actualEndDate: '实际结束日期'
|
||||
}
|
||||
|
||||
// 头部字段
|
||||
const statusStr = this.formatStatus(item.status, item.enabled)
|
||||
const createdAtStr = this.pickTime(item, ['created_at', 'createdAt', 'create_time', 'createTime'])
|
||||
|
||||
// 规范化派生字段(farm对象拆解)
|
||||
const extra = {}
|
||||
if (item && typeof item.farm === 'object' && item.farm) {
|
||||
if (!item.farmName && item.farm.name) extra.farmName = item.farm.name
|
||||
if (!item.farmId && (item.farm.id || item.farm_id)) extra.farmId = item.farm.id || item.farm_id
|
||||
}
|
||||
const merged = { ...item, ...extra }
|
||||
|
||||
// 全量字段展示
|
||||
const pairs = Object.keys(merged || {}).map(k => {
|
||||
const zh = keyMap[k] || k
|
||||
const val = this.formatValue(k, merged[k])
|
||||
return { key: k, keyZh: zh, val }
|
||||
})
|
||||
|
||||
return {
|
||||
...merged,
|
||||
statusStr,
|
||||
createdAtStr,
|
||||
displayPairs: pairs
|
||||
}
|
||||
},
|
||||
|
||||
formatStatus(status, enabled) {
|
||||
if (enabled === true || enabled === 1) return '启用'
|
||||
if (enabled === false || enabled === 0) return '停用'
|
||||
if (status === 'enabled') return '启用'
|
||||
if (status === 'disabled') return '停用'
|
||||
if (status === 'active') return '活动'
|
||||
if (status === 'inactive') return '非活动'
|
||||
if (status === undefined || status === null) return ''
|
||||
return String(status)
|
||||
},
|
||||
|
||||
pickTime(obj, keys) {
|
||||
for (const k of keys) {
|
||||
if (obj && obj[k]) return this.formatDate(obj[k])
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
formatValue(key, val) {
|
||||
if (val === null || val === undefined) return ''
|
||||
// 时间类字段统一格式
|
||||
if (/time|date/i.test(key)) {
|
||||
return this.formatDate(val)
|
||||
}
|
||||
if (typeof val === 'number') return String(val)
|
||||
if (typeof val === 'boolean') return val ? '是' : '否'
|
||||
if (Array.isArray(val)) return val.map(v => typeof v === 'object' ? JSON.stringify(v) : String(v)).join(', ')
|
||||
if (typeof val === 'object') return JSON.stringify(val)
|
||||
return String(val)
|
||||
},
|
||||
|
||||
formatDate(input) {
|
||||
try {
|
||||
const d = new Date(input)
|
||||
if (isNaN(d.getTime())) return String(input)
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n)
|
||||
const Y = d.getFullYear()
|
||||
const M = pad(d.getMonth() + 1)
|
||||
const D = pad(d.getDate())
|
||||
const h = pad(d.getHours())
|
||||
const m = pad(d.getMinutes())
|
||||
return `${Y}-${M}-${D} ${h}:${m}`
|
||||
} catch (e) {
|
||||
return String(input)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "牛只批次设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
@@ -0,0 +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;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// pages/cattle/cattle.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { get, del } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
@@ -8,11 +8,17 @@ Page({
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
// 设备编号精确查询
|
||||
deviceNumber: '',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
// 按需求使用每页10条
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0
|
||||
total: 0,
|
||||
// 分页页码集合
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -51,32 +57,56 @@ Page({
|
||||
|
||||
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)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const cattleList = this.data.page === 1 ? newList : [...this.data.cattleList, ...newList]
|
||||
|
||||
this.setData({
|
||||
cattleList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: cattleList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// 统一兼容响应结构(尽可能兼容不同后端命名)
|
||||
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({
|
||||
@@ -106,6 +136,13 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 设备编号输入(精确查询)
|
||||
onDeviceInput(e) {
|
||||
this.setData({
|
||||
deviceNumber: e.detail.value.trim()
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
@@ -116,10 +153,27 @@ Page({
|
||||
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: []
|
||||
@@ -245,5 +299,123 @@ Page({
|
||||
// 格式化时间
|
||||
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)
|
||||
}
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
<view class="search-input-wrapper">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索牛只耳号、姓名..."
|
||||
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>
|
||||
|
||||
@@ -73,9 +74,27 @@
|
||||
<text class="detail-item">耳号: {{item.earNumber}}</text>
|
||||
<text class="detail-item">品种: {{item.breed || '未知'}}</text>
|
||||
</view>
|
||||
<view class="cattle-meta">
|
||||
<text class="meta-item">年龄: {{item.age || '未知'}}岁</text>
|
||||
<text class="meta-item">体重: {{item.weight || '未知'}}kg</text>
|
||||
|
||||
<!-- 扩展详情,展示全部字段 -->
|
||||
<view 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>
|
||||
|
||||
@@ -112,6 +131,21 @@
|
||||
</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>
|
||||
|
||||
@@ -139,6 +139,26 @@
|
||||
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;
|
||||
@@ -223,6 +243,37 @@
|
||||
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;
|
||||
|
||||
258
mini_program/farm-monitor-dashboard/pages/cattle/exit/exit.js
Normal file
258
mini_program/farm-monitor-dashboard/pages/cattle/exit/exit.js
Normal file
@@ -0,0 +1,258 @@
|
||||
// pages/cattle/exit/exit.js
|
||||
const { cattleExitApi } = require('../../../services/api')
|
||||
const { formatDate, formatTime } = require('../../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
loading: false,
|
||||
searchEarNumber: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 规范化时间戳,兼容秒/毫秒
|
||||
normalizeTs(ts) {
|
||||
if (!ts) return ''
|
||||
const n = Number(ts)
|
||||
if (!Number.isFinite(n)) return ''
|
||||
return n < 10_000_000_000 ? n * 1000 : n
|
||||
},
|
||||
|
||||
// 兼容数值时间戳与 ISO 字符串
|
||||
formatAnyTime(v) {
|
||||
if (!v) return ''
|
||||
if (typeof v === 'number') {
|
||||
const n = this.normalizeTs(v)
|
||||
return n ? `${formatDate(n)} ${formatTime(n)}` : ''
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const p = Date.parse(v)
|
||||
if (!Number.isNaN(p)) {
|
||||
return `${formatDate(p)} ${formatTime(p)}`
|
||||
}
|
||||
return v
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
this.setData({ loading: true })
|
||||
const { page, pageSize, searchEarNumber } = this.data
|
||||
try {
|
||||
// 支持分页与耳号精确查询(search)
|
||||
const resp = await cattleExitApi.getExitRecords({ page, pageSize, search: searchEarNumber })
|
||||
|
||||
let list = []
|
||||
let total = 0
|
||||
let totalPagesOverride = 0
|
||||
let isUnknownTotal = false
|
||||
|
||||
if (Array.isArray(resp)) {
|
||||
list = resp
|
||||
isUnknownTotal = true
|
||||
} else if (resp && typeof resp === 'object') {
|
||||
const data = resp.data || resp
|
||||
list = data.list || data.records || data.items || []
|
||||
total = Number(data.total || data.count || data.totalCount || 0)
|
||||
if (total > 0) {
|
||||
totalPagesOverride = Math.ceil(total / pageSize)
|
||||
} else {
|
||||
isUnknownTotal = true
|
||||
}
|
||||
}
|
||||
|
||||
let mapped = (list || []).map(this.mapRecord.bind(this))
|
||||
|
||||
// 前端再次做耳号精确过滤,确保“精确查询”要求
|
||||
if (searchEarNumber && typeof searchEarNumber === 'string') {
|
||||
const kw = searchEarNumber.trim()
|
||||
if (kw) {
|
||||
mapped = mapped.filter(r => String(r.earNumber) === kw)
|
||||
// 在精确查询场景下通常结果有限,分页根据当前页构造
|
||||
isUnknownTotal = true
|
||||
total = mapped.length
|
||||
totalPagesOverride = 1
|
||||
}
|
||||
}
|
||||
|
||||
const hasMore = isUnknownTotal ? (mapped.length >= pageSize) : (page < Math.max(1, totalPagesOverride))
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: total || 0,
|
||||
hasMore
|
||||
})
|
||||
this.buildPagination(total || 0, totalPagesOverride, isUnknownTotal)
|
||||
} catch (error) {
|
||||
console.error('获取离栏记录失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 字段映射(显示全部返回字段),并尽量做中文映射与嵌套对象处理
|
||||
mapRecord(item = {}) {
|
||||
const normalize = (v) => (v === null || v === undefined || v === '') ? '-' : v
|
||||
|
||||
// 主展示字段(可能包含嵌套对象)
|
||||
const earNumber = item.earNumber || item.earNo || item.earTag || '-'
|
||||
const exitPen = (item.originalPen && (item.originalPen.name || item.originalPen.code))
|
||||
|| (item.pen && (item.pen.name || item.pen.code))
|
||||
|| item.originalPenName || item.penName || item.barnName || item.pen || item.barn || '-'
|
||||
const exitTime = item.exitDate || item.exitTime || item.exitAt || item.createdAt || item.createTime || item.time || item.created_at || ''
|
||||
|
||||
const labelMap = {
|
||||
id: '记录ID',
|
||||
recordId: '记录编号',
|
||||
animalId: '动物ID', cattleId: '牛只ID',
|
||||
earNumber: '耳号', earNo: '耳号', earTag: '耳号',
|
||||
penName: '栏舍', barnName: '栏舍', pen: '栏舍', barn: '栏舍', penId: '栏舍ID',
|
||||
originalPen: '原栏舍', originalPenId: '原栏舍ID',
|
||||
exitDate: '离栏时间', exitTime: '离栏时间', exitAt: '离栏时间', time: '离栏时间',
|
||||
createdAt: '创建时间', createTime: '创建时间', updatedAt: '更新时间', updateTime: '更新时间',
|
||||
created_at: '创建时间', updated_at: '更新时间',
|
||||
operator: '操作人', operatorId: '操作人ID', handler: '经办人',
|
||||
remark: '备注', note: '备注', reason: '原因', exitReason: '离栏原因', status: '状态',
|
||||
disposalMethod: '处置方式', destination: '去向',
|
||||
batchId: '批次ID', batchName: '批次',
|
||||
farmId: '农场ID', farmName: '农场',
|
||||
deviceNumber: '设备编号', deviceSn: '设备编号', deviceId: '设备ID',
|
||||
weight: '体重(kg)'
|
||||
}
|
||||
|
||||
const displayFields = []
|
||||
Object.keys(item).forEach((key) => {
|
||||
let value = item[key]
|
||||
// 嵌套对象处理
|
||||
if (key === 'farm' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'farm.name', label: '农场', value: normalize(value.name) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'farm.id', label: '农场ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'pen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'pen.name', label: '栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'pen.code', label: '栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'pen.id', label: '栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'originalPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'originalPen.name', label: '原栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'originalPen.code', label: '原栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'originalPen.id', label: '原栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
|
||||
// 时间字段统一格式化
|
||||
if (/time|At|Date$|_at$/i.test(key)) {
|
||||
value = this.formatAnyTime(value) || '-'
|
||||
}
|
||||
// 将 ID 字段展示为对应的名称
|
||||
if (key === 'farmId') {
|
||||
displayFields.push({ key: 'farmId', label: '农场', value: normalize((item.farm && item.farm.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'penId') {
|
||||
displayFields.push({ key: 'penId', label: '栏舍', value: normalize((item.pen && item.pen.name) || value) })
|
||||
return
|
||||
}
|
||||
|
||||
displayFields.push({
|
||||
key,
|
||||
label: labelMap[key] || key,
|
||||
value: normalize(value)
|
||||
})
|
||||
})
|
||||
|
||||
const headerKeys = [
|
||||
'earNumber', 'earNo', 'earTag',
|
||||
'penName', 'barnName', 'pen', 'barn', 'pen.name', 'originalPen.name',
|
||||
'exitDate', 'exitTime', 'exitAt', 'time', 'createdAt', 'created_at', 'farm.name',
|
||||
'recordId', 'status'
|
||||
]
|
||||
const details = displayFields.filter(f => !headerKeys.includes(f.key))
|
||||
|
||||
return {
|
||||
id: item.id || item._id || '-',
|
||||
recordId: item.recordId || '-',
|
||||
status: item.status || '-',
|
||||
earNumber,
|
||||
pen: exitPen,
|
||||
exitTimeStr: this.formatAnyTime(exitTime) || '-',
|
||||
operator: item.operator || item.handler || '-',
|
||||
remark: item.remark || item.note || '-',
|
||||
batchName: item.batchName || '-',
|
||||
farmName: (item.farm && item.farm.name) || item.farmName || '-',
|
||||
deviceNumber: item.deviceNumber || item.deviceSn || item.deviceId || '-',
|
||||
details,
|
||||
raw: item
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入(耳号精确查询)
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchEarNumber: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onClearSearch() {
|
||||
this.setData({ searchEarNumber: '', page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 分页构造与交互
|
||||
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
|
||||
const pageSize = this.data.pageSize
|
||||
const current = this.data.page
|
||||
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)
|
||||
}
|
||||
this.setData({ pages, lastPage: totalPages })
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
if (this.data.page <= 1) return
|
||||
this.setData({ page: this.data.page - 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPageTap(e) {
|
||||
const p = Number(e.currentTarget.dataset.page)
|
||||
if (!Number.isFinite(p)) return
|
||||
if (p === this.data.page) return
|
||||
this.setData({ page: p, records: [] })
|
||||
this.loadRecords()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<view class="footer">
|
||||
<view class="tips">总数:{{total}};当前页:{{page}} / {{lastPage}}</view>
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,32 @@
|
||||
page {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.footer { padding: 12px; text-align: center; color: #666; }
|
||||
.loading { margin-top: 8px; }
|
||||
285
mini_program/farm-monitor-dashboard/pages/cattle/pens/pens.js
Normal file
285
mini_program/farm-monitor-dashboard/pages/cattle/pens/pens.js
Normal file
@@ -0,0 +1,285 @@
|
||||
// pages/cattle/pens/pens.js - 栏舍设置页面
|
||||
const { cattlePenApi } = require('../../../services/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
records: [],
|
||||
displayRecords: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
pages: [],
|
||||
currentPage: 1,
|
||||
searchName: '',
|
||||
hasMore: false,
|
||||
headerKeys: ['name', 'code', 'farmName', 'status', 'capacity', 'createdAtStr'],
|
||||
paginationWindow: 7
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords().finally(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
const { page, hasMore } = this.data
|
||||
if (hasMore) {
|
||||
this.setData({ page: page + 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
},
|
||||
|
||||
// 统一时间格式化
|
||||
formatAnyTime(val) {
|
||||
if (!val) return ''
|
||||
try {
|
||||
const d = typeof val === 'string' ? new Date(val) : new Date(val)
|
||||
if (isNaN(d.getTime())) return String(val)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mi = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${dd} ${hh}:${mi}`
|
||||
} catch (e) {
|
||||
return String(val)
|
||||
}
|
||||
},
|
||||
|
||||
// 字段中文映射
|
||||
getLabelMap() {
|
||||
return {
|
||||
id: '栏舍ID',
|
||||
name: '栏舍名称',
|
||||
penName: '栏舍名称',
|
||||
code: '栏舍编码',
|
||||
penId: '栏舍编号',
|
||||
barnId: '所在牛舍ID',
|
||||
barn: '所在牛舍',
|
||||
farmId: '养殖场ID',
|
||||
farm_id: '养殖场ID',
|
||||
farm: '养殖场',
|
||||
capacity: '容量',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
location: '位置',
|
||||
area: '面积',
|
||||
currentCount: '当前数量',
|
||||
manager: '负责人',
|
||||
contact: '联系电话',
|
||||
phone: '联系电话',
|
||||
remark: '备注',
|
||||
note: '备注',
|
||||
createdAt: '创建时间',
|
||||
created_at: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
updated_at: '更新时间',
|
||||
createdTime: '创建时间',
|
||||
updatedTime: '更新时间',
|
||||
isActive: '是否启用',
|
||||
enabled: '是否启用',
|
||||
disabled: '是否禁用'
|
||||
}
|
||||
},
|
||||
|
||||
// 映射单条记录为展示结构
|
||||
mapRecord(raw) {
|
||||
const labelMap = this.getLabelMap()
|
||||
const rec = { ...raw }
|
||||
// 头部主信息
|
||||
const name = rec.name || rec.penName || rec.pen || rec.title || ''
|
||||
const code = rec.code || rec.penCode || rec.number || rec.no || ''
|
||||
const capacity = rec.capacity || rec.size || rec.maxCapacity || ''
|
||||
const status = rec.status || rec.state || rec.enable || rec.enabled
|
||||
let farmName = ''
|
||||
if (rec.farm && typeof rec.farm === 'object') {
|
||||
farmName = rec.farm.name || rec.farm.title || rec.farm.code || ''
|
||||
} else if (rec.farmName) {
|
||||
farmName = rec.farmName
|
||||
}
|
||||
const createdAtStr = this.formatAnyTime(
|
||||
rec.createdAt || rec.createdTime || rec.createTime || rec.created_at
|
||||
)
|
||||
|
||||
// 详情区:将所有可用字段转为 label + value
|
||||
const displayFields = []
|
||||
|
||||
const pushField = (key, value) => {
|
||||
if (value === undefined || value === null || value === '') return
|
||||
const label = labelMap[key] || key
|
||||
// 时间字段统一格式(兼容驼峰/下划线)
|
||||
const isTimeKey = /(time|Time|createdAt|updatedAt|created_at|updated_at|createTime|updateTime|create_time|update_time)/i.test(key)
|
||||
const v = isTimeKey ? this.formatAnyTime(value) : value
|
||||
displayFields.push({ key, label, value: v })
|
||||
}
|
||||
|
||||
// 扁平字段
|
||||
Object.keys(rec).forEach(k => {
|
||||
const val = rec[k]
|
||||
if (typeof val !== 'object' || val === null) {
|
||||
// 避免重复添加已在头部展示的字段
|
||||
if (['name', 'penName', 'code', 'penCode'].includes(k)) return
|
||||
pushField(k, val)
|
||||
}
|
||||
})
|
||||
|
||||
// 嵌套对象:farm、barn 等
|
||||
const pushNested = (obj, prefixLabel) => {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const nameV = obj.name || obj.title || obj.code
|
||||
const idV = obj.id
|
||||
if (nameV) displayFields.push({ key: `${prefixLabel}Name`, label: `${prefixLabel}名称`, value: nameV })
|
||||
if (obj.code) displayFields.push({ key: `${prefixLabel}Code`, label: `${prefixLabel}编码`, value: obj.code })
|
||||
if (idV) displayFields.push({ key: `${prefixLabel}Id`, label: `${prefixLabel}ID`, value: idV })
|
||||
}
|
||||
|
||||
pushNested(rec.farm, '养殖场')
|
||||
pushNested(rec.barn, '所在牛舍')
|
||||
|
||||
return {
|
||||
name,
|
||||
code,
|
||||
capacity,
|
||||
status,
|
||||
farmName,
|
||||
createdAtStr,
|
||||
displayFields
|
||||
}
|
||||
},
|
||||
|
||||
// 构建分页页码
|
||||
buildPagination(total, pageSize, currentPage) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||
const windowSize = this.data.paginationWindow || 7
|
||||
let start = Math.max(1, currentPage - Math.floor(windowSize / 2))
|
||||
let end = Math.min(totalPages, start + windowSize - 1)
|
||||
// 如果窗口不足,向前回补
|
||||
start = Math.max(1, end - windowSize + 1)
|
||||
const pages = []
|
||||
for (let i = start; i <= end; i++) pages.push(i)
|
||||
return { pages, totalPages }
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
async loadRecords() {
|
||||
const { page, pageSize, searchName } = this.data
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const params = { page, pageSize }
|
||||
// 后端使用 search 参数进行关键词搜索,这里与后端保持一致
|
||||
if (searchName && searchName.trim()) params.search = searchName.trim()
|
||||
|
||||
const res = await cattlePenApi.getPens(params)
|
||||
|
||||
// 统一规范化响应结构,避免 list.map 报错
|
||||
const normalize = (response) => {
|
||||
// 纯数组直接返回
|
||||
if (Array.isArray(response)) {
|
||||
return { list: response, total: response.length, pagination: { total: response.length } }
|
||||
}
|
||||
// 优先从 data 节点取值
|
||||
const dataNode = response?.data !== undefined ? response.data : response
|
||||
|
||||
// 提取列表
|
||||
let list = []
|
||||
if (Array.isArray(dataNode)) {
|
||||
list = dataNode
|
||||
} else if (Array.isArray(dataNode?.list)) {
|
||||
list = dataNode.list
|
||||
} else if (Array.isArray(dataNode?.items)) {
|
||||
list = dataNode.items
|
||||
} else if (Array.isArray(response?.list)) {
|
||||
list = response.list
|
||||
} else if (Array.isArray(response?.items)) {
|
||||
list = response.items
|
||||
} else {
|
||||
list = []
|
||||
}
|
||||
|
||||
// 提取分页与总数
|
||||
const pagination = dataNode?.pagination || response?.pagination || {}
|
||||
const total = (
|
||||
pagination?.total ??
|
||||
dataNode?.total ??
|
||||
response?.total ??
|
||||
(typeof dataNode?.count === 'number' ? dataNode.count : undefined) ??
|
||||
(typeof response?.count === 'number' ? response.count : undefined) ??
|
||||
list.length
|
||||
)
|
||||
|
||||
return { list, total, pagination }
|
||||
}
|
||||
|
||||
const { list, total, pagination } = normalize(res)
|
||||
|
||||
// 映射展示结构
|
||||
const safeList = Array.isArray(list) ? list : []
|
||||
let mapped = safeList.map(item => this.mapRecord(item))
|
||||
|
||||
// 前端严格精确查询(避免远程不支持或模糊匹配)
|
||||
if (searchName && searchName.trim()) {
|
||||
const kw = searchName.trim()
|
||||
mapped = mapped.filter(r => String(r.name) === kw)
|
||||
}
|
||||
|
||||
const { pages, totalPages } = this.buildPagination(total, pageSize, page)
|
||||
const hasMore = page < totalPages
|
||||
|
||||
this.setData({
|
||||
records: safeList,
|
||||
displayRecords: mapped,
|
||||
total,
|
||||
pages,
|
||||
currentPage: page,
|
||||
hasMore,
|
||||
loading: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取栏舍数据失败:', error)
|
||||
wx.showToast({ title: error.message || '获取失败', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入变更
|
||||
onSearchInput(e) {
|
||||
const v = e.detail?.value ?? ''
|
||||
this.setData({ searchName: v })
|
||||
},
|
||||
|
||||
// 执行搜索(精确)
|
||||
onSearchConfirm() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 切换页码
|
||||
onPageTap(e) {
|
||||
const p = Number(e.currentTarget.dataset.page)
|
||||
if (!p || p === this.data.currentPage) return
|
||||
this.setData({ page: p })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
const { currentPage } = this.data
|
||||
if (currentPage > 1) {
|
||||
this.setData({ page: currentPage - 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
const { currentPage, pages } = this.data
|
||||
const max = pages.length ? Math.max(...pages) : currentPage
|
||||
if (currentPage < max) {
|
||||
this.setData({ page: currentPage + 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "栏舍设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
@@ -0,0 +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; }
|
||||
.empty { text-align: center; color: #999; padding: 20px 0; }
|
||||
@@ -0,0 +1,257 @@
|
||||
// pages/cattle/transfer/transfer.js
|
||||
const { cattleTransferApi } = require('../../../services/api')
|
||||
const { formatDate, formatTime } = require('../../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
loading: false,
|
||||
searchEarNumber: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 规范化时间戳,兼容秒/毫秒
|
||||
normalizeTs(ts) {
|
||||
if (!ts) return ''
|
||||
const n = Number(ts)
|
||||
if (!Number.isFinite(n)) return ''
|
||||
return n < 10_000_000_000 ? n * 1000 : n
|
||||
},
|
||||
|
||||
// 兼容数值时间戳与 ISO 字符串
|
||||
formatAnyTime(v) {
|
||||
if (!v) return ''
|
||||
if (typeof v === 'number') {
|
||||
const n = this.normalizeTs(v)
|
||||
return n ? `${formatDate(n)} ${formatTime(n)}` : ''
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const p = Date.parse(v)
|
||||
if (!Number.isNaN(p)) {
|
||||
return `${formatDate(p)} ${formatTime(p)}`
|
||||
}
|
||||
// 非法字符串,直接返回原值
|
||||
return v
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
this.setData({ loading: true })
|
||||
const { page, pageSize, searchEarNumber } = this.data
|
||||
try {
|
||||
// 使用统一的列表接口,并传递 search 参数以满足“耳号精确查询”需求
|
||||
const resp = await cattleTransferApi.getTransferRecords({ page, pageSize, search: searchEarNumber })
|
||||
|
||||
let list = []
|
||||
let total = 0
|
||||
let totalPagesOverride = 0
|
||||
let isUnknownTotal = false
|
||||
|
||||
if (Array.isArray(resp)) {
|
||||
list = resp
|
||||
// 未返回总数时,认为总数未知;是否有更多由本页数量是否满页来推断
|
||||
isUnknownTotal = true
|
||||
} else if (resp && typeof resp === 'object') {
|
||||
// 常见结构兼容:{ list, total } 或 { data: { list, total } } 或 { records, total }
|
||||
const data = resp.data || resp
|
||||
list = data.list || data.records || data.items || []
|
||||
total = Number(data.total || data.count || data.totalCount || 0)
|
||||
if (total > 0) {
|
||||
totalPagesOverride = Math.ceil(total / pageSize)
|
||||
} else {
|
||||
isUnknownTotal = true
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = (list || []).map(this.mapRecord.bind(this))
|
||||
const hasMore = isUnknownTotal ? (mapped.length >= pageSize) : (page < Math.max(1, totalPagesOverride))
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: total || 0,
|
||||
hasMore
|
||||
})
|
||||
this.buildPagination(total || 0, totalPagesOverride, isUnknownTotal)
|
||||
} catch (error) {
|
||||
console.error('获取转栏记录失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 字段映射(尽量覆盖常见后端字段命名),并保留原始字段用于“全部显示”
|
||||
mapRecord(item = {}) {
|
||||
const normalize = (v) => (v === null || v === undefined || v === '') ? '-' : v
|
||||
|
||||
// 头部主展示字段(兼容嵌套对象与旧字段)
|
||||
const earNumber = item.earNumber || item.earNo || item.earTag || '-'
|
||||
const fromPen = (item.fromPen && (item.fromPen.name || item.fromPen.code))
|
||||
|| item.fromPenName || item.fromBarnName || item.fromPen || item.fromBarn || '-'
|
||||
const toPen = (item.toPen && (item.toPen.name || item.toPen.code))
|
||||
|| item.toPenName || item.toBarnName || item.toPen || item.toBarn || '-'
|
||||
const transferTime = item.transferDate || item.transferTime || item.transferAt || item.createdAt || item.createTime || item.time || item.created_at || ''
|
||||
|
||||
// 友好中文标签映射(补充新结构字段)
|
||||
const labelMap = {
|
||||
id: '记录ID',
|
||||
recordId: '记录编号',
|
||||
animalId: '动物ID',
|
||||
cattleId: '牛只ID',
|
||||
earNumber: '耳号', earNo: '耳号', earTag: '耳号',
|
||||
fromPenName: '原栏舍', fromBarnName: '原栏舍', fromPen: '原栏舍', fromBarn: '原栏舍', fromPenId: '原栏舍ID',
|
||||
toPenName: '目标栏舍', toBarnName: '目标栏舍', toPen: '目标栏舍', toBarn: '目标栏舍', toPenId: '目标栏舍ID',
|
||||
transferDate: '转栏时间', transferTime: '转栏时间', transferAt: '转栏时间', time: '转栏时间',
|
||||
createdAt: '创建时间', createTime: '创建时间', updatedAt: '更新时间', updateTime: '更新时间',
|
||||
created_at: '创建时间', updated_at: '更新时间',
|
||||
operator: '操作人', operatorId: '操作人ID', handler: '经办人',
|
||||
remark: '备注', note: '备注', reason: '原因', status: '状态',
|
||||
batchId: '批次ID', batchName: '批次',
|
||||
farmId: '农场ID', farmName: '农场',
|
||||
deviceNumber: '设备编号', deviceSn: '设备编号', deviceId: '设备ID',
|
||||
weightBefore: '转前体重(kg)', weightAfter: '转后体重(kg)', weight: '体重(kg)'
|
||||
}
|
||||
|
||||
// 将所有字段整理为可展示的键值对,时间戳/ISO 自动格式化
|
||||
const displayFields = []
|
||||
Object.keys(item).forEach((key) => {
|
||||
let value = item[key]
|
||||
// 嵌套对象特殊处理
|
||||
if (key === 'farm' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'farm.name', label: '农场', value: normalize(value.name) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'farm.id', label: '农场ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'fromPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'fromPen.name', label: '原栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'fromPen.code', label: '原栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'fromPen.id', label: '原栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'toPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'toPen.name', label: '目标栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'toPen.code', label: '目标栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'toPen.id', label: '目标栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
|
||||
// 时间字段格式化(兼容 ISO 字符串)
|
||||
if (/time|At|Date$|_at$/i.test(key)) {
|
||||
value = this.formatAnyTime(value) || '-'
|
||||
}
|
||||
// 将 ID 字段展示为对应的名称
|
||||
if (key === 'farmId') {
|
||||
displayFields.push({ key: 'farmId', label: '农场', value: normalize((item.farm && item.farm.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'fromPenId') {
|
||||
displayFields.push({ key: 'fromPenId', label: '原栏舍', value: normalize((item.fromPen && item.fromPen.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'toPenId') {
|
||||
displayFields.push({ key: 'toPenId', label: '目标栏舍', value: normalize((item.toPen && item.toPen.name) || value) })
|
||||
return
|
||||
}
|
||||
displayFields.push({
|
||||
key,
|
||||
label: labelMap[key] || key,
|
||||
value: normalize(value)
|
||||
})
|
||||
})
|
||||
|
||||
// 去重:头部已展示的字段不在详情中重复
|
||||
const headerKeys = [
|
||||
'earNumber', 'earNo', 'earTag',
|
||||
'fromPenName', 'fromBarnName', 'fromPen', 'fromBarn', 'fromPen.name',
|
||||
'toPenName', 'toBarnName', 'toPen', 'toBarn', 'toPen.name',
|
||||
'transferDate', 'transferTime', 'transferAt', 'time', 'createdAt', 'created_at', 'farm.name'
|
||||
]
|
||||
const details = displayFields.filter(f => !headerKeys.includes(f.key))
|
||||
|
||||
return {
|
||||
id: item.id || item._id || '-',
|
||||
recordId: item.recordId || '-',
|
||||
status: item.status || '-',
|
||||
earNumber,
|
||||
fromPen,
|
||||
toPen,
|
||||
transferTimeStr: this.formatAnyTime(transferTime) || '-',
|
||||
operator: item.operator || item.handler || '-',
|
||||
remark: item.remark || item.note || '-',
|
||||
batchName: item.batchName || '-',
|
||||
farmName: (item.farm && item.farm.name) || item.farmName || '-',
|
||||
deviceNumber: item.deviceNumber || item.deviceSn || item.deviceId || '-',
|
||||
details,
|
||||
raw: item
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchEarNumber: e.detail.value || '' })
|
||||
},
|
||||
|
||||
// 执行搜索(耳号精确查询)
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({ searchEarNumber: '', page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 分页构造与交互
|
||||
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
|
||||
const pageSize = this.data.pageSize
|
||||
const current = this.data.page
|
||||
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)
|
||||
}
|
||||
this.setData({ pages, lastPage: totalPages })
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
if (this.data.page <= 1) return
|
||||
this.setData({ page: this.data.page - 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPageTap(e) {
|
||||
const targetPage = Number(e.currentTarget.dataset.page)
|
||||
if (!targetPage || targetPage === this.data.page) return
|
||||
this.setData({ page: targetPage, records: [] })
|
||||
this.loadRecords()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!-- pages/cattle/transfer/transfer.wxml -->
|
||||
<view class="transfer-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>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && records.length === 0}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,211 @@
|
||||
/* pages/cattle/transfer/transfer.wxss */
|
||||
.transfer-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;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.record-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ear-number {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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); }
|
||||
}
|
||||
@@ -77,7 +77,7 @@ Page({
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize, searchValue } = this.data
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
@@ -290,7 +290,7 @@ Page({
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/collars?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
@@ -66,7 +66,7 @@ Page({
|
||||
// 调用私有API获取耳标列表,然后筛选出指定耳标
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
|
||||
@@ -113,7 +113,7 @@ Page({
|
||||
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: page,
|
||||
@@ -500,7 +500,7 @@ Page({
|
||||
// 调用API获取所有数据的总数(不受分页限制)
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
@@ -621,7 +621,7 @@ Page({
|
||||
// 调用私有API获取所有数据,然后进行客户端搜索
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
|
||||
@@ -100,7 +100,7 @@ Page({
|
||||
if (!this.checkLoginStatus()) return
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
const url = `https://ad.ningmuyun.com/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
|
||||
@@ -61,7 +61,7 @@ Page({
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize } = this.data
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
@@ -303,7 +303,7 @@ Page({
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// pages/home/home.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { alertApi } = require('../../services/api.js')
|
||||
const { formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
@@ -123,7 +124,7 @@ Page({
|
||||
},
|
||||
|
||||
// 更新预警数据
|
||||
updateAlertData(tabIndex) {
|
||||
async updateAlertData(tabIndex) {
|
||||
let alertData = []
|
||||
|
||||
switch (tabIndex) {
|
||||
@@ -139,15 +140,9 @@ Page({
|
||||
]
|
||||
break
|
||||
case 1: // 耳标预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '2', isAlert: false, bgIcon: '📄', url: '/pages/alert/eartag' },
|
||||
{ title: '耳标脱落', value: '1', isAlert: true, bgIcon: '👂', url: '/pages/alert/eartag' },
|
||||
{ title: '温度异常', value: '0', isAlert: false, bgIcon: '🌡️', url: '/pages/alert/eartag' },
|
||||
{ title: '心率异常', value: '1', isAlert: true, bgIcon: '💓', url: '/pages/alert/eartag' },
|
||||
{ title: '位置异常', value: '0', isAlert: false, bgIcon: '📍', url: '/pages/alert/eartag' },
|
||||
{ title: '电量偏低', value: '3', isAlert: false, bgIcon: '🔋', url: '/pages/alert/eartag' }
|
||||
]
|
||||
break
|
||||
// 动态调用真实耳标预警接口,使用返回数据填充首页卡片
|
||||
await this.fetchEartagAlertCards()
|
||||
return
|
||||
case 2: // 脚环预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '1', isAlert: false, bgIcon: '📄', url: '/pages/alert/ankle' },
|
||||
@@ -169,6 +164,47 @@ Page({
|
||||
this.setData({ currentAlertData: alertData })
|
||||
},
|
||||
|
||||
// 动态加载耳标预警数据并映射到首页卡片
|
||||
async fetchEartagAlertCards() {
|
||||
try {
|
||||
this.setData({ loading: true })
|
||||
const params = { search: '', alertType: '', page: 1, limit: 10 }
|
||||
const res = await alertApi.getEartagAlerts(params)
|
||||
// 兼容响应结构:可能是 { success, data: [...], stats, pagination } 或直接返回 { data: [...], stats }
|
||||
const list = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
|
||||
const stats = res?.stats || {}
|
||||
|
||||
// 统计映射(根据公开API字段)
|
||||
const offline = Number(stats.offline || 0) // 离线数量(近似“今日未被采集”)
|
||||
const highTemperature = Number(stats.highTemperature || 0) // 温度异常数量
|
||||
const lowBattery = Number(stats.lowBattery || 0) // 电量偏低数量
|
||||
const abnormalMovement = Number(stats.abnormalMovement || 0) // 运动异常/当日运动量为0
|
||||
|
||||
// 构建首页卡片(保持既有文案,无法统计的置0)
|
||||
const alertData = [
|
||||
{ title: '今日未被采集', value: String(offline), isAlert: offline > 0, bgIcon: '📄', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '耳标脱落', value: '0', isAlert: false, bgIcon: '👂', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '温度异常', value: String(highTemperature), isAlert: highTemperature > 0, bgIcon: '🌡️', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '心率异常', value: '0', isAlert: false, bgIcon: '💓', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '位置异常', value: '0', isAlert: false, bgIcon: '📍', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '电量偏低', value: String(lowBattery), isAlert: lowBattery > 0, bgIcon: '🔋', url: '/pages/alert/alert?type=eartag' }
|
||||
]
|
||||
|
||||
// 如果有“运动异常”,在卡片上以“今日运动量异常”补充显示(替代心率异常)
|
||||
if (abnormalMovement > 0) {
|
||||
alertData.splice(3, 1, { title: '今日运动量异常', value: String(abnormalMovement), isAlert: true, bgIcon: '📉', url: '/pages/alert/alert?type=eartag' })
|
||||
}
|
||||
|
||||
this.setData({ currentAlertData: alertData })
|
||||
} catch (error) {
|
||||
console.error('获取耳标预警失败:', error)
|
||||
wx.showToast({ title: '耳标预警加载失败', icon: 'none' })
|
||||
// 失败时保持原有数据不变
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 导航到指定页面
|
||||
navigateTo(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
|
||||
@@ -126,7 +126,7 @@ Page({
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/auth/login',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: username,
|
||||
|
||||
@@ -4,6 +4,30 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
// 直接跳转:牛-栏舍设置
|
||||
goCattlePenSettings() {
|
||||
console.log('准备跳转到牛-栏舍设置页面')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/pens/pens',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 直接跳转:牛-批次设置
|
||||
goCattleBatchSettings() {
|
||||
console.log('准备跳转到牛-批次设置页面')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/batches/batches',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('生产管理页面加载')
|
||||
},
|
||||
@@ -19,6 +43,21 @@ Page({
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 进入牛只管理(普通页面)
|
||||
goCattleManage() {
|
||||
// 牛只管理并非 app.json 的 tabBar 页面,使用 navigateTo 进行跳转
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/cattle',
|
||||
fail: (err) => {
|
||||
console.error('跳转牛只管理失败:', err)
|
||||
wx.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 导航到指定页面
|
||||
navigateTo(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
@@ -75,6 +114,7 @@ Page({
|
||||
}
|
||||
|
||||
const animalNames = {
|
||||
'cattle': '牛',
|
||||
'pig': '猪',
|
||||
'sheep': '羊',
|
||||
'poultry': '家禽'
|
||||
@@ -83,6 +123,55 @@ Page({
|
||||
const functionName = functionNames[functionType] || '未知功能'
|
||||
const animalName = animalNames[animalType] || '未知动物'
|
||||
|
||||
// 对“牛-转栏记录”和“牛-离栏记录”开放跳转,其它功能保留提示
|
||||
if (animalType === 'cattle' && functionType === 'pen-transfer') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/transfer/transfer',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (animalType === 'cattle' && functionType === 'pen-exit') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/exit/exit',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 牛-栏舍设置:跳转到新创建的栏舍设置页面
|
||||
if (animalType === 'cattle' && functionType === 'pen-settings') {
|
||||
console.log('匹配到牛-栏舍设置分支,开始跳转')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/pens/pens',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 牛-批次设置:跳转到新创建的批次设置页面
|
||||
if (animalType === 'cattle' && functionType === 'batch-settings') {
|
||||
console.log('匹配到牛-批次设置分支,开始跳转')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/batches/batches',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: `${animalName}${functionName}功能开发中`,
|
||||
icon: 'none',
|
||||
|
||||
@@ -3,6 +3,71 @@
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-title">生产管理</view>
|
||||
|
||||
<!-- 牛只管理模块 -->
|
||||
<view class="management-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title-bar"></view>
|
||||
<text class="section-title">牛只管理</text>
|
||||
</view>
|
||||
|
||||
<view class="function-grid">
|
||||
<view class="function-item" bindtap="goCattleManage">
|
||||
<view class="function-icon cattle-archive">🐄</view>
|
||||
<text class="function-text">牛只档案</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="estrus-record">
|
||||
<view class="function-icon estrus-record">💗</view>
|
||||
<text class="function-text">发情记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="mating-record">
|
||||
<view class="function-icon mating-record">🧪</view>
|
||||
<text class="function-text">配种记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pregnancy-check">
|
||||
<view class="function-icon pregnancy-check">📅</view>
|
||||
<text class="function-text">妊检记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="farrowing-record">
|
||||
<view class="function-icon farrowing-record">👶</view>
|
||||
<text class="function-text">分娩记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="weaning-record">
|
||||
<view class="function-icon weaning-record">✏️</view>
|
||||
<text class="function-text">断奶记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pen-transfer">
|
||||
<view class="function-icon pen-transfer">🏠</view>
|
||||
<text class="function-text">转栏记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pen-exit">
|
||||
<view class="function-icon pen-exit">🏠</view>
|
||||
<text class="function-text">离栏记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="goCattlePenSettings" data-animal="cattle" data-type="pen-settings">
|
||||
<view class="function-icon pen-settings">🏠</view>
|
||||
<text class="function-text">栏舍设置</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="goCattleBatchSettings" data-animal="cattle" data-type="batch-settings">
|
||||
<view class="function-icon batch-settings">📄</view>
|
||||
<text class="function-text">批次设置</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="epidemic-warning">
|
||||
<view class="function-icon epidemic-warning">🛡️</view>
|
||||
<text class="function-text">防疫预警</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 猪档案管理模块 -->
|
||||
<view class="management-section">
|
||||
<view class="section-header">
|
||||
|
||||
@@ -137,6 +137,11 @@
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 牛只管理图标颜色 */
|
||||
.function-icon.cattle-archive {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.function-icon.scan-entry {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const app = getApp()
|
||||
// 基础配置
|
||||
const config = {
|
||||
// 使用真实的智能耳标API接口(直接连接后端)
|
||||
baseUrl: 'https://ad.ningmuyun.com/api', // 智能耳标API地址
|
||||
baseUrl: 'https://ad.ningmuyun.com/farm/api', // 智能耳标API地址(生产环境)
|
||||
timeout: 10000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -89,49 +89,81 @@ const responseInterceptor = (response) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
// 通用请求方法(带故障转移)
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 应用请求拦截器
|
||||
const processedOptions = requestInterceptor({
|
||||
url: config.baseUrl + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
...config.header,
|
||||
...options.header
|
||||
},
|
||||
timeout: options.timeout || config.timeout
|
||||
})
|
||||
|
||||
wx.request({
|
||||
...processedOptions,
|
||||
success: (response) => {
|
||||
console.log('wx.request成功:', response)
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
console.log('响应拦截器处理结果:', result)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.log('响应拦截器错误:', error)
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('wx.request失败:', error)
|
||||
let message = '网络连接异常'
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败'
|
||||
const failoverBases = [
|
||||
'http://localhost:5350/api',
|
||||
'http://127.0.0.1:5350/api'
|
||||
]
|
||||
|
||||
const allowFailover = options.allowFailover !== false
|
||||
|
||||
const doWxRequest = (baseUrlToUse, isFailoverAttempt = false) => {
|
||||
// 应用请求拦截器
|
||||
const processedOptions = requestInterceptor({
|
||||
url: baseUrlToUse + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
...config.header,
|
||||
...options.header
|
||||
},
|
||||
timeout: options.timeout || config.timeout
|
||||
})
|
||||
|
||||
console.log('[API] 请求URL:', processedOptions.url, '方法:', processedOptions.method, '是否故障转移:', isFailoverAttempt)
|
||||
|
||||
wx.request({
|
||||
...processedOptions,
|
||||
success: (response) => {
|
||||
console.log('[API] 响应状态码:', response.statusCode, '请求URL:', processedOptions.url)
|
||||
// 当远程网关或上游故障(5xx)时,自动尝试本地备用服务(5350)
|
||||
if (
|
||||
response.statusCode >= 500 &&
|
||||
allowFailover &&
|
||||
!isFailoverAttempt &&
|
||||
baseUrlToUse === config.baseUrl &&
|
||||
failoverBases.length > 0
|
||||
) {
|
||||
console.warn('[API] 远程服务返回', response.statusCode, ',尝试切换到本地备用服务:', failoverBases[0])
|
||||
return doWxRequest(failoverBases[0], true)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
console.log('响应拦截器处理结果:', result)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.log('响应拦截器错误:', error)
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('wx.request失败:', error, '请求URL:', processedOptions.url)
|
||||
let message = '网络连接异常'
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是远程网络失败(非HTTP响应),也尝试本地备用
|
||||
if (allowFailover && !isFailoverAttempt && baseUrlToUse === config.baseUrl && failoverBases.length > 0) {
|
||||
console.warn('[API] 远程网络失败,尝试切换到本地备用服务:', failoverBases[0])
|
||||
return doWxRequest(failoverBases[0], true)
|
||||
}
|
||||
|
||||
reject(new Error(message))
|
||||
}
|
||||
|
||||
reject(new Error(message))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 首次请求使用当前配置的基础地址
|
||||
doWxRequest(config.baseUrl, false)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user