This commit is contained in:
邹方成 2026-02-06 18:38:09 +08:00
parent 2a98cde85f
commit e7256ae88e
5 changed files with 440 additions and 495 deletions

View File

@ -188,6 +188,10 @@ export function redeemCouponByPoints(user_id, coupon_id) {
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-coupon`, method: 'POST', data: { coupon_id } }) return authRequest({ url: `/api/app/users/${user_id}/points/redeem-coupon`, method: 'POST', data: { coupon_id } })
} }
export function transferCoupon(user_id, user_coupon_id, receiver_id) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/${user_coupon_id}/transfer`, method: 'POST', data: { receiver_id } })
}
export function redeemCoupon(user_id, code) { export function redeemCoupon(user_id, code) {
return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } }) return authRequest({ url: `/api/app/users/${user_id}/coupons/redeem`, method: 'POST', data: { code } })
} }
@ -243,9 +247,18 @@ export function getStoreItems(kind = 'product', page = 1, page_size = 20, filter
if (filters.price_max !== undefined && filters.price_max !== null && filters.price_max !== '') { if (filters.price_max !== undefined && filters.price_max !== null && filters.price_max !== '') {
data.price_max = parseInt(filters.price_max) data.price_max = parseInt(filters.price_max)
} }
// 添加分类ID筛选
if (filters.category_id !== undefined && filters.category_id !== null && filters.category_id > 0) {
data.category_id = filters.category_id
}
return authRequest({ url: '/api/app/store/items', method: 'GET', data }) return authRequest({ url: '/api/app/store/items', method: 'GET', data })
} }
export function getProductCategories() {
return authRequest({ url: '/api/app/product_categories', method: 'GET' })
}
export function getTasks(page = 1, page_size = 20) { export function getTasks(page = 1, page_size = 20) {
return authRequest({ url: '/api/app/task-center/tasks', method: 'GET', data: { page, page_size } }) return authRequest({ url: '/api/app/task-center/tasks', method: 'GET', data: { page, page_size } })
} }

View File

