refactor: 重构数据库配置为SQLite开发环境并移除冗余文档

This commit is contained in:
2025-09-21 15:16:48 +08:00
parent d207610009
commit 3c8648a635
259 changed files with 88239 additions and 8379 deletions

View File

@@ -0,0 +1,284 @@
<template>
<view id="app">
<!-- 应用入口 -->
</view>
</template>
<script>
export default {
name: 'App',
onLaunch: function() {
console.log('养殖管理小程序启动');
// 初始化应用
this.initApp();
},
onShow: function() {
console.log('养殖管理小程序显示');
},
onHide: function() {
console.log('养殖管理小程序隐藏');
},
methods: {
// 初始化应用
initApp() {
// 检查更新
this.checkUpdate();
// 初始化全局数据
this.initGlobalData();
// 检查登录状态
this.checkLoginStatus();
},
// 检查小程序更新
checkUpdate() {
// #ifdef MP-WEIXIN
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('发现新版本');
}
});
updateManager.onUpdateReady(() => {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate();
}
}
});
});
updateManager.onUpdateFailed(() => {
uni.showModal({
title: '更新失败',
content: '新版本下载失败,请检查网络后重试',
showCancel: false
});
});
}
// #endif
},
// 初始化全局数据
initGlobalData() {
// 设置全局数据
getApp().globalData = {
userInfo: null,
token: '',
baseUrl: 'https://api.farming.com',
version: '1.0.0'
};
},
// 检查登录状态
checkLoginStatus() {
const token = uni.getStorageSync('token');
if (token) {
getApp().globalData.token = token;
// 验证token有效性
this.validateToken(token);
}
},
// 验证token
async validateToken(token) {
try {
const res = await uni.request({
url: getApp().globalData.baseUrl + '/auth/validate',
method: 'POST',
header: {
'Authorization': 'Bearer ' + token
}
});
if (res.data.code === 200) {
getApp().globalData.userInfo = res.data.data;
} else {
// token无效清除本地存储
uni.removeStorageSync('token');
getApp().globalData.token = '';
}
} catch (error) {
console.error('验证token失败', error);
}
}
}
};
</script>
<style lang="scss">
/* 全局样式 */
@import '@/uni_modules/uni-scss/index.scss';
/* 应用全局样式 */
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
/* 通用样式 */
.container {
padding: 20rpx;
}
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
/* 按钮样式 */
.btn-primary {
background: linear-gradient(135deg, #2E8B57, #3CB371);
color: #ffffff;
border: none;
border-radius: 12rpx;
padding: 24rpx 48rpx;
font-size: 32rpx;
font-weight: 500;
}
.btn-secondary {
background: #ffffff;
color: #2E8B57;
border: 2rpx solid #2E8B57;
border-radius: 12rpx;
padding: 22rpx 46rpx;
font-size: 32rpx;
}
/* 文本样式 */
.text-primary {
color: #2E8B57;
}
.text-secondary {
color: #666666;
}
.text-muted {
color: #999999;
}
/* 布局样式 */
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.flex-center {
align-items: center;
justify-content: center;
}
.flex-between {
justify-content: space-between;
}
.flex-around {
justify-content: space-around;
}
.flex-1 {
flex: 1;
}
/* 间距样式 */
.mt-10 { margin-top: 10rpx; }
.mt-20 { margin-top: 20rpx; }
.mt-30 { margin-top: 30rpx; }
.mb-10 { margin-bottom: 10rpx; }
.mb-20 { margin-bottom: 20rpx; }
.mb-30 { margin-bottom: 30rpx; }
.ml-10 { margin-left: 10rpx; }
.ml-20 { margin-left: 20rpx; }
.mr-10 { margin-right: 10rpx; }
.mr-20 { margin-right: 20rpx; }
.pt-10 { padding-top: 10rpx; }
.pt-20 { padding-top: 20rpx; }
.pb-10 { padding-bottom: 10rpx; }
.pb-20 { padding-bottom: 20rpx; }
.pl-10 { padding-left: 10rpx; }
.pl-20 { padding-left: 20rpx; }
.pr-10 { padding-right: 10rpx; }
.pr-20 { padding-right: 20rpx; }
/* 字体大小 */
.font-12 { font-size: 24rpx; }
.font-14 { font-size: 28rpx; }
.font-16 { font-size: 32rpx; }
.font-18 { font-size: 36rpx; }
.font-20 { font-size: 40rpx; }
.font-24 { font-size: 48rpx; }
/* 字体粗细 */
.font-normal { font-weight: normal; }
.font-bold { font-weight: bold; }
/* 文本对齐 */
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* 溢出处理 */
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ellipsis-2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 动画 */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式 */
@media screen and (max-width: 750rpx) {
.container {
padding: 15rpx;
}
}
</style>

View File

@@ -1,39 +1,61 @@
// 养殖管理小程序
App({
onLaunch() {
// 小程序初始化
console.log('牛肉商城小程序初始化');
console.log('养殖管理小程序初始化');
// 检查登录状态
this.checkLoginStatus();
// 初始化系统信息
this.getSystemInfo();
},
onShow() {
// 小程序显示
console.log('牛肉商城小程序显示');
console.log('养殖管理小程序显示');
},
onHide() {
// 小程序隐藏
console.log('牛肉商城小程序隐藏');
console.log('养殖管理小程序隐藏');
},
onError(msg) {
// 错误处理
console.log('小程序发生错误:', msg);
console.error('小程序发生错误:', msg);
this.reportError(msg);
},
globalData: {
userInfo: null,
token: null,
baseUrl: 'http://localhost:8000/api'
baseUrl: 'https://api.xlxumu.com/app/v1',
systemInfo: null,
statusBarHeight: 0,
navBarHeight: 0
},
// 获取系统信息
getSystemInfo() {
const systemInfo = wx.getSystemInfoSync();
this.globalData.systemInfo = systemInfo;
this.globalData.statusBarHeight = systemInfo.statusBarHeight;
// 计算导航栏高度
const menuButton = wx.getMenuButtonBoundingClientRect();
this.globalData.navBarHeight = menuButton.height + (menuButton.top - systemInfo.statusBarHeight) * 2;
},
// 检查登录状态
checkLoginStatus() {
try {
const token = wx.getStorageSync('token');
if (token) {
const userInfo = wx.getStorageSync('userInfo');
if (token && userInfo) {
this.globalData.token = token;
this.globalData.userInfo = userInfo;
// 验证token有效性
this.verifyToken(token);
}
@@ -45,30 +67,83 @@ App({
// 验证token
verifyToken(token) {
wx.request({
url: `${this.globalData.baseUrl}/auth/verify`,
url: `${this.globalData.baseUrl}/auth/user`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
success: (res) => {
if (res.data.valid) {
this.globalData.userInfo = res.data.user;
if (res.data.success) {
this.globalData.userInfo = res.data.data;
wx.setStorageSync('userInfo', res.data.data);
} else {
// token无效清除本地存储
wx.removeStorageSync('token');
this.globalData.token = null;
this.globalData.userInfo = null;
this.logout();
}
},
fail: (err) => {
console.error('验证token失败', err);
this.logout();
}
});
},
// 登录方法
login(userInfo) {
this.globalData.userInfo = userInfo;
// 微信登录
wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => {
if (res.code) {
// 获取用户信息
wx.getUserProfile({
desc: '用于完善用户资料',
success: (userRes) => {
// 发送登录请求
wx.request({
url: `${this.globalData.baseUrl}/auth/wechat/login`,
method: 'POST',
data: {
code: res.code,
encrypted_data: userRes.encryptedData,
iv: userRes.iv
},
header: {
'Content-Type': 'application/json'
},
success: (loginRes) => {
if (loginRes.data.success) {
const { token, user } = loginRes.data.data;
this.globalData.token = token;
this.globalData.userInfo = user;
// 保存到本地存储
wx.setStorageSync('token', token);
wx.setStorageSync('userInfo', user);
resolve(loginRes.data.data);
} else {
reject(loginRes.data.message);
}
},
fail: (err) => {
reject(err);
}
});
},
fail: (err) => {
reject('用户拒绝授权');
}
});
} else {
reject('获取微信登录code失败');
}
},
fail: (err) => {
reject(err);
}
});
});
},
// 登出方法
@@ -76,5 +151,96 @@ App({
this.globalData.userInfo = null;
this.globalData.token = null;
wx.removeStorageSync('token');
wx.removeStorageSync('userInfo');
},
// 网络请求封装
request(options) {
return new Promise((resolve, reject) => {
const { url, method = 'GET', data = {}, header = {} } = options;
// 添加认证头
if (this.globalData.token) {
header['Authorization'] = `Bearer ${this.globalData.token}`;
}
wx.request({
url: this.globalData.baseUrl + url,
method,
data,
header: {
'Content-Type': 'application/json',
...header
},
success: (res) => {
if (res.data.success) {
resolve(res.data);
} else {
// 处理token过期
if (res.data.code === 10002) {
this.logout();
wx.navigateTo({
url: '/pages/auth/login'
});
}
reject(res.data);
}
},
fail: (err) => {
reject(err);
}
});
});
},
// 显示加载提示
showLoading(title = '加载中...') {
wx.showLoading({
title,
mask: true
});
},
// 隐藏加载提示
hideLoading() {
wx.hideLoading();
},
// 显示成功提示
showSuccess(title) {
wx.showToast({
title,
icon: 'success',
duration: 2000
});
},
// 显示错误提示
showError(title) {
wx.showToast({
title,
icon: 'error',
duration: 2000
});
},
// 错误上报
reportError(error) {
// 上报错误到服务器
if (this.globalData.token) {
wx.request({
url: `${this.globalData.baseUrl}/errors/report`,
method: 'POST',
data: {
error: error.toString(),
timestamp: Date.now(),
system_info: this.globalData.systemInfo
},
header: {
'Authorization': `Bearer ${this.globalData.token}`,
'Content-Type': 'application/json'
}
});
}
}
})
});

View File

@@ -1,14 +1,75 @@
{
"pages": [
"pages/index/index",
"pages/logs/logs"
"pages/auth/login",
"pages/farm/list",
"pages/farm/detail",
"pages/farm/add",
"pages/farm/edit",
"pages/animal/list",
"pages/animal/detail",
"pages/animal/add",
"pages/animal/edit",
"pages/animal/health",
"pages/statistics/farm",
"pages/statistics/animal",
"pages/profile/index",
"pages/profile/edit",
"pages/settings/index",
"pages/message/index",
"pages/message/detail"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "锡林郭勒盟智慧养殖",
"navigationBarTextStyle": "black"
"navigationBarBackgroundColor": "#2E8B57",
"navigationBarTitleText": "智慧养殖管理",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#2E8B57",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/tab/home.png",
"selectedIconPath": "images/tab/home-active.png"
},
{
"pagePath": "pages/farm/list",
"text": "养殖场",
"iconPath": "images/tab/farm.png",
"selectedIconPath": "images/tab/farm-active.png"
},
{
"pagePath": "pages/animal/list",
"text": "动物",
"iconPath": "images/tab/animal.png",
"selectedIconPath": "images/tab/animal-active.png"
},
{
"pagePath": "pages/statistics/farm",
"text": "统计",
"iconPath": "images/tab/chart.png",
"selectedIconPath": "images/tab/chart-active.png"
},
{
"pagePath": "pages/profile/index",
"text": "我的",
"iconPath": "images/tab/profile.png",
"selectedIconPath": "images/tab/profile-active.png"
}
]
},
"networkTimeout": {
"request": 10000,
"downloadFile": 10000
},
"debug": false,
"style": "v2",
"sitemapLocation": "sitemap.json"
}
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

View File

