Files
WAI_Project_UNIX/pages/index/home.uvue
2026-01-21 01:37:34 +08:00

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>