@ -98,6 +98,7 @@
<!-- 优化后的按钮位置 --> <!-- 优化后的按钮位置 -->
<view class="coupon-action-wrapper" v-if="currentTab === 1"> <view class="coupon-action-wrapper" v-if="currentTab === 1">
<view class="transfer-link" @click.stop="onTransferCoupon(item)">转赠给好友</view>
<view class="use-btn" @click.stop="onUseCoupon(item)"> <view class="use-btn" @click.stop="onUseCoupon(item)">
<text class="btn-text">去使用</text> <text class="btn-text">去使用</text>
<view class="btn-shine"></view> <view class="btn-shine"></view>
@ -123,7 +124,7 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app' import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getUserCoupons } from '../../api/appUser' import { getUserCoupons, transferCoupon } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js' import { vibrateShort } from '@/utils/vibrate.js'
const list = ref([]) const list = ref([])
@ -309,6 +310,43 @@ function onUseCoupon(item) {
// #endif // #endif
} }
//
function onTransferCoupon(item) {
vibrateShort()
uni.showModal({
title: '转赠优惠券',
content: '请输入接收方用户 ID',
editable: true,
placeholderText: '请输入用户ID',
success: async (res) => {
if (res.confirm && res.content) {
const receiverId = parseInt(res.content)
if (isNaN(receiverId)) {
uni.showToast({ title: '请输入有效的用户ID', icon: 'none' })
return
}
uni.showLoading({ title: '转赠中...' })
try {
const userId = getUserId()
await transferCoupon(userId, item.id, receiverId)
uni.hideLoading()
uni.showToast({ title: '转赠成功', icon: 'success' })
//
onRefresh()
} catch (e) {
uni.hideLoading()
uni.showModal({
title: '转赠失败',
content: e.message || '操作失败',
showCancel: false
})
}
}
}
})
}
onLoad(() => { onLoad(() => {
fetchData() fetchData()
}) })
@ -654,6 +692,18 @@ onLoad(() => {
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
z-index: 10; z-index: 10;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 16rpx;
}
.transfer-link {
font-size: 22rpx;
color: $brand-primary;
text-decoration: underline;
opacity: 0.8;
&:active { opacity: 1; }
} }
.use-btn { .use-btn {

View File

@ -102,6 +102,10 @@
<view v-if="isTierClaimed(task.id, tier.id)" class="tier-btn claimed"> <view v-if="isTierClaimed(task.id, tier.id)" class="tier-btn claimed">
<text>已领取</text> <text>已领取</text>
</view> </view>
<!-- 已领完 (限量逻辑) -->
<view v-else-if="tier.quota > 0 && tier.remaining === 0" class="tier-btn disabled">
<text>已领完</text>
</view>
<!-- 可领取 --> <!-- 可领取 -->
<view v-else-if="isTierClaimable(task, tier)" class="tier-btn claimable" @click="claimReward(task, tier)"> <view v-else-if="isTierClaimable(task, tier)" class="tier-btn claimable" @click="claimReward(task, tier)">
<text>{{ claiming[`${task.id}_${tier.id}`] ? '领取中...' : '领取' }}</text> <text>{{ claiming[`${task.id}_${tier.id}`] ? '领取中...' : '领取' }}</text>
@ -109,6 +113,8 @@
<!-- 进度中 --> <!-- 进度中 -->
<view v-else class="tier-progress"> <view v-else class="tier-progress">
<text class="progress-text">{{ getTierProgressText(task, tier) }}</text> <text class="progress-text">{{ getTierProgressText(task, tier) }}</text>
<!-- 限量进度提示 -->
<text class="quota-text" v-if="tier.quota > 0">剩余 {{ tier.remaining }} </text>
</view> </view>
</view> </view>
</view> </view>
@ -793,9 +799,31 @@ onLoad(() => {
color: $brand-primary; color: $brand-primary;
} }
.tier-right { .tier-progress {
flex-shrink: 0; display: flex;
margin-left: 16rpx; flex-direction: column;
align-items: flex-end;
}
.progress-text {
font-size: 24rpx;
color: $text-sub;
font-weight: 600;
}
.quota-text {
font-size: 18rpx;
color: $brand-primary;
margin-top: 4rpx;
opacity: 0.8;
}
.tier-btn {
&.disabled {
background: #f0f0f0;
color: #ccc;
cursor: not-allowed;
}
} }
.tier-btn { .tier-btn {

View File

@ -2,167 +2,125 @@
<view class="page"> <view class="page">
<view class="bg-decoration"></view> <view class="bg-decoration"></view>
<!-- 自定义 tabBar --> <!-- [NEW] 全新左右布局布局容器 -->
<!-- #ifdef MP-TOUTIAO --> <view class="shop-layout">
<customTabBarToutiao />
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO -->
<customTabBar />
<!-- #endif -->
<!-- 顶部固定区域 --> <!-- 左侧边栏 - 极致轻盈设计 -->
<view class="header-section"> <view class="sidebar glass-effect">
<view class="search-box"> <scroll-view scroll-y class="sidebar-scroll">
<view class="search-input-wrap"> <view class="sidebar-list">
<text class="search-icon">🔍</text> <view
<input class="search-input" v-model="keyword" placeholder="搜索心仪的宝贝" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" /> class="sidebar-item"
</view> :class="{ active: selectedCategoryId === 0 }"
</view> @tap="onCategorySelect(0)"
>
<!-- 价格筛选 --> <view class="indicator" v-if="selectedCategoryId === 0"></view>
<view class="filter-section"> <text class="item-text">全部</text>
<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>
<view class="product-info"> <view
<text class="product-title">{{ p.title }}</text> v-for="cat in categories"
<view class="product-bottom"> :key="cat.id"
<view class="price-row"> class="sidebar-item"
<text class="points-val">{{ p.points || 0 }}</text> :class="{ active: selectedCategoryId === cat.id }"
<text class="points-unit">积分</text> @tap="onCategorySelect(cat.id)"
</view> >
<view <view class="indicator" v-if="selectedCategoryId === cat.id"></view>
class="redeem-btn" <text class="item-text">{{ cat.name }}</text>
:class="{ disabled: p.stock === 0 }" </view>
@tap.stop="onRedeemTap(p)" </view>
> </scroll-view>
{{ p.stock === 0 ? '已售罄' : '兑换' }} </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>
<!-- 频道切换 (商品/优惠券) -->
<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> </view>
</view> </view>
</view>
<!-- 优惠券视图 (票据样式列表) --> <!-- 内容滚动容器 (带缩放动画) -->
<view v-else-if="currentTab === 'coupon'" class="coupons-list"> <scroll-view
<view class="coupon-card" v-for="c in items" :key="c.id"> scroll-y
<view class="coupon-left"> class="content-scroll-area"
<view class="coupon-val-row"> @scrolltolower="handleScrollToLower"
<text class="coupon-symbol">¥</text> >
<text class="coupon-val">{{ (c.discount_value || 0) / 100 }}</text> <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" @tap.stop="onRedeemTap(p)">+</view>
</view>
</view>
</view>
</view>
</view> </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 v-else-if="currentTab === 'coupon'" class="coupons-compact-list">
<view class="coupon-divider"> <view class="coupon-compact-card" v-for="c in items" :key="c.id">
<view class="notch top"></view> <view class="c-val-box">
<view class="dash"></view> <text class="c-val">¥{{ (c.discount_value || 0) / 100 }}</text>
<view class="notch bottom"></view> </view>
</view> <view class="c-info-box">
<view class="coupon-right"> <text class="c-title">{{ c.title }}</text>
<view class="coupon-name">{{ c.title || c.name }}</view> <text class="c-price">{{ c.points }} 积分</text>
<view class="coupon-bottom"> </view>
<view class="coupon-price"> <view class="c-btn" @tap="onRedeemTap(c)">兑换</view>
<text class="p-val">{{ c.points || 0 }}</text> </view>
<text class="p-unit">积分</text> </view>
</view>
<view class="coupon-btn" @tap="onRedeemTap(c)">立即兑换</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 src="/static/empty.png" mode="widthFix" />
<text>空空如也</text>
</view>
<view v-if="loading" class="mini-loading">
<view class="pulse"></view>
</view> </view>
</view> </view>
</view> </scroll-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>
</view> </view>
</template> </template>
@ -170,8 +128,9 @@
<script setup> <script setup>
import { onShow, onReachBottom } from '@dcloudio/uni-app' import { onShow, onReachBottom } from '@dcloudio/uni-app'
import { ref, watch, onUnmounted } from 'vue' import { ref, watch, onUnmounted } from 'vue'
import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints } from '../../api/appUser' import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints, getProductCategories } from '../../api/appUser'
import { checkPhoneBound, checkPhoneBoundSync } from '../../utils/checkPhone.js' import { checkPhoneBound, checkPhoneBoundSync } from '../../utils/checkPhone.js'
import { vibrateShort } from '@/utils/vibrate.js'
// #ifdef MP-TOUTIAO // #ifdef MP-TOUTIAO
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue' import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
// #endif // #endif
@ -190,6 +149,10 @@ const page = ref(1)
const pageSize = 20 const pageSize = 20
const hasMore = ref(true) const hasMore = ref(true)
//
const categories = ref([])
const selectedCategoryId = ref(0)
// //
const loadingTexts = [ const loadingTexts = [
'请稍等,正在努力加载中。。。', '请稍等,正在努力加载中。。。',
@ -204,6 +167,7 @@ const loadingTexts = [
'加载进度99%... 开个玩笑😄' '加载进度99%... 开个玩笑😄'
] ]
const loadingText = ref(loadingTexts[0]) const loadingText = ref(loadingTexts[0])
const isSwitching = ref(false)
let loadingTextInterval = null let loadingTextInterval = null
// //
@ -293,7 +257,9 @@ function normalizeItems(list, kind) {
function switchTab(id) { function switchTab(id) {
if (currentTab.value === id) return if (currentTab.value === id) return
vibrateShort()
currentTab.value = id currentTab.value = id
triggerSwitching()
items.value = [] items.value = []
allItems.value = [] allItems.value = []
page.value = 1 page.value = 1
@ -301,6 +267,14 @@ function switchTab(id) {
loadItems() loadItems()
} }
function triggerSwitching() {
isSwitching.value = true
}
function onAnimationEnd() {
isSwitching.value = false
}
async function loadItems(append = false) { async function loadItems(append = false) {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
@ -316,6 +290,10 @@ async function loadItems(append = false) {
if (priceMax.value !== '' && priceMax.value !== null) { if (priceMax.value !== '' && priceMax.value !== null) {
filters.price_max = priceMax.value 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 res = await getStoreItems(currentTab.value, page.value, pageSize, filters)
const list = res.list || res || [] const list = res.list || res || []
@ -481,6 +459,35 @@ async function onRedeemTap(item) {
}) })
} }
//
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(() => { onShow(() => {
// //
if (!checkPhoneBoundSync()) return if (!checkPhoneBoundSync()) return
@ -491,16 +498,22 @@ onShow(() => {
hasMore.value = true hasMore.value = true
allItems.value = [] allItems.value = []
loadItems() loadItems()
fetchCategories()
} }
}) })
onReachBottom(() => { onReachBottom(() => {
// // scroll-view
handleScrollToLower()
})
// scroll-view
function handleScrollToLower() {
if (hasMore.value && !loading.value) { if (hasMore.value && !loading.value) {
page.value++ page.value++
loadItems(true) loadItems(true)
} }
}) }
watch(keyword, () => applyFilters()) watch(keyword, () => applyFilters())
@ -513,370 +526,211 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.page { .page {
min-height: 100vh; height: 100vh;
background-color: $bg-page; background-color: #f7f8fa;
padding-bottom: 40rpx; 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; position: relative;
overflow-x: hidden; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* 顶部 Header */ .item-text {
.header-section { font-size: 26rpx;
position: fixed; color: #666;
top: 0; left: 0; right: 0; transition: all 0.3s;
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 { &.active {
background: $gradient-brand; .item-text {
color: #fff; color: $brand-primary;
font-weight: 600; font-weight: 700;
transform: scale(1.1);
}
} }
} }
.indicator {
/* Tabs */ position: absolute;
.tab-row { left: 0;
display: flex; width: 8rpx;
justify-content: space-around; height: 32rpx;
padding-bottom: 4rpx; background: $gradient-brand;
border-radius: 0 4rpx 4rpx 0;
} }
.tab-item {
position: relative; /* 主内容区 */
padding: 16rpx 20rpx; .main-content {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #fff;
position: relative;
}
.top-nav {
padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.03);
}
.search-wrap {
display: flex;
align-items: center; align-items: center;
} justify-content: space-between;
.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; 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; }
.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;
}
/* 缩放动画的核心部分 */
.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 { .product-card {
background: #fff; background: #fff;
border-radius: $radius-lg; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
box-shadow: $shadow-sm; 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 { .thumb-wrap {
position: relative; width: 100%; padding-top: 100%; width: 100%;
background: #f8f8f8; padding-top: 100%;
position: relative;
background: #fafafa;
overflow: hidden;
image {
transition: opacity 0.3s;
}
} }
.product-thumb { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .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-info { padding: 16rpx; }
.product-title { .product-title {
font-size: 26rpx; color: $text-main; font-weight: 600; font-size: 24rpx; color: #333; line-height: 1.4; height: 2.8em;
line-height: 1.4; height: 2.8em; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; 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; } .product-bottom { display: flex; align-items: center; justify-content: space-between; margin-top: 12rpx; }
.points-val { font-size: 32rpx; font-weight: 800; color: #FF9800; font-family: 'DIN-Bold'; } .points-val { font-size: 28rpx; color: $brand-primary; font-weight: 700; }
.points-unit { font-size: 20rpx; color: #FF9800; margin-left: 2rpx; } .points-unit { font-size: 18rpx; color: $brand-primary; margin-left: 2rpx; }
.redeem-btn { .redeem-btn-sm {
background: $gradient-brand; color: #fff; font-size: 22rpx; width: 44rpx; height: 44rpx; background: $gradient-brand;
padding: 6rpx 18rpx; border-radius: 24rpx; font-weight: 600; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center;
&.disabled { font-weight: 700; font-size: 28rpx;
background: #ccc;
color: #999;
}
} }
/* 优惠券 (Ticket Style) */ /* 紧凑型优惠券列表 */
.coupons-list { display: flex; flex-direction: column; gap: 24rpx; } .coupons-compact-list { display: flex; flex-direction: column; gap: 20rpx; }
.coupon-card { .coupon-compact-card {
display: flex; background: #fff; border-radius: 16rpx; padding: 24rpx;
background: #fff; display: flex; align-items: center; border: 1rpx solid rgba(0,0,0,0.03);
border-radius: $radius-lg;
height: 180rpx;
box-shadow: $shadow-sm;
overflow: hidden;
} }
.coupon-left { .c-val-box { width: 100rpx; font-weight: 800; color: #FF6B6B; }
width: 200rpx; .c-info-box { flex: 1; }
background: linear-gradient(135deg, #FF6B6B, #FF3B30); .c-title { display: block; font-size: 24rpx; font-weight: 600; color: #333; }
display: flex; flex-direction: column; align-items: center; justify-content: center; .c-price { font-size: 20rpx; color: #999; }
color: #fff; .c-btn { background: #333; color: #fff; font-size: 22rpx; padding: 6rpx 20rpx; border-radius: 10rpx; }
}
.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 { .empty-mini { padding-top: 100rpx; text-align: center; color: #ccc; font-size: 24rpx; image { width: 160rpx; display: block; margin: 0 auto 10rpx; } }
width: 2rpx; position: relative; .mini-loading { padding-top: 60rpx; display: flex; justify-content: center; }
background: #eee; .pulse { width: 40rpx; height: 40rpx; background: $brand-primary; border-radius: 50%; opacity: 0.3; animation: pulse 1s infinite; }
.notch { @keyframes pulse { from { transform: scale(1); opacity: 0.3; } to { transform: scale(2); opacity: 0; } }
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 { .no-more {
text-align: center; text-align: center;
padding: 40rpx 0; padding: 40rpx 0;
color: $text-tertiary; color: $text-tertiary;
font-size: 24rpx;
opacity: 0.6; opacity: 0.6;
} }

View File

@ -1,5 +1,5 @@
//const BASE_URL = 'http://127.0.0.1:9991' const BASE_URL = 'http://127.0.0.1:9991'
const BASE_URL = 'https://kdy.1024tool.vip' // const BASE_URL = 'https://kdy.1024tool.vip'
let authModalShown = false let authModalShown = false
function handleAuthExpired() { function handleAuthExpired() {