@@ -1,174 +1,447 @@
/* 全局样式 */
page {
background-color: #f5f5f5;
font-size: 28rpx;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
/* 通用样式 */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 通用样式类 */
.flex-row {
display: flex;
flex-direction: row;
}
.flex-column {
display: flex;
flex-direction: column;
}
.align-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.text-center {
text-align: center;
}
.text-primary {
color: #4CAF50;
}
.text-secondary {
color: #388E3C;
}
.text-accent {
color: #FF9800;
}
.bg-white {
background-color: #ffffff;
}
.padding {
padding: 20rpx;
min-height: 100vh;
box-sizing: border-box;
}
.padding-horizontal {
padding-left: 20rpx;
padding-right: 20rpx;
.page-container {
padding: 0;
min-height: 100vh;
}
.padding-vertical {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.margin {
margin: 20rpx;
}
.margin-horizontal {
margin-left: 20rpx;
margin-right: 20rpx;
}
.margin-vertical {
margin-top: 20rpx;
/* 卡片样式 */
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.card-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
/* 按钮样式 */
.btn {
display: inline-flex;
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
border-radius: 10rpx;
font-size: 32rpx;
padding: 24rpx 32rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s;
outline: none;
}
.btn-primary {
background-color: #4CAF50;
color: white;
background: #2E8B57;
color: #fff;
}
.btn-primary:hover {
background-color: #388E3C;
.btn-primary:active {
background: #228B22;
}
.btn-secondary {
background-color: #FF9800;
color: white;
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
}
.btn-secondary:hover {
background-color: #F57C00;
.btn-secondary:active {
background: #e9ecef;
}
.btn-danger {
background: #dc3545;
color: #fff;
}
.btn-danger:active {
background: #c82333;
}
.btn-small {
padding: 16rpx 24rpx;
font-size: 24rpx;
}
.btn-large {
padding: 32rpx 48rpx;
font-size: 32rpx;
}
.btn-block {
width: 100%;
}
/* 卡片样式 */
.card {
background-color: #ffffff;
border-radius: 10rpx;
padding: 20rpx;
margin: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.btn-disabled {
opacity: 0.6;
pointer-events: none;
}
/* 表单样式 */
.form-group {
margin-bottom: 30rpx;
margin-bottom: 32rpx;
}
.form-group label {
.form-label {
display: block;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
}
.form-group input,
.form-group picker,
.form-group textarea {
.form-input {
width: 100%;
padding: 20rpx;
border: 2rpx solid #ddd;
border-radius: 10rpx;
padding: 24rpx;
border: 1rpx solid #e9ecef;
border-radius: 12rpx;
font-size: 28rpx;
background: #fff;
box-sizing: border-box;
}
/* 加载动画 */
.form-input:focus {
border-color: #2E8B57;
}
.form-textarea {
min-height: 120rpx;
resize: vertical;
}
.form-select {
position: relative;
}
.form-select::after {
content: '';
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-top: 8rpx solid #999;
}
/* 列表样式 */
.list {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.list-item {
display: flex;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.list-item:last-child {
border-bottom: none;
}
.list-item:active {
background: #f8f9fa;
}
.list-item-content {
flex: 1;
}
.list-item-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.list-item-desc {
font-size: 24rpx;
color: #999;
}
.list-item-arrow {
width: 16rpx;
height: 16rpx;
border-top: 2rpx solid #ccc;
border-right: 2rpx solid #ccc;
transform: rotate(45deg);
margin-left: 16rpx;
}
/* 状态样式 */
.status {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 500;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.status-danger {
background: #f8d7da;
color: #721c24;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
/* 标签样式 */
.tag {
display: inline-block;
padding: 8rpx 12rpx;
background: #f8f9fa;
color: #666;
font-size: 20rpx;
border-radius: 8rpx;
margin-right: 8rpx;
margin-bottom: 8rpx;
}
.tag-primary {
background: #e3f2fd;
color: #1976d2;
}
.tag-success {
background: #e8f5e8;
color: #2e7d32;
}
.tag-warning {
background: #fff8e1;
color: #f57c00;
}
.tag-danger {
background: #ffebee;
color: #d32f2f;
}
/* 加载样式 */
.loading {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
padding: 40rpx;
color: #999;
font-size: 24rpx;
}
.spinner {
width: 50rpx;
height: 50rpx;
border: 5rpx solid #f3f3f3;
border-top: 5rpx solid #4CAF50;
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #f3f3f3;
border-top: 2rpx solid #2E8B57;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 16rpx;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态样式 */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
color: #999;
}
.empty-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 24rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 24rpx;
color: #ccc;
text-align: center;
line-height: 1.5;
}
/* 工具类 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-primary {
color: #2E8B57;
}
.text-success {
color: #28a745;
}
.text-warning {
color: #ffc107;
}
.text-danger {
color: #dc3545;
}
.text-muted {
color: #6c757d;
}
.bg-primary {
background-color: #2E8B57;
}
.bg-success {
background-color: #28a745;
}
.bg-warning {
background-color: #ffc107;
}
.bg-danger {
background-color: #dc3545;
}
.bg-light {
background-color: #f8f9fa;
}
.bg-white {
background-color: #fff;
}
.m-0 { margin: 0; }
.m-1 { margin: 8rpx; }
.m-2 { margin: 16rpx; }
.m-3 { margin: 24rpx; }
.m-4 { margin: 32rpx; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 8rpx; }
.mt-2 { margin-top: 16rpx; }
.mt-3 { margin-top: 24rpx; }
.mt-4 { margin-top: 32rpx; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 8rpx; }
.mb-2 { margin-bottom: 16rpx; }
.mb-3 { margin-bottom: 24rpx; }
.mb-4 { margin-bottom: 32rpx; }
.p-0 { padding: 0; }
.p-1 { padding: 8rpx; }
.p-2 { padding: 16rpx; }
.p-3 { padding: 24rpx; }
.p-4 { padding: 32rpx; }
.pt-0 { padding-top: 0; }
.pt-1 { padding-top: 8rpx; }
.pt-2 { padding-top: 16rpx; }
.pt-3 { padding-top: 24rpx; }
.pt-4 { padding-top: 32rpx; }
.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 8rpx; }
.pb-2 { padding-bottom: 16rpx; }
.pb-3 { padding-bottom: 24rpx; }
.pb-4 { padding-bottom: 32rpx; }
.d-flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.align-items-center {
align-items: center;
}
.justify-content-center {
justify-content: center;
}
.justify-content-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
/* 响应式 */
@media (max-width: 750rpx) {
.container {
padding: 16rpx;
}
.card {
padding: 20rpx;
margin-bottom: 16rpx;
}
}

View File

@@ -0,0 +1,115 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 全局配置
app.config.globalProperties.$baseUrl = 'https://api.farming.com'
// 全局方法
app.config.globalProperties.$request = (options) => {
return new Promise((resolve, reject) => {
// 添加token
const token = uni.getStorageSync('token')
if (token) {
options.header = options.header || {}
options.header.Authorization = 'Bearer ' + token
}
// 添加基础URL
if (!options.url.startsWith('http')) {
options.url = app.config.globalProperties.$baseUrl + options.url
}
// 发起请求
uni.request({
...options,
success: (res) => {
if (res.data.code === 200) {
resolve(res.data)
} else if (res.data.code === 401) {
// token过期跳转登录
uni.removeStorageSync('token')
uni.navigateTo({
url: '/pages/auth/login'
})
reject(res.data)
} else {
uni.showToast({
title: res.data.message || '请求失败',
icon: 'none'
})
reject(res.data)
}
},
fail: (error) => {
uni.showToast({
title: '网络错误',
icon: 'none'
})
reject(error)
}
})
})
}
// 全局工具方法
app.config.globalProperties.$utils = {
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
// 格式化日期
formatDate(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
// 格式化金额
formatMoney(amount) {
return '¥' + Number(amount).toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,')
},
// 防抖
debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
},
// 节流
throttle(func, limit) {
let inThrottle
return function() {
const args = arguments
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
}
return {
app
}
}

View File

@@ -0,0 +1,116 @@
{
"name": "farming-manager",
"appid": "wx1234567890abcdef",
"description": "专业的养殖场管理工具",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />",
"<uses-permission android:name=\"android.permission.VIBRATE\" />",
"<uses-permission android:name=\"android.permission.READ_LOGS\" />",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
"<uses-feature android:name=\"android.hardware.camera.autofocus\" />",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.CAMERA\" />",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\" />",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\" />",
"<uses-feature android:name=\"android.hardware.camera\" />",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wx1234567890abcdef",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于定位养殖场地理位置"
},
"scope.camera": {
"desc": "需要使用摄像头拍摄牲畜照片"
},
"scope.album": {
"desc": "需要访问相册选择牲畜照片"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation",
"chooseImage",
"chooseVideo"
]
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3"
}

View File

@@ -0,0 +1,226 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "养殖管理",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
},
{
"path": "pages/livestock/list",
"style": {
"navigationBarTitleText": "牲畜列表",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
},
{
"path": "pages/livestock/detail",
"style": {
"navigationBarTitleText": "牲畜详情"
}
},
{
"path": "pages/livestock/add",
"style": {
"navigationBarTitleText": "添加牲畜"
}
},
{
"path": "pages/livestock/edit",
"style": {
"navigationBarTitleText": "编辑牲畜"
}
},
{
"path": "pages/health/monitor",
"style": {
"navigationBarTitleText": "健康监控",
"enablePullDownRefresh": true
}
},
{
"path": "pages/health/record",
"style": {
"navigationBarTitleText": "健康记录"
}
},
{
"path": "pages/health/check",
"style": {
"navigationBarTitleText": "健康检查"
}
},
{
"path": "pages/breeding/manage",
"style": {
"navigationBarTitleText": "繁殖管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/breeding/record",
"style": {
"navigationBarTitleText": "繁殖记录"
}
},
{
"path": "pages/feeding/manage",
"style": {
"navigationBarTitleText": "饲料管理",
"enablePullDownRefresh": true
}
},
{
"path": "pages/feeding/plan",
"style": {
"navigationBarTitleText": "饲料计划"
}
},
{
"path": "pages/production/records",
"style": {
"navigationBarTitleText": "生产记录",
"enablePullDownRefresh": true
}
},
{
"path": "pages/production/add",
"style": {
"navigationBarTitleText": "添加生产记录"
}
},
{
"path": "pages/report/dashboard",
"style": {
"navigationBarTitleText": "数据报表",
"enablePullDownRefresh": true
}
},
{
"path": "pages/report/generate",
"style": {
"navigationBarTitleText": "生成报表"
}
},
{
"path": "pages/settings/index",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/settings/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/settings/alerts",
"style": {
"navigationBarTitleText": "预警设置"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "养殖管理",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#2e8b57",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/livestock/list",
"iconPath": "static/tabbar/livestock.png",
"selectedIconPath": "static/tabbar/livestock-active.png",
"text": "牲畜"
},
{
"pagePath": "pages/health/monitor",
"iconPath": "static/tabbar/health.png",
"selectedIconPath": "static/tabbar/health-active.png",
"text": "健康"
},
{
"pagePath": "pages/report/dashboard",
"iconPath": "static/tabbar/report.png",
"selectedIconPath": "static/tabbar/report-active.png",
"text": "报表"
},
{
"pagePath": "pages/settings/index",
"iconPath": "static/tabbar/settings.png",
"selectedIconPath": "static/tabbar/settings-active.png",
"text": "设置"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "首页",
"path": "pages/index/index",
"query": ""
},
{
"name": "牲畜列表",
"path": "pages/livestock/list",
"query": ""
},
{
"name": "健康监控",
"path": "pages/health/monitor",
"query": ""
}
]
},
"subPackages": [
{
"root": "subpages",
"pages": [
{
"path": "statistics/index",
"style": {
"navigationBarTitleText": "统计分析"
}
},
{
"path": "export/index",
"style": {
"navigationBarTitleText": "数据导出"
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["subpages"]
}
}
}

View File

@@ -0,0 +1,345 @@
// 动物列表页面
const app = getApp();
Page({
data: {
animalList: [],
loading: true,
refreshing: false,
hasMore: true,
cursor: null,
searchKeyword: '',
filterFarm: '',
filterStatus: 'all',
filterBreed: '',
farmList: [],
breedList: [],
statusOptions: [
{ value: 'all', label: '全部状态' },
{ value: 'healthy', label: '健康' },
{ value: 'sick', label: '患病' },
{ value: 'treatment', label: '治疗中' },
{ value: 'quarantine', label: '隔离' }
]
},
onLoad(options) {
// 如果有指定养殖场ID设置筛选条件
if (options.farmId) {
this.setData({ filterFarm: options.farmId });
}
this.checkLogin();
},
onShow() {
if (app.globalData.userInfo) {
this.loadInitialData();
}
},
onPullDownRefresh() {
this.setData({ refreshing: true });
this.loadAnimalList(true).finally(() => {
this.setData({ refreshing: false });
wx.stopPullDownRefresh();
});
},
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.loadAnimalList();
}
},
// 检查登录状态
checkLogin() {
if (!app.globalData.userInfo) {
wx.navigateTo({
url: '/pages/auth/login'
});
return;
}
this.loadInitialData();
},
// 加载初始数据
async loadInitialData() {
try {
await Promise.all([
this.loadFarmList(),
this.loadBreedList(),
this.loadAnimalList(true)
]);
} catch (error) {
console.error('加载初始数据失败:', error);
app.showError('加载失败,请重试');
}
},
// 加载养殖场列表(用于筛选)
async loadFarmList() {
try {
const res = await app.request({
url: '/farms',
method: 'GET',
data: { limit: 100 }
});
this.setData({
farmList: res.data.list || []
});
} catch (error) {
console.error('加载养殖场列表失败:', error);
}
},
// 加载品种列表(用于筛选)
async loadBreedList() {
try {
const res = await app.request({
url: '/animals/breeds',
method: 'GET'
});
this.setData({
breedList: res.data || []
});
} catch (error) {
console.error('加载品种列表失败:', error);
}
},
// 加载动物列表
async loadAnimalList(refresh = false) {
if (this.data.loading && !refresh) return;
try {
this.setData({ loading: true });
if (refresh) {
this.setData({
animalList: [],
cursor: null,
hasMore: true
});
}
const params = {
limit: 20,
cursor: this.data.cursor
};
// 添加筛选条件
if (this.data.searchKeyword) {
params.keyword = this.data.searchKeyword;
}
if (this.data.filterFarm) {
params.farm_id = this.data.filterFarm;
}
if (this.data.filterStatus !== 'all') {
params.health_status = this.data.filterStatus;
}
if (this.data.filterBreed) {
params.breed = this.data.filterBreed;
}
const res = await app.request({
url: '/animals',
method: 'GET',
data: params
});
const { list, has_more, next_cursor } = res.data;
this.setData({
animalList: refresh ? list : [...this.data.animalList, ...list],
hasMore: has_more,
cursor: next_cursor
});
} catch (error) {
console.error('加载动物列表失败:', error);
app.showError('加载失败,请重试');
} finally {
this.setData({ loading: false });
}
},
// 搜索输入
onSearchInput(e) {
this.setData({
searchKeyword: e.detail.value
});
},
// 搜索确认
onSearchConfirm() {
this.loadAnimalList(true);
},
// 清除搜索
onSearchClear() {
this.setData({
searchKeyword: ''
});
this.loadAnimalList(true);
},
// 养殖场筛选
onFarmFilter() {
const farmOptions = ['全部养殖场', ...this.data.farmList.map(farm => farm.name)];
wx.showActionSheet({
itemList: farmOptions,
success: (res) => {
const selectedFarm = res.tapIndex === 0 ? '' : this.data.farmList[res.tapIndex - 1].id;
this.setData({
filterFarm: selectedFarm
});
this.loadAnimalList(true);
}
});
},
// 状态筛选
onStatusFilter() {
wx.showActionSheet({
itemList: this.data.statusOptions.map(item => item.label),
success: (res) => {
const selectedStatus = this.data.statusOptions[res.tapIndex];
this.setData({
filterStatus: selectedStatus.value
});
this.loadAnimalList(true);
}
});
},
// 品种筛选
onBreedFilter() {
const breedOptions = ['全部品种', ...this.data.breedList];
wx.showActionSheet({
itemList: breedOptions,
success: (res) => {
const selectedBreed = res.tapIndex === 0 ? '' : this.data.breedList[res.tapIndex - 1];
this.setData({
filterBreed: selectedBreed
});
this.loadAnimalList(true);
}
});
},
// 点击动物项
onAnimalTap(e) {
const { animal } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/animal/detail?id=${animal.id}`
});
},
// 编辑动物
onEditAnimal(e) {
e.stopPropagation();
const { animal } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/animal/edit?id=${animal.id}`
});
},
// 删除动物
onDeleteAnimal(e) {
e.stopPropagation();
const { animal } = e.currentTarget.dataset;
wx.showModal({
title: '确认删除',
content: `确定要删除动物"${animal.name}"吗?此操作不可恢复。`,
confirmText: '删除',
confirmColor: '#dc3545',
success: async (res) => {
if (res.confirm) {
await this.deleteAnimal(animal.id);
}
}
});
},
// 执行删除
async deleteAnimal(animalId) {
try {
app.showLoading('删除中...');
await app.request({
url: `/animals/${animalId}`,
method: 'DELETE'
});
app.showSuccess('删除成功');
this.loadAnimalList(true);
} catch (error) {
console.error('删除动物失败:', error);
app.showError('删除失败,请重试');
} finally {
app.hideLoading();
}
},
// 添加动物
onAddAnimal() {
wx.navigateTo({
url: '/pages/animal/add'
});
},
// 健康记录
onHealthRecord(e) {
e.stopPropagation();
const { animal } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/animal/health?id=${animal.id}`
});
},
// 格式化健康状态
formatHealthStatus(status) {
const statusMap = {
'healthy': { text: '健康', class: 'status-success' },
'sick': { text: '患病', class: 'status-danger' },
'treatment': { text: '治疗中', class: 'status-warning' },
'quarantine': { text: '隔离', class: 'status-info' },
'recovery': { text: '康复中', class: 'status-warning' }
};
return statusMap[status] || { text: '未知', class: 'status-info' };
},
// 计算年龄
calculateAge(birthDate) {
if (!birthDate) return '未知';
const birth = new Date(birthDate);
const now = new Date();
const diffTime = Math.abs(now - birth);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 30) {
return `${diffDays}`;
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30);
return `${months}个月`;
} else {
const years = Math.floor(diffDays / 365);
const months = Math.floor((diffDays % 365) / 30);
return months > 0 ? `${years}${months}个月` : `${years}`;
}
},
// 格式化体重
formatWeight(weight) {
if (!weight) return '未称重';
return `${weight}kg`;
}
});

View File

@@ -0,0 +1,994 @@
<template>
<view class="animal-list-container">
<!-- 顶部搜索和筛选 -->
<view class="search-filter-bar">
<view class="search-wrapper">
<text class="search-icon">🔍</text>
<input
class="search-input"
type="text"
placeholder="搜索动物编号或名称"
v-model="searchKeyword"
@input="onSearchInput"
/>
</view>
<button class="filter-btn" @tap="showFilterModal = true">
<text class="filter-icon">📊</text>
</button>
</view>
<!-- 快速统计 -->
<view class="quick-stats">
<view class="stat-item">
<text class="stat-number">{{ stats.total }}</text>
<text class="stat-label">总数量</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ stats.healthy }}</text>
<text class="stat-label">健康</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ stats.sick }}</text>
<text class="stat-label">患病</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ stats.pregnant }}</text>
<text class="stat-label">怀孕</text>
</view>
</view>
<!-- 动物列表 -->
<view class="animal-list">
<view
class="animal-card"
v-for="animal in filteredAnimals"
:key="animal.id"
@tap="goToAnimalDetail(animal.id)"
>
<!-- 动物头像 -->
<view class="animal-avatar">
<image
:src="animal.avatar || getDefaultAvatar(animal.species)"
mode="aspectFill"
class="avatar-image"
></image>
<view class="health-indicator" :class="animal.healthStatus"></view>
</view>
<!-- 动物信息 -->
<view class="animal-info">
<view class="animal-header">
<text class="animal-name">{{ animal.name || animal.code }}</text>
<view class="animal-tags">
<text class="tag species-tag">{{ getSpeciesText(animal.species) }}</text>
<text class="tag gender-tag" :class="animal.gender">{{ getGenderText(animal.gender) }}</text>
</view>
</view>
<view class="animal-details">
<view class="detail-row">
<text class="detail-label">编号</text>
<text class="detail-value">{{ animal.code }}</text>
</view>
<view class="detail-row">
<text class="detail-label">年龄</text>
<text class="detail-value">{{ calculateAge(animal.birthDate) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">体重</text>
<text class="detail-value">{{ animal.weight }}kg</text>
</view>
<view class="detail-row">
<text class="detail-label">健康状态</text>
<text class="detail-value" :class="animal.healthStatus">{{ getHealthStatusText(animal.healthStatus) }}</text>
</view>
</view>
<view class="animal-footer">
<text class="update-time">{{ formatTime(animal.updatedAt) }}</text>
<view class="quick-actions">
<button class="action-btn health-btn" @tap.stop="recordHealth(animal)">
<text class="btn-icon">🏥</text>
</button>
<button class="action-btn feed-btn" @tap.stop="recordFeed(animal)">
<text class="btn-icon">🌾</text>
</button>
<button class="action-btn more-btn" @tap.stop="showMoreActions(animal)">
<text class="btn-icon"></text>
</button>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<empty
v-if="!loading && filteredAnimals.length === 0"
title="暂无动物档案"
description="点击右下角按钮添加第一个动物档案"
:show-action="true"
action-text="添加动物"
@action="addAnimal"
></empty>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<button class="load-more-btn" @tap="loadMore">加载更多</button>
</view>
<!-- 添加按钮 -->
<view class="fab-button" @tap="addAnimal">
<text class="fab-icon">+</text>
</view>
<!-- 筛选弹窗 -->
<modal
:show="showFilterModal"
title="筛选条件"
@close="showFilterModal = false"
@confirm="applyFilter"
>
<view class="filter-content">
<!-- 物种筛选 -->
<view class="filter-section">
<text class="section-title">物种</text>
<view class="filter-options">
<view
class="filter-chip"
:class="{ active: filterData.species.includes(species.value) }"
v-for="species in speciesOptions"
:key="species.value"
@tap="toggleSpecies(species.value)"
>
<text class="chip-text">{{ species.label }}</text>
</view>
</view>
</view>
<!-- 健康状态筛选 -->
<view class="filter-section">
<text class="section-title">健康状态</text>
<view class="filter-options">
<view
class="filter-chip"
:class="{ active: filterData.healthStatus.includes(status.value) }"
v-for="status in healthStatusOptions"
:key="status.value"
@tap="toggleHealthStatus(status.value)"
>
<text class="chip-text">{{ status.label }}</text>
</view>
</view>
</view>
<!-- 年龄范围 -->
<view class="filter-section">
<text class="section-title">年龄范围</text>
<view class="age-range">
<input
class="age-input"
type="number"
placeholder="最小年龄"
v-model="filterData.minAge"
/>
<text class="range-separator">-</text>
<input
class="age-input"
type="number"
placeholder="最大年龄"
v-model="filterData.maxAge"
/>
</view>
</view>
</view>
</modal>
<!-- 更多操作弹窗 -->
<modal
:show="showActionsModal"
title="更多操作"
:show-footer="false"
@close="showActionsModal = false"
>
<view class="actions-content">
<button class="action-item" @tap="editAnimal">
<text class="action-icon"></text>
<text class="action-text">编辑信息</text>
</button>
<button class="action-item" @tap="viewHistory">
<text class="action-icon">📋</text>
<text class="action-text">查看记录</text>
</button>
<button class="action-item" @tap="generateQR">
<text class="action-icon">📱</text>
<text class="action-text">生成二维码</text>
</button>
<button class="action-item danger" @tap="deleteAnimal">
<text class="action-icon">🗑</text>
<text class="action-text">删除档案</text>
</button>
</view>
</modal>
<!-- 加载组件 -->
<loading :show="loading" text="加载中..."></loading>
</view>
</template>
<script>
import Empty from '@/common/components/empty/empty.vue'
import Modal from '@/common/components/modal/modal.vue'
import Loading from '@/common/components/loading/loading.vue'
export default {
name: 'AnimalList',
components: {
Empty,
Modal,
Loading
},
data() {
return {
searchKeyword: '',
loading: false,
hasMore: true,
currentPage: 1,
pageSize: 20,
// 统计数据
stats: {
total: 0,
healthy: 0,
sick: 0,
pregnant: 0
},
// 动物列表
animals: [],
// 筛选相关
showFilterModal: false,
filterData: {
species: [],
healthStatus: [],
minAge: '',
maxAge: ''
},
// 更多操作
showActionsModal: false,
selectedAnimal: null,
// 筛选选项
speciesOptions: [
{ label: '牛', value: 'cattle' },
{ label: '羊', value: 'sheep' },
{ label: '猪', value: 'pig' },
{ label: '鸡', value: 'chicken' }
],
healthStatusOptions: [
{ label: '健康', value: 'healthy' },
{ label: '患病', value: 'sick' },
{ label: '治疗中', value: 'treating' },
{ label: '康复中', value: 'recovering' }
]
}
},
computed: {
// 过滤后的动物列表
filteredAnimals() {
let result = this.animals
// 搜索过滤
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase()
result = result.filter(animal =>
(animal.name && animal.name.toLowerCase().includes(keyword)) ||
animal.code.toLowerCase().includes(keyword)
)
}
return result
}
},
onLoad() {
this.loadAnimals()
this.loadStats()
},
onPullDownRefresh() {
this.refreshData()
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore()
}
},
methods: {
// 加载动物列表
async loadAnimals(refresh = false) {
if (this.loading) return
this.loading = true
try {
const params = {
page: refresh ? 1 : this.currentPage,
pageSize: this.pageSize,
keyword: this.searchKeyword,
...this.filterData
}
const res = await this.$request({
url: '/farming/animals',
method: 'GET',
data: params,
loading: false
})
if (refresh) {
this.animals = res.data.list
this.currentPage = 1
} else {
this.animals = [...this.animals, ...res.data.list]
}
this.hasMore = res.data.hasMore
this.currentPage++
} catch (error) {
console.error('加载动物列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
if (refresh) {
uni.stopPullDownRefresh()
}
}
},
// 加载统计数据
async loadStats() {
try {
const res = await this.$request({
url: '/farming/animals/stats',
method: 'GET',
loading: false
})
this.stats = res.data
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
// 刷新数据
refreshData() {
this.loadAnimals(true)
this.loadStats()
},
// 加载更多
loadMore() {
this.loadAnimals()
},
// 搜索输入
onSearchInput() {
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => {
this.refreshData()
}, 500)
},
// 切换物种筛选
toggleSpecies(species) {
const index = this.filterData.species.indexOf(species)
if (index > -1) {
this.filterData.species.splice(index, 1)
} else {
this.filterData.species.push(species)
}
},
// 切换健康状态筛选
toggleHealthStatus(status) {
const index = this.filterData.healthStatus.indexOf(status)
if (index > -1) {
this.filterData.healthStatus.splice(index, 1)
} else {
this.filterData.healthStatus.push(status)
}
},
// 应用筛选
applyFilter() {
this.showFilterModal = false
this.refreshData()
},
// 跳转到动物详情
goToAnimalDetail(animalId) {
uni.navigateTo({
url: `/pages/animal/detail?id=${animalId}`
})
},
// 添加动物
addAnimal() {
uni.navigateTo({
url: '/pages/animal/add'
})
},
// 记录健康信息
recordHealth(animal) {
uni.navigateTo({
url: `/pages/health/record?animalId=${animal.id}`
})
},
// 记录喂养信息
recordFeed(animal) {
uni.navigateTo({
url: `/pages/feed/record?animalId=${animal.id}`
})
},
// 显示更多操作
showMoreActions(animal) {
this.selectedAnimal = animal
this.showActionsModal = true
},
// 编辑动物
editAnimal() {
this.showActionsModal = false
uni.navigateTo({
url: `/pages/animal/edit?id=${this.selectedAnimal.id}`
})
},
// 查看历史记录
viewHistory() {
this.showActionsModal = false
uni.navigateTo({
url: `/pages/animal/history?id=${this.selectedAnimal.id}`
})
},
// 生成二维码
generateQR() {
this.showActionsModal = false
uni.navigateTo({
url: `/pages/animal/qrcode?id=${this.selectedAnimal.id}`
})
},
// 删除动物
async deleteAnimal() {
this.showActionsModal = false
const result = await uni.showModal({
title: '确认删除',
content: '删除后无法恢复,确定要删除这个动物档案吗?',
confirmColor: '#ff4444'
})
if (result.confirm) {
try {
await this.$request({
url: `/farming/animals/${this.selectedAnimal.id}`,
method: 'DELETE'
})
uni.showToast({
title: '删除成功',
icon: 'success'
})
this.refreshData()
} catch (error) {
console.error('删除动物失败:', error)
}
}
},
// 获取默认头像
getDefaultAvatar(species) {
const avatarMap = {
cattle: '/static/images/cattle-avatar.png',
sheep: '/static/images/sheep-avatar.png',
pig: '/static/images/pig-avatar.png',
chicken: '/static/images/chicken-avatar.png'
}
return avatarMap[species] || '/static/images/animal-default.png'
},
// 获取物种文本
getSpeciesText(species) {
const speciesMap = {
cattle: '牛',
sheep: '羊',
pig: '猪',
chicken: '鸡'
}
return speciesMap[species] || '未知'
},
// 获取性别文本
getGenderText(gender) {
const genderMap = {
male: '公',
female: '母'
}
return genderMap[gender] || '未知'
},
// 获取健康状态文本
getHealthStatusText(status) {
const statusMap = {
healthy: '健康',
sick: '患病',
treating: '治疗中',
recovering: '康复中'
}
return statusMap[status] || '未知'
},
// 计算年龄
calculateAge(birthDate) {
if (!birthDate) return '未知'
const birth = new Date(birthDate)
const now = new Date()
const diffTime = Math.abs(now - birth)
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays < 30) {
return `${diffDays}`
} else if (diffDays < 365) {
return `${Math.floor(diffDays / 30)}个月`
} else {
return `${Math.floor(diffDays / 365)}`
}
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
return `${date.getMonth() + 1}${date.getDate()}${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
}
}
}
</script>
<style lang="scss" scoped>
.animal-list-container {
min-height: 100vh;
background: #f8f9fa;
padding-bottom: 120rpx;
}
.search-filter-bar {
background: #ffffff;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.search-wrapper {
flex: 1;
background: #f5f5f5;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
.search-icon {
font-size: 28rpx;
color: #999999;
margin-right: 16rpx;
}
.search-input {
flex: 1;
height: 70rpx;
font-size: 28rpx;
color: #333333;
}
}
.filter-btn {
width: 70rpx;
height: 70rpx;
background: #2E8B57;
border: none;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
.filter-icon {
font-size: 28rpx;
color: #ffffff;
}
&::after {
border: none;
}
}
}
.quick-stats {
display: flex;
padding: 20rpx 30rpx;
gap: 15rpx;
.stat-item {
flex: 1;
background: #ffffff;
border-radius: 12rpx;
padding: 20rpx 15rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.stat-number {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #2E8B57;
margin-bottom: 6rpx;
}
.stat-label {
font-size: 22rpx;
color: #666666;
}
}
}
.animal-list {
padding: 0 30rpx;
.animal-card {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
display: flex;
gap: 24rpx;
.animal-avatar {
position: relative;
.avatar-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
}
.health-indicator {
position: absolute;
bottom: -6rpx;
right: -6rpx;
width: 24rpx;
height: 24rpx;
border-radius: 50%;
border: 3rpx solid #ffffff;
&.healthy {
background: #4CAF50;
}
&.sick {
background: #F44336;
}
&.treating {
background: #FF9800;
}
&.recovering {
background: #2196F3;
}
}
}
.animal-info {
flex: 1;
.animal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
.animal-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.animal-tags {
display: flex;
gap: 8rpx;
.tag {
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 6rpx;
&.species-tag {
background: rgba(46, 139, 87, 0.1);
color: #2E8B57;
}
&.gender-tag {
&.male {
background: rgba(33, 150, 243, 0.1);
color: #2196F3;
}
&.female {
background: rgba(233, 30, 99, 0.1);
color: #E91E63;
}
}
}
}
}
.animal-details {
margin-bottom: 16rpx;
.detail-row {
display: flex;
margin-bottom: 8rpx;
.detail-label {
font-size: 24rpx;
color: #999999;
width: 120rpx;
}
.detail-value {
font-size: 24rpx;
color: #666666;
&.healthy {
color: #4CAF50;
}
&.sick {
color: #F44336;
}
&.treating {
color: #FF9800;
}
&.recovering {
color: #2196F3;
}
}
}
}
.animal-footer {
display: flex;
justify-content: space-between;
align-items: center;
.update-time {
font-size: 22rpx;
color: #999999;
}
.quick-actions {
display: flex;
gap: 12rpx;
.action-btn {
width: 60rpx;
height: 60rpx;
border: none;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.btn-icon {
font-size: 24rpx;
}
&.health-btn {
background: rgba(244, 67, 54, 0.1);
.btn-icon {
color: #F44336;
}
}
&.feed-btn {
background: rgba(255, 152, 0, 0.1);
.btn-icon {
color: #FF9800;
}
}
&.more-btn {
background: rgba(158, 158, 158, 0.1);
.btn-icon {
color: #9E9E9E;
}
}
&::after {
border: none;
}
}
}
}
}
}
}
.load-more {
padding: 30rpx;
text-align: center;
.load-more-btn {
background: #ffffff;
color: #2E8B57;
border: 2rpx solid #2E8B57;
border-radius: 12rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
&::after {
border: none;
}
}
}
.fab-button {
position: fixed;
bottom: 40rpx;
right: 40rpx;
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, #2E8B57, #3CB371);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 139, 87, 0.3);
z-index: 100;
.fab-icon {
font-size: 60rpx;
color: #ffffff;
font-weight: 300;
}
}
.filter-content {
.filter-section {
margin-bottom: 40rpx;
.section-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.filter-chip {
padding: 16rpx 24rpx;
border: 2rpx solid #e0e0e0;
border-radius: 20rpx;
background: #ffffff;
.chip-text {
font-size: 28rpx;
color: #666666;
}
&.active {
border-color: #2E8B57;
background: rgba(46, 139, 87, 0.1);
.chip-text {
color: #2E8B57;
}
}
}
}
.age-range {
display: flex;
align-items: center;
gap: 20rpx;
.age-input {
flex: 1;
height: 80rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.range-separator {
font-size: 28rpx;
color: #999999;
}
}
}
}
.actions-content {
.action-item {
width: 100%;
height: 100rpx;
background: transparent;
border: none;
display: flex;
align-items: center;
padding: 0 20rpx;
margin-bottom: 10rpx;
border-radius: 8rpx;
.action-icon {
font-size: 32rpx;
margin-right: 20rpx;
width: 40rpx;
}
.action-text {
font-size: 32rpx;
color: #333333;
}
&.danger {
.action-icon,
.action-text {
color: #F44336;
}
}
&:hover {
background: #f5f5f5;
}
&::after {
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,165 @@
<!--动物列表页面-->
<view class="page-container">
<!-- 搜索和筛选栏 -->
<view class="search-filter-bar">
<view class="search-input-wrapper">
<input
class="search-input"
placeholder="搜索动物名称或编号"
value="{{searchKeyword}}"
bindinput="onSearchInput"
bindconfirm="onSearchConfirm"
/>
<view class="search-icon" bindtap="onSearchConfirm">
<image src="/images/icons/search.png" mode="aspectFit"></image>
</view>
<view class="clear-icon" wx:if="{{searchKeyword}}" bindtap="onSearchClear">
<image src="/images/icons/close.png" mode="aspectFit"></image>
</view>
</view>
<view class="filter-buttons">
<button class="filter-btn" bindtap="onFarmFilter">
<image src="/images/icons/farm.png" mode="aspectFit"></image>
</button>
<button class="filter-btn" bindtap="onStatusFilter">
<image src="/images/icons/health.png" mode="aspectFit"></image>
</button>
<button class="filter-btn" bindtap="onBreedFilter">
<image src="/images/icons/breed.png" mode="aspectFit"></image>
</button>
</view>
</view>
<!-- 动物列表 -->
<scroll-view
class="animal-list-scroll"
scroll-y
refresher-enabled
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onPullDownRefresh"
bindscrolltolower="onReachBottom"
>
<view class="animal-list">
<view
class="animal-item"
wx:for="{{animalList}}"
wx:key="id"
data-animal="{{item}}"
bindtap="onAnimalTap"
>
<!-- 动物头像和基本信息 -->
<view class="animal-header">
<image
class="animal-avatar"
src="{{item.avatar || '/images/default-animal.png'}}"
mode="aspectFill"
></image>
<view class="animal-basic-info">
<text class="animal-name">{{item.name}}</text>
<text class="animal-code">编号:{{item.code}}</text>
<text class="animal-farm">{{item.farm_name}}</text>
</view>
<view class="animal-status">
<text class="status {{formatHealthStatus(item.health_status).class}}">
{{formatHealthStatus(item.health_status).text}}
</text>
</view>
</view>
<!-- 动物详细信息 -->
<view class="animal-details">
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">品种</text>
<text class="detail-value">{{item.breed}}</text>
</view>
<view class="detail-item">
<text class="detail-label">性别</text>
<text class="detail-value">{{item.gender === 'male' ? '公' : '母'}}</text>
</view>
</view>
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">年龄</text>
<text class="detail-value">{{calculateAge(item.birth_date)}}</text>
</view>
<view class="detail-item">
<text class="detail-label">体重</text>
<text class="detail-value">{{formatWeight(item.weight)}}</text>
</view>
</view>
</view>
<!-- 健康信息 -->
<view class="health-info" wx:if="{{item.last_health_check}}">
<view class="health-item">
<text class="health-label">最近检查</text>
<text class="health-value">{{item.last_health_check}}</text>
</view>
<view class="health-item" wx:if="{{item.temperature}}">
<text class="health-label">体温</text>
<text class="health-value {{item.temperature > 39.5 ? 'text-danger' : 'text-success'}}">
{{item.temperature}}°C
</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="animal-actions">
<button
class="action-btn secondary"
data-animal="{{item}}"
bindtap="onHealthRecord"
>
健康记录
</button>
<button
class="action-btn secondary"
data-animal="{{item}}"
bindtap="onEditAnimal"
>
编辑
</button>
<button
class="action-btn danger"
data-animal="{{item}}"
bindtap="onDeleteAnimal"
>
删除
</button>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" wx:if="{{hasMore && animalList.length > 0}}">
<view class="loading-spinner" wx:if="{{loading}}"></view>
<text class="load-text">{{loading ? '加载中...' : '上拉加载更多'}}</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" wx:if="{{!hasMore && animalList.length > 0}}">
<text>没有更多数据了</text>
</view>
</scroll-view>
<!-- 空状态 -->
<view class="empty" wx:if="{{!loading && animalList.length === 0}}">
<image class="empty-icon" src="/images/empty-animal.png" mode="aspectFit"></image>
<text class="empty-text">还没有动物</text>
<text class="empty-desc">添加您的第一只动物开始管理</text>
<button class="btn btn-primary mt-3" bindtap="onAddAnimal">添加动物</button>
</view>
<!-- 加载状态 -->
<view class="loading" wx:if="{{loading && animalList.length === 0}}">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<!-- 添加按钮 -->
<view class="add-btn" bindtap="onAddAnimal">
<image src="/images/icons/add.png" mode="aspectFit"></image>
</view>
</view>

View File

@@ -0,0 +1,401 @@
/* 动物列表页面样式 */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 搜索和筛选栏 */
.search-filter-bar {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
gap: 16rpx;
}
.search-input-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
background: #f8f9fa;
border-radius: 24rpx;
padding: 0 48rpx 0 20rpx;
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333;
}
.search-input::placeholder {
color: #999;
}
.search-icon {
position: absolute;
right: 16rpx;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-icon image {
width: 24rpx;
height: 24rpx;
}
.clear-icon {
position: absolute;
right: 48rpx;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.clear-icon image {
width: 20rpx;
height: 20rpx;
}
.filter-buttons {
display: flex;
gap: 8rpx;
}
.filter-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 20rpx;
border: none;
}
.filter-btn image {
width: 24rpx;
height: 24rpx;
}
/* 列表滚动区域 */
.animal-list-scroll {
flex: 1;
padding: 0 24rpx;
}
.animal-list {
padding: 20rpx 0;
}
/* 动物项 */
.animal-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.animal-item:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
/* 动物头部 */
.animal-header {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
}
.animal-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 3rpx solid #f0f0f0;
}
.animal-basic-info {
flex: 1;
display: flex;
flex-direction: column;
}
.animal-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.animal-code {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.animal-farm {
font-size: 24rpx;
color: #666;
}
.animal-status {
margin-left: 16rpx;
}
/* 动物详情 */
.animal-details {
margin-bottom: 20rpx;
}
.detail-row {
display: flex;
margin-bottom: 12rpx;
}
.detail-row:last-child {
margin-bottom: 0;
}
.detail-item {
flex: 1;
display: flex;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #666;
margin-right: 12rpx;
min-width: 60rpx;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
/* 健康信息 */
.health-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 16rpx 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.health-item {
display: flex;
flex-direction: column;
align-items: center;
}
.health-label {
font-size: 22rpx;
color: #666;
margin-bottom: 8rpx;
}
.health-value {
font-size: 24rpx;
font-weight: 600;
color: #333;
}
/* 操作按钮 */
.animal-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
flex: 1;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 500;
border: none;
outline: none;
}
.action-btn.secondary {
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
}
.action-btn.secondary:active {
background: #e9ecef;
}
.action-btn.danger {
background: #fff5f5;
color: #dc3545;
border: 1rpx solid #f5c6cb;
}
.action-btn.danger:active {
background: #f5c6cb;
}
/* 加载更多 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
color: #999;
}
.load-text {
font-size: 24rpx;
margin-left: 16rpx;
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
color: #ccc;
font-size: 24rpx;
}
/* 添加按钮 */
.add-btn {
position: fixed;
right: 40rpx;
bottom: 40rpx;
width: 112rpx;
height: 112rpx;
background: #2E8B57;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 139, 87, 0.3);
z-index: 100;
}
.add-btn:active {
transform: scale(0.95);
}
.add-btn image {
width: 48rpx;
height: 48rpx;
}
/* 状态样式 */
.status {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 500;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.status-danger {
background: #f8d7da;
color: #721c24;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
/* 工具类 */
.text-success {
color: #28a745;
}
.text-warning {
color: #ffc107;
}
.text-danger {
color: #dc3545;
}
.mt-3 {
margin-top: 32rpx;
}
/* 响应式适配 */
@media (max-width: 750rpx) {
.animal-item {
padding: 20rpx;
}
.animal-header {
flex-direction: column;
align-items: flex-start;
gap: 16rpx;
}
.animal-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 0;
margin-bottom: 12rpx;
}
.animal-status {
margin-left: 0;
}
.detail-row {
flex-direction: column;
gap: 8rpx;
}
.detail-item {
justify-content: space-between;
}
.health-info {
flex-direction: column;
align-items: flex-start;
gap: 12rpx;
}
.health-item {
flex-direction: row;
align-items: center;
gap: 12rpx;
}
.health-label {
margin-bottom: 0;
}
}

View File

@@ -0,0 +1,136 @@
// 登录页面
const app = getApp();
Page({
data: {
loading: false,
canIUse: wx.canIUse('button.open-type.getUserInfo')
},
onLoad() {
// 检查是否已登录
if (app.globalData.userInfo) {
this.redirectToHome();
}
},
// 微信登录
async onWxLogin() {
if (this.data.loading) return;
try {
this.setData({ loading: true });
const loginResult = await app.wxLogin();
app.showSuccess('登录成功');
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
this.redirectToHome();
}, 1000);
} catch (error) {
console.error('登录失败:', error);
app.showError(typeof error === 'string' ? error : '登录失败,请重试');
} finally {
this.setData({ loading: false });
}
},
// 获取用户信息(兼容旧版本)
onGetUserInfo(e) {
if (e.detail.userInfo) {
this.handleUserInfo(e.detail);
} else {
app.showError('需要授权才能使用小程序');
}
},
// 处理用户信息
async handleUserInfo(userInfo) {
try {
this.setData({ loading: true });
// 先获取登录code
const loginRes = await this.wxLogin();
// 发送登录请求
const response = await app.request({
url: '/auth/wechat/login',
method: 'POST',
data: {
code: loginRes.code,
encrypted_data: userInfo.encryptedData,
iv: userInfo.iv
}
});
const { token, user } = response.data;
app.globalData.token = token;
app.globalData.userInfo = user;
// 保存到本地存储
wx.setStorageSync('token', token);
wx.setStorageSync('userInfo', user);
app.showSuccess('登录成功');
setTimeout(() => {
this.redirectToHome();
}, 1000);
} catch (error) {
console.error('登录失败:', error);
app.showError('登录失败,请重试');
} finally {
this.setData({ loading: false });
}
},
// 微信登录获取code
wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
});
});
},
// 跳转到首页
redirectToHome() {
wx.switchTab({
url: '/pages/index/index'
});
},
// 跳过登录(游客模式)
onSkipLogin() {
wx.showModal({
title: '提示',
content: '游客模式下功能受限,建议登录后使用完整功能',
confirmText: '继续',
cancelText: '登录',
success: (res) => {
if (res.confirm) {
this.redirectToHome();
}
}
});
},
// 查看隐私政策
onPrivacyPolicy() {
wx.navigateTo({
url: '/pages/common/privacy'
});
},
// 查看用户协议
onUserAgreement() {
wx.navigateTo({
url: '/pages/common/agreement'
});
}
});

View File

@@ -0,0 +1,593 @@
<template>
<view class="login-container">
<!-- 背景装饰 -->
<view class="background-decoration">
<view class="decoration-circle circle-1"></view>
<view class="decoration-circle circle-2"></view>
<view class="decoration-circle circle-3"></view>
</view>
<!-- 登录内容 -->
<view class="login-content">
<!-- Logo和标题 -->
<view class="login-header">
<image class="logo" src="/static/images/logo.png" mode="aspectFit"></image>
<text class="app-name">养殖管理</text>
<text class="app-slogan">智慧畜牧科学养殖</text>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- 手机号输入 -->
<view class="form-item">
<view class="input-wrapper">
<text class="input-icon">📱</text>
<input
class="form-input"
type="number"
placeholder="请输入手机号"
v-model="formData.phone"
maxlength="11"
@input="onPhoneInput"
/>
</view>
<text class="error-text" v-if="errors.phone">{{ errors.phone }}</text>
</view>
<!-- 验证码输入 -->
<view class="form-item">
<view class="input-wrapper">
<text class="input-icon">🔐</text>
<input
class="form-input code-input"
type="number"
placeholder="请输入验证码"
v-model="formData.code"
maxlength="6"
/>
<button
class="code-button"
:class="{ 'disabled': codeDisabled }"
:disabled="codeDisabled"
@tap="sendCode"
>
{{ codeText }}
</button>
</view>
<text class="error-text" v-if="errors.code">{{ errors.code }}</text>
</view>
<!-- 登录按钮 -->
<button
class="login-button"
:class="{ 'disabled': !canLogin }"
:disabled="!canLogin"
@tap="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</button>
<!-- 其他登录方式 -->
<view class="other-login">
<text class="other-text">其他登录方式</text>
<view class="other-methods">
<button class="wechat-login" @tap="wechatLogin">
<text class="method-icon">🔐</text>
<text class="method-text">微信登录</text>
</button>
</view>
</view>
<!-- 协议条款 -->
<view class="agreement">
<checkbox-group @change="onAgreementChange">
<label class="agreement-item">
<checkbox :checked="agreed" color="#2E8B57" />
<text class="agreement-text">
我已阅读并同意
<text class="link-text" @tap="showPrivacy">隐私政策</text>
<text class="link-text" @tap="showTerms">用户协议</text>
</text>
</label>
</checkbox-group>
</view>
</view>
</view>
<!-- 加载组件 -->
<loading :show="loading" text="登录中..."></loading>
</view>
</template>
<script>
import { validate } from '@/common/utils/validator'
import Loading from '@/common/components/loading/loading.vue'
export default {
name: 'Login',
components: {
Loading
},
data() {
return {
formData: {
phone: '',
code: ''
},
errors: {},
loading: false,
agreed: false,
codeDisabled: false,
codeCountdown: 0,
codeTimer: null
}
},
computed: {
canLogin() {
return this.formData.phone &&
this.formData.code &&
this.agreed &&
!this.loading
},
codeText() {
return this.codeCountdown > 0 ? `${this.codeCountdown}s` : '获取验证码'
}
},
onLoad() {
this.checkAutoLogin()
},
onUnload() {
if (this.codeTimer) {
clearInterval(this.codeTimer)
}
},
methods: {
// 检查自动登录
async checkAutoLogin() {
const token = uni.getStorageSync('token')
if (token) {
try {
// 验证token有效性
const res = await this.$request({
url: '/auth/validate',
method: 'POST',
loading: false
})
if (res.code === 200) {
// token有效直接跳转
this.loginSuccess(res.data)
return
}
} catch (error) {
// token无效清除本地存储
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
}
}
},
// 手机号输入
onPhoneInput() {
this.validatePhone()
},
// 验证手机号
validatePhone() {
const phone = this.formData.phone
if (!phone) {
this.errors.phone = '请输入手机号'
return false
}
if (!validate.phone(phone)) {
this.errors.phone = '请输入正确的手机号'
return false
}
delete this.errors.phone
return true
},
// 发送验证码
async sendCode() {
if (!this.validatePhone()) {
return
}
try {
await this.$request({
url: '/auth/send-code',
method: 'POST',
data: {
phone: this.formData.phone
}
})
uni.showToast({
title: '验证码已发送',
icon: 'success'
})
// 开始倒计时
this.startCountdown()
} catch (error) {
console.error('发送验证码失败:', error)
}
},
// 开始倒计时
startCountdown() {
this.codeDisabled = true
this.codeCountdown = 60
this.codeTimer = setInterval(() => {
this.codeCountdown--
if (this.codeCountdown <= 0) {
this.codeDisabled = false
clearInterval(this.codeTimer)
this.codeTimer = null
}
}, 1000)
},
// 处理登录
async handleLogin() {
if (!this.validateForm()) {
return
}
this.loading = true
try {
const res = await this.$request({
url: '/auth/login',
method: 'POST',
data: {
phone: this.formData.phone,
code: this.formData.code
},
loading: false
})
this.loginSuccess(res.data)
} catch (error) {
console.error('登录失败:', error)
} finally {
this.loading = false
}
},
// 微信登录
async wechatLogin() {
if (!this.agreed) {
uni.showToast({
title: '请先同意用户协议',
icon: 'none'
})
return
}
try {
// 获取微信登录code
const loginRes = await uni.login({
provider: 'weixin'
})
if (loginRes.code) {
this.loading = true
const res = await this.$request({
url: '/auth/wechat-login',
method: 'POST',
data: {
code: loginRes.code
},
loading: false
})
this.loginSuccess(res.data)
}
} catch (error) {
console.error('微信登录失败:', error)
uni.showToast({
title: '微信登录失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 登录成功
loginSuccess(data) {
// 保存用户信息和token
uni.setStorageSync('token', data.token)
uni.setStorageSync('userInfo', data.userInfo)
// 设置全局数据
getApp().globalData.token = data.token
getApp().globalData.userInfo = data.userInfo
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
},
// 验证表单
validateForm() {
this.errors = {}
let isValid = true
// 验证手机号
if (!this.validatePhone()) {
isValid = false
}
// 验证验证码
if (!this.formData.code) {
this.errors.code = '请输入验证码'
isValid = false
} else if (this.formData.code.length !== 6) {
this.errors.code = '请输入6位验证码'
isValid = false
}
return isValid
},
// 协议变更
onAgreementChange(e) {
this.agreed = e.detail.value.length > 0
},
// 显示隐私政策
showPrivacy() {
uni.navigateTo({
url: '/pages/legal/privacy'
})
},
// 显示用户协议
showTerms() {
uni.navigateTo({
url: '/pages/legal/terms'
})
}
}
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
right: -150rpx;
}
&.circle-2 {
width: 200rpx;
height: 200rpx;
bottom: 200rpx;
left: -100rpx;
}
&.circle-3 {
width: 150rpx;
height: 150rpx;
top: 300rpx;
left: 50rpx;
}
}
}
.login-content {
position: relative;
z-index: 1;
padding: 100rpx 60rpx 60rpx;
}
.login-header {
text-align: center;
margin-bottom: 100rpx;
.logo {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
}
.app-name {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 16rpx;
}
.app-slogan {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
.login-form {
.form-item {
margin-bottom: 40rpx;
.input-wrapper {
position: relative;
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
backdrop-filter: blur(10rpx);
.input-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.form-input {
flex: 1;
height: 100rpx;
font-size: 32rpx;
color: #333333;
&.code-input {
padding-right: 160rpx;
}
}
.code-button {
position: absolute;
right: 20rpx;
height: 60rpx;
padding: 0 20rpx;
background: linear-gradient(135deg, #2E8B57, #3CB371);
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 24rpx;
&.disabled {
background: #cccccc;
color: #999999;
}
&::after {
border: none;
}
}
}
.error-text {
display: block;
font-size: 24rpx;
color: #ff6b6b;
margin-top: 16rpx;
margin-left: 30rpx;
}
}
.login-button {
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #2E8B57, #3CB371);
color: #ffffff;
border: none;
border-radius: 16rpx;
font-size: 36rpx;
font-weight: 600;
margin-bottom: 60rpx;
&.disabled {
background: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.6);
}
&::after {
border: none;
}
}
}
.other-login {
text-align: center;
margin-bottom: 60rpx;
.other-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 30rpx;
display: block;
}
.other-methods {
.wechat-login {
background: rgba(255, 255, 255, 0.95);
border: none;
border-radius: 16rpx;
padding: 20rpx 40rpx;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10rpx);
.method-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.method-text {
font-size: 32rpx;
color: #333333;
}
&::after {
border: none;
}
}
}
}
.agreement {
.agreement-item {
display: flex;
align-items: flex-start;
.agreement-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
margin-left: 16rpx;
.link-text {
color: #ffffff;
text-decoration: underline;
}
}
}
}
</style>

View File

@@ -0,0 +1,87 @@
<!--登录页面-->
<view class="login-container">
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="bg-circle bg-circle-1"></view>
<view class="bg-circle bg-circle-2"></view>
<view class="bg-circle bg-circle-3"></view>
</view>
<!-- 顶部Logo区域 -->
<view class="logo-section">
<image class="logo" src="/images/logo.png" mode="aspectFit"></image>
<text class="app-name">智慧养殖管理</text>
<text class="app-desc">专业的畜牧养殖管理平台</text>
</view>
<!-- 登录区域 -->
<view class="login-section">
<view class="login-card">
<view class="card-header">
<text class="welcome-title">欢迎使用</text>
<text class="welcome-desc">请登录您的账号开始使用</text>
</view>
<view class="login-methods">
<!-- 微信登录按钮 -->
<button
class="login-btn wx-login-btn"
open-type="getUserProfile"
bindgetuserprofile="onGetUserInfo"
loading="{{loading}}"
disabled="{{loading}}"
>
<image class="btn-icon" src="/images/icons/wechat.png" mode="aspectFit"></image>
<text class="btn-text">{{loading ? '登录中...' : '微信快速登录'}}</text>
</button>
<!-- 兼容旧版本的登录方式 -->
<button
wx:if="{{!canIUse}}"
class="login-btn wx-login-btn"
open-type="getUserInfo"
bindgetuserinfo="onGetUserInfo"
loading="{{loading}}"
disabled="{{loading}}"
>
<image class="btn-icon" src="/images/icons/wechat.png" mode="aspectFit"></image>
<text class="btn-text">{{loading ? '登录中...' : '微信快速登录'}}</text>
</button>
<!-- 游客模式 -->
<view class="guest-login">
<text class="guest-text">暂不登录,</text>
<text class="guest-link" bindtap="onSkipLogin">以游客身份浏览</text>
</view>
</view>
</view>
</view>
<!-- 底部协议 -->
<view class="agreement-section">
<text class="agreement-text">登录即表示同意</text>
<text class="agreement-link" bindtap="onUserAgreement">《用户协议》</text>
<text class="agreement-text">和</text>
<text class="agreement-link" bindtap="onPrivacyPolicy">《隐私政策》</text>
</view>
<!-- 功能特色展示 -->
<view class="features-section">
<view class="feature-item">
<image class="feature-icon" src="/images/icons/farm.png" mode="aspectFit"></image>
<text class="feature-text">养殖场管理</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/icons/animal.png" mode="aspectFit"></image>
<text class="feature-text">动物档案</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/icons/health.png" mode="aspectFit"></image>
<text class="feature-text">健康监控</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/icons/chart.png" mode="aspectFit"></image>
<text class="feature-text">数据统计</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,268 @@
/* 登录页面样式 */
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #2E8B57 0%, #3CB371 100%);
position: relative;
display: flex;
flex-direction: column;
padding: 0 32rpx;
box-sizing: border-box;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
}
.bg-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.bg-circle-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
right: -150rpx;
}
.bg-circle-2 {
width: 200rpx;
height: 200rpx;
top: 200rpx;
left: -100rpx;
}
.bg-circle-3 {
width: 150rpx;
height: 150rpx;
bottom: 300rpx;
right: 50rpx;
}
/* Logo区域 */
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0 80rpx;
position: relative;
z-index: 1;
}
.logo {
width: 120rpx;
height: 120rpx;
margin-bottom: 32rpx;
}
.app-name {
font-size: 48rpx;
font-weight: 700;
color: #fff;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.app-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
}
/* 登录区域 */
.login-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
z-index: 1;
}
.login-card {
background: #fff;
border-radius: 32rpx;
padding: 48rpx 40rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10rpx);
}
.card-header {
text-align: center;
margin-bottom: 48rpx;
}
.welcome-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.welcome-desc {
display: block;
font-size: 26rpx;
color: #666;
}
/* 登录方式 */
.login-methods {
display: flex;
flex-direction: column;
align-items: center;
}
.login-btn {
width: 100%;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
outline: none;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.wx-login-btn {
background: #07c160;
color: #fff;
margin-bottom: 32rpx;
}
.wx-login-btn:active {
background: #06ad56;
transform: scale(0.98);
}
.wx-login-btn[disabled] {
background: #ccc;
transform: none;
}
.btn-icon {
width: 36rpx;
height: 36rpx;
margin-right: 16rpx;
}
.btn-text {
font-size: 32rpx;
font-weight: 500;
}
/* 游客登录 */
.guest-login {
display: flex;
align-items: center;
margin-top: 16rpx;
}
.guest-text {
font-size: 26rpx;
color: #999;
}
.guest-link {
font-size: 26rpx;
color: #2E8B57;
text-decoration: underline;
}
/* 协议区域 */
.agreement-section {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
padding: 32rpx 0;
position: relative;
z-index: 1;
}
.agreement-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.agreement-link {
font-size: 24rpx;
color: #fff;
text-decoration: underline;
margin: 0 8rpx;
}
/* 功能特色 */
.features-section {
display: flex;
justify-content: space-around;
padding: 40rpx 0 60rpx;
position: relative;
z-index: 1;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
}
.feature-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 12rpx;
opacity: 0.8;
}
.feature-text {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
}
/* 加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式适配 */
@media (max-width: 750rpx) {
.login-container {
padding: 0 24rpx;
}
.logo-section {
padding: 100rpx 0 60rpx;
}
.logo {
width: 100rpx;
height: 100rpx;
}
.app-name {
font-size: 42rpx;
}
.login-card {
padding: 40rpx 32rpx;
}
.features-section {
padding: 32rpx 0 40rpx;
}
}

View File

@@ -0,0 +1,253 @@
// 养殖场详情页面
const app = getApp();
Page({
data: {
farmId: null,
farmInfo: null,
animalList: [],
statistics: null,
weatherInfo: null,
loading: true,
refreshing: false,
activeTab: 'overview', // overview, animals, statistics
tabs: [
{ key: 'overview', name: '概览' },
{ key: 'animals', name: '动物' },
{ key: 'statistics', name: '统计' }
]
},
onLoad(options) {
if (options.id) {
this.setData({ farmId: options.id });
this.loadFarmDetail();
} else {
app.showError('参数错误');
wx.navigateBack();
}
},
onShow() {
if (this.data.farmId) {
this.loadFarmDetail();
}
},
onPullDownRefresh() {
this.setData({ refreshing: true });
this.loadFarmDetail().finally(() => {
this.setData({ refreshing: false });
wx.stopPullDownRefresh();
});
},
// 加载养殖场详情
async loadFarmDetail() {
try {
this.setData({ loading: true });
await Promise.all([
this.loadFarmInfo(),
this.loadFarmStatistics(),
this.loadAnimalList(),
this.loadWeatherInfo()
]);
} catch (error) {
console.error('加载养殖场详情失败:', error);
app.showError('加载失败,请重试');
} finally {
this.setData({ loading: false });
}
},
// 加载养殖场基本信息
async loadFarmInfo() {
try {
const res = await app.request({
url: `/farms/${this.data.farmId}`,
method: 'GET'
});
this.setData({
farmInfo: res.data
});
// 设置页面标题
wx.setNavigationBarTitle({
title: res.data.name
});
} catch (error) {
console.error('加载养殖场信息失败:', error);
throw error;
}
},
// 加载统计数据
async loadFarmStatistics() {
try {
const res = await app.request({
url: `/farms/${this.data.farmId}/statistics`,
method: 'GET'
});
this.setData({
statistics: res.data
});
} catch (error) {
console.error('加载统计数据失败:', error);
}
},
// 加载动物列表
async loadAnimalList() {
try {
const res = await app.request({
url: '/animals',
method: 'GET',
data: {
farm_id: this.data.farmId,
limit: 10
}
});
this.setData({
animalList: res.data.list || []
});
} catch (error) {
console.error('加载动物列表失败:', error);
}
},
// 加载天气信息
async loadWeatherInfo() {
try {
// 这里应该根据养殖场位置获取天气信息
// 暂时使用模拟数据
this.setData({
weatherInfo: {
temperature: 15,
humidity: 65,
weather: '晴',
wind: '微风',
updated_at: new Date().toLocaleString()
}
});
} catch (error) {
console.error('加载天气信息失败:', error);
}
},
// 切换标签
onTabChange(e) {
const { tab } = e.currentTarget.dataset;
this.setData({
activeTab: tab
});
},
// 编辑养殖场
onEditFarm() {
wx.navigateTo({
url: `/pages/farm/edit?id=${this.data.farmId}`
});
},
// 添加动物
onAddAnimal() {
wx.navigateTo({
url: `/pages/animal/add?farmId=${this.data.farmId}`
});
},
// 查看所有动物
onViewAllAnimals() {
wx.navigateTo({
url: `/pages/animal/list?farmId=${this.data.farmId}`
});
},
// 查看动物详情
onAnimalTap(e) {
const { animal } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/animal/detail?id=${animal.id}`
});
},
// 查看位置
onViewLocation() {
const { farmInfo } = this.data;
if (farmInfo.longitude && farmInfo.latitude) {
wx.openLocation({
latitude: farmInfo.latitude,
longitude: farmInfo.longitude,
name: farmInfo.name,
address: farmInfo.address
});
} else {
app.showError('位置信息不完整');
}
},
// 拨打电话
onCallPhone() {
const { farmInfo } = this.data;
if (farmInfo.contact_phone) {
wx.makePhoneCall({
phoneNumber: farmInfo.contact_phone
});
} else {
app.showError('联系电话不存在');
}
},
// 分享养殖场
onShareAppMessage() {
const { farmInfo } = this.data;
return {
title: `${farmInfo.name} - 智慧养殖管理`,
path: `/pages/farm/detail?id=${this.data.farmId}`,
imageUrl: farmInfo.cover_image || '/images/share-farm.png'
};
},
// 格式化状态
formatStatus(status) {
const statusMap = {
'active': { text: '正常运营', class: 'status-success' },
'pending': { text: '待审核', class: 'status-warning' },
'suspended': { text: '暂停运营', class: 'status-danger' },
'inactive': { text: '已停用', class: 'status-info' }
};
return statusMap[status] || { text: '未知状态', class: 'status-info' };
},
// 格式化面积
formatArea(area) {
if (area >= 10000) {
return (area / 10000).toFixed(1) + '万㎡';
}
return area + '㎡';
},
// 格式化数值
formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
return num.toString();
},
// 格式化金额
formatMoney(amount) {
if (amount >= 10000) {
return '¥' + (amount / 10000).toFixed(1) + '万';
}
return '¥' + amount.toFixed(2);
}
});

View File

@@ -0,0 +1,228 @@
<!--养殖场详情页面-->
<view class="page-container">
<!-- 养殖场头部信息 -->
<view class="farm-header" wx:if="{{farmInfo}}">
<view class="header-bg">
<image
class="bg-image"
src="{{farmInfo.cover_image || '/images/default-farm-bg.jpg'}}"
mode="aspectFill"
></image>
<view class="header-overlay"></view>
</view>
<view class="header-content">
<view class="farm-basic-info">
<text class="farm-name">{{farmInfo.name}}</text>
<text class="farm-code">编号:{{farmInfo.code}}</text>
<view class="farm-status-row">
<text class="status {{formatStatus(farmInfo.status).class}}">
{{formatStatus(farmInfo.status).text}}
</text>
<text class="farm-type">{{farmInfo.type_name}}</text>
</view>
</view>
<view class="header-actions">
<button class="action-btn" bindtap="onEditFarm">
<image src="/images/icons/edit.png" mode="aspectFit"></image>
</button>
<button class="action-btn" open-type="share">
<image src="/images/icons/share.png" mode="aspectFit"></image>
</button>
</view>
</view>
</view>
<!-- 标签切换 -->
<view class="tab-bar">
<view
class="tab-item {{activeTab === item.key ? 'active' : ''}}"
wx:for="{{tabs}}"
wx:key="key"
data-tab="{{item.key}}"
bindtap="onTabChange"
>
<text>{{item.name}}</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
refresher-enabled
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onPullDownRefresh"
>
<!-- 概览标签 -->
<view class="tab-content" wx:if="{{activeTab === 'overview'}}">
<!-- 基本信息卡片 -->
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-grid">
<view class="info-item">
<text class="info-label">面积</text>
<text class="info-value">{{formatArea(farmInfo.area)}}</text>
</view>
<view class="info-item">
<text class="info-label">容量</text>
<text class="info-value">{{formatNumber(farmInfo.capacity)}}头</text>
</view>
<view class="info-item">
<text class="info-label">存栏</text>
<text class="info-value">{{statistics.overview.total_animals}}头</text>
</view>
<view class="info-item">
<text class="info-label">健康率</text>
<text class="info-value text-success">{{statistics.overview.health_rate}}%</text>
</view>
</view>
</view>
<!-- 天气信息卡片 -->
<view class="weather-card" wx:if="{{weatherInfo}}">
<view class="card-title">天气信息</view>
<view class="weather-content">
<view class="weather-main">
<text class="temperature">{{weatherInfo.temperature}}°C</text>
<text class="weather-desc">{{weatherInfo.weather}}</text>
</view>
<view class="weather-details">
<view class="weather-item">
<text class="weather-label">湿度</text>
<text class="weather-value">{{weatherInfo.humidity}}%</text>
</view>
<view class="weather-item">
<text class="weather-label">风力</text>
<text class="weather-value">{{weatherInfo.wind}}</text>
</view>
</view>
</view>
<text class="weather-update">更新时间:{{weatherInfo.updated_at}}</text>
</view>
<!-- 地址信息卡片 -->
<view class="address-card">
<view class="card-title">位置信息</view>
<view class="address-content">
<text class="address-text">{{farmInfo.address}}</text>
<button class="location-btn" bindtap="onViewLocation">
<image src="/images/icons/location.png" mode="aspectFit"></image>
<text>查看位置</text>
</button>
</view>
</view>
<!-- 联系信息卡片 -->
<view class="contact-card" wx:if="{{farmInfo.contact_phone}}">
<view class="card-title">联系方式</view>
<view class="contact-content">
<text class="contact-phone">{{farmInfo.contact_phone}}</text>
<button class="call-btn" bindtap="onCallPhone">
<image src="/images/icons/phone.png" mode="aspectFit"></image>
<text>拨打电话</text>
</button>
</view>
</view>
</view>
<!-- 动物标签 -->
<view class="tab-content" wx:if="{{activeTab === 'animals'}}">
<view class="animals-header">
<text class="section-title">动物列表</text>
<view class="animals-actions">
<button class="btn-small btn-secondary" bindtap="onViewAllAnimals">查看全部</button>
<button class="btn-small btn-primary" bindtap="onAddAnimal">添加动物</button>
</view>
</view>
<view class="animals-list" wx:if="{{animalList.length > 0}}">
<view
class="animal-item"
wx:for="{{animalList}}"
wx:key="id"
data-animal="{{item}}"
bindtap="onAnimalTap"
>
<image
class="animal-avatar"
src="{{item.avatar || '/images/default-animal.png'}}"
mode="aspectFill"
></image>
<view class="animal-info">
<text class="animal-name">{{item.name}}</text>
<text class="animal-breed">{{item.breed}}</text>
<text class="animal-age">{{item.age_days}}天</text>
</view>
<view class="animal-status">
<text class="status {{item.health_status === 'healthy' ? 'status-success' : 'status-warning'}}">
{{item.health_status_name}}
</text>
<text class="animal-weight">{{item.weight}}kg</text>
</view>
</view>
</view>
<view class="empty" wx:if="{{animalList.length === 0}}">
<image class="empty-icon" src="/images/empty-animal.png" mode="aspectFit"></image>
<text class="empty-text">还没有动物</text>
<button class="btn btn-primary mt-3" bindtap="onAddAnimal">添加动物</button>
</view>
</view>
<!-- 统计标签 -->
<view class="tab-content" wx:if="{{activeTab === 'statistics' && statistics}}">
<!-- 概览统计 -->
<view class="stats-overview">
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{statistics.overview.total_animals}}</text>
<text class="stat-label">总数量</text>
</view>
<view class="stat-item">
<text class="stat-number">{{statistics.overview.healthy_animals}}</text>
<text class="stat-label">健康</text>
</view>
<view class="stat-item">
<text class="stat-number">{{statistics.overview.sick_animals}}</text>
<text class="stat-label">患病</text>
</view>
<view class="stat-item">
<text class="stat-number">{{formatMoney(statistics.overview.total_value)}}</text>
<text class="stat-label">总价值</text>
</view>
</view>
</view>
<!-- 品种分布 -->
<view class="breed-distribution" wx:if="{{statistics.breed_distribution}}">
<view class="card-title">品种分布</view>
<view class="breed-list">
<view
class="breed-item"
wx:for="{{statistics.breed_distribution}}"
wx:key="breed"
>
<text class="breed-name">{{item.breed}}</text>
<view class="breed-progress">
<view class="progress-bar">
<view
class="progress-fill"
style="width: {{item.percentage}}%"
></view>
</view>
<text class="breed-count">{{item.count}}头 ({{item.percentage}}%)</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 加载状态 -->
<view class="loading" wx:if="{{loading}}">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
</view>

View File

@@ -0,0 +1,599 @@
/* 养殖场详情页面样式 */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 养殖场头部 */
.farm-header {
position: relative;
height: 400rpx;
overflow: hidden;
}
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.bg-image {
width: 100%;
height: 100%;
}
.header-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.6));
}
.header-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 32rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: flex-end;
color: #fff;
}
.farm-basic-info {
flex: 1;
}
.farm-name {
display: block;
font-size: 40rpx;
font-weight: 700;
margin-bottom: 8rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
}
.farm-code {
display: block;
font-size: 24rpx;
opacity: 0.9;
margin-bottom: 16rpx;
}
.farm-status-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.farm-type {
font-size: 24rpx;
opacity: 0.9;
}
.header-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: none;
backdrop-filter: blur(10rpx);
}
.action-btn image {
width: 32rpx;
height: 32rpx;
}
/* 标签栏 */
.tab-bar {
display: flex;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
position: relative;
transition: all 0.3s ease;
}
.tab-item.active {
color: #2E8B57;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #2E8B57;
border-radius: 2rpx;
}
/* 内容滚动区域 */
.content-scroll {
flex: 1;
padding: 0 24rpx;
}
.tab-content {
padding: 20rpx 0;
}
/* 信息卡片 */
.info-card,
.weather-card,
.address-card,
.contact-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
/* 基本信息网格 */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.info-label {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.info-value {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
/* 天气卡片 */
.weather-content {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.weather-main {
display: flex;
align-items: center;
gap: 16rpx;
}
.temperature {
font-size: 48rpx;
font-weight: 700;
color: #2E8B57;
}
.weather-desc {
font-size: 28rpx;
color: #333;
}
.weather-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.weather-item {
display: flex;
align-items: center;
gap: 12rpx;
}
.weather-label {
font-size: 24rpx;
color: #666;
min-width: 60rpx;
}
.weather-value {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
.weather-update {
font-size: 20rpx;
color: #999;
}
/* 地址卡片 */
.address-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.address-text {
flex: 1;
font-size: 26rpx;
color: #333;
line-height: 1.5;
margin-right: 16rpx;
}
.location-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 20rpx;
background: #f0f8f0;
color: #2E8B57;
border-radius: 8rpx;
font-size: 24rpx;
border: none;
}
.location-btn image {
width: 24rpx;
height: 24rpx;
}
/* 联系卡片 */
.contact-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.contact-phone {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-right: 16rpx;
}
.call-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 20rpx;
background: #e8f5e8;
color: #2E8B57;
border-radius: 8rpx;
font-size: 24rpx;
border: none;
}
.call-btn image {
width: 24rpx;
height: 24rpx;
}
/* 动物列表 */
.animals-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.animals-actions {
display: flex;
gap: 12rpx;
}
.btn-small {
padding: 12rpx 20rpx;
font-size: 24rpx;
border-radius: 8rpx;
border: none;
}
.btn-secondary {
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
}
.btn-primary {
background: #2E8B57;
color: #fff;
}
.animals-list {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.animal-item {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: all 0.3s ease;
}
.animal-item:last-child {
border-bottom: none;
}
.animal-item:active {
background: #f8f9fa;
}
.animal-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 2rpx solid #f0f0f0;
}
.animal-info {
flex: 1;
display: flex;
flex-direction: column;
}
.animal-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.animal-breed {
font-size: 24rpx;
color: #666;
margin-bottom: 4rpx;
}
.animal-age {
font-size: 22rpx;
color: #999;
}
.animal-status {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.animal-weight {
font-size: 24rpx;
color: #666;
margin-top: 8rpx;
}
/* 统计概览 */
.stats-overview {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.stat-number {
font-size: 32rpx;
font-weight: 700;
color: #2E8B57;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
/* 品种分布 */
.breed-distribution {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.breed-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.breed-item {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.breed-name {
font-size: 26rpx;
font-weight: 500;
color: #333;
}
.breed-progress {
display: flex;
align-items: center;
gap: 16rpx;
}
.progress-bar {
flex: 1;
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2E8B57, #3CB371);
border-radius: 6rpx;
transition: width 0.3s ease;
}
.breed-count {
font-size: 22rpx;
color: #666;
min-width: 120rpx;
text-align: right;
}
/* 状态样式 */
.status {
display: inline-block;
padding: 6rpx 12rpx;
border-radius: 16rpx;
font-size: 20rpx;
font-weight: 500;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.status-danger {
background: #f8d7da;
color: #721c24;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
/* 工具类 */
.text-success {
color: #28a745;
}
.text-warning {
color: #ffc107;
}
.text-danger {
color: #dc3545;
}
.mt-3 {
margin-top: 32rpx;
}
/* 响应式适配 */
@media (max-width: 750rpx) {
.farm-header {
height: 320rpx;
}
.farm-name {
font-size: 36rpx;
}
.info-grid,
.stats-grid {
grid-template-columns: 1fr;
gap: 16rpx;
}
.weather-content {
flex-direction: column;
align-items: flex-start;
gap: 16rpx;
}
.address-content,
.contact-content {
flex-direction: column;
align-items: flex-start;
gap: 16rpx;
}
.animals-header {
flex-direction: column;
align-items: flex-start;
gap: 16rpx;
}
.breed-progress {
flex-direction: column;
align-items: flex-start;
gap: 8rpx;
}
.breed-count {
min-width: auto;
text-align: left;
}
}

View File

@@ -0,0 +1,240 @@
// 养殖场列表页面
const app = getApp();
Page({
data: {
farmList: [],
loading: true,
refreshing: false,
hasMore: true,
cursor: null,
searchKeyword: '',
filterStatus: 'all',
statusOptions: [
{ value: 'all', label: '全部状态' },
{ value: 'active', label: '正常运营' },
{ value: 'pending', label: '待审核' },
{ value: 'suspended', label: '暂停运营' }
]
},
onLoad() {
this.checkLogin();
},
onShow() {
if (app.globalData.userInfo) {
this.loadFarmList(true);
}
},
onPullDownRefresh() {
this.setData({ refreshing: true });
this.loadFarmList(true).finally(() => {
this.setData({ refreshing: false });
wx.stopPullDownRefresh();
});
},
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.loadFarmList();
}
},
// 检查登录状态
checkLogin() {
if (!app.globalData.userInfo) {
wx.navigateTo({
url: '/pages/auth/login'
});
return;
}
this.loadFarmList(true);
},
// 加载养殖场列表
async loadFarmList(refresh = false) {
if (this.data.loading && !refresh) return;
try {
this.setData({ loading: true });
if (refresh) {
this.setData({
farmList: [],
cursor: null,
hasMore: true
});
}
const params = {
limit: 20,
cursor: this.data.cursor
};
// 添加搜索条件
if (this.data.searchKeyword) {
params.keyword = this.data.searchKeyword;
}
// 添加状态筛选
if (this.data.filterStatus !== 'all') {
params.status = this.data.filterStatus;
}
const res = await app.request({
url: '/farms',
method: 'GET',
data: params
});
const { list, has_more, next_cursor } = res.data;
this.setData({
farmList: refresh ? list : [...this.data.farmList, ...list],
hasMore: has_more,
cursor: next_cursor
});
} catch (error) {
console.error('加载养殖场列表失败:', error);
app.showError('加载失败,请重试');
} finally {
this.setData({ loading: false });
}
},
// 搜索输入
onSearchInput(e) {
this.setData({
searchKeyword: e.detail.value
});
},
// 搜索确认
onSearchConfirm() {
this.loadFarmList(true);
},
// 清除搜索
onSearchClear() {
this.setData({
searchKeyword: ''
});
this.loadFarmList(true);
},
// 状态筛选
onStatusFilter() {
wx.showActionSheet({
itemList: this.data.statusOptions.map(item => item.label),
success: (res) => {
const selectedStatus = this.data.statusOptions[res.tapIndex];
this.setData({
filterStatus: selectedStatus.value
});
this.loadFarmList(true);
}
});
},
// 点击养殖场项
onFarmTap(e) {
const { farm } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/farm/detail?id=${farm.id}`
});
},
// 编辑养殖场
onEditFarm(e) {
e.stopPropagation();
const { farm } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/farm/edit?id=${farm.id}`
});
},
// 删除养殖场
onDeleteFarm(e) {
e.stopPropagation();
const { farm } = e.currentTarget.dataset;
wx.showModal({
title: '确认删除',
content: `确定要删除养殖场"${farm.name}"吗?此操作不可恢复。`,
confirmText: '删除',
confirmColor: '#dc3545',
success: async (res) => {
if (res.confirm) {
await this.deleteFarm(farm.id);
}
}
});
},
// 执行删除
async deleteFarm(farmId) {
try {
app.showLoading('删除中...');
await app.request({
url: `/farms/${farmId}`,
method: 'DELETE'
});
app.showSuccess('删除成功');
this.loadFarmList(true);
} catch (error) {
console.error('删除养殖场失败:', error);
app.showError('删除失败,请重试');
} finally {
app.hideLoading();
}
},
// 添加养殖场
onAddFarm() {
wx.navigateTo({
url: '/pages/farm/add'
});
},
// 查看统计
onViewStatistics(e) {
e.stopPropagation();
const { farm } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/statistics/farm?farmId=${farm.id}`
});
},
// 格式化状态
formatStatus(status) {
const statusMap = {
'active': { text: '正常运营', class: 'status-success' },
'pending': { text: '待审核', class: 'status-warning' },
'suspended': { text: '暂停运营', class: 'status-danger' },
'inactive': { text: '已停用', class: 'status-info' }
};
return statusMap[status] || { text: '未知状态', class: 'status-info' };
},
// 格式化面积
formatArea(area) {
if (area >= 10000) {
return (area / 10000).toFixed(1) + '万㎡';
}
return area + '㎡';
},
// 格式化数值
formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
return num.toString();
}
});

