2025-12-09 22:57:49 +08:00

407 lines
8.1 KiB
Vue

<template>
<view class="category-page">
<view class="search-bar flex">
<input class="search-input" v-model="keyword" placeholder="搜索商品" confirm-type="search"
@confirm="handleSearch" />
<button class="search-btn" @click="handleSearch">搜索</button>
</view>
<view class="body-wrap">
<scroll-view class="side-menu" scroll-y>
<view
v-for="item in topCategories"
:key="item.id"
class="side-item"
:class="{ active: item.id === activeTopId }"
@click="handleTopSelect(item.id)"
>
<image v-if="item.image_url" class="side-thumb" :src="item.image_url" mode="aspectFill" />
<view class="side-text">{{ item.name }}</view>
</view>
</scroll-view>
<scroll-view class="content" scroll-y @scrolltolower="loadMore">
<view class="sub-wrap" v-if="subCategories.length">
<view
v-for="sub in subCategories"
:key="sub.id"
class="sub-tag"
:class="{ active: sub.id === activeSubId }"
@click="handleSubSelect(sub.id)"
>
{{ sub.name }}
</view>
</view>
<view class="product-list">
<view
class="product-card shadow"
v-for="item in products"
:key="item.id"
@click="goDetail(item)"
>
<image class="product-image" :src="item.main_image_url || item.image_url" mode="aspectFill" />
<view class="product-info">
<view class="title-row">
<view class="product-title">{{ item.name }}</view>
<view class="hot-tag" v-if="item.is_hot_selling === 1">热销</view>
</view>
<view class="product-desc">{{ item.description }}</view>
<view class="meta-row flex justify-between">
<view class="rating">
<text class="iconfont icon-xingxing"></text>
<text class="rating-num">{{ item.rating || 0 }}</text>
<text class="like">· {{ item.like_count || 0 }} 喜欢</text>
</view>
<view class="price" v-if="item.skus && item.skus.length">
<text class="current">{{ formatPrice(item.skus[0].price) }}</text>
<text class="origin" v-if="item.skus[0].original_price">{{ formatPrice(item.skus[0].original_price) }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="status" v-if="!products.length && !loading && !catLoading">
暂无商品
</view>
<view class="status" v-if="loading">加载中...</view>
<view class="status" v-if="noMore && products.length">没有更多了</view>
</scroll-view>
</view>
</view>
</template>
<script>
import request from '/api/request'
export default {
data() {
return {
keyword: '',
topCategories: [],
subCategories: [],
activeTopId: null,
activeSubId: null,
products: [],
page: 1,
page_size: 20,
loading: false,
catLoading: false,
noMore: false
}
},
onLoad() {
this.initPage()
},
onPullDownRefresh() {
this.initPage().finally(() => uni.stopPullDownRefresh())
},
onReachBottom() {
this.loadMore()
},
methods: {
async initPage() {
this.page = 1
this.noMore = false
this.products = []
await this.fetchTopCategories()
},
async fetchTopCategories() {
this.catLoading = true
try {
const res = await request('xcx/product/categories', 'GET', { parent_id: 0 })
this.topCategories = res.list || []
if (this.topCategories.length) {
this.activeTopId = this.topCategories[0].id
await this.fetchSubCategories(this.activeTopId)
} else {
this.subCategories = []
this.activeTopId = null
this.activeSubId = null
}
} finally {
this.catLoading = false
}
},
async fetchSubCategories(parentId) {
this.catLoading = true
try {
const res = await request('xcx/product/categories', 'GET', { parent_id: parentId })
this.subCategories = res.list || []
this.activeSubId = this.subCategories.length ? this.subCategories[0].id : null
} finally {
this.catLoading = false
this.resetProducts()
}
},
resetProducts() {
this.page = 1
this.noMore = false
this.products = []
this.loadProducts(true)
},
async loadProducts(reset = false) {
if (this.loading || this.noMore) return
this.loading = true
try {
const params = {
page: this.page,
page_size: this.page_size
}
if (this.activeSubId) {
params.category_id = this.activeSubId
}
if (this.keyword) {
params.name = this.keyword.trim()
}
const res = await request('xcx/products', 'GET', params)
const list = res.list || []
this.products = reset || this.page === 1 ? list : this.products.concat(list)
const total = res.total || 0
if (this.products.length >= total || list.length < this.page_size) {
this.noMore = true
} else {
this.page += 1
}
} finally {
this.loading = false
}
},
loadMore() {
this.loadProducts()
},
handleTopSelect(id) {
if (id === this.activeTopId) return
this.activeTopId = id
this.fetchSubCategories(id)
},
handleSubSelect(id) {
if (id === this.activeSubId) return
this.activeSubId = id
this.resetProducts()
},
handleSearch() {
this.resetProducts()
},
formatPrice(value) {
if (!value) return '0.00'
return (value / 100).toFixed(2)
},
goDetail(item) {
uni.navigateTo({
url: `/pages/shopDetail/index?id=${item.id}`
})
}
}
}
</script>
<style lang="scss" scoped>
.category-page {
min-height: 100vh;
background: #f7f7f9;
padding: 20rpx;
box-sizing: border-box;
}
.search-bar {
background: #fff;
border-radius: 16rpx;
padding: 12rpx 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
margin-bottom: 16rpx;
align-items: center;
column-gap: 12rpx;
}
.search-input {
flex: 1;
height: 72rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333;
background: #f6f6f8;
border-radius: 12rpx;
box-sizing: border-box;
}
.search-btn {
height: 72rpx;
line-height: 72rpx;
padding: 0 28rpx;
color: #fff;
background: linear-gradient(90deg, #ad46ff, #f6339a);
border-radius: 12rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.body-wrap {
display: flex;
background: #fff;
border-radius: 18rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
min-height: 70vh;
}
.side-menu {
width: 200rpx;
background: #fafafa;
border-right: 1rpx solid #f0f0f0;
}
.side-item {
padding: 24rpx 16rpx;
text-align: center;
color: #666;
font-size: 28rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.side-item.active {
background: #fff;
color: #9810fa;
font-weight: 600;
border-left: 6rpx solid #9810fa;
}
.side-thumb {
width: 88rpx;
height: 88rpx;
border-radius: 12rpx;
margin-bottom: 12rpx;
}
.side-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
flex: 1;
padding: 20rpx;
box-sizing: border-box;
}
.sub-wrap {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 20rpx;
}
.sub-tag {
padding: 10rpx 18rpx;
background: #f6f6f8;
border-radius: 30rpx;
font-size: 26rpx;
color: #666;
}
.sub-tag.active {
color: #fff;
background: linear-gradient(90deg, #ad46ff, #f6339a);
}
.product-list {
display: grid;
grid-template-columns: 1fr;
gap: 20rpx;
}
.product-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.product-image {
width: 100%;
height: 320rpx;
background: #f7f7f7;
}
.product-info {
padding: 20rpx;
}
.title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.product-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 12rpx;
}
.hot-tag {
color: #fb2c36;
font-size: 24rpx;
background: rgba(251, 44, 54, 0.08);
border-radius: 20rpx;
padding: 6rpx 14rpx;
}
.product-desc {
font-size: 26rpx;
color: #777;
line-height: 1.5;
margin-bottom: 12rpx;
}
.meta-row {
align-items: center;
}
.rating {
font-size: 24rpx;
color: #999;
}
.rating .iconfont {
color: #ffb400;
margin-right: 4rpx;
}
.rating-num {
margin-right: 6rpx;
color: #333;
}
.like {
color: #999;
}
.price {
text-align: right;
}
.price .current {
color: #9810fa;
font-size: 32rpx;
font-weight: 700;
margin-right: 8rpx;
}
.price .origin {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
.status {
text-align: center;
color: #888;
padding: 24rpx 0;
font-size: 26rpx;
}
</style>