更新模板

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

279
pages/mall/cart.uvue Normal file
View File

@@ -0,0 +1,279 @@
<template>
<cl-page>
<cl-topbar title="购物车" />
<view class="cart-page">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length > 0">
<view class="cart-item" v-for="(item, index) in cartList" :key="item.id">
<view class="item-selector" @click="toggleSelect(index)">
<cl-icon :name="item.selected == 1 ? 'checkbox-circle-fill' : 'checkbox-blank-circle-line'"
:color="item.selected == 1 ? '#52c41a' : '#ccc'"
:size="40" />
</view>
<image class="item-img" :src="item.productImage" mode="aspectFill" />
<view class="item-info">
<text class="item-name">{{ item.productName }}</text>
<text class="item-spec">{{ item.specifications || '默认规格' }}</text>
<view class="item-bottom">
<text class="item-price">¥{{ item.price }}</text>
<cl-input-number v-model="item.quantity" :min="1" :max="99" @change="onQtyChange(item)" />
</view>
</view>
<view class="item-delete" @click="onDelete(item.id)">
<cl-icon name="delete-bin-line" color="#ff4d4f" :size="36" />
</view>
</view>
</view>
<cl-empty v-else text="购物车空空如也" icon="/static/images/empty_cart.png">
<cl-button type="primary" size="small" round @click="goMall">去逛逛</cl-button>
</cl-empty>
<!-- 底部结算栏 -->
<view class="cart-footer" v-if="cartList.length > 0">
<view class="select-all" @click="toggleSelectAll">
<cl-icon :name="isAllSelected ? 'checkbox-circle-fill' : 'checkbox-blank-circle-line'"
:color="isAllSelected ? '#52c41a' : '#ccc'"
:size="40" />
<text class="ml-2">全选</text>
</view>
<view class="footer-right">
<view class="total-info">
<text class="total-label">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<cl-button type="primary" round custom-class="checkout-btn" @click="onCheckout">结算({{ selectedCount }})</cl-button>
</view>
</view>
</view>
</cl-page>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { router, useCool } from "@/cool";
const { service } = useCool();
const cartList = ref<any[]>([]);
const isAllSelected = computed(() => {
return cartList.value.length > 0 && cartList.value.every(item => item.selected == 1);
});
const totalPrice = computed(() => {
return cartList.value
.filter(item => item.selected == 1)
.reduce((sum, item) => sum + (item.price * item.quantity), 0)
.toFixed(2);
});
const selectedCount = computed(() => {
return cartList.value.filter(item => item.selected == 1).length;
});
async function loadCart() {
try {
const res = await service.nongchuang.cart.list();
if (res) {
cartList.value = (res as any[]).map(item => {
// 后端返回的是 0/1前端图标显示需要 item.selected
return item;
});
}
} catch (e) {
uni.showToast({ title: "加载购物车失败", icon: "none" });
}
}
async function toggleSelect(index: number) {
const item = cartList.value[index];
try {
await service.nongchuang.cart.toggleSelect({ id: item.id });
// 简单处理:本地直接取反,或者重新 loadCart
item.selected = item.selected == 1 ? 0 : 1;
} catch (e) {
uni.showToast({ title: "操作失败", icon: "none" });
}
}
async function toggleSelectAll() {
try {
const newVal = isAllSelected.value ? 0 : 1;
await service.nongchuang.cart.selectAll({ selected: newVal });
// 简单处理:全部设为新值
cartList.value.forEach(item => item.selected = newVal);
} catch (e) {
uni.showToast({ title: "操作失败", icon: "none" });
}
}
async function onQtyChange(item: any) {
try {
await service.nongchuang.cart.update({ id: item.id, quantity: item.quantity });
// 如果数量为 0后端逻辑会删除本地也刷新下
if (item.quantity <= 0) {
loadCart();
}
} catch (e) {
uni.showToast({ title: "更新失败", icon: "none" });
}
}
function onDelete(id: number) {
uni.showModal({
title: "提示",
content: "确定删除该商品吗?",
success: async (res) => {
if (res.confirm) {
try {
await service.nongchuang.cart.delete(id);
loadCart();
} catch (e) {
uni.showToast({ title: "删除失败", icon: "none" });
}
}
}
});
}
function onCheckout() {
if (selectedCount.value === 0) {
return uni.showToast({ title: "请选择商品", icon: "none" });
}
const selectedIds = cartList.value.filter(item => item.selected).map(item => item.id);
router.push({ path: "/pages/order/create", query: { cartIds: selectedIds.join(",") } });
}
function goMall() {
router.push({ path: "/pages/mall/index" });
}
onMounted(() => {
loadCart();
});
</script>
<style lang="scss" scoped>
.cart-page {
padding: 20rpx;
padding-bottom: 120rpx;
}
.cart-item {
display: flex;
background: #fff;
padding: 20rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
align-items: center;
}
.item-selector {
padding: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.item-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin: 0 20rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 160rpx;
}
.item-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-spec {
font-size: 24rpx;
color: #999;
}
.item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-price {
color: #ff4d4f;
font-size: 32rpx;
font-weight: bold;
}
.item-delete {
padding: 20rpx;
}
.cart-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100rpx;
background: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
z-index: 100;
}
.select-all {
display: flex;
align-items: center;
font-size: 28rpx;
color: #333;
}
.footer-right {
display: flex;
align-items: center;
}
.total-info {
margin-right: 20rpx;
display: flex;
align-items: center;
}
.total-label {
font-size: 28rpx;
color: #333;
}
.total-price {
font-size: 36rpx;
color: #ff4d4f;
font-weight: bold;
margin-left: 10rpx;
}
.checkout-btn {
min-width: 200rpx;
}
</style>

