更新模板
This commit is contained in:
279
pages/mall/cart.uvue
Normal file
279
pages/mall/cart.uvue
Normal 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
269
pages/mall/detail.uvue
Normal 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
255
pages/mall/index.uvue
Normal 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
143
pages/mall/products.uvue
Normal 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
198
pages/mall/search.uvue
Normal 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>
|
||||
Reference in New Issue
Block a user