View File

@@ -0,0 +1,726 @@
<template>
<view class="farm-list-container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<text class="search-icon">🔍</text>
<input
class="search-input"
type="text"
placeholder="搜索养殖场名称"
v-model="searchKeyword"
@input="onSearchInput"
/>
<text class="clear-icon" v-if="searchKeyword" @tap="clearSearch">×</text>
</view>
<button class="filter-button" @tap="showFilter">
<text class="filter-icon">🔽</text>
<text>筛选</text>
</button>
</view>
<!-- 统计卡片 -->
<view class="stats-cards">
<view class="stat-card">
<text class="stat-number">{{ stats.totalFarms }}</text>
<text class="stat-label">总养殖场</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ stats.activeFarms }}</text>
<text class="stat-label">运营中</text>
</view>
<view class="stat-card">
<text class="stat-number">{{ stats.totalAnimals }}</text>
<text class="stat-label">总牲畜</text>
</view>
</view>
<!-- 养殖场列表 -->
<view class="farm-list">
<view
class="farm-item"
v-for="farm in filteredFarms"
:key="farm.id"
@tap="goToFarmDetail(farm.id)"
>
<!-- 养殖场图片 -->
<view class="farm-image">
<image
:src="farm.coverImage || '/static/images/farm-default.jpg'"
mode="aspectFill"
class="image"
></image>
<view class="status-badge" :class="farm.status">
{{ getStatusText(farm.status) }}
</view>
</view>
<!-- 养殖场信息 -->
<view class="farm-info">
<view class="farm-header">
<text class="farm-name">{{ farm.name }}</text>
<text class="farm-type">{{ farm.type }}</text>
</view>
<view class="farm-details">
<view class="detail-item">
<text class="detail-icon">📍</text>
<text class="detail-text">{{ farm.location }}</text>
</view>
<view class="detail-item">
<text class="detail-icon">🐄</text>
<text class="detail-text">{{ farm.animalCount }}头牲畜</text>
</view>
<view class="detail-item">
<text class="detail-icon">📏</text>
<text class="detail-text">{{ farm.area }}</text>
</view>
</view>
<view class="farm-footer">
<text class="update-time">更新于 {{ formatTime(farm.updatedAt) }}</text>
<view class="action-buttons">
<button class="action-btn edit-btn" @tap.stop="editFarm(farm)">编辑</button>
<button class="action-btn view-btn" @tap.stop="goToFarmDetail(farm.id)">查看</button>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<empty
v-if="!loading && filteredFarms.length === 0"
title="暂无养殖场"
description="点击右下角按钮添加第一个养殖场"
:show-action="true"
action-text="添加养殖场"
@action="addFarm"
></empty>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<button class="load-more-btn" @tap="loadMore">加载更多</button>
</view>
<!-- 添加按钮 -->
<view class="add-button" @tap="addFarm">
<text class="add-icon">+</text>
</view>
<!-- 筛选弹窗 -->
<modal
:show="showFilterModal"
title="筛选条件"
@close="showFilterModal = false"
@confirm="applyFilter"
>
<view class="filter-content">
<!-- 状态筛选 -->
<view class="filter-group">
<text class="filter-title">运营状态</text>
<radio-group @change="onStatusChange">
<label class="filter-option" v-for="status in statusOptions" :key="status.value">
<radio :value="status.value" :checked="filterData.status === status.value" />
<text class="option-text">{{ status.label }}</text>
</label>
</radio-group>
</view>
<!-- 类型筛选 -->
<view class="filter-group">
<text class="filter-title">养殖类型</text>
<checkbox-group @change="onTypeChange">
<label class="filter-option" v-for="type in typeOptions" :key="type.value">
<checkbox :value="type.value" :checked="filterData.types.includes(type.value)" />
<text class="option-text">{{ type.label }}</text>
</label>
</checkbox-group>
</view>
<!-- 规模筛选 -->
<view class="filter-group">
<text class="filter-title">养殖规模</text>
<view class="scale-filter">
<input
class="scale-input"
type="number"
placeholder="最小数量"
v-model="filterData.minAnimals"
/>
<text class="scale-separator">-</text>
<input
class="scale-input"
type="number"
placeholder="最大数量"
v-model="filterData.maxAnimals"
/>
</view>
</view>
</view>
</modal>
<!-- 加载组件 -->
<loading :show="loading" text="加载中..."></loading>
</view>
</template>
<script>
import Empty from '@/common/components/empty/empty.vue'
import Modal from '@/common/components/modal/modal.vue'
import Loading from '@/common/components/loading/loading.vue'
export default {
name: 'FarmList',
components: {
Empty,
Modal,
Loading
},
data() {
return {
searchKeyword: '',
loading: false,
hasMore: true,
currentPage: 1,
pageSize: 10,
// 统计数据
stats: {
totalFarms: 0,
activeFarms: 0,
totalAnimals: 0
},
// 养殖场列表
farms: [],
// 筛选相关
showFilterModal: false,
filterData: {
status: '',
types: [],
minAnimals: '',
maxAnimals: ''
},
// 筛选选项
statusOptions: [
{ label: '全部', value: '' },
{ label: '运营中', value: 'active' },
{ label: '暂停', value: 'paused' },
{ label: '维护中', value: 'maintenance' }
],
typeOptions: [
{ label: '肉牛养殖', value: 'beef_cattle' },
{ label: '奶牛养殖', value: 'dairy_cattle' },
{ label: '羊养殖', value: 'sheep' },
{ label: '猪养殖', value: 'pig' },
{ label: '鸡养殖', value: 'chicken' }
]
}
},
computed: {
// 过滤后的养殖场列表
filteredFarms() {
let result = this.farms
// 搜索过滤
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase()
result = result.filter(farm =>
farm.name.toLowerCase().includes(keyword) ||
farm.location.toLowerCase().includes(keyword)
)
}
return result
}
},
onLoad() {
this.loadFarms()
this.loadStats()
},
onPullDownRefresh() {
this.refreshData()
},
onReachBottom() {
if (this.hasMore && !this.loading) {
this.loadMore()
}
},
methods: {
// 加载养殖场列表
async loadFarms(refresh = false) {
if (this.loading) return
this.loading = true
try {
const params = {
page: refresh ? 1 : this.currentPage,
pageSize: this.pageSize,
keyword: this.searchKeyword,
...this.filterData
}
const res = await this.$request({
url: '/farming/farms',
method: 'GET',
data: params,
loading: false
})
if (refresh) {
this.farms = res.data.list
this.currentPage = 1
} else {
this.farms = [...this.farms, ...res.data.list]
}
this.hasMore = res.data.hasMore
this.currentPage++
} catch (error) {
console.error('加载养殖场列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
if (refresh) {
uni.stopPullDownRefresh()
}
}
},
// 加载统计数据
async loadStats() {
try {
const res = await this.$request({
url: '/farming/stats',
method: 'GET',
loading: false
})
this.stats = res.data
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
// 刷新数据
refreshData() {
this.loadFarms(true)
this.loadStats()
},
// 加载更多
loadMore() {
this.loadFarms()
},
// 搜索输入
onSearchInput() {
// 防抖处理
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => {
this.refreshData()
}, 500)
},
// 清除搜索
clearSearch() {
this.searchKeyword = ''
this.refreshData()
},
// 显示筛选
showFilter() {
this.showFilterModal = true
},
// 状态变更
onStatusChange(e) {
this.filterData.status = e.detail.value
},
// 类型变更
onTypeChange(e) {
this.filterData.types = e.detail.value
},
// 应用筛选
applyFilter() {
this.showFilterModal = false
this.refreshData()
},
// 跳转到养殖场详情
goToFarmDetail(farmId) {
uni.navigateTo({
url: `/pages/farm/detail?id=${farmId}`
})
},
// 编辑养殖场
editFarm(farm) {
uni.navigateTo({
url: `/pages/farm/edit?id=${farm.id}`
})
},
// 添加养殖场
addFarm() {
uni.navigateTo({
url: '/pages/farm/add'
})
},
// 获取状态文本
getStatusText(status) {
const statusMap = {
active: '运营中',
paused: '暂停',
maintenance: '维护中'
}
return statusMap[status] || '未知'
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return `${date.getMonth() + 1}${date.getDate()}`
}
}
}
}
</script>
<style lang="scss" scoped>
.farm-list-container {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 120rpx;
}
.search-bar {
background: #ffffff;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.search-input-wrapper {
flex: 1;
position: relative;
background: #f8f8f8;
border-radius: 12rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
.search-icon {
font-size: 28rpx;
color: #999999;
margin-right: 16rpx;
}
.search-input {
flex: 1;
height: 70rpx;
font-size: 28rpx;
color: #333333;
}
.clear-icon {
font-size: 32rpx;
color: #999999;
padding: 10rpx;
}
}
.filter-button {
background: #2E8B57;
color: #ffffff;
border: none;
border-radius: 12rpx;
padding: 0 24rpx;
height: 70rpx;
font-size: 28rpx;
display: flex;
align-items: center;
gap: 8rpx;
.filter-icon {
font-size: 24rpx;
}
&::after {
border: none;
}
}
}
.stats-cards {
display: flex;
padding: 30rpx;
gap: 20rpx;
.stat-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx 20rpx;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.stat-number {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #2E8B57;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666666;
}
}
}
.farm-list {
padding: 0 30rpx;
.farm-item {
background: #ffffff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.farm-image {
position: relative;
height: 300rpx;
.image {
width: 100%;
height: 100%;
}
.status-badge {
position: absolute;
top: 20rpx;
right: 20rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #ffffff;
&.active {
background: #4CAF50;
}
&.paused {
background: #FF9800;
}
&.maintenance {
background: #F44336;
}
}
}
.farm-info {
padding: 30rpx;
.farm-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.farm-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
}
.farm-type {
font-size: 24rpx;
color: #2E8B57;
background: rgba(46, 139, 87, 0.1);
padding: 6rpx 12rpx;
border-radius: 8rpx;
}
}
.farm-details {
margin-bottom: 20rpx;
.detail-item {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.detail-icon {
font-size: 24rpx;
margin-right: 12rpx;
width: 32rpx;
}
.detail-text {
font-size: 28rpx;
color: #666666;
}
}
}
.farm-footer {
display: flex;
justify-content: space-between;
align-items: center;
.update-time {
font-size: 24rpx;
color: #999999;
}
.action-buttons {
display: flex;
gap: 16rpx;
.action-btn {
border: none;
border-radius: 8rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
&.edit-btn {
background: #f0f0f0;
color: #666666;
}
&.view-btn {
background: #2E8B57;
color: #ffffff;
}
&::after {
border: none;
}
}
}
}
}
}
}
.load-more {
padding: 30rpx;
text-align: center;
.load-more-btn {
background: #ffffff;
color: #2E8B57;
border: 2rpx solid #2E8B57;
border-radius: 12rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
&::after {
border: none;
}
}
}
.add-button {
position: fixed;
bottom: 40rpx;
right: 40rpx;
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, #2E8B57, #3CB371);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 139, 87, 0.3);
z-index: 100;
.add-icon {
font-size: 60rpx;
color: #ffffff;
font-weight: 300;
}
}
.filter-content {
.filter-group {
margin-bottom: 40rpx;
.filter-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
}
.filter-option {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.option-text {
font-size: 28rpx;
color: #666666;
margin-left: 16rpx;
}
}
.scale-filter {
display: flex;
align-items: center;
gap: 20rpx;
.scale-input {
flex: 1;
height: 80rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.scale-separator {
font-size: 28rpx;
color: #999999;
}
}
}
}
</style>

