更新模板

This commit is contained in:
2026-01-21 01:37:34 +08:00
parent b7be8c51bf
commit c5c73828bd
83 changed files with 8687 additions and 1235 deletions

260
pages/index/device.uvue Normal file
View File

@@ -0,0 +1,260 @@
<template>
<cl-page>
<view class="device-page">
<!-- 顶部状态栏适配 -->
<view :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<!-- 顶部导航 -->
<view class="header">
<text class="page-title">我的设备</text>
<view class="add-btn" @click="addDevice">
<cl-icon name="add-line" :size="36" color="#52c41a" />
</view>
</view>
<!-- 设备状态汇总 -->
<view class="status-summary">
<view class="status-item">
<text class="count-online">{{ onlineCount }}</text>
<text class="status-label">在线</text>
</view>
<view class="status-item">
<text class="count-offline">{{ offlineCount }}</text>
<text class="status-label">离线</text>
</view>
<view class="status-item">
<text class="count-warning">{{ warningCount }}</text>
<text class="status-label">告警</text>
</view>
</view>
<!-- 设备列表 -->
<scroll-view scroll-y class="device-list">
<view v-for="device in deviceList" :key="device.id" class="device-card" @click="goDeviceDetail(device)">
<view class="device-header">
<view class="status-dot" :class="getStatusClass(device.status)"></view>
<text class="device-name">{{ device.name }}</text>
<text class="device-no">{{ device.deviceNo }}</text>
</view>
<view class="device-data">
<view class="data-item">
<cl-icon name="sun-line" :size="32" color="#ff4d4f" />
<text class="data-value">{{ device.temperature || '--' }}°C</text>
</view>
<view class="data-item">
<cl-icon name="cloud-line" :size="32" color="#1890ff" />
<text class="data-value">{{ device.humidity || '--' }}%</text>
</view>
<view class="data-item">
<cl-icon name="earth-line" :size="32" color="#52c41a" />
<text class="data-value">{{ device.soilMoisture || '--' }}%</text>
</view>
</view>
</view>
<cl-empty v-if="deviceList.length === 0" text="暂无设备">
<cl-button type="primary" size="small" round @click="addDevice">添加设备</cl-button>
</cl-empty>
</scroll-view>
</view>
<!-- 底部TabBar -->
<custom-tabbar :current="1" />
</cl-page>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue';
import { router, useCool } from '@/cool';
import CustomTabbar from '@/components/tabbar.uvue';
const { service } = useCool();
const statusBarHeight = ref(uni.getWindowInfo().statusBarHeight);
const deviceList = ref<any[]>([]);
const onlineCount = computed(() => deviceList.value.filter(d => d.status === 1).length);
const offlineCount = computed(() => deviceList.value.filter(d => d.status === 0).length);
const warningCount = computed(() => deviceList.value.filter(d => d.status === 2).length);
function getStatusClass(status: number) {
if (status === 1) return 'online';
if (status === 0) return 'offline';
return 'warning';
}
async function loadDevices() {
try {
const res = await service.nongchuang.device.list();
if (res != null) {
// 后端直接返回数组,不需要解析
if (Array.isArray(res)) {
deviceList.value = res;
} else if (res.list != null) {
deviceList.value = res.list as any[];
} else {
deviceList.value = [];
}
}
} catch (e) {
console.error('加载设备失败', e);
deviceList.value = [];
}
}
function goDeviceDetail(device: any) {
router.push({ path: '/pages/device/detail', query: { id: device.id } });
}
function addDevice() {
router.push('/pages/device/add');
}
onMounted(() => {
loadDevices();
});
</script>
<style lang="scss" scoped>
.device-page {
flex: 1;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 60rpx 30rpx 30rpx;
background-color: #fff;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.add-btn {
width: 70rpx;
height: 70rpx;
background-color: #f6ffed;
border-radius: 35rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-summary {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 30rpx;
background-color: #fff;
margin-bottom: 20rpx;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
}
.count-online {
font-size: 48rpx;
font-weight: bold;
color: #52c41a;
}
.count-offline {
font-size: 48rpx;
font-weight: bold;
color: #8c8c8c;
}
.count-warning {
font-size: 48rpx;
font-weight: bold;
color: #faad14;
}
.status-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.device-list {
flex: 1;
padding: 0 20rpx;
}
.device-card {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
}
.device-header {
display: flex;
flex-direction: row;
align-items: center;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 8rpx;
margin-right: 12rpx;
}
.status-dot.online {
background-color: #52c41a;
}
.status-dot.offline {
background-color: #8c8c8c;
}
.status-dot.warning {
background-color: #faad14;
}
.device-name {
flex: 1;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.device-no {
font-size: 24rpx;
color: #999;
}
.device-data {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-top: 20rpx;
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
}
.data-item {
display: flex;
flex-direction: row;
align-items: center;
}
.data-value {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-left: 8rpx;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,276 +1,415 @@
<template>
<cl-page>
<cl-topbar
fixed
:height="100"
:show-back="false"
safe-area-top
background-color="transparent"
>
<view class="flex flex-row items-center w-full flex-1 px-3">
<view class="top-icon dark:!bg-surface-700" @tap="toSet">
<cl-icon name="settings-line"></cl-icon>
</view>
<view class="my-page">
<!-- 顶部状态栏适配 -->
<view class="status-bar-bg" :style="{ height: (statusBarHeight + 10) + 'px' }"></view>
<view class="top-icon dark:!bg-surface-700" @tap="toTest">
<cl-icon name="notification-4-line"></cl-icon>
<!-- 用户信息头部 -->
<view class="user-header">
<view class="user-info" @click="goLogin">
<image class="avatar" :src="userInfo.getString('avatar') != null ? userInfo.getString('avatar') : '/static/images/avatar.png'" mode="aspectFill" @error="userInfo['avatar'] = '/static/images/avatar.png'" />
<view class="info">
<text class="nickname">{{ userInfo.getString('nickname') != null ? userInfo.getString('nickname') : '点击登录' }}</text>
<view class="id-wrapper" v-if="userInfo.get('id') != null">
<text class="id-label">ID:</text>
<text class="id-value">{{ userInfo.get('id') }}</text>
</view>
<text class="phone" v-else-if="userInfo.getString('phone') != null">{{ maskPhone(userInfo.getString('phone') as string) }}</text>
<text class="phone" v-else-if="userInfo.getString('mobile') != null">{{ maskPhone(userInfo.getString('mobile') as string) }}</text>
</view>
</view>
<view class="edit-btn" @click="goEdit">
<cl-icon name="edit-box-line" :size="36" color="#fff" />
</view>
</view>
</cl-topbar>
<view class="p-3">
<view class="flex flex-col justify-center items-center pt-6 pb-3">
<view class="relative overflow-visible" @tap="toEdit">
<cl-avatar
:src="userInfo?.avatarUrl"
:size="150"
:pt="{ className: '!rounded-3xl', icon: { size: 60 } }"
>
</cl-avatar>
<view
class="flex flex-col justify-center items-center absolute bottom-0 right-[-6rpx] bg-black rounded-full p-1"
v-if="!user.isNull()"
>
<cl-icon name="edit-line" color="white" :size="24"></cl-icon>
</view>
<!-- 数据统计 -->
<view class="stats-section">
<view class="stat-item" @click="goMySeeds">
<text class="stat-value">{{ stats.seedCount != null ? stats.seedCount : 0 }}</text>
<text class="stat-label">我的种子</text>
</view>
<view class="flex-1 flex flex-col justify-center items-center w-full" @tap="toEdit">
<cl-text :pt="{ className: '!text-xl mt-5 mb-1 font-bold' }">{{
userInfo?.nickName ?? t("未登录")
}}</cl-text>
<cl-text color="info" v-if="!user.isNull()">{{ userInfo?.phone }}</cl-text>
<view class="stat-item" @click="goMyAdoption">
<text class="stat-value">{{ stats.adoptionCount != null ? stats.adoptionCount : 0 }}</text>
<text class="stat-label">认养项目</text>
</view>
<view class="stat-item" @click="goOrders">
<text class="stat-value">{{ stats.orderCount != null ? stats.orderCount : 0 }}</text>
<text class="stat-label">订单</text>
</view>
</view>
<cl-row
:pt="{
className: 'pt-3 pb-6'
}"
>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="171"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("总点击")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="24"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("赞")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="89"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("收藏")
}}</cl-text>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col items-center justify-center">
<cl-rolling-number
:pt="{ className: '!text-xl' }"
:value="653"
></cl-rolling-number>
<cl-text :pt="{ className: 'mt-1 !text-xs' }" color="info">{{
t("粉丝")
}}</cl-text>
</view>
</cl-col>
</cl-row>
<cl-row :gutter="20" :pt="{ className: 'mb-3' }">
<cl-col :span="12">
<view class="bg-white dark:!bg-surface-800 p-4 rounded-2xl flex flex-row">
<view class="flex flex-col mr-auto">
<cl-text
ellipsis
:pt="{
className: '!w-[180rpx]'
}"
>{{ t("接单模式") }}</cl-text
>
<cl-text :pt="{ className: '!text-xs mt-1' }" color="info">{{
t("已关闭")
}}</cl-text>
<!-- 功能菜单 -->
<view class="menu-section" v-if="myMenus.length > 0">
<view v-for="(item, index) in myMenus" :key="index" class="menu-item" @click="onMenuClick(item)">
<view class="menu-left">
<cl-image v-if="isUrl(item.icon)" :src="item.icon" :size="40" />
<cl-icon v-else :name="item.icon" :size="40" :color="item.color != null ? item.color : '#52c41a'" />
<text class="menu-label">{{ item.label }}</text>
</view>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<cl-switch></cl-switch>
</view>
</cl-col>
<cl-col :span="12">
<view class="bg-white dark:!bg-surface-800 p-4 rounded-2xl flex flex-row">
<view class="flex flex-col mr-auto">
<cl-text
ellipsis
:pt="{
className: '!w-[180rpx]'
}"
>{{ t("消息通知") }}</cl-text
>
<cl-text :pt="{ className: '!text-xs mt-1' }" color="info">{{
t("已关闭")
}}</cl-text>
</view>
<cl-switch></cl-switch>
</view>
</cl-col>
</cl-row>
<view class="bg-white dark:!bg-surface-800 py-5 rounded-2xl mb-3 h-[160rpx]">
<cl-row :pt="{ className: 'overflow-visible' }">
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="money-cny-circle-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("待支付") }}</cl-text
>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="box-1-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("未发货") }}</cl-text
>
</view>
</cl-col>
<cl-col :span="6">
<view
class="flex flex-col justify-center items-center relative overflow-visible px-2"
>
<cl-icon name="flight-takeoff-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("已发货") }}</cl-text
>
<cl-badge
type="primary"
:value="3"
position
:pt="{ className: '!right-6' }"
></cl-badge>
</view>
</cl-col>
<cl-col :span="6">
<view class="flex flex-col justify-center items-center px-2">
<cl-icon name="exchange-cny-line" :size="46"></cl-icon>
<cl-text
:pt="{ className: '!text-xs mt-2 text-center' }"
color="info"
>{{ t("售后 / 退款") }}</cl-text
>
</view>
</cl-col>
</cl-row>
</view>
<cl-list :pt="{ className: 'mb-3' }">
<cl-list-item
:label="t('我的钱包')"
icon="wallet-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('数据看板')"
icon="pie-chart-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('历史记录')"
icon="history-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
<cl-list-item
:label="t('邀请好友')"
icon="share-line"
arrow
hoverable
@tap="toTest"
>
</cl-list-item>
</cl-list>
<cl-list>
<cl-list-item :label="t('设置')" icon="settings-line" arrow hoverable @tap="toSet">
</cl-list-item>
</cl-list>
<view class="menu-section" v-else>
<view class="menu-item" @click="goMySeeds">
<cl-icon name="landscape-line" :size="44" color="#52c41a" />
<text class="menu-title">我的种子</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<view class="menu-item" @click="goMyAdoption">
<cl-icon name="heart-line" :size="44" color="#ff7a45" />
<text class="menu-title">我的认养</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<view class="menu-item" @click="goOrders">
<cl-icon name="file-list-line" :size="44" color="#1890ff" />
<text class="menu-title">我的订单</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<view class="menu-item" @click="goGiftCard">
<cl-icon name="gift-line" :size="44" color="#faad14" />
<text class="menu-title">礼品卡兑换</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<view class="menu-item" @click="goAddress">
<cl-icon name="map-pin-2-line" :size="44" color="#722ed1" />
<text class="menu-title">收货地址</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
<view class="menu-item" @click="goSettings">
<cl-icon name="settings-line" :size="44" color="#666" />
<text class="menu-title">设置</text>
<cl-icon name="arrow-right-s-line" :size="32" color="#ccc" />
</view>
</view>
<!-- 客服与帮助 -->
<view class="help-section">
<view class="help-item" @click="goHelp">
<cl-icon name="question-line" :size="36" color="#999" />
<text class="help-text">帮助中心</text>
</view>
<view class="help-divider"></view>
<view class="help-item" @click="callService">
<cl-icon name="phone-line" :size="36" color="#999" />
<text class="help-text">联系客服</text>
</view>
</view>
</view>
<!-- 自定义底部导航栏 -->
<custom-tabbar></custom-tabbar>
<!-- 底部TabBar -->
<custom-tabbar :current="3" />
</cl-page>
</template>
<script setup lang="ts">
import { router, userInfo, useStore } from "@/cool";
import { t } from "@/locale";
import { useUi } from "@/uni_modules/cool-ui";
import CustomTabbar from "@/components/tabbar.uvue";
<script setup lang="uts">
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { router, useCool, parseObject } from '@/cool';
import CustomTabbar from '@/components/tabbar.uvue';
const { user } = useStore();
const ui = useUi();
const { service } = useCool();
function toTest() {
ui.showToast({
message: t("开发中,敬请期待")
});
const statusBarHeight = ref(uni.getWindowInfo().statusBarHeight);
const loading = ref(false);
const userInfo = ref<any>({});
const stats = ref<any>({
seedCount: 0,
adoptionCount: 0,
orderCount: 0
});
const myMenus = ref<any[]>([]);
import { auth } from '@/utils/auth';
import { request } from '@/utils/request';
async function loadData() {
if (!auth.isLogin.value) {
userInfo.value = {};
stats.value = { seedCount: 0, adoptionCount: 0, orderCount: 0 };
return;
}
if (loading.value) return;
loading.value = true;
try {
const res = await request({
url: "/api/nongchuang/user/info"
});
if (res != null) {
const data = res as any;
userInfo.value = data.userInfo;
const s = data.stats;
if (s != null) {
stats.value = s;
}
}
} catch (e) {
console.error('获取个人信息失败', e);
} finally {
loading.value = false;
}
}
function toSet() {
router.to("/pages/set/index");
function maskPhone(phone: string) {
if (phone.length >= 11) {
return phone.substring(0, 3) + '****' + phone.substring(7);
}
return phone;
}
function toEdit() {
router.to("/pages/user/edit");
function goLogin() {
if (!auth.isLogin.value) {
router.push({ path: '/pages/user/login' });
}
}
onReady(() => {
user.get();
// 权限检查装饰器/辅助函数
function checkAuthAndRun(fn: Function) {
if (!auth.isLogin.value) {
uni.showModal({
title: '提示',
content: '该功能需要登录后使用,是否现在前往登录?',
success: (res) => {
if (res.confirm) {
router.push({ path: '/pages/user/login' });
}
}
});
return;
}
fn();
}
function goEdit() {
checkAuthAndRun(() => router.push({ path: '/pages/user/edit' }));
}
function goMySeeds() {
checkAuthAndRun(() => router.push({ path: '/pages/seed/my-seeds' }));
}
function goMyAdoption() {
checkAuthAndRun(() => router.push({ path: '/pages/adoption/my' }));
}
function goOrders() {
checkAuthAndRun(() => router.push({ path: '/pages/order/list' }));
}
function goGiftCard() {
checkAuthAndRun(() => router.push({ path: '/pages/adoption/gift-card' }));
}
function goAddress() {
checkAuthAndRun(() => router.push({ path: '/pages/template/shop/address' }));
}
function goSettings() {
router.push({ path: '/pages/set/index' });
}
function onHelpClick(item: any) {
uni.showToast({ title: item.label + '功能开发中', icon: 'none' });
}
function isUrl(str: string | null): boolean {
if (str == null) return false;
return str.startsWith('http') || str.startsWith('/') || str.startsWith('wxfile');
}
function goHelp() {
uni.showToast({ title: '帮助中心开发中', icon: 'none' });
}
function callService() {
uni.makePhoneCall({ phoneNumber: '400-123-4567' });
}
function onMenuClick(item : any) {
if (item.path != null) {
if (item.isAuth) {
checkAuthAndRun(() => router.push({ path: item.path }));
} else {
router.push({ path: item.path });
}
}
}
onMounted(() => {
loadData();
});
onShow(() => {
loadData();
});
</script>
<style lang="scss" scoped>
.top-icon {
@apply flex items-center justify-center rounded-lg bg-white mr-3 p-2;
.my-page {
flex: 1;
background-color: #f8f9fa;
padding-bottom: 60rpx;
}
.user-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 40rpx 100rpx;
background: linear-gradient(135deg, #52c41a, #95de64);
}
.status-bar-bg {
background: linear-gradient(135deg, #52c41a, #95de64);
}
.user-info {
display: flex;
flex-direction: row;
align-items: center;
}
.avatar {
width: 110rpx;
height: 110rpx;
border-radius: 55rpx;
border-width: 4rpx;
border-style: solid;
border-color: rgba(255, 255, 255, 0.4);
}
.info {
display: flex;
flex-direction: column;
margin-left: 24rpx;
}
.nickname {
font-size: 34rpx;
font-weight: bold;
color: #fff;
}
.id-wrapper {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 8rpx;
background-color: rgba(255, 255, 255, 0.2);
padding: 4rpx 12rpx;
border-radius: 20rpx;
}
.id-label, .id-value {
font-size: 20rpx;
color: #fff;
}
.id-value {
margin-left: 6rpx;
}
.phone {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 4rpx;
}
.edit-btn {
width: 64rpx;
height: 64rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.stats-section {
display: flex;
flex-direction: row;
background-color: #fff;
margin: -60rpx 30rpx 20rpx;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 22rpx;
color: #666;
margin-top: 4rpx;
}
.menu-section {
background-color: #fff;
margin: 0 30rpx 20rpx;
border-radius: 20rpx;
overflow: hidden;
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx 30rpx;
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #f5f5f5;
}
.menu-item:last-child {
border-bottom-width: 0;
}
.menu-title {
flex: 1;
font-size: 28rpx;
color: #333;
margin-left: 20rpx;
}
.help-section {
display: flex;
flex-direction: row;
background-color: #fff;
margin: 0 30rpx;
border-radius: 20rpx;
}
.help-item {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 24rpx;
}
.help-divider {
width: 1rpx;
background-color: #eee;
}
.help-text {
font-size: 24rpx;
color: #666;
margin-left: 12rpx;
}
</style>

363
pages/index/shop.uvue Normal file
View File

@@ -0,0 +1,363 @@
<template>
<cl-page>
<view class="shop-page">
<!-- 自定义顶部导航栏 (Fixed Top) -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="page-title">农场店</text>
</view>
</view>
<!-- 顶部占位 (Status + NavHeight 44) -->
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 搜索栏与购物车 (Flows normally below navbar) -->
<view class="search-row">
<view class="search-bar" @click="goSearch">
<cl-icon name="search-line" :size="32" color="#999" />
<text class="search-placeholder">搜索农特产品</text>
</view>
<view class="cart-btn-wrap" @click="goCart">
<view class="cart-btn">
<cl-icon name="shopping-cart-line" :size="40" color="#333" />
</view>
<view class="badge" v-if="cartCount > 0">
<text class="badge-text">{{ cartCount }}</text>
</view>
</view>
</view>
<!-- 分类导航 -->
<scroll-view scroll-x class="category-scroll" :show-scrollbar="false">
<view class="category-list">
<view
v-for="cat in categories"
:key="cat.id"
class="category-item"
:class="{ active: currentCategory === cat.id }"
@click="selectCategory(cat.id)"
>
<text class="category-text" :class="{ 'active-text': currentCategory === cat.id }">{{ cat.name }}</text>
</view>
</view>
</scroll-view>
<!-- 商品列表 -->
<scroll-view scroll-y class="product-scroll" @scrolltolower="loadMore">
<view class="product-list">
<view class="product-row">
<view
v-for="(product, index) in productList"
:key="product.id"
class="product-card"
@click="goProductDetail(product.id)"
>
<image class="product-image" :src="product.mainImage || '/static/images/product.png'" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-origin">{{ product.origin }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<view class="add-btn" @click.stop="addToCart(product)">
<cl-icon name="add-line" :size="28" color="#fff" />
</view>
</view>
</view>
</view>
</view>
</view>
<cl-empty v-if="productList.length === 0" text="暂无商品" />
</scroll-view>
</view>
<!-- 底部TabBar -->
<custom-tabbar :current="2" />
</cl-page>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue';
import { router, useCool } from '@/cool';
import CustomTabbar from '@/components/tabbar.uvue';
const { service } = useCool();
const statusBarHeight = ref(uni.getWindowInfo().statusBarHeight);
const categories = ref<any[]>([
{ id: 0, name: '全部' }
]);
const currentCategory = ref(0);
const productList = ref<any[]>([]);
const cartCount = ref(2);
// 加载分类
async function loadCategories() {
try {
const res = await service.nongchuang.category.list();
if (Array.isArray(res)) {
categories.value = [{ id: 0, name: '全部' }, ...res];
}
} catch (e) {
console.error('加载分类失败', e);
}
}
async function loadProducts() {
try {
const params: any = {};
if (currentCategory.value > 0) {
params.categoryId = currentCategory.value;
}
const res = await service.nongchuang.product.list(params);
if (res != null) {
// 后端直接返回数组或包含list的对象
if (Array.isArray(res)) {
productList.value = res;
} else if (res.list != null) {
productList.value = res.list as any[];
} else {
productList.value = [];
}
}
} catch (e) {
console.error('加载商品失败', e);
productList.value = [];
}
}
function selectCategory(id: number) {
currentCategory.value = id;
loadProducts();
}
function loadMore() {
// 加载更多
}
function goSearch() {
router.push({ path: '/pages/mall/search' });
}
function goCart() {
router.push({ path: '/pages/mall/cart' });
}
function goProductDetail(id: number) {
router.push({ path: '/pages/mall/detail', query: { id } });
}
async function addToCart(product: any) {
try {
await service.nongchuang.cart.add({ productId: product.id, quantity: 1 });
cartCount.value++;
uni.showToast({ title: '已加入购物车', icon: 'success' });
} catch (e: any) {
const msg = e.message;
uni.showToast({ title: msg != null ? msg : '添加失败', icon: 'none' });
}
}
onMounted(() => {
loadCategories();
loadProducts();
});
</script>
<style lang="scss" scoped>
.shop-page {
flex: 1;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background-color: #fff;
}
.navbar-content {
height: 44px; /* Standard Nav Height */
display: flex;
align-items: center;
padding-left: 30rpx;
}
.page-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.search-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
/* 避免胶囊按钮遮挡,如果是放在顶部 */
}
.cart-btn-wrap {
position: relative;
margin-left: 20rpx;
}
.cart-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08); /* 增加阴影提升质感 */
border: 1rpx solid #f0f0f0;
}
.badge {
position: absolute;
top: -6rpx;
right: -6rpx;
min-width: 32rpx;
height: 32rpx;
background-color: #ff4d4f;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8rpx;
z-index: 10;
border: 2rpx solid #fff; /* White border for separation */
}
.badge-text {
font-size: 20rpx;
color: #fff;
line-height: 1;
font-weight: bold;
}
.search-bar {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 30rpx;
background-color: #f5f5f5;
border-radius: 40rpx;
height: 72rpx;
}
.search-placeholder {
font-size: 28rpx;
color: #999;
margin-left: 16rpx;
}
.category-scroll {
background-color: #fff;
}
.category-list {
display: flex;
flex-direction: row;
padding: 20rpx;
}
.category-item {
padding: 16rpx 30rpx;
margin-right: 16rpx;
background-color: #f5f5f5;
border-radius: 30rpx;
}
.category-item.active {
background-color: #52c41a;
}
.category-text {
font-size: 26rpx;
color: #666;
}
.active-text {
color: #fff;
}
.product-scroll {
flex: 1;
padding: 20rpx;
}
.product-list {
display: flex;
flex-direction: column;
}
.product-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.product-card {
width: 48%;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.product-image {
width: 100%;
height: 280rpx;
}
.product-info {
padding: 16rpx;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.product-origin {
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
.product-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.product-price {
font-size: 32rpx;
font-weight: bold;
color: #ff4d4f;
}
.add-btn {
width: 48rpx;
height: 48rpx;
background-color: #52c41a;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>