269
pages/mall/detail.uvue Normal file
View File

@@ -0,0 +1,269 @@
<template>
<cl-page>
<custom-back />
<scroll-view scroll-y class="content" v-if="product != null">
<swiper class="banner" autoplay circular indicator-dots>
<swiper-item v-for="(img, index) in productImages" :key="index">
<image class="banner-image" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<view class="product-info">
<view class="price-row">
<text class="price">¥{{ product.price }}</text>
<text class="original-price" v-if="product.originalPrice != null">¥{{ product.originalPrice }}</text>
<text class="sales">已售{{ product.salesCount != null ? product.salesCount : 0 }}件</text>
</view>
<text class="product-name">{{ product.name }}</text>
<text class="product-origin" v-if="product.origin != null">产地: {{ product.origin }}</text>
</view>
<view class="spec-section">
<view class="spec-row">
<text class="spec-label">规格</text>
<text class="spec-value">{{ selectedSpec.length > 0 ? selectedSpec : '请选择' }}</text>
<cl-icon name="arrow-right" :size="28" color="#999" />
</view>
</view>
<view class="detail-section">
<text class="section-title">商品详情</text>
<rich-text class="detail-content" :nodes="product.detail != null ? product.detail : '暂无详情'"></rich-text>
</view>
<view style="height: 140rpx;"></view>
</scroll-view>
<view class="bottom-bar" v-if="product != null">
<view class="action-btns">
<view class="action-item" @click="goCart">
<cl-icon name="shopping-cart" :size="44" color="#666" />
<text class="action-text">购物车</text>
</view>
</view>
<view class="buy-btns">
<cl-button class="btn-cart" type="warning" round @click="addToCart">加入购物车</cl-button>
<cl-button class="btn-buy" type="primary" round @click="buyNow">立即购买</cl-button>
</view>
</view>
</cl-page>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue';
import { router, useCool, parseObject } from '@/cool';
import CustomBack from '@/components/custom-back.uvue';
const { service } = useCool();
const product = ref<any>(null);
const selectedSpec = ref('');
const productImages = computed(() : string[] => {
if (product.value == null) return ['/static/images/product.png'];
const p = product.value as UTSJSONObject;
const mainImage = p.getString('mainImage') != null ? p.getString('mainImage') : '/static/images/product.png';
if (p.get('images') == null) return [mainImage as string];
try {
const images = parseObject<string[]>(p.get('images') as string);
return images != null ? images : [mainImage as string];
} catch {
return [mainImage as string];
}
});
async function loadProduct() {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const productId = Number(currentPage.options != null && currentPage.options!['id'] != null ? currentPage.options!['id'] : 0);
if (productId != 0) {
try {
const res = await service.nongchuang.product.detail({ id: productId });
product.value = res;
} catch (e) {
product.value = {
id: productId,
name: '有机番茄',
price: 9.9,
originalPrice: 15.9,
salesCount: 128,
origin: '山东寿光',
detail: '新鲜有机番茄,当日采摘'
};
}
}
}
function goCart() {
router.push({ path: '/pages/mall/cart' });
}
async function addToCart() {
if (product.value == null) return;
try {
await service.nongchuang.cart.add({ productId: product.value.id, quantity: 1 });
uni.showToast({ title: '已加入购物车', icon: 'success' });
} catch (e: any) {
const msg = e.message;
uni.showToast({ title: msg != null ? msg : '添加失败', icon: 'none' });
}
}
function buyNow() {
uni.showToast({ title: '购买功能开发中', icon: 'none' });
}
onMounted(() => {
loadProduct();
});
</script>
<style lang="scss" scoped>
.content {
flex: 1;
}
.banner {
height: 500rpx;
.banner-image {
width: 100%;
height: 100%;
}
}
.product-info {
padding: 30rpx;
background: #fff;
.price-row {
display: flex;
flex-direction: row;
align-items: flex-end;
.price {
font-size: 44rpx;
font-weight: bold;
color: #ff4d4f;
margin-right: 16rpx;
}
.original-price {
font-size: 28rpx;
color: #999;
}
.sales {
font-size: 24rpx;
color: #999;
margin-left: auto;
}
}
.product-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-top: 16rpx;
}
.product-origin {
font-size: 26rpx;
color: #999;
margin-top: 10rpx;
}
}
.spec-section {
margin-top: 20rpx;
background: #fff;
.spec-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx 30rpx;
.spec-label {
font-size: 28rpx;
color: #333;
}
.spec-value {
flex: 1;
text-align: right;
font-size: 28rpx;
color: #999;
margin-right: 10rpx;
}
}
}
.detail-section {
margin-top: 20rpx;
padding: 30rpx;
background: #fff;
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.detail-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 20rpx;
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
.action-btns {
display: flex;
flex-direction: row;
padding-right: 30rpx;
.action-item {
margin-right: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
.action-text {
font-size: 20rpx;
color: #666;
}
}
}
.buy-btns {
flex: 1;
display: flex;
flex-direction: row;
.btn-cart {
flex: 1;
margin-right: 16rpx;
}
.btn-buy {
flex: 1;
}
}
}
</style>

