refactor: 重构数据库配置为SQLite开发环境并移除冗余文档
This commit is contained in:
284
mini_program/farming-manager/App.vue
Normal file
284
mini_program/farming-manager/App.vue
Normal 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>
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
115
mini_program/farming-manager/main.js
Normal file
115
mini_program/farming-manager/main.js
Normal 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
|
||||
}
|
||||
}
|
||||
116
mini_program/farming-manager/manifest.json
Normal file
116
mini_program/farming-manager/manifest.json
Normal 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"
|
||||
}
|
||||
226
mini_program/farming-manager/pages.json
Normal file
226
mini_program/farming-manager/pages.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
345
mini_program/farming-manager/pages/animal/list.js
Normal file
345
mini_program/farming-manager/pages/animal/list.js
Normal 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`;
|
||||
}
|
||||
});
|
||||
994
mini_program/farming-manager/pages/animal/list.vue
Normal file
994
mini_program/farming-manager/pages/animal/list.vue
Normal 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>
|
||||
165
mini_program/farming-manager/pages/animal/list.wxml
Normal file
165
mini_program/farming-manager/pages/animal/list.wxml
Normal 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>
|
||||
401
mini_program/farming-manager/pages/animal/list.wxss
Normal file
401
mini_program/farming-manager/pages/animal/list.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
136
mini_program/farming-manager/pages/auth/login.js
Normal file
136
mini_program/farming-manager/pages/auth/login.js
Normal 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'
|
||||
});
|
||||
}
|
||||
});
|
||||
593
mini_program/farming-manager/pages/auth/login.vue
Normal file
593
mini_program/farming-manager/pages/auth/login.vue
Normal 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>
|
||||
87
mini_program/farming-manager/pages/auth/login.wxml
Normal file
87
mini_program/farming-manager/pages/auth/login.wxml
Normal 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>
|
||||
268
mini_program/farming-manager/pages/auth/login.wxss
Normal file
268
mini_program/farming-manager/pages/auth/login.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
253
mini_program/farming-manager/pages/farm/detail.js
Normal file
253
mini_program/farming-manager/pages/farm/detail.js
Normal 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);
|
||||
}
|
||||
});
|
||||
228
mini_program/farming-manager/pages/farm/detail.wxml
Normal file
228
mini_program/farming-manager/pages/farm/detail.wxml
Normal 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>
|
||||
599
mini_program/farming-manager/pages/farm/detail.wxss
Normal file
599
mini_program/farming-manager/pages/farm/detail.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
240
mini_program/farming-manager/pages/farm/list.js
Normal file
240
mini_program/farming-manager/pages/farm/list.js
Normal 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();
|
||||
}
|
||||
});
|
||||
726
mini_program/farming-manager/pages/farm/list.vue
Normal file
726
mini_program/farming-manager/pages/farm/list.vue
Normal 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>
|
||||
147
mini_program/farming-manager/pages/farm/list.wxml
Normal file
147
mini_program/farming-manager/pages/farm/list.wxml
Normal 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>
|
||||
365
mini_program/farming-manager/pages/farm/list.wxss
Normal file
365
mini_program/farming-manager/pages/farm/list.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
211
mini_program/farming-manager/pages/index/index.js
Normal file
211
mini_program/farming-manager/pages/index/index.js
Normal 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}`
|
||||
});
|
||||
}
|
||||
});
|
||||
589
mini_program/farming-manager/pages/index/index.vue
Normal file
589
mini_program/farming-manager/pages/index/index.vue
Normal 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>
|
||||
96
mini_program/farming-manager/pages/index/index.wxml
Normal file
96
mini_program/farming-manager/pages/index/index.wxml
Normal 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>
|
||||
318
mini_program/farming-manager/pages/index/index.wxss
Normal file
318
mini_program/farming-manager/pages/index/index.wxss
Normal 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;
|
||||
}
|
||||
}
|
||||
685
mini_program/farming-manager/pages/livestock/livestock.vue
Normal file
685
mini_program/farming-manager/pages/livestock/livestock.vue
Normal 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>
|
||||
1395
mini_program/farming-manager/pages/report/dashboard.vue
Normal file
1395
mini_program/farming-manager/pages/report/dashboard.vue
Normal file
File diff suppressed because it is too large
Load Diff
7
mini_program/farming-manager/sitemap.json
Normal file
7
mini_program/farming-manager/sitemap.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user