feat(inde): 首页改版
This commit is contained in:
parent
5c52892f34
commit
2f1a31d349
|
@ -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;
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue