559 lines
13 KiB
Vue
559 lines
13 KiB
Vue
<template>
|
|
<view class="container">
|
|
<!-- Search Bar (独立按钮版) -->
|
|
<view class="header-section">
|
|
<view class="search-bar-row">
|
|
<view class="search-input-box">
|
|
<up-search
|
|
type="text"
|
|
placeholder="搜索产品型号"
|
|
placeholder-class="search-placeholder"
|
|
v-model="title"
|
|
:showAction="false"
|
|
:animation="false"
|
|
bgColor="#f5f5f5"
|
|
shape="round"
|
|
@search="handleSearch"
|
|
/>
|
|
</view>
|
|
<view class="custom-search-btn" @tap="handleSearch" hover-class="btn-hover">
|
|
搜索
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Category Tabs (Sticky) -->
|
|
<view class="tabs-wrapper">
|
|
<scroll-view
|
|
class="category-tabs"
|
|
scroll-x
|
|
show-scrollbar="false"
|
|
:scroll-into-view="'tab-' + currentTab"
|
|
scroll-with-animation
|
|
>
|
|
<view
|
|
v-for="(tab, index) in tabs"
|
|
:key="index"
|
|
:id="'tab-' + index"
|
|
:class="['tab-item', { active: currentTab === index }]"
|
|
@tap="switchTab(index, tab.id)"
|
|
>
|
|
<text class="tab-text">{{ tab.name }}</text>
|
|
<view class="active-dot" v-if="currentTab === index"></view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Content Area -->
|
|
<view class="content-area">
|
|
<!-- Loading State (Skeleton Screen) -->
|
|
<view class="loading-container" v-if="loading && products.length === 0">
|
|
<!-- CSS 骨架屏结构 -->
|
|
<view class="waterfall-container">
|
|
<view class="waterfall-column">
|
|
<view class="skeleton-card" v-for="i in 3" :key="'l'+i">
|
|
<view class="skeleton-img pulse"></view>
|
|
<view class="skeleton-text pulse"></view>
|
|
</view>
|
|
</view>
|
|
<view class="waterfall-column">
|
|
<view class="skeleton-card" v-for="i in 3" :key="'r'+i">
|
|
<view class="skeleton-img pulse" style="height: 300rpx"></view>
|
|
<view class="skeleton-text pulse"></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Empty State -->
|
|
<view class="empty-container" v-else-if="products.length === 0">
|
|
<image src="/static/images/empty.png" mode="aspectFit" class="empty-image animate-float" />
|
|
<text class="empty-text">暂无相关产品</text>
|
|
</view>
|
|
|
|
<!-- Waterfall Layout -->
|
|
<view class="waterfall-container" v-else>
|
|
<view class="waterfall-column">
|
|
<view
|
|
v-for="(product, index) in leftProducts"
|
|
:key="product.id"
|
|
class="product-item animate-card-enter"
|
|
:style="{ animationDelay: index * 0.1 + 's' }"
|
|
@tap="navigateToDetail(product.id)"
|
|
>
|
|
<view class="img-box">
|
|
<image
|
|
:src="product.cover"
|
|
mode="widthFix"
|
|
class="product-image"
|
|
@load="imageLoaded(index * 2)"
|
|
/>
|
|
</view>
|
|
<view class="info-box">
|
|
<text class="product-code">{{ product.title }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="waterfall-column">
|
|
<view
|
|
v-for="(product, index) in rightProducts"
|
|
:key="product.id"
|
|
class="product-item animate-card-enter"
|
|
:style="{ animationDelay: (index * 0.1 + 0.05) + 's' }"
|
|
@tap="navigateToDetail(product.id)"
|
|
>
|
|
<view class="img-box">
|
|
<image
|
|
:src="product.cover"
|
|
mode="widthFix"
|
|
class="product-image"
|
|
@load="imageLoaded(index * 2 + 1)"
|
|
/>
|
|
</view>
|
|
<view class="info-box">
|
|
<text class="product-code">{{ product.title }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Footer State -->
|
|
<view class="footer-state">
|
|
<view class="loading-more" v-if="loadingMore">
|
|
<view class="loading-spinner"></view>
|
|
</view>
|
|
<view class="no-more-data" v-if="noMoreData && products.length > 0">
|
|
<text class="divider-line"></text>
|
|
<text>THE END</text>
|
|
<text class="divider-line"></text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { getCategoryListByPidApi, getProductListApi } from "@/api/page/product"
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
page: 1,
|
|
pageSize: 10,
|
|
pid: 0,
|
|
currentTab: 0,
|
|
categoryId: 0,
|
|
title: "",
|
|
tabs: [],
|
|
products: [],
|
|
loading: false,
|
|
loadingMore: false,
|
|
noMoreData: false,
|
|
totalPages: 1,
|
|
}
|
|
},
|
|
computed: {
|
|
// 将产品分为左右两列,实现瀑布流布局
|
|
leftProducts() {
|
|
return this.products.filter((_, index) => index % 2 === 0)
|
|
},
|
|
rightProducts() {
|
|
return this.products.filter((_, index) => index % 2 === 1)
|
|
},
|
|
},
|
|
onLoad(options) {
|
|
if (options && options.pid) {
|
|
this.pid = options.pid
|
|
this.categoryId = options.pid
|
|
this.getCategoryList()
|
|
} else {
|
|
uni.showToast({
|
|
title: "参数错误",
|
|
icon: "none",
|
|
})
|
|
}
|
|
},
|
|
onPullDownRefresh() {
|
|
this.refreshProductList()
|
|
},
|
|
onReachBottom() {
|
|
this.loadMoreProducts()
|
|
},
|
|
mounted() {
|
|
this.getProductList()
|
|
},
|
|
methods: {
|
|
navigateBack() {
|
|
uni.navigateBack()
|
|
},
|
|
switchTab(index, id) {
|
|
this.currentTab = index
|
|
this.categoryId = id
|
|
this.page = 1
|
|
this.products = []
|
|
this.noMoreData = false
|
|
this.getProductList()
|
|
},
|
|
navigateToDetail(id) {
|
|
uni.navigateTo({
|
|
url: `/pages/product/detail?id=${id}`,
|
|
})
|
|
},
|
|
getCategoryList() {
|
|
getCategoryListByPidApi(this.pid).then((res) => {
|
|
res.unshift({
|
|
id: 0,
|
|
name: "全部",
|
|
})
|
|
this.tabs = res
|
|
})
|
|
},
|
|
handleSearch() {
|
|
// 这里的搜索逻辑是正确的:重置分页和数据,重新请求
|
|
this.page = 1
|
|
this.products = []
|
|
this.noMoreData = false
|
|
this.getProductList()
|
|
},
|
|
refreshProductList() {
|
|
this.page = 1
|
|
this.products = []
|
|
this.noMoreData = false
|
|
this.getProductList()
|
|
.then(() => {
|
|
uni.stopPullDownRefresh()
|
|
uni.showToast({
|
|
title: "刷新成功",
|
|
icon: "success",
|
|
duration: 1500,
|
|
})
|
|
})
|
|
.catch(() => {
|
|
uni.stopPullDownRefresh()
|
|
uni.showToast({
|
|
title: "刷新失败",
|
|
icon: "none",
|
|
duration: 1500,
|
|
})
|
|
})
|
|
},
|
|
loadMoreProducts() {
|
|
if (this.loadingMore || this.noMoreData) return
|
|
|
|
this.page++
|
|
this.loadingMore = true
|
|
|
|
getProductListApi({
|
|
page: this.page,
|
|
size: this.pageSize,
|
|
title: this.title,
|
|
category_id: this.categoryId,
|
|
})
|
|
.then((res) => {
|
|
if (res.data && res.data.length > 0) {
|
|
this.products = [...this.products, ...res.data]
|
|
if (res.data.length < this.pageSize || this.page >= this.totalPages) {
|
|
this.noMoreData = true
|
|
}
|
|
} else {
|
|
this.noMoreData = true
|
|
}
|
|
this.loadingMore = false
|
|
})
|
|
.catch(() => {
|
|
this.page--
|
|
this.loadingMore = false
|
|
uni.showToast({
|
|
title: "加载失败",
|
|
icon: "none",
|
|
})
|
|
})
|
|
},
|
|
getProductList() {
|
|
this.loading = true
|
|
return new Promise((resolve, reject) => {
|
|
getProductListApi({
|
|
page: this.page,
|
|
size: this.pageSize,
|
|
title: this.title,
|
|
category_id: this.categoryId,
|
|
})
|
|
.then((res) => {
|
|
if (this.page === 1) {
|
|
this.products = res.data || []
|
|
} else {
|
|
this.products = [...this.products, ...(res.data || [])]
|
|
}
|
|
if (!res.data || res.data.length < this.pageSize) {
|
|
this.noMoreData = true
|
|
}
|
|
if (res.last_page) {
|
|
this.totalPages = res.last_page
|
|
}
|
|
this.loading = false
|
|
resolve(res)
|
|
})
|
|
.catch((err) => {
|
|
this.loading = false
|
|
uni.showToast({
|
|
title: "加载失败",
|
|
icon: "none",
|
|
})
|
|
reject(err)
|
|
})
|
|
})
|
|
},
|
|
imageLoaded(index) {
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.container {
|
|
min-height: 100vh;
|
|
background-color: #F7F8FA;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.header-section {
|
|
background: #fff;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.02);
|
|
}
|
|
|
|
/* 独立的搜索行样式 */
|
|
.search-bar-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.search-input-box {
|
|
flex: 1;
|
|
}
|
|
|
|
.custom-search-btn {
|
|
padding: 12rpx 30rpx;
|
|
background: #B4854D;
|
|
color: #fff;
|
|
font-size: 28rpx;
|
|
border-radius: 40rpx;
|
|
font-weight: 500;
|
|
box-shadow: 0 4rpx 10rpx rgba(180, 133, 77, 0.3);
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.btn-hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.tabs-wrapper {
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.category-tabs {
|
|
white-space: nowrap;
|
|
padding: 0 10rpx;
|
|
height: 90rpx;
|
|
}
|
|
|
|
.tab-item {
|
|
display: inline-block;
|
|
padding: 0 30rpx;
|
|
height: 90rpx;
|
|
line-height: 90rpx;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.tab-text {
|
|
font-size: 28rpx;
|
|
color: #666;
|
|
font-weight: 400;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab-item.active .tab-text {
|
|
color: #B4854D;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.active-dot {
|
|
position: absolute;
|
|
bottom: 16rpx;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 8rpx;
|
|
height: 8rpx;
|
|
background-color: #B4854D;
|
|
border-radius: 50%;
|
|
box-shadow: 0 2rpx 4rpx rgba(180, 133, 77, 0.4);
|
|
}
|
|
|
|
/* 瀑布流布局优化 */
|
|
.waterfall-container {
|
|
display: flex;
|
|
padding: 24rpx;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.waterfall-column {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.waterfall-column:first-child {
|
|
padding-right: 12rpx;
|
|
}
|
|
|
|
.waterfall-column:last-child {
|
|
padding-left: 12rpx;
|
|
}
|
|
|
|
.product-item {
|
|
background-color: #ffffff;
|
|
border-radius: 16rpx;
|
|
overflow: hidden;
|
|
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.03);
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.product-item:active {
|
|
transform: scale(0.98);
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
.product-image {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
.info-box {
|
|
padding: 24rpx 20rpx;
|
|
}
|
|
|
|
.product-code {
|
|
display: block;
|
|
font-size: 28rpx;
|
|
color: #333333;
|
|
line-height: 1.4;
|
|
font-weight: 500;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 动画 Keyframes */
|
|
@keyframes fadeInUp {
|
|
from { opacity: 0; transform: translateY(40rpx); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 0.6; background: #eee; }
|
|
50% { opacity: 1; background: #ddd; }
|
|
100% { opacity: 0.6; background: #eee; }
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@keyframes float {
|
|
0% { transform: translateY(0); }
|
|
50% { transform: translateY(-10rpx); }
|
|
100% { transform: translateY(0); }
|
|
}
|
|
|
|
.animate-card-enter {
|
|
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
|
|
}
|
|
|
|
.animate-float {
|
|
animation: float 3s ease-in-out infinite;
|
|
}
|
|
|
|
/* 骨架屏样式 */
|
|
.skeleton-card {
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
overflow: hidden;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.skeleton-img {
|
|
width: 100%;
|
|
height: 240rpx;
|
|
background: #eee;
|
|
}
|
|
|
|
.skeleton-text {
|
|
height: 30rpx;
|
|
margin: 20rpx;
|
|
width: 60%;
|
|
background: #eee;
|
|
border-radius: 8rpx;
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 1.5s infinite ease-in-out;
|
|
}
|
|
|
|
/* 底部状态 */
|
|
.loading-more {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 30rpx 0;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 30rpx;
|
|
height: 30rpx;
|
|
border: 4rpx solid #ddd;
|
|
border-top-color: #B4854D;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
.no-more-data {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40rpx 0;
|
|
color: #ccc;
|
|
font-size: 22rpx;
|
|
letter-spacing: 2rpx;
|
|
}
|
|
|
|
.divider-line {
|
|
width: 40rpx;
|
|
height: 2rpx;
|
|
background: #eee;
|
|
margin: 0 16rpx;
|
|
}
|
|
|
|
/* 空状态 */
|
|
.empty-container {
|
|
padding-top: 200rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.empty-image {
|
|
width: 240rpx;
|
|
height: 240rpx;
|
|
opacity: 0.8;
|
|
}
|
|
</style> |