feat: 新增我的优惠券、物品卡片、邀请、任务页面,并优化活动相关组件和页面。
This commit is contained in:
parent
d5527625bc
commit
7406f8b308
@ -150,6 +150,14 @@ export function redeemProductByPoints(user_id, product_id, quantity) {
|
|||||||
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-product`, method: 'POST', data: { product_id, quantity } })
|
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-product`, method: 'POST', data: { product_id, quantity } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function redeemItemCardByPoints(user_id, item_card_id, quantity = 1) {
|
||||||
|
return authRequest({ url: `/api/app/users/${user_id}/points/redeem-item-card`, method: 'POST', data: { item_card_id, quantity } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoreItems(kind = 'product', page = 1, page_size = 20) {
|
||||||
|
return authRequest({ url: '/api/app/store/items', method: 'GET', data: { kind, page, page_size } })
|
||||||
|
}
|
||||||
|
|
||||||
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 } })
|
||||||
}
|
}
|
||||||
@ -158,6 +166,10 @@ export function getTaskProgress(task_id, user_id) {
|
|||||||
return authRequest({ url: `/api/app/task-center/tasks/${task_id}/progress/${user_id}`, method: 'GET' })
|
return authRequest({ url: `/api/app/task-center/tasks/${task_id}/progress/${user_id}`, method: 'GET' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function claimTaskReward(task_id, user_id, tier_id) {
|
||||||
|
return authRequest({ url: `/api/app/task-center/tasks/${task_id}/claim/${user_id}`, method: 'POST', data: { tier_id } })
|
||||||
|
}
|
||||||
|
|
||||||
export function getShipments(user_id, page = 1, page_size = 20) {
|
export function getShipments(user_id, page = 1, page_size = 20) {
|
||||||
return authRequest({ url: `/api/app/users/${user_id}/shipments`, method: 'GET', data: { page, page_size } })
|
return authRequest({ url: `/api/app/users/${user_id}/shipments`, method: 'GET', data: { page, page_size } })
|
||||||
}
|
}
|
||||||
@ -229,3 +241,15 @@ export function checkMatchingGame(game_id, total_pairs) {
|
|||||||
data: { game_id, total_pairs }
|
data: { game_id, total_pairs }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付成功后获取游戏数据
|
||||||
|
* @param {string} game_id - 游戏ID
|
||||||
|
*/
|
||||||
|
export function getMatchingGameCards(game_id) {
|
||||||
|
return authRequest({
|
||||||
|
url: '/api/app/matching/cards',
|
||||||
|
method: 'GET',
|
||||||
|
data: { game_id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="action-bar">
|
<view class="action-bar" v-if="!hideActionBar">
|
||||||
<view class="selection-info" v-if="selectedItems.length > 0">
|
<view class="selection-info" v-if="selectedItems.length > 0">
|
||||||
已选 <text class="highlight">{{ selectedItems.length }}</text> 个位置
|
已选 <text class="highlight">{{ selectedItems.length }}</text> 个位置
|
||||||
</view>
|
</view>
|
||||||
@ -62,10 +62,11 @@ const props = defineProps({
|
|||||||
issueId: { type: [String, Number], required: true },
|
issueId: { type: [String, Number], required: true },
|
||||||
pricePerDraw: { type: Number, default: 0 },
|
pricePerDraw: { type: Number, default: 0 },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
disabledText: { type: String, default: '' }
|
disabledText: { type: String, default: '' },
|
||||||
|
hideActionBar: { type: Boolean, default: false } // 支持隐藏内置操作栏
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['payment-success'])
|
const emit = defineEmits(['payment-success', 'selection-change'])
|
||||||
|
|
||||||
const choices = ref([])
|
const choices = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -162,6 +163,9 @@ function handleSelect(item) {
|
|||||||
} else {
|
} else {
|
||||||
selectedItems.value.push(item)
|
selectedItems.value.push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知父组件选中变化
|
||||||
|
emit('selection-change', [...selectedItems.value])
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBuy() {
|
function handleBuy() {
|
||||||
@ -320,6 +324,13 @@ async function onPaymentConfirm(paymentData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暴露方法供外部调用
|
||||||
|
defineExpose({
|
||||||
|
handleRandomOne,
|
||||||
|
handleBuy,
|
||||||
|
selectedItems: () => selectedItems.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -74,14 +74,17 @@ const formattedPrice = computed(() => {
|
|||||||
|
|
||||||
.header-card {
|
.header-card {
|
||||||
margin: $spacing-xl $spacing-lg;
|
margin: $spacing-xl $spacing-lg;
|
||||||
background: rgba($bg-card, 0.85);
|
background: rgba($bg-card, 0.72);
|
||||||
backdrop-filter: blur(24rpx);
|
backdrop-filter: blur(32rpx);
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-xl;
|
||||||
padding: $spacing-lg;
|
padding: $spacing-lg;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: $shadow-card;
|
box-shadow:
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
0 1rpx 0 rgba(255,255,255,0.5) inset,
|
||||||
|
0 -1rpx 0 rgba(0,0,0,0.02) inset,
|
||||||
|
$shadow-card;
|
||||||
|
border: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
|||||||
@ -106,15 +106,15 @@ defineProps({
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 700rpx;
|
height: 900rpx;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-image {
|
.bg-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
filter: blur(30rpx) brightness(0.9);
|
filter: blur(40rpx) brightness(0.85) saturate(1.1);
|
||||||
transform: scale(1.1);
|
transform: scale(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-mask {
|
.bg-mask {
|
||||||
@ -123,7 +123,16 @@ defineProps({
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(180deg, rgba($bg-page, 0.2) 0%, $bg-page 90%, $bg-page 100%);
|
/* 6段式平滑过渡,模拟ease-out曲线 */
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg,
|
||||||
|
rgba($bg-page, 0) 0%,
|
||||||
|
rgba($bg-page, 0.05) 15%,
|
||||||
|
rgba($bg-page, 0.2) 35%,
|
||||||
|
rgba($bg-page, 0.5) 55%,
|
||||||
|
rgba($bg-page, 0.8) 70%,
|
||||||
|
$bg-page 82%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-scroll {
|
.main-scroll {
|
||||||
|
|||||||
@ -49,11 +49,13 @@ const staggerClass = computed(() => `stagger-${props.stagger}`)
|
|||||||
/* Section Container - 与原始设计一致 */
|
/* Section Container - 与原始设计一致 */
|
||||||
.section-container {
|
.section-container {
|
||||||
margin: 0 $spacing-lg $spacing-lg;
|
margin: 0 $spacing-lg $spacing-lg;
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.78);
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-xl;
|
||||||
padding: $spacing-lg;
|
padding: $spacing-lg;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow:
|
||||||
backdrop-filter: blur(10rpx);
|
0 1rpx 0 rgba(255,255,255,0.4) inset,
|
||||||
|
$shadow-sm;
|
||||||
|
backdrop-filter: blur(16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modern Tabs - 与原始设计完全一致 */
|
/* Modern Tabs - 与原始设计完全一致 */
|
||||||
|
|||||||
259
components/activity/CabinetPreviewPopup.vue
Normal file
259
components/activity/CabinetPreviewPopup.vue
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="cabinet-overlay" @touchmove.stop.prevent>
|
||||||
|
<view class="cabinet-mask" @tap="close"></view>
|
||||||
|
<view class="cabinet-panel" @tap.stop>
|
||||||
|
<view class="cabinet-header">
|
||||||
|
<text class="cabinet-title">我的盒柜</text>
|
||||||
|
<text class="cabinet-close" @tap="close">×</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="cabinet-content">
|
||||||
|
<view v-if="loading" class="cabinet-loading">
|
||||||
|
<text class="loading-icon">📦</text>
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="items.length === 0" class="cabinet-empty">
|
||||||
|
<text class="empty-icon">🎁</text>
|
||||||
|
<text class="empty-text">暂无物品</text>
|
||||||
|
<text class="empty-hint">参与活动获得奖品后会显示在这里</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="cabinet-grid">
|
||||||
|
<view v-for="item in displayItems" :key="item.id" class="cabinet-item">
|
||||||
|
<image class="item-image" :src="item.image" mode="aspectFill" />
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="item-name">{{ item.name }}</text>
|
||||||
|
<text class="item-count">x{{ item.count }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="hasMore" class="load-more" @tap="goFullCabinet">
|
||||||
|
<text>查看全部 {{ total }} 件物品</text>
|
||||||
|
<text class="arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
activityId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const items = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const maxDisplay = 6 // 最多显示6个
|
||||||
|
|
||||||
|
const displayItems = computed(() => items.value.slice(0, maxDisplay))
|
||||||
|
const hasMore = computed(() => items.value.length > maxDisplay || total.value > items.value.length)
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goFullCabinet() {
|
||||||
|
close()
|
||||||
|
uni.switchTab({ url: '/pages/cabinet/index' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟加载数据,实际项目中应该调用API
|
||||||
|
async function loadItems() {
|
||||||
|
if (!props.activityId) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// TODO: 调用实际API获取当前活动相关的盒柜物品
|
||||||
|
// const res = await getUserCabinetItems(props.activityId)
|
||||||
|
// items.value = res.items || []
|
||||||
|
// total.value = res.total || 0
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
await new Promise(r => setTimeout(r, 300))
|
||||||
|
items.value = []
|
||||||
|
total.value = 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载盒柜失败', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (v) => {
|
||||||
|
if (v) loadItems()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cabinet-overlay {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: $spacing-lg;
|
||||||
|
right: $spacing-lg;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 24rpx);
|
||||||
|
max-height: 65vh;
|
||||||
|
background: rgba($bg-card, 0.95);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-title {
|
||||||
|
font-size: $font-lg;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: $text-tertiary;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-content {
|
||||||
|
max-height: 50vh;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-loading,
|
||||||
|
.cabinet-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon,
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64rpx;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.empty-text {
|
||||||
|
font-size: $font-md;
|
||||||
|
color: $text-sub;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-top: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
background: $bg-secondary;
|
||||||
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-main;
|
||||||
|
font-weight: 600;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: $spacing-lg 0;
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
color: $brand-primary;
|
||||||
|
font-size: $font-sm;
|
||||||
|
font-weight: 600;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(40rpx);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view>
|
<view class="records-wrapper">
|
||||||
<view class="records-list" v-if="records && records.length">
|
<view class="records-list" v-if="records && records.length">
|
||||||
<view v-for="(item, idx) in records" :key="item.id || idx" class="record-item">
|
<view v-for="(item, idx) in records" :key="item.id || idx" class="record-item">
|
||||||
<image class="record-img" :src="item.image" mode="aspectFill" />
|
<image class="record-img" :src="item.image" mode="aspectFill" />
|
||||||
@ -12,9 +12,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="empty-state" v-else>
|
<view class="empty-state-compact" v-else>
|
||||||
<text class="empty-icon">📝</text>
|
<view class="empty-icon-wrap">
|
||||||
<text class="empty-text">{{ emptyText }}</text>
|
<text class="empty-icon">🎁</text>
|
||||||
|
</view>
|
||||||
|
<text class="empty-title">{{ emptyText }}</text>
|
||||||
|
<text class="empty-hint">快来参与活动获得奖品吧</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -87,21 +90,40 @@ defineProps({
|
|||||||
color: $text-sub;
|
color: $text-sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
/* 紧凑优雅的空状态 */
|
||||||
|
.empty-state-compact {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: $spacing-xl;
|
padding: $spacing-lg $spacing-xl;
|
||||||
color: $text-sub;
|
min-height: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon-wrap {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($accent-gold, 0.1) 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
font-size: 64rpx;
|
font-size: 40rpx;
|
||||||
margin-bottom: $spacing-sm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-title {
|
||||||
font-size: $font-sm;
|
font-size: $font-md;
|
||||||
|
color: $text-sub;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-tertiary;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -223,6 +223,7 @@ const rewardGroups = computed(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: $spacing-xl;
|
padding: $spacing-xl;
|
||||||
color: $text-sub;
|
color: $text-sub;
|
||||||
|
min-height: 300rpx; /* 防止切换时布局跳动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-icon {
|
||||||
|
|||||||
125
components/activity/RulesPopup.vue
Normal file
125
components/activity/RulesPopup.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="rules-overlay" @touchmove.stop.prevent>
|
||||||
|
<view class="rules-mask" @tap="close"></view>
|
||||||
|
<view class="rules-panel" @tap.stop>
|
||||||
|
<view class="rules-header">
|
||||||
|
<text class="rules-title">{{ title }}</text>
|
||||||
|
<text class="rules-close" @tap="close">×</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="rules-content">
|
||||||
|
<!-- 使用 rich-text 渲染富文本 HTML -->
|
||||||
|
<rich-text v-if="content" class="rules-richtext" :nodes="content"></rich-text>
|
||||||
|
<view v-else class="rules-empty">暂无活动规则</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '活动规则'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible'])
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.rules-overlay {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: $spacing-lg;
|
||||||
|
right: $spacing-lg;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom) + 24rpx);
|
||||||
|
max-height: 70vh;
|
||||||
|
background: rgba($bg-card, 0.95);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideUp 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-title {
|
||||||
|
font-size: $font-lg;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
line-height: 1;
|
||||||
|
color: $text-tertiary;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content {
|
||||||
|
max-height: 55vh;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-richtext {
|
||||||
|
font-size: $font-sm;
|
||||||
|
color: $text-main;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: $text-sub;
|
||||||
|
padding: $spacing-xl;
|
||||||
|
font-size: $font-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(40rpx);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
24
pages.json
24
pages.json
@ -42,6 +42,30 @@
|
|||||||
"navigationBarTitleText": "积分记录"
|
"navigationBarTitleText": "积分记录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/coupons/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的优惠券"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/item-cards/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的道具卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/invites/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "邀请记录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/tasks/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "任务中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/orders/index",
|
"path": "pages/orders/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ActivityPageLayout :cover-url="coverUrl">
|
<ActivityPageLayout :cover-url="coverUrl">
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
<template #header>
|
<template #header>
|
||||||
<ActivityHeader
|
<ActivityHeader
|
||||||
:title="detail.name || detail.title || '对对碰'"
|
:title="detail.name || detail.title || '对对碰'"
|
||||||
@ -78,46 +79,69 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #modals>
|
<template #modals>
|
||||||
<!-- 游戏弹窗 -->
|
<!-- 游戏弹窗 - 全屏沉浸式 -->
|
||||||
<view v-if="gameVisible" class="rewards-overlay" @touchmove.stop.prevent>
|
<view v-if="gameVisible" class="game-overlay" @touchmove.stop.prevent>
|
||||||
<view class="rewards-mask" @tap="closeGame"></view>
|
<view class="game-mask"></view>
|
||||||
<view class="rewards-panel" @tap.stop>
|
<view class="game-fullscreen" @tap.stop>
|
||||||
<view class="rewards-header">
|
<!-- 顶部标题栏 -->
|
||||||
<text class="rewards-title">对对碰游戏</text>
|
<view class="game-topbar">
|
||||||
<text class="rewards-close" @tap="closeGame">×</text>
|
<text class="game-topbar-title">对对碰游戏</text>
|
||||||
|
<view class="game-close-btn" @tap="closeGame">
|
||||||
|
<text>×</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view scroll-y class="game-scroll-list">
|
|
||||||
<view v-if="gameLoading" class="game-empty-state">加载中...</view>
|
<!-- 游戏信息 -->
|
||||||
<view v-else-if="gameError" class="game-empty-state">{{ gameError }}</view>
|
<view class="game-stats glass-card">
|
||||||
<view v-else>
|
<view class="stat-item">
|
||||||
<view class="game-info-card">
|
<text class="stat-label">总对数</text>
|
||||||
<view class="game-info-content">
|
<text class="stat-value">{{ totalPairs }}</text>
|
||||||
<view class="game-info-title">总对数:{{ totalPairs }} 摸牌机会:{{ chance }}</view>
|
</view>
|
||||||
<view class="game-info-meta">
|
<view class="stat-item">
|
||||||
<text>牌组剩余 {{ deckRemaining }}</text>
|
<text class="stat-label">摸牌机会</text>
|
||||||
<text v-if="selectedPositionText">位置 {{ selectedPositionText }}</text>
|
<text class="stat-value">{{ chance }}</text>
|
||||||
<text v-if="gameIdText">ID {{ gameIdText }}</text>
|
</view>
|
||||||
</view>
|
<view class="stat-item">
|
||||||
</view>
|
<text class="stat-label">牌组剩余</text>
|
||||||
</view>
|
<text class="stat-value">{{ deckRemaining }}</text>
|
||||||
<view class="match-grid">
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 游戏区域 -->
|
||||||
|
<view class="game-content">
|
||||||
|
<view v-if="gameLoading" class="game-loading">
|
||||||
|
<text class="loading-icon">⏳</text>
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="gameError" class="game-error">
|
||||||
|
<text class="error-icon">⚠️</text>
|
||||||
|
<text class="error-text">{{ gameError }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="game-board glass-card">
|
||||||
|
<view class="match-grid-fullscreen">
|
||||||
<view
|
<view
|
||||||
v-for="(cell, idx) in handGridCells"
|
v-for="(cell, idx) in handGridCells"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="match-cell"
|
class="match-cell-large"
|
||||||
:class="{ empty: cell.empty, chosen: cell.isChosen, picked: cell.isPicked }"
|
:class="{ empty: cell.empty, chosen: cell.isChosen, picked: cell.isPicked }"
|
||||||
@tap="() => onCellTap(cell)"
|
@tap="() => onCellTap(cell)"
|
||||||
>
|
>
|
||||||
<image v-if="cell.image" class="match-cell-img" :src="cell.image" mode="aspectFill" />
|
<image v-if="cell.image" class="match-cell-img-large" :src="cell.image" mode="aspectFill" />
|
||||||
<view v-else class="match-cell-img"></view>
|
<view v-else class="match-cell-img-large"></view>
|
||||||
<text v-if="!cell.empty && cell.type" class="match-cell-type">{{ cell.type }}</text>
|
<text v-if="!cell.empty && cell.type" class="match-cell-type-large">{{ cell.type }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</view>
|
||||||
<view class="flip-actions" style="padding: 20rpx 24rpx;">
|
|
||||||
<button class="close-btn" style="flex: 1;" @tap="manualDraw" :disabled="gameLoading || !canManualDraw">摸牌</button>
|
<!-- 底部操作栏 -->
|
||||||
<button class="close-btn" style="flex: 1; background: linear-gradient(135deg, #ff7a18, #ffb347); color: #fff;" @tap="advanceOne" :disabled="gameLoading">下一步</button>
|
<view class="game-actions">
|
||||||
|
<button class="game-btn btn-secondary" @tap="manualDraw" :disabled="gameLoading || !canManualDraw">
|
||||||
|
<text>摸牌</text>
|
||||||
|
</button>
|
||||||
|
<button class="game-btn btn-primary" @tap="advanceOne" :disabled="gameLoading">
|
||||||
|
<text>下一步</text>
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -137,6 +161,15 @@
|
|||||||
:showCards="true"
|
:showCards="true"
|
||||||
@confirm="onPaymentConfirm"
|
@confirm="onPaymentConfirm"
|
||||||
/>
|
/>
|
||||||
|
<RulesPopup
|
||||||
|
v-model:visible="rulesVisible"
|
||||||
|
:content="detail.gameplay_intro"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CabinetPreviewPopup
|
||||||
|
v-model:visible="cabinetVisible"
|
||||||
|
:activity-id="activityId"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ActivityPageLayout>
|
</ActivityPageLayout>
|
||||||
</template>
|
</template>
|
||||||
@ -151,7 +184,9 @@ import ActivityTabs from '@/components/activity/ActivityTabs.vue'
|
|||||||
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
||||||
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||||||
import RecordsList from '@/components/activity/RecordsList.vue'
|
import RecordsList from '@/components/activity/RecordsList.vue'
|
||||||
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs } from '../../../api/appUser'
|
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||||
|
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||||
|
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs, getMatchingGameCards } from '../../../api/appUser'
|
||||||
|
|
||||||
const detail = ref({})
|
const detail = ref({})
|
||||||
const statusText = ref('')
|
const statusText = ref('')
|
||||||
@ -173,6 +208,8 @@ const cardTypesLoading = ref(false)
|
|||||||
const cardTypes = ref([])
|
const cardTypes = ref([])
|
||||||
const selectedCardTypeCode = ref('')
|
const selectedCardTypeCode = ref('')
|
||||||
const rewardsVisible = ref(false)
|
const rewardsVisible = ref(false)
|
||||||
|
const rulesVisible = ref(false)
|
||||||
|
const cabinetVisible = ref(false)
|
||||||
const resumeGame = ref(null)
|
const resumeGame = ref(null)
|
||||||
const resumeIssueId = ref('')
|
const resumeIssueId = ref('')
|
||||||
const hasResumeGame = computed(() => {
|
const hasResumeGame = computed(() => {
|
||||||
@ -613,15 +650,11 @@ function closeRewardsPopup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showRules() {
|
function showRules() {
|
||||||
uni.showModal({
|
rulesVisible.value = true
|
||||||
title: '活动规则',
|
|
||||||
content: detail.value.rules || '1. 选择卡牌类型进行对对碰\\n2. 每次抽取随机获得奖品\\n3. 奖池与概率以页面展示为准',
|
|
||||||
showCancel: false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goCabinet() {
|
function goCabinet() {
|
||||||
uni.switchTab({ url: '/pages/cabinet/index' })
|
cabinetVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPreviewBanner() {
|
function onPreviewBanner() {
|
||||||
@ -823,6 +856,7 @@ function drawOne() {
|
|||||||
function manualDraw() {
|
function manualDraw() {
|
||||||
if (gameLoading.value) return
|
if (gameLoading.value) return
|
||||||
if (!canManualDraw.value) return
|
if (!canManualDraw.value) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
drawOne()
|
drawOne()
|
||||||
chance.value = Math.max(0, Number(chance.value || 0) - 1)
|
chance.value = Math.max(0, Number(chance.value || 0) - 1)
|
||||||
pickedHandIndex.value = -1
|
pickedHandIndex.value = -1
|
||||||
@ -854,6 +888,7 @@ async function autoDrawIfStuck() {
|
|||||||
async function onCellTap(cell) {
|
async function onCellTap(cell) {
|
||||||
if (gameLoading.value) return
|
if (gameLoading.value) return
|
||||||
if (!cell || cell.empty) return
|
if (!cell || cell.empty) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
const hi = Number(cell.handIndex)
|
const hi = Number(cell.handIndex)
|
||||||
if (!Number.isFinite(hi) || hi < 0) return
|
if (!Number.isFinite(hi) || hi < 0) return
|
||||||
|
|
||||||
@ -912,6 +947,7 @@ async function finishAndReport() {
|
|||||||
|
|
||||||
async function advanceOne() {
|
async function advanceOne() {
|
||||||
if (gameLoading.value) return
|
if (gameLoading.value) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
const entry = gameEntry.value || null
|
const entry = gameEntry.value || null
|
||||||
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
|
||||||
if (!gameId) return
|
if (!gameId) return
|
||||||
@ -969,6 +1005,7 @@ async function autoRun() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onParticipate() {
|
async function onParticipate() {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
const aid = activityId.value || ''
|
const aid = activityId.value || ''
|
||||||
const iid = currentIssueId.value || ''
|
const iid = currentIssueId.value || ''
|
||||||
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
|
||||||
@ -1014,6 +1051,7 @@ async function applyResumeEntry(entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onResumeGame() {
|
async function onResumeGame() {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
const aid = activityId.value || ''
|
const aid = activityId.value || ''
|
||||||
const latest = syncResumeGame(aid)
|
const latest = syncResumeGame(aid)
|
||||||
if (!latest || !latest.entry || !latest.entry.game_id) return
|
if (!latest || !latest.entry || !latest.entry.game_id) return
|
||||||
@ -1060,7 +1098,7 @@ async function doDraw() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 调用 createMatchingPreorder 创建对对碰订单(同时返回游戏数据)
|
// 1. 调用 createMatchingPreorder 创建对对碰订单(不再返回游戏数据)
|
||||||
const preRes = await createMatchingPreorder({
|
const preRes = await createMatchingPreorder({
|
||||||
issue_id: Number(iid),
|
issue_id: Number(iid),
|
||||||
position: String(selectedCardType.value.code || ''),
|
position: String(selectedCardType.value.code || ''),
|
||||||
@ -1069,12 +1107,12 @@ async function doDraw() {
|
|||||||
})
|
})
|
||||||
if (!preRes) throw new Error('创建订单失败')
|
if (!preRes) throw new Error('创建订单失败')
|
||||||
|
|
||||||
// 2. 提取订单号和游戏数据
|
// 2. 提取订单号和游戏ID(注意:all_cards 不再在这里返回)
|
||||||
const orderNo = preRes.order_no || preRes.data?.order_no || preRes.result?.order_no || preRes.orderNo
|
const orderNo = preRes.order_no || preRes.data?.order_no || preRes.result?.order_no || preRes.orderNo
|
||||||
if (!orderNo) throw new Error('未获取到订单号')
|
if (!orderNo) throw new Error('未获取到订单号')
|
||||||
|
|
||||||
const gameId = preRes.game_id || preRes.data?.game_id || preRes.result?.game_id || preRes.gameId
|
const gameId = preRes.game_id || preRes.data?.game_id || preRes.result?.game_id || preRes.gameId
|
||||||
const allCards = normalizeAllCards(preRes.all_cards || preRes.data?.all_cards || preRes.result?.all_cards || [])
|
if (!gameId) throw new Error('未获取到游戏ID')
|
||||||
|
|
||||||
// 3. 用对对碰订单号调用微信支付
|
// 3. 用对对碰订单号调用微信支付
|
||||||
uni.showLoading({ title: '拉起支付...' })
|
uni.showLoading({ title: '拉起支付...' })
|
||||||
@ -1092,20 +1130,26 @@ async function doDraw() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 4. 支付成功后保存游戏数据到本地缓存
|
// 4. 【关键】支付成功后,调用新接口获取游戏数据
|
||||||
if (gameId) {
|
uni.showLoading({ title: '加载游戏...' })
|
||||||
writeMatchingGameCacheEntry(aid, iid, {
|
const cardsRes = await getMatchingGameCards(gameId)
|
||||||
game_id: String(gameId),
|
if (!cardsRes) throw new Error('获取游戏数据失败')
|
||||||
position: String(selectedCardType.value.code || ''),
|
|
||||||
all_cards: allCards,
|
const allCards = normalizeAllCards(cardsRes.all_cards || cardsRes.data?.all_cards || [])
|
||||||
ts: Date.now()
|
if (!allCards.length) throw new Error('游戏数据为空')
|
||||||
})
|
|
||||||
}
|
// 5. 保存游戏数据到本地缓存
|
||||||
|
writeMatchingGameCacheEntry(aid, iid, {
|
||||||
|
game_id: String(gameId),
|
||||||
|
position: String(selectedCardType.value.code || ''),
|
||||||
|
all_cards: allCards,
|
||||||
|
ts: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({ title: '支付成功', icon: 'success' })
|
uni.showToast({ title: '支付成功', icon: 'success' })
|
||||||
|
|
||||||
// 5. 自动打开游戏
|
// 6. 自动打开游戏
|
||||||
syncResumeGame(aid)
|
syncResumeGame(aid)
|
||||||
const latest = findLatestMatchingGameCacheEntry(aid)
|
const latest = findLatestMatchingGameCacheEntry(aid)
|
||||||
if (latest && latest.entry && latest.entry.game_id) {
|
if (latest && latest.entry && latest.entry.game_id) {
|
||||||
@ -1335,8 +1379,10 @@ onLoad((opts) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
transition: all 0.2s;
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 0.6;
|
transform: scale(0.92);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.action-icon {
|
.action-icon {
|
||||||
@ -1872,4 +1918,219 @@ onLoad((opts) => {
|
|||||||
animation: fadeInUp 0.5s ease-out backwards;
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
animation-delay: var(--delay, 0s);
|
animation-delay: var(--delay, 0s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============= 全屏游戏样式 ============= */
|
||||||
|
.game-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.92);
|
||||||
|
backdrop-filter: blur(16rpx);
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-fullscreen {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: scaleIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-topbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40rpx 32rpx;
|
||||||
|
padding-top: calc(40rpx + env(safe-area-inset-top));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-topbar-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 32rpx;
|
||||||
|
top: calc(40rpx + env(safe-area-inset-top));
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 48rpx;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
margin: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $accent-gold;
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-loading,
|
||||||
|
.game-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon,
|
||||||
|
.error-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.error-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-board {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-grid-fullscreen {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-cell-large {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.15);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chosen {
|
||||||
|
border-color: $brand-primary;
|
||||||
|
box-shadow: 0 0 24rpx rgba($brand-primary, 0.5);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.picked {
|
||||||
|
border-color: $accent-gold;
|
||||||
|
box-shadow: 0 0 24rpx rgba($accent-gold, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-cell-img-large {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-cell-type-large {
|
||||||
|
position: absolute;
|
||||||
|
left: 12rpx;
|
||||||
|
bottom: 12rpx;
|
||||||
|
max-width: 85%;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
@include text-ellipsis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -72,6 +72,15 @@
|
|||||||
:propCards="propCards"
|
:propCards="propCards"
|
||||||
@confirm="onPaymentConfirm"
|
@confirm="onPaymentConfirm"
|
||||||
/>
|
/>
|
||||||
|
<RulesPopup
|
||||||
|
v-model:visible="rulesVisible"
|
||||||
|
:content="detail.gameplay_intro"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CabinetPreviewPopup
|
||||||
|
v-model:visible="cabinetVisible"
|
||||||
|
:activity-id="activityId"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ActivityPageLayout>
|
</ActivityPageLayout>
|
||||||
</template>
|
</template>
|
||||||
@ -86,6 +95,8 @@ import ActivityTabs from '@/components/activity/ActivityTabs.vue'
|
|||||||
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
||||||
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||||||
import RecordsList from '@/components/activity/RecordsList.vue'
|
import RecordsList from '@/components/activity/RecordsList.vue'
|
||||||
|
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||||
|
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||||
import FlipGrid from '@/components/FlipGrid.vue'
|
import FlipGrid from '@/components/FlipGrid.vue'
|
||||||
import PaymentPopup from '@/components/PaymentPopup.vue'
|
import PaymentPopup from '@/components/PaymentPopup.vue'
|
||||||
// Composables
|
// Composables
|
||||||
@ -126,6 +137,8 @@ const {
|
|||||||
// ============ 本地状态 ============
|
// ============ 本地状态 ============
|
||||||
const tabActive = ref('pool')
|
const tabActive = ref('pool')
|
||||||
const rewardsVisible = ref(false)
|
const rewardsVisible = ref(false)
|
||||||
|
const rulesVisible = ref(false)
|
||||||
|
const cabinetVisible = ref(false)
|
||||||
const showFlip = ref(false)
|
const showFlip = ref(false)
|
||||||
const flipRef = ref(null)
|
const flipRef = ref(null)
|
||||||
const drawLoading = ref(false)
|
const drawLoading = ref(false)
|
||||||
@ -141,15 +154,11 @@ const selectedCard = ref(null)
|
|||||||
|
|
||||||
// ============ 业务方法 ============
|
// ============ 业务方法 ============
|
||||||
function showRules() {
|
function showRules() {
|
||||||
uni.showModal({
|
rulesVisible.value = true
|
||||||
title: '活动规则',
|
|
||||||
content: detail.value.rules || '1. 选择档位进行抽赏\n2. 每次抽赏随机获得奖品\n3. 奖池与概率以页面展示为准',
|
|
||||||
showCancel: false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goCabinet() {
|
function goCabinet() {
|
||||||
uni.switchTab({ url: '/pages/cabinet/index' })
|
cabinetVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFlip() {
|
function closeFlip() {
|
||||||
|
|||||||
@ -49,20 +49,41 @@
|
|||||||
<text class="issue-block-text">{{ orderBlockedReason }}</text>
|
<text class="issue-block-text">{{ orderBlockedReason }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 选号组件 -->
|
<!-- 选号组件 - 隐藏内置操作栏 -->
|
||||||
<view class="selector-body" v-if="activityId && currentIssueId">
|
<view class="selector-body" v-if="activityId && currentIssueId">
|
||||||
<YifanSelector
|
<YifanSelector
|
||||||
|
ref="yifanSelectorRef"
|
||||||
:activity-id="activityId"
|
:activity-id="activityId"
|
||||||
:issue-id="currentIssueId"
|
:issue-id="currentIssueId"
|
||||||
:price-per-draw="Number(detail.price_draw || 0) / 100"
|
:price-per-draw="Number(detail.price_draw || 0) / 100"
|
||||||
:disabled="!isOrderAllowed"
|
:disabled="!isOrderAllowed"
|
||||||
:disabled-text="orderBlockedReason"
|
:disabled-text="orderBlockedReason"
|
||||||
|
:hide-action-bar="true"
|
||||||
@payment-success="onPaymentSuccess"
|
@payment-success="onPaymentSuccess"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<!-- 固定底部操作栏 -->
|
||||||
|
<view class="float-bar">
|
||||||
|
<view class="float-bar-inner">
|
||||||
|
<view class="selection-info" v-if="selectedCount > 0">
|
||||||
|
已选 <text class="highlight">{{ selectedCount }}</text> 个位置
|
||||||
|
</view>
|
||||||
|
<view class="selection-info" v-else>
|
||||||
|
请选择位置
|
||||||
|
</view>
|
||||||
|
<view class="action-buttons">
|
||||||
|
<button v-if="selectedCount === 0" class="action-btn primary" @tap="handleRandomDraw" :disabled="!isOrderAllowed">随机一发</button>
|
||||||
|
<button v-else class="action-btn primary" @tap="handlePayment" :disabled="!isOrderAllowed">去支付</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #modals>
|
<template #modals>
|
||||||
<!-- 翻牌弹窗 -->
|
<!-- 翻牌弹窗 -->
|
||||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||||
@ -79,6 +100,18 @@
|
|||||||
:title="`${currentIssueTitle} · 奖品与概率`"
|
:title="`${currentIssueTitle} · 奖品与概率`"
|
||||||
:reward-groups="rewardGroups"
|
:reward-groups="rewardGroups"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 规则弹窗 -->
|
||||||
|
<RulesPopup
|
||||||
|
v-model:visible="rulesVisible"
|
||||||
|
:content="detail.gameplay_intro"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 盒柜预览弹窗 -->
|
||||||
|
<CabinetPreviewPopup
|
||||||
|
v-model:visible="cabinetVisible"
|
||||||
|
:activity-id="activityId"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ActivityPageLayout>
|
</ActivityPageLayout>
|
||||||
</template>
|
</template>
|
||||||
@ -93,6 +126,8 @@ import ActivityTabs from '@/components/activity/ActivityTabs.vue'
|
|||||||
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
import RewardsPreview from '@/components/activity/RewardsPreview.vue'
|
||||||
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||||||
import RecordsList from '@/components/activity/RecordsList.vue'
|
import RecordsList from '@/components/activity/RecordsList.vue'
|
||||||
|
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||||
|
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||||
import FlipGrid from '@/components/FlipGrid.vue'
|
import FlipGrid from '@/components/FlipGrid.vue'
|
||||||
import YifanSelector from '@/components/YifanSelector.vue'
|
import YifanSelector from '@/components/YifanSelector.vue'
|
||||||
// Composables
|
// Composables
|
||||||
@ -133,8 +168,31 @@ const {
|
|||||||
// ============ 本地状态 ============
|
// ============ 本地状态 ============
|
||||||
const tabActive = ref('pool')
|
const tabActive = ref('pool')
|
||||||
const rewardsVisible = ref(false)
|
const rewardsVisible = ref(false)
|
||||||
|
const rulesVisible = ref(false)
|
||||||
|
const cabinetVisible = ref(false)
|
||||||
const showFlip = ref(false)
|
const showFlip = ref(false)
|
||||||
const flipRef = ref(null)
|
const flipRef = ref(null)
|
||||||
|
const yifanSelectorRef = ref(null)
|
||||||
|
const selectedCount = ref(0) // 从外部追踪选中数量
|
||||||
|
|
||||||
|
// 接收选中变化事件
|
||||||
|
function onSelectionChange(items) {
|
||||||
|
selectedCount.value = Array.isArray(items) ? items.length : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发随机选号
|
||||||
|
function handleRandomDraw() {
|
||||||
|
if (yifanSelectorRef.value && yifanSelectorRef.value.handleRandomOne) {
|
||||||
|
yifanSelectorRef.value.handleRandomOne()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发支付
|
||||||
|
function handlePayment() {
|
||||||
|
if (yifanSelectorRef.value && yifanSelectorRef.value.handleBuy) {
|
||||||
|
yifanSelectorRef.value.handleBuy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 倒计时相关(一番赏专属) ============
|
// ============ 倒计时相关(一番赏专属) ============
|
||||||
const nowMs = ref(Date.now())
|
const nowMs = ref(Date.now())
|
||||||
@ -179,15 +237,11 @@ const orderBlockedReason = computed(() => {
|
|||||||
|
|
||||||
// ============ 业务方法 ============
|
// ============ 业务方法 ============
|
||||||
function showRules() {
|
function showRules() {
|
||||||
uni.showModal({
|
rulesVisible.value = true
|
||||||
title: '活动规则',
|
|
||||||
content: detail.value.rules || '1. 选择号码进行抽选\n2. 每个号码对应一个奖品\n3. 已售号码不可再选\n4.未满足开赏条件,将自动为所有参与用户退款,款项将原路返回',
|
|
||||||
showCancel: false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goCabinet() {
|
function goCabinet() {
|
||||||
uni.switchTab({ url: '/pages/cabinet/index' })
|
cabinetVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFlip() {
|
function closeFlip() {
|
||||||
@ -426,4 +480,77 @@ watch(currentIssueId, (newId) => {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============= 底部固定操作栏 ============= */
|
||||||
|
.float-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 32rpx;
|
||||||
|
right: 32rpx;
|
||||||
|
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||||
|
z-index: 100;
|
||||||
|
animation: slideUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-bar-inner {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
backdrop-filter: blur(30rpx);
|
||||||
|
padding: 24rpx 40rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-info {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-main;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin: 0 8rpx;
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
height: 88rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
padding: 0 56rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: $gradient-brand !important;
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="wrap">
|
<view class="wrap">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
<!-- 顶部 Tab -->
|
<!-- 顶部 Tab -->
|
||||||
<view class="tabs">
|
<view class="tabs glass-card">
|
||||||
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="switchTab(0)">
|
<view class="tab-item" :class="{ active: currentTab === 0 }" @tap="switchTab(0)">
|
||||||
<text class="tab-text">待处理</text>
|
<text class="tab-text">待处理</text>
|
||||||
<text class="tab-count" v-if="aggregatedList.length > 0">({{ aggregatedList.length }})</text>
|
<text class="tab-count" v-if="aggregatedList.length > 0">({{ aggregatedList.length }})</text>
|
||||||
@ -79,7 +81,7 @@
|
|||||||
<view class="shipment-status" :class="getStatusClass(item.status)">
|
<view class="shipment-status" :class="getStatusClass(item.status)">
|
||||||
{{ getStatusText(item.status) }}
|
{{ getStatusText(item.status) }}
|
||||||
</view>
|
</view>
|
||||||
<text class="shipment-cancel" v-if="Number(item.status) === 1 && item.batch_no" @tap="onCancelShipping(item)">取消发货</text>
|
<text class="shipment-cancel" v-if="Number(item.status) === 1 && item.batch_no" @tap="onCancelShipping(item)">撤销发货</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -301,7 +303,8 @@ function getStatusClass(status) {
|
|||||||
1: 'status-pending', // 待发货
|
1: 'status-pending', // 待发货
|
||||||
2: 'status-shipped', // 已发货
|
2: 'status-shipped', // 已发货
|
||||||
3: 'status-delivered', // 已签收
|
3: 'status-delivered', // 已签收
|
||||||
4: 'status-cancelled' // 已取消
|
4: 'status-abnormal', // 异常
|
||||||
|
5: 'status-cancelled' // 已取消
|
||||||
}
|
}
|
||||||
return statusMap[status] || 'status-pending'
|
return statusMap[status] || 'status-pending'
|
||||||
}
|
}
|
||||||
@ -311,7 +314,8 @@ function getStatusText(status) {
|
|||||||
1: '待发货',
|
1: '待发货',
|
||||||
2: '运输中',
|
2: '运输中',
|
||||||
3: '已签收',
|
3: '已签收',
|
||||||
4: '已取消'
|
4: '异常',
|
||||||
|
5: '已取消'
|
||||||
}
|
}
|
||||||
return statusMap[status] || '待发货'
|
return statusMap[status] || '待发货'
|
||||||
}
|
}
|
||||||
@ -590,6 +594,7 @@ async function fetchProductPrices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelect(item) {
|
function toggleSelect(item) {
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
item.selected = !item.selected
|
item.selected = !item.selected
|
||||||
if (item.selected) {
|
if (item.selected) {
|
||||||
// 选中时默认数量为最大值
|
// 选中时默认数量为最大值
|
||||||
@ -604,6 +609,7 @@ function toggleSelect(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
const newState = !isAllSelected.value
|
const newState = !isAllSelected.value
|
||||||
aggregatedList.value.forEach(item => {
|
aggregatedList.value.forEach(item => {
|
||||||
item.selected = newState
|
item.selected = newState
|
||||||
@ -628,6 +634,7 @@ function changeCount(item, delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onRedeem() {
|
async function onRedeem() {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
const user_id = uni.getStorageSync('user_id')
|
const user_id = uni.getStorageSync('user_id')
|
||||||
if (!user_id) return
|
if (!user_id) return
|
||||||
|
|
||||||
@ -675,6 +682,7 @@ async function onRedeem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onShip() {
|
async function onShip() {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
const user_id = uni.getStorageSync('user_id')
|
const user_id = uni.getStorageSync('user_id')
|
||||||
if (!user_id) return
|
if (!user_id) return
|
||||||
|
|
||||||
@ -726,15 +734,15 @@ function onCancelShipping(shipment) {
|
|||||||
const batchNo = shipment && shipment.batch_no
|
const batchNo = shipment && shipment.batch_no
|
||||||
if (!user_id || !batchNo) return
|
if (!user_id || !batchNo) return
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '取消发货',
|
title: '撤销发货',
|
||||||
content: `确认取消发货单 ${batchNo} 吗?`,
|
content: `确认不再发货,并撤销发货单 ${batchNo} 吗?`,
|
||||||
confirmText: '确认取消',
|
confirmText: '确认撤销',
|
||||||
success: async (res) => {
|
success: async (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
uni.showLoading({ title: '处理中...' })
|
uni.showLoading({ title: '处理中...' })
|
||||||
try {
|
try {
|
||||||
await cancelShipping(user_id, batchNo)
|
await cancelShipping(user_id, batchNo)
|
||||||
uni.showToast({ title: '已取消发货', icon: 'success' })
|
uni.showToast({ title: '已撤销发货', icon: 'success' })
|
||||||
page.value = 1
|
page.value = 1
|
||||||
hasMore.value = true
|
hasMore.value = true
|
||||||
shippedList.value = []
|
shippedList.value = []
|
||||||
@ -762,25 +770,28 @@ function onCancelShipping(shipment) {
|
|||||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部 Tab */
|
/* 顶部 Tab */
|
||||||
.tabs {
|
.tabs {
|
||||||
|
@extend .glass-card;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
background: rgba($bg-card, 0.9);
|
|
||||||
backdrop-filter: blur(20rpx);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@ -1233,10 +1244,6 @@ function onCancelShipping(shipment) {
|
|||||||
from { opacity: 0; transform: translateY(20rpx); }
|
from { opacity: 0; transform: translateY(20rpx); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@keyframes slideUp {
|
|
||||||
from { transform: translateY(100%); }
|
|
||||||
to { transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-spacer {
|
.bottom-spacer {
|
||||||
height: 120rpx;
|
height: 120rpx;
|
||||||
|
|||||||
729
pages/coupons/index.vue
Normal file
729
pages/coupons/index.vue
Normal file
@ -0,0 +1,729 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
|
<view class="header-area">
|
||||||
|
<view class="page-title">我的优惠券</view>
|
||||||
|
<view class="page-subtitle">My Coupons</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Tab栏 - 毛玻璃风格 -->
|
||||||
|
<view class="tab-bar glass-card">
|
||||||
|
<view class="tab-item" :class="{ active: currentTab === 1 }" @click="switchTab(1)">
|
||||||
|
<text class="tab-text">未使用</text>
|
||||||
|
<view class="tab-indicator" v-if="currentTab === 1"></view>
|
||||||
|
</view>
|
||||||
|
<view class="tab-item" :class="{ active: currentTab === 2 }" @click="switchTab(2)">
|
||||||
|
<text class="tab-text">已使用</text>
|
||||||
|
<view class="tab-indicator" v-if="currentTab === 2"></view>
|
||||||
|
</view>
|
||||||
|
<view class="tab-item" :class="{ active: currentTab === 3 }" @click="switchTab(3)">
|
||||||
|
<text class="tab-text">已过期</text>
|
||||||
|
<view class="tab-indicator" v-if="currentTab === 3"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="content-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading && list.length === 0" class="loading-state">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="list.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">🎟️</text>
|
||||||
|
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优惠券列表 -->
|
||||||
|
<view v-else class="coupon-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="coupon-ticket"
|
||||||
|
:class="getCouponClass()"
|
||||||
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||||
|
>
|
||||||
|
<!-- 左侧金额区域 -->
|
||||||
|
<view class="coupon-left">
|
||||||
|
<view class="coupon-value">
|
||||||
|
<text class="coupon-symbol">¥</text>
|
||||||
|
<text class="coupon-amount">{{ formatValue(item.remaining ?? item.amount ?? 0) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="coupon-label">{{ currentTab === 1 ? '可用' : (currentTab === 2 ? '已用' : '过期') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 中间分割线 -->
|
||||||
|
<view class="coupon-divider">
|
||||||
|
<view class="divider-notch top"></view>
|
||||||
|
<view class="divider-dash"></view>
|
||||||
|
<view class="divider-notch bottom"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右侧信息区域 -->
|
||||||
|
<view class="coupon-right">
|
||||||
|
<view class="coupon-header">
|
||||||
|
<text class="coupon-name">{{ item.name || '优惠券' }}</text>
|
||||||
|
<view class="coupon-original" v-if="item.amount && item.remaining !== undefined && item.remaining !== item.amount">
|
||||||
|
<text>原值 ¥{{ formatValue(item.amount) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="coupon-rules">{{ item.rules || '全场通用' }}</text>
|
||||||
|
|
||||||
|
<!-- 使用进度条 -->
|
||||||
|
<view class="coupon-progress" v-if="item.amount && item.remaining !== undefined && item.remaining < item.amount">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill" :style="{ width: getUsedPercent(item) + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="progress-text">已用 {{ formatValue(item.amount - item.remaining) }} ({{ getUsedPercent(item) }}%)</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="coupon-footer">
|
||||||
|
<view class="footer-left">
|
||||||
|
<text class="coupon-expire">{{ formatExpiry(item) }}</text>
|
||||||
|
<text class="coupon-used-time" v-if="currentTab === 2 && item.used_at">使用时间:{{ formatDateTime(item.used_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优化后的按钮位置 -->
|
||||||
|
<view class="coupon-action-wrapper" v-if="currentTab === 1">
|
||||||
|
<view class="use-btn" @click.stop="onUseCoupon(item)">
|
||||||
|
<text class="btn-text">去使用</text>
|
||||||
|
<view class="btn-shine"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="coupon-status" v-else>
|
||||||
|
<text class="status-tag" :class="currentTab === 2 ? 'used' : 'expired'">{{ currentTab === 2 ? '已使用' : '已过期' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="loading && list.length > 0" class="loading-more">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载更多...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!hasMore && list.length > 0" class="no-more">- 到底啦 -</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { getUserCoupons } from '../../api/appUser'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
const currentTab = ref(1)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const hasMore = ref(true)
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
function getUserId() {
|
||||||
|
return uni.getStorageSync('user_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkAuth() {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const userId = getUserId()
|
||||||
|
if (!token || !userId) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先登录',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额 (分转元)
|
||||||
|
function formatValue(val) {
|
||||||
|
return (Number(val) / 100).toFixed(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化有效期
|
||||||
|
function formatExpiry(item) {
|
||||||
|
if (!item.end_time) return '长期有效'
|
||||||
|
const d = new Date(item.end_time)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `有效期至 ${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
function formatDateTime(t) {
|
||||||
|
if (!t) return ''
|
||||||
|
const d = new Date(t)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0')
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day} ${hh}:${mm}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算使用百分比
|
||||||
|
function getUsedPercent(item) {
|
||||||
|
if (!item.amount || !item.remaining) return 0
|
||||||
|
const used = item.amount - item.remaining
|
||||||
|
return Math.floor((used / item.amount) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取空状态文本
|
||||||
|
function getEmptyText() {
|
||||||
|
if (currentTab.value === 1) return '暂无可用优惠券'
|
||||||
|
if (currentTab.value === 2) return '暂无使用记录'
|
||||||
|
return '暂无过期优惠券'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取优惠券样式类
|
||||||
|
function getCouponClass() {
|
||||||
|
if (currentTab.value === 2) return 'coupon-used'
|
||||||
|
if (currentTab.value === 3) return 'coupon-expired'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换Tab
|
||||||
|
function switchTab(tab) {
|
||||||
|
if (currentTab.value === tab) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
|
currentTab.value = tab
|
||||||
|
list.value = []
|
||||||
|
page.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
async function onRefresh() {
|
||||||
|
isRefreshing.value = true
|
||||||
|
page.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
await fetchData(false)
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading.value || !hasMore.value) return
|
||||||
|
await fetchData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
async function fetchData(append = false) {
|
||||||
|
if (!checkAuth()) return
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const userId = getUserId()
|
||||||
|
// status: 0=unused, 1=used, 2=expired
|
||||||
|
const statusMap = { 1: 0, 2: 1, 3: 2 }
|
||||||
|
const res = await getUserCoupons(userId, statusMap[currentTab.value], page.value, pageSize)
|
||||||
|
const items = res.list || res.data || []
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
list.value = [...list.value, ...items]
|
||||||
|
} else {
|
||||||
|
list.value = items
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length < pageSize) {
|
||||||
|
hasMore.value = false
|
||||||
|
} else {
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取优惠券失败:', e)
|
||||||
|
hasMore.value = false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去使用优惠券
|
||||||
|
function onUseCoupon(item) {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
|
// 通常跳转到首页或抽盒页
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-page;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景装饰 - 漂浮光球 (与个人中心统一) */
|
||||||
|
.bg-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -100rpx; right: -100rpx;
|
||||||
|
width: 600rpx; height: 600rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
||||||
|
filter: blur(60rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: float 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 200rpx; left: -200rpx;
|
||||||
|
width: 500rpx; height: 500rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||||
|
filter: blur(50rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: float 15s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(30rpx, 50rpx); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-area {
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab栏 */
|
||||||
|
.tab-bar {
|
||||||
|
@extend .glass-card;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 $spacing-lg;
|
||||||
|
padding: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-text {
|
||||||
|
color: $text-main;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: $brand-primary;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容滚动区 */
|
||||||
|
.content-scroll {
|
||||||
|
height: calc(100vh - 280rpx);
|
||||||
|
padding: $spacing-lg;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优惠券列表 */
|
||||||
|
.coupon-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优惠券卡片 */
|
||||||
|
.coupon-ticket {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-left {
|
||||||
|
width: 180rpx;
|
||||||
|
background: linear-gradient(135deg, #FFF5E6, #fff);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-value {
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-amount {
|
||||||
|
font-size: 56rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
border: 1px solid $brand-primary;
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
.coupon-divider {
|
||||||
|
width: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch.top {
|
||||||
|
top: -12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch.bottom {
|
||||||
|
bottom: -12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-dash {
|
||||||
|
width: 0;
|
||||||
|
height: 80%;
|
||||||
|
border-left: 2rpx dashed #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-name {
|
||||||
|
font-size: $font-md;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-original {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-rules {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-sub;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度条 */
|
||||||
|
.coupon-progress {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 6rpx;
|
||||||
|
background: $bg-secondary;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: $brand-primary;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-expire {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-used-time {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化后的按钮样式 */
|
||||||
|
.coupon-action-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
bottom: 24rpx;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-btn {
|
||||||
|
background: linear-gradient(135deg, #FF8D3F, #FF5C00);
|
||||||
|
padding: 12rpx 32rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(255, 92, 0, 0.3);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(255, 92, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-shine {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.3) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
transform: skewX(-25deg);
|
||||||
|
animation: shine 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { left: -100%; }
|
||||||
|
20%, 100% { left: 150%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
background: #F5F5F5;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过期/已使用状态 */
|
||||||
|
.coupon-used .coupon-left,
|
||||||
|
.coupon-expired .coupon-left {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-used .coupon-value,
|
||||||
|
.coupon-expired .coupon-value,
|
||||||
|
.coupon-used .coupon-label,
|
||||||
|
.coupon-expired .coupon-label {
|
||||||
|
color: $text-tertiary;
|
||||||
|
border-color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-used .coupon-name,
|
||||||
|
.coupon-expired .coupon-name {
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多 */
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border: 3rpx solid $bg-secondary;
|
||||||
|
border-top-color: $text-tertiary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,18 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- 品牌级背景装饰系统 -->
|
<!-- 品牌级背景装饰系统 - 统一漂浮光球 -->
|
||||||
<view class="premium-bg">
|
<view class="bg-decoration"></view>
|
||||||
<view class="bg-shape circle-1"></view>
|
|
||||||
<view class="bg-shape circle-2"></view>
|
|
||||||
<view class="bg-shape mesh-gradient"></view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 顶部导航栏 (搜索) -->
|
<!-- 顶部导航栏 (搜索) -->
|
||||||
<view class="nav-header">
|
<view class="nav-header">
|
||||||
<view class="brand-logo">
|
<!-- 品牌标识已按需移除 -->
|
||||||
<text class="brand-text">柯大鸭潮玩</text>
|
|
||||||
<view class="brand-star">✨</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 滚动区域 -->
|
<!-- 滚动区域 -->
|
||||||
@ -333,7 +326,7 @@ export default {
|
|||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||||
return {
|
return {
|
||||||
title: '柯大鸭潮玩 - 开箱惊喜等你来',
|
title: '柯大鸭 - 开箱惊喜等你来',
|
||||||
path: `/pages/index/index?invite_code=${inviteCode}`,
|
path: `/pages/index/index?invite_code=${inviteCode}`,
|
||||||
imageUrl: '/static/logo.png'
|
imageUrl: '/static/logo.png'
|
||||||
}
|
}
|
||||||
@ -342,7 +335,7 @@ export default {
|
|||||||
onShareTimeline() {
|
onShareTimeline() {
|
||||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||||
return {
|
return {
|
||||||
title: '柯大鸭潮玩 - 开箱惊喜等你来',
|
title: '柯大鸭 - 开箱惊喜等你来',
|
||||||
query: `invite_code=${inviteCode}`,
|
query: `invite_code=${inviteCode}`,
|
||||||
imageUrl: '/static/logo.png'
|
imageUrl: '/static/logo.png'
|
||||||
}
|
}
|
||||||
@ -352,88 +345,31 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
/* ============================================
|
/* ============================================
|
||||||
柯大鸭潮玩 - 首页样式 (V6.0 Pro Refined)
|
柯大鸭 - 首页样式 (V6.0 Pro Refined)
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #F8F9FB;
|
background-color: $bg-page;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 品牌级背景系统 ========== */
|
|
||||||
.premium-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-shape {
|
|
||||||
position: absolute;
|
|
||||||
filter: blur(100rpx);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-1 {
|
|
||||||
width: 600rpx;
|
|
||||||
height: 600rpx;
|
|
||||||
background: radial-gradient(circle, rgba(255, 107, 0, 0.15) 0%, transparent 70%);
|
|
||||||
top: -200rpx;
|
|
||||||
right: -100rpx;
|
|
||||||
animation: float 10s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-2 {
|
|
||||||
width: 500rpx;
|
|
||||||
height: 500rpx;
|
|
||||||
background: radial-gradient(circle, rgba(255, 193, 7, 0.1) 0%, transparent 70%);
|
|
||||||
bottom: 200rpx;
|
|
||||||
left: -100rpx;
|
|
||||||
animation: float 12s ease-in-out infinite alternate-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mesh-gradient {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg, rgba(255, 107, 0, 0.03) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 193, 7, 0.03) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 顶部导航栏 ========== */
|
/* ========== 顶部导航栏 ========== */
|
||||||
.nav-header {
|
.nav-header {
|
||||||
display: flex;
|
height: env(safe-area-inset-top);
|
||||||
align-items: center;
|
|
||||||
padding: $spacing-md $spacing-lg;
|
|
||||||
padding-top: calc($spacing-md + env(safe-area-inset-top));
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
background: rgba($bg-page, 0.8);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-text {
|
|
||||||
font-size: 44rpx;
|
|
||||||
font-weight: 900;
|
|
||||||
color: $text-main;
|
|
||||||
font-style: italic;
|
|
||||||
letter-spacing: -2rpx;
|
|
||||||
background: linear-gradient(135deg, #1A1A1A 0%, #444 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-star {
|
/* ========== 滚动主内容区 ========== */
|
||||||
font-size: 28rpx;
|
|
||||||
margin-left: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ========== 滚动主内容区 ========== */
|
/* ========== 滚动主内容区 ========== */
|
||||||
@ -441,19 +377,17 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding-top: $spacing-sm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Banner Container (Modern Floating) */
|
/* Banner Container (Modern Floating) */
|
||||||
.banner-container {
|
.banner-container {
|
||||||
padding: 0 $spacing-lg $spacing-xl;
|
padding: $spacing-sm $spacing-lg $spacing-xl;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-swiper {
|
.banner-swiper {
|
||||||
height: 360rpx;
|
height: 360rpx;
|
||||||
overflow: visible; /* 让阴影不被切断 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-card {
|
.banner-card {
|
||||||
@ -464,7 +398,7 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
transform: scale(0.96);
|
transform: scale(0.96);
|
||||||
transition: all 0.5s $ease-out;
|
transition: all 0.5s $ease-out;
|
||||||
box-shadow: 0 16rpx 48rpx rgba(255, 107, 0, 0.1);
|
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-card.active {
|
.banner-card.active {
|
||||||
@ -541,14 +475,15 @@ export default {
|
|||||||
/* Notice Bar V2 (Minimalist) */
|
/* Notice Bar V2 (Minimalist) */
|
||||||
.notice-bar-v2 {
|
.notice-bar-v2 {
|
||||||
margin: 0 $spacing-lg $spacing-xl;
|
margin: 0 $spacing-lg $spacing-xl;
|
||||||
background: #FFFFFF;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
border-radius: 32rpx;
|
border-radius: 32rpx;
|
||||||
padding: 24rpx 32rpx;
|
padding: 24rpx 32rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: $shadow-sm;
|
||||||
border: 1rpx solid rgba(0,0,0,0.02);
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice-icon { font-size: 32rpx; }
|
.notice-icon { font-size: 32rpx; }
|
||||||
@ -587,6 +522,7 @@ export default {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title::before {
|
.section-title::before {
|
||||||
@ -619,6 +555,7 @@ export default {
|
|||||||
padding: 22rpx;
|
padding: 22rpx;
|
||||||
box-shadow: $shadow-card;
|
box-shadow: $shadow-card;
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-card-large:active {
|
.game-card-large:active {
|
||||||
@ -757,17 +694,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.activity-item {
|
.activity-item {
|
||||||
background: #FFFFFF;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
border-radius: 32rpx;
|
backdrop-filter: blur(10rpx);
|
||||||
|
border-radius: $radius-xl;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.03);
|
box-shadow: $shadow-sm;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-item:active {
|
.activity-item:active {
|
||||||
transform: translateY(4rpx);
|
transform: translateY(4rpx);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-thumb-box {
|
.activity-thumb-box {
|
||||||
|
|||||||
403
pages/invites/index.vue
Normal file
403
pages/invites/index.vue
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
|
<view class="header-area">
|
||||||
|
<view class="page-title">邀请记录</view>
|
||||||
|
<view class="page-subtitle">Invitations</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 统计卡片 - 毛玻璃风格 -->
|
||||||
|
<view class="stats-card glass-card">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-num">{{ list.length }}</text>
|
||||||
|
<text class="stat-label">邀请人数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider"></view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-num">{{ getRewardsTotal() }}</text>
|
||||||
|
<text class="stat-label">累计奖励</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="content-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading && list.length === 0" class="loading-state">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="list.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">👥</text>
|
||||||
|
<text class="empty-text">暂无邀请记录</text>
|
||||||
|
<text class="empty-hint">分享给好友,一起来玩吧!</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 邀请列表 -->
|
||||||
|
<view v-else class="invite-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="invite-item"
|
||||||
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||||
|
>
|
||||||
|
<image class="invite-avatar" :src="item.avatar || '/static/logo.png'" mode="aspectFill"></image>
|
||||||
|
<view class="invite-info">
|
||||||
|
<text class="invite-name">{{ item.nickname || '用户' + item.id }}</text>
|
||||||
|
<text class="invite-time">{{ formatDate(item.created_at) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="invite-status">
|
||||||
|
<text class="status-text">已邀请</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="loading && list.length > 0" class="loading-more">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载更多...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!hasMore && list.length > 0" class="no-more">- 到底啦 -</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getUserInvites } from '../../api/appUser'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const hasMore = ref(true)
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
function getUserId() {
|
||||||
|
return uni.getStorageSync('user_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkAuth() {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const userId = getUserId()
|
||||||
|
if (!token || !userId) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先登录',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(t) {
|
||||||
|
if (!t) return ''
|
||||||
|
const d = new Date(t)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算累计奖励
|
||||||
|
function getRewardsTotal() {
|
||||||
|
// 根据实际业务逻辑计算,目前简单显示邀请人数 × 积分奖励
|
||||||
|
const rewardPerInvite = 10 // 每邀请一人奖励积分
|
||||||
|
return list.value.length * rewardPerInvite
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
async function onRefresh() {
|
||||||
|
isRefreshing.value = true
|
||||||
|
page.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
await fetchData(false)
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading.value || !hasMore.value) return
|
||||||
|
await fetchData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
async function fetchData(append = false) {
|
||||||
|
if (!checkAuth()) return
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const userId = getUserId()
|
||||||
|
const res = await getUserInvites(userId, page.value, pageSize)
|
||||||
|
const items = res.list || res.data || []
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
list.value = [...list.value, ...items]
|
||||||
|
} else {
|
||||||
|
list.value = items
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length < pageSize) {
|
||||||
|
hasMore.value = false
|
||||||
|
} else {
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取邀请记录失败:', e)
|
||||||
|
hasMore.value = false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-page;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-area {
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 统计卡片 */
|
||||||
|
.stats-card {
|
||||||
|
@extend .glass-card;
|
||||||
|
margin: 0 $spacing-lg $spacing-lg;
|
||||||
|
padding: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $brand-primary;
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 60rpx;
|
||||||
|
background: $border-color-light;
|
||||||
|
margin: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容滚动区 */
|
||||||
|
.content-scroll {
|
||||||
|
height: calc(100vh - 400rpx);
|
||||||
|
padding: 0 $spacing-lg $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 邀请列表 */
|
||||||
|
.invite-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-avatar {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $bg-secondary;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-name {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $uni-color-success;
|
||||||
|
background: rgba($uni-color-success, 0.1);
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多 */
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 30rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border: 3rpx solid $bg-secondary;
|
||||||
|
border-top-color: $text-tertiary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
623
pages/item-cards/index.vue
Normal file
623
pages/item-cards/index.vue
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
|
<view class="header-area">
|
||||||
|
<view class="page-title">我的道具卡</view>
|
||||||
|
<view class="page-subtitle">My Item Cards</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Tab栏 - 毛玻璃风格 -->
|
||||||
|
<view class="tab-bar glass-card">
|
||||||
|
<view class="tab-item" :class="{ active: currentTab === 0 }" @click="switchTab(0)">
|
||||||
|
<text class="tab-text">未使用</text>
|
||||||
|
<view class="tab-indicator" v-if="currentTab === 0"></view>
|
||||||
|
</view>
|
||||||
|
<view class="tab-item" :class="{ active: currentTab === 1 }" @click="switchTab(1)">
|
||||||
|
<text class="tab-text">已使用</text>
|
||||||
|
<view class="tab-indicator" v-if="currentTab === 1"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="content-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading && list.length === 0" class="loading-state">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="list.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">🃏</text>
|
||||||
|
<text class="empty-text">{{ currentTab === 0 ? '暂无可用道具卡' : '暂无使用记录' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 道具卡列表 -->
|
||||||
|
<view v-else class="item-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="item-ticket"
|
||||||
|
:class="{ 'used': currentTab === 1 }"
|
||||||
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||||
|
>
|
||||||
|
<!-- 左侧图标区域 -->
|
||||||
|
<view class="ticket-left">
|
||||||
|
<view class="card-icon-wrap">
|
||||||
|
<text class="card-icon">{{ getCardIcon(item.type || item.name) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-count-badge" v-if="currentTab === 0">
|
||||||
|
<text class="count-num">×{{ item.remaining ?? item.count ?? 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 中间分割线 -->
|
||||||
|
<view class="ticket-divider">
|
||||||
|
<view class="divider-notch top"></view>
|
||||||
|
<view class="divider-dash"></view>
|
||||||
|
<view class="divider-notch bottom"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右侧信息区域 -->
|
||||||
|
<view class="ticket-right">
|
||||||
|
<view class="card-info">
|
||||||
|
<text class="card-name">{{ item.name || item.title || '道具卡' }}</text>
|
||||||
|
<text class="card-desc">{{ item.description || item.rules || '可在抽奖时使用' }}</text>
|
||||||
|
<view class="usage-info" v-if="currentTab === 1">
|
||||||
|
<text class="card-use-time" v-if="item.used_at">使用时间:{{ formatDateTime(item.used_at) }}</text>
|
||||||
|
<view class="usage-detail" v-if="item.used_activity_name">
|
||||||
|
<text class="detail-label">使用于:</text>
|
||||||
|
<text class="detail-val">{{ item.used_activity_name }}</text>
|
||||||
|
<text class="detail-val" v-if="item.used_issue_number"> - 期号 {{ item.used_issue_number }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="usage-detail" v-if="item.used_reward_name">
|
||||||
|
<text class="detail-label">效果:</text>
|
||||||
|
<text class="detail-val highlight">{{ item.used_reward_name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 优化后的按钮位置 -->
|
||||||
|
<view class="ticket-action-wrapper" v-if="currentTab === 0">
|
||||||
|
<view class="use-btn" @click.stop="onUseCard(item)">
|
||||||
|
<text class="btn-text">去使用</text>
|
||||||
|
<view class="btn-shine"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-used-badge" v-else>
|
||||||
|
<text class="used-text">已使用</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getItemCards } from '../../api/appUser'
|
||||||
|
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
const currentTab = ref(0)
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
function getUserId() {
|
||||||
|
return uni.getStorageSync('user_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkAuth() {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const userId = getUserId()
|
||||||
|
if (!token || !userId) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先登录',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
function formatDateTime(t) {
|
||||||
|
if (!t) return ''
|
||||||
|
const d = new Date(t)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0')
|
||||||
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const ss = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取卡片图标
|
||||||
|
function getCardIcon(type) {
|
||||||
|
const t = String(type || '').toLowerCase()
|
||||||
|
if (t.includes('透视')) return '👁️'
|
||||||
|
if (t.includes('提示')) return '💡'
|
||||||
|
if (t.includes('重置')) return '🔄'
|
||||||
|
if (t.includes('翻倍')) return '✨'
|
||||||
|
if (t.includes('保护')) return '🛡️'
|
||||||
|
return '🃏'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换Tab
|
||||||
|
function switchTab(tab) {
|
||||||
|
if (currentTab.value === tab) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
|
currentTab.value = tab
|
||||||
|
list.value = []
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
async function onRefresh() {
|
||||||
|
isRefreshing.value = true
|
||||||
|
await fetchData()
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
async function fetchData() {
|
||||||
|
if (!checkAuth()) return
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const userId = getUserId()
|
||||||
|
// status: 1=unused, 2=used
|
||||||
|
const status = currentTab.value === 0 ? 1 : 2
|
||||||
|
const res = await getItemCards(userId, status)
|
||||||
|
|
||||||
|
let items = Array.isArray(res) ? res : (res.list || res.data || [])
|
||||||
|
|
||||||
|
// 处理数据,确保count字段存在
|
||||||
|
items = items.map(item => ({
|
||||||
|
...item,
|
||||||
|
count: item.count ?? item.remaining ?? 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 未使用状态时过滤掉数量为0的卡片
|
||||||
|
if (currentTab.value === 0) {
|
||||||
|
items = items.filter(i => i.count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.value = items
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取道具卡失败:', e)
|
||||||
|
list.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去使用道具卡
|
||||||
|
function onUseCard(item) {
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
|
// 道具卡通常去首页或指定的活动页
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-page;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-area {
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab栏 */
|
||||||
|
.tab-bar {
|
||||||
|
@extend .glass-card;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 $spacing-lg;
|
||||||
|
padding: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-text {
|
||||||
|
color: $text-main;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: $brand-primary;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容滚动区 */
|
||||||
|
.content-scroll {
|
||||||
|
height: calc(100vh - 280rpx);
|
||||||
|
padding: $spacing-lg;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 道具卡列表 */
|
||||||
|
.item-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 票券式卡片 */
|
||||||
|
.item-ticket {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-left {
|
||||||
|
width: 180rpx;
|
||||||
|
background: linear-gradient(135deg, #E6F7FF, #fff);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrap {
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 150, 250, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-count-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12rpx;
|
||||||
|
right: 12rpx;
|
||||||
|
background: rgba(0, 150, 250, 0.1);
|
||||||
|
padding: 2rpx 10rpx;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-num {
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0096FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分割线 */
|
||||||
|
.ticket-divider {
|
||||||
|
width: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background: $bg-page;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch.top {
|
||||||
|
top: -12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-notch.bottom {
|
||||||
|
bottom: -12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-dash {
|
||||||
|
width: 0;
|
||||||
|
height: 80%;
|
||||||
|
border-left: 2rpx dashed #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-right {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-right: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: $font-md;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
color: $text-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-sub;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: $font-xs;
|
||||||
|
color: $text-sub;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-info {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
border-top: 1rpx dashed #eee;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-use-time {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-detail {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-val {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.ticket-action-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-btn {
|
||||||
|
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 150, 250, 0.2);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.92);
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0, 150, 250, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-shine {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.3) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
transform: skewX(-25deg);
|
||||||
|
animation: shine 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
0% { left: -100%; }
|
||||||
|
20%, 100% { left: 150%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-used-badge {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #F5F5F5;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已使用状态 */
|
||||||
|
.item-ticket.used {
|
||||||
|
.ticket-left {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrap {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.spinner {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border: 3rpx solid $bg-secondary;
|
||||||
|
border-top-color: $text-tertiary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -11,7 +11,6 @@
|
|||||||
<view class="logo-box">
|
<view class="logo-box">
|
||||||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="app-name">柯大鸭潮玩</view>
|
|
||||||
<view class="welcome-text">开启欧气之旅 ✨</view>
|
<view class="welcome-text">开启欧气之旅 ✨</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@ -11,35 +11,36 @@
|
|||||||
<view class="user-meta">
|
<view class="user-meta">
|
||||||
<view class="name-row">
|
<view class="name-row">
|
||||||
<text class="nickname">{{ nickname || '未登录' }}</text>
|
<text class="nickname">{{ nickname || '未登录' }}</text>
|
||||||
<view class="level-badge" v-if="nickname">
|
<view class="level-badge" v-if="title">
|
||||||
<image class="level-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNiAzSDE4TDIxIDlMOSAyMSAzIDlMNiAzWiIgLz48L3N2Zz4=" mode="aspectFit"></image>
|
<image class="level-icon" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNiAzSDE4TDIxIDlMOSAyMSAzIDlMNiAzWiIgLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||||
<text class="level-text">Lv1 青铜</text>
|
<text class="level-text">Lv1 {{ title }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="progress-container" v-if="nickname">
|
<view class="userid" v-if="userId">ID: {{ userId }}</view>
|
||||||
|
<!-- <view class="progress-container" v-if="nickname">
|
||||||
<view class="progress-bar">
|
<view class="progress-bar">
|
||||||
<view class="progress-fill" style="width: 20%;"></view>
|
<view class="progress-fill" style="width: 20%;"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="progress-text">100/5000 升级Lv2</text>
|
<text class="progress-text">100/5000 升级Lv2</text>
|
||||||
</view>
|
</view> -->
|
||||||
<view class="userid" v-else>ID: {{ userId || '-' }}</view>
|
<!-- <view class="userid" v-else>ID: {{ userId || '-' }}</view> -->
|
||||||
</view>
|
</view>
|
||||||
<view class="join-btn" @click="handleJoin" v-if="!nickname">立即登录</view>
|
<view class="join-btn" @click="handleJoin" v-if="!nickname">立即登录</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 数据统计栏 -->
|
<!-- 数据统计栏 -->
|
||||||
<view class="stats-row">
|
<view class="stats-row">
|
||||||
<view class="stat-item" @click="showPointsPopup">
|
<view class="stat-item" @click="toPointsPage">
|
||||||
<text class="stat-num">{{ pointsBalance || 0 }}</text>
|
<text class="stat-num">{{ pointsBalance || 0 }}</text>
|
||||||
<text class="stat-label">积分</text>
|
<text class="stat-label">积分</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-divider"></view>
|
<view class="stat-divider"></view>
|
||||||
<view class="stat-item" @click="showCouponsPopup">
|
<view class="stat-item" @click="toCouponsPage">
|
||||||
<text class="stat-num">{{ stats.coupon_count || 0 }}</text>
|
<text class="stat-num">{{ stats.coupon_count || 0 }}</text>
|
||||||
<text class="stat-label">优惠券</text>
|
<text class="stat-label">优惠券</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-divider"></view>
|
<view class="stat-divider"></view>
|
||||||
<view class="stat-item" @click="showItemCardsPopup">
|
<view class="stat-item" @click="toItemCardsPage">
|
||||||
<text class="stat-num">{{ stats.item_card_count || 0 }}</text>
|
<text class="stat-num">{{ stats.item_card_count || 0 }}</text>
|
||||||
<text class="stat-label">道具卡</text>
|
<text class="stat-label">道具卡</text>
|
||||||
</view>
|
</view>
|
||||||
@ -130,25 +131,25 @@
|
|||||||
<text class="section-title">常用功能</text>
|
<text class="section-title">常用功能</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="grid-menu">
|
<view class="grid-menu">
|
||||||
<view class="menu-item" @click="showCouponsPopup">
|
<view class="menu-item" @click="toCouponsPage">
|
||||||
<view class="menu-icon-box">
|
<view class="menu-icon-box">
|
||||||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA2SDNhMiAyIDAgMCAwLTIgMnY4YTIgMiAwIDAgMCAyIDJoMThhMiAyIDAgMCAwIDItMnYtOGEyIDIgMCAwIDAtMi0yWiIgLz48cGF0aCBkPSJNNiAxMnYtMiIgLz48cGF0aCBkPSJNNiAxNnYtMiIgLz48cGF0aCBkPSJNMTYgNnYxMiIgLz48L3N2Zz4=" mode="aspectFit"></image>
|
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA2SDNhMiAyIDAgMCAwLTIgMnY4YTIgMiAwIDAgMCAyIDJoMThhMiAyIDAgMCAwIDItMnYtOGEyIDIgMCAwIDAtMi0yWiIgLz48cGF0aCBkPSJNNiAxMnYtMiIgLz48cGF0aCBkPSJNNiAxNnYtMiIgLz48cGF0aCBkPSJNMTYgNnYxMiIgLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-label">优惠券</text>
|
<text class="menu-label">优惠券</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="showItemCardsPopup">
|
<view class="menu-item" @click="toItemCardsPage">
|
||||||
<view class="menu-icon-box">
|
<view class="menu-icon-box">
|
||||||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iNCIgLz48cGF0aCBkPSJNMjEgMTVsLTUuNDMtMy4yM2EyIDIgMCAwIDAtMS44NCAwbC0yLjMxIDEuMzciIC8+PC9zdmc+" mode="aspectFit"></image>
|
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSIxOCIgaGVpZ2h0PSIxOCIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iNCIgLz48cGF0aCBkPSJNMjEgMTVsLTUuNDMtMy4yM2EyIDIgMCAwIDAtMS44NCAwbC0yLjMxIDEuMzciIC8+PC9zdmc+" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-label">道具卡</text>
|
<text class="menu-label">道具卡</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="showTasksPopup">
|
<view class="menu-item" @click="toTasksPage">
|
||||||
<view class="menu-icon-box">
|
<view class="menu-icon-box">
|
||||||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDExbDMgM0wyMiA0Ii8+PHBhdGggZD0iTTIxIDEydjdhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJWNWEyIDIgMCAwIDEgMi0yaDExIi8+PC9zdmc+" mode="aspectFit"></image>
|
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik05IDExbDMgM0wyMiA0Ii8+PHBhdGggZD0iTTIxIDEydjdhMiAyIDAgMCAxLTIgMkg1YTIgMiAwIDAgMS0yLTJWNWEyIDIgMCAwIDEgMi0yaDExIi8+PC9zdmc+" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<text class="menu-label">任务中心</text>
|
<text class="menu-label">任务中心</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="showInvitesPopup">
|
<view class="menu-item" @click="toInvitesPage">
|
||||||
<view class="menu-icon-box">
|
<view class="menu-icon-box">
|
||||||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNyAyMXYtMmE0IDQgMCAwIDAtNC00SDdhNCA0IDAgMCAwLTQgNHYyIj48L3BhdGg+PGNpcmNsZSBjeD0iMTAiIGN5PSI3IiByPSI0Ij48L2NpcmNsZT48cGF0aCBkPSJNMjMgMjF2LTJhNCA0IDAgMCAwLTMtMy4yNyI+PC9wYXRoPjxwYXRoIGQ9Ik0xNiAzLjEzYTQgNCAwIDAgMSAwIDcuNzUiPjwvcGF0aD48L3N2Zz4=" mode="aspectFit"></image>
|
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNyAyMXYtMmE0IDQgMCAwIDAtNC00SDdhNCA0IDAgMCAwLTQgNHYyIj48L3BhdGg+PGNpcmNsZSBjeD0iMTAiIGN5PSI3IiByPSI0Ij48L2NpcmNsZT48cGF0aCBkPSJNMjMgMjF2LTJhNCA0IDAgMCAwLTMtMy4yNyI+PC9wYXRoPjxwYXRoIGQ9Ik0xNiAzLjEzYTQgNCAwIDAgMSAwIDcuNzUiPjwvcGF0aD48L3N2Zz4=" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
@ -462,6 +463,7 @@ export default {
|
|||||||
userId: '',
|
userId: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
|
title: '', // 用户头衔
|
||||||
inviteCode: '',
|
inviteCode: '',
|
||||||
pointsBalance: 0,
|
pointsBalance: 0,
|
||||||
|
|
||||||
@ -584,6 +586,7 @@ export default {
|
|||||||
this.nickname = cachedUser.nickname
|
this.nickname = cachedUser.nickname
|
||||||
this.avatar = cachedUser.avatar
|
this.avatar = cachedUser.avatar
|
||||||
this.inviteCode = cachedUser.invite_code
|
this.inviteCode = cachedUser.invite_code
|
||||||
|
this.title = cachedUser.title || ''
|
||||||
this.pointsBalance = this.normalizePointsBalance(cachedUser.points_balance)
|
this.pointsBalance = this.normalizePointsBalance(cachedUser.points_balance)
|
||||||
} else if (cachedUserId) {
|
} else if (cachedUserId) {
|
||||||
this.userId = cachedUserId
|
this.userId = cachedUserId
|
||||||
@ -610,6 +613,7 @@ export default {
|
|||||||
this.userId = res.id
|
this.userId = res.id
|
||||||
this.nickname = res.nickname
|
this.nickname = res.nickname
|
||||||
this.avatar = res.avatar
|
this.avatar = res.avatar
|
||||||
|
this.title = res.title || res.level_name || ''
|
||||||
this.inviteCode = res.invite_code
|
this.inviteCode = res.invite_code
|
||||||
this.pointsBalance = this.normalizePointsBalance(res.points_balance)
|
this.pointsBalance = this.normalizePointsBalance(res.points_balance)
|
||||||
uni.setStorageSync('user_info', res)
|
uni.setStorageSync('user_info', res)
|
||||||
@ -629,6 +633,7 @@ export default {
|
|||||||
this.userId = ''
|
this.userId = ''
|
||||||
this.nickname = ''
|
this.nickname = ''
|
||||||
this.avatar = ''
|
this.avatar = ''
|
||||||
|
this.title = ''
|
||||||
this.pointsBalance = 0
|
this.pointsBalance = 0
|
||||||
this.stats = { coupon_count: 0, item_card_count: 0 }
|
this.stats = { coupon_count: 0, item_card_count: 0 }
|
||||||
},
|
},
|
||||||
@ -645,6 +650,21 @@ export default {
|
|||||||
toAddresses() {
|
toAddresses() {
|
||||||
uni.navigateTo({ url: '/pages/address/index' })
|
uni.navigateTo({ url: '/pages/address/index' })
|
||||||
},
|
},
|
||||||
|
toPointsPage() {
|
||||||
|
uni.navigateTo({ url: '/pages/points/index' })
|
||||||
|
},
|
||||||
|
toCouponsPage() {
|
||||||
|
uni.navigateTo({ url: '/pages/coupons/index' })
|
||||||
|
},
|
||||||
|
toItemCardsPage() {
|
||||||
|
uni.navigateTo({ url: '/pages/item-cards/index' })
|
||||||
|
},
|
||||||
|
toInvitesPage() {
|
||||||
|
uni.navigateTo({ url: '/pages/invites/index' })
|
||||||
|
},
|
||||||
|
toTasksPage() {
|
||||||
|
uni.navigateTo({ url: '/pages/tasks/index' })
|
||||||
|
},
|
||||||
toHelp() {
|
toHelp() {
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: ['购买协议', '用户协议'],
|
itemList: ['购买协议', '用户协议'],
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<view v-if="loading" class="loading-state">
|
<view v-if="loading" class="loading-state">
|
||||||
<view class="loading-spinner"></view>
|
<view class="loading-spinner"></view>
|
||||||
@ -64,7 +66,9 @@
|
|||||||
<view class="item-meta">
|
<view class="item-meta">
|
||||||
<view class="price-wrap">
|
<view class="price-wrap">
|
||||||
<text class="currency" v-if="item.price > 0">¥</text>
|
<text class="currency" v-if="item.price > 0">¥</text>
|
||||||
<text class="price">{{ item.price > 0 ? formatPrice(item.price) : '奖品' }}</text>
|
<text class="price" v-if="item.price > 0">{{ formatPrice(item.price) }}</text>
|
||||||
|
<text class="price" v-else-if="order.points_amount > 0">{{ Math.floor(order.points_amount / (order.items && order.items.length || 1)) }}积分</text>
|
||||||
|
<text class="price" v-else>奖品</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="item-quantity">x{{ item.quantity }}</text>
|
<text class="item-quantity">x{{ item.quantity }}</text>
|
||||||
</view>
|
</view>
|
||||||
@ -86,9 +90,11 @@
|
|||||||
<text class="tag" v-if="order.issue_number">第{{ order.issue_number }}期</text>
|
<text class="tag" v-if="order.issue_number">第{{ order.issue_number }}期</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="item-meta">
|
<view class="item-meta">
|
||||||
<view class="price-wrap">
|
<view class="price-wrap">
|
||||||
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
||||||
<text class="price">{{ order.actual_amount > 0 ? formatPrice(order.actual_amount) : '奖品' }}</text>
|
<text class="price" v-if="order.actual_amount > 0">{{ formatPrice(order.actual_amount) }}</text>
|
||||||
|
<text class="price" v-else-if="order.points_amount > 0">{{ order.points_amount }}积分</text>
|
||||||
|
<text class="price" v-else>奖品</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="item-quantity">x1</text>
|
<text class="item-quantity">x1</text>
|
||||||
</view>
|
</view>
|
||||||
@ -128,7 +134,8 @@
|
|||||||
<view class="section-card amount-section">
|
<view class="section-card amount-section">
|
||||||
<view class="info-row">
|
<view class="info-row">
|
||||||
<text class="label">商品总额</text>
|
<text class="label">商品总额</text>
|
||||||
<text class="value">¥{{ formatPrice(order.total_amount) }}</text>
|
<text class="value" v-if="order.points_amount > 0">{{ order.points_amount }}积分</text>
|
||||||
|
<text class="value" v-else>¥{{ formatPrice(order.total_amount) }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 优惠券 -->
|
<!-- 优惠券 -->
|
||||||
@ -159,7 +166,9 @@
|
|||||||
<text class="total-label">{{ order.actual_amount > 0 ? '实付款' : '状态' }}</text>
|
<text class="total-label">{{ order.actual_amount > 0 ? '实付款' : '状态' }}</text>
|
||||||
<view class="total-price-wrap">
|
<view class="total-price-wrap">
|
||||||
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
||||||
<text class="total-price">{{ order.actual_amount > 0 ? formatPrice(order.actual_amount) : '无需支付' }}</text>
|
<text class="total-price" v-if="order.actual_amount > 0">{{ formatPrice(order.actual_amount) }}</text>
|
||||||
|
<text class="total-price" v-else-if="order.points_amount > 0">{{ order.points_amount }}积分</text>
|
||||||
|
<text class="total-price" v-else>无需支付</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -410,8 +419,47 @@ function showProofHelp() {
|
|||||||
.page-container {
|
.page-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg-page;
|
background: $bg-page;
|
||||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(140rpx + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景装饰 - 漂浮光球 (与各主要页面统一) */
|
||||||
|
.bg-decoration {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -100rpx; right: -100rpx;
|
||||||
|
width: 600rpx; height: 600rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
||||||
|
filter: blur(60rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: float 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 200rpx; left: -200rpx;
|
||||||
|
width: 500rpx; height: 500rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||||
|
filter: blur(50rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: float 15s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(30rpx, 50rpx); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 状态头部背景 */
|
/* 状态头部背景 */
|
||||||
@ -420,6 +468,7 @@ function showProofHelp() {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0 0 40rpx 40rpx;
|
border-radius: 0 0 40rpx 40rpx;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&.status-pending { background: linear-gradient(135deg, #FF9F43 0%, #FF6B6B 100%); }
|
&.status-pending { background: linear-gradient(135deg, #FF9F43 0%, #FF6B6B 100%); }
|
||||||
&.status-completed { background: linear-gradient(135deg, #2ECC71 0%, #27AE60 100%); }
|
&.status-completed { background: linear-gradient(135deg, #2ECC71 0%, #27AE60 100%); }
|
||||||
@ -428,7 +477,7 @@ function showProofHelp() {
|
|||||||
.bg-circle {
|
.bg-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
&.c1 { width: 300rpx; height: 300rpx; top: -100rpx; right: -50rpx; }
|
&.c1 { width: 300rpx; height: 300rpx; top: -100rpx; right: -50rpx; }
|
||||||
&.c2 { width: 200rpx; height: 200rpx; bottom: 50rpx; left: -50rpx; }
|
&.c2 { width: 200rpx; height: 200rpx; bottom: 50rpx; left: -50rpx; }
|
||||||
@ -438,13 +487,14 @@ function showProofHelp() {
|
|||||||
/* 状态卡片 */
|
/* 状态卡片 */
|
||||||
.status-card {
|
.status-card {
|
||||||
margin: -60rpx $spacing-lg 0;
|
margin: -60rpx $spacing-lg 0;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(20rpx);
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-xl;
|
||||||
padding: $spacing-xl;
|
padding: $spacing-xl;
|
||||||
box-shadow: $shadow-card;
|
box-shadow: $shadow-card;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
|
||||||
.status-content {
|
.status-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -488,11 +538,15 @@ function showProofHelp() {
|
|||||||
/* 通用卡片样式 */
|
/* 通用卡片样式 */
|
||||||
.section-card {
|
.section-card {
|
||||||
margin: $spacing-lg;
|
margin: $spacing-lg;
|
||||||
background: $bg-card;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(20rpx);
|
||||||
border-radius: $radius-lg;
|
border-radius: $radius-lg;
|
||||||
padding: $spacing-lg;
|
padding: $spacing-lg;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: $shadow-sm;
|
||||||
animation: slideUp 0.4s ease-out;
|
animation: slideUp 0.4s ease-out;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
<!-- 顶部 Tab -->
|
<!-- 顶部 Tab -->
|
||||||
<view class="tabs">
|
<view class="tabs glass-card">
|
||||||
<view
|
<view
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: currentTab === 'pending' }"
|
:class="{ active: currentTab === 'pending' }"
|
||||||
@ -161,7 +163,10 @@ function formatTime(t) {
|
|||||||
return `${m}-${day} ${hh}:${mm}`
|
return `${m}-${day} ${hh}:${mm}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAmount(a) {
|
function formatAmount(a, item) {
|
||||||
|
if (item && item.points_amount > 0) {
|
||||||
|
return `${item.points_amount}积分`
|
||||||
|
}
|
||||||
if (a === undefined || a === null) return '¥0.00'
|
if (a === undefined || a === null) return '¥0.00'
|
||||||
const n = Number(a)
|
const n = Number(a)
|
||||||
if (Number.isNaN(n)) return '¥0.00'
|
if (Number.isNaN(n)) return '¥0.00'
|
||||||
@ -175,6 +180,7 @@ function shouldShowAmountLabel(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAmountText(item) {
|
function getAmountText(item) {
|
||||||
|
if (item.points_amount > 0) return formatAmount(0, item)
|
||||||
const amount = item.actual_amount || item.total_amount
|
const amount = item.actual_amount || item.total_amount
|
||||||
if (amount > 0) return formatAmount(amount)
|
if (amount > 0) return formatAmount(amount)
|
||||||
|
|
||||||
@ -283,6 +289,7 @@ function getStatusClass(item) {
|
|||||||
|
|
||||||
function switchTab(tab) {
|
function switchTab(tab) {
|
||||||
if (currentTab.value === tab) return
|
if (currentTab.value === tab) return
|
||||||
|
uni.vibrateShort({ type: 'light' })
|
||||||
currentTab.value = tab
|
currentTab.value = tab
|
||||||
fetchOrders(false)
|
fetchOrders(false)
|
||||||
}
|
}
|
||||||
@ -448,23 +455,26 @@ onReachBottom(() => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg-page;
|
background: $bg-page;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(40rpx + env(safe-area-inset-top) + env(safe-area-inset-bottom));
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶部 Tab - 与盒柜页面保持一致 */
|
/* 顶部 Tab */
|
||||||
.tabs {
|
.tabs {
|
||||||
|
@extend .glass-card;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
background: rgba($bg-card, 0.95);
|
|
||||||
backdrop-filter: blur(20rpx);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
|
border-radius: 0;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@ -614,16 +624,19 @@ onReachBottom(() => {
|
|||||||
|
|
||||||
/* 订单卡片 */
|
/* 订单卡片 */
|
||||||
.order-card {
|
.order-card {
|
||||||
background: $bg-card;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(10rpx);
|
||||||
border-radius: $radius-xl;
|
border-radius: $radius-xl;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: $shadow-card;
|
box-shadow: $shadow-sm;
|
||||||
animation: fadeInUp 0.4s ease-out backwards;
|
animation: fadeInUp 0.4s ease-out backwards;
|
||||||
animation-delay: var(--delay, 0s);
|
animation-delay: var(--delay, 0s);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="wrap">
|
<view class="wrap">
|
||||||
<!-- 顶部装饰背景 -->
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
<view class="page-bg-decoration"></view>
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
<view class="header-area">
|
<view class="header-area">
|
||||||
<view class="page-title">积分明细</view>
|
<view class="page-title">积分明细</view>
|
||||||
@ -26,14 +26,14 @@
|
|||||||
class="record-item"
|
class="record-item"
|
||||||
:style="{ animationDelay: `${index * 0.05}s` }"
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||||
>
|
>
|
||||||
<view class="record-icon" :class="{ 'is-add': (item.change || item.amount || 0) > 0 }">
|
<view class="record-icon" :class="{ 'is-add': (item.points || 0) > 0 }">
|
||||||
{{ (item.change || item.amount || 0) > 0 ? '↓' : '↑' }}
|
{{ (item.points || 0) > 0 ? '↓' : '↑' }}
|
||||||
</view>
|
</view>
|
||||||
<view class="record-content">
|
<view class="record-content">
|
||||||
<view class="record-main">
|
<view class="record-main">
|
||||||
<view class="record-title">{{ item.title || item.reason || '积分变更' }}</view>
|
<view class="record-title">{{ getActionText(item.action) || item.title || item.reason || '积分变更' }}</view>
|
||||||
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
|
<view class="record-amount" :class="{ inc: (item.points || 0) > 0, dec: (item.points || 0) < 0 }">
|
||||||
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
|
{{ (item.points ?? 0) > 0 ? '+' : '' }}{{ item.points ?? 0 }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-footer">
|
<view class="record-footer">
|
||||||
@ -77,6 +77,26 @@ function formatTime(t) {
|
|||||||
return `${y}-${m}-${day} ${hh}:${mm}`
|
return `${y}-${m}-${day} ${hh}:${mm}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActionText(action) {
|
||||||
|
const map = {
|
||||||
|
'signin': '每日签到',
|
||||||
|
'register': '注册赠送',
|
||||||
|
'invite_reward': '邀请奖励',
|
||||||
|
'order_deduct': '下单抵扣',
|
||||||
|
'consume_order': '下单消费',
|
||||||
|
'refund_restore': '退款返还',
|
||||||
|
'refund_points': '积分退回',
|
||||||
|
'refund_amount': '金额退款奖励',
|
||||||
|
'manual_add': '管理手动增加',
|
||||||
|
'manual': '系统调整',
|
||||||
|
'redeem_coupon': '兑换优惠券',
|
||||||
|
'redeem_product': '兑换商品',
|
||||||
|
'redeem_reward': '奖品兑换积分',
|
||||||
|
'redeem_item_card': '兑换道具卡'
|
||||||
|
}
|
||||||
|
return map[action] || ''
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchRecords(append = false) {
|
async function fetchRecords(append = false) {
|
||||||
const user_id = uni.getStorageSync('user_id')
|
const user_id = uni.getStorageSync('user_id')
|
||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
@ -106,7 +126,7 @@ async function fetchRecords(append = false) {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
const list = await getPointsRecords(user_id, page.value, pageSize.value)
|
const list = await getPointsRecords(user_id, page.value, pageSize.value)
|
||||||
const items = Array.isArray(list) ? list : (list && list.items) || []
|
const items = Array.isArray(list) ? list : (list && (list.list || list.items)) || []
|
||||||
const total = (list && list.total) || 0
|
const total = (list && list.total) || 0
|
||||||
if (append) {
|
if (append) {
|
||||||
records.value = records.value.concat(items)
|
records.value = records.value.concat(items)
|
||||||
@ -143,33 +163,27 @@ onReachBottom(() => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: $bg-page;
|
background-color: $bg-page;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-bg-decoration {
|
/* 背景装饰 - 漂浮光球 (与个人中心统一) */
|
||||||
position: absolute;
|
|
||||||
top: -200rpx;
|
|
||||||
right: -200rpx;
|
|
||||||
width: 600rpx;
|
|
||||||
height: 600rpx;
|
|
||||||
background: radial-gradient(circle, rgba($brand-primary, 0.15), transparent 70%);
|
|
||||||
border-radius: 50%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-area {
|
.header-area {
|
||||||
padding: $spacing-xl $spacing-lg;
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 48rpx;
|
font-size: 48rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: $text-main;
|
color: $text-main;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
letter-spacing: 1rpx;
|
letter-spacing: 1rpx;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: $text-tertiary;
|
color: $text-tertiary;
|
||||||
|
|||||||
@ -7,14 +7,15 @@
|
|||||||
<view class="info-card">
|
<view class="info-card">
|
||||||
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
||||||
<view class="price-row">
|
<view class="price-row">
|
||||||
<view class="points-wrap" v-if="detail.points_required">
|
<view class="points-wrap">
|
||||||
<text class="points-val">{{ detail.points_required }}</text>
|
<text class="points-val">{{ detail.points_required || (detail.price ? Math.floor(detail.price / 100) : 0) }}</text>
|
||||||
<text class="points-unit">积分</text>
|
<text class="points-unit">积分</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="price" v-else>¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存:{{ detail.stock }}</view>
|
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存:{{ detail.stock }}</view>
|
||||||
<view class="desc" v-if="detail.description">{{ detail.description }}</view>
|
<view class="desc" v-if="detail.description">
|
||||||
|
<rich-text :nodes="detail.description"></rich-text>
|
||||||
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -23,8 +24,7 @@
|
|||||||
<!-- Action Bar Moved Outside info-card -->
|
<!-- Action Bar Moved Outside info-card -->
|
||||||
<view class="action-bar-placeholder" v-if="detail.id"></view>
|
<view class="action-bar-placeholder" v-if="detail.id"></view>
|
||||||
<view class="action-bar" v-if="detail.id">
|
<view class="action-bar" v-if="detail.id">
|
||||||
<view class="action-btn redeem" v-if="detail.points_required" @tap="onRedeem">立即兑换</view>
|
<view class="action-btn redeem" @tap="onRedeem">立即兑换</view>
|
||||||
<view class="action-btn buy" v-else @tap="onBuy">立即购买</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -74,9 +74,10 @@ async function onRedeem() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const points = detail.value.points_required || (detail.value.price ? Math.floor(detail.value.price / 100) : 0)
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认兑换',
|
title: '确认兑换',
|
||||||
content: `是否消耗 ${p.points_required} 积分兑换 ${p.title}?`,
|
content: `是否消耗 ${points} 积分兑换 ${p.title || p.name}?`,
|
||||||
success: async (res) => {
|
success: async (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showLoading({ title: '兑换中...' })
|
uni.showLoading({ title: '兑换中...' })
|
||||||
@ -85,16 +86,18 @@ async function onRedeem() {
|
|||||||
if (!userId) throw new Error('用户ID不存在')
|
if (!userId) throw new Error('用户ID不存在')
|
||||||
|
|
||||||
await redeemProductByPoints(userId, p.id, 1)
|
await redeemProductByPoints(userId, p.id, 1)
|
||||||
uni.showToast({ title: '兑换成功', icon: 'success' })
|
|
||||||
|
|
||||||
// Refresh detail
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchDetail(p.id)
|
|
||||||
}, 1500)
|
|
||||||
} catch (e) {
|
|
||||||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
uni.showModal({
|
||||||
|
title: '兑换成功',
|
||||||
|
content: `您已成功兑换 ${p.title || p.name}`,
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
fetchDetail(p.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,6 +119,46 @@ onLoad((opts) => {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg-page;
|
background: $bg-page;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景装饰 - 漂浮光球 (与各主要页面统一) */
|
||||||
|
.bg-decoration {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -100rpx; right: -100rpx;
|
||||||
|
width: 600rpx; height: 600rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
||||||
|
filter: blur(60rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: float 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 200rpx; left: -200rpx;
|
||||||
|
width: 500rpx; height: 500rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||||
|
filter: blur(50rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: float 15s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(30rpx, 50rpx); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading, .empty {
|
.loading, .empty {
|
||||||
@ -123,30 +166,37 @@ onLoad((opts) => {
|
|||||||
padding: 120rpx 40rpx;
|
padding: 120rpx 40rpx;
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
font-size: $font-md;
|
font-size: $font-md;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-wrap {
|
.detail-wrap {
|
||||||
padding-bottom: 40rpx;
|
padding-bottom: 40rpx;
|
||||||
animation: fadeInUp 0.4s ease-out;
|
animation: fadeInUp 0.4s ease-out;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-image {
|
.main-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 750rpx; /* Square aspect ratio */
|
height: 750rpx;
|
||||||
display: block;
|
display: block;
|
||||||
background: $bg-secondary;
|
background: $bg-secondary;
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: #fff;
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
backdrop-filter: blur(20rpx);
|
||||||
border-radius: $radius-xl $radius-xl 0 0;
|
border-radius: $radius-xl $radius-xl 0 0;
|
||||||
padding: $spacing-xl;
|
padding: $spacing-xl;
|
||||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.05);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
margin-top: -40rpx; /* Slight overlap */
|
margin-top: -40rpx;
|
||||||
min-height: 50vh;
|
min-height: 50vh;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@ -155,6 +205,7 @@ onLoad((opts) => {
|
|||||||
color: $text-main;
|
color: $text-main;
|
||||||
margin-bottom: $spacing-md;
|
margin-bottom: $spacing-md;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-row {
|
.price-row {
|
||||||
@ -197,7 +248,7 @@ onLoad((opts) => {
|
|||||||
font-size: $font-sm;
|
font-size: $font-sm;
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
margin-bottom: $spacing-lg;
|
margin-bottom: $spacing-lg;
|
||||||
background: $bg-secondary;
|
background: rgba(0,0,0,0.05);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 6rpx $spacing-md;
|
padding: 6rpx $spacing-md;
|
||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
@ -218,6 +269,12 @@ onLoad((opts) => {
|
|||||||
margin-bottom: $spacing-sm;
|
margin-bottom: $spacing-sm;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar-placeholder { height: 120rpx; }
|
.action-bar-placeholder { height: 120rpx; }
|
||||||
@ -231,8 +288,9 @@ onLoad((opts) => {
|
|||||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 88rpx;
|
height: 88rpx;
|
||||||
@ -243,6 +301,9 @@ onLoad((opts) => {
|
|||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:active { transform: scale(0.96); }
|
||||||
}
|
}
|
||||||
.action-btn.redeem { background: linear-gradient(135deg, #FFB74D, #FF9800); box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3); }
|
.action-btn.redeem { background: linear-gradient(135deg, #FFB74D, #FF9800); box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3); }
|
||||||
.action-btn.buy { background: linear-gradient(135deg, #FF6B6B, #FF3B30); box-shadow: 0 8rpx 20rpx rgba(255, 59, 48, 0.3); }
|
.action-btn.buy { background: linear-gradient(135deg, #FF6B6B, #FF3B30); box-shadow: 0 8rpx 20rpx rgba(255, 59, 48, 0.3); }
|
||||||
|
|||||||
@ -1,59 +1,112 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="bg-decoration"></view>
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
<!-- 顶部固定区域 -->
|
<!-- 顶部固定区域 -->
|
||||||
<view class="header-section">
|
<view class="header-section">
|
||||||
<view class="search-box">
|
<view class="search-box">
|
||||||
<view class="search-input-wrap">
|
<view class="search-input-wrap">
|
||||||
<text class="search-icon">🔍</text>
|
<text class="search-icon">🔍</text>
|
||||||
<input class="search-input" v-model="keyword" placeholder="搜索心仪的商品" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" />
|
<input class="search-input" v-model="keyword" placeholder="搜索心仪的宝贝" placeholder-class="placeholder-style" confirm-type="search" @confirm="onSearchConfirm" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="filter-row">
|
<!-- 分类标签 -->
|
||||||
<view class="price-range">
|
<view class="tab-row">
|
||||||
<text class="price-label">价格区间</text>
|
<view
|
||||||
<input class="price-input" type="number" v-model="minPrice" placeholder="最低" placeholder-class="price-ph" />
|
v-for="tab in tabs"
|
||||||
<text class="price-sep">-</text>
|
:key="tab.id"
|
||||||
<input class="price-input" type="number" v-model="maxPrice" placeholder="最高" placeholder-class="price-ph" />
|
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>
|
||||||
<button class="filter-btn" hover-class="btn-hover" @tap="onApplyFilters">筛选</button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 占位,防止内容被头部遮挡 -->
|
<!-- 占位 -->
|
||||||
<view class="header-placeholder"></view>
|
<view class="header-placeholder"></view>
|
||||||
|
|
||||||
<view v-if="loading && !products.length" class="loading-wrap"><view class="spinner"></view></view>
|
<view v-if="loading && !items.length" class="loading-wrap"><view class="spinner"></view></view>
|
||||||
|
|
||||||
<view class="products-container" v-else>
|
<view class="content-container" v-else>
|
||||||
<view v-if="products.length > 0" class="products-grid">
|
<!-- 商品视图 (网格) -->
|
||||||
<view class="product-item" v-for="p in products" :key="p.id" @tap="onProductTap(p)">
|
<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="product-card">
|
||||||
<view class="thumb-wrap">
|
<view class="thumb-wrap">
|
||||||
<image class="product-thumb" :src="p.image" mode="aspectFill" lazy-load="true" />
|
<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}}件</view>
|
<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 class="product-info">
|
||||||
<text class="product-title">{{ p.title }}</text>
|
<text class="product-title">{{ p.title }}</text>
|
||||||
<view class="product-bottom">
|
<view class="product-bottom">
|
||||||
<view class="price-row" v-if="p.points">
|
<view class="price-row">
|
||||||
<text class="points-val">{{ p.points }}</text>
|
<text class="points-val">{{ p.points || 0 }}</text>
|
||||||
<text class="points-unit">积分</text>
|
<text class="points-unit">积分</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="price-row" v-else>
|
<view class="redeem-btn" @tap.stop="onRedeemTap(p)">兑换</view>
|
||||||
<text class="price-symbol">¥</text>
|
|
||||||
<text class="price-val">{{ p.price }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="redeem-btn" v-if="p.points" @tap.stop="onRedeemTap(p)">兑换</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="empty-state">
|
|
||||||
|
<!-- 优惠券视图 (票据样式列表) -->
|
||||||
|
<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">
|
||||||
|
<view class="item-card" v-for="ic in items" :key="ic.id">
|
||||||
|
<view class="ic-icon-wrap">
|
||||||
|
<text class="ic-icon">🃏</text>
|
||||||
|
</view>
|
||||||
|
<view class="ic-info">
|
||||||
|
<text class="ic-name">{{ ic.title || ic.name }}</text>
|
||||||
|
<text class="ic-desc">{{ ic.description || '可在抽奖时使用' }}</text>
|
||||||
|
<view class="ic-bottom">
|
||||||
|
<view class="ic-price">
|
||||||
|
<text class="p-val">{{ ic.points || 0 }}</text>
|
||||||
|
<text class="p-unit">积分</text>
|
||||||
|
</view>
|
||||||
|
<view class="ic-btn" @tap="onRedeemTap(ic)">兑换</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!items.length" class="empty-state">
|
||||||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||||||
<text class="empty-text">暂无相关商品</text>
|
<text class="empty-text">暂无相关兑换项</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -61,22 +114,20 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { request, authRequest } from '../../utils/request.js'
|
import { getStoreItems, redeemProductByPoints, redeemCouponByPoints, redeemItemCardByPoints } from '../../api/appUser'
|
||||||
|
|
||||||
const products = ref([])
|
|
||||||
const CACHE_KEY = 'products_cache_v1'
|
|
||||||
const TTL_MS = 10 * 60 * 1000
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const minPrice = ref('')
|
const currentTab = ref('product')
|
||||||
const maxPrice = ref('')
|
const items = ref([])
|
||||||
|
const allItems = ref([])
|
||||||
|
|
||||||
function apiGet(url, data = {}) {
|
const tabs = [
|
||||||
const token = uni.getStorageSync('token')
|
{ id: 'product', name: '商品' },
|
||||||
const fn = token ? authRequest : request
|
{ id: 'coupon', name: '优惠券' },
|
||||||
return fn({ url, method: 'GET', data })
|
{ id: 'item_card', name: '道具卡' }
|
||||||
}
|
]
|
||||||
|
|
||||||
function cleanUrl(u) {
|
function cleanUrl(u) {
|
||||||
const s = String(u || '').trim()
|
const s = String(u || '').trim()
|
||||||
@ -85,31 +136,62 @@ function cleanUrl(u) {
|
|||||||
return s.replace(/[`'\"]/g, '').trim()
|
return s.replace(/[`'\"]/g, '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProducts(list) {
|
function normalizeItems(list, kind) {
|
||||||
if (!Array.isArray(list)) return []
|
if (!Array.isArray(list)) return []
|
||||||
return list.map((i, idx) => ({
|
return list.map((i, idx) => ({
|
||||||
id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx),
|
id: i.id,
|
||||||
image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
|
kind: i.kind || kind,
|
||||||
title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '',
|
image: cleanUrl(i.main_image || i.image || ''),
|
||||||
price: (() => { const raw = i.price_sale ?? i.price ?? i.price_min ?? i.amount ?? null; return raw == null ? null : (Number(raw) / 100) })(),
|
title: i.name || i.title || '',
|
||||||
points: i.points_required ?? i.points ?? i.integral ?? null,
|
price: i.price || i.discount_value || 0,
|
||||||
stock: i.stock ?? i.inventory ?? i.quantity ?? null,
|
points: i.points_required || (i.price ? Math.floor(i.price / 100) : (i.discount_value ? Math.floor(i.discount_value / 100) : 0)),
|
||||||
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
|
stock: i.in_stock ? 99 : 0, // Simplified stock check if returned as bool
|
||||||
})).filter(i => i.image || i.title)
|
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
|
||||||
|
loading.value = true
|
||||||
|
items.value = []
|
||||||
|
loadItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadItems() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getStoreItems(currentTab.value, 1, 100)
|
||||||
|
const list = res.list || res || []
|
||||||
|
allItems.value = normalizeItems(list, currentTab.value)
|
||||||
|
applyFilters()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
items.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const k = String(keyword.value || '').trim().toLowerCase()
|
||||||
|
items.value = allItems.value.filter(item => {
|
||||||
|
const title = String(item.title || '').toLowerCase()
|
||||||
|
return !k || title.includes(k)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchConfirm() { applyFilters() }
|
||||||
|
|
||||||
function onProductTap(p) {
|
function onProductTap(p) {
|
||||||
const id = p && p.id
|
if (p.kind === 'product') {
|
||||||
if (id !== undefined && id !== null && id !== '') {
|
uni.navigateTo({ url: `/pages/shop/detail?id=${p.id}` })
|
||||||
uni.navigateTo({ url: `/pages/shop/detail?id=${id}` })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (p.link && /^\/.+/.test(p.link)) {
|
|
||||||
uni.navigateTo({ url: p.link })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRedeemTap(p) {
|
async function onRedeemTap(item) {
|
||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
@ -123,150 +205,51 @@ async function onRedeemTap(p) {
|
|||||||
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认兑换',
|
title: '确认兑换',
|
||||||
content: `是否消耗 ${p.points} 积分兑换 ${p.title}?`,
|
content: `是否消耗 ${item.points} 积分兑换 ${item.title}?`,
|
||||||
success: async (res) => {
|
success: async (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showLoading({ title: '兑换中...' })
|
uni.showLoading({ title: '兑换中...' })
|
||||||
try {
|
try {
|
||||||
// Get user_id from storage
|
|
||||||
const userId = uni.getStorageSync('user_id')
|
const userId = uni.getStorageSync('user_id')
|
||||||
if (!userId) throw new Error('用户ID不存在')
|
if (!userId) throw new Error('用户ID不存在')
|
||||||
|
|
||||||
await redeemProductByPoints(userId, p.id, 1)
|
if (item.kind === 'product') {
|
||||||
uni.showToast({ title: '兑换成功', icon: 'success' })
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh products to update stock/points if needed
|
|
||||||
setTimeout(() => {
|
|
||||||
loadProducts()
|
|
||||||
}, 1500)
|
|
||||||
} catch (e) {
|
|
||||||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
uni.hideLoading()
|
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' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter logic
|
onShow(() => {
|
||||||
const allProducts = ref([]) // Store all fetched products for client-side filtering
|
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
const k = String(keyword.value || '').trim().toLowerCase()
|
|
||||||
const min = Number(minPrice.value)
|
|
||||||
const max = Number(maxPrice.value)
|
|
||||||
const hasMin = !isNaN(min) && String(minPrice.value).trim() !== ''
|
|
||||||
const hasMax = !isNaN(max) && String(maxPrice.value).trim() !== ''
|
|
||||||
|
|
||||||
const list = allProducts.value
|
|
||||||
products.value = list.filter(p => {
|
|
||||||
const title = String(p.title || '').toLowerCase()
|
|
||||||
if (k && !title.includes(k)) return false
|
|
||||||
const priceNum = typeof p.price === 'number' ? p.price : Number(p.price)
|
|
||||||
if (hasMin) {
|
|
||||||
if (isNaN(priceNum)) return false
|
|
||||||
if (priceNum < min) return false
|
|
||||||
}
|
|
||||||
if (hasMax) {
|
|
||||||
if (isNaN(priceNum)) return false
|
|
||||||
if (priceNum > max) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearchConfirm() { applyFilters() }
|
|
||||||
function onApplyFilters() { applyFilters() }
|
|
||||||
|
|
||||||
async function loadProducts() {
|
|
||||||
try {
|
|
||||||
const cached = uni.getStorageSync(CACHE_KEY)
|
|
||||||
if (cached && cached.data && Date.now() - cached.ts < TTL_MS) {
|
|
||||||
allProducts.value = cached.data
|
|
||||||
applyFilters()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = await apiGet('/api/app/products', { page: 1 })
|
|
||||||
// Simple extraction
|
|
||||||
let list = []
|
|
||||||
let total = 0
|
|
||||||
if (first && first.list) { list = first.list; total = first.total }
|
|
||||||
else if (first && first.data && first.data.list) { list = first.data.list; total = first.data.total }
|
|
||||||
|
|
||||||
// If not too many, fetch all for better client UX
|
|
||||||
const pageSize = 20
|
|
||||||
const totalPages = Math.ceil((total || 0) / pageSize)
|
|
||||||
|
|
||||||
if (totalPages > 1) {
|
|
||||||
const tasks = []
|
|
||||||
for (let p = 2; p <= totalPages; p++) {
|
|
||||||
tasks.push(apiGet('/api/app/products', { page: p }))
|
|
||||||
}
|
|
||||||
const results = await Promise.allSettled(tasks)
|
|
||||||
results.forEach(r => {
|
|
||||||
if (r.status === 'fulfilled') {
|
|
||||||
const val = r.value
|
|
||||||
const subList = (val && val.list) || (val && val.data && val.data.list) || []
|
|
||||||
if (Array.isArray(subList)) list = list.concat(subList)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = normalizeProducts(list)
|
|
||||||
allProducts.value = normalized
|
|
||||||
applyFilters()
|
|
||||||
uni.setStorageSync(CACHE_KEY, { data: normalized, ts: Date.now() })
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
products.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onShow(async () => {
|
|
||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||||||
if (!token || !phoneBound) {
|
if (token && phoneBound) {
|
||||||
uni.showModal({
|
loadItems()
|
||||||
title: '提示',
|
} else {
|
||||||
content: '请先登录并绑定手机号',
|
// Redirect logic if needed
|
||||||
confirmText: '去登录',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
uni.navigateTo({ url: '/pages/login/index' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
await loadProducts()
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分享功能
|
|
||||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
|
||||||
|
|
||||||
onShareAppMessage(() => {
|
|
||||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
|
||||||
return {
|
|
||||||
title: '柯大鸭潮玩商城 - 好物等你来兑',
|
|
||||||
path: `/pages/index/index?invite_code=${inviteCode}`,
|
|
||||||
imageUrl: '/static/logo.png'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onShareTimeline(() => {
|
watch(keyword, () => applyFilters())
|
||||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
|
||||||
return {
|
|
||||||
title: '柯大鸭潮玩商城 - 好物等你来兑',
|
|
||||||
query: `invite_code=${inviteCode}`,
|
|
||||||
imageUrl: '/static/logo.png'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -279,302 +262,179 @@ onShareTimeline(() => {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-decoration {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; width: 100%; height: 100vh;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -100rpx; left: -100rpx;
|
|
||||||
width: 600rpx; height: 600rpx;
|
|
||||||
background: radial-gradient(circle, rgba($brand-primary, 0.1) 0%, transparent 70%);
|
|
||||||
filter: blur(60rpx);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.6;
|
|
||||||
animation: float 10s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10%; right: -100rpx;
|
|
||||||
width: 500rpx; height: 500rpx;
|
|
||||||
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
|
||||||
filter: blur(50rpx);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.5;
|
|
||||||
animation: float 15s ease-in-out infinite reverse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translate(0, 0); }
|
|
||||||
50% { transform: translate(30rpx, 50rpx); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部 Header */
|
/* 顶部 Header */
|
||||||
.header-section {
|
.header-section {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0; left: 0; right: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: $bg-glass;
|
background: rgba(255, 255, 255, 0.85);
|
||||||
backdrop-filter: blur(20rpx);
|
backdrop-filter: blur(20rpx);
|
||||||
padding: 0 24rpx 24rpx;
|
padding: 20rpx 24rpx 0;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
|
||||||
border-bottom: 1rpx solid $border-color-light;
|
|
||||||
}
|
|
||||||
.header-placeholder {
|
|
||||||
height: 160rpx; /* 根据 header 高度调整 */
|
|
||||||
}
|
}
|
||||||
|
.header-placeholder { height: 180rpx; }
|
||||||
|
|
||||||
.page-title {
|
.search-box { margin-bottom: 20rpx; }
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: $text-main;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 搜索框 */
|
|
||||||
.search-box {
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
margin-top: 20rpx;
|
|
||||||
}
|
|
||||||
.search-input-wrap {
|
.search-input-wrap {
|
||||||
display: flex;
|
display: flex; align-items: center;
|
||||||
align-items: center;
|
background: rgba(0, 0, 0, 0.04);
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: $radius-round;
|
border-radius: $radius-round;
|
||||||
padding: 18rpx 24rpx;
|
padding: 16rpx 24rpx;
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.search-input-wrap:focus-within {
|
|
||||||
background: $bg-card;
|
|
||||||
border-color: $brand-primary;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.1);
|
|
||||||
}
|
|
||||||
.search-icon {
|
|
||||||
font-size: 28rpx;
|
|
||||||
margin-right: 16rpx;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: $text-main;
|
|
||||||
}
|
|
||||||
.placeholder-style {
|
|
||||||
color: $text-tertiary;
|
|
||||||
}
|
}
|
||||||
|
.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; }
|
||||||
|
|
||||||
/* 筛选行 */
|
/* Tabs */
|
||||||
.filter-row {
|
.tab-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-around;
|
||||||
justify-content: space-between;
|
padding-bottom: 4rpx;
|
||||||
gap: 20rpx;
|
|
||||||
}
|
}
|
||||||
.price-range {
|
.tab-item {
|
||||||
flex: 1;
|
position: relative;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: $bg-secondary;
|
|
||||||
border-radius: $radius-md;
|
|
||||||
padding: 10rpx 20rpx;
|
|
||||||
}
|
}
|
||||||
.price-label {
|
.tab-text {
|
||||||
font-size: 24rpx;
|
font-size: 28rpx;
|
||||||
color: $text-sub;
|
color: $text-secondary;
|
||||||
margin-right: 16rpx;
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.price-input {
|
.tab-item.active .tab-text {
|
||||||
flex: 1;
|
color: $brand-primary;
|
||||||
font-size: 26rpx;
|
font-weight: 700;
|
||||||
text-align: center;
|
font-size: 30rpx;
|
||||||
color: $text-main;
|
|
||||||
}
|
}
|
||||||
.price-ph {
|
.tab-line {
|
||||||
color: $text-tertiary;
|
position: absolute;
|
||||||
font-size: 24rpx;
|
bottom: 0;
|
||||||
}
|
width: 40rpx;
|
||||||
.price-sep {
|
height: 6rpx;
|
||||||
color: $text-tertiary;
|
|
||||||
margin: 0 10rpx;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
background: $gradient-brand;
|
background: $gradient-brand;
|
||||||
color: $text-inverse;
|
border-radius: 4rpx;
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: $radius-md;
|
|
||||||
padding: 0 32rpx;
|
|
||||||
height: 64rpx;
|
|
||||||
line-height: 64rpx;
|
|
||||||
border: none;
|
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
}
|
|
||||||
.btn-hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 商品 Grid 容器 */
|
.content-container { padding: 24rpx; }
|
||||||
.products-container {
|
|
||||||
padding: 24rpx;
|
/* 商品 Grid */
|
||||||
}
|
|
||||||
.products-grid {
|
.products-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 商品卡片 */
|
|
||||||
.product-item {
|
|
||||||
animation: fadeInUp 0.5s ease-out backwards;
|
|
||||||
}
|
|
||||||
@for $i from 1 through 10 {
|
|
||||||
.product-item:nth-child(#{$i}) {
|
|
||||||
animation-delay: #{$i * 0.05}s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card {
|
.product-card {
|
||||||
background: $bg-card;
|
background: #fff;
|
||||||
border-radius: $radius-lg;
|
border-radius: $radius-lg;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: $shadow-card;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.product-item:active .product-card {
|
|
||||||
transform: scale(0.98);
|
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: $shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb-wrap {
|
.thumb-wrap {
|
||||||
position: relative;
|
position: relative; width: 100%; padding-top: 100%;
|
||||||
width: 100%;
|
background: #f8f8f8;
|
||||||
padding-top: 100%; /* 1:1 Aspect Ratio */
|
|
||||||
background: $bg-secondary;
|
|
||||||
}
|
|
||||||
.product-thumb {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
}
|
||||||
|
.product-thumb { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||||
.stock-tag {
|
.stock-tag {
|
||||||
position: absolute;
|
position: absolute; bottom: 0; right: 0;
|
||||||
bottom: 0;
|
background: rgba($brand-primary, 0.9);
|
||||||
right: 0;
|
color: #fff; font-size: 20rpx; padding: 4rpx 12rpx;
|
||||||
background: rgba($accent-red, 0.9);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 20rpx;
|
|
||||||
padding: 4rpx 12rpx;
|
|
||||||
border-top-left-radius: 12rpx;
|
border-top-left-radius: 12rpx;
|
||||||
backdrop-filter: blur(4px);
|
&.out { background: #999; }
|
||||||
font-weight: 700;
|
|
||||||
box-shadow: 0 -2rpx 8rpx rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
|
.product-info { padding: 16rpx; }
|
||||||
.product-info {
|
|
||||||
padding: 20rpx;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-title {
|
.product-title {
|
||||||
font-size: 28rpx;
|
font-size: 26rpx; color: $text-main; font-weight: 600;
|
||||||
color: $text-main;
|
line-height: 1.4; height: 2.8em; overflow: hidden;
|
||||||
line-height: 1.4;
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||||
height: 2.8em; /* 2 lines */
|
margin-bottom: 12rpx;
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
.product-bottom { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.product-bottom {
|
.points-val { font-size: 32rpx; font-weight: 800; color: #FF9800; font-family: 'DIN-Bold'; }
|
||||||
display: flex;
|
.points-unit { font-size: 20rpx; color: #FF9800; margin-left: 2rpx; }
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8rpx;
|
|
||||||
}
|
|
||||||
.price-row {
|
|
||||||
display: flex; align-items: baseline;
|
|
||||||
}
|
|
||||||
.points-val { font-size: 36rpx; font-weight: 700; color: #FF9800; }
|
|
||||||
.points-unit { font-size: 22rpx; color: #FF9800; margin-left: 4rpx; }
|
|
||||||
.price-symbol { font-size: 24rpx; color: #FF3B30; }
|
|
||||||
.price-val { font-size: 36rpx; font-weight: 700; color: #FF3B30; }
|
|
||||||
|
|
||||||
.redeem-btn {
|
.redeem-btn {
|
||||||
background: #FF9800;
|
background: $gradient-brand; color: #fff; font-size: 22rpx;
|
||||||
color: #fff;
|
padding: 6rpx 18rpx; border-radius: 24rpx; font-weight: 600;
|
||||||
font-size: 24rpx;
|
|
||||||
padding: 8rpx 20rpx;
|
|
||||||
border-radius: 30rpx;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading & Empty */
|
/* 优惠券 (Ticket Style) */
|
||||||
.loading-wrap {
|
.coupons-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||||
padding: 100rpx 0;
|
.coupon-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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-wrap { padding: 100rpx 0; display: flex; justify-content: center; }
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 50rpx;
|
width: 48rpx; height: 48rpx; border: 4rpx solid #eee; border-top-color: $brand-primary;
|
||||||
height: 50rpx;
|
border-radius: 50%; animation: spin 0.8s linear infinite;
|
||||||
border: 4rpx solid rgba($brand-primary, 0.2);
|
|
||||||
border-top-color: $brand-primary;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
}
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 100rpx;
|
|
||||||
}
|
|
||||||
.empty-img {
|
|
||||||
width: 240rpx;
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.empty-text {
|
|
||||||
color: $text-tertiary;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: $text-sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
808
pages/tasks/index.vue
Normal file
808
pages/tasks/index.vue
Normal file
@ -0,0 +1,808 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
|
<view class="header-area">
|
||||||
|
<view class="page-title">任务中心</view>
|
||||||
|
<view class="page-subtitle">Task Center</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 进度统计卡片 - 毛玻璃风格 -->
|
||||||
|
<view class="progress-card glass-card">
|
||||||
|
<view class="progress-header">
|
||||||
|
<text class="progress-title">📊 我的任务进度</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value highlight">{{ userProgress.orderCount || 0 }}</text>
|
||||||
|
<text class="stat-label">累计订单</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider"></view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value highlight">{{ userProgress.inviteCount || 0 }}</text>
|
||||||
|
<text class="stat-label">邀请人数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-divider"></view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value first-order-check" :class="{ done: userProgress.firstOrder }">
|
||||||
|
{{ userProgress.firstOrder ? '✓' : '—' }}
|
||||||
|
</view>
|
||||||
|
<text class="stat-label">首单完成</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="content-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="isRefreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view v-if="loading && tasks.length === 0" class="loading-state">
|
||||||
|
<view class="spinner"></view>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-else-if="tasks.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">📝</text>
|
||||||
|
<text class="empty-text">暂无可用任务</text>
|
||||||
|
<text class="empty-hint">敬请期待更多精彩活动</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 任务卡片列表 -->
|
||||||
|
<view v-else class="task-list">
|
||||||
|
<view
|
||||||
|
v-for="(task, index) in tasks"
|
||||||
|
:key="task.id"
|
||||||
|
class="task-card"
|
||||||
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||||
|
>
|
||||||
|
<!-- 任务头部 -->
|
||||||
|
<view class="task-header" @click="toggleTask(task.id)">
|
||||||
|
<view class="task-info">
|
||||||
|
<text class="task-icon">{{ getTaskIcon(task) }}</text>
|
||||||
|
<view class="task-meta">
|
||||||
|
<text class="task-name">{{ task.name }}</text>
|
||||||
|
<text class="task-desc">{{ task.description }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="task-status-wrap">
|
||||||
|
<view class="task-status" :class="getTaskStatusClass(task)">
|
||||||
|
{{ getTaskStatusText(task) }}
|
||||||
|
</view>
|
||||||
|
<text class="expand-arrow" :class="{ expanded: expandedTasks[task.id] }">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 档位列表 (可展开) -->
|
||||||
|
<view class="tier-list" v-if="expandedTasks[task.id] && task.tiers && task.tiers.length > 0">
|
||||||
|
<view
|
||||||
|
v-for="tier in task.tiers"
|
||||||
|
:key="tier.id"
|
||||||
|
class="tier-item"
|
||||||
|
:class="{ 'tier-claimed': isTierClaimed(task.id, tier.id), 'tier-claimable': isTierClaimable(task, tier) }"
|
||||||
|
>
|
||||||
|
<view class="tier-left">
|
||||||
|
<view class="tier-condition">
|
||||||
|
<text class="tier-badge">{{ getTierBadge(tier) }}</text>
|
||||||
|
<text class="tier-text">{{ getTierConditionText(tier) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="tier-reward">
|
||||||
|
<text class="reward-icon">🎁</text>
|
||||||
|
<text class="reward-text">{{ getTierRewardText(task, tier) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="tier-right">
|
||||||
|
<!-- 已领取 -->
|
||||||
|
<view v-if="isTierClaimed(task.id, tier.id)" class="tier-btn claimed">
|
||||||
|
<text>已领取</text>
|
||||||
|
</view>
|
||||||
|
<!-- 可领取 -->
|
||||||
|
<view v-else-if="isTierClaimable(task, tier)" class="tier-btn claimable" @click="claimReward(task, tier)">
|
||||||
|
<text>{{ claiming[`${task.id}_${tier.id}`] ? '领取中...' : '领取' }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 进度中 -->
|
||||||
|
<view v-else class="tier-progress">
|
||||||
|
<text class="progress-text">{{ getTierProgressText(task, tier) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 无档位提示 -->
|
||||||
|
<view class="no-tier-hint" v-if="expandedTasks[task.id] && (!task.tiers || task.tiers.length === 0)">
|
||||||
|
<text>暂无可领取档位</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getTasks, getTaskProgress, claimTaskReward } from '../../api/appUser'
|
||||||
|
|
||||||
|
const tasks = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const isRefreshing = ref(false)
|
||||||
|
const expandedTasks = reactive({})
|
||||||
|
const claiming = reactive({})
|
||||||
|
|
||||||
|
// 用户进度 (汇总)
|
||||||
|
const userProgress = reactive({
|
||||||
|
orderCount: 0,
|
||||||
|
inviteCount: 0,
|
||||||
|
firstOrder: false,
|
||||||
|
claimedTiers: {} // { taskId: [tierId1, tierId2] }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
function getUserId() {
|
||||||
|
return uni.getStorageSync('user_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
function checkAuth() {
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const userId = getUserId()
|
||||||
|
if (!token || !userId) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '请先登录',
|
||||||
|
confirmText: '去登录',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.navigateTo({ url: '/pages/login/index' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务图标
|
||||||
|
function getTaskIcon(task) {
|
||||||
|
const name = (task.name || '').toLowerCase()
|
||||||
|
if (name.includes('首单') || name.includes('first')) return '🎁'
|
||||||
|
if (name.includes('订单') || name.includes('order')) return '📦'
|
||||||
|
if (name.includes('邀请') || name.includes('invite')) return '👥'
|
||||||
|
if (name.includes('签到') || name.includes('check')) return '📅'
|
||||||
|
if (name.includes('分享') || name.includes('share')) return '📣'
|
||||||
|
return '⭐'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务状态类
|
||||||
|
function getTaskStatusClass(task) {
|
||||||
|
const progress = userProgress.claimedTiers[task.id] || []
|
||||||
|
const allTiers = task.tiers || []
|
||||||
|
if (allTiers.length === 0) return 'status-waiting'
|
||||||
|
|
||||||
|
// 检查是否全部完成
|
||||||
|
const allClaimed = allTiers.every(t => progress.includes(t.id))
|
||||||
|
if (allClaimed) return 'status-done'
|
||||||
|
|
||||||
|
// 检查是否有可领取的
|
||||||
|
if (allTiers.some(t => isTierClaimable(task, t) && !progress.includes(t.id))) {
|
||||||
|
return 'status-claimable'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'status-progress'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务状态文字
|
||||||
|
function getTaskStatusText(task) {
|
||||||
|
const progress = userProgress.claimedTiers[task.id] || []
|
||||||
|
const allTiers = task.tiers || []
|
||||||
|
if (allTiers.length === 0) return '暂无档位'
|
||||||
|
|
||||||
|
const allClaimed = allTiers.every(t => progress.includes(t.id))
|
||||||
|
if (allClaimed) return '已完成'
|
||||||
|
|
||||||
|
if (allTiers.some(t => isTierClaimable(task, t) && !progress.includes(t.id))) {
|
||||||
|
return '可领取'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '进行中'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开/收起任务
|
||||||
|
function toggleTask(taskId) {
|
||||||
|
expandedTasks[taskId] = !expandedTasks[taskId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取档位徽章
|
||||||
|
function getTierBadge(tier) {
|
||||||
|
const metric = tier.metric || ''
|
||||||
|
if (metric === 'first_order') return '首'
|
||||||
|
if (metric === 'order_count') return `${tier.threshold}单`
|
||||||
|
if (metric === 'invite_count') return `${tier.threshold}人`
|
||||||
|
return tier.threshold || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取档位条件文字
|
||||||
|
function getTierConditionText(tier) {
|
||||||
|
const metric = tier.metric || ''
|
||||||
|
if (metric === 'first_order') return '完成首笔订单'
|
||||||
|
if (metric === 'order_count') return `累计下单 ${tier.threshold} 笔`
|
||||||
|
if (metric === 'invite_count') return `邀请 ${tier.threshold} 位好友`
|
||||||
|
return `达成 ${tier.threshold}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取档位奖励文字
|
||||||
|
function getTierRewardText(task, tier) {
|
||||||
|
const rewards = (task.rewards || []).filter(r => r.tier_id === tier.id)
|
||||||
|
if (rewards.length === 0) return '神秘奖励'
|
||||||
|
|
||||||
|
const texts = rewards.map(r => {
|
||||||
|
const type = r.reward_type || ''
|
||||||
|
const payload = r.reward_payload || {}
|
||||||
|
const qty = r.quantity || 1
|
||||||
|
|
||||||
|
if (type === 'points') {
|
||||||
|
const value = payload.value || payload.amount || qty
|
||||||
|
return `${value}积分`
|
||||||
|
}
|
||||||
|
if (type === 'coupon') {
|
||||||
|
const value = payload.value || payload.amount
|
||||||
|
return value ? `¥${value / 100}优惠券` : '优惠券'
|
||||||
|
}
|
||||||
|
if (type === 'item_card') {
|
||||||
|
const name = payload.name || '道具卡'
|
||||||
|
return qty > 1 ? `${name}×${qty}` : name
|
||||||
|
}
|
||||||
|
if (type === 'title') {
|
||||||
|
return payload.name || '专属称号'
|
||||||
|
}
|
||||||
|
return '奖励'
|
||||||
|
})
|
||||||
|
|
||||||
|
return texts.join(' + ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否已领取
|
||||||
|
function isTierClaimed(taskId, tierId) {
|
||||||
|
const claimed = userProgress.claimedTiers[taskId] || []
|
||||||
|
return claimed.includes(tierId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否可领取
|
||||||
|
function isTierClaimable(task, tier) {
|
||||||
|
const metric = tier.metric || ''
|
||||||
|
const threshold = tier.threshold || 0
|
||||||
|
const operator = tier.operator || '>='
|
||||||
|
|
||||||
|
let current = 0
|
||||||
|
if (metric === 'first_order') {
|
||||||
|
return userProgress.firstOrder
|
||||||
|
} else if (metric === 'order_count') {
|
||||||
|
current = userProgress.orderCount || 0
|
||||||
|
} else if (metric === 'invite_count') {
|
||||||
|
current = userProgress.inviteCount || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operator === '>=') return current >= threshold
|
||||||
|
if (operator === '==') return current === threshold
|
||||||
|
if (operator === '>') return current > threshold
|
||||||
|
return current >= threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取进度文字
|
||||||
|
function getTierProgressText(task, tier) {
|
||||||
|
const metric = tier.metric || ''
|
||||||
|
const threshold = tier.threshold || 0
|
||||||
|
|
||||||
|
let current = 0
|
||||||
|
if (metric === 'first_order') {
|
||||||
|
return userProgress.firstOrder ? '已完成' : '未完成'
|
||||||
|
} else if (metric === 'order_count') {
|
||||||
|
current = userProgress.orderCount || 0
|
||||||
|
} else if (metric === 'invite_count') {
|
||||||
|
current = userProgress.inviteCount || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${current}/${threshold}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 领取奖励
|
||||||
|
async function claimReward(task, tier) {
|
||||||
|
const key = `${task.id}_${tier.id}`
|
||||||
|
if (claiming[key]) return
|
||||||
|
|
||||||
|
uni.vibrateShort({ type: 'medium' })
|
||||||
|
claiming[key] = true
|
||||||
|
try {
|
||||||
|
const userId = getUserId()
|
||||||
|
await claimTaskReward(task.id, userId, tier.id)
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
if (!userProgress.claimedTiers[task.id]) {
|
||||||
|
userProgress.claimedTiers[task.id] = []
|
||||||
|
}
|
||||||
|
userProgress.claimedTiers[task.id].push(tier.id)
|
||||||
|
|
||||||
|
uni.showToast({ title: '领取成功!', icon: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('领取失败:', e)
|
||||||
|
uni.showToast({ title: e.message || '领取失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
claiming[key] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
async function onRefresh() {
|
||||||
|
isRefreshing.value = true
|
||||||
|
await fetchData()
|
||||||
|
isRefreshing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
async function fetchData() {
|
||||||
|
if (!checkAuth()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const userId = getUserId()
|
||||||
|
|
||||||
|
// 获取任务列表
|
||||||
|
const res = await getTasks(1, 50)
|
||||||
|
const list = res.list || res.data || []
|
||||||
|
tasks.value = list
|
||||||
|
|
||||||
|
// 默认展开第一个任务
|
||||||
|
if (list.length > 0 && Object.keys(expandedTasks).length === 0) {
|
||||||
|
expandedTasks[list[0].id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户进度 (取第一个任务的进度作为汇总)
|
||||||
|
if (list.length > 0) {
|
||||||
|
try {
|
||||||
|
const progressRes = await getTaskProgress(list[0].id, userId)
|
||||||
|
userProgress.orderCount = progressRes.order_count || 0
|
||||||
|
userProgress.inviteCount = progressRes.invite_count || 0
|
||||||
|
userProgress.firstOrder = progressRes.first_order || false
|
||||||
|
|
||||||
|
// 初始化已领取的档位
|
||||||
|
if (progressRes.claimed_tiers) {
|
||||||
|
userProgress.claimedTiers[list[0].id] = progressRes.claimed_tiers
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取进度失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行获取其他任务的进度
|
||||||
|
const otherTasks = list.slice(1)
|
||||||
|
const progressPromises = otherTasks.map(t =>
|
||||||
|
getTaskProgress(t.id, userId).catch(() => null)
|
||||||
|
)
|
||||||
|
const progressResults = await Promise.allSettled(progressPromises)
|
||||||
|
|
||||||
|
progressResults.forEach((result, index) => {
|
||||||
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
|
const taskId = otherTasks[index].id
|
||||||
|
userProgress.claimedTiers[taskId] = result.value.claimed_tiers || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取任务失败:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-page;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-area {
|
||||||
|
padding: $spacing-xl $spacing-lg;
|
||||||
|
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-main;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进度统计卡片 */
|
||||||
|
.progress-card {
|
||||||
|
@extend .glass-card;
|
||||||
|
margin: 0 $spacing-lg $spacing-lg;
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-main;
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.highlight {
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.first-order-check {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $text-tertiary;
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 50rpx;
|
||||||
|
background: $border-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容滚动区 */
|
||||||
|
.content-scroll {
|
||||||
|
height: calc(100vh - 400rpx);
|
||||||
|
padding: 0 $spacing-lg $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载状态 */
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 26rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 任务列表 */
|
||||||
|
.task-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 任务卡片 */
|
||||||
|
.task-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
animation: fadeInUp 0.5s ease-out backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-name {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-main;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
|
||||||
|
&.status-done {
|
||||||
|
background: rgba($uni-color-success, 0.1);
|
||||||
|
color: $uni-color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-claimable {
|
||||||
|
background: rgba($brand-primary, 0.1);
|
||||||
|
color: $brand-primary;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-progress {
|
||||||
|
background: rgba($brand-primary, 0.05);
|
||||||
|
color: $text-sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-waiting {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-arrow {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: $text-tertiary;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 档位列表 */
|
||||||
|
.tier-list {
|
||||||
|
border-top: 1rpx solid $border-color-light;
|
||||||
|
padding: 16rpx 24rpx 24rpx;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
border: 1rpx solid $border-color-light;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tier-claimed {
|
||||||
|
background: #f5f5f5;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tier-claimable {
|
||||||
|
border-color: $brand-primary;
|
||||||
|
background: rgba($brand-primary, 0.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-left {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-condition {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-badge {
|
||||||
|
background: $text-main;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18rpx;
|
||||||
|
padding: 4rpx 10rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-main;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-reward {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
margin-right: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-btn {
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border-radius: 100rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.claimed {
|
||||||
|
background: #eee;
|
||||||
|
color: $text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.claimable {
|
||||||
|
background: $brand-primary;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-progress {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: $text-sub;
|
||||||
|
font-family: 'DIN Alternate', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tier-hint {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-tertiary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
background: #fafafa;
|
||||||
|
border-top: 1rpx solid $border-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.spinner {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
border: 3rpx solid $bg-secondary;
|
||||||
|
border-top-color: $text-tertiary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
uni.scss
84
uni.scss
@ -182,4 +182,88 @@ $uni-font-size-paragraph: 15px;
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: $lines;
|
-webkit-line-clamp: $lines;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
💎 核心公共 UI 类 (Premium UI 6.0)
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 1. 统一背景装饰 - 漂浮光球 */
|
||||||
|
.bg-decoration {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -100rpx; right: -100rpx;
|
||||||
|
width: 600rpx; height: 600rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
||||||
|
filter: blur(60rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: float 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 200rpx; left: -200rpx;
|
||||||
|
width: 500rpx; height: 500rpx;
|
||||||
|
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||||
|
filter: blur(50rpx);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: float 15s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. 毛玻璃卡片基类 */
|
||||||
|
.glass-card {
|
||||||
|
background: $bg-glass;
|
||||||
|
backdrop-filter: blur(20rpx);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. 通用功能按钮 */
|
||||||
|
.btn-primary {
|
||||||
|
background: $gradient-brand;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: $radius-round;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: $shadow-warm;
|
||||||
|
transition: all 0.2s $ease-out;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
color: $text-main;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: $radius-round;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s $ease-out;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -94,10 +94,13 @@ export function normalizeRewards(list, cleanUrl = (u) => u) {
|
|||||||
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
|
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
|
||||||
}))
|
}))
|
||||||
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
||||||
const enriched = items.map(it => ({
|
const enriched = items.map(it => {
|
||||||
...it,
|
const rawPercent = total > 0 ? (it.weight / total) * 100 : 0
|
||||||
percent: total > 0 ? Math.round((it.weight / total) * 1000) / 10 : 0
|
return {
|
||||||
}))
|
...it,
|
||||||
|
percent: parseFloat(rawPercent.toFixed(2)) // 统一保留2位小数
|
||||||
|
}
|
||||||
|
})
|
||||||
enriched.sort((a, b) => (b.percent - a.percent))
|
enriched.sort((a, b) => (b.percent - a.percent))
|
||||||
return enriched
|
return enriched
|
||||||
}
|
}
|
||||||
@ -143,7 +146,7 @@ export function groupRewardsByLevel(rewards) {
|
|||||||
return {
|
return {
|
||||||
level: key,
|
level: key,
|
||||||
rewards: levelRewards,
|
rewards: levelRewards,
|
||||||
totalPercent: total.toFixed(1)
|
totalPercent: total.toFixed(2) // 统一保留2位小数
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* 缓存管理工具
|
* 缓存管理工具
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const REWARD_CACHE_KEY = 'reward_cache_v1'
|
const REWARD_CACHE_KEY = 'reward_cache_v2' // v2: 修复概率精度问题
|
||||||
const MATCHING_GAME_CACHE_KEY = 'matching_game_cache_v1'
|
const MATCHING_GAME_CACHE_KEY = 'matching_game_cache_v1'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -22,7 +22,7 @@ function handleAuthExpired() {
|
|||||||
export function request({ url, method = 'GET', data = {}, header = {} }) {
|
export function request({ url, method = 'GET', data = {}, header = {} }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const finalHeader = { ...buildDefaultHeaders(), ...header }
|
const finalHeader = { ...buildDefaultHeaders(), ...header }
|
||||||
try { console.log('HTTP request', method, url, 'data', data, 'headers', finalHeader) } catch (e) {}
|
try { console.log('HTTP request', method, url, 'data', data, 'headers', finalHeader) } catch (e) { }
|
||||||
uni.request({
|
uni.request({
|
||||||
url: BASE_URL + url,
|
url: BASE_URL + url,
|
||||||
method,
|
method,
|
||||||
@ -31,7 +31,7 @@ export function request({ url, method = 'GET', data = {}, header = {} }) {
|
|||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const code = res.statusCode
|
const code = res.statusCode
|
||||||
try { console.log('HTTP response', method, url, 'status', code, 'body', res.data) } catch (e) {}
|
try { console.log('HTTP response', method, url, 'status', code, 'body', res.data) } catch (e) { }
|
||||||
if (code >= 200 && code < 300) {
|
if (code >= 200 && code < 300) {
|
||||||
const body = res.data
|
const body = res.data
|
||||||
resolve(body && body.data !== undefined ? body.data : body)
|
resolve(body && body.data !== undefined ? body.data : body)
|
||||||
@ -47,7 +47,7 @@ export function request({ url, method = 'GET', data = {}, header = {} }) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
try { console.error('HTTP fail', method, url, 'err', err) } catch (e) {}
|
try { console.error('HTTP fail', method, url, 'err', err) } catch (e) { }
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -66,11 +66,11 @@ export function authRequest(options) {
|
|||||||
return request({ ...options, header })
|
return request({ ...options, header })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redeemProductByPoints(user_id, product_id, count) {
|
export function redeemProductByPoints(user_id, product_id, quantity) {
|
||||||
return authRequest({
|
return authRequest({
|
||||||
url: '/api/app/users/points/redeem-product',
|
url: `/api/app/users/${user_id}/points/redeem-product`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { user_id, product_id, count }
|
data: { product_id, quantity }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user