255
pages/mall/index.uvue Normal file
View File

@@ -0,0 +1,255 @@
<template>
<cl-page>
<cl-topbar title="商城" />
<scroll-view scroll-y class="content">
<view class="search-bar" @click="goSearch">
<cl-icon class="search-icon" name="search" :size="32" color="#999" />
<text class="placeholder">搜索农特产品</text>
</view>
<swiper class="banner" autoplay circular indicator-dots>
<swiper-item v-for="(item, index) in banners" :key="index">
<image class="banner-image" :src="item.image" mode="aspectFill" />
</swiper-item>
</swiper>
<view class="category-section">
<view
v-for="cat in categories"
:key="cat.id"
class="category-item"
@click="goCategory(cat.id)"
>
<view class="category-icon" :style="{ background: cat.color }">
<cl-icon :name="cat.icon" :size="44" color="#fff" />
</view>
<text class="category-name">{{ cat.name }}</text>
</view>
</view>
<view class="products-section">
<view class="section-header">
<text class="section-title">热门推荐</text>
</view>
<view class="product-grid">
<view
v-for="product in products"
:key="product.id"
class="product-card"
@click="goDetail(product.id)"
>
<image class="product-image" :src="product.getString('mainImage') != null ? product.getString('mainImage') : '/static/images/product.png'" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<view class="add-btn" @click.stop="addToCart(product)">
<cl-icon name="plus" :size="28" color="#fff" />
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</cl-page>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue';
import { router, useCool } from '@/cool';
const { service } = useCool();
const banners = ref([
{ id: 1, image: '/static/images/banner1.png' },
{ id: 2, image: '/static/images/banner2.png' }
]);
const categories = ref([
{ id: 1, name: '新鲜蔬菜', icon: 'leaf', color: '#52c41a' },
{ id: 2, name: '时令水果', icon: 'apple', color: '#ff7a45' },
{ id: 3, name: '有机粮油', icon: 'rice', color: '#faad14' },
{ id: 4, name: '土特产', icon: 'gift', color: '#1890ff' }
]);
const products = ref<any[]>([]);
async function loadProducts() {
try {
const res = await service.nongchuang.product.list({ isRecommend: 1 });
if (res != null) {
const data = res as UTSJSONObject;
if (Array.isArray(res)) {
list.value = res as any[];
} else if (res != null) {
const data = res as UTSJSONObject;
const arr = data.getArray<any>("list");
list.value = arr != null ? arr : [];
}
products.value = list != null ? list : (res as any[]);
}
} catch (e) {
products.value = [
{ id: 1, name: '有机番茄', mainImage: '/static/images/tomato.png', price: 9.9 },
{ id: 2, name: '新鲜草莓', mainImage: '/static/images/strawberry.png', price: 29.9 }
];
}
}
function goSearch() {
router.push({ path: '/pages/mall/search' });
}
function goCategory(categoryId: number) {
router.push({ path: '/pages/mall/products', query: { categoryId } });
}
function goDetail(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 });
uni.showToast({ title: '已加入购物车', icon: 'success' });
} catch (e: any) {
const msg = e.message;
uni.showToast({ title: msg != null ? msg : '添加失败', icon: 'none' });
}
}
onMounted(() => {
loadProducts();
});
</script>
<style lang="scss" scoped>
.content {
flex: 1;
}
.search-bar {
display: flex;
flex-direction: row;
align-items: center;
margin: 20rpx;
padding: 20rpx 30rpx;
background: #f5f5f5;
border-radius: 40rpx;
.search-icon {
margin-right: 16rpx;
}
.placeholder {
font-size: 28rpx;
color: #999;
}
}
.banner {
height: 300rpx;
margin: 0 20rpx;
border-radius: 16rpx;
overflow: hidden;
.banner-image {
width: 100%;
height: 100%;
}
}
.category-section {
display: flex;
justify-content: space-around;
padding: 30rpx 20rpx;
.category-item {
text-align: center;
.category-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.category-name {
font-size: 24rpx;
color: #333;
margin-top: 10rpx;
}
}
}
.products-section {
padding: 0 20rpx 20rpx;
.section-header {
margin-bottom: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.product-card {
width: 48%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
.product-image {
width: 100%;
height: 280rpx;
}
.product-info {
padding: 16rpx;
.product-name {
font-size: 28rpx;
color: #333;
}
.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: #52c41a;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
</style>

143
pages/mall/products.uvue Normal file
View File

@@ -0,0 +1,143 @@
<template>
<cl-page>
<cl-topbar title="商品列表" />
<scroll-view scroll-y class="content" @scrolltolower="loadMore">
<view class="product-grid">
<view
v-for="product in products"
:key="product.id"
class="product-card"
@click="goDetail(product.id)"
>
<image class="product-image" :src="product.mainImage != null ? product.mainImage : '/static/images/product.png'" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-footer">
<text class="product-price">¥{{ product.price }}</text>
<view class="add-btn" @click.stop="addToCart(product)">
<cl-icon name="plus" :size="28" color="#fff" />
</view>
</view>
</view>
</view>
</view>
<cl-empty v-if="products.length === 0 && loading == false" text="暂无商品" />
</scroll-view>
</cl-page>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue';
import { router, useCool } from '@/cool';
const { service } = useCool();
const products = ref<any[]>([]);
const loading = ref(false);
async function loadProducts() {
loading.value = true;
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const categoryId = currentPage.options?.categoryId;
try {
const res = await service.nongchuang.product.list({ categoryId });
if (res != null) {
const data = res as UTSJSONObject;
const list = data.getArray<any>("list");
products.value = list != null ? list : (res as any[]);
}
} catch (e) {
products.value = [
{ id: 1, name: '有机番茄', mainImage: '/static/images/tomato.png', price: 9.9 },
{ id: 2, name: '新鲜草莓', mainImage: '/static/images/strawberry.png', price: 29.9 }
];
}
loading.value = false;
}
function loadMore() {
// 加载更多
}
function goDetail(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 });
uni.showToast({ title: '已加入购物车', icon: 'success' });
} catch (e: any) {
const msg = e.message;
uni.showToast({ title: msg != null ? msg : '添加失败', icon: 'none' });
}
}
onMounted(() => {
loadProducts();
});
</script>
<style lang="scss" scoped>
.content {
flex: 1;
padding: 20rpx;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.product-card {
width: 48%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 30rpx;
.product-image {
width: 100%;
height: 280rpx;
}
.product-info {
padding: 16rpx;
.product-name {
font-size: 28rpx;
color: #333;
}
.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: #52c41a;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
</style>

198
pages/mall/search.uvue Normal file
View File

@@ -0,0 +1,198 @@
<template>
<cl-page>
<cl-topbar title="搜索" />
<view class="search-page">
<view class="search-bar">
<cl-icon class="search-icon" name="search" :size="32" color="#999" />
<input class="search-input" v-model="keyword" type="text" placeholder="请输入商品名称" @confirm="doSearch" />
<view v-if="keyword.length > 0" class="clear-btn" @click="clearKeyword">
<cl-icon name="close-circle" :size="32" color="#999" />
</view>
</view>
<view v-if="searched == false" class="history-section">
<view class="section-header">
<text class="section-title">搜索历史</text>
<view class="clear-history" @click="clearHistory">
<cl-icon name="delete" :size="28" color="#999" />
</view>
</view>
<view class="history-tags">
<view v-for="(item, index) in history" :key="index" class="history-tag" @click="searchByHistory(item)">
<text>{{ item }}</text>
</view>
</view>
</view>
<view v-if="searched == true" class="result-section">
<view class="product-grid">
<view
v-for="product in results"
:key="product.id"
class="product-card"
@click="goDetail(product.id)"
>
<image class="product-image" :src="product.mainImage != null ? product.mainImage : '/static/images/product.png'" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<text class="product-price">¥{{ product.price }}</text>
</view>
</view>
</view>
<cl-empty v-if="results.length === 0" text="未找到商品" />
</view>
</view>
</cl-page>
</template>
<script setup lang="uts">
import { ref } from 'vue';
import { router, useCool } from '@/cool';
const { service } = useCool();
const keyword = ref('');
const searched = ref(false);
const history = ref<string[]>(['番茄', '草莓', '苹果']);
const results = ref<any[]>([]);
function clearKeyword() {
keyword.value = '';
searched.value = false;
}
async function doSearch() {
if (keyword.value.trim().length == 0) return;
// 添加到历史
if (history.value.includes(keyword.value) == false) {
history.value.unshift(keyword.value);
if (history.value.length > 10) history.value.pop();
}
searched.value = true;
try {
const res = await service.nongchuang.product.list({ keyword: keyword.value });
if (res != null) {
const data = res as UTSJSONObject;
const list = data.getArray<any>("list");
results.value = list != null ? list : (res as any[]);
}
} catch (e) {
results.value = [];
}
}
function searchByHistory(item: string) {
keyword.value = item;
doSearch();
}
function clearHistory() {
history.value = [];
}
function goDetail(id: number) {
router.push({ path: '/pages/mall/detail', query: { id } });
}
</script>
<style lang="scss" scoped>
.search-page {
padding: 20rpx;
}
.search-bar {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 24rpx;
background: #f5f5f5;
border-radius: 40rpx;
.search-icon {
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
}
.history-section {
margin-top: 30rpx;
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.history-tags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 20rpx;
.history-tag {
padding: 12rpx 24rpx;
background: #f5f5f5;
border-radius: 30rpx;
font-size: 26rpx;
color: #666;
margin-right: 16rpx;
margin-bottom: 16rpx;
}
}
}
.result-section {
margin-top: 20rpx;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.product-card {
width: 48%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
.product-image {
width: 100%;
height: 240rpx;
}
.product-info {
padding: 16rpx;
.product-name {
font-size: 28rpx;
color: #333;
}
.product-price {
font-size: 32rpx;
font-weight: bold;
color: #ff4d4f;
margin-top: 10rpx;
}
}
}
</style>