954 lines
25 KiB
Vue
954 lines
25 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="bg-decoration"></view>
|
||
|
||
<!-- 自定义 tabBar -->
|
||
<!-- #ifdef MP-TOUTIAO -->
|
||
<customTabBarToutiao />
|
||
<!-- #endif -->
|
||
<!-- #ifndef MP-TOUTIAO -->
|
||
<customTabBar />
|
||
<!-- #endif -->
|
||
|
||
<!-- [NEW] 全新左右布局布局容器 -->
|
||
<view class="shop-layout">
|
||
|
||
<!-- 左侧边栏 - 极致轻盈设计 -->
|
||
<view class="sidebar glass-effect">
|
||
<scroll-view scroll-y class="sidebar-scroll">
|
||
<view class="sidebar-list">
|
||
<view
|
||
class="sidebar-item"
|
||
:class="{ active: selectedCategoryId === 0 }"
|
||
@tap="onCategorySelect(0)"
|
||
>
|
||
<view class="indicator" v-if="selectedCategoryId === 0"></view>
|
||
<text class="item-text">全部</text>
|
||
</view>
|
||
<view
|
||
v-for="cat in categories"
|
||
:key="cat.id"
|
||
class="sidebar-item"
|
||
:class="{ active: selectedCategoryId === cat.id }"
|
||
@tap="onCategorySelect(cat.id)"
|
||
>
|
||
<view class="indicator" v-if="selectedCategoryId === cat.id"></view>
|
||
<text class="item-text">{{ cat.name }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 右侧主内容区域 -->
|
||
<view class="main-content">
|
||
<!-- 顶部搜索浮层 -->
|
||
<view class="top-nav glass-effect">
|
||
<view class="search-wrap">
|
||
<view class="search-bar">
|
||
<text class="search-icon">🔍</text>
|
||
<input class="search-input" v-model="keyword" placeholder="搜好物" @confirm="onSearchConfirm" />
|
||
<view class="filter-btn" @tap="togglePriceFilter">
|
||
<text class="filter-icon">🔽</text>
|
||
</view>
|
||
</view>
|
||
<!-- 频道切换 (商品/优惠券) -->
|
||
<view class="tab-pill">
|
||
<view
|
||
class="tab-pill-item"
|
||
v-for="tab in tabs" :key="tab.id"
|
||
:class="{ active: currentTab === tab.id }"
|
||
@tap="switchTab(tab.id)"
|
||
>
|
||
{{ tab.name }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 价格区间筛选面板 (独立浮层) -->
|
||
<view class="price-filter-panel" :class="{ expanded: showPriceFilter }">
|
||
<view class="filter-row">
|
||
<view class="price-input-group">
|
||
<input class="price-input" v-model="priceMin" type="digit" placeholder="最低价" />
|
||
<text class="price-separator">-</text>
|
||
<input class="price-input" v-model="priceMax" type="digit" placeholder="最高价" />
|
||
</view>
|
||
<view class="filter-actions">
|
||
<view class="filter-btn-reset" @tap="resetPriceFilter">重置</view>
|
||
<view class="filter-btn-confirm" @tap="applyPriceFilter">确定</view>
|
||
</view>
|
||
</view>
|
||
<!-- 快捷价格区间 -->
|
||
<view class="quick-price-ranges">
|
||
<view
|
||
v-for="range in priceRanges"
|
||
:key="range.key"
|
||
class="price-range-tag"
|
||
:class="{ active: isRangeActive(range.key) }"
|
||
@tap="selectQuickPrice(range)"
|
||
>
|
||
{{ range.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容滚动容器 (带缩放动画) -->
|
||
<scroll-view
|
||
scroll-y
|
||
class="content-scroll-area"
|
||
@scrolltolower="handleScrollToLower"
|
||
>
|
||
<view
|
||
class="transition-container"
|
||
:class="{ switching: isSwitching }"
|
||
@animationend="onAnimationEnd"
|
||
>
|
||
<!-- 商品 Grid (保持原有逻辑,优化视觉) -->
|
||
<view v-if="currentTab === 'product'" class="products-grid">
|
||
<view class="product-item" v-for="p in items" :key="p.id" @tap="onProductTap(p)">
|
||
<view class="product-card">
|
||
<view class="thumb-wrap">
|
||
<image class="product-thumb" :src="p.image" mode="aspectFill" lazy-load="true" />
|
||
<view class="stock-tag" v-if="p.stock !== null && p.stock < 10 && p.stock > 0">仅剩{{p.stock}}</view>
|
||
<view class="stock-tag out" v-else-if="p.stock === 0">售罄</view>
|
||
</view>
|
||
<view class="product-info">
|
||
<text class="product-title">{{ p.title }}</text>
|
||
<view class="product-bottom">
|
||
<view class="price-row">
|
||
<text class="points-val">{{ p.points }}</text>
|
||
<text class="points-unit">积分</text>
|
||
</view>
|
||
<view class="redeem-btn-sm" v-if="p.stock > 0" @tap.stop="onRedeemTap(p)">+</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 优惠券视图 -->
|
||
<view v-else-if="currentTab === 'coupon'" class="coupons-compact-list">
|
||
<view class="coupon-compact-card" v-for="c in items" :key="c.id">
|
||
<view class="c-val-box">
|
||
<text class="c-val">¥{{ (c.discount_value || 0) / 100 }}</text>
|
||
</view>
|
||
<view class="c-info-box">
|
||
<text class="c-title">{{ c.title }}</text>
|
||
<text class="c-price">{{ c.points }} 积分</text>
|
||
</view>
|
||
<view class="c-btn" @tap="onRedeemTap(c)">兑换</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 道具卡视图 (后期开放) -->
|
||
<view v-else-if="currentTab === 'item_card'" class="item-cards-list">
|
||
<view class="empty-state" style="padding-top: 50rpx;">
|
||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||
<text class="empty-text">暂未开放</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!items.length && !loading" class="empty-mini">
|
||
<image class="empty-mini-img" src="/static/empty.png" mode="widthFix" />
|
||
<text>空空如也</text>
|
||
</view>
|
||
|
||
<view v-if="loading" class="mini-loading">
|
||
<view class="pulse"></view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||
import { ref, watch, onUnmounted } from 'vue'
|
||
import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints, getProductCategories } from '../../api/appUser'
|
||
import { checkPhoneBound, checkPhoneBoundSync } from '../../utils/checkPhone.js'
|
||
import { vibrateShort } from '@/utils/vibrate.js'
|
||
// #ifdef MP-TOUTIAO
|
||
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
|
||
// #endif
|
||
// #ifndef MP-TOUTIAO
|
||
import customTabBar from '@/components/app-tab-bar.vue'
|
||
// #endif
|
||
|
||
// 由于是 setup 语法,组件会自动注册,无需手动声明
|
||
|
||
const loading = ref(false)
|
||
const keyword = ref('')
|
||
const currentTab = ref('product')
|
||
const items = ref([])
|
||
const allItems = ref([])
|
||
const page = ref(1)
|
||
const pageSize = 20
|
||
const hasMore = ref(true)
|
||
|
||
// 分类相关
|
||
const categories = ref([])
|
||
const selectedCategoryId = ref(0)
|
||
|
||
// 标记是否已经初始化加载过数据
|
||
let hasInitialized = false
|
||
|
||
// 趣味加载文字数组
|
||
const loadingTexts = [
|
||
'请稍等,正在努力加载中。。。',
|
||
'精彩马上就来,请稍候片刻~',
|
||
'正在搜寻更多好物,别急哦~',
|
||
'加载中,好东西值得等待~',
|
||
'正在为您准备惊喜,马上就好~',
|
||
'小憩一下,精彩内容即将呈现~',
|
||
'努力加载中,比心❤️~',
|
||
'正在赶来,马上就到~',
|
||
'稍安勿躁,美好值得等待~',
|
||
'加载进度99%... 开个玩笑😄'
|
||
]
|
||
const loadingText = ref(loadingTexts[0])
|
||
const isSwitching = ref(false)
|
||
let loadingTextInterval = null
|
||
|
||
// 开始切换加载文字
|
||
function startLoadingTextRotation() {
|
||
if (loadingTextInterval) return
|
||
let index = 0
|
||
loadingText.value = loadingTexts[index]
|
||
loadingTextInterval = setInterval(() => {
|
||
index = (index + 1) % loadingTexts.length
|
||
loadingText.value = loadingTexts[index]
|
||
}, 2000) // 每2秒切换一次
|
||
}
|
||
|
||
// 停止切换加载文字
|
||
function stopLoadingTextRotation() {
|
||
if (loadingTextInterval) {
|
||
clearInterval(loadingTextInterval)
|
||
loadingTextInterval = null
|
||
}
|
||
}
|
||
|
||
// 监听 loading 状态,自动启停文字轮播
|
||
watch(loading, (newVal) => {
|
||
if (newVal) {
|
||
startLoadingTextRotation()
|
||
} else {
|
||
stopLoadingTextRotation()
|
||
}
|
||
})
|
||
|
||
// 价格筛选相关
|
||
const priceMin = ref('')
|
||
const priceMax = ref('')
|
||
const showPriceFilter = ref(false)
|
||
const priceRanges = [
|
||
{ key: 'all', label: '全部', min: null, max: null },
|
||
{ key: '0-100', label: '0-100', min: 0, max: 100 },
|
||
{ key: '100-500', label: '100-500', min: 100, max: 500 },
|
||
{ key: '500-1000', label: '500-1K', min: 500, max: 1000 },
|
||
{ key: '1000-5000', label: '1K-5K', min: 1000, max: 5000 },
|
||
{ key: '5000+', label: '5K+', min: 5000, max: null }
|
||
]
|
||
const activePriceRange = ref('all')
|
||
|
||
const tabs = [
|
||
{ id: 'product', name: '商品' },
|
||
{ id: 'coupon', name: '优惠券' }
|
||
// { id: 'item_card', name: '道具卡' } // 暂不开放
|
||
]
|
||
|
||
function cleanUrl(u) {
|
||
const s = String(u || '').trim()
|
||
const m = s.match(/https?:\/\/[^\s'"`]+/)
|
||
if (m && m[0]) return m[0]
|
||
return s.replace(/[`'\"]/g, '').trim()
|
||
}
|
||
|
||
function normalizeItems(list, kind) {
|
||
if (!Array.isArray(list)) return []
|
||
|
||
// 格式化积分显示 - 不四舍五入,保留两位小数
|
||
const formatPoints = (value) => {
|
||
if (value === undefined || value === null) return '0.00'
|
||
const num = Number(value)
|
||
if (isNaN(num)) return '0.00'
|
||
|
||
// 价格字段单位是分,如 1250 = 12.50积分
|
||
// 除以100得到显示值
|
||
const finalValue = num / 100
|
||
|
||
// 使用 Math.floor 避免四舍五入,保留两位小数
|
||
return String(Math.floor(finalValue * 100) / 100).replace(/(\.\d)$/, '$10')
|
||
}
|
||
|
||
return list.map((i, idx) => ({
|
||
id: i.id,
|
||
kind: i.kind || kind,
|
||
image: cleanUrl(i.main_image || i.image || ''),
|
||
title: i.name || i.title || '',
|
||
price: i.price || i.discount_value || 0,
|
||
points: formatPoints(i.price || i.discount_value),
|
||
stock: i.in_stock ? 99 : 0, // Simplified stock check if returned as bool
|
||
discount_value: i.discount_value || 0,
|
||
min_spend: i.min_spend || 0,
|
||
description: i.description || ''
|
||
})).filter(i => i.title)
|
||
}
|
||
|
||
function switchTab(id) {
|
||
if (currentTab.value === id) return
|
||
vibrateShort()
|
||
currentTab.value = id
|
||
triggerSwitching()
|
||
items.value = []
|
||
allItems.value = []
|
||
page.value = 1
|
||
hasMore.value = true
|
||
loadItems()
|
||
}
|
||
|
||
function triggerSwitching() {
|
||
isSwitching.value = true
|
||
}
|
||
|
||
function onAnimationEnd() {
|
||
isSwitching.value = false
|
||
}
|
||
|
||
async function loadItems(append = false) {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
// 构建筛选参数
|
||
const filters = {}
|
||
if (keyword.value && keyword.value.trim()) {
|
||
filters.keyword = keyword.value.trim()
|
||
}
|
||
if (priceMin.value !== '' && priceMin.value !== null) {
|
||
filters.price_min = priceMin.value
|
||
}
|
||
if (priceMax.value !== '' && priceMax.value !== null) {
|
||
filters.price_max = priceMax.value
|
||
}
|
||
// 添加分类筛选
|
||
if (selectedCategoryId.value > 0) {
|
||
filters.category_id = selectedCategoryId.value
|
||
}
|
||
|
||
const res = await getStoreItems(currentTab.value, page.value, pageSize, filters)
|
||
const list = res.list || res || []
|
||
const total = res.total || 0
|
||
const newItems = normalizeItems(list, currentTab.value)
|
||
|
||
if (append) {
|
||
allItems.value = [...allItems.value, ...newItems]
|
||
} else {
|
||
allItems.value = newItems
|
||
}
|
||
|
||
// 直接使用服务端返回的数据(已经过筛选)
|
||
items.value = allItems.value
|
||
|
||
// 检查是否还有更多数据
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
hasMore.value = page.value < totalPages
|
||
} catch (e) {
|
||
console.error(e)
|
||
hasMore.value = false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function applyFilters() {
|
||
const k = String(keyword.value || '').trim().toLowerCase()
|
||
const minPrice = priceMin.value !== '' ? parseFloat(priceMin.value) : null
|
||
const maxPrice = priceMax.value !== '' ? parseFloat(priceMax.value) : null
|
||
|
||
items.value = allItems.value.filter(item => {
|
||
// 关键词筛选
|
||
const title = String(item.title || '').toLowerCase()
|
||
const matchKeyword = !k || title.includes(k)
|
||
|
||
// 价格筛选
|
||
const itemPrice = item.points || 0
|
||
let matchPrice = true
|
||
if (minPrice !== null) {
|
||
matchPrice = matchPrice && itemPrice >= minPrice
|
||
}
|
||
if (maxPrice !== null) {
|
||
matchPrice = matchPrice && itemPrice <= maxPrice
|
||
}
|
||
|
||
return matchKeyword && matchPrice
|
||
})
|
||
}
|
||
|
||
function applyPriceFilter() {
|
||
activePriceRange.value = 'custom'
|
||
// 重新加载数据(服务端筛选)
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
// 关闭筛选面板
|
||
showPriceFilter.value = false
|
||
}
|
||
|
||
function resetPriceFilter() {
|
||
priceMin.value = ''
|
||
priceMax.value = ''
|
||
activePriceRange.value = 'all'
|
||
// 重新加载数据
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
// 关闭筛选面板
|
||
showPriceFilter.value = false
|
||
}
|
||
|
||
function selectQuickPrice(range) {
|
||
vibrateShort()
|
||
activePriceRange.value = range.key
|
||
if (range.min !== null) {
|
||
priceMin.value = range.min.toString()
|
||
} else {
|
||
priceMin.value = ''
|
||
}
|
||
if (range.max !== null) {
|
||
priceMax.value = range.max.toString()
|
||
} else {
|
||
priceMax.value = ''
|
||
}
|
||
// 重新加载数据
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
// 关闭筛选面板
|
||
showPriceFilter.value = false
|
||
}
|
||
|
||
function isRangeActive(range) {
|
||
return activePriceRange.value === range.key
|
||
}
|
||
|
||
// 切换价格筛选面板显示/隐藏
|
||
function togglePriceFilter() {
|
||
vibrateShort()
|
||
showPriceFilter.value = !showPriceFilter.value
|
||
}
|
||
|
||
function onSearchConfirm() {
|
||
// 搜索时重新加载数据
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
}
|
||
|
||
function onProductTap(p) {
|
||
if (p.kind === 'product') {
|
||
uni.navigateTo({ url: `/pages-shop/shop/detail?id=${p.id}` })
|
||
}
|
||
}
|
||
|
||
async function onRedeemTap(item) {
|
||
// 检查商品库存
|
||
if (item.kind === 'product' && item.stock === 0) {
|
||
uni.showModal({
|
||
title: '商品已售罄',
|
||
content: '该商品库存不足,请联系客服处理',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
const token = uni.getStorageSync('token')
|
||
if (!token) {
|
||
uni.showModal({
|
||
title: '温馨提示',
|
||
content: '兑换商品需要先登录哦',
|
||
confirmText: '去登录',
|
||
cancelText: '暂不登录',
|
||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '确认兑换',
|
||
content: `是否消耗 ${item.points} 积分兑换 ${item.title}?`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
uni.showLoading({ title: '兑换中...' })
|
||
try {
|
||
const userId = uni.getStorageSync('user_id')
|
||
if (!userId) throw new Error('用户ID不存在')
|
||
|
||
if (item.kind === 'product') {
|
||
await redeemProductByPoints(userId, item.id, 1)
|
||
} else if (item.kind === 'coupon') {
|
||
await redeemCouponByPoints(userId, item.id)
|
||
} else if (item.kind === 'item_card') {
|
||
await redeemItemCardByPoints(userId, item.id, 1)
|
||
}
|
||
|
||
uni.hideLoading()
|
||
uni.showModal({
|
||
title: '兑换成功',
|
||
content: `您已成功兑换 ${item.title || item.name}`,
|
||
showCancel: false,
|
||
success: () => {
|
||
loadItems()
|
||
}
|
||
})
|
||
} catch (e) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 获取分类列表
|
||
async function fetchCategories() {
|
||
try {
|
||
const res = await getProductCategories()
|
||
// 兼容可能存在的不同数据结构
|
||
if (res && res.items) {
|
||
categories.value = res.items
|
||
} else if (Array.isArray(res)) {
|
||
categories.value = res
|
||
} else {
|
||
categories.value = []
|
||
}
|
||
} catch (e) {
|
||
console.error('获取分类失败:', e)
|
||
}
|
||
}
|
||
|
||
// 选择分类
|
||
function onCategorySelect(id) {
|
||
if (selectedCategoryId.value === id) return
|
||
vibrateShort()
|
||
selectedCategoryId.value = id
|
||
triggerSwitching()
|
||
page.value = 1
|
||
hasMore.value = true
|
||
items.value = []
|
||
loadItems()
|
||
}
|
||
|
||
onShow(() => {
|
||
// 检查手机号绑定状态(快速检查本地缓存)
|
||
if (!checkPhoneBoundSync()) return
|
||
|
||
const token = uni.getStorageSync('token')
|
||
if (token) {
|
||
// 只在首次加载时初始化数据,避免从详情页返回时重复加载
|
||
if (!hasInitialized) {
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
fetchCategories()
|
||
hasInitialized = true
|
||
}
|
||
}
|
||
})
|
||
|
||
onReachBottom(() => {
|
||
// 触底加载更多(页面级,scroll-view 内不触发)
|
||
handleScrollToLower()
|
||
})
|
||
|
||
// scroll-view 触底加载
|
||
function handleScrollToLower() {
|
||
if (hasMore.value && !loading.value) {
|
||
page.value++
|
||
loadItems(true)
|
||
}
|
||
}
|
||
|
||
watch(keyword, () => applyFilters())
|
||
|
||
// 组件卸载时清理定时器
|
||
onUnmounted(() => {
|
||
stopLoadingTextRotation()
|
||
})
|
||
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
height: 100vh;
|
||
background-color: #f7f8fa;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.shop-layout {
|
||
display: flex;
|
||
height: 100vh;
|
||
width: 100%;
|
||
}
|
||
|
||
.glass-effect {
|
||
background: rgba(255, 255, 255, 0.7);
|
||
backdrop-filter: blur(30rpx);
|
||
-webkit-backdrop-filter: blur(30rpx);
|
||
}
|
||
|
||
/* 侧边栏 */
|
||
.sidebar {
|
||
width: 160rpx;
|
||
height: 100vh;
|
||
border-right: 1rpx solid rgba(0, 0, 0, 0.05);
|
||
z-index: 10;
|
||
}
|
||
.sidebar-scroll { height: 100%; }
|
||
.sidebar-list { padding: 40rpx 0; }
|
||
.sidebar-item {
|
||
height: 100rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
|
||
.item-text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
&.active {
|
||
.item-text {
|
||
color: $brand-primary;
|
||
font-weight: 700;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
}
|
||
.indicator {
|
||
position: absolute;
|
||
left: 0;
|
||
width: 8rpx;
|
||
height: 32rpx;
|
||
background: $gradient-brand;
|
||
border-radius: 0 4rpx 4rpx 0;
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
position: relative;
|
||
overflow: visible;
|
||
}
|
||
|
||
.top-nav {
|
||
padding: 20rpx 24rpx;
|
||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.03);
|
||
position: relative;
|
||
z-index: 50;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(30rpx);
|
||
-webkit-backdrop-filter: blur(30rpx);
|
||
flex-shrink: 0;
|
||
}
|
||
.search-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
.search-bar {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
}
|
||
.search-icon { font-size: 24rpx; opacity: 0.4; }
|
||
.search-input { flex: 1; margin-left: 12rpx; font-size: 24rpx; }
|
||
|
||
/* 筛选按钮 */
|
||
.filter-btn {
|
||
margin-left: 12rpx;
|
||
padding: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.filter-icon {
|
||
font-size: 20rpx;
|
||
opacity: 0.5;
|
||
transition: transform 0.3s;
|
||
}
|
||
|
||
/* 价格筛选面板 */
|
||
.price-filter-panel {
|
||
position: absolute;
|
||
top: 120rpx;
|
||
left: 24rpx;
|
||
right: 24rpx;
|
||
padding: 20rpx;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
max-height: 0;
|
||
opacity: 0;
|
||
transition: all 0.3s ease-out;
|
||
z-index: 100;
|
||
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.12);
|
||
pointer-events: none;
|
||
|
||
&.expanded {
|
||
max-height: 500rpx;
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.price-input-group {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(0, 0, 0, 0.03);
|
||
border-radius: 12rpx;
|
||
padding: 0 16rpx;
|
||
height: 60rpx;
|
||
}
|
||
|
||
.price-input {
|
||
flex: 1;
|
||
height: 100%;
|
||
font-size: 24rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.price-separator {
|
||
margin: 0 12rpx;
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.filter-actions {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.filter-btn-reset,
|
||
.filter-btn-confirm {
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 24rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-btn-reset {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
color: #666;
|
||
}
|
||
|
||
.filter-btn-confirm {
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
}
|
||
|
||
/* 快捷价格区间 */
|
||
.quick-price-ranges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.price-range-tag {
|
||
padding: 10rpx 20rpx;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: 20rpx;
|
||
font-size: 22rpx;
|
||
color: #666;
|
||
transition: all 0.2s;
|
||
|
||
&.active {
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.95);
|
||
}
|
||
}
|
||
|
||
.tab-pill {
|
||
display: flex;
|
||
background: rgba(0, 0, 0, 0.03);
|
||
padding: 4rpx;
|
||
border-radius: 30rpx;
|
||
}
|
||
.tab-pill-item {
|
||
padding: 8rpx 20rpx;
|
||
font-size: 22rpx;
|
||
color: #888;
|
||
border-radius: 26rpx;
|
||
&.active {
|
||
background: #fff;
|
||
color: #333;
|
||
font-weight: 600;
|
||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||
}
|
||
}
|
||
|
||
.content-scroll-area {
|
||
flex: 1;
|
||
height: 0; /* 关键:在 flex 布局中需要设置为 0 才能正确计算高度 */
|
||
background: #fcfcfc;
|
||
overflow: hidden;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 缩放动画的核心部分 */
|
||
.transition-container {
|
||
padding: 24rpx;
|
||
min-height: 100%;
|
||
&.switching {
|
||
animation: zoomInOut 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
}
|
||
|
||
@keyframes zoomInOut {
|
||
0% { transform: scale(1); opacity: 1; }
|
||
45% { transform: scale(0.95); opacity: 0.7; }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
|
||
/* [NEW] 真正瀑布流布局 (Masonry) */
|
||
.products-grid {
|
||
column-count: 2;
|
||
column-gap: 20rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.product-item {
|
||
break-inside: avoid;
|
||
margin-bottom: 20rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.product-card {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||
border: 1rpx solid rgba(0,0,0,0.02);
|
||
transition: transform 0.2s;
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
/* 创造高度落差感 */
|
||
.product-item:nth-child(2n) .thumb-wrap {
|
||
padding-top: 120%; /* 偶数项更高,产生交错效果 */
|
||
}
|
||
|
||
.thumb-wrap {
|
||
width: 100%;
|
||
padding-top: 100%;
|
||
position: relative;
|
||
background: #fafafa;
|
||
overflow: hidden;
|
||
}
|
||
.thumb-wrap .product-thumb {
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
/* 库存标签 */
|
||
.stock-tag {
|
||
position: absolute;
|
||
top: 8rpx;
|
||
right: 8rpx;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 20rpx;
|
||
color: #fff;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
backdrop-filter: blur(4rpx);
|
||
-webkit-backdrop-filter: blur(4rpx);
|
||
z-index: 2;
|
||
|
||
&.out {
|
||
background: rgba(255, 59, 48, 0.9);
|
||
}
|
||
}
|
||
|
||
.product-thumb { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||
.product-info { padding: 16rpx; }
|
||
.product-title {
|
||
font-size: 24rpx; color: #333; line-height: 1.4; height: 2.8em;
|
||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||
}
|
||
.product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 12rpx; }
|
||
.points-val { font-size: 28rpx; color: $brand-primary; font-weight: 700; }
|
||
.points-unit { font-size: 18rpx; color: $brand-primary; margin-left: 2rpx; }
|
||
.redeem-btn-sm {
|
||
width: 44rpx; height: 44rpx; background: $gradient-brand;
|
||
color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||
font-weight: 700; font-size: 28rpx;
|
||
}
|
||
|
||
/* 紧凑型优惠券列表 */
|
||
.coupons-compact-list { display: flex; flex-direction: column; gap: 20rpx; }
|
||
.coupon-compact-card {
|
||
background: #fff; border-radius: 16rpx; padding: 24rpx;
|
||
display: flex; align-items: center; border: 1rpx solid rgba(0,0,0,0.03);
|
||
}
|
||
.c-val-box { width: 100rpx; font-weight: 800; color: #FF6B6B; }
|
||
.c-info-box { flex: 1; }
|
||
.c-title { display: block; font-size: 24rpx; font-weight: 600; color: #333; }
|
||
.c-price { font-size: 20rpx; color: #999; }
|
||
.c-btn { background: #333; color: #fff; font-size: 22rpx; padding: 6rpx 20rpx; border-radius: 10rpx; }
|
||
|
||
.empty-mini {
|
||
padding-top: 100rpx;
|
||
text-align: center;
|
||
color: #ccc;
|
||
font-size: 24rpx;
|
||
}
|
||
.empty-mini-img {
|
||
width: 160rpx;
|
||
display: block;
|
||
margin: 0 auto 10rpx;
|
||
}
|
||
.mini-loading { padding-top: 60rpx; display: flex; justify-content: center; }
|
||
.pulse { width: 40rpx; height: 40rpx; background: $brand-primary; border-radius: 50%; opacity: 0.3; animation: pulse 1s infinite; }
|
||
@keyframes pulse { from { transform: scale(1); opacity: 0.3; } to { transform: scale(2); opacity: 0; } }
|
||
|
||
.no-more {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
color: $text-tertiary;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
</style>
|