View File

@@ -0,0 +1,147 @@
<!--养殖场列表页面-->
<view class="page-container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrapper">
<input
class="search-input"
placeholder="搜索养殖场名称或编号"
value="{{searchKeyword}}"
bindinput="onSearchInput"
bindconfirm="onSearchConfirm"
/>
<view class="search-icon" bindtap="onSearchConfirm">
<image src="/images/icons/search.png" mode="aspectFit"></image>
</view>
<view class="clear-icon" wx:if="{{searchKeyword}}" bindtap="onSearchClear">
<image src="/images/icons/close.png" mode="aspectFit"></image>
</view>
</view>
<view class="filter-btn" bindtap="onStatusFilter">
<image src="/images/icons/filter.png" mode="aspectFit"></image>
</view>
</view>
<!-- 养殖场列表 -->
<scroll-view
class="farm-list-scroll"
scroll-y
refresher-enabled
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onPullDownRefresh"
bindscrolltolower="onReachBottom"
>
<view class="farm-list">
<view
class="farm-item"
wx:for="{{farmList}}"
wx:key="id"
data-farm="{{item}}"
bindtap="onFarmTap"
>
<!-- 养殖场基本信息 -->
<view class="farm-header">
<view class="farm-info">
<text class="farm-name">{{item.name}}</text>
<text class="farm-code">编号:{{item.code}}</text>
</view>
<view class="farm-status">
<text class="status {{formatStatus(item.status).class}}">
{{formatStatus(item.status).text}}
</text>
</view>
</view>
<!-- 养殖场详细信息 -->
<view class="farm-details">
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">类型</text>
<text class="detail-value">{{item.type_name}}</text>
</view>
<view class="detail-item">
<text class="detail-label">面积</text>
<text class="detail-value">{{formatArea(item.area)}}</text>
</view>
</view>
<view class="detail-row">
<view class="detail-item">
<text class="detail-label">容量</text>
<text class="detail-value">{{formatNumber(item.capacity)}}头</text>
</view>
<view class="detail-item">
<text class="detail-label">存栏</text>
<text class="detail-value">{{formatNumber(item.animal_count)}}头</text>
</view>
</view>
</view>
<!-- 健康率和地址 -->
<view class="farm-stats">
<view class="health-rate">
<text class="health-label">健康率</text>
<text class="health-value {{item.health_rate >= 95 ? 'text-success' : item.health_rate >= 90 ? 'text-warning' : 'text-danger'}}">
{{item.health_rate}}%
</text>
</view>
<text class="farm-address">{{item.address}}</text>
</view>
<!-- 操作按钮 -->
<view class="farm-actions">
<button
class="action-btn secondary"
data-farm="{{item}}"
bindtap="onViewStatistics"
>
统计
</button>
<button
class="action-btn secondary"
data-farm="{{item}}"
bindtap="onEditFarm"
>
编辑
</button>
<button
class="action-btn danger"
data-farm="{{item}}"
bindtap="onDeleteFarm"
>
删除
</button>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" wx:if="{{hasMore && farmList.length > 0}}">
<view class="loading-spinner" wx:if="{{loading}}"></view>
<text class="load-text">{{loading ? '加载中...' : '上拉加载更多'}}</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" wx:if="{{!hasMore && farmList.length > 0}}">
<text>没有更多数据了</text>
</view>
</scroll-view>
<!-- 空状态 -->
<view class="empty" wx:if="{{!loading && farmList.length === 0}}">
<image class="empty-icon" src="/images/empty-farm.png" mode="aspectFit"></image>
<text class="empty-text">还没有养殖场</text>
<text class="empty-desc">添加您的第一个养殖场开始管理</text>
<button class="btn btn-primary mt-3" bindtap="onAddFarm">添加养殖场</button>
</view>
<!-- 加载状态 -->
<view class="loading" wx:if="{{loading && farmList.length === 0}}">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<!-- 添加按钮 -->
<view class="add-btn" bindtap="onAddFarm">
<image src="/images/icons/add.png" mode="aspectFit"></image>
</view>
</view>

View File

@@ -0,0 +1,365 @@
/* 养殖场列表页面样式 */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.search-input-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
background: #f8f9fa;
border-radius: 24rpx;
padding: 0 48rpx 0 20rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: #333;
}
.search-input::placeholder {
color: #999;
}
.search-icon {
position: absolute;
right: 16rpx;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-icon image {
width: 24rpx;
height: 24rpx;
}
.clear-icon {
position: absolute;
right: 48rpx;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.clear-icon image {
width: 20rpx;
height: 20rpx;
}
.filter-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 24rpx;
}
.filter-btn image {
width: 28rpx;
height: 28rpx;
}
/* 列表滚动区域 */
.farm-list-scroll {
flex: 1;
padding: 0 24rpx;
}
.farm-list {
padding: 20rpx 0;
}
/* 养殖场项 */
.farm-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.farm-item:active {
transform: scale(0.98);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
}
/* 养殖场头部 */
.farm-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.farm-info {
flex: 1;
}
.farm-name {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.farm-code {
font-size: 24rpx;
color: #999;
}
.farm-status {
margin-left: 16rpx;
}
/* 养殖场详情 */
.farm-details {
margin-bottom: 20rpx;
}
.detail-row {
display: flex;
margin-bottom: 12rpx;
}
.detail-row:last-child {
margin-bottom: 0;
}
.detail-item {
flex: 1;
display: flex;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #666;
margin-right: 12rpx;
min-width: 60rpx;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
/* 养殖场统计 */
.farm-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding: 16rpx 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.health-rate {
display: flex;
align-items: center;
}
.health-label {
font-size: 24rpx;
color: #666;
margin-right: 12rpx;
}
.health-value {
font-size: 28rpx;
font-weight: 600;
}
.farm-address {
font-size: 24rpx;
color: #666;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 操作按钮 */
.farm-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
flex: 1;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 500;
border: none;
outline: none;
}
.action-btn.secondary {
background: #f8f9fa;
color: #666;
border: 1rpx solid #e9ecef;
}
.action-btn.secondary:active {
background: #e9ecef;
}
.action-btn.danger {
background: #fff5f5;
color: #dc3545;
border: 1rpx solid #f5c6cb;
}
.action-btn.danger:active {
background: #f5c6cb;
}
/* 加载更多 */
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
color: #999;
}
.load-text {
font-size: 24rpx;
margin-left: 16rpx;
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
color: #ccc;
font-size: 24rpx;
}
/* 添加按钮 */
.add-btn {
position: fixed;
right: 40rpx;
bottom: 40rpx;
width: 112rpx;
height: 112rpx;
background: #2E8B57;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 139, 87, 0.3);
z-index: 100;
}
.add-btn:active {
transform: scale(0.95);
}
.add-btn image {
width: 48rpx;
height: 48rpx;
}
/* 状态样式 */
.status {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 500;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.status-danger {
background: #f8d7da;
color: #721c24;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
/* 工具类 */
.text-success {
color: #28a745;
}
.text-warning {
color: #ffc107;
}
.text-danger {
color: #dc3545;
}
/* 响应式适配 */
@media (max-width: 750rpx) {
.farm-item {
padding: 20rpx;
}
.farm-name {
font-size: 30rpx;
}
.detail-row {
flex-direction: column;
gap: 8rpx;
}
.detail-item {
justify-content: space-between;
}
.farm-stats {
flex-direction: column;
align-items: flex-start;
gap: 12rpx;
}
.farm-address {
max-width: none;
}
}

View File

@@ -0,0 +1,211 @@
// 首页
const app = getApp();
Page({
data: {
userInfo: null,
farmStats: {
farmCount: 0,
animalCount: 0,
healthRate: 0,
totalValue: 0
},
recentActivities: [],
weatherInfo: null,
loading: true,
refreshing: false
},
onLoad() {
this.checkLogin();
},
onShow() {
if (app.globalData.userInfo) {
this.setData({
userInfo: app.globalData.userInfo
});
this.loadData();
}
},
onPullDownRefresh() {
this.setData({ refreshing: true });
this.loadData().finally(() => {
this.setData({ refreshing: false });
wx.stopPullDownRefresh();
});
},
// 检查登录状态
checkLogin() {
if (!app.globalData.userInfo) {
wx.navigateTo({
url: '/pages/auth/login'
});
return;
}
this.setData({
userInfo: app.globalData.userInfo
});
this.loadData();
},
// 加载数据
async loadData() {
try {
this.setData({ loading: true });
await Promise.all([
this.loadFarmStats(),
this.loadRecentActivities(),
this.loadWeatherInfo()
]);
} catch (error) {
console.error('加载数据失败:', error);
app.showError('加载数据失败');
} finally {
this.setData({ loading: false });
}
},
// 加载养殖场统计
async loadFarmStats() {
try {
const res = await app.request({
url: '/statistics/personal',
method: 'GET'
});
this.setData({
farmStats: {
farmCount: res.data.overview.farm_count || 0,
animalCount: res.data.overview.animal_count || 0,
healthRate: res.data.farm_stats[0]?.health_rate || 0,
totalValue: res.data.overview.total_income || 0
}
});
} catch (error) {
console.error('加载养殖场统计失败:', error);
}
},
// 加载最近活动
async loadRecentActivities() {
try {
const res = await app.request({
url: '/messages',
method: 'GET',
data: { limit: 5 }
});
this.setData({
recentActivities: res.data.list || []
});
} catch (error) {
console.error('加载最近活动失败:', error);
}
},
// 加载天气信息
async loadWeatherInfo() {
try {
// 获取位置信息
const location = await this.getLocation();
// 这里应该调用天气API暂时使用模拟数据
this.setData({
weatherInfo: {
temperature: 15,
humidity: 65,
weather: '晴',
location: '北京市朝阳区'
}
});
} catch (error) {
console.error('加载天气信息失败:', error);
}
},
// 获取位置信息
getLocation() {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
success: resolve,
fail: reject
});
});
},
// 导航到养殖场列表
goToFarmList() {
wx.switchTab({
url: '/pages/farm/list'
});
},
// 导航到动物列表
goToAnimalList() {
wx.switchTab({
url: '/pages/animal/list'
});
},
// 导航到统计页面
goToStatistics() {
wx.switchTab({
url: '/pages/statistics/farm'
});
},
// 导航到消息中心
goToMessages() {
wx.navigateTo({
url: '/pages/message/index'
});
},
// 快捷操作
quickActions: [
{
name: '添加动物',
icon: 'add-animal',
url: '/pages/animal/add'
},
{
name: '健康记录',
icon: 'health',
url: '/pages/animal/health'
},
{
name: '数据统计',
icon: 'chart',
url: '/pages/statistics/farm'
},
{
name: '设置',
icon: 'settings',
url: '/pages/settings/index'
}
],
// 快捷操作点击
onQuickAction(e) {
const { url } = e.currentTarget.dataset;
if (url.includes('tab')) {
wx.switchTab({ url });
} else {
wx.navigateTo({ url });
}
},
// 查看活动详情
onActivityTap(e) {
const { item } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/message/detail?id=${item.id}`
});
}
});

View File

@@ -0,0 +1,589 @@
<template>
<view class="page-container">
<!-- 顶部用户信息 -->
<view class="header-section">
<view class="user-info" v-if="userInfo">
<image class="avatar" :src="userInfo.avatar || '/static/images/default-avatar.png'" mode="aspectFill"></image>
<view class="user-details">
<text class="username">{{ userInfo.name || '养殖户' }}</text>
<text class="user-role">{{ userInfo.role || '普通用户' }}</text>
</view>
</view>
<view class="weather-info" v-if="weatherData">
<text class="weather-text">{{ weatherData.weather }}</text>
<text class="temperature">{{ weatherData.temperature }}°C</text>
</view>
</view>
<!-- 数据概览 -->
<view class="stats-section">
<view class="stats-grid">
<view class="stat-item" v-for="(item, index) in statsData" :key="index" @click="onStatClick(item)">
<view class="stat-icon" :style="{ backgroundColor: item.color }">
<text class="iconfont" :class="item.icon"></text>
</view>
<view class="stat-info">
<text class="stat-value">{{ item.value }}</text>
<text class="stat-label">{{ item.label }}</text>
</view>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="section-title">
<text class="title-text">快捷操作</text>
</view>
<view class="actions-grid">
<view class="action-item" v-for="(action, index) in quickActions" :key="index" @click="onActionClick(action)">
<view class="action-icon">
<text class="iconfont" :class="action.icon"></text>
</view>
<text class="action-text">{{ action.name }}</text>
</view>
</view>
</view>
<!-- 最近活动 -->
<view class="recent-activities">
<view class="section-title">
<text class="title-text">最近活动</text>
<text class="more-text" @click="onViewAllActivities">查看全部</text>
</view>
<view class="activity-list">
<view class="activity-item" v-for="(activity, index) in recentActivities" :key="index" @click="onActivityClick(activity)">
<view class="activity-icon">
<text class="iconfont" :class="activity.icon"></text>
</view>
<view class="activity-content">
<text class="activity-title">{{ activity.title }}</text>
<text class="activity-desc">{{ activity.description }}</text>
<text class="activity-time">{{ $utils.formatTime(activity.time) }}</text>
</view>
</view>
</view>
</view>
<!-- 加载组件 -->
<uni-load-more :status="loadStatus" :content-text="loadText"></uni-load-more>
</view>
</template>
<script>
export default {
name: 'Index',
data() {
return {
userInfo: null,
weatherData: null,
statsData: [
{
label: '养殖场',
value: 0,
icon: 'icon-farm',
color: '#2E8B57',
type: 'farm'
},
{
label: '动物总数',
value: 0,
icon: 'icon-animal',
color: '#FF6B6B',
type: 'animal'
},
{
label: '健康率',
value: '0%',
icon: 'icon-health',
color: '#4ECDC4',
type: 'health'
},
{
label: '本月收益',
value: '¥0',
icon: 'icon-money',
color: '#45B7D1',
type: 'income'
}
],
quickActions: [
{
name: '添加动物',
icon: 'icon-add-animal',
url: '/pages/animal/add'
},
{
name: '健康记录',
icon: 'icon-health-record',
url: '/pages/health/record'
},
{
name: '饲料管理',
icon: 'icon-feed',
url: '/pages/feed/index'
},
{
name: '疫苗接种',
icon: 'icon-vaccine',
url: '/pages/vaccine/index'
},
{
name: '生产记录',
icon: 'icon-production',
url: '/pages/production/index'
},
{
name: '销售记录',
icon: 'icon-sale',
url: '/pages/sale/index'
}
],
recentActivities: [],
loadStatus: 'loading',
loadText: {
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
}
}
},
onLoad() {
this.initPage()
},
onShow() {
this.refreshData()
},
onPullDownRefresh() {
this.refreshData().finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
// 初始化页面
async initPage() {
try {
await this.checkLogin()
await this.loadData()
} catch (error) {
console.error('初始化页面失败:', error)
}
},
// 检查登录状态
async checkLogin() {
const token = uni.getStorageSync('token')
if (!token) {
uni.navigateTo({
url: '/pages/auth/login'
})
return Promise.reject('未登录')
}
// 获取用户信息
try {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
this.userInfo = userInfo
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
},
// 加载数据
async loadData() {
this.loadStatus = 'loading'
try {
await Promise.all([
this.loadStatsData(),
this.loadWeatherData(),
this.loadRecentActivities()
])
this.loadStatus = 'noMore'
} catch (error) {
console.error('加载数据失败:', error)
this.loadStatus = 'noMore'
uni.showToast({
title: '加载数据失败',
icon: 'none'
})
}
},
// 刷新数据
async refreshData() {
return this.loadData()
},
// 加载统计数据
async loadStatsData() {
try {
const res = await this.$request({
url: '/farming/stats',
method: 'GET'
})
if (res.data) {
this.statsData[0].value = res.data.farmCount || 0
this.statsData[1].value = res.data.animalCount || 0
this.statsData[2].value = (res.data.healthRate || 0) + '%'
this.statsData[3].value = this.$utils.formatMoney(res.data.monthlyIncome || 0)
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
// 加载天气数据
async loadWeatherData() {
try {
// 获取位置信息
const location = await this.getCurrentLocation()
// 获取天气信息
const res = await this.$request({
url: '/weather/current',
method: 'GET',
data: {
latitude: location.latitude,
longitude: location.longitude
}
})
if (res.data) {
this.weatherData = res.data
}
} catch (error) {
console.error('加载天气数据失败:', error)
}
},
// 加载最近活动
async loadRecentActivities() {
try {
const res = await this.$request({
url: '/farming/activities',
method: 'GET',
data: {
limit: 10
}
})
if (res.data && res.data.list) {
this.recentActivities = res.data.list
}
} catch (error) {
console.error('加载最近活动失败:', error)
}
},
// 获取当前位置
getCurrentLocation() {
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
success: resolve,
fail: reject
})
})
},
// 统计项点击
onStatClick(item) {
switch (item.type) {
case 'farm':
uni.switchTab({
url: '/pages/farm/list'
})
break
case 'animal':
uni.switchTab({
url: '/pages/animal/list'
})
break
case 'health':
uni.navigateTo({
url: '/pages/health/record'
})
break
case 'income':
uni.switchTab({
url: '/pages/statistics/index'
})
break
}
},
// 快捷操作点击
onActionClick(action) {
uni.navigateTo({
url: action.url
})
},
// 活动项点击
onActivityClick(activity) {
if (activity.url) {
uni.navigateTo({
url: activity.url
})
}
},
// 查看全部活动
onViewAllActivities() {
uni.navigateTo({
url: '/pages/activity/list'
})
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* 头部区域 */
.header-section {
padding: 40rpx 30rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.user-details {
display: flex;
flex-direction: column;
.username {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 8rpx;
}
.user-role {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
}
.weather-info {
display: flex;
flex-direction: column;
align-items: flex-end;
.weather-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
}
.temperature {
font-size: 32rpx;
font-weight: bold;
color: #ffffff;
}
}
/* 统计区域 */
.stats-section {
margin: 0 30rpx 40rpx;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.stat-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
display: flex;
align-items: center;
backdrop-filter: blur(10rpx);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
.stat-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
.iconfont {
font-size: 32rpx;
color: #ffffff;
}
}
.stat-info {
flex: 1;
display: flex;
flex-direction: column;
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666666;
}
}
}
/* 快捷操作 */
.quick-actions {
margin: 0 30rpx 40rpx;
}
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.title-text {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
}
.more-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
}
.actions-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.action-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
padding: 30rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
backdrop-filter: blur(10rpx);
.action-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #2E8B57, #3CB371);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.iconfont {
font-size: 32rpx;
color: #ffffff;
}
}
.action-text {
font-size: 24rpx;
color: #333333;
text-align: center;
}
}
/* 最近活动 */
.recent-activities {
margin: 0 30rpx 40rpx;
}
.activity-list {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
backdrop-filter: blur(10rpx);
overflow: hidden;
}
.activity-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.activity-icon {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
.iconfont {
font-size: 28rpx;
color: #ffffff;
}
}
.activity-content {
flex: 1;
display: flex;
flex-direction: column;
.activity-title {
font-size: 32rpx;
font-weight: 500;
color: #333333;
margin-bottom: 8rpx;
}
.activity-desc {
font-size: 28rpx;
color: #666666;
margin-bottom: 8rpx;
}
.activity-time {
font-size: 24rpx;
color: #999999;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<!--首页-->
<view class="page-container">
<!-- 顶部用户信息 -->
<view class="header">
<view class="user-info">
<image class="avatar" src="{{userInfo.avatar || '/images/default-avatar.png'}}" mode="aspectFill"></image>
<view class="user-details">
<text class="nickname">{{userInfo.nickname || '未登录'}}</text>
<text class="location">{{weatherInfo.location || '获取位置中...'}}</text>
</view>
</view>
<view class="weather" wx:if="{{weatherInfo}}">
<text class="temperature">{{weatherInfo.temperature}}°</text>
<text class="weather-desc">{{weatherInfo.weather}}</text>
</view>
</view>
<!-- 统计卡片 -->
<view class="stats-container">
<view class="stats-grid">
<view class="stat-item" bindtap="goToFarmList">
<text class="stat-number">{{farmStats.farmCount}}</text>
<text class="stat-label">养殖场</text>
</view>
<view class="stat-item" bindtap="goToAnimalList">
<text class="stat-number">{{farmStats.animalCount}}</text>
<text class="stat-label">动物数量</text>
</view>
<view class="stat-item" bindtap="goToStatistics">
<text class="stat-number">{{farmStats.healthRate}}%</text>
<text class="stat-label">健康率</text>
</view>
<view class="stat-item" bindtap="goToStatistics">
<text class="stat-number">¥{{farmStats.totalValue}}</text>
<text class="stat-label">总价值</text>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="section-title">快捷操作</view>
<view class="actions-grid">
<view
class="action-item"
wx:for="{{quickActions}}"
wx:key="name"
data-url="{{item.url}}"
bindtap="onQuickAction"
>
<view class="action-icon">
<image src="/images/icons/{{item.icon}}.png" mode="aspectFit"></image>
</view>
<text class="action-name">{{item.name}}</text>
</view>
</view>
</view>
<!-- 最近活动 -->
<view class="recent-activities" wx:if="{{recentActivities.length > 0}}">
<view class="section-header">
<text class="section-title">最近活动</text>
<text class="section-more" bindtap="goToMessages">查看更多</text>
</view>
<view class="activities-list">
<view
class="activity-item"
wx:for="{{recentActivities}}"
wx:key="id"
data-item="{{item}}"
bindtap="onActivityTap"
>
<view class="activity-content">
<text class="activity-title">{{item.title}}</text>
<text class="activity-desc">{{item.content}}</text>
<text class="activity-time">{{item.created_at}}</text>
</view>
<view class="activity-arrow"></view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading" wx:if="{{loading}}">
<view class="loading-spinner"></view>
<text>加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{!loading && farmStats.farmCount === 0}}">
<image class="empty-icon" src="/images/empty-farm.png" mode="aspectFit"></image>
<text class="empty-text">还没有养殖场</text>
<text class="empty-desc">添加您的第一个养殖场开始管理</text>
<button class="btn btn-primary mt-3" bindtap="goToFarmList">添加养殖场</button>
</view>
</view>

View File

@@ -0,0 +1,318 @@
/* 首页样式 */
.page-container {
min-height: 100vh;
background: linear-gradient(135deg, #2E8B57 0%, #3CB371 100%);
}
/* 顶部区域 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 24rpx 32rpx;
color: #fff;
}
.user-info {
display: flex;
align-items: center;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 2rpx solid rgba(255, 255, 255, 0.3);
}
.user-details {
display: flex;
flex-direction: column;
}
.nickname {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.location {
font-size: 24rpx;
opacity: 0.8;
}
.weather {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.temperature {
font-size: 36rpx;
font-weight: 600;
margin-bottom: 4rpx;
}
.weather-desc {
font-size: 24rpx;
opacity: 0.8;
}
/* 统计区域 */
.stats-container {
margin: 0 24rpx 32rpx;
background: #fff;
border-radius: 20rpx;
padding: 32rpx 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
border-radius: 16rpx;
background: #f8f9fa;
transition: all 0.3s ease;
}
.stat-item:active {
background: #e9ecef;
transform: scale(0.98);
}
.stat-number {
font-size: 36rpx;
font-weight: 700;
color: #2E8B57;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
/* 快捷操作 */
.quick-actions {
margin: 0 24rpx 32rpx;
background: #fff;
border-radius: 20rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 16rpx;
transition: all 0.3s ease;
}
.action-item:active {
background: #f8f9fa;
transform: scale(0.95);
}
.action-icon {
width: 64rpx;
height: 64rpx;
margin-bottom: 12rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f8f0;
border-radius: 50%;
}
.action-icon image {
width: 36rpx;
height: 36rpx;
}
.action-name {
font-size: 24rpx;
color: #666;
text-align: center;
}
/* 最近活动 */
.recent-activities {
margin: 0 24rpx 32rpx;
background: #fff;
border-radius: 20rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-more {
font-size: 24rpx;
color: #2E8B57;
}
.activities-list {
display: flex;
flex-direction: column;
}
.activity-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
transition: all 0.3s ease;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-item:active {
background: #f8f9fa;
margin: 0 -24rpx;
padding: 20rpx 24rpx;
border-radius: 12rpx;
}
.activity-content {
flex: 1;
display: flex;
flex-direction: column;
}
.activity-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
font-weight: 500;
}
.activity-desc {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-time {
font-size: 20rpx;
color: #999;
}
.activity-arrow {
width: 16rpx;
height: 16rpx;
border-top: 2rpx solid #ccc;
border-right: 2rpx solid #ccc;
transform: rotate(45deg);
margin-left: 16rpx;
}
/* 加载状态 */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
color: #fff;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 3rpx solid rgba(255, 255, 255, 0.3);
border-top: 3rpx solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
color: #fff;
text-align: center;
}
.empty-icon {
width: 160rpx;
height: 160rpx;
margin-bottom: 32rpx;
opacity: 0.8;
}
.empty-text {
font-size: 32rpx;
font-weight: 500;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 24rpx;
opacity: 0.8;
line-height: 1.5;
margin-bottom: 32rpx;
}
/* 响应式适配 */
@media (max-width: 750rpx) {
.stats-grid {
gap: 24rpx;
}
.actions-grid {
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.action-icon {
width: 56rpx;
height: 56rpx;
}
.action-icon image {
width: 32rpx;
height: 32rpx;
}
.action-name {
font-size: 22rpx;
}
}

View File

@@ -0,0 +1,685 @@
<template>
<view class="livestock-page">
<!-- 顶部搜索栏 -->
<view class="search-section">
<Search
v-model="searchKeyword"
placeholder="搜索牲畜编号、品种"
@search="handleSearch"
@clear="handleSearchClear"
/>
</view>
<!-- 筛选标签 -->
<view class="filter-section">
<Tabs
:tabs="filterTabs"
:activeIndex="activeFilterIndex"
type="button"
size="small"
@change="handleFilterChange"
/>
</view>
<!-- 统计卡片 -->
<view class="stats-section">
<view class="stats-grid">
<Card
v-for="stat in statsData"
:key="stat.key"
:title="stat.value"
:subtitle="stat.label"
:type="stat.type"
size="small"
:clickable="true"
@click="handleStatClick(stat)"
>
<template #extra>
<text class="stat-icon">{{ stat.icon }}</text>
</template>
</Card>
</view>
</view>
<!-- 牲畜列表 -->
<view class="livestock-list">
<Card
v-for="animal in filteredLivestock"
:key="animal.id"
:clickable="true"
class="livestock-card"
@click="handleAnimalClick(animal)"
>
<view class="animal-info">
<!-- 基本信息 -->
<view class="animal-basic">
<view class="animal-header">
<text class="animal-id">{{ animal.id }}</text>
<view class="animal-status" :class="getStatusClass(animal.status)">
{{ getStatusText(animal.status) }}
</view>
</view>
<view class="animal-details">
<view class="detail-item">
<text class="label">品种</text>
<text class="value">{{ animal.breed }}</text>
</view>
<view class="detail-item">
<text class="label">性别</text>
<text class="value">{{ animal.gender === 'male' ? '公' : '母' }}</text>
</view>
<view class="detail-item">
<text class="label">年龄</text>
<text class="value">{{ animal.age }}个月</text>
</view>
<view class="detail-item">
<text class="label">体重</text>
<text class="value">{{ animal.weight }}kg</text>
</view>
</view>
</view>
<!-- 健康状态 -->
<view class="animal-health">
<view class="health-item">
<text class="health-label">健康状态</text>
<view class="health-status" :class="getHealthClass(animal.healthStatus)">
{{ getHealthText(animal.healthStatus) }}
</view>
</view>
<view class="health-item" v-if="animal.lastCheckDate">
<text class="health-label">最后检查</text>
<text class="health-value">{{ formatDate(animal.lastCheckDate) }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="animal-actions">
<button
class="action-btn primary"
@click.stop="handleHealthCheck(animal)"
>
健康检查
</button>
<button
class="action-btn"
@click.stop="handleFeedRecord(animal)"
>
喂养记录
</button>
<button
class="action-btn"
@click.stop="handleMoreActions(animal)"
>
更多
</button>
</view>
</view>
</Card>
</view>
<!-- 空状态 -->
<Empty
v-if="filteredLivestock.length === 0"
description="暂无牲畜数据"
image="🐄"
/>
<!-- 添加按钮 -->
<view class="add-btn" @click="handleAddAnimal">
<text class="add-icon">+</text>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore">
<button class="load-more-btn" @click="loadMore" :loading="loading">
{{ loading ? '加载中...' : '加载更多' }}
</button>
</view>
</view>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import Search from '@/common/components/search/search.vue'
import Tabs from '@/common/components/tabs/tabs.vue'
import Card from '@/common/components/card/card.vue'
import Empty from '@/common/components/empty/empty.vue'
import { farmingApi } from '@/common/api'
import { formatDate } from '@/common/utils/format'
export default {
name: 'LivestockPage',
components: {
Search,
Tabs,
Card,
Empty
},
setup() {
// 响应式数据
const searchKeyword = ref('')
const activeFilterIndex = ref(0)
const loading = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
// 筛选标签
const filterTabs = [
{ title: '全部', value: 'all' },
{ title: '健康', value: 'healthy' },
{ title: '生病', value: 'sick' },
{ title: '怀孕', value: 'pregnant' },
{ title: '幼崽', value: 'young' }
]
// 牲畜数据
const livestockData = ref([])
// 统计数据
const statsData = computed(() => [
{
key: 'total',
label: '总数量',
value: livestockData.value.length,
icon: '🐄',
type: 'primary'
},
{
key: 'healthy',
label: '健康',
value: livestockData.value.filter(item => item.healthStatus === 'healthy').length,
icon: '💚',
type: 'success'
},
{
key: 'sick',
label: '生病',
value: livestockData.value.filter(item => item.healthStatus === 'sick').length,
icon: '🤒',
type: 'danger'
},
{
key: 'pregnant',
label: '怀孕',
value: livestockData.value.filter(item => item.status === 'pregnant').length,
icon: '🤱',
type: 'warning'
}
])
// 过滤后的牲畜数据
const filteredLivestock = computed(() => {
let result = livestockData.value
// 关键词搜索
if (searchKeyword.value) {
result = result.filter(item =>
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.breed.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
// 状态筛选
const filterValue = filterTabs[activeFilterIndex.value].value
if (filterValue !== 'all') {
result = result.filter(item => {
switch (filterValue) {
case 'healthy':
return item.healthStatus === 'healthy'
case 'sick':
return item.healthStatus === 'sick'
case 'pregnant':
return item.status === 'pregnant'
case 'young':
return item.age < 12
default:
return true
}
})
}
return result
})
// 方法
const loadLivestockData = async () => {
try {
loading.value = true
const response = await farmingApi.getLivestockList({
page: currentPage.value,
pageSize: 20
})
if (currentPage.value === 1) {
livestockData.value = response.data.list
} else {
livestockData.value.push(...response.data.list)
}
hasMore.value = response.data.hasMore
} catch (error) {
console.error('加载牲畜数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
loading.value = false
}
}
const handleSearch = (keyword) => {
console.log('搜索:', keyword)
}
const handleSearchClear = () => {
searchKeyword.value = ''
}
const handleFilterChange = (index) => {
activeFilterIndex.value = index
}
const handleStatClick = (stat) => {
// 根据统计项筛选
const filterMap = {
total: 0,
healthy: 1,
sick: 2,
pregnant: 3
}
if (filterMap[stat.key] !== undefined) {
activeFilterIndex.value = filterMap[stat.key]
}
}
const handleAnimalClick = (animal) => {
uni.navigateTo({
url: `/pages/livestock/detail?id=${animal.id}`
})
}
const handleHealthCheck = (animal) => {
uni.navigateTo({
url: `/pages/health/check?animalId=${animal.id}`
})
}
const handleFeedRecord = (animal) => {
uni.navigateTo({
url: `/pages/feed/record?animalId=${animal.id}`
})
}
const handleMoreActions = (animal) => {
uni.showActionSheet({
itemList: ['编辑信息', '疫苗记录', '繁殖记录', '删除'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 编辑信息
uni.navigateTo({
url: `/pages/livestock/edit?id=${animal.id}`
})
break
case 1:
// 疫苗记录
uni.navigateTo({
url: `/pages/vaccine/record?animalId=${animal.id}`
})
break
case 2:
// 繁殖记录
uni.navigateTo({
url: `/pages/breeding/record?animalId=${animal.id}`
})
break
case 3:
// 删除
handleDeleteAnimal(animal)
break
}
}
})
}
const handleDeleteAnimal = (animal) => {
uni.showModal({
title: '确认删除',
content: `确定要删除牲畜 ${animal.id} 吗?`,
success: async (res) => {
if (res.confirm) {
try {
await farmingApi.deleteLivestock(animal.id)
uni.showToast({
title: '删除成功',
icon: 'success'
})
loadLivestockData()
} catch (error) {
uni.showToast({
title: '删除失败',
icon: 'error'
})
}
}
}
})
}
const handleAddAnimal = () => {
uni.navigateTo({
url: '/pages/livestock/add'
})
}
const loadMore = () => {
if (loading.value || !hasMore.value) return
currentPage.value++
loadLivestockData()
}
const getStatusClass = (status) => {
const classMap = {
normal: 'status-normal',
pregnant: 'status-pregnant',
sick: 'status-sick',
sold: 'status-sold'
}
return classMap[status] || 'status-normal'
}
const getStatusText = (status) => {
const textMap = {
normal: '正常',
pregnant: '怀孕',
sick: '生病',
sold: '已售'
}
return textMap[status] || '正常'
}
const getHealthClass = (healthStatus) => {
const classMap = {
healthy: 'health-good',
sick: 'health-bad',
warning: 'health-warning'
}
return classMap[healthStatus] || 'health-good'
}
const getHealthText = (healthStatus) => {
const textMap = {
healthy: '健康',
sick: '生病',
warning: '注意'
}
return textMap[healthStatus] || '健康'
}
// 生命周期
onMounted(() => {
loadLivestockData()
})
return {
searchKeyword,
activeFilterIndex,
loading,
hasMore,
filterTabs,
livestockData,
statsData,
filteredLivestock,
handleSearch,
handleSearchClear,
handleFilterChange,
handleStatClick,
handleAnimalClick,
handleHealthCheck,
handleFeedRecord,
handleMoreActions,
handleAddAnimal,
loadMore,
getStatusClass,
getStatusText,
getHealthClass,
getHealthText,
formatDate
}
}
}
</script>
<style lang="scss" scoped>
.livestock-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.search-section {
padding: 32rpx;
background-color: #fff;
border-bottom: 2rpx solid #f0f0f0;
}
.filter-section {
padding: 24rpx 32rpx;
background-color: #fff;
border-bottom: 2rpx solid #f0f0f0;
}
.stats-section {
padding: 32rpx;
background-color: #fff;
margin-bottom: 16rpx;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.stat-icon {
font-size: 32rpx;
}
.livestock-list {
padding: 0 32rpx;
}
.livestock-card {
margin-bottom: 24rpx;
}
.animal-info {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.animal-basic {
.animal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.animal-id {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.animal-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-normal {
background-color: #f6ffed;
color: #52c41a;
}
&.status-pregnant {
background-color: #fff7e6;
color: #faad14;
}
&.status-sick {
background-color: #fff2f0;
color: #ff4d4f;
}
&.status-sold {
background-color: #f0f0f0;
color: #999;
}
}
.animal-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12rpx;
}
.detail-item {
display: flex;
align-items: center;
.label {
font-size: 24rpx;
color: #666;
margin-right: 8rpx;
}
.value {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
}
}
.animal-health {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
.health-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.health-label {
font-size: 22rpx;
color: #666;
}
.health-status {
padding: 6rpx 12rpx;
border-radius: 16rpx;
font-size: 22rpx;
&.health-good {
background-color: #f6ffed;
color: #52c41a;
}
&.health-warning {
background-color: #fff7e6;
color: #faad14;
}
&.health-bad {
background-color: #fff2f0;
color: #ff4d4f;
}
}
.health-value {
font-size: 22rpx;
color: #333;
}
}
.animal-actions {
display: flex;
gap: 16rpx;
}
.action-btn {
flex: 1;
height: 64rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 24rpx;
color: #666;
&.primary {
border-color: #2e8b57;
background-color: #2e8b57;
color: #fff;
}
&:active {
opacity: 0.8;
}
}
.add-btn {
position: fixed;
right: 32rpx;
bottom: 32rpx;
width: 112rpx;
height: 112rpx;
background-color: #2e8b57;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 139, 87, 0.3);
z-index: 100;
.add-icon {
font-size: 48rpx;
color: #fff;
font-weight: bold;
}
&:active {
transform: scale(0.95);
}
}
.load-more {
padding: 32rpx;
text-align: center;
}
.load-more-btn {
width: 100%;
height: 80rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 28rpx;
color: #666;
&:active {
background-color: #f8f9fa;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

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