feat(inde): 首页改版

This commit is contained in:
woody 2025-06-03 16:40:49 +08:00
parent 5c52892f34
commit 2f1a31d349
5 changed files with 801 additions and 250 deletions

View File

@ -1,5 +1,5 @@
<template>
<view class="simple-vertical-swiper" :style="{ height: height + 'px' }">
<view class="simple-vertical-swiper" :style="{ height: height + 'rpx' }">
<view
class="swiper-wrapper"
@touchstart.stop="onTouchStart"
@ -7,10 +7,10 @@
@touchend.stop="onTouchEnd"
>
<view
v-for="(item, index) in items"
v-for="(item, index) in displayItems"
:key="index"
class="swiper-slide"
:class="{ 'swiper-slide-active': index === currentIndex }"
:class="{ 'swiper-slide-active': index === virtualCurrentIndex }"
:style="getSlideStyle(index)"
>
<!-- 公告类型 -->
@ -44,7 +44,7 @@
</view>
<!-- 自定义插槽 -->
<slot v-else :item="item" :index="index">
<slot v-else :item="item" :index="getOriginalIndex(index)">
<view class="default-item">
<text>{{ item.text || item.content || item }}</text>
</view>
@ -55,10 +55,10 @@
<!-- 指示器 -->
<view v-if="showIndicators && items.length > 1" class="indicators">
<view
v-for="(item, index) in items"
:key="index"
v-for="(_item, originalItemIndex) in items"
:key="originalItemIndex"
class="indicator"
:class="{ active: index === currentIndex }"
:class="{ active: originalItemIndex === currentIndex }"
></view>
</view>
</view>
@ -111,100 +111,145 @@ export default {
},
data() {
return {
currentIndex: 0,
currentIndex: 0, // Index in original items array
virtualCurrentIndex: 0, // Index in displayItems array
timer: null,
isTransitioning: false,
//
isJumping: false, // Flag for instant jumps, to disable CSS transition
touchStartY: 0,
touchStartTime: 0,
isTouching: false,
}
},
computed: {
itemsLength() {
originalItemsLength() {
return this.items.length
},
displayItems() {
if (!this.items || this.originalItemsLength === 0) {
return []
}
if (!this.circular) {
return this.items
}
if (this.originalItemsLength === 1) {
return [this.items[0], this.items[0], this.items[0]]
}
const lastItem = this.items[this.originalItemsLength - 1]
const firstItem = this.items[0]
return [lastItem, ...this.items, firstItem]
},
},
watch: {
items: {
handler() {
this.initSwiper()
this.initSwiperState()
},
immediate: true,
},
autoplay(newVal) {
if (this.originalItemsLength === 0) return
if (newVal) {
this.startAutoplay()
} else {
this.clearTimer()
}
},
circular() {
this.initSwiperState()
},
virtualCurrentIndex() {
// This watcher can be used for debugging or complex state reactions if needed
},
},
mounted() {
this.initSwiper()
// Initialization is handled by immediate watch on items
},
beforeDestroy() {
this.clearTimer()
},
// uniapp
onReady() {
this.initSwiper()
this.initSwiperState()
},
onUnload() {
this.clearTimer()
},
methods: {
// -
getSlideStyle(index) {
if (this.itemsLength <= 1) {
return {
transform: 'translateY(0px)',
opacity: 1,
zIndex: 1,
}
getOriginalIndex(displayIndex) {
if (this.originalItemsLength === 0) return 0
if (!this.circular) {
return displayIndex
}
if (this.originalItemsLength === 1) {
return 0
}
if (displayIndex === 0) return this.originalItemsLength - 1
if (displayIndex === this.displayItems.length - 1) return 0
if (displayIndex > 0 && displayIndex <= this.originalItemsLength) {
return displayIndex - 1
}
return 0 // Fallback, should ideally not be reached with correct logic
},
initSwiperState() {
this.clearTimer()
if (this.originalItemsLength === 0) {
this.currentIndex = 0
this.virtualCurrentIndex = 0
if (this.autoplay) this.startAutoplay() // Try to start autoplay even if no items initially
return
}
let translateY = 0
if (this.circular) {
if (this.originalItemsLength === 1) {
this.virtualCurrentIndex = 1
this.currentIndex = 0
} else {
this.virtualCurrentIndex = 1
this.currentIndex = 0
}
} else {
this.virtualCurrentIndex = 0
this.currentIndex = 0
}
// Ensure DOM is updated if items change, then start autoplay
this.$nextTick(() => {
if (this.autoplay) {
this.startAutoplay()
}
})
},
getSlideStyle(indexInDisplayItems) {
const offset = indexInDisplayItems - this.virtualCurrentIndex
const translateY = offset * this.height
let opacity = 0.6
let zIndex = 1
if (index === this.currentIndex) {
//
translateY = 0
if (offset === 0) {
// Active slide
opacity = 1
zIndex = 10
} else if (Math.abs(offset) === 1) {
// Adjacent slides
opacity = 0.3
zIndex = 5
} else {
//
let offset = index - this.currentIndex
//
if (this.circular && this.itemsLength > 2) {
const halfLength = this.itemsLength / 2
//
if (offset > halfLength) {
offset = offset - this.itemsLength //
} else if (offset < -halfLength) {
offset = offset + this.itemsLength //
}
}
translateY = offset * this.height
//
const absOffset = Math.abs(offset)
if (absOffset <= 1) {
opacity = absOffset === 1 ? 0.3 : 0.6
zIndex = absOffset === 1 ? 5 : 1
} else {
opacity = 0
zIndex = 0
}
// Far away slides
opacity = 0
zIndex = 0
}
const transition =
this.isTransitioning && !this.isTouching
? `all ${this.duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
(this.isTransitioning || this.isJumping) &&
!this.isTouching &&
!(this.isJumping && !this.isTransitioning) // Allow transition if only isTransitioning is true
? this.isJumping
? 'none'
: `all ${this.duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
: 'none'
return {
@ -214,168 +259,196 @@ export default {
transition: transition,
}
},
//
initSwiper() {
console.log(
'初始化轮播, items数量:',
this.itemsLength,
'autoplay:',
this.autoplay
)
if (this.itemsLength === 0) return
this.currentIndex = 0
if (this.autoplay && this.itemsLength > 1) {
this.startAutoplay()
}
},
//
startAutoplay() {
this.clearTimer()
if (this.autoplay && this.itemsLength > 1) {
console.log('开始自动轮播,间隔:', this.interval)
// Condition for autoplay: must have items, and if not circular, must have more than 1 item.
const canAutoplay =
this.originalItemsLength > 0 &&
(this.circular || this.originalItemsLength > 1)
if (this.autoplay && canAutoplay) {
this.timer = setInterval(() => {
this.next()
}, this.interval)
}
},
//
clearTimer() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
updateCurrentIndex(currentVirtualIdx, isMidJump = false) {
const oldOriginalIndex = this.currentIndex
let newOriginalIndex = 0
//
if (this.originalItemsLength === 0) {
this.currentIndex = 0
return
}
if (!this.circular) {
newOriginalIndex = currentVirtualIdx
} else if (this.originalItemsLength === 1) {
newOriginalIndex = 0
} else {
if (currentVirtualIdx === 0) {
newOriginalIndex = this.originalItemsLength - 1
} else if (currentVirtualIdx === this.displayItems.length - 1) {
newOriginalIndex = 0
} else {
newOriginalIndex = currentVirtualIdx - 1
}
}
this.currentIndex = newOriginalIndex
// Emit change only if it's not a mid-jump correction that results in the same original index visually
// Or if the original index actually changed.
if (oldOriginalIndex !== newOriginalIndex || !isMidJump) {
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}
},
moveTo(targetVirtualIndex, direction) {
// direction can be 'next' or 'prev', used to emit correct original index during transition
if (this.isTransitioning && !this.isJumping) return
if (this.originalItemsLength === 0) return
this.isTransitioning = true
this.virtualCurrentIndex = targetVirtualIndex
// Emit change based on where we are going (even if it's a clone)
this.updateCurrentIndex(targetVirtualIndex)
setTimeout(() => {
this.isTransitioning = false
if (this.circular) {
this.checkAndCorrectLoopBoundary()
}
}, this.duration)
},
checkAndCorrectLoopBoundary() {
if (this.originalItemsLength <= 1 && this.circular) {
// Special handling for 1 item circular
// displayItems = [item, item, item], virtualCurrentIndex can be 0, 1, 2
if (this.virtualCurrentIndex !== 1) {
this.isJumping = true
this.virtualCurrentIndex = 1 // Jump to the middle "real" one
this.updateCurrentIndex(1, true)
this.$nextTick(() => {
this.isJumping = false
})
}
return
}
if (this.originalItemsLength < 2 || !this.circular) return // Only for 2+ items circular
const firstRealItemVirtualIdx = 1
const lastRealItemVirtualIdx = this.originalItemsLength
let jumpToVirtualIndex = -1
if (this.virtualCurrentIndex === 0) {
jumpToVirtualIndex = lastRealItemVirtualIdx
} else if (this.virtualCurrentIndex === this.displayItems.length - 1) {
jumpToVirtualIndex = firstRealItemVirtualIdx
}
if (jumpToVirtualIndex !== -1) {
this.isJumping = true
this.virtualCurrentIndex = jumpToVirtualIndex
this.updateCurrentIndex(jumpToVirtualIndex, true) // Update original index reflecting the jump destination
this.$nextTick(() => {
this.isJumping = false
})
}
},
next() {
if (this.isTransitioning || this.itemsLength <= 1) return
if (this.originalItemsLength === 0) return
if (
!this.circular &&
this.virtualCurrentIndex >= this.originalItemsLength - 1
)
return
console.log('切换到下一页,当前:', this.currentIndex)
this.isTransitioning = true
const nextIndex = this.circular
? (this.currentIndex + 1) % this.itemsLength
: Math.min(this.currentIndex + 1, this.itemsLength - 1)
this.currentIndex = nextIndex
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
let targetVirtualIndex = this.virtualCurrentIndex + 1
this.moveTo(targetVirtualIndex, 'next')
},
//
prev() {
if (this.isTransitioning || this.itemsLength <= 1) return
if (this.originalItemsLength === 0) return
if (!this.circular && this.virtualCurrentIndex <= 0) return
this.isTransitioning = true
const prevIndex = this.circular
? (this.currentIndex - 1 + this.itemsLength) % this.itemsLength
: Math.max(this.currentIndex - 1, 0)
this.currentIndex = prevIndex
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
let targetVirtualIndex = this.virtualCurrentIndex - 1
this.moveTo(targetVirtualIndex, 'prev')
},
goToSlide(originalIndex) {
if (originalIndex < 0 || originalIndex >= this.originalItemsLength) return
// if (originalIndex === this.currentIndex && !this.isTransitioning && !this.isJumping) return;
//
goToSlide(index) {
if (index === this.currentIndex || this.isTransitioning) return
if (index < 0 || index >= this.itemsLength) return
this.isTransitioning = true
this.currentIndex = index
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
let targetVirtualIndex
if (!this.circular) {
targetVirtualIndex = originalIndex
} else {
if (this.originalItemsLength === 1) targetVirtualIndex = 1
else targetVirtualIndex = originalIndex + 1
}
this.clearTimer()
this.moveTo(targetVirtualIndex)
if (this.autoplay) {
setTimeout(() => this.startAutoplay(), this.duration + 100)
}
},
//
onTouchStart(e) {
if (this.itemsLength <= 1) return
if (this.originalItemsLength === 0) return
if (!this.circular && this.originalItemsLength <= 1) return // No swipe for single non-circular item
console.log('触摸开始')
this.clearTimer() //
this.clearTimer()
this.touchStartY = e.touches[0].clientY
this.touchStartTime = Date.now()
this.isTouching = true
this.isJumping = false
},
//
onTouchMove(e) {
if (this.isTransitioning || !this.isTouching || this.itemsLength <= 1)
return
const deltaY = e.touches[0].clientY - this.touchStartY
console.log('触摸移动, deltaY:', deltaY)
//
//
if (!this.isTouching) return
},
//
onTouchEnd(e) {
if (!this.isTouching || this.itemsLength <= 1) return
if (!this.isTouching || this.originalItemsLength === 0) {
this.isTouching = false // Ensure isTouching is reset
return
}
this.isTouching = false
const currentY = e.changedTouches[0].clientY
const deltaY = currentY - this.touchStartY
const deltaTime = Date.now() - this.touchStartTime
const velocity = Math.abs(deltaY) / deltaTime
console.log('触摸结束, deltaY:', deltaY, 'velocity:', velocity)
//
const threshold = this.height * 0.25 //
const threshold = this.height * 0.2
const velocityThreshold = 0.3
const shouldSwitch =
Math.abs(deltaY) > threshold || velocity > velocityThreshold
const velocity = deltaTime > 0 ? Math.abs(deltaY) / deltaTime : 0
if (shouldSwitch) {
if (deltaY > 0) {
//
console.log('手势触发:上一页')
this.prev()
} else {
//
console.log('手势触发:下一页')
if (Math.abs(deltaY) > threshold || velocity > velocityThreshold) {
if (deltaY < 0) {
this.next()
} else if (deltaY > 0) {
this.prev()
}
} else {
console.log('手势未达到切换阈值,不切换')
}
//
setTimeout(() => {
// No swipe, or too small. Potentially restart autoplay if it was interrupted.
if (this.autoplay) {
this.startAutoplay()
}
}, 1000)
}
// Autoplay restart is handled within next/prev through moveTo or if no swipe occurs
// If a swipe occurs, next/prev call moveTo, which stops autoplay via clearTimer. Then this onTouchEnd may restart.
// If no swipe, this onTouchEnd directly restarts.
// Consider a more unified autoplay restart logic after touch interaction concludes.
// For now, let's consolidate: if autoplay was on, restart it after a delay.
if (this.autoplay) {
setTimeout(() => {
if (!this.timer) this.startAutoplay() // Restart only if not already restarted by a quick succession
}, this.duration + 200) // Delay after potential transition
}
},
},
}
@ -396,7 +469,7 @@ export default {
width: 100%;
height: 100%;
// uniapp App
touch-action: none;
touch-action: pan-y; /* More specific touch action */
//
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;

View File

@ -20,3 +20,7 @@ export const pickLogList = params =>
export const getBanners = params =>
http.get('/system/api/banner/list', { params })
// 首页商品列表
export const getIndexGoodsList = params =>
http.get('/sale/api/order/index-wares-list', { params })

View File

@ -30,7 +30,7 @@
<simple-vertical-swiper
:items="noticeList"
type="custom"
:height="40"
:height="80"
:autoplay="true"
:interval="3000"
:show-indicators="false"
@ -46,80 +46,104 @@
</template>
</simple-vertical-swiper>
</view>
<view class="goods-sort">
<view class="goods-flexs">
<view v-for="(item, index) in recommendSpecialAreaList" :key="index">
<view
class="goods-view"
@click="navTap(item)"
v-if="item.waresList && (index < 8 || moreFlag == true)"
>
<area-product-list
:list="item.waresList"
:title="item.specialAreaName"
size="small"
/>
<view class="recommend-goods-list">
<view
v-if="isEmpty(recommendGoodsList) == false"
class="recommend-section"
>
<view class="recommend-header">
<view class="recommend-title">
<!-- <text class="title-icon"></text> -->
<text class="title-text">精选推荐</text>
</view>
</view>
</view>
<view v-if="isEmpty(goodsList.recommendSpecialAreaList) == false">
<view
class="more"
@click="more"
v-if="!moreFlag && goodsList.recommendSpecialAreaList.length > 6"
>
{{ '查看更多' }}
<view class="recommend-swiper-container">
<simple-vertical-swiper
:items="recommendGoodsList"
type="custom"
:height="450"
autoplay
:interval="4000"
:show-indicators="false"
>
<template #default="{ item }">
<view @click="goDetails(item)" class="recommend-item">
<view class="recommend-item-image">
<image :src="item.cover" mode="aspectFill"></image>
<view class="recommend-badge">推荐</view>
</view>
<view class="recommend-item-content">
<view class="recommend-item-name">{{
item.waresName
}}</view>
<view class="recommend-item-price">
<text class="price-symbol" v-if="priceSymbolVisible">{{
priceSymbol
}}</text>
<text class="price-value">{{
formatCurrency(item.waresPrice)
}}</text>
</view>
</view>
<view class="recommend-arrow"></view>
</view>
</template>
</simple-vertical-swiper>
</view>
</view>
<view class="goods_content">
<view
class="goods-center-lists"
v-for="item in goodsList.waresVoList"
:key="item.waresId"
@click="goDetails(item)"
>
</view>
<!-- New Product List Section Wrapper -->
<view class="hot-sellers-card-wrapper">
<view
class="main-product-list-section"
v-if="displayedGoodsList.length > 0"
>
<!-- <view class="section-title">热销甄选</view> -->
<view class="product-grid">
<view
class="fly"
v-show="item.preSaleStatus == 3 || item.isSale == 1"
></view>
<view class="goods-flex-s">
<view class="goods-img">
<image :src="item.cover1"></image>
</view>
<view class="padding_s goods-info">
<view class="goods-name">{{ item.waresName }}</view>
<view class="goods-sales-wrapper">
<!-- <view class="goods-sales">累计销量{{ formatSales(item.sales) }}</view> -->
<view
class="goods-price"
v-if="item.specialArea == 31 && userInfo.isMakerSpace == 1"
>
class="product-card"
v-for="item in displayedGoodsList"
:key="item.waresId"
@click="goDetails(item)"
>
<view
class="fly"
v-show="item.preSaleStatus == 3 || item.isSale == 1"
></view>
<image
class="product-image"
:src="item.cover"
mode="aspectFill"
></image>
<view class="product-info">
<text class="product-name">{{ item.waresName }}</text>
<text class="product-tagline"></text>
<view class="product-price-row">
<view class="product-price">
<span v-if="priceSymbolVisible" class="price-symbol">
{{ priceSymbol }}
</span>
<span>{{ formatCurrency(item.vipPrice) }}</span>
</view>
<view class="goods-price" v-if="item.specialArea != 31">
<span v-if="priceSymbolVisible" class="price-symbol">
{{ priceSymbol }}
</span>
<span>{{ formatCurrency(item.waresPrice) }}</span>
</view>
<view
class="goods-price"
v-if="item.specialArea == 31 && userInfo.isMakerSpace == 0"
>
<span v-if="priceSymbolVisible" class="price-symbol">
{{ priceSymbol }}
</span>
<span>{{ formatCurrency(item.waresPrice) }}</span>
<span
v-if="
item.specialArea == 31 && userInfo.isMakerSpace == 1
"
>{{ formatCurrency(item.vipPrice) }}</span
>
<span v-else>{{ formatCurrency(item.waresPrice) }}</span>
</view>
<view class="buy-now-button">立即购买</view>
</view>
</view>
</view>
</view>
<view class="load-more-container" v-if="hasMoreGoods">
<button @click="loadMoreGoods" class="load-more-button">
加载更多
</button>
</view>
</view>
</view>
<!-- End of New Product List Section Wrapper -->
<cl-tabbar :current="0"></cl-tabbar>
<div>
<!-- 公告弹窗 -->
@ -204,10 +228,15 @@ export default {
interval: 5000,
autoplay: true,
duration: 500,
goodsList: [],
recommendSpecialAreaList: [],
banners: [],
noticeList: [],
recommendGoodsList: [],
goodsList: [],
displayedGoodsList: [],
goodsListPage: 1,
goodsListPageSize: 4,
hasMoreGoods: true,
zoneList: [
{
label: '注册专区',
@ -403,10 +432,11 @@ export default {
uni.getStorageSync('showInfo') == 0
) {
}
this.getGoodsInfo()
this.getAreaGoods()
// this.getAreaGoods()
this.getBanners()
this.getNoticeList()
this.getRecommendGoodsList()
this.getGoodsList()
// this.getLanguage();
this.getService()
},
@ -416,7 +446,6 @@ export default {
onPullDownRefresh() {
let that = this
setTimeout(() => {
that.getGoodsInfo()
uni.stopPullDownRefresh() //
}, 1000)
},
@ -440,10 +469,8 @@ export default {
})
},
goNotice(item) {
console.log(item, '....item')
return
uni.navigateTo({
url: '/pages/notice/index?id=' + item.id,
url: '/pages/email/noticeDetail?index=1&pkId=' + item.pkId,
})
},
goAreaUrl() {
@ -470,11 +497,44 @@ export default {
}
})
},
getAreaGoods() {
getAreaGoods().then(res => {
this.recommendSpecialAreaList = res.data?.recommendSpecialAreaList || []
getRecommendGoodsList() {
indexApi.getIndexGoodsList({ isRecommend: 0 }).then(res => {
if (res.code == 200) {
this.recommendGoodsList = res.rows || []
}
})
},
getGoodsList() {
indexApi.getIndexGoodsList().then(res => {
if (res.code == 200) {
this.goodsList = res.rows || []
this.goodsListPage = 1
this.displayedGoodsList = []
this.loadMoreGoods()
}
})
},
loadMoreGoods() {
if (!this.goodsList || this.goodsList.length === 0) {
this.hasMoreGoods = false
return
}
const start = (this.goodsListPage - 1) * this.goodsListPageSize
const end = start + this.goodsListPageSize
const newGoods = this.goodsList.slice(start, end)
if (newGoods.length > 0) {
this.displayedGoodsList = [...this.displayedGoodsList, ...newGoods]
this.goodsListPage++
}
if (this.displayedGoodsList.length >= this.goodsList.length) {
this.hasMoreGoods = false
} else {
this.hasMoreGoods = true
}
},
toDel() {
this.promptFlag = false
if (this.jumpPage == 1) {
@ -555,11 +615,6 @@ export default {
window.location.href = urls
}
},
getGoodsInfo() {
indexApi.userIndex().then(res => {
this.goodsList = res.data
})
},
goUrl(item) {
ban.agreementName().then(res => {
@ -671,6 +726,8 @@ export default {
.content_a {
overflow: auto;
height: 100%;
padding-bottom: 120rpx;
box-sizing: border-box;
}
}
.content1 {
@ -680,6 +737,8 @@ export default {
.content_a {
overflow: auto;
height: 100%;
padding-bottom: 120rpx;
box-sizing: border-box;
}
}
.more {
@ -1204,4 +1263,367 @@ page {
transform: rotate(360deg);
}
}
.recommend-header {
padding: 0rpx;
.recommend-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.title-icon {
font-size: 28rpx;
// margin-right: 12rpx;
color: #ff6b6b;
}
.title-text {
color: #333;
font-size: 28rpx;
font-weight: bold;
}
}
.recommend-subtitle {
color: #999;
font-size: 24rpx;
margin-left: 40rpx;
}
}
.recommend-swiper-container {
background: #fff;
}
.recommend-item {
display: block;
padding: 8px 0;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease;
width: 100%;
}
.recommend-item:last-child {
border-bottom: none;
}
.recommend-item:active {
background-color: #f8f9fa;
}
.recommend-item-image {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 6px;
overflow: hidden;
margin-right: 0;
margin-bottom: 10px;
position: relative;
background-color: #f0f0f0;
}
.recommend-item-image image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.recommend-badge {
position: absolute;
top: 0px;
right: 0px;
background-color: #de3932;
color: #ffffff;
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
z-index: 1;
}
.recommend-item-content {
padding: 0 4px;
}
.recommend-item-name {
color: #333333;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recommend-item-price {
display: flex;
align-items: baseline;
}
.recommend-item-price .price-symbol {
color: #de3932;
font-size: 12px;
margin-right: 2px;
}
.recommend-item-price .price-value {
color: #de3932;
font-size: 16px;
font-weight: bold;
}
.recommend-arrow {
display: none;
}
// Styles for New Product List
.main-product-list-section {
margin: 20rpx;
padding-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
padding: 18rpx 24rpx;
text-align: left;
// background: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
}
.product-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.product-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
position: relative;
.product-image {
display: block;
width: 100%;
height: 200px;
object-fit: cover;
background-color: #f0f0f0;
}
.product-info {
padding: 12px;
display: flex;
flex-direction: column;
flex-grow: 1;
background-color: #f7f8fa;
}
.product-name {
font-size: 15px;
color: #2d3748;
font-weight: 600;
line-height: 1.4;
margin-bottom: 6px;
min-height: calc(15px * 1.4 * 2);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.product-tagline {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
line-height: 1.3;
}
.product-price-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.product-price {
color: #de3932;
font-size: 32rpx;
font-weight: bold;
.price-symbol {
font-size: 22rpx;
margin-right: 2rpx;
}
}
.buy-now-button {
background-color: transparent;
color: #005bac;
border: 1px solid #005bac;
padding: 7px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-align: center;
transition:
background-color 0.2s,
color 0.2s;
}
.buy-now-button:active {
background-color: #e6f0fa;
color: #005bac;
}
}
.load-more-container {
margin-top: 30rpx;
text-align: center;
.load-more-button {
background-color: #fff;
color: #333;
border: 1px solid #eee;
padding: 16rpx 40rpx;
font-size: 28rpx;
border-radius: 40rpx;
display: inline-block;
transition: background-color 0.2s;
&:active {
background-color: #f0f0f0;
}
}
}
.hot-sellers-card-wrapper {
margin-top: 20rpx;
background: #fff;
// border-radius: 16rpx;
overflow: hidden;
// box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
/* --- 精选推荐模块样式 --- */
/* 模块容器 */
.recommend-section {
background-color: #ffffff;
padding: 20rpx;
}
/* 模块标题 */
.recommend-title {
display: flex;
align-items: center;
margin-bottom: 0;
}
.recommend-title .title-icon {
font-size: 16px;
color: #005bac;
margin-right: 6px;
}
.recommend-title .title-text {
color: #222222;
font-size: 18px;
font-weight: 600;
}
/* 副标题 (如果存在且需要调整) */
.recommend-subtitle {
color: #777777;
font-size: 13px;
margin-left: 0;
padding-left: 22px;
}
/* 单个推荐商品项 */
.recommend-item {
display: block;
padding: 8px 0;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease;
}
.recommend-item:last-child {
border-bottom: none;
}
.recommend-item:active {
background-color: #f8f9fa;
}
.recommend-item-image {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 6px;
overflow: hidden;
margin-right: 0;
margin-bottom: 10px;
position: relative;
background-color: #f0f0f0;
}
.recommend-item-image image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.recommend-badge {
position: absolute;
top: 0px;
right: 0px;
background-color: #de3932;
color: #ffffff;
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
z-index: 1;
}
.recommend-item-content {
padding: 0 4px;
}
.recommend-item-name {
color: #333333;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recommend-item-price {
display: flex;
align-items: baseline;
}
.recommend-item-price .price-symbol {
color: #de3932;
font-size: 12px;
margin-right: 2px;
}
.recommend-item-price .price-value {
color: #de3932;
font-size: 16px;
font-weight: bold;
}
.recommend-arrow {
display: none;
}
/* 如果 simple-vertical-swiper 的高度需要适配内容 */
.recommend-swiper-container {
/* simple-vertical-swiper :height="140" (rpx/px?)
那么内部 recommend-item 的数量和padding需要仔细计算以避免显示问题
这里假设 simple-vertical-swiper 内部可以正确处理其子项的布局
如果swiper高度导致内容显示不全可能需要调整swiper的height参数
或者调整recommend-item的padding/margin
*/
}
</style>

View File

@ -1,18 +1,56 @@
<template>
<view>
<view> 123321 </view>
<AreaWrapper v-if="!loading" :areaSpecialAreaList="areaSpecialAreaList" />
<cl-tabbar :current="1"></cl-tabbar>
</view>
</template>
<script>
import clTabbar from '@/components/cl-tabbar.vue'
import AreaWrapper from '@/components/area-wrapper/index.vue'
import { REPEAT_PURCHASE, RESCISSION } from '@/util/specialAreaMap'
import { mapActions } from 'vuex'
export default {
name: 'Mall',
components: {
'cl-tabbar': clTabbar,
AreaWrapper,
},
computed: {},
data() {
return {
areaSpecialAreaList: [],
visible: false,
loading: true,
}
},
onLoad(options) {
this.init()
},
onShow() {},
onHide() {},
beforeDestroy() {},
methods: {
...mapActions(['getSpecialAreaAuth']),
init() {
uni.showLoading({
title: '加载中...',
})
this.getSpecialAreaAuth()
.then(specialAreaList => {
if (specialAreaList.includes(REPEAT_PURCHASE.menuKey)) {
this.areaSpecialAreaList.push(REPEAT_PURCHASE)
}
if (specialAreaList.includes(RESCISSION.menuKey)) {
this.areaSpecialAreaList.push(RESCISSION)
}
})
.finally(() => {
this.loading = false
this.$nextTick(() => {
uni.hideLoading()
})
})
},
},
}
</script>
<style></style>

View File

@ -11,6 +11,20 @@ export const UPGRADE = {
specialArea: 2,
menuKey: 'upgrade',
}
// 复购专区
export const REPEAT_PURCHASE = {
label: '复购专区',
specialArea: 3,
menuKey: 'repurchase',
}
// 重消专区
export const RESCISSION = {
label: '重消专区',
specialArea: 10,
menuKey: 'rescission',
}
export const specialAreaMap = {
regiest: REGIEST,
upgrade: UPGRADE,