827 lines
23 KiB
Vue
827 lines
23 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="bg-decoration"></view>
|
||
|
||
<!-- 顶部固定区域 -->
|
||
<view class="header-section">
|
||
<view class="search-box">
|
||
<view class="search-input-wrap">
|
||
<text class="search-icon">🔍</text>
|
||
<input class="search-input" v-model="keyword" placeholder="搜索心仪的宝贝" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 价格筛选 -->
|
||
<view class="filter-section">
|
||
<view class="filter-row">
|
||
<text class="filter-label">价格区间:</text>
|
||
<view class="price-inputs">
|
||
<input
|
||
class="price-input"
|
||
type="number"
|
||
v-model="priceMin"
|
||
placeholder="最低"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
<text class="price-separator">-</text>
|
||
<input
|
||
class="price-input"
|
||
type="number"
|
||
v-model="priceMax"
|
||
placeholder="最高"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="filter-btn" @tap="applyPriceFilter">筛选</view>
|
||
<view class="filter-btn reset" @tap="resetPriceFilter" v-if="priceMin || priceMax">重置</view>
|
||
</view>
|
||
<view class="quick-prices">
|
||
<view
|
||
v-for="range in priceRanges"
|
||
:key="range.key"
|
||
class="quick-price-tag"
|
||
:class="{ active: isRangeActive(range) }"
|
||
@tap="selectQuickPrice(range)"
|
||
>
|
||
{{ range.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分类标签 -->
|
||
<view class="tab-row">
|
||
<view
|
||
v-for="tab in tabs"
|
||
:key="tab.id"
|
||
class="tab-item"
|
||
:class="{ active: currentTab === tab.id }"
|
||
@tap="switchTab(tab.id)"
|
||
>
|
||
<text class="tab-text">{{ tab.name }}</text>
|
||
<view class="tab-line" v-if="currentTab === tab.id"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 占位 -->
|
||
<view class="header-placeholder"></view>
|
||
|
||
<view v-if="loading && !items.length" class="loading-container">
|
||
<view class="loading-animation">
|
||
<view class="circle circle-1"></view>
|
||
<view class="circle circle-2"></view>
|
||
<view class="circle circle-3"></view>
|
||
<view class="circle circle-4"></view>
|
||
</view>
|
||
<text class="loading-text">{{ loadingText }}</text>
|
||
</view>
|
||
|
||
<view class="content-container" v-else>
|
||
<!-- 商品视图 (网格) -->
|
||
<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 || 0 }}</text>
|
||
<text class="points-unit">积分</text>
|
||
</view>
|
||
<view
|
||
class="redeem-btn"
|
||
:class="{ disabled: p.stock === 0 }"
|
||
@tap.stop="onRedeemTap(p)"
|
||
>
|
||
{{ p.stock === 0 ? '已售罄' : '兑换' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 优惠券视图 (票据样式列表) -->
|
||
<view v-else-if="currentTab === 'coupon'" class="coupons-list">
|
||
<view class="coupon-card" v-for="c in items" :key="c.id">
|
||
<view class="coupon-left">
|
||
<view class="coupon-val-row">
|
||
<text class="coupon-symbol">¥</text>
|
||
<text class="coupon-val">{{ (c.discount_value || 0) / 100 }}</text>
|
||
</view>
|
||
<text class="coupon-limit" v-if="c.min_spend > 0">满{{ (c.min_spend || 0) / 100 }}可用</text>
|
||
<text class="coupon-limit" v-else>无门槛</text>
|
||
</view>
|
||
<view class="coupon-divider">
|
||
<view class="notch top"></view>
|
||
<view class="dash"></view>
|
||
<view class="notch bottom"></view>
|
||
</view>
|
||
<view class="coupon-right">
|
||
<view class="coupon-name">{{ c.title || c.name }}</view>
|
||
<view class="coupon-bottom">
|
||
<view class="coupon-price">
|
||
<text class="p-val">{{ c.points || 0 }}</text>
|
||
<text class="p-unit">积分</text>
|
||
</view>
|
||
<view class="coupon-btn" @tap="onRedeemTap(c)">立即兑换</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 道具卡视图 (列表卡片) -->
|
||
<view v-else-if="currentTab === 'item_card'" class="item-cards-list">
|
||
<!-- TODO: 后续我们要打开的 -->
|
||
<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 && currentTab !== 'item_card'" class="empty-state">
|
||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||
<text class="empty-text">暂无相关兑换项</text>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view v-if="loading && items.length > 0" class="loading-more">
|
||
<view class="spinner"></view>
|
||
<text>{{ loadingText }}</text>
|
||
</view>
|
||
<view v-else-if="!hasMore && items.length > 0" class="no-more">- 到底啦 -</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||
import { ref, watch, onUnmounted } from 'vue'
|
||
import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints } from '../../api/appUser'
|
||
import { checkPhoneBound, checkPhoneBoundSync } from '../../utils/checkPhone.js'
|
||
|
||
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 loadingTexts = [
|
||
'请稍等,正在努力加载中。。。',
|
||
'精彩马上就来,请稍候片刻~',
|
||
'正在搜寻更多好物,别急哦~',
|
||
'加载中,好东西值得等待~',
|
||
'正在为您准备惊喜,马上就好~',
|
||
'小憩一下,精彩内容即将呈现~',
|
||
'努力加载中,比心❤️~',
|
||
'正在赶来,马上就到~',
|
||
'稍安勿躁,美好值得等待~',
|
||
'加载进度99%... 开个玩笑😄'
|
||
]
|
||
const loadingText = ref(loadingTexts[0])
|
||
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 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 []
|
||
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: i.points_required ? (i.points_required / 100).toFixed(1) : (i.price ? (i.price / 100).toFixed(1) : (i.discount_value ? (i.discount_value / 100).toFixed(1) : '0.0')),
|
||
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
|
||
currentTab.value = id
|
||
items.value = []
|
||
allItems.value = []
|
||
page.value = 1
|
||
hasMore.value = true
|
||
loadItems()
|
||
}
|
||
|
||
async function loadItems(append = false) {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
try {
|
||
const res = await getStoreItems(currentTab.value, page.value, pageSize)
|
||
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
|
||
}
|
||
|
||
// 根据返回的total计算总页数
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
|
||
// 如果当前页小于总页数,继续加载下一页
|
||
if (page.value < totalPages) {
|
||
page.value++
|
||
// 递归加载下一页
|
||
loading.value = false // 临时释放loading状态,允许递归调用
|
||
await loadItems(true)
|
||
} else {
|
||
// 所有数据加载完成
|
||
hasMore.value = false
|
||
}
|
||
|
||
applyFilters()
|
||
} 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'
|
||
applyFilters()
|
||
}
|
||
|
||
function resetPriceFilter() {
|
||
priceMin.value = ''
|
||
priceMax.value = ''
|
||
activePriceRange.value = 'all'
|
||
applyFilters()
|
||
}
|
||
|
||
function selectQuickPrice(range) {
|
||
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 = ''
|
||
}
|
||
applyFilters()
|
||
}
|
||
|
||
function isRangeActive(range) {
|
||
return activePriceRange.value === range.key
|
||
}
|
||
|
||
function onSearchConfirm() { applyFilters() }
|
||
|
||
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: '去登录',
|
||
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' })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
onShow(() => {
|
||
// 检查手机号绑定状态(快速检查本地缓存)
|
||
if (!checkPhoneBoundSync()) return
|
||
|
||
const token = uni.getStorageSync('token')
|
||
if (token) {
|
||
page.value = 1
|
||
hasMore.value = true
|
||
allItems.value = []
|
||
loadItems()
|
||
}
|
||
})
|
||
|
||
onReachBottom(() => {
|
||
// 所有数据已在页面加载时一次性请求完成,无需触底加载
|
||
})
|
||
|
||
watch(keyword, () => applyFilters())
|
||
|
||
// 组件卸载时清理定时器
|
||
onUnmounted(() => {
|
||
stopLoadingTextRotation()
|
||
})
|
||
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background-color: $bg-page;
|
||
padding-bottom: 40rpx;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 顶部 Header */
|
||
.header-section {
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0;
|
||
z-index: 100;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(20rpx);
|
||
padding: 20rpx 24rpx 0;
|
||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
|
||
}
|
||
.header-placeholder { height: 340rpx; }
|
||
|
||
.search-box { margin-bottom: 20rpx; }
|
||
.search-input-wrap {
|
||
display: flex; align-items: center;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: $radius-round;
|
||
padding: 16rpx 24rpx;
|
||
}
|
||
.search-icon { font-size: 28rpx; margin-right: 16rpx; opacity: 0.5; }
|
||
.search-input { flex: 1; font-size: 26rpx; color: $text-main; }
|
||
.placeholder-style { color: $text-tertiary; }
|
||
|
||
/* 价格筛选 */
|
||
.filter-section {
|
||
margin-bottom: 16rpx;
|
||
padding: 16rpx;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
border-radius: $radius-md;
|
||
}
|
||
.filter-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.filter-label {
|
||
font-size: 24rpx;
|
||
color: $text-secondary;
|
||
white-space: nowrap;
|
||
margin-right: 12rpx;
|
||
}
|
||
.price-inputs {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
.price-input {
|
||
flex: 1;
|
||
height: 56rpx;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: $radius-sm;
|
||
padding: 0 16rpx;
|
||
font-size: 24rpx;
|
||
color: $text-main;
|
||
text-align: center;
|
||
}
|
||
.input-placeholder { color: $text-tertiary; }
|
||
.price-separator {
|
||
font-size: 24rpx;
|
||
color: $text-tertiary;
|
||
padding: 0 4rpx;
|
||
}
|
||
.filter-btn {
|
||
padding: 12rpx 20rpx;
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
border-radius: $radius-round;
|
||
margin-left: 12rpx;
|
||
white-space: nowrap;
|
||
&.reset {
|
||
background: rgba(0, 0, 0, 0.06);
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
.quick-prices {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
.quick-price-tag {
|
||
padding: 8rpx 20rpx;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: $radius-round;
|
||
font-size: 22rpx;
|
||
color: $text-secondary;
|
||
transition: all 0.2s;
|
||
&.active {
|
||
background: $gradient-brand;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
/* Tabs */
|
||
.tab-row {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
padding-bottom: 4rpx;
|
||
}
|
||
.tab-item {
|
||
position: relative;
|
||
padding: 16rpx 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.tab-text {
|
||
font-size: 28rpx;
|
||
color: $text-secondary;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.tab-item.active .tab-text {
|
||
color: $brand-primary;
|
||
font-weight: 700;
|
||
font-size: 30rpx;
|
||
}
|
||
.tab-line {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 40rpx;
|
||
height: 6rpx;
|
||
background: $gradient-brand;
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.content-container { padding: 24rpx; }
|
||
|
||
/* 商品 Grid */
|
||
.products-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 20rpx;
|
||
}
|
||
.product-card {
|
||
background: #fff;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
box-shadow: $shadow-sm;
|
||
}
|
||
.thumb-wrap {
|
||
position: relative; width: 100%; padding-top: 100%;
|
||
background: #f8f8f8;
|
||
}
|
||
.product-thumb { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||
.stock-tag {
|
||
position: absolute; bottom: 0; right: 0;
|
||
background: rgba($brand-primary, 0.9);
|
||
color: #fff; font-size: 20rpx; padding: 4rpx 12rpx;
|
||
border-top-left-radius: 12rpx;
|
||
&.out { background: #999; }
|
||
}
|
||
.product-info { padding: 16rpx; }
|
||
.product-title {
|
||
font-size: 26rpx; color: $text-main; font-weight: 600;
|
||
line-height: 1.4; height: 2.8em; overflow: hidden;
|
||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.product-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||
.points-val { font-size: 32rpx; font-weight: 800; color: #FF9800; font-family: 'DIN-Bold'; }
|
||
.points-unit { font-size: 20rpx; color: #FF9800; margin-left: 2rpx; }
|
||
.redeem-btn {
|
||
background: $gradient-brand; color: #fff; font-size: 22rpx;
|
||
padding: 6rpx 18rpx; border-radius: 24rpx; font-weight: 600;
|
||
&.disabled {
|
||
background: #ccc;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
/* 优惠券 (Ticket Style) */
|
||
.coupons-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||
.coupon-card {
|
||
display: flex;
|
||
background: #fff;
|
||
border-radius: $radius-lg;
|
||
height: 180rpx;
|
||
box-shadow: $shadow-sm;
|
||
overflow: hidden;
|
||
}
|
||
.coupon-left {
|
||
width: 200rpx;
|
||
background: linear-gradient(135deg, #FF6B6B, #FF3B30);
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
color: #fff;
|
||
}
|
||
.coupon-val-row { display: flex; align-items: baseline; }
|
||
.coupon-symbol { font-size: 24rpx; font-weight: 600; }
|
||
.coupon-val { font-size: 56rpx; font-weight: 800; font-family: 'DIN-Bold'; }
|
||
.coupon-limit { font-size: 20rpx; opacity: 0.9; }
|
||
|
||
.coupon-divider {
|
||
width: 2rpx; position: relative;
|
||
background: #eee;
|
||
.notch {
|
||
position: absolute; width: 24rpx; height: 24rpx; background: $bg-page;
|
||
border-radius: 50%; left: -12rpx;
|
||
&.top { top: -12rpx; }
|
||
&.bottom { bottom: -12rpx; }
|
||
}
|
||
}
|
||
.coupon-right {
|
||
flex: 1; padding: 24rpx;
|
||
display: flex; flex-direction: column; justify-content: space-between;
|
||
}
|
||
.coupon-name { font-size: 30rpx; font-weight: 700; color: $text-main; }
|
||
.coupon-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||
.coupon-price .p-val { font-size: 36rpx; font-weight: 800; color: #FF9800; }
|
||
.coupon-price .p-unit { font-size: 22rpx; color: #FF9800; }
|
||
.coupon-btn {
|
||
background: #333; color: #fff; font-size: 24rpx;
|
||
padding: 10rpx 24rpx; border-radius: 30rpx; font-weight: 600;
|
||
}
|
||
|
||
/* 道具卡 (Card Style) */
|
||
.item-cards-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||
.item-card {
|
||
display: flex; background: #fff; border-radius: $radius-lg; padding: 24rpx;
|
||
box-shadow: $shadow-sm; align-items: center;
|
||
}
|
||
.ic-icon-wrap {
|
||
width: 100rpx; height: 100rpx; background: #f0f0f0;
|
||
border-radius: $radius-md; display: flex; align-items: center; justify-content: center;
|
||
font-size: 48rpx; margin-right: 24rpx;
|
||
}
|
||
.ic-info { flex: 1; display: flex; flex-direction: column; }
|
||
.ic-name { font-size: 30rpx; font-weight: 700; color: $text-main; margin-bottom: 4rpx; }
|
||
.ic-desc { font-size: 22rpx; color: $text-tertiary; margin-bottom: 20rpx; }
|
||
.ic-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||
.ic-price .p-val { font-size: 32rpx; font-weight: 800; color: #FF9800; }
|
||
.ic-btn {
|
||
background: $gradient-brand; color: #fff; font-size: 24rpx;
|
||
padding: 8rpx 24rpx; border-radius: 30rpx;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex; flex-direction: column; align-items: center; padding-top: 100rpx;
|
||
}
|
||
.empty-img { width: 240rpx; opacity: 0.6; }
|
||
.empty-text { color: $text-tertiary; font-size: 26rpx; margin-top: 20rpx; }
|
||
|
||
/* 酷炫加载动画 */
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding-top: 150rpx;
|
||
}
|
||
|
||
.loading-animation {
|
||
position: relative;
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.circle {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
border: 4rpx solid transparent;
|
||
}
|
||
|
||
.circle-1 {
|
||
border-top-color: #FF6B6B;
|
||
border-right-color: #FF6B6B;
|
||
animation: rotate 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||
}
|
||
|
||
.circle-2 {
|
||
width: 80%;
|
||
height: 80%;
|
||
top: 10%;
|
||
left: 10%;
|
||
border-top-color: #4ECDC4;
|
||
border-left-color: #4ECDC4;
|
||
animation: rotate 2s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite reverse;
|
||
}
|
||
|
||
.circle-3 {
|
||
width: 60%;
|
||
height: 60%;
|
||
top: 20%;
|
||
left: 20%;
|
||
border-bottom-color: #FFE66D;
|
||
border-left-color: #FFE66D;
|
||
animation: rotate 2.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||
}
|
||
|
||
.circle-4 {
|
||
width: 40%;
|
||
height: 40%;
|
||
top: 30%;
|
||
left: 30%;
|
||
border-top-color: #95E1D3;
|
||
border-bottom-color: #95E1D3;
|
||
animation: rotate 3s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite reverse;
|
||
}
|
||
|
||
@keyframes rotate {
|
||
0% {
|
||
transform: rotate(0deg) scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: rotate(180deg) scale(1.1);
|
||
opacity: 0.8;
|
||
}
|
||
100% {
|
||
transform: rotate(360deg) scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: $text-secondary;
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
opacity: 0.6;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.loading-wrap { padding: 100rpx 0; display: flex; justify-content: center; }
|
||
.spinner {
|
||
width: 48rpx; height: 48rpx; border: 4rpx solid #eee; border-top-color: $brand-primary;
|
||
border-radius: 50%; animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* 加载更多 */
|
||
.loading-more {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 30rpx 0;
|
||
color: $text-tertiary;
|
||
font-size: 24rpx;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.no-more {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
color: $text-tertiary;
|
||
font-size: 24rpx;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
</style>
|