599 lines
16 KiB
Plaintext
599 lines
16 KiB
Plaintext
<template>
|
|
<cl-page>
|
|
<view class="home-page">
|
|
<!-- 顶部状态栏适配 -->
|
|
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
|
|
|
|
<!-- 顶部导航 -->
|
|
<view class="header">
|
|
<view class="header-left">
|
|
<text class="app-title">星球庄园</text>
|
|
<text class="app-subtitle">科技兴农 · 臻选未来</text>
|
|
</view>
|
|
<view class="header-right">
|
|
<view class="icon-btn" @click="goMessage">
|
|
<cl-icon name="notification-4-line" :size="44" color="#333" />
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 设备状态卡片 -->
|
|
<view class="status-card" @click="goDeviceDetail">
|
|
<view class="status-header">
|
|
<cl-icon name="dashboard-horizontal-line" :size="36" color="#52c41a" />
|
|
<text class="status-title">{{ hasDevice ? deviceName : '暂无设备' }}</text>
|
|
</view>
|
|
<view v-if="hasDevice" class="status-data">
|
|
<view class="data-item">
|
|
<text class="data-value">{{ deviceData.temperature != null ? deviceData.temperature : '--' }}°C</text>
|
|
<text class="data-label">温度</text>
|
|
</view>
|
|
<view class="data-item">
|
|
<text class="data-value">{{ deviceData.humidity != null ? deviceData.humidity : '--' }}%</text>
|
|
<text class="data-label">湿度</text>
|
|
</view>
|
|
<view class="data-item">
|
|
<text class="data-value">{{ deviceData.light != null ? deviceData.light : '--' }}</text>
|
|
<text class="data-label">光照</text>
|
|
</view>
|
|
</view>
|
|
<view v-else class="no-device">
|
|
<text class="no-device-text">点击右侧按钮添加设备</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="planet-section">
|
|
<view class="planet-container">
|
|
<!-- 改为精美静态图片展示,提升加载速度与稳定性 -->
|
|
<image class="planet-image" :src="planetSrc" @error="onPlanetError" mode="aspectFill" />
|
|
<view class="planet-info">
|
|
<text class="planet-status-text">{{ isRunning ? '核心园区运行中' : '园区系统待命中' }}</text>
|
|
<view class="status-dot" :style="{ backgroundColor: isRunning ? '#52c41a' : '#faad14', boxShadow: isRunning ? '0 0 10rpx rgba(82, 196, 26, 0.6)' : '0 0 10rpx rgba(250, 173, 20, 0.6)' }"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 成长进度 -->
|
|
<view class="growth-progress" v-if="currentCrop != null">
|
|
<view class="progress-header">
|
|
<text class="progress-title">{{ currentCrop.name }}</text>
|
|
<text class="progress-percent">{{ currentCrop != null && currentCrop!['progress'] != null ? currentCrop!['progress'] : 0 }}%</text>
|
|
</view>
|
|
<view class="progress-bar">
|
|
<view class="progress-fill" :style="{ width: (currentCrop.progress != null ? currentCrop.progress : 0) + '%' }"></view>
|
|
</view>
|
|
<view class="progress-stage">
|
|
<text class="stage-text">当前阶段: {{ currentCrop != null && currentCrop!['stageName'] != null ? currentCrop!['stageName'] : '播种期' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 底部功能区 - 重新设计的卡片 -->
|
|
<view class="bottom-modules" v-if="homeMenus.length > 0">
|
|
<view v-for="(item, index) in homeMenus" :key="index"
|
|
class="module-card"
|
|
@click="onMenuClick(item)">
|
|
<view class="module-content" :style="{ background: item.color }">
|
|
<image class="module-icon" :src="item.icon" mode="aspectFit" />
|
|
<view class="module-text-wrap">
|
|
<text class="module-title">{{ item.title }}</text>
|
|
<text class="module-subtitle">{{ item.subtitle }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 全屏可任意拖拽的浮动按钮 -->
|
|
<view
|
|
class="fab-btn"
|
|
:style="{ left: fabLeft + 'px', top: fabTop + 'px' }"
|
|
@touchstart="onTouchStart"
|
|
@touchmove.stop.prevent="onTouchMove"
|
|
@click="onFabClick"
|
|
>
|
|
<view class="fab-inner">
|
|
<cl-icon name="add-circle-line" :size="50" color="#fff" />
|
|
</view>
|
|
<text class="fab-label">添加设备</text>
|
|
</view>
|
|
|
|
<!-- 底部TabBar -->
|
|
<custom-tabbar :current="0" />
|
|
</cl-page>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { router, useCool, parseObject } from '@/cool';
|
|
import CustomTabbar from '@/components/tabbar.uvue';
|
|
import { remixicon } from '@/icons/remixicon';
|
|
|
|
const { service } = useCool();
|
|
|
|
const loading = ref(false);
|
|
const hasDevice = ref(false);
|
|
const deviceName = ref('');
|
|
const deviceData = ref<any>({});
|
|
const currentCrop = ref<any>(null);
|
|
const homeMenus = ref<any[]>([]);
|
|
|
|
// 图片资源
|
|
const planetSrc = ref('/static/images/planet.png');
|
|
|
|
function onPlanetError() {
|
|
// 图片加载失败,使用兜底图或远程图
|
|
planetSrc.value = 'https://mp-bbfe5648-527e-4680-928d-c78233f268b8.cdn.bspapp.com/cloudstorage/planet-fallback.png';
|
|
}
|
|
|
|
// 系统信息
|
|
const statusBarHeight = ref(uni.getWindowInfo().statusBarHeight);
|
|
const isRunning = ref(true);
|
|
|
|
// 悬浮按钮位置
|
|
const fabLeft = ref(uni.getWindowInfo().windowWidth - 80);
|
|
const fabTop = ref(uni.getWindowInfo().windowHeight - 240);
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let lastLeft = 0;
|
|
let lastTop = 0;
|
|
|
|
import { auth } from '@/utils/auth';
|
|
import { request } from '@/utils/request';
|
|
|
|
function onTouchStart(e: TouchEvent) {
|
|
startX = e.touches[0].clientX;
|
|
startY = e.touches[0].clientY;
|
|
lastLeft = fabLeft.value;
|
|
lastTop = fabTop.value;
|
|
}
|
|
|
|
function onTouchMove(e: TouchEvent) {
|
|
const moveX = e.touches[0].clientX - startX;
|
|
const moveY = e.touches[0].clientY - startY;
|
|
|
|
let left = lastLeft + moveX;
|
|
let top = lastTop + moveY;
|
|
|
|
// 边界限制
|
|
const sysInfo = uni.getWindowInfo();
|
|
if (left < 10) left = 10;
|
|
if (left > sysInfo.windowWidth - 70) left = sysInfo.windowWidth - 70;
|
|
if (top < 100) top = 100;
|
|
if (top > sysInfo.windowHeight - 150) top = sysInfo.windowHeight - 150;
|
|
|
|
fabLeft.value = left;
|
|
fabTop.value = top;
|
|
}
|
|
|
|
function onFabClick() {
|
|
if (!auth.isLogin.value) {
|
|
uni.showModal({
|
|
title: '提示',
|
|
content: '添加设备需先登录,是否前往登录?',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
router.push({ path: '/pages/user/login' });
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
router.push({ path: '/pages/device/add' });
|
|
}
|
|
|
|
// 获取数据
|
|
async function loadData() {
|
|
if (loading.value) return;
|
|
loading.value = true;
|
|
|
|
// 1. 默认静态菜单
|
|
homeMenus.value = [
|
|
{ title: '云农场', subtitle: '认养作物', icon: '/static/images/farm_icon.png', path: '/pages/seed/store', color: 'linear-gradient(135deg, #52c41a, #95de64)' },
|
|
{ title: '今日菜园', subtitle: '新鲜直达', icon: '/static/images/shop_icon.png', path: '/pages/mall/index', color: 'linear-gradient(135deg, #40a9ff, #91d5ff)' }
|
|
];
|
|
|
|
try {
|
|
// 并行请求所有数据
|
|
const [menuRes, deviceRes, cropRes] = await Promise.allSettled([
|
|
request({ url: '/api/nongchuang/app-menu/list-by-type', data: { type: 'home' } }),
|
|
request({ url: '/api/nongchuang/device/list' }),
|
|
request({ url: '/api/nongchuang/user-adoption/page', data: { page: 1, size: 1, status: 1 } })
|
|
]);
|
|
|
|
// 2. 处理动态菜单配置
|
|
if (menuRes.status === 'fulfilled') {
|
|
try {
|
|
const menus = menuRes.value as any[];
|
|
if (menus != null && menus.length > 0) {
|
|
const fixedRes = menus.map((item : any) : any => {
|
|
let icon = item.icon as string || "";
|
|
if (icon == "leaf" || icon == "plant-line" || icon == "landscape-line" || icon == "image-line" || icon == "farm" || item.title == "云农场") {
|
|
icon = "/static/images/farm_icon.png";
|
|
}
|
|
if (icon == "basket" || icon == "shop" || icon == "shopping-basket" || icon == "shopping-cart" || icon == "shopping-cart-2-line" || icon == "shopping-cart-line" || icon == "store-2-line" || item.title == "今日菜园") {
|
|
icon = "/static/images/shop_icon.png";
|
|
}
|
|
if (!isUrl(icon)) {
|
|
// @ts-ignore
|
|
if (remixicon[icon] == null) {
|
|
icon = "message-3-line";
|
|
}
|
|
}
|
|
item.icon = icon;
|
|
let path = item.path as string || "";
|
|
if (path == "" || path == "nongchuang/product/page") path = "/pages/index/home";
|
|
if (path != "" && !path.startsWith("/")) path = "/" + path;
|
|
item.path = path;
|
|
return item;
|
|
});
|
|
homeMenus.value = fixedRes;
|
|
}
|
|
} catch (e) {
|
|
console.log('处理动态菜单失败');
|
|
}
|
|
}
|
|
|
|
// 3. 处理设备信息
|
|
if (deviceRes.status === 'fulfilled') {
|
|
try {
|
|
const devices = deviceRes.value as any[];
|
|
if (devices != null && devices.length > 0) {
|
|
const dev = devices[0];
|
|
hasDevice.value = true;
|
|
deviceName.value = dev.name;
|
|
deviceData.value = {
|
|
temperature: dev.temperature,
|
|
humidity: dev.humidity,
|
|
light: dev.light
|
|
};
|
|
isRunning.value = devices.some(d => d.status == 1);
|
|
} else {
|
|
isRunning.value = false;
|
|
}
|
|
} catch (e) {
|
|
console.log('处理设备数据失败');
|
|
}
|
|
}
|
|
|
|
// 4. 处理当前认养作物
|
|
if (cropRes.status === 'fulfilled') {
|
|
try {
|
|
const crop = cropRes.value as any;
|
|
if (crop != null && crop.list != null && crop.list.length > 0) {
|
|
const item = crop.list[0];
|
|
currentCrop.value = {
|
|
name: item.projectName ?? '我的作物',
|
|
image: item.projectImage,
|
|
progress: item.progress ?? 0,
|
|
stageName: (item.progress ?? 0) >= 100 ? '已成熟' : '成长期'
|
|
};
|
|
}
|
|
} catch (e) {
|
|
console.log('处理认养数据失败');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('加载总体数据失败', e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function goMessage() {
|
|
router.push({ path: '/pages/index/template' });
|
|
}
|
|
|
|
function goDeviceDetail() {
|
|
uni.switchTab({ url: '/pages/index/device' });
|
|
}
|
|
|
|
function onMenuClick(item : any) {
|
|
if (item.path != null) {
|
|
let path = item.path as string;
|
|
if (!path.startsWith('/')) path = '/' + path;
|
|
router.push({ path: path });
|
|
}
|
|
}
|
|
|
|
function isUrl(str: string | null): boolean {
|
|
if (str == null) return false;
|
|
return str.startsWith('http') || str.startsWith('/') || str.startsWith('wxfile');
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData();
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.home-page {
|
|
flex: 1;
|
|
background-color: #f8f9fa;
|
|
padding-bottom: 60rpx;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx 10rpx;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.app-title {
|
|
font-size: 38rpx;
|
|
font-weight: bold;
|
|
color: #2e3a23;
|
|
}
|
|
|
|
.app-subtitle {
|
|
font-size: 22rpx;
|
|
color: #7d8c6d;
|
|
margin-top: 4rpx;
|
|
}
|
|
|
|
.icon-btn {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
background-color: #fff;
|
|
border-radius: 36rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.status-card {
|
|
margin: 20rpx 30rpx;
|
|
padding: 24rpx;
|
|
background-color: #fff;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
.status-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.status-title {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-left: 12rpx;
|
|
}
|
|
|
|
.status-data {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-around;
|
|
margin-top: 10rpx;
|
|
padding: 10rpx 0;
|
|
}
|
|
|
|
.data-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.data-value {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #52c41a;
|
|
}
|
|
|
|
.data-label {
|
|
font-size: 20rpx;
|
|
color: #999;
|
|
margin-top: 4rpx;
|
|
}
|
|
|
|
.no-device {
|
|
padding: 20rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.no-device-text {
|
|
font-size: 24rpx;
|
|
color: #bbb;
|
|
}
|
|
|
|
.planet-section {
|
|
padding: 10rpx 30rpx;
|
|
}
|
|
|
|
.planet-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 520rpx;
|
|
background-color: #fff;
|
|
border-radius: 32rpx;
|
|
overflow: hidden;
|
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.planet-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.planet-info {
|
|
position: absolute;
|
|
top: 30rpx;
|
|
left: 30rpx;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
padding: 10rpx 20rpx;
|
|
border-radius: 30rpx;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.planet-status-text {
|
|
font-size: 20rpx;
|
|
color: #52c41a;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 12rpx;
|
|
height: 12rpx;
|
|
background-color: #52c41a;
|
|
border-radius: 6rpx;
|
|
margin-left: 10rpx;
|
|
box-shadow: 0 0 10rpx rgba(82, 196, 26, 0.6);
|
|
}
|
|
|
|
.growth-progress {
|
|
background-color: rgba(255, 255, 255, 0.95);
|
|
border-radius: 20rpx;
|
|
padding: 24rpx;
|
|
margin-top: -40rpx;
|
|
margin-left: 30rpx;
|
|
margin-right: 30rpx;
|
|
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.08);
|
|
z-index: 100;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.progress-title {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.progress-percent {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #52c41a;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 16rpx;
|
|
background-color: #f0f2f5;
|
|
border-radius: 8rpx;
|
|
margin-top: 16rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #52c41a, #95de64);
|
|
border-radius: 8rpx;
|
|
}
|
|
|
|
.progress-stage {
|
|
margin-top: 12rpx;
|
|
}
|
|
|
|
.stage-text {
|
|
font-size: 22rpx;
|
|
color: #888;
|
|
}
|
|
|
|
.bottom-modules {
|
|
display: flex;
|
|
flex-direction: row;
|
|
padding: 40rpx 30rpx;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.module-card {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-right: 30rpx;
|
|
}
|
|
|
|
.module-card:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.module-content {
|
|
width: 100%;
|
|
height: 160rpx;
|
|
border-radius: 24rpx;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 0 30rpx;
|
|
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.module-icon {
|
|
width: 90rpx;
|
|
height: 90rpx;
|
|
}
|
|
|
|
.module-text-wrap {
|
|
margin-left: 20rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.module-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
|
|
.module-subtitle {
|
|
font-size: 20rpx;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
margin-top: 4rpx;
|
|
}
|
|
|
|
/* 悬浮按钮样式 */
|
|
.fab-btn {
|
|
position: fixed;
|
|
z-index: 999;
|
|
width: 120rpx;
|
|
height: 160rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.fab-inner {
|
|
width: 100rpx;
|
|
height: 100rpx;
|
|
background: linear-gradient(135deg, #52c41a, #73d13d);
|
|
border-radius: 50rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.4);
|
|
border: 4rpx solid #fff;
|
|
}
|
|
|
|
.fab-label {
|
|
margin-top: 12rpx;
|
|
font-size: 20rpx;
|
|
color: #52c41a;
|
|
font-weight: bold;
|
|
background-color: rgba(255, 255, 255, 0.9);
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.05);
|
|
}
|
|
</style>
|