Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fb2536c64 | |||
| ece6ce9740 | |||
| c8e49ab3b4 | |||
| c785ead9d7 | |||
| d530ec11e7 | |||
| 8a3676eb9f | |||
| fb520a6895 | |||
| 2895c2d5b7 | |||
|
|
e0a1d6e934 | ||
| 575ccb2cfa | |||
|
|
eca0561cd9 |
3
App.vue
3
App.vue
@ -11,7 +11,6 @@ export default {
|
||||
try { uni.setStorageSync('inviter_code', options.query.invite_code) } catch (e) { console.error('Save invite code failed', e) }
|
||||
}
|
||||
|
||||
// 加载公开配置 (如订阅消息模板ID)
|
||||
getPublicConfig().then(res => {
|
||||
if (res && res.subscribe_templates) {
|
||||
console.log('Loaded public config:', res)
|
||||
@ -20,8 +19,6 @@ export default {
|
||||
}).catch(err => {
|
||||
console.warn('Failed to load public config:', err)
|
||||
})
|
||||
|
||||
// 抖音平台现在也显示首页,不再需要强制跳转
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
|
||||
3
androidPrivacy.json
Normal file
3
androidPrivacy.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"prompt" : "template"
|
||||
}
|
||||
@ -154,6 +154,10 @@ export function requestShipping(user_id, ids, address_id) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/request-shipping`, method: 'POST', data })
|
||||
}
|
||||
|
||||
export function checkShippingFee(user_id, ids) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/shipping-fee/check`, method: 'POST', data: { inventory_ids: ids } })
|
||||
}
|
||||
|
||||
export function createShippingFeeOrder(user_id, ids) {
|
||||
return authRequest({ url: `/api/app/users/${user_id}/inventory/shipping-fee/preorder`, method: 'POST', data: { inventory_ids: ids } })
|
||||
}
|
||||
@ -435,3 +439,10 @@ export function purchaseGamePass(package_id, count = 1, coupon_ids = []) {
|
||||
export function bindDouyinID(douyin_id) {
|
||||
return authRequest({ url: '/api/app/users/douyin/bind', method: 'POST', data: { douyin_id } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步当前用户绑定的抖音订单
|
||||
*/
|
||||
export function syncMyDouyinOrders() {
|
||||
return authRequest({ url: '/api/app/users/douyin/orders/sync', method: 'POST' })
|
||||
}
|
||||
|
||||
9
api/prizeClaim.js
Normal file
9
api/prizeClaim.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { authRequest } from '@/utils/request'
|
||||
|
||||
export function getPendingPrizeGrantActivity() {
|
||||
return authRequest({ url: '/api/app/prize-grant-activities/pending', method: 'GET' })
|
||||
}
|
||||
|
||||
export function claimPrizeGrantActivity(id) {
|
||||
return authRequest({ url: `/api/app/prize-grant-activities/${id}/claim`, method: 'POST' })
|
||||
}
|
||||
@ -8,6 +8,10 @@ export function doSynthesis(userId, recipeId) {
|
||||
return authRequest({ url: `/api/app/users/${userId}/synthesis/do`, method: 'POST', data: { recipe_id: recipeId } })
|
||||
}
|
||||
|
||||
export function doBatchSynthesis(userId, recipeId) {
|
||||
return authRequest({ url: `/api/app/users/${userId}/synthesis/do-batch`, method: 'POST', data: { recipe_id: recipeId } })
|
||||
}
|
||||
|
||||
export function getSynthesisLogs(userId, page = 1, pageSize = 20) {
|
||||
return authRequest({ url: `/api/app/users/${userId}/synthesis/logs`, method: 'GET', data: { page, page_size: pageSize } })
|
||||
}
|
||||
|
||||
25
api/thresholdActivity.js
Normal file
25
api/thresholdActivity.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { request, authRequest } from '../utils/request'
|
||||
|
||||
export function listThresholdActivities(params = {}) {
|
||||
return request({ url: '/api/app/threshold-activities', method: 'GET', data: params })
|
||||
}
|
||||
|
||||
export function getThresholdActivity(id) {
|
||||
return request({ url: `/api/app/threshold-activities/${id}`, method: 'GET' })
|
||||
}
|
||||
|
||||
export function getMyThresholdActivity(id) {
|
||||
return authRequest({ url: `/api/app/threshold-activities/${id}/my`, method: 'GET', suppressAuthModal: true })
|
||||
}
|
||||
|
||||
export function joinThresholdActivity(id) {
|
||||
return authRequest({ url: `/api/app/threshold-activities/${id}/join`, method: 'POST' })
|
||||
}
|
||||
|
||||
export function listThresholdParticipants(id, page = 1, page_size = 20) {
|
||||
return request({ url: `/api/app/threshold-activities/${id}/participants`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
|
||||
export function listThresholdWinners(id, page = 1, page_size = 100) {
|
||||
return request({ url: `/api/app/threshold-activities/${id}/winners`, method: 'GET', data: { page, page_size } })
|
||||
}
|
||||
@ -65,21 +65,22 @@
|
||||
<picker
|
||||
class="picker-full"
|
||||
mode="selector"
|
||||
:range="coupons"
|
||||
:range="couponOptions"
|
||||
range-key="name"
|
||||
@change="onCouponChange"
|
||||
:value="couponIndex"
|
||||
:disabled="(!coupons || coupons.length === 0) || useGamePass"
|
||||
:disabled="couponPickerDisabled"
|
||||
>
|
||||
<view class="picker-display" :class="{ 'picker-disabled': useGamePass }">
|
||||
<text v-if="useGamePass" class="placeholder" style="color: #666;">
|
||||
多次卡不可与优惠券同享
|
||||
</text>
|
||||
<text v-if="selectedCoupon" class="selected-text">
|
||||
<text v-else-if="selectedCoupon" class="selected-text">
|
||||
{{ selectedCoupon.name }} (-¥{{ effectiveCouponDiscount.toFixed(2) }})
|
||||
<text v-if="selectedCoupon.amount > maxDeductible" style="font-size: 20rpx; color: #FF9800;">(最高抵扣50%)</text>
|
||||
</text>
|
||||
<text v-else-if="!coupons || coupons.length === 0" class="placeholder">暂无优惠券可用</text>
|
||||
<text v-else-if="!hasUsableCoupons" class="placeholder">暂无优惠券可用</text>
|
||||
<text v-else-if="couponOptionalSelected" class="placeholder">不使用优惠券</text>
|
||||
<text v-else class="placeholder">请选择优惠券</text>
|
||||
<text class="arrow"></text>
|
||||
</view>
|
||||
@ -129,7 +130,10 @@ const props = defineProps({
|
||||
coupons: { type: Array, default: () => [] },
|
||||
propCards: { type: Array, default: () => [] },
|
||||
showCards: { type: Boolean, default: true },
|
||||
gamePasses: { type: Object, default: () => null } // { total_remaining, passes }
|
||||
gamePasses: { type: Object, default: () => null }, // { total_remaining, passes }
|
||||
couponOptional: { type: Boolean, default: false },
|
||||
defaultUseCoupon: { type: Boolean, default: true },
|
||||
defaultUseGamePass: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'confirm', 'cancel'])
|
||||
@ -203,6 +207,20 @@ const couponIndex = ref(-1)
|
||||
const cardIndex = ref(-1)
|
||||
const useGamePass = ref(false)
|
||||
|
||||
const hasUsableCoupons = computed(() => Array.isArray(props.coupons) && props.coupons.length > 0)
|
||||
|
||||
const couponOptions = computed(() => {
|
||||
const list = Array.isArray(props.coupons) ? props.coupons : []
|
||||
if (!props.couponOptional) return list
|
||||
return [{ id: 0, name: '不使用优惠券', amount: 0, noCoupon: true }, ...list]
|
||||
})
|
||||
|
||||
const couponOptionalSelected = computed(() => props.couponOptional && Number(couponIndex.value) === 0)
|
||||
|
||||
const couponPickerDisabled = computed(() => {
|
||||
return useGamePass.value || !hasUsableCoupons.value
|
||||
})
|
||||
|
||||
// 次数卡余额
|
||||
const gamePassRemaining = computed(() => {
|
||||
return props.gamePasses?.total_remaining || 0
|
||||
@ -211,8 +229,7 @@ const gamePassRemaining = computed(() => {
|
||||
// 监听弹窗打开,若有次数卡则默认选中
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
// 若有次数卡,默认选中
|
||||
useGamePass.value = gamePassRemaining.value > 0
|
||||
useGamePass.value = props.defaultUseGamePass && gamePassRemaining.value > 0
|
||||
}
|
||||
})
|
||||
|
||||
@ -221,12 +238,20 @@ function toggleGamePass() {
|
||||
// Mutually Exclusive: If Game Pass is ON, clear Coupon.
|
||||
if (useGamePass.value) {
|
||||
couponIndex.value = -1
|
||||
} else {
|
||||
resetCouponSelection()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCoupon = computed(() => {
|
||||
if (couponIndex.value >= 0 && props.coupons[couponIndex.value]) {
|
||||
return props.coupons[couponIndex.value]
|
||||
const index = Number(couponIndex.value)
|
||||
if (Number.isNaN(index)) return null
|
||||
if (props.couponOptional) {
|
||||
if (index <= 0) return null
|
||||
return props.coupons[index - 1] || null
|
||||
}
|
||||
if (index >= 0 && props.coupons[index]) {
|
||||
return props.coupons[index]
|
||||
}
|
||||
return null
|
||||
})
|
||||
@ -262,24 +287,37 @@ const finalPayAmount = computed(() => {
|
||||
return Math.max(0, amt - effectiveCouponDiscount.value).toFixed(2)
|
||||
})
|
||||
|
||||
function resetCouponSelection() {
|
||||
if (useGamePass.value) {
|
||||
couponIndex.value = -1
|
||||
return
|
||||
}
|
||||
if (!hasUsableCoupons.value) {
|
||||
couponIndex.value = props.couponOptional ? 0 : -1
|
||||
return
|
||||
}
|
||||
if (props.couponOptional) {
|
||||
couponIndex.value = props.defaultUseCoupon ? 1 : 0
|
||||
return
|
||||
}
|
||||
if (props.defaultUseCoupon && couponIndex.value < 0) {
|
||||
couponIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => props.visible, () => (Array.isArray(props.coupons) ? props.coupons.length : 0)],
|
||||
([vis, len]) => {
|
||||
([vis]) => {
|
||||
if (!vis) return
|
||||
cardIndex.value = -1
|
||||
if (len <= 0) {
|
||||
couponIndex.value = -1
|
||||
return
|
||||
}
|
||||
if (couponIndex.value < 0) {
|
||||
couponIndex.value = 0
|
||||
}
|
||||
resetCouponSelection()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function onCouponChange(e) {
|
||||
couponIndex.value = e.detail.value
|
||||
const index = Number(e.detail.value)
|
||||
couponIndex.value = Number.isNaN(index) ? -1 : index
|
||||
}
|
||||
|
||||
function onCardChange(e) {
|
||||
|
||||
289
components/activity/PrizeClaimPopup.vue
Normal file
289
components/activity/PrizeClaimPopup.vue
Normal file
@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<view v-if="visible && activity" class="prize-claim-overlay" @touchmove.stop.prevent>
|
||||
<view class="prize-claim-mask" @tap="handleClose"></view>
|
||||
<view class="prize-claim-panel" @tap.stop>
|
||||
<view class="prize-claim-hero">
|
||||
<view class="hero-glow"></view>
|
||||
<view class="hero-top">
|
||||
<view class="hero-badge">奖励发放</view>
|
||||
<text class="prize-claim-close" @tap="handleClose">×</text>
|
||||
</view>
|
||||
<view class="hero-content">
|
||||
<text class="hero-title">奖励已到账,待你领取</text>
|
||||
<text class="hero-reason">{{ activity.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="prize-claim-body">
|
||||
<view class="section-title">奖品内容</view>
|
||||
<view class="reward-list">
|
||||
<view
|
||||
v-for="(item, index) in activity.rewards || []"
|
||||
:key="`${item.reward_type}-${item.reward_ref_id}-${index}`"
|
||||
class="reward-item"
|
||||
>
|
||||
<view class="reward-thumb-wrap">
|
||||
<image v-if="item.image" class="reward-thumb" :src="item.image" mode="aspectFill" />
|
||||
<view v-else class="reward-thumb-empty">{{ typeShortLabel(item.reward_type) }}</view>
|
||||
</view>
|
||||
<view class="reward-main">
|
||||
<text class="reward-name">{{ item.name || item.reward_type }}</text>
|
||||
<view class="reward-meta-row">
|
||||
<text class="reward-type">{{ typeLabel(item.reward_type) }}</text>
|
||||
<text v-if="item.value_cents > 0" class="reward-value">单价 ¥{{ (item.value_cents / 100).toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="reward-side">
|
||||
<text class="reward-quantity">x{{ item.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="prize-claim-footer">
|
||||
<button class="claim-button" :disabled="loading" @tap="handleClaim">
|
||||
{{ loading ? '领取中...' : '立即领取' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
activity: { type: Object, default: null },
|
||||
loading: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'claim', 'close'])
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function handleClaim() {
|
||||
if (!props.loading) emit('claim')
|
||||
}
|
||||
|
||||
function typeLabel(type) {
|
||||
if (type === 'product') return '商品'
|
||||
if (type === 'coupon') return '优惠券'
|
||||
if (type === 'item_card') return '道具卡'
|
||||
return type
|
||||
}
|
||||
|
||||
function typeShortLabel(type) {
|
||||
if (type === 'product') return '商品'
|
||||
if (type === 'coupon') return '券'
|
||||
if (type === 'item_card') return '卡'
|
||||
return '奖'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.prize-claim-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.prize-claim-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(6rpx);
|
||||
}
|
||||
|
||||
.prize-claim-panel {
|
||||
position: relative;
|
||||
width: 88%;
|
||||
max-height: 78vh;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-lg;
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
|
||||
.prize-claim-hero {
|
||||
position: relative;
|
||||
padding: $spacing-lg $spacing-lg $spacing-xl;
|
||||
background: linear-gradient(135deg, $brand-primary 0%, $brand-primary-light 100%);
|
||||
}
|
||||
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
right: -80rpx;
|
||||
top: -80rpx;
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.hero-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: #fff;
|
||||
font-size: $font-xs;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prize-claim-close {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 48rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.hero-reason {
|
||||
display: block;
|
||||
font-size: $font-md;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prize-claim-body {
|
||||
padding: $spacing-lg;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: $font-md;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.reward-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.reward-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-md;
|
||||
}
|
||||
|
||||
.reward-thumb-wrap {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.reward-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.reward-thumb-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
background: rgba($brand-primary, 0.08);
|
||||
}
|
||||
|
||||
.reward-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reward-name {
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.reward-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.reward-type,
|
||||
.reward-value,
|
||||
.reward-quantity {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: $brand-primary-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.reward-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64rpx;
|
||||
}
|
||||
|
||||
.reward-quantity {
|
||||
font-size: $font-md;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.prize-claim-footer {
|
||||
padding: 0 $spacing-lg $spacing-lg;
|
||||
}
|
||||
|
||||
.claim-button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: $radius-round;
|
||||
background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
|
||||
color: #fff;
|
||||
font-size: $font-md;
|
||||
font-weight: 700;
|
||||
padding: 24rpx 0;
|
||||
box-shadow: $shadow-warm;
|
||||
}
|
||||
|
||||
.claim-button[disabled] {
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
@ -8,3 +8,4 @@ export { default as ActivityTabs } from './ActivityTabs.vue'
|
||||
export { default as RewardsPreview } from './RewardsPreview.vue'
|
||||
export { default as RewardsPopup } from './RewardsPopup.vue'
|
||||
export { default as RecordsList } from './RecordsList.vue'
|
||||
export { default as PrizeClaimPopup } from './PrizeClaimPopup.vue'
|
||||
|
||||
@ -180,6 +180,9 @@
|
||||
:propCards="propCards"
|
||||
:showCards="true"
|
||||
:gamePasses="gamePasses"
|
||||
:couponOptional="true"
|
||||
:defaultUseCoupon="false"
|
||||
:defaultUseGamePass="false"
|
||||
@confirm="onPaymentConfirm"
|
||||
/>
|
||||
|
||||
@ -687,6 +690,8 @@ function normalizeRewards(list, playType = 'normal') {
|
||||
weight: Number(i.weight) || 0,
|
||||
boss: detectBoss(i),
|
||||
min_score: Number(i.min_score) || 0, // Extract min_score
|
||||
drop_quantity: Number(i.drop_quantity) || 1,
|
||||
product_price: Number(i.price_snapshot_cents ?? i.product_price ?? i.price ?? i.reference_price) || 0,
|
||||
level: levelToAlpha(i.prize_level ?? i.level ?? (detectBoss(i) ? 'BOSS' : '赏'))
|
||||
}))
|
||||
const total = items.reduce((acc, it) => acc + (it.weight > 0 ? it.weight : 0), 0)
|
||||
|
||||
586
pages-activity/activity/threshold/detail.vue
Normal file
586
pages-activity/activity/threshold/detail.vue
Normal file
@ -0,0 +1,586 @@
|
||||
<template>
|
||||
<view class="threshold-detail-page" v-if="detail">
|
||||
<image v-if="detail.cover_image" class="detail-cover" :src="detail.cover_image" mode="aspectFill" />
|
||||
<view v-else class="detail-cover empty">暂无活动图片</view>
|
||||
|
||||
<view class="detail-panel hero-panel">
|
||||
<view class="hero-head">
|
||||
<text class="detail-title">{{ detail.title }}</text>
|
||||
<text class="detail-type" :class="typeClass(detail.type)">{{ typeLabel(detail.type) }}</text>
|
||||
</view>
|
||||
<text class="detail-status" :class="statusClass(detail.status)">{{ statusText(detail.status) }}</text>
|
||||
<text class="detail-time">开奖时间:{{ formatTime(detail.draw_time) }}</text>
|
||||
<text class="detail-desc" v-if="detail.description">{{ detail.description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel qualification-panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">我的参与资格</text>
|
||||
<text class="panel-side">{{ qualificationModeLabel(detail.qualification_mode) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="share-guide-card" v-if="showInviteGuide">
|
||||
<view class="share-guide-head">
|
||||
<text class="share-guide-title">邀请好友一起冲开奖</text>
|
||||
<text class="share-guide-badge">裂变加速</text>
|
||||
</view>
|
||||
<text class="share-guide-desc">当前还差 {{ crowdGapCount }} 人达到开奖标准,分享活动给好友可更快凑齐人数。</text>
|
||||
<view class="share-guide-actions">
|
||||
<button class="share-action-btn primary" open-type="share">立即邀请好友</button>
|
||||
<view class="share-action-btn secondary" @tap="copyInviteTip">复制邀请口令</view>
|
||||
<view class="share-action-btn secondary" @tap="goMyInvites">我的邀请记录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="metric-card">
|
||||
<view class="metric-head">
|
||||
<text class="metric-title">消费进度</text>
|
||||
<text class="metric-state" :class="qualification?.spend_qualified ? 'ok' : 'pending'">
|
||||
{{ qualification?.spend_qualified ? '已达标' : '未达标' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="metric-value">{{ money(qualification?.current_paid || 0) }} / {{ money(detail.spend_threshold_amount) }}</text>
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill spend" :style="{ width: spendProgressPercent + '%' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="metric-card">
|
||||
<view class="metric-head">
|
||||
<text class="metric-title">邀请进度</text>
|
||||
<text class="metric-state" :class="qualification?.invite_qualified ? 'ok' : 'pending'">
|
||||
{{ qualification?.invite_qualified ? '已达标' : '未达标' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="metric-value">{{ qualification?.effective_invite_count || 0 }} / {{ detail.invite_threshold_count || 0 }} 人</text>
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill invite" :style="{ width: inviteProgressPercent + '%' }"></view>
|
||||
</view>
|
||||
<text class="metric-help" v-if="detail.invite_effective_amount > 0">每位活动开始后新邀请的用户消费满 {{ money(detail.invite_effective_amount) }} 才算 1 位有效邀请</text>
|
||||
</view>
|
||||
|
||||
<view class="qualification-summary">
|
||||
<text class="qualification-text">{{ qualificationSummary }}</text>
|
||||
<view class="join-btn" :class="joinButtonClass" @tap="handleJoin">{{ joinButtonText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel crowd-panel">
|
||||
<view class="panel-head compact">
|
||||
<text class="panel-title">开奖进度</text>
|
||||
<text class="panel-side">{{ detail.participant_count || 0 }} / {{ detail.min_participants || 0 }} 人</text>
|
||||
</view>
|
||||
<view class="crowd-progress-bar">
|
||||
<view class="crowd-progress-fill" :style="{ width: crowdProgressPercent + '%' }"></view>
|
||||
</view>
|
||||
<view class="crowd-summary">
|
||||
<text class="crowd-summary-main">{{ crowdSummary }}</text>
|
||||
<text class="crowd-summary-sub">{{ crowdHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">奖品</text>
|
||||
<text class="panel-side">参与人数 {{ detail.participant_count || 0 }} / 最低开奖 {{ detail.min_participants || 0 }}</text>
|
||||
</view>
|
||||
<scroll-view v-if="sortedPrizes.length" scroll-x class="prize-scroll" show-scrollbar="false">
|
||||
<view class="prize-track">
|
||||
<view v-for="prize in sortedPrizes" :key="prize.id" class="prize-item">
|
||||
<image v-if="prize.image" class="prize-image" :src="prize.image" mode="aspectFill" />
|
||||
<view v-else class="prize-image empty">{{ prizePlaceholderText(prize) }}</view>
|
||||
<text class="prize-name">{{ prize.name }}</text>
|
||||
<text class="prize-price">参考价 ¥{{ formatAmount(prizePrice(prize)) }}</text>
|
||||
<text class="prize-count">x{{ prize.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else class="empty-text">暂无奖品</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel">
|
||||
<view class="panel-head participant-head">
|
||||
<text class="panel-title">参与玩家</text>
|
||||
<view class="participant-head-actions">
|
||||
<view class="head-action-btn overview" @tap="openWinnerOverview">中奖概览</view>
|
||||
<view v-if="showMoreParticipants" class="head-action-btn" @tap="openParticipantsPopup">查看更多</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="participant-meta-row">
|
||||
<text class="panel-side">共 {{ participantCount }} 人</text>
|
||||
<text class="panel-side emphasis">{{ crowdGapText }}</text>
|
||||
</view>
|
||||
<view class="avatar-row" v-if="previewParticipants.length">
|
||||
<view v-for="player in previewParticipants" :key="player.user_id" class="avatar-item" :class="{ mine: isSelfParticipant(player) }">
|
||||
<image class="participant-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">暂无参与玩家</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel history-entry" @tap="goHistory">
|
||||
<view>
|
||||
<text class="panel-title">查看往期裂变活动</text>
|
||||
<text class="history-subtitle">浏览历史开奖与流产活动</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="winnerPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
|
||||
<view class="winner-popup-mask" @tap="winnerPopupVisible = false"></view>
|
||||
<view class="winner-popup-panel" @tap.stop>
|
||||
<view class="winner-popup-head">
|
||||
<text class="winner-popup-title">当前活动中奖概览</text>
|
||||
<text class="winner-popup-close" @tap="winnerPopupVisible = false">×</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="winner-popup-list">
|
||||
<view v-if="winnerOverviewList.length">
|
||||
<view v-for="item in winnerOverviewList" :key="item.id" class="winner-popup-item">
|
||||
<image v-if="item.prize_image" class="winner-popup-image" :src="item.prize_image" mode="aspectFill" />
|
||||
<view v-else class="winner-popup-image empty">{{ rewardPlaceholderText(item) }}</view>
|
||||
<view class="winner-popup-info">
|
||||
<text class="winner-popup-name">{{ item.nickname || ('用户' + item.user_id) }} · {{ item.prize_name }}</text>
|
||||
<text class="winner-popup-price">参考价 ¥{{ formatAmount(winnerPrice(item)) }}</text>
|
||||
<text class="winner-popup-time">{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">还未开奖</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="participantsPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
|
||||
<view class="winner-popup-mask" @tap="participantsPopupVisible = false"></view>
|
||||
<view class="winner-popup-panel" @tap.stop>
|
||||
<view class="winner-popup-head">
|
||||
<text class="winner-popup-title">全部参与玩家</text>
|
||||
<text class="winner-popup-close" @tap="participantsPopupVisible = false">×</text>
|
||||
</view>
|
||||
<view class="winner-popup-meta">
|
||||
<text class="winner-popup-meta-main">当前 {{ participantCount }} / {{ detail.min_participants || 0 }} 人</text>
|
||||
<text class="winner-popup-meta-sub">{{ crowdGapText }}</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="winner-popup-list">
|
||||
<view v-if="allParticipants.length">
|
||||
<view v-for="player in allParticipants" :key="player.user_id" class="winner-popup-item participant-popup-item">
|
||||
<image class="winner-popup-image participant-popup-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
|
||||
<view class="winner-popup-info">
|
||||
<text class="winner-popup-name">{{ player.nickname || ('用户' + player.user_id) }}</text>
|
||||
<text class="winner-popup-time" :class="{ 'self-tag': isSelfParticipant(player) }">{{ isSelfParticipant(player) ? '我已参与' : '活动参与玩家' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">暂无参与玩家</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getMyThresholdActivity,
|
||||
getThresholdActivity,
|
||||
joinThresholdActivity,
|
||||
listThresholdParticipants,
|
||||
listThresholdWinners
|
||||
} from '@/api/thresholdActivity'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
detail: null,
|
||||
winners: [],
|
||||
participants: [],
|
||||
participantsPage: 1,
|
||||
participantsPageSize: 20,
|
||||
participantsLoading: false,
|
||||
winnerPopupVisible: false,
|
||||
participantsPopupVisible: false,
|
||||
currentUserId: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
qualification() {
|
||||
return this.detail?.qualification_progress || {}
|
||||
},
|
||||
sortedPrizes() {
|
||||
const prizes = Array.isArray(this.detail?.prizes) ? [...this.detail.prizes] : []
|
||||
return prizes.sort((a, b) => {
|
||||
const aPrice = this.prizePrice(a)
|
||||
const bPrice = this.prizePrice(b)
|
||||
if (aPrice !== bPrice) return bPrice - aPrice
|
||||
return Number(a.sort || 0) - Number(b.sort || 0)
|
||||
})
|
||||
},
|
||||
spendProgressPercent() {
|
||||
const current = Number(this.qualification?.current_paid || 0)
|
||||
const target = Number(this.detail?.spend_threshold_amount || 0)
|
||||
if (target <= 0) return 0
|
||||
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
|
||||
},
|
||||
inviteProgressPercent() {
|
||||
const current = Number(this.qualification?.effective_invite_count || 0)
|
||||
const target = Number(this.detail?.invite_threshold_count || 0)
|
||||
if (target <= 0) return 0
|
||||
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
|
||||
},
|
||||
crowdProgressPercent() {
|
||||
const current = Number(this.detail?.participant_count || 0)
|
||||
const target = Number(this.detail?.min_participants || 0)
|
||||
if (target <= 0) return 0
|
||||
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
|
||||
},
|
||||
crowdGapText() {
|
||||
const target = Number(this.detail?.min_participants || 0)
|
||||
const current = Number(this.detail?.participant_count || 0)
|
||||
const gap = Math.max(0, target - current)
|
||||
if (this.detail?.status === 'aborted') return '本期参与人数未达标,活动已流产'
|
||||
if (this.detail?.status === 'finished') return '本期已结束'
|
||||
if (gap <= 0) return '已满足开奖人数条件'
|
||||
return `还差 ${gap} 人达到开奖标准`
|
||||
},
|
||||
crowdGapCount() {
|
||||
return Math.max(0, Number(this.detail?.min_participants || 0) - Number(this.detail?.participant_count || 0))
|
||||
},
|
||||
showInviteGuide() {
|
||||
return this.detail?.status === 'active' && this.crowdGapCount > 0
|
||||
},
|
||||
crowdSummary() {
|
||||
const current = Number(this.detail?.participant_count || 0)
|
||||
const target = Number(this.detail?.min_participants || 0)
|
||||
return `当前已参与 ${current} 人 / 开奖至少需要 ${target} 人`
|
||||
},
|
||||
crowdHint() {
|
||||
const gap = Math.max(0, Number(this.detail?.min_participants || 0) - Number(this.detail?.participant_count || 0))
|
||||
if (this.detail?.status === 'aborted') return '本期人数不足,未开奖'
|
||||
if (this.detail?.status === 'finished') return '本期已开奖结束'
|
||||
if (gap <= 0) return '已满足开奖人数条件,等待开奖'
|
||||
return `还差 ${gap} 人即可开奖`
|
||||
},
|
||||
qualificationSummary() {
|
||||
if (this.detail?.joined) return '您已成功参加本期活动'
|
||||
if (this.detail?.status === 'aborted') return '本期人数不足,活动已流产'
|
||||
if (this.detail?.status === 'finished') return '活动已开奖结束'
|
||||
if (this.detail?.can_join) return '已达到参与门槛,可立即报名'
|
||||
return this.buildPendingHint()
|
||||
},
|
||||
joinButtonText() {
|
||||
if (this.detail?.joined) return '已参加'
|
||||
if (this.detail?.status === 'aborted') return '已流产'
|
||||
if (this.detail?.status === 'finished') return '已结束'
|
||||
const startTime = this.detail?.start_time ? new Date(this.detail.start_time).getTime() : 0
|
||||
if (startTime && startTime > Date.now()) return '未开始'
|
||||
if (this.detail?.can_join) return '参加活动'
|
||||
return '未达门槛'
|
||||
},
|
||||
joinButtonClass() {
|
||||
if (this.detail?.can_join && !this.detail?.joined) return 'primary'
|
||||
if (this.detail?.joined) return 'joined'
|
||||
if (this.detail?.status === 'aborted') return 'aborted'
|
||||
return 'muted'
|
||||
},
|
||||
participantCount() {
|
||||
return Number(this.detail?.participant_count || this.participants.length || 0)
|
||||
},
|
||||
previewParticipants() {
|
||||
return this.sortedParticipants.slice(0, 6)
|
||||
},
|
||||
sortedParticipants() {
|
||||
const list = Array.isArray(this.participants) ? [...this.participants] : []
|
||||
if (!this.currentUserId) return list
|
||||
return list.sort((a, b) => {
|
||||
const aSelf = Number(a?.user_id || 0) === this.currentUserId ? 1 : 0
|
||||
const bSelf = Number(b?.user_id || 0) === this.currentUserId ? 1 : 0
|
||||
return bSelf - aSelf
|
||||
})
|
||||
},
|
||||
showMoreParticipants() {
|
||||
return this.participantCount > this.previewParticipants.length
|
||||
},
|
||||
allParticipants() {
|
||||
return this.sortedParticipants
|
||||
},
|
||||
winnerOverviewList() {
|
||||
return Array.isArray(this.winners) ? this.winners : []
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.id = Number(options?.id || 0)
|
||||
const inviteCode = options?.invite_code || options?.inviteCode || ''
|
||||
if (inviteCode) {
|
||||
try { uni.setStorageSync('inviter_code', inviteCode) } catch (_) {}
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
onShareAppMessage() {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
const title = this?.detail?.title || '拉新裂变活动'
|
||||
const crowd = Number(this?.detail?.participant_count || 0)
|
||||
const min = Number(this?.detail?.min_participants || 0)
|
||||
const gap = Math.max(0, min - crowd)
|
||||
return {
|
||||
title: gap > 0 ? `${title}|还差 ${gap} 人开奖,快来一起冲!` : `${title}|已达开奖人数,快来参加!`,
|
||||
path: `/pages-user/invite/landing?invite_code=${inviteCode}`,
|
||||
imageUrl: this?.detail?.cover_image || '/static/logo.png'
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
const title = this?.detail?.title || '拉新裂变活动'
|
||||
return {
|
||||
title,
|
||||
query: `invite_code=${inviteCode}`,
|
||||
imageUrl: this?.detail?.cover_image || '/static/logo.png'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
if (!this.id) return
|
||||
try {
|
||||
try {
|
||||
this.detail = await getMyThresholdActivity(this.id)
|
||||
} catch (_) {
|
||||
this.detail = await getThresholdActivity(this.id)
|
||||
}
|
||||
if (!this.detail.cover_image && this.detail.prizes && this.detail.prizes.length) {
|
||||
this.detail.cover_image = this.detail.prizes.find((item) => item.image)?.image || ''
|
||||
}
|
||||
this.participants = Array.isArray(this.detail?.participants) ? this.detail.participants : []
|
||||
this.currentUserId = Number(uni.getStorageSync('user_id') || 0)
|
||||
this.participantsPage = 1
|
||||
const winnersRes = await listThresholdWinners(this.id, 1, 100)
|
||||
this.winners = winnersRes?.list || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handleJoin() {
|
||||
if (this.detail?.joined || !this.detail?.can_join) return
|
||||
try {
|
||||
await joinThresholdActivity(this.id)
|
||||
uni.showToast({ title: '参加成功', icon: 'success' })
|
||||
await this.loadData()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '参加失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
goMyInvites() {
|
||||
uni.navigateTo({ url: '/pages-user/invites/index' })
|
||||
},
|
||||
copyInviteTip() {
|
||||
const title = this.detail?.title || '裂变活动'
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
const text = inviteCode ? `一起参加${title},用我的邀请码 ${inviteCode} 登录后更快凑齐开奖人数!` : `一起参加${title},快来帮我凑齐开奖人数!`
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({ title: '邀请口令已复制', icon: 'success' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '复制失败,请手动分享', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadMoreParticipants() {
|
||||
if (this.participantsLoading) return
|
||||
if (this.participants.length >= this.participantCount) return
|
||||
this.participantsLoading = true
|
||||
try {
|
||||
const nextPage = this.participantsPage + 1
|
||||
const res = await listThresholdParticipants(this.id, nextPage, this.participantsPageSize)
|
||||
const list = Array.isArray(res?.list) ? res.list : []
|
||||
const seen = new Set(this.participants.map(item => String(item.user_id)))
|
||||
list.forEach(item => {
|
||||
const key = String(item.user_id)
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
this.participants.push(item)
|
||||
}
|
||||
})
|
||||
this.participantsPage = nextPage
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载参与玩家失败', icon: 'none' })
|
||||
} finally {
|
||||
this.participantsLoading = false
|
||||
}
|
||||
},
|
||||
async openParticipantsPopup() {
|
||||
this.participantsPopupVisible = true
|
||||
await this.loadAllParticipants()
|
||||
},
|
||||
async loadAllParticipants() {
|
||||
if (this.participantsLoading) return
|
||||
while (this.participants.length < this.participantCount) {
|
||||
const prevLength = this.participants.length
|
||||
await this.loadMoreParticipants()
|
||||
if (this.participants.length === prevLength) break
|
||||
}
|
||||
},
|
||||
openWinnerOverview() {
|
||||
this.winnerPopupVisible = true
|
||||
},
|
||||
goHistory() {
|
||||
uni.navigateTo({ url: '/pages-activity/activity/threshold/index?mode=history' })
|
||||
},
|
||||
buildPendingHint() {
|
||||
const qualificationMode = this.detail?.qualification_mode
|
||||
const spendTarget = Number(this.detail?.spend_threshold_amount || 0)
|
||||
const spendCurrent = Number(this.qualification?.current_paid || 0)
|
||||
const inviteTarget = Number(this.detail?.invite_threshold_count || 0)
|
||||
const inviteCurrent = Number(this.qualification?.effective_invite_count || 0)
|
||||
if (qualificationMode === 'spend_only') {
|
||||
return `消费还差 ${this.money(Math.max(0, spendTarget - spendCurrent))} 即可参加`
|
||||
}
|
||||
if (qualificationMode === 'invite_only') {
|
||||
return `有效邀请还差 ${Math.max(0, inviteTarget - inviteCurrent)} 人即可参加`
|
||||
}
|
||||
const parts = []
|
||||
if (spendTarget > spendCurrent) parts.push(`消费还差 ${this.money(spendTarget - spendCurrent)}`)
|
||||
if (inviteTarget > inviteCurrent) parts.push(`有效邀请还差 ${inviteTarget - inviteCurrent} 人`)
|
||||
return parts.length ? `${parts.join(',')} 即可参加` : '当前未满足参加条件'
|
||||
},
|
||||
statusText(status) {
|
||||
return { active: '进行中', finished: '已结束', aborted: '已流产' }[status] || status || '-'
|
||||
},
|
||||
statusClass(status) {
|
||||
return { active: 'status-active', finished: 'status-finished', aborted: 'status-aborted' }[status] || 'status-finished'
|
||||
},
|
||||
typeLabel(type) {
|
||||
return { daily: '每日裂变', weekly: '每周裂变', monthly: '每月裂变' }[type] || '裂变活动'
|
||||
},
|
||||
typeClass(type) {
|
||||
return { daily: 'type-daily', weekly: 'type-weekly', monthly: 'type-monthly' }[type] || 'type-default'
|
||||
},
|
||||
qualificationModeLabel(mode) {
|
||||
return { spend_only: '消费达标', invite_only: '邀请达标', either: '任一达标' }[mode] || '门槛活动'
|
||||
},
|
||||
formatTime(v) {
|
||||
if (!v) return '-'
|
||||
return String(v).replace('T', ' ').slice(0, 16)
|
||||
},
|
||||
formatAmount(cents) {
|
||||
return (Number(cents || 0) / 100).toFixed(2)
|
||||
},
|
||||
money(v) {
|
||||
return `¥${(Number(v || 0) / 100).toFixed(2)}`
|
||||
},
|
||||
prizePrice(prize) {
|
||||
return Number(prize?.price_cents ?? prize?.price ?? prize?.product_price ?? prize?.price_snapshot_cents ?? 0)
|
||||
},
|
||||
winnerPrice(item) {
|
||||
return Number(item?.price_cents ?? item?.price ?? item?.product_price ?? item?.price_snapshot_cents ?? 0)
|
||||
},
|
||||
rewardPlaceholderText(item) {
|
||||
const type = String(item?.reward_type || '').toLowerCase()
|
||||
if (type === 'coupon') return '优惠券'
|
||||
if (type === 'item_card') return '道具卡'
|
||||
return '奖品'
|
||||
},
|
||||
prizePlaceholderText(prize) {
|
||||
return this.rewardPlaceholderText(prize)
|
||||
},
|
||||
isSelfParticipant(player) {
|
||||
return Number(player?.user_id || 0) > 0 && Number(player?.user_id || 0) === this.currentUserId
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.threshold-detail-page { min-height: 100vh; padding-bottom: 40rpx; background: #fff7ed; }
|
||||
.detail-cover { width: 100%; height: 420rpx; display: block; background: #f3f4f6; }
|
||||
.detail-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.detail-panel { margin: 24rpx 28rpx 0; padding: 28rpx; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); }
|
||||
.hero-head { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; }
|
||||
.detail-title { display: block; font-size: 38rpx; font-weight: 900; color: #1f2937; flex: 1; }
|
||||
.detail-type { display: inline-flex; align-items: center; padding: 10rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
|
||||
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
|
||||
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
|
||||
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
|
||||
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
|
||||
.detail-status { display: inline-block; margin-top: 16rpx; padding: 8rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
|
||||
.status-active { background: #dcfce7; color: #16a34a; }
|
||||
.status-finished { background: #e2e8f0; color: #64748b; }
|
||||
.status-aborted { background: #fee2e2; color: #dc2626; }
|
||||
.detail-time, .detail-desc { display: block; margin-top: 16rpx; font-size: 24rpx; color: #4b5563; }
|
||||
.panel-head { margin-bottom: 18rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.panel-title { font-size: 28rpx; font-weight: 900; color: #1f2937; }
|
||||
.panel-side { font-size: 22rpx; color: #9ca3af; }
|
||||
.share-guide-card { margin-top: 18rpx; padding: 24rpx; border-radius: 24rpx; background: linear-gradient(135deg, #fff7ed, #ffedd5); border: 2rpx solid rgba(249, 115, 22, 0.14); }
|
||||
.share-guide-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.share-guide-title { font-size: 28rpx; font-weight: 900; color: #7c2d12; }
|
||||
.share-guide-badge { padding: 8rpx 18rpx; border-radius: 999rpx; background: rgba(249, 115, 22, 0.12); color: #f97316; font-size: 22rpx; font-weight: 800; }
|
||||
.share-guide-desc { display: block; margin-top: 14rpx; font-size: 24rpx; line-height: 1.6; color: #9a3412; }
|
||||
.share-guide-actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
|
||||
.share-action-btn { flex: 1; min-height: 80rpx; display: flex; align-items: center; justify-content: center; border-radius: 999rpx; font-size: 24rpx; font-weight: 800; box-sizing: border-box; }
|
||||
button.share-action-btn { padding: 0 24rpx; line-height: 80rpx; }
|
||||
button.share-action-btn::after { border: none; }
|
||||
.share-action-btn.primary { background: linear-gradient(135deg, #fb923c, #f97316); color: #fff; }
|
||||
.share-action-btn.secondary { background: #fff; color: #ea580c; border: 2rpx solid rgba(249, 115, 22, 0.18); }
|
||||
.metric-card { padding: 22rpx; border-radius: 22rpx; background: #fff7ed; margin-top: 18rpx; }
|
||||
.crowd-panel { background: linear-gradient(180deg, #fff 0%, #fff7ed 100%); }
|
||||
.crowd-progress-bar { height: 18rpx; border-radius: 999rpx; background: #fde7cf; overflow: hidden; margin-top: 18rpx; }
|
||||
.crowd-progress-fill { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #f59e0b, #f97316); }
|
||||
.crowd-summary { display: flex; flex-direction: column; gap: 10rpx; margin-top: 18rpx; }
|
||||
.crowd-summary-main { font-size: 28rpx; font-weight: 800; color: #1f2937; }
|
||||
.crowd-summary-sub { font-size: 22rpx; color: #9a3412; }
|
||||
.metric-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.metric-title { font-size: 26rpx; font-weight: 800; color: #1f2937; }
|
||||
.metric-state { font-size: 22rpx; font-weight: 800; }
|
||||
.metric-state.ok { color: #16a34a; }
|
||||
.metric-state.pending { color: #f97316; }
|
||||
.metric-value { display: block; margin-top: 14rpx; font-size: 30rpx; font-weight: 800; color: #1f2937; }
|
||||
.metric-help { display: block; margin-top: 12rpx; font-size: 22rpx; color: #6b7280; }
|
||||
.progress-bar { height: 18rpx; border-radius: 999rpx; background: #fed7aa; overflow: hidden; margin-top: 14rpx; }
|
||||
.progress-fill { height: 100%; border-radius: 999rpx; }
|
||||
.progress-fill.spend { background: linear-gradient(90deg, #f97316, #fb7185); }
|
||||
.progress-fill.invite { background: linear-gradient(90deg, #0ea5e9, #22c55e); }
|
||||
.qualification-summary { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-top: 22rpx; }
|
||||
.qualification-text { flex: 1; font-size: 22rpx; color: #6b7280; }
|
||||
.join-btn { min-width: 180rpx; padding: 18rpx 28rpx; text-align: center; border-radius: 999rpx; font-size: 24rpx; font-weight: 800; }
|
||||
.join-btn.primary { background: linear-gradient(135deg, #fb923c, #f97316); color: #fff; }
|
||||
.join-btn.joined { background: #dcfce7; color: #16a34a; }
|
||||
.join-btn.aborted { background: #fee2e2; color: #dc2626; }
|
||||
.join-btn.muted { background: #f3f4f6; color: #9ca3af; }
|
||||
.prize-scroll { white-space: nowrap; }
|
||||
.prize-track { display: inline-flex; gap: 20rpx; }
|
||||
.prize-item { width: 260rpx; flex-shrink: 0; }
|
||||
.prize-image { width: 260rpx; height: 180rpx; border-radius: 20rpx; background: #f3f4f6; }
|
||||
.prize-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.prize-name { display: -webkit-box; margin-top: 12rpx; min-height: 68rpx; font-size: 24rpx; font-weight: 700; color: #1f2937; white-space: normal; word-break: break-all; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.prize-price { display: block; margin-top: 8rpx; font-size: 22rpx; color: #f97316; line-height: 1.4; }
|
||||
.prize-count { display: block; margin-top: 6rpx; font-size: 22rpx; color: #6b7280; line-height: 1.4; }
|
||||
.participant-head { align-items: flex-start; }
|
||||
.participant-head-actions { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
|
||||
.participant-meta-row { margin-bottom: 16rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
|
||||
.panel-side.emphasis { color: #f97316; font-weight: 800; }
|
||||
.avatar-row { display: flex; align-items: center; gap: 12rpx; overflow-x: auto; padding-bottom: 4rpx; }
|
||||
.avatar-item { position: relative; flex-shrink: 0; }
|
||||
.avatar-item.mine .participant-avatar { border-color: #fb923c; box-shadow: 0 0 0 4rpx rgba(251, 146, 60, 0.16); }
|
||||
.participant-avatar { width: 68rpx; height: 68rpx; border-radius: 50%; background: #f3f4f6; border: 4rpx solid #fff; box-shadow: 0 8rpx 18rpx rgba(0,0,0,.08); }
|
||||
.head-action-btn { flex-shrink: 0; padding: 14rpx 20rpx; border-radius: 999rpx; background: #fff7ed; color: #f97316; font-size: 22rpx; font-weight: 800; border: 2rpx solid rgba(249,115,22,.18); }
|
||||
.head-action-btn.overview { background: linear-gradient(135deg, #fff7ed, #ffedd5); }
|
||||
.empty-text { font-size: 24rpx; color: #9ca3af; }
|
||||
.history-entry { display: flex; justify-content: space-between; align-items: center; }
|
||||
.history-subtitle { display: block; margin-top: 10rpx; font-size: 22rpx; color: #9ca3af; }
|
||||
.winner-popup-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.winner-popup-mask { position: absolute; inset: 0; background: rgba(0,0,0,.48); }
|
||||
.winner-popup-panel { position: relative; width: 88%; max-height: 72vh; background: #fff; border-radius: 28rpx; overflow: hidden; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.18); }
|
||||
.winner-popup-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 26rpx 28rpx; border-bottom: 2rpx solid #f3f4f6; }
|
||||
.winner-popup-meta { padding: 16rpx 28rpx 0; display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.winner-popup-meta-main { font-size: 24rpx; font-weight: 800; color: #1f2937; }
|
||||
.winner-popup-meta-sub { font-size: 22rpx; color: #f97316; }
|
||||
.winner-popup-title { flex: 1; font-size: 28rpx; font-weight: 900; color: #1f2937; }
|
||||
.winner-popup-close { font-size: 42rpx; color: #9ca3af; line-height: 1; }
|
||||
.winner-popup-list { max-height: 54vh; padding: 24rpx 28rpx; }
|
||||
.winner-popup-item { display: flex; gap: 16rpx; align-items: center; padding: 18rpx 0; }
|
||||
.participant-popup-item { align-items: center; }
|
||||
.participant-popup-avatar { border-radius: 50%; }
|
||||
.self-tag { color: #f97316; font-weight: 800; }
|
||||
.winner-popup-image { width: 96rpx; height: 96rpx; border-radius: 20rpx; background: #f3f4f6; flex-shrink: 0; }
|
||||
.winner-popup-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.winner-popup-info { flex: 1; min-width: 0; }
|
||||
.winner-popup-name { display: block; font-size: 26rpx; font-weight: 800; color: #1f2937; }
|
||||
.winner-popup-price, .winner-popup-time { display: block; margin-top: 8rpx; font-size: 22rpx; color: #6b7280; }
|
||||
</style>
|
||||
130
pages-activity/activity/threshold/index.vue
Normal file
130
pages-activity/activity/threshold/index.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<view class="threshold-list-page">
|
||||
<view class="page-head">
|
||||
<text class="page-title">{{ mode === 'history' ? '往期裂变活动' : '拉新裂变活动' }}</text>
|
||||
<text class="page-subtitle">{{ mode === 'history' ? '查看历史开奖与流产活动' : '完成消费或邀请门槛即可参与' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-btn" :class="{ active: mode === 'active' }" @tap="switchMode('active')">进行中</view>
|
||||
<view class="toolbar-btn" :class="{ active: mode === 'history' }" @tap="switchMode('history')">往期</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="state">加载中...</view>
|
||||
<view v-else-if="activities.length === 0" class="state">{{ mode === 'history' ? '暂无往期活动' : '暂无进行中的活动' }}</view>
|
||||
|
||||
<view v-else class="activity-grid">
|
||||
<view v-for="item in activities" :key="item.id" class="activity-card" @tap="goDetail(item.id)">
|
||||
<image v-if="item.cover_image" class="activity-cover" :src="item.cover_image" mode="aspectFill" />
|
||||
<view v-else class="activity-cover empty">暂无图片</view>
|
||||
<view class="activity-meta">
|
||||
<view class="title-row">
|
||||
<text class="activity-name">{{ item.title }}</text>
|
||||
<text class="activity-status" :class="statusClass(item.status)">{{ statusLabel(item.status) }}</text>
|
||||
</view>
|
||||
<view class="tag-row">
|
||||
<text class="activity-type" :class="typeClass(item.type)">{{ typeLabel(item.type) }}</text>
|
||||
<text class="activity-mode">{{ qualificationModeLabel(item.qualification_mode) }}</text>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text>消费门槛 {{ money(item.spend_threshold_amount) }}</text>
|
||||
<text>最低开奖 {{ item.min_participants || 0 }} 人</text>
|
||||
</view>
|
||||
<view class="stat-row" v-if="Number(item.invite_threshold_count || 0) > 0">
|
||||
<text>邀请门槛 {{ item.invite_threshold_count || 0 }} 人</text>
|
||||
<text>有效消费 {{ money(item.invite_effective_amount) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listThresholdActivities } from '@/api/thresholdActivity'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
mode: 'active',
|
||||
activities: []
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.mode = options?.mode === 'history' ? 'history' : 'active'
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = { page: 1, page_size: 50 }
|
||||
if (this.mode === 'active') params.status = 'active'
|
||||
const res = await listThresholdActivities(params)
|
||||
const list = Array.isArray(res?.list) ? res.list : []
|
||||
this.activities = this.mode === 'history' ? list.filter(item => item.status !== 'active') : list
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
switchMode(mode) {
|
||||
if (this.mode === mode) return
|
||||
this.mode = mode
|
||||
this.loadData()
|
||||
},
|
||||
goDetail(id) {
|
||||
uni.navigateTo({ url: `/pages-activity/activity/threshold/detail?id=${id}` })
|
||||
},
|
||||
typeLabel(type) {
|
||||
return { daily: '每日裂变', weekly: '每周裂变', monthly: '每月裂变' }[type] || '裂变活动'
|
||||
},
|
||||
typeClass(type) {
|
||||
return { daily: 'type-daily', weekly: 'type-weekly', monthly: 'type-monthly' }[type] || 'type-default'
|
||||
},
|
||||
qualificationModeLabel(mode) {
|
||||
return { spend_only: '消费达标', invite_only: '邀请达标', either: '任一达标' }[mode] || '门槛活动'
|
||||
},
|
||||
statusLabel(status) {
|
||||
return { active: '进行中', finished: '已结束', aborted: '已流产' }[status] || status || '-'
|
||||
},
|
||||
statusClass(status) {
|
||||
return { active: 'status-active', finished: 'status-finished', aborted: 'status-aborted' }[status] || 'status-finished'
|
||||
},
|
||||
money(v) {
|
||||
return `¥${(Number(v || 0) / 100).toFixed(2)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.threshold-list-page { min-height: 100vh; padding: 28rpx; background: #fff7ed; }
|
||||
.page-head { margin-bottom: 28rpx; }
|
||||
.page-title { display: block; font-size: 46rpx; font-weight: 900; color: #1f2937; }
|
||||
.page-subtitle { display: block; margin-top: 10rpx; font-size: 24rpx; color: #9ca3af; }
|
||||
.toolbar { display: flex; gap: 16rpx; margin-bottom: 28rpx; }
|
||||
.toolbar-btn { flex: 1; text-align: center; padding: 20rpx 0; background: #fff; border-radius: 999rpx; color: #9a5b24; font-weight: 800; }
|
||||
.toolbar-btn.active { background: #ff8a3d; color: #fff; }
|
||||
.state { padding: 120rpx 0; text-align: center; color: #9ca3af; }
|
||||
.activity-grid { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.activity-card { overflow: hidden; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); }
|
||||
.activity-cover { width: 100%; height: 260rpx; display: block; background: #f3f4f6; }
|
||||
.activity-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.activity-meta { padding: 24rpx 22rpx; display: flex; flex-direction: column; gap: 14rpx; }
|
||||
.title-row, .tag-row, .stat-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
|
||||
.activity-name { flex: 1; min-width: 0; font-size: 30rpx; font-weight: 800; color: #1f2937; }
|
||||
.activity-status { font-size: 22rpx; font-weight: 800; }
|
||||
.status-active { color: #10b981; }
|
||||
.status-finished { color: #94a3b8; }
|
||||
.status-aborted { color: #ef4444; }
|
||||
.activity-type, .activity-mode { display: inline-flex; align-items: center; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
|
||||
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
|
||||
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
|
||||
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
|
||||
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
|
||||
.activity-mode { background: rgba(14, 165, 233, .12); color: #0284c7; }
|
||||
.stat-row { font-size: 22rpx; color: #6b7280; }
|
||||
</style>
|
||||
423
pages-activity/activity/welfare/detail.vue
Normal file
423
pages-activity/activity/welfare/detail.vue
Normal file
@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<view class="welfare-detail-page" v-if="detail">
|
||||
<image v-if="detail.cover_image" class="detail-cover" :src="detail.cover_image" mode="aspectFill" />
|
||||
<view v-else class="detail-cover empty">暂无活动图片</view>
|
||||
|
||||
<view class="detail-panel hero-panel">
|
||||
<view class="hero-head">
|
||||
<text class="detail-title">{{ detail.title }}</text>
|
||||
<text class="detail-type" :class="typeClass(detail.type)">{{ typeLabel(detail.type) }}</text>
|
||||
</view>
|
||||
<text class="detail-status">{{ statusText(detail.status) }}</text>
|
||||
<text class="detail-time">开奖时间:{{ formatTime(detail.draw_time) }}</text>
|
||||
<text class="detail-desc" v-if="detail.description">{{ detail.description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel join-panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">我的参与进度</text>
|
||||
<text class="panel-side">{{ progressText }}</text>
|
||||
</view>
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
|
||||
</view>
|
||||
<view class="progress-foot">
|
||||
<text class="progress-hint">{{ progressHint }}</text>
|
||||
<view
|
||||
class="join-btn"
|
||||
:class="joinButtonClass"
|
||||
@tap="handleJoin"
|
||||
>
|
||||
{{ joinButtonText }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">奖品</text>
|
||||
<text v-if="sortedPrizes.length > 1" class="panel-side">按价格从高到低</text>
|
||||
</view>
|
||||
<scroll-view v-if="sortedPrizes.length" scroll-x class="prize-scroll" show-scrollbar="false">
|
||||
<view class="prize-track">
|
||||
<view v-for="prize in sortedPrizes" :key="prize.id" class="prize-item">
|
||||
<image v-if="prize.image" class="prize-image" :src="prize.image" mode="aspectFill" />
|
||||
<view v-else class="prize-image empty">{{ prizePlaceholderText(prize) }}</view>
|
||||
<text class="prize-name">{{ prize.name }}</text>
|
||||
<text class="prize-price">参考价 ¥{{ formatAmount(prizePrice(prize)) }}</text>
|
||||
<text class="prize-count">x{{ prize.quantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else class="empty-text">暂无奖品</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel">
|
||||
<view class="panel-head participant-head">
|
||||
<text class="panel-title">参与玩家</text>
|
||||
<view class="participant-head-actions">
|
||||
<view class="head-action-btn overview" @tap="openWinnerOverview">中奖概览</view>
|
||||
<view v-if="showMoreParticipants" class="head-action-btn" @tap="openParticipantsPopup">查看更多</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="participant-meta-row">
|
||||
<text class="panel-side">共 {{ participantCount }} 人</text>
|
||||
</view>
|
||||
<view class="avatar-row" v-if="previewParticipants.length">
|
||||
<view
|
||||
v-for="player in previewParticipants"
|
||||
:key="player.user_id"
|
||||
class="avatar-item"
|
||||
:class="{ mine: isSelfParticipant(player) }"
|
||||
>
|
||||
<image class="participant-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">暂无参与玩家</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-panel history-entry" @tap="goHistory">
|
||||
<view>
|
||||
<text class="panel-title">查看往期活动</text>
|
||||
<text class="history-subtitle">浏览已结束的福利活动与中奖信息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="winnerPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
|
||||
<view class="winner-popup-mask" @tap="winnerPopupVisible = false"></view>
|
||||
<view class="winner-popup-panel" @tap.stop>
|
||||
<view class="winner-popup-head">
|
||||
<text class="winner-popup-title">当前活动中奖概览</text>
|
||||
<text class="winner-popup-close" @tap="winnerPopupVisible = false">×</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="winner-popup-list">
|
||||
<view v-if="winnerOverviewList.length">
|
||||
<view v-for="item in winnerOverviewList" :key="item.id" class="winner-popup-item">
|
||||
<image v-if="item.prize_image" class="winner-popup-image" :src="item.prize_image" mode="aspectFill" />
|
||||
<view v-else class="winner-popup-image empty">{{ rewardPlaceholderText(item) }}</view>
|
||||
<view class="winner-popup-info">
|
||||
<text class="winner-popup-name">{{ item.nickname || ('用户' + item.user_id) }} · {{ item.prize_name }}</text>
|
||||
<text class="winner-popup-price">参考价 ¥{{ formatAmount(winnerPrice(item)) }}</text>
|
||||
<text class="winner-popup-time">{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">还未开奖</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="participantsPopupVisible" class="winner-popup-overlay" @touchmove.stop.prevent>
|
||||
<view class="winner-popup-mask" @tap="participantsPopupVisible = false"></view>
|
||||
<view class="winner-popup-panel" @tap.stop>
|
||||
<view class="winner-popup-head">
|
||||
<text class="winner-popup-title">全部参与玩家</text>
|
||||
<text class="winner-popup-close" @tap="participantsPopupVisible = false">×</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="winner-popup-list">
|
||||
<view v-if="allParticipants.length">
|
||||
<view v-for="player in allParticipants" :key="player.user_id" class="winner-popup-item participant-popup-item">
|
||||
<image class="winner-popup-image participant-popup-avatar" :src="player.avatar || '/static/logo.png'" mode="aspectFill" />
|
||||
<view class="winner-popup-info">
|
||||
<text class="winner-popup-name">{{ player.nickname || ('用户' + player.user_id) }}</text>
|
||||
<text class="winner-popup-time" :class="{ 'self-tag': isSelfParticipant(player) }">
|
||||
{{ isSelfParticipant(player) ? '我已参与' : '活动参与玩家' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-text">暂无参与玩家</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { request, authRequest } from '@/utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
detail: null,
|
||||
winners: [],
|
||||
participants: [],
|
||||
participantsPage: 1,
|
||||
participantsPageSize: 20,
|
||||
participantsLoading: false,
|
||||
winnerPopupVisible: false,
|
||||
participantsPopupVisible: false,
|
||||
currentUserId: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedPrizes() {
|
||||
const prizes = Array.isArray(this.detail?.prizes) ? [...this.detail.prizes] : []
|
||||
return prizes.sort((a, b) => {
|
||||
const aPrice = this.prizePrice(a)
|
||||
const bPrice = this.prizePrice(b)
|
||||
if (aPrice !== bPrice) return bPrice - aPrice
|
||||
return Number(a.sort || 0) - Number(b.sort || 0)
|
||||
})
|
||||
},
|
||||
progressPercent() {
|
||||
const current = Number(this.detail?.current_paid || 0)
|
||||
const target = Number(this.detail?.threshold_amount || 0)
|
||||
if (target <= 0) return this.detail?.joined ? 100 : 0
|
||||
return Math.max(0, Math.min(100, Math.round((current / target) * 100)))
|
||||
},
|
||||
progressText() {
|
||||
const current = this.formatAmount(this.detail?.current_paid || 0)
|
||||
const target = this.formatAmount(this.detail?.threshold_amount || 0)
|
||||
return `${this.windowLabel(this.detail?.type)}消费 ${current}/${target}`
|
||||
},
|
||||
progressHint() {
|
||||
if (this.detail?.joined) return '您已成功参加本期活动'
|
||||
if (this.detail?.can_join) return '已达参与门槛,可立即参加活动'
|
||||
const startTime = this.detail?.start_time ? new Date(this.detail.start_time).getTime() : 0
|
||||
if (startTime && startTime > Date.now()) {
|
||||
return `活动将于 ${this.formatTime(this.detail.start_time)} 开始`
|
||||
}
|
||||
const target = Number(this.detail?.threshold_amount || 0)
|
||||
const current = Number(this.detail?.current_paid || 0)
|
||||
if (target > current) {
|
||||
return `还差 ¥${this.formatAmount(target - current)} 即可参加`
|
||||
}
|
||||
return '当前未满足参加条件'
|
||||
},
|
||||
joinButtonText() {
|
||||
if (this.detail?.joined) return '已参加'
|
||||
const startTime = this.detail?.start_time ? new Date(this.detail.start_time).getTime() : 0
|
||||
if (startTime && startTime > Date.now()) return '未开始'
|
||||
if (this.detail?.can_join) return '参加活动'
|
||||
return '未达门槛'
|
||||
},
|
||||
joinButtonClass() {
|
||||
if (this.detail?.joined) return 'disabled'
|
||||
if (this.detail?.can_join) return 'primary'
|
||||
return 'muted'
|
||||
},
|
||||
participantCount() {
|
||||
return Number(this.detail?.participant_count || this.participants.length || 0)
|
||||
},
|
||||
previewParticipants() {
|
||||
return this.sortedParticipants.slice(0, 6)
|
||||
},
|
||||
sortedParticipants() {
|
||||
const list = Array.isArray(this.participants) ? [...this.participants] : []
|
||||
if (!this.currentUserId) return list
|
||||
return list.sort((a, b) => {
|
||||
const aSelf = Number(a?.user_id || 0) === this.currentUserId ? 1 : 0
|
||||
const bSelf = Number(b?.user_id || 0) === this.currentUserId ? 1 : 0
|
||||
return bSelf - aSelf
|
||||
})
|
||||
},
|
||||
showMoreParticipants() {
|
||||
return this.participantCount > this.previewParticipants.length
|
||||
},
|
||||
allParticipants() {
|
||||
return this.sortedParticipants
|
||||
},
|
||||
winnerOverviewList() {
|
||||
return Array.isArray(this.winners) ? this.winners : []
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
this.id = Number(options?.id || 0)
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
if (!this.id) return
|
||||
try {
|
||||
try {
|
||||
this.detail = await authRequest({ url: `/api/app/welfare-activities/${this.id}/my`, suppressAuthModal: true })
|
||||
} catch (_) {
|
||||
this.detail = await request({ url: `/api/app/welfare-activities/${this.id}` })
|
||||
}
|
||||
if (!this.detail.cover_image && this.detail.prizes && this.detail.prizes.length) {
|
||||
this.detail.cover_image = this.detail.prizes.find((item) => item.image)?.image || ''
|
||||
}
|
||||
this.participants = Array.isArray(this.detail?.participants) ? this.detail.participants : []
|
||||
this.currentUserId = Number(uni.getStorageSync('user_id') || 0)
|
||||
this.participantsPage = 1
|
||||
const winnersRes = await request({ url: `/api/app/welfare-activities/${this.id}/winners?page=1&page_size=100` })
|
||||
this.winners = winnersRes?.list || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handleJoin() {
|
||||
if (this.detail?.joined || !this.detail?.can_join) return
|
||||
try {
|
||||
await authRequest({ url: `/api/app/welfare-activities/${this.id}/join`, method: 'POST' })
|
||||
uni.showToast({ title: '参加成功', icon: 'success' })
|
||||
await this.loadData()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '参加失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async loadMoreParticipants() {
|
||||
if (this.participantsLoading) return
|
||||
if (this.participants.length >= this.participantCount) return
|
||||
this.participantsLoading = true
|
||||
try {
|
||||
const nextPage = this.participantsPage + 1
|
||||
const res = await request({
|
||||
url: `/api/app/welfare-activities/${this.id}/participants?page=${nextPage}&page_size=${this.participantsPageSize}`
|
||||
})
|
||||
const list = Array.isArray(res?.list) ? res.list : []
|
||||
const seen = new Set(this.participants.map(item => String(item.user_id)))
|
||||
list.forEach(item => {
|
||||
const key = String(item.user_id)
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
this.participants.push(item)
|
||||
}
|
||||
})
|
||||
this.participantsPage = nextPage
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载参与玩家失败', icon: 'none' })
|
||||
} finally {
|
||||
this.participantsLoading = false
|
||||
}
|
||||
},
|
||||
async openParticipantsPopup() {
|
||||
this.participantsPopupVisible = true
|
||||
await this.loadAllParticipants()
|
||||
},
|
||||
async loadAllParticipants() {
|
||||
if (this.participantsLoading) return
|
||||
while (this.participants.length < this.participantCount) {
|
||||
const prevLength = this.participants.length
|
||||
await this.loadMoreParticipants()
|
||||
if (this.participants.length === prevLength) break
|
||||
}
|
||||
},
|
||||
openWinnerOverview() {
|
||||
this.winnerPopupVisible = true
|
||||
},
|
||||
goHistory() {
|
||||
uni.navigateTo({ url: '/pages-activity/activity/welfare/index?mode=finished' })
|
||||
},
|
||||
statusText(status) {
|
||||
return { active: '进行中', finished: '已结束' }[status] || status || '-'
|
||||
},
|
||||
typeLabel(type) {
|
||||
return { daily: '每日福利', weekly: '每周福利', monthly: '每月福利' }[type] || '福利活动'
|
||||
},
|
||||
typeClass(type) {
|
||||
return {
|
||||
daily: 'type-daily',
|
||||
weekly: 'type-weekly',
|
||||
monthly: 'type-monthly'
|
||||
}[type] || 'type-default'
|
||||
},
|
||||
windowLabel(type) {
|
||||
return { daily: '每日', weekly: '每周', monthly: '每月' }[type] || '活动'
|
||||
},
|
||||
formatTime(v) {
|
||||
if (!v) return '-'
|
||||
return String(v).replace('T', ' ').slice(0, 16)
|
||||
},
|
||||
formatAmount(cents) {
|
||||
return (Number(cents || 0) / 100).toFixed(2)
|
||||
},
|
||||
prizePrice(prize) {
|
||||
return Number(prize?.price_cents ?? prize?.price ?? prize?.product_price ?? prize?.price_snapshot_cents ?? 0)
|
||||
},
|
||||
winnerPrice(item) {
|
||||
return Number(item?.price_cents ?? item?.price ?? item?.product_price ?? item?.price_snapshot_cents ?? 0)
|
||||
},
|
||||
rewardPlaceholderText(item) {
|
||||
const type = String(item?.reward_type || '').toLowerCase()
|
||||
if (type === 'coupon') return '优惠券'
|
||||
if (type === 'item_card') return '道具卡'
|
||||
return '奖品'
|
||||
},
|
||||
prizePlaceholderText(prize) {
|
||||
return this.rewardPlaceholderText(prize)
|
||||
},
|
||||
isSelfParticipant(player) {
|
||||
return Number(player?.user_id || 0) > 0 && Number(player?.user_id || 0) === this.currentUserId
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.welfare-detail-page { min-height: 100vh; padding-bottom: 40rpx; background: #fff7ed; }
|
||||
.detail-cover { width: 100%; height: 420rpx; display: block; background: #f3f4f6; }
|
||||
.detail-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.detail-panel { margin: 24rpx 28rpx 0; padding: 28rpx; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); }
|
||||
.hero-head { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; }
|
||||
.detail-title { display: block; font-size: 38rpx; font-weight: 900; color: #1f2937; flex: 1; }
|
||||
.detail-type { display: inline-flex; align-items: center; padding: 10rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
|
||||
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
|
||||
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
|
||||
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
|
||||
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
|
||||
.detail-status { display: inline-block; margin-top: 16rpx; padding: 8rpx 20rpx; border-radius: 999rpx; background: #fef3c7; color: #b45309; font-size: 22rpx; font-weight: 800; }
|
||||
.detail-time, .detail-desc { display: block; margin-top: 16rpx; font-size: 24rpx; color: #4b5563; }
|
||||
.panel-head { margin-bottom: 18rpx; display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.panel-head.compact { margin-bottom: 10rpx; }
|
||||
.panel-title { font-size: 28rpx; font-weight: 900; color: #1f2937; }
|
||||
.panel-side { font-size: 22rpx; color: #9ca3af; }
|
||||
.progress-bar { height: 18rpx; border-radius: 999rpx; background: #fed7aa; overflow: hidden; }
|
||||
.progress-fill { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #f97316, #fb7185); }
|
||||
.progress-foot { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; margin-top: 20rpx; }
|
||||
.progress-hint { flex: 1; font-size: 22rpx; color: #6b7280; }
|
||||
.join-btn { min-width: 180rpx; padding: 18rpx 28rpx; text-align: center; border-radius: 999rpx; font-size: 24rpx; font-weight: 800; }
|
||||
.join-btn.primary { background: linear-gradient(135deg, #fb923c, #f97316); color: #fff; }
|
||||
.join-btn.disabled { background: #dcfce7; color: #16a34a; }
|
||||
.join-btn.muted { background: #f3f4f6; color: #9ca3af; }
|
||||
.prize-scroll { white-space: nowrap; }
|
||||
.prize-track { display: inline-flex; gap: 20rpx; }
|
||||
.prize-item { width: 260rpx; flex-shrink: 0; }
|
||||
.prize-image { width: 260rpx; height: 180rpx; border-radius: 20rpx; background: #f3f4f6; }
|
||||
.prize-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.prize-name { display: -webkit-box; margin-top: 12rpx; min-height: 68rpx; font-size: 24rpx; font-weight: 700; color: #1f2937; white-space: normal; word-break: break-all; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||
.prize-price { display: block; margin-top: 8rpx; font-size: 22rpx; color: #f97316; line-height: 1.4; }
|
||||
.prize-count { display: block; margin-top: 6rpx; font-size: 22rpx; color: #6b7280; line-height: 1.4; }
|
||||
.participant-preview { display: flex; align-items: center; gap: 12rpx; overflow: hidden; }
|
||||
.participant-head { align-items: flex-start; }
|
||||
.participant-head-actions { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
|
||||
.participant-meta-row { margin-bottom: 16rpx; }
|
||||
.avatar-row { display: flex; align-items: center; gap: 12rpx; overflow-x: auto; padding-bottom: 4rpx; }
|
||||
.avatar-item { position: relative; flex-shrink: 0; }
|
||||
.avatar-item.mine .participant-avatar { border-color: #fb923c; box-shadow: 0 0 0 4rpx rgba(251, 146, 60, 0.16); }
|
||||
.participant-avatar { width: 68rpx; height: 68rpx; border-radius: 50%; background: #f3f4f6; border: 4rpx solid #fff; box-shadow: 0 8rpx 18rpx rgba(0,0,0,.08); }
|
||||
.head-action-btn { flex-shrink: 0; padding: 14rpx 20rpx; border-radius: 999rpx; background: #fff7ed; color: #f97316; font-size: 22rpx; font-weight: 800; border: 2rpx solid rgba(249,115,22,.18); }
|
||||
.head-action-btn.overview { background: linear-gradient(135deg, #fff7ed, #ffedd5); }
|
||||
.more-count { min-width: 68rpx; height: 68rpx; padding: 0 16rpx; border-radius: 34rpx; background: #fff7ed; color: #f97316; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 800; }
|
||||
.participant-list { display: none; }
|
||||
.participant-overview-card, .participant-card { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 20rpx; border-radius: 22rpx; background: #fff7ed; }
|
||||
.overview-icon { width: 76rpx; height: 76rpx; border-radius: 50%; background: linear-gradient(135deg, #fde68a, #fb7185); display: flex; align-items: center; justify-content: center; font-size: 34rpx; flex-shrink: 0; }
|
||||
.participant-main { display: flex; align-items: center; gap: 14rpx; flex: 1; min-width: 0; }
|
||||
.participant-card-avatar { width: 76rpx; height: 76rpx; border-radius: 50%; background: #f3f4f6; }
|
||||
.participant-card-info { flex: 1; min-width: 0; }
|
||||
.participant-name { display: block; font-size: 26rpx; font-weight: 800; color: #1f2937; }
|
||||
.participant-sub { display: block; margin-top: 8rpx; font-size: 22rpx; color: #6b7280; }
|
||||
.winner-btn { padding: 14rpx 20rpx; border-radius: 999rpx; background: #fff; color: #f97316; font-size: 22rpx; font-weight: 800; flex-shrink: 0; }
|
||||
.overview-btn { min-width: 112rpx; text-align: center; }
|
||||
.expand-btn { display: none; }
|
||||
.empty-text { font-size: 24rpx; color: #9ca3af; }
|
||||
.history-entry { display: flex; justify-content: space-between; align-items: center; }
|
||||
.history-subtitle { display: block; margin-top: 10rpx; font-size: 22rpx; color: #9ca3af; }
|
||||
.winner-popup-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
|
||||
.winner-popup-mask { position: absolute; inset: 0; background: rgba(0,0,0,.48); }
|
||||
.winner-popup-panel { position: relative; width: 88%; max-height: 72vh; background: #fff; border-radius: 28rpx; overflow: hidden; box-shadow: 0 18rpx 50rpx rgba(0,0,0,.18); }
|
||||
.winner-popup-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 26rpx 28rpx; border-bottom: 2rpx solid #f3f4f6; }
|
||||
.winner-popup-title { flex: 1; font-size: 28rpx; font-weight: 900; color: #1f2937; }
|
||||
.winner-popup-close { font-size: 42rpx; color: #9ca3af; line-height: 1; }
|
||||
.winner-popup-list { max-height: 54vh; padding: 24rpx 28rpx; }
|
||||
.winner-popup-item { display: flex; gap: 16rpx; align-items: center; padding: 18rpx 0; }
|
||||
.participant-popup-item { align-items: center; }
|
||||
.participant-popup-avatar { border-radius: 50%; }
|
||||
.self-tag { color: #f97316; font-weight: 800; }
|
||||
.winner-popup-image { width: 96rpx; height: 96rpx; border-radius: 20rpx; background: #f3f4f6; flex-shrink: 0; }
|
||||
.winner-popup-image.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.winner-popup-info { flex: 1; min-width: 0; }
|
||||
.winner-popup-name { display: block; font-size: 26rpx; font-weight: 800; color: #1f2937; }
|
||||
.winner-popup-price, .winner-popup-time { display: block; margin-top: 8rpx; font-size: 22rpx; color: #6b7280; }
|
||||
</style>
|
||||
108
pages-activity/activity/welfare/index.vue
Normal file
108
pages-activity/activity/welfare/index.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<view class="welfare-list-page">
|
||||
<view class="page-head">
|
||||
<text class="page-title">{{ mode === 'finished' ? '往期活动' : '福利活动' }}</text>
|
||||
<text class="page-subtitle">{{ mode === 'finished' ? '查看已结束活动' : '当前仅展示进行中的活动' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="toolbar">
|
||||
<view class="toolbar-btn" :class="{ active: mode === 'active' }" @tap="switchMode('active')">进行中</view>
|
||||
<view class="toolbar-btn" :class="{ active: mode === 'finished' }" @tap="switchMode('finished')">往期活动</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="state">加载中...</view>
|
||||
<view v-else-if="activities.length === 0" class="state">{{ mode === 'finished' ? '暂无往期活动' : '暂无进行中的活动' }}</view>
|
||||
|
||||
<view v-else class="activity-grid">
|
||||
<view v-for="item in activities" :key="item.id" class="activity-card" @tap="goDetail(item.id)">
|
||||
<image v-if="item.cover_image" class="activity-cover" :src="item.cover_image" mode="aspectFill" />
|
||||
<view v-else class="activity-cover empty">暂无图片</view>
|
||||
<view class="activity-meta">
|
||||
<text class="activity-name">{{ item.title }}</text>
|
||||
<view class="activity-type-row">
|
||||
<text class="activity-type" :class="typeClass(item.type)">{{ typeLabel(item.type) }}</text>
|
||||
<text class="activity-status" :class="mode === 'finished' ? 'status-finished' : 'status-active'">
|
||||
{{ mode === 'finished' ? '已结束' : '进行中' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { request } from '@/utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
mode: 'active',
|
||||
activities: []
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.mode = options?.mode === 'finished' ? 'finished' : 'active'
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const status = this.mode === 'finished' ? 'finished' : 'active'
|
||||
const res = await request({ url: `/api/app/welfare-activities?status=${status}&page=1&page_size=50` })
|
||||
this.activities = Array.isArray(res?.list) ? res.list : []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
switchMode(mode) {
|
||||
if (this.mode === mode) return
|
||||
this.mode = mode
|
||||
this.loadData()
|
||||
},
|
||||
goDetail(id) {
|
||||
uni.navigateTo({ url: `/pages-activity/activity/welfare/detail?id=${id}` })
|
||||
},
|
||||
typeLabel(type) {
|
||||
return { daily: '每日福利', weekly: '每周福利', monthly: '每月福利' }[type] || '福利活动'
|
||||
},
|
||||
typeClass(type) {
|
||||
return {
|
||||
daily: 'type-daily',
|
||||
weekly: 'type-weekly',
|
||||
monthly: 'type-monthly'
|
||||
}[type] || 'type-default'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.welfare-list-page { min-height: 100vh; padding: 28rpx; background: #fff7ed; }
|
||||
.page-head { margin-bottom: 28rpx; }
|
||||
.page-title { display: block; font-size: 46rpx; font-weight: 900; color: #1f2937; }
|
||||
.page-subtitle { display: block; margin-top: 10rpx; font-size: 24rpx; color: #9ca3af; }
|
||||
.toolbar { display: flex; gap: 16rpx; margin-bottom: 28rpx; }
|
||||
.toolbar-btn { flex: 1; text-align: center; padding: 20rpx 0; background: #fff; border-radius: 999rpx; color: #9a5b24; font-weight: 800; }
|
||||
.toolbar-btn.active { background: #ff8a3d; color: #fff; }
|
||||
.state { padding: 120rpx 0; text-align: center; color: #9ca3af; }
|
||||
.activity-grid { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.activity-card { display: flex; overflow: hidden; border-radius: 28rpx; background: #fff; box-shadow: 0 12rpx 30rpx rgba(0,0,0,.06); min-height: 220rpx; }
|
||||
.activity-cover { width: 240rpx; height: 220rpx; display: block; background: #f3f4f6; flex-shrink: 0; }
|
||||
.activity-cover.empty { display: flex; align-items: center; justify-content: center; color: #9ca3af; }
|
||||
.activity-meta { flex: 1; padding: 24rpx 22rpx; display: flex; flex-direction: column; justify-content: space-between; min-width: 0; }
|
||||
.activity-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 30rpx; font-weight: 800; color: #1f2937; line-height: 1.4; }
|
||||
.activity-type-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 12rpx; flex-wrap: wrap; }
|
||||
.activity-type { display: inline-flex; align-items: center; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 800; }
|
||||
.type-daily { background: rgba(249, 115, 22, .12); color: #f97316; }
|
||||
.type-weekly { background: rgba(239, 68, 68, .12); color: #ef4444; }
|
||||
.type-monthly { background: linear-gradient(135deg, #a855f7, #ec4899, #f59e0b); color: #fff; }
|
||||
.type-default { background: rgba(148, 163, 184, .12); color: #64748b; }
|
||||
.activity-status { font-size: 22rpx; font-weight: 700; }
|
||||
.status-active { color: #10b981; }
|
||||
.status-finished { color: #94a3b8; }
|
||||
</style>
|
||||
@ -71,6 +71,8 @@
|
||||
:coupons="coupons"
|
||||
:gamePasses="gamePasses"
|
||||
:propCards="propCards"
|
||||
:couponOptional="true"
|
||||
:defaultUseCoupon="false"
|
||||
@confirm="onPaymentConfirm"
|
||||
/>
|
||||
<RulesPopup
|
||||
|
||||
@ -43,27 +43,21 @@
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="footer">
|
||||
<view
|
||||
class="btn-primary"
|
||||
<view
|
||||
class="btn-primary"
|
||||
:class="{ disabled: ticketCount <= 0 || entering }"
|
||||
@tap="enterGame('minesweeper')"
|
||||
>
|
||||
<text class="enter-btn-text">{{ entering ? '正在进入...' : (ticketCount > 0 ? '立即开局' : '资格不足') }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="btn-free"
|
||||
:class="{ disabled: entering }"
|
||||
@tap="enterGame('minesweeper_free')"
|
||||
>
|
||||
<text class="free-btn-text">🍭 练习试玩 </text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="btn-secondary"
|
||||
@tap="goRoomList"
|
||||
>
|
||||
<text class="secondary-btn-text">📡 对战列表 / 围观</text>
|
||||
<view class="minesweeper-actions">
|
||||
<view class="btn-secondary" @tap="goRoomList">
|
||||
<text class="secondary-btn-text">📡 对战列表 / 围观</text>
|
||||
</view>
|
||||
<view class="btn-secondary leaderboard-btn" @tap="openLeaderboard">
|
||||
<text class="secondary-btn-text">🏆 扫雷排行榜</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -85,6 +79,9 @@ export default {
|
||||
this.loadTickets()
|
||||
},
|
||||
methods: {
|
||||
async openLeaderboard() {
|
||||
uni.navigateTo({ url: '/pages-game/game/minesweeper/leaderboard' })
|
||||
},
|
||||
|
||||
async loadTickets() {
|
||||
this.loading = true
|
||||
@ -108,7 +105,7 @@ export default {
|
||||
},
|
||||
async enterGame(code) {
|
||||
const targetCode = code || this.gameCode
|
||||
if (targetCode !== 'minesweeper_free' && this.ticketCount <= 0) return
|
||||
if (this.ticketCount <= 0) return
|
||||
if (this.entering) return
|
||||
|
||||
this.entering = true
|
||||
@ -121,9 +118,14 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
|
||||
const nakamaServer = 'wss://game.1024tool.vip'
|
||||
const gameBaseUrl = 'https://game.1024tool.vip'
|
||||
const userInfo = uni.getStorageSync('user_info') || {}
|
||||
const uid = userInfo.id || userInfo.user_id || ''
|
||||
const nickname = encodeURIComponent(userInfo.nickname || userInfo.name || '')
|
||||
const gameUrl = `${gameBaseUrl}/?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&game_type=${encodeURIComponent(targetCode)}&uid=${encodeURIComponent(uid)}&nickname=${nickname}`
|
||||
uni.navigateTo({
|
||||
url: `/pages-game/game/minesweeper/play?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
|
||||
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
@ -140,14 +142,15 @@ export default {
|
||||
const res = await authRequest({
|
||||
url: '/api/app/games/enter',
|
||||
method: 'POST',
|
||||
data: {
|
||||
game_code: this.gameCode
|
||||
}
|
||||
data: { game_code: this.gameCode }
|
||||
})
|
||||
|
||||
const nakamaServer = res.nakama_server || 'wss://kdy.1024tool.vip/ws'
|
||||
const nakamaServer = 'wss://kdy.1024tool.vip'
|
||||
const gameBaseUrl = 'http://192.168.31.185:8082'
|
||||
const userInfo = uni.getStorageSync('user_info') || {}
|
||||
const uid = userInfo.id || userInfo.user_id || ''
|
||||
const gameUrl = `${gameBaseUrl}/?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}&game_type=${encodeURIComponent(this.gameCode)}&uid=${encodeURIComponent(uid)}&scene=room-list`
|
||||
uni.navigateTo({
|
||||
url: `/pages-game/game/minesweeper/room-list?game_token=${encodeURIComponent(res.game_token)}&nakama_server=${encodeURIComponent(nakamaServer)}&nakama_key=${encodeURIComponent(res.nakama_key)}`
|
||||
url: `/pages-game/game/webview?url=${encodeURIComponent(gameUrl)}`
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '无法获取对战列表', icon: 'none' })
|
||||
@ -306,6 +309,12 @@ export default {
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.minesweeper-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 110rpx;
|
||||
width: 100%;
|
||||
@ -318,37 +327,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-free {
|
||||
margin-top: 24rpx;
|
||||
height: 110rpx;
|
||||
width: 100%;
|
||||
border-radius: 55rpx;
|
||||
background: rgba($brand-primary, 0.15);
|
||||
border: 1px solid rgba($brand-primary, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: rgba($brand-primary, 0.25);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.free-btn-text {
|
||||
color: $brand-primary;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
margin-top: 24rpx;
|
||||
btn-secondary {
|
||||
flex: 1;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 55rpx;
|
||||
@ -358,6 +338,11 @@ export default {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.leaderboard-btn {
|
||||
background: rgba($brand-primary, 0.10);
|
||||
border-color: rgba($brand-primary, 0.18);
|
||||
}
|
||||
|
||||
.secondary-btn-text {
|
||||
color: #94a3b8;
|
||||
font-size: 32rpx;
|
||||
|
||||
399
pages-game/game/minesweeper/leaderboard.vue
Normal file
399
pages-game/game/minesweeper/leaderboard.vue
Normal file
@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="bg-decoration"></view>
|
||||
|
||||
<!-- 对战分说明 -->
|
||||
<view class="score-tip glass-card">
|
||||
<text class="score-tip-icon">💡</text>
|
||||
<text class="score-tip-text">对战分 = 游戏内排名积分,赢局 +1000 基础分,得分/伤害/宝箱均有加成,与平台充值积分无关</text>
|
||||
</view>
|
||||
|
||||
<!-- 我的排名 -->
|
||||
<view class="my-card glass-card" v-if="myRank">
|
||||
<view class="my-left">
|
||||
<text class="my-label">我的排名</text>
|
||||
<text class="my-rank">{{ myRank.rank ? `#${myRank.rank}` : '未上榜' }}</text>
|
||||
</view>
|
||||
<view class="my-divider"></view>
|
||||
<view class="my-stats">
|
||||
<view class="stat-col">
|
||||
<text class="stat-val">{{ myRank.wins || 0 }}</text>
|
||||
<text class="stat-key">胜场</text>
|
||||
</view>
|
||||
<view class="stat-col">
|
||||
<text class="stat-val">{{ myRank.matches_played || 0 }}</text>
|
||||
<text class="stat-key">总场次</text>
|
||||
</view>
|
||||
<view class="stat-col">
|
||||
<text class="stat-val">{{ formatWinRate(myRank.win_rate) }}</text>
|
||||
<text class="stat-key">胜率</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="my-divider"></view>
|
||||
<view class="my-right">
|
||||
<text class="my-pts">{{ myRank.total_rank_points || 0 }}</text>
|
||||
<text class="my-pts-label">对战分</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 榜单 -->
|
||||
<scroll-view scroll-y class="list-wrap" @scrolltolower="loadMore">
|
||||
|
||||
<view v-if="loading && list.length === 0" class="state-box">
|
||||
<text class="state-icon">⏳</text>
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!loading && list.length === 0" class="state-box">
|
||||
<text class="state-icon">🏆</text>
|
||||
<text class="state-text">暂无数据,快来挑战吧!</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="list">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.user_id"
|
||||
class="rank-row glass-card"
|
||||
:class="{ 'is-me': item.user_id === myUserId }"
|
||||
>
|
||||
<view class="rank-badge">
|
||||
<text v-if="item.rank === 1" class="medal">🥇</text>
|
||||
<text v-else-if="item.rank === 2" class="medal">🥈</text>
|
||||
<text v-else-if="item.rank === 3" class="medal">🥉</text>
|
||||
<text v-else class="rank-no">{{ item.rank }}</text>
|
||||
</view>
|
||||
|
||||
<image class="avatar" :src="item.avatar || fallback" mode="aspectFill" />
|
||||
|
||||
<view class="item-info">
|
||||
<view class="name-row">
|
||||
<text class="item-name">{{ item.nickname || '匿名玩家' }}</text>
|
||||
<text v-if="item.user_id === myUserId" class="me-tag">我</text>
|
||||
</view>
|
||||
<text class="item-sub">{{ item.wins }}胜 · {{ item.matches_played }}场 · {{ formatWinRate(item.win_rate) }}胜率</text>
|
||||
</view>
|
||||
|
||||
<view class="pts-box">
|
||||
<text class="pts-val">{{ item.total_rank_points }}</text>
|
||||
<text class="pts-unit">分</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingMore" class="footer-tip"><text class="footer-txt">加载中...</text></view>
|
||||
<view v-if="!hasMore && list.length > 0" class="footer-tip"><text class="footer-txt">— 已显示全部 —</text></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { authRequest } from '../../../utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
gameType: 'minesweeper',
|
||||
list: [],
|
||||
myRank: null,
|
||||
myUserId: null,
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
fallback: 'https://via.placeholder.com/80/FF6B00/FFFFFF?text=U',
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
const info = uni.getStorageSync('user_info') || {}
|
||||
this.myUserId = info.id || info.user_id || null
|
||||
this.fetchList(true)
|
||||
},
|
||||
methods: {
|
||||
async fetchList(reset = false) {
|
||||
if (reset) {
|
||||
this.list = []
|
||||
this.page = 1
|
||||
this.hasMore = true
|
||||
this.myRank = null
|
||||
this.loading = true
|
||||
} else {
|
||||
if (!this.hasMore || this.loadingMore) return
|
||||
this.loadingMore = true
|
||||
}
|
||||
try {
|
||||
const res = await authRequest({
|
||||
url: '/api/app/games/leaderboard',
|
||||
method: 'GET',
|
||||
data: { game_type: this.gameType, page: this.page, page_size: this.pageSize },
|
||||
})
|
||||
const items = res.list || []
|
||||
this.list = reset ? items : [...this.list, ...items]
|
||||
this.total = res.total || 0
|
||||
this.myRank = res.me || null
|
||||
this.hasMore = this.list.length < this.total
|
||||
if (this.hasMore) this.page++
|
||||
} catch {
|
||||
uni.showToast({ title: '加载失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.loadingMore = false
|
||||
}
|
||||
},
|
||||
loadMore() { this.fetchList(false) },
|
||||
formatWinRate(rate) {
|
||||
if (rate == null) return '0%'
|
||||
return `${(rate * 100).toFixed(1)}%`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/uni.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 对战分说明 */
|
||||
.score-tip {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin: 0 32rpx 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.score-tip-icon {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.score-tip-text {
|
||||
font-size: 22rpx;
|
||||
color: $text-sub;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 我的排名 */
|
||||
.my-card {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.my-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.my-label {
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.my-rank {
|
||||
font-size: 38rpx;
|
||||
font-weight: 900;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.my-divider {
|
||||
width: 1px;
|
||||
height: 60rpx;
|
||||
background: $border-color-light;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.my-stats {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stat-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.stat-key {
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.my-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 80rpx;
|
||||
}
|
||||
|
||||
.my-pts {
|
||||
font-size: 38rpx;
|
||||
font-weight: 900;
|
||||
color: $brand-primary;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.my-pts-label {
|
||||
font-size: $font-xs;
|
||||
color: $text-sub;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.list-wrap {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.state-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.state-icon { font-size: 80rpx; }
|
||||
.state-text { font-size: $font-md; color: $text-sub; }
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.rank-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
gap: 20rpx;
|
||||
|
||||
&.is-me {
|
||||
border: 2rpx solid rgba($brand-primary, 0.4);
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.06) 0%, $bg-glass 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.medal { font-size: 44rpx; }
|
||||
|
||||
.rank-no {
|
||||
font-size: $font-md;
|
||||
font-weight: 800;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-secondary;
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid $border-color-light;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: $font-lg;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 220rpx;
|
||||
}
|
||||
|
||||
.me-tag {
|
||||
font-size: $font-xs;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: $gradient-brand;
|
||||
padding: 2rpx 14rpx;
|
||||
border-radius: $radius-round;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.pts-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pts-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.pts-unit {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.footer-tip {
|
||||
padding: 32rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-txt {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<web-view :src="url" @message="onMessage"></web-view>
|
||||
</view>
|
||||
<web-view v-if="url" :src="url" @message="onMessage"></web-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -21,33 +19,16 @@ onLoad((options) => {
|
||||
})
|
||||
|
||||
function onMessage(e) {
|
||||
console.log('Message from Game:', e.detail)
|
||||
const data = e.detail.data || []
|
||||
|
||||
// Handle specific messages
|
||||
data.forEach(msg => {
|
||||
if (msg.action === 'close') {
|
||||
uni.navigateBack()
|
||||
} else if (msg.action === 'playAgain') {
|
||||
// 再来一局: 返回上一页,上一页会自动刷新重新获取token进入游戏
|
||||
console.log('PlayAgain: 返回游戏入口页面')
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
success: () => {
|
||||
// 可选: 发送事件通知上一页刷新
|
||||
uni.$emit('refreshGame')
|
||||
}
|
||||
success: () => uni.$emit('refreshGame')
|
||||
})
|
||||
} else if (msg.action === 'game_over') {
|
||||
// Optional: Refresh user balance or state
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
<view class="ol">
|
||||
<view class="li">发货时效:发货订单提交成功后,本平台将在3-15个工作日内安排发货(预售商品按页面说明执行)。</view>
|
||||
<view class="li">物流信息:您可在“我的订单”中查看物流状态。因地址错误、联系不畅导致的配送失败,责任由您承担。</view>
|
||||
<view class="li">用户收到货物后需要保留完整的开箱视频,以便售后作为依据。无视频依据,将不与售后处理。</view>
|
||||
<view class="li">在对产品质量等问题发生时,我们仅提供退换服务,如有更多诉求,我们将协助用户与供货商进行沟通处理。</view>
|
||||
</view>
|
||||
<view class="h2">四、售后服务</view>
|
||||
<view class="ol">
|
||||
|
||||
@ -185,13 +185,18 @@ function onGetPhoneNumber(e) {
|
||||
} catch(e) {}
|
||||
|
||||
uni.showToast({ title: '欢迎加入!', icon: 'success' })
|
||||
const backTarget = '/pages-activity/activity/threshold/index'
|
||||
setTimeout(() => {
|
||||
// #ifdef MP-TOUTIAO
|
||||
// 抖音平台跳转到商城
|
||||
uni.switchTab({ url: '/pages/shop/index' })
|
||||
// #endif
|
||||
// #ifndef MP-TOUTIAO
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
// #endif
|
||||
}, 500)
|
||||
|
||||
|
||||
@ -19,6 +19,16 @@
|
||||
<text class="stat-num">{{ getRewardsTotal() }}</text>
|
||||
<text class="stat-label">累计奖励</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-num">{{ shareCount }}</text>
|
||||
<text class="stat-label">分享引导</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<button class="share-btn" open-type="share">继续邀请好友</button>
|
||||
<view class="copy-btn" @tap="copyInviteLink">复制邀请码</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区 -->
|
||||
@ -83,6 +93,7 @@ const isRefreshing = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const hasMore = ref(true)
|
||||
const shareCount = ref(0)
|
||||
|
||||
// 获取用户ID
|
||||
function getUserId() {
|
||||
@ -126,6 +137,16 @@ function getRewardsTotal() {
|
||||
return list.value.length * rewardPerInvite
|
||||
}
|
||||
|
||||
function copyInviteLink() {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
const text = inviteCode ? `快来和我一起参加裂变活动,邀请码:${inviteCode}` : '快来和我一起参加裂变活动'
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => uni.showToast({ title: '邀请码已复制', icon: 'success' }),
|
||||
fail: () => uni.showToast({ title: '复制失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
async function onRefresh() {
|
||||
isRefreshing.value = true
|
||||
@ -172,8 +193,19 @@ async function fetchData(append = false) {
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
shareCount.value = inviteCode ? 1 : 0
|
||||
fetchData()
|
||||
})
|
||||
|
||||
onShareAppMessage(() => {
|
||||
const inviteCode = uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || ''
|
||||
return {
|
||||
title: '邀请好友一起参加裂变活动,达标就能开奖!',
|
||||
path: `/pages-user/invite/landing?invite_code=${inviteCode}`,
|
||||
imageUrl: '/static/logo.png'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -248,6 +280,39 @@ onLoad(() => {
|
||||
}
|
||||
|
||||
/* 内容滚动区 */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin: 0 $spacing-lg $spacing-lg;
|
||||
}
|
||||
|
||||
.share-btn, .copy-btn {
|
||||
flex: 1;
|
||||
min-height: 84rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba($brand-primary, 0.08);
|
||||
color: $brand-primary;
|
||||
border: 2rpx solid rgba($brand-primary, 0.16);
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: calc(100vh - 400rpx);
|
||||
padding: 0 $spacing-lg $spacing-lg;
|
||||
|
||||
@ -93,16 +93,30 @@
|
||||
|
||||
<!-- 底部:进度 + 按钮 -->
|
||||
<view class="ticket-footer">
|
||||
<text class="ready-hint">
|
||||
{{ getReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<view
|
||||
class="synth-btn"
|
||||
:class="recipe.can_synthesize ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '合成' : '不足') }}</text>
|
||||
<view v-if="recipe.can_synthesize" class="btn-shine"></view>
|
||||
<view class="ready-meta">
|
||||
<text class="ready-hint">
|
||||
{{ getReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<text class="batch-hint" v-if="getMaxSynthesizeCount(recipe) > 0">
|
||||
最多可合成 {{ getMaxSynthesizeCount(recipe) }} 次
|
||||
</text>
|
||||
</view>
|
||||
<view class="action-group">
|
||||
<view
|
||||
class="synth-btn synth-btn-secondary"
|
||||
:class="recipe.can_synthesize && !batchSynthesizing ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '单次合成' : '不足') }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="synth-btn synth-btn-primary"
|
||||
:class="getMaxSynthesizeCount(recipe) > 0 && !synthesizing ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onBatchSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ batchSynthesizing ? '批量中' : (getMaxSynthesizeCount(recipe) > 0 ? '一键合成' : '不足') }}</text>
|
||||
<view v-if="getMaxSynthesizeCount(recipe) > 0 && !batchSynthesizing" class="btn-shine"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -115,10 +129,11 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getSynthesisRecipes, doSynthesis } from '../../api/synthesis.js'
|
||||
import { getSynthesisRecipes, doSynthesis, doBatchSynthesis } from '../../api/synthesis.js'
|
||||
|
||||
const loading = ref(true)
|
||||
const synthesizing = ref(false)
|
||||
const batchSynthesizing = ref(false)
|
||||
const isRefreshing = ref(false)
|
||||
const recipes = ref([])
|
||||
|
||||
@ -142,6 +157,21 @@ function getOverallProgress(recipe) {
|
||||
return Math.round((getReadyCount(recipe) / recipe.materials.length) * 100)
|
||||
}
|
||||
|
||||
function getMaxSynthesizeCount(recipe) {
|
||||
return Number(recipe?.max_synthesize_count || 0)
|
||||
}
|
||||
|
||||
function confirmSynthesis({ title, content }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
success: (res) => res.confirm ? resolve() : reject(new Error('cancel')),
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRecipes() {
|
||||
loading.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
@ -166,15 +196,11 @@ async function onRefresh() {
|
||||
}
|
||||
|
||||
async function onSynthesize(recipe) {
|
||||
if (synthesizing.value || !recipe.can_synthesize) return
|
||||
if (synthesizing.value || batchSynthesizing.value || !recipe.can_synthesize) return
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: '确认合成',
|
||||
content: `确定要合成「${recipe.target_product?.name || '目标商品'}」吗?合成后碎片将被消耗。`,
|
||||
success: (res) => res.confirm ? resolve() : reject('cancel'),
|
||||
fail: reject
|
||||
})
|
||||
await confirmSynthesis({
|
||||
title: '确认合成',
|
||||
content: `确定要合成「${recipe.target_product?.name || '目标商品'}」吗?合成后碎片将被消耗。`
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
@ -193,6 +219,33 @@ async function onSynthesize(recipe) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onBatchSynthesize(recipe) {
|
||||
const maxCount = getMaxSynthesizeCount(recipe)
|
||||
if (batchSynthesizing.value || synthesizing.value || maxCount <= 0) return
|
||||
|
||||
try {
|
||||
await confirmSynthesis({
|
||||
title: '确认一键合成',
|
||||
content: `将消耗当前全部可用碎片,预计合成 ${maxCount} 次「${recipe.target_product?.name || '目标商品'}」,是否继续?`
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
batchSynthesizing.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
try {
|
||||
const res = await doBatchSynthesis(userId, recipe.id)
|
||||
const count = Number(res?.synthesized_count || maxCount)
|
||||
uni.showToast({ title: `一键合成成功,共合成 ${count} 次`, icon: 'none' })
|
||||
await loadRecipes()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '一键合成失败', icon: 'none' })
|
||||
} finally {
|
||||
batchSynthesizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadRecipes()
|
||||
})
|
||||
@ -535,27 +588,47 @@ defineExpose({ onShow })
|
||||
/* 底部行:进度提示 + 按钮 */
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 14rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.ready-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.ready-hint {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.batch-hint {
|
||||
font-size: 20rpx;
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 合成按钮 - 小胶囊 */
|
||||
.synth-btn {
|
||||
height: 56rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 28rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 52rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||
|
||||
&.btn-ready {
|
||||
@ -563,7 +636,7 @@ defineExpose({ onShow })
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
transform: scale(0.96);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@ -573,15 +646,25 @@ defineExpose({ onShow })
|
||||
}
|
||||
}
|
||||
|
||||
.synth-btn-primary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.synth-btn-secondary {
|
||||
&.btn-ready {
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.14), rgba($brand-primary, 0.08));
|
||||
box-shadow: none;
|
||||
border: 1.5rpx solid rgba($brand-primary, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
letter-spacing: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.btn-ready & { color: #fff; }
|
||||
.btn-locked & { color: $text-tertiary; }
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-shine {
|
||||
|
||||
94
pages.json
94
pages.json
@ -36,38 +36,62 @@
|
||||
{
|
||||
"root": "pages-activity",
|
||||
"pages": [
|
||||
{
|
||||
"path": "activity/yifanshang/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "一番赏"
|
||||
{
|
||||
"path": "activity/yifanshang/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "一番赏"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/wuxianshang/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "无限赏"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/duiduipeng/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "对对碰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/list/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/pata/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "爬塔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/welfare/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "福利活动"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/welfare/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/threshold/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "拉新裂变活动"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/threshold/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动详情"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/wuxianshang/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "无限赏"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/duiduipeng/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "对对碰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/list/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activity/pata/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "爬塔"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages-user",
|
||||
"pages": [
|
||||
@ -223,6 +247,12 @@
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "game/minesweeper/leaderboard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "扫雷战绩榜"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "game/webview",
|
||||
"style": {
|
||||
@ -283,4 +313,4 @@
|
||||
"__usePrivacyCheck__": true
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,16 +213,30 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="ticket-footer">
|
||||
<text class="ready-hint">
|
||||
{{ getSynthReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<view
|
||||
class="synth-btn"
|
||||
:class="recipe.can_synthesize ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '合成' : '不足') }}</text>
|
||||
<view v-if="recipe.can_synthesize" class="btn-shine"></view>
|
||||
<view class="ready-meta">
|
||||
<text class="ready-hint">
|
||||
{{ getSynthReadyCount(recipe) }}/{{ recipe.materials?.length || 0 }} 材料就绪
|
||||
</text>
|
||||
<text class="batch-hint" v-if="getSynthMaxCount(recipe) > 0">
|
||||
最多可合成 {{ getSynthMaxCount(recipe) }} 次
|
||||
</text>
|
||||
</view>
|
||||
<view class="action-group">
|
||||
<view
|
||||
class="synth-btn synth-btn-secondary"
|
||||
:class="recipe.can_synthesize && !batchSynthesizing ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ synthesizing ? '合成中' : (recipe.can_synthesize ? '单次合成' : '不足') }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="synth-btn synth-btn-primary"
|
||||
:class="getSynthMaxCount(recipe) > 0 && !synthesizing ? 'btn-ready' : 'btn-locked'"
|
||||
@tap="onBatchSynthesize(recipe)"
|
||||
>
|
||||
<text class="btn-text">{{ batchSynthesizing ? '批量中' : (getSynthMaxCount(recipe) > 0 ? '一键合成' : '不足') }}</text>
|
||||
<view v-if="getSynthMaxCount(recipe) > 0 && !batchSynthesizing" class="btn-shine"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -294,8 +308,8 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare, createShippingFeeOrder } from '@/api/appUser'
|
||||
import { getSynthesisRecipes, doSynthesis } from '@/api/synthesis.js'
|
||||
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare, checkShippingFee, createShippingFeeOrder } from '@/api/appUser'
|
||||
import { getSynthesisRecipes, doSynthesis, doBatchSynthesis } from '@/api/synthesis.js'
|
||||
import { vibrateShort } from '@/utils/vibrate.js'
|
||||
import { checkPhoneBoundSync } from '@/utils/checkPhone.js'
|
||||
import { executePaymentFlow } from '@/utils/payment.js'
|
||||
@ -326,6 +340,7 @@ const pendingShipIds = ref([])
|
||||
const recipes = ref([])
|
||||
const synthLoading = ref(false)
|
||||
const synthesizing = ref(false)
|
||||
const batchSynthesizing = ref(false)
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return aggregatedList.value.reduce((sum, item) => sum + (item.count || 1), 0)
|
||||
@ -763,6 +778,21 @@ function getSynthReadyCount(recipe) {
|
||||
return recipe.materials.filter(m => m.owned_count >= m.required_count).length
|
||||
}
|
||||
|
||||
function getSynthMaxCount(recipe) {
|
||||
return Number(recipe?.max_synthesize_count || 0)
|
||||
}
|
||||
|
||||
function confirmSynthesisAction({ title, content }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
success: (res) => res.confirm ? resolve() : reject(new Error('cancel')),
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRecipes(uid) {
|
||||
synthLoading.value = true
|
||||
const userId = uid || uni.getStorageSync('user_id')
|
||||
@ -778,23 +808,21 @@ async function loadRecipes(uid) {
|
||||
}
|
||||
|
||||
async function onSynthesize(recipe) {
|
||||
if (synthesizing.value || !recipe.can_synthesize) return
|
||||
if (synthesizing.value || batchSynthesizing.value || !recipe.can_synthesize) return
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: '确认合成',
|
||||
content: `确定要合成「${recipe.target_product?.name || '目标商品'}」吗?合成后碎片将被消耗。`,
|
||||
success: (res) => res.confirm ? resolve() : reject('cancel'),
|
||||
fail: reject
|
||||
})
|
||||
await confirmSynthesisAction({
|
||||
title: '确认合成',
|
||||
content: `确定要合成「${recipe.target_product?.name || '目标商品'}」吗?合成后碎片将被消耗。`
|
||||
})
|
||||
} catch { return }
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
synthesizing.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
try {
|
||||
await doSynthesis(userId, recipe.id)
|
||||
uni.showToast({ title: '合成成功!', icon: 'success' })
|
||||
await loadRecipes(userId)
|
||||
await Promise.all([loadRecipes(userId), loadInventory(userId)])
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '合成失败', icon: 'none' })
|
||||
} finally {
|
||||
@ -802,6 +830,32 @@ async function onSynthesize(recipe) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onBatchSynthesize(recipe) {
|
||||
const maxCount = getSynthMaxCount(recipe)
|
||||
if (batchSynthesizing.value || synthesizing.value || maxCount <= 0) return
|
||||
try {
|
||||
await confirmSynthesisAction({
|
||||
title: '确认一键合成',
|
||||
content: `将消耗当前全部可用碎片,预计合成 ${maxCount} 次「${recipe.target_product?.name || '目标商品'}」,是否继续?`
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
batchSynthesizing.value = true
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
try {
|
||||
const res = await doBatchSynthesis(userId, recipe.id)
|
||||
const count = Number(res?.synthesized_count || maxCount)
|
||||
uni.showToast({ title: `一键合成成功,共合成 ${count} 次`, icon: 'none' })
|
||||
await Promise.all([loadRecipes(userId), loadInventory(userId)])
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '一键合成失败', icon: 'none' })
|
||||
} finally {
|
||||
batchSynthesizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRedeem() {
|
||||
vibrateShort()
|
||||
const user_id = uni.getStorageSync('user_id')
|
||||
@ -907,13 +961,26 @@ async function confirmShipWithAddress() {
|
||||
showAddressPicker.value = false
|
||||
|
||||
const FREIGHT_THRESHOLD = 5
|
||||
const FREIGHT_FEE = 10
|
||||
|
||||
if (allIds.length < FREIGHT_THRESHOLD) {
|
||||
let shippingCheck
|
||||
try {
|
||||
shippingCheck = await checkShippingFee(user_id, allIds)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '运费校验失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (shippingCheck?.need_fee) {
|
||||
const fee = Number(shippingCheck?.fee_cents || 0) / 100
|
||||
const reason = shippingCheck?.reason
|
||||
const content = reason === 'contains_non_free_shipping_item'
|
||||
? `所选商品包含不包邮商品,需支付 ¥${fee.toFixed(2)} 运费,确认继续?`
|
||||
: `共 ${allIds.length} 件商品,不满 ${FREIGHT_THRESHOLD} 件需支付 ¥${fee.toFixed(2)} 运费,确认继续?`
|
||||
|
||||
const confirmed = await new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: '需支付运费',
|
||||
content: `共 ${allIds.length} 件商品,不满 ${FREIGHT_THRESHOLD} 件需支付 ¥${FREIGHT_FEE}.00 运费,确认继续?`,
|
||||
content,
|
||||
confirmText: '去支付',
|
||||
cancelText: '取消',
|
||||
success: (res) => resolve(res.confirm)
|
||||
@ -934,25 +1001,8 @@ async function confirmShipWithAddress() {
|
||||
return
|
||||
}
|
||||
uni.hideLoading()
|
||||
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
try {
|
||||
await requestShipping(user_id, allIds, addressId)
|
||||
uni.showToast({ title: '申请成功', icon: 'success' })
|
||||
pendingShipIds.value = []
|
||||
aggregatedList.value = []
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
loadInventory(user_id)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '申请失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 满5件包邮直接发货
|
||||
uni.showLoading({ title: '提交中...' })
|
||||
try {
|
||||
await requestShipping(user_id, allIds, addressId)
|
||||
@ -1977,33 +2027,54 @@ function onCopyShareLink() {
|
||||
|
||||
.ticket-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 14rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.ready-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.ready-hint {
|
||||
font-size: 20rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.batch-hint {
|
||||
font-size: 20rpx;
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.synth-btn {
|
||||
height: 56rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 28rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 52rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
|
||||
|
||||
&.btn-ready {
|
||||
background: $gradient-brand;
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
transform: scale(0.96);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@ -2013,12 +2084,25 @@ function onCopyShareLink() {
|
||||
}
|
||||
}
|
||||
|
||||
.synth-btn-primary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.synth-btn-secondary {
|
||||
&.btn-ready {
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.14), rgba($brand-primary, 0.08));
|
||||
box-shadow: none;
|
||||
border: 1.5rpx solid rgba($brand-primary, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
letter-spacing: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
white-space: nowrap;
|
||||
|
||||
.btn-ready & { color: #fff; }
|
||||
.btn-locked & { color: $text-tertiary; }
|
||||
|
||||
@ -61,12 +61,11 @@
|
||||
<view class="gameplay-grid-v2">
|
||||
<!-- 上排:两大核心 -->
|
||||
<view class="grid-row-top">
|
||||
<view class="game-card-large card-yifan" @tap="navigateTo('/pages-activity/activity/list/index?category=一番赏')">
|
||||
<view class="card-bg-decoration"></view>
|
||||
<view class="game-card-large card-match" @tap="navigateTo('/pages-activity/activity/list/index?category=对对碰')">
|
||||
<view class="card-content-large">
|
||||
<text class="card-title-large">一番赏</text>
|
||||
<view class="card-tag-large">欧皇擂台</view>
|
||||
<image class="card-mascot-large" src="https://via.placeholder.com/150/90EE90/000000?text=YI" mode="aspectFit" />
|
||||
<text class="card-title-large">对对碰</text>
|
||||
<view class="card-tag-large">碰一碰消除</view>
|
||||
<image class="card-mascot-large" src="https://via.placeholder.com/150/FFB6C1/000000?text=Match" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="game-card-large card-wuxian" @tap="navigateTo('/pages-activity/activity/list/index?category=无限赏')">
|
||||
@ -78,12 +77,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下排:三小功能 -->
|
||||
<!-- 下排:四个功能 -->
|
||||
<view class="grid-row-bottom">
|
||||
<view class="game-card-small card-match" @tap="navigateTo('/pages-activity/activity/list/index?category=对对碰')">
|
||||
<text class="card-title-small">对对碰</text>
|
||||
<text class="card-subtitle-small">碰一碰消除</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/FFB6C1/000000?text=Match" mode="aspectFit" />
|
||||
<view class="game-card-small card-yifan-small" @tap="navigateTo('/pages-activity/activity/list/index?category=一番赏')">
|
||||
<text class="card-title-small">一番赏</text>
|
||||
<text class="card-subtitle-small">欧皇擂台</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/90EE90/000000?text=YI" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="game-card-small card-tower" @tap="navigateTo('/pages-game/game/minesweeper/index')">
|
||||
@ -92,10 +91,16 @@
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/9370DB/000000?text=Mine" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="game-card-small card-more" @tap="navigateTo('#')">
|
||||
<text class="card-title-small">更多</text>
|
||||
<text class="card-subtitle-small">敬请期待</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/E0E0E0/000000?text=More" mode="aspectFit" />
|
||||
<view class="game-card-small card-welfare" @tap="navigateTo('/pages-activity/activity/welfare/index')">
|
||||
<text class="card-title-small">福利活动</text>
|
||||
<text class="card-subtitle-small">日周月福利</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/98FB98/000000?text=Gift" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<view class="game-card-small card-threshold" @tap="navigateTo('/pages-activity/activity/threshold/index')">
|
||||
<text class="card-title-small">裂变活动</text>
|
||||
<text class="card-subtitle-small">拉新达标参与</text>
|
||||
<image class="card-icon-small" src="https://via.placeholder.com/80/87CEFA/000000?text=Invite" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -135,19 +140,30 @@
|
||||
<!-- 底部垫高 - 避开TabBar -->
|
||||
<view style="height: 140rpx"></view>
|
||||
</scroll-view>
|
||||
|
||||
<PrizeClaimPopup
|
||||
v-model:visible="prizeClaimVisible"
|
||||
:activity="prizeClaimActivity"
|
||||
:loading="prizeClaimLoading"
|
||||
@close="handlePrizeClaimClose"
|
||||
@claim="handlePrizeClaim"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { authRequest, request } from '../../utils/request.js'
|
||||
import { getPendingPrizeGrantActivity, claimPrizeGrantActivity } from '@/api/prizeClaim'
|
||||
import SplashScreen from '@/components/SplashScreen.vue'
|
||||
import PrizeClaimPopup from '@/components/activity/PrizeClaimPopup.vue'
|
||||
// #ifdef MP-TOUTIAO
|
||||
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SplashScreen
|
||||
SplashScreen,
|
||||
PrizeClaimPopup
|
||||
// #ifdef MP-TOUTIAO
|
||||
, customTabBarToutiao
|
||||
// #endif
|
||||
@ -161,7 +177,12 @@ export default {
|
||||
bannerIndex: 0,
|
||||
isHomeLoading: false,
|
||||
swiperAutoplay: true,
|
||||
swiperKey: 0
|
||||
swiperKey: 0,
|
||||
prizeClaimVisible: false,
|
||||
prizeClaimLoading: false,
|
||||
prizeClaimActivity: null,
|
||||
prizeClaimChecking: false,
|
||||
prizeClaimLastCheckAt: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -219,6 +240,7 @@ export default {
|
||||
if (this.activities.length === 0 && !this.isHomeLoading) {
|
||||
this.loadHomeData()
|
||||
}
|
||||
this.checkPrizeGrantActivity()
|
||||
},
|
||||
onHide() {
|
||||
this.swiperAutoplay = false
|
||||
@ -257,6 +279,55 @@ export default {
|
||||
const fn = token ? authRequest : request
|
||||
return fn({ url })
|
||||
},
|
||||
getPrizeClaimSessionKey() {
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
const sessionId = uni.getStorageSync('app_session_id')
|
||||
if (!userId || !sessionId) return ''
|
||||
return `prize_claim_closed:${userId}:${sessionId}`
|
||||
},
|
||||
async checkPrizeGrantActivity(force = false) {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token || this.prizeClaimChecking || this.prizeClaimVisible) return
|
||||
const sessionKey = this.getPrizeClaimSessionKey()
|
||||
if (!force && sessionKey && uni.getStorageSync(sessionKey)) return
|
||||
const now = Date.now()
|
||||
if (!force && now - this.prizeClaimLastCheckAt < 10000) return
|
||||
this.prizeClaimChecking = true
|
||||
this.prizeClaimLastCheckAt = now
|
||||
try {
|
||||
const res = await getPendingPrizeGrantActivity()
|
||||
if (res && res.has_pending && res.activity) {
|
||||
this.prizeClaimActivity = res.activity
|
||||
this.prizeClaimVisible = true
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('checkPrizeGrantActivity failed', err)
|
||||
} finally {
|
||||
this.prizeClaimChecking = false
|
||||
}
|
||||
},
|
||||
handlePrizeClaimClose() {
|
||||
const sessionKey = this.getPrizeClaimSessionKey()
|
||||
if (sessionKey) {
|
||||
try { uni.setStorageSync(sessionKey, 1) } catch (_) {}
|
||||
}
|
||||
this.prizeClaimVisible = false
|
||||
},
|
||||
async handlePrizeClaim() {
|
||||
if (!this.prizeClaimActivity?.id || this.prizeClaimLoading) return
|
||||
this.prizeClaimLoading = true
|
||||
try {
|
||||
await claimPrizeGrantActivity(this.prizeClaimActivity.id)
|
||||
uni.showToast({ title: '领取成功', icon: 'success' })
|
||||
this.prizeClaimVisible = false
|
||||
this.prizeClaimActivity = null
|
||||
this.checkPrizeGrantActivity(true)
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err?.message || '领取失败', icon: 'none' })
|
||||
} finally {
|
||||
this.prizeClaimLoading = false
|
||||
}
|
||||
},
|
||||
normalizeNotices(list) {
|
||||
const arr = this.unwrap(list)
|
||||
return arr.map((i, idx) => ({
|
||||
@ -380,6 +451,9 @@ export default {
|
||||
if(url === '#') return
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
async openLeaderboard() {
|
||||
uni.navigateTo({ url: '/pages-game/game/minesweeper/index?from=home&focus=leaderboard' })
|
||||
},
|
||||
onNoticeTap() {
|
||||
const content = this.displayNotices.map(n => n.text).join('\n')
|
||||
uni.showModal({
|
||||
@ -413,12 +487,12 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
/* ============================================
|
||||
柯大鸭 - 首页样式 (V6.0 Pro Refined)
|
||||
柯大鸭 - 首页样式 (V7.0 Clean Modern Refresh)
|
||||
============================================ */
|
||||
|
||||
.page {
|
||||
padding: 0;
|
||||
background-color: $bg-page;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f3f6fb 55%, #eef3f8 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -432,57 +506,48 @@ export default {
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba($bg-page, 0.8);
|
||||
backdrop-filter: blur(10rpx);
|
||||
background: rgba(248, 250, 252, 0.78);
|
||||
backdrop-filter: blur(14rpx);
|
||||
}
|
||||
|
||||
|
||||
/* ========== 滚动主内容区 ========== */
|
||||
|
||||
|
||||
/* ========== 滚动主内容区 ========== */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Banner Container - Claymorphism Style */
|
||||
/* Banner - 更轻、更干净 */
|
||||
.banner-container {
|
||||
padding: $spacing-sm $spacing-lg $spacing-xl;
|
||||
padding: 12rpx $spacing-lg 28rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
height: 360rpx;
|
||||
height: 320rpx;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.banner-card {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 40rpx;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transform: scale(0.96);
|
||||
transition: all 0.5s $ease-out;
|
||||
|
||||
/* Claymorphism 双阴影效果 */
|
||||
transform: scale(0.985);
|
||||
transition: all 0.35s $ease-out;
|
||||
box-shadow:
|
||||
12rpx 12rpx 24rpx rgba(0, 0, 0, 0.08),
|
||||
-12rpx -12rpx 24rpx rgba(255, 255, 255, 0.7),
|
||||
inset 4rpx 4rpx 8rpx rgba(255, 255, 255, 0.5),
|
||||
inset -4rpx -4rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
0 14rpx 36rpx rgba(15, 23, 42, 0.08),
|
||||
0 2rpx 10rpx rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.banner-card.active {
|
||||
transform: scale(1);
|
||||
box-shadow:
|
||||
16rpx 16rpx 32rpx rgba(255, 107, 0, 0.12),
|
||||
-16rpx -16rpx 32rpx rgba(255, 255, 255, 0.6),
|
||||
inset 6rpx 6rpx 12rpx rgba(255, 255, 255, 0.4),
|
||||
inset -6rpx -6rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
0 18rpx 44rpx rgba(15, 23, 42, 0.10),
|
||||
0 4rpx 14rpx rgba(255, 107, 0, 0.08);
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
@ -494,302 +559,280 @@ export default {
|
||||
.banner-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
|
||||
background: linear-gradient(135deg, #ffb36b 0%, #ff8f6b 45%, #ff735d 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: 36rpx 32rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fallback-glow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, transparent 80%);
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at top right, rgba(255,255,255,0.24) 0%, transparent 55%);
|
||||
}
|
||||
|
||||
.banner-fallback-text {
|
||||
font-size: 52rpx;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
font-style: italic;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 2rpx;
|
||||
.banner-fallback-text {
|
||||
font-size: 42rpx;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
margin-bottom: 10rpx;
|
||||
letter-spacing: 1rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.banner-tag {
|
||||
background: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: $radius-round;
|
||||
font-size: 22rpx;
|
||||
.banner-tag {
|
||||
background: rgba(255,255,255,0.16);
|
||||
color: rgba(255,255,255,0.92);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: $radius-round;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Indicator */
|
||||
.banner-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-top: -16rpx;
|
||||
gap: 10rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.indicator-dot {
|
||||
width: 12rpx;
|
||||
height: 6rpx;
|
||||
background: rgba(0,0,0,0.1);
|
||||
border-radius: 4rpx;
|
||||
transition: all 0.3s ease;
|
||||
width: 14rpx;
|
||||
height: 8rpx;
|
||||
background: rgba(148, 163, 184, 0.35);
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.indicator-dot.active {
|
||||
width: 32rpx;
|
||||
width: 34rpx;
|
||||
background: $brand-primary;
|
||||
}
|
||||
|
||||
/* Notice Bar - Claymorphism Style */
|
||||
/* Notice Bar */
|
||||
.notice-bar-v2 {
|
||||
margin: 0 $spacing-lg $spacing-xl;
|
||||
background: linear-gradient(145deg, #ffffff, #f5f5f5);
|
||||
border-radius: 40rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin: 0 $spacing-lg 28rpx;
|
||||
background: rgba(255,255,255,0.78);
|
||||
border-radius: 28rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
|
||||
/* Claymorphism 双阴影 */
|
||||
box-shadow:
|
||||
8rpx 8rpx 16rpx rgba(0, 0, 0, 0.06),
|
||||
-8rpx -8rpx 16rpx rgba(255, 255, 255, 0.8),
|
||||
inset 2rpx 2rpx 4rpx rgba(255, 255, 255, 0.9),
|
||||
inset -2rpx -2rpx 4rpx rgba(0, 0, 0, 0.03);
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
gap: 16rpx;
|
||||
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
|
||||
border: 1px solid rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
.notice-icon { font-size: 32rpx; }
|
||||
.notice-swiper { flex: 1; height: 36rpx; }
|
||||
.notice-item {
|
||||
font-size: 26rpx;
|
||||
color: $text-main;
|
||||
line-height: 36rpx;
|
||||
.notice-icon { font-size: 28rpx; }
|
||||
.notice-swiper { flex: 1; height: 34rpx; }
|
||||
.notice-item {
|
||||
font-size: 24rpx;
|
||||
color: #334155;
|
||||
line-height: 34rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.notice-arrow {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-top: 3rpx solid #DDD;
|
||||
border-right: 3rpx solid #DDD;
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-top: 2rpx solid #cbd5e1;
|
||||
border-right: 2rpx solid #cbd5e1;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
|
||||
/* 玩法专区 - 极质设计 */
|
||||
/* 玩法专区 */
|
||||
.gameplay-section {
|
||||
padding: 0 $spacing-lg;
|
||||
margin-bottom: $spacing-xl;
|
||||
margin-bottom: 36rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 38rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
font-style: italic;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background: $brand-primary;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 4rpx;
|
||||
transform: skewX(-15deg);
|
||||
height: 28rpx;
|
||||
background: linear-gradient(180deg, $brand-primary, $brand-secondary);
|
||||
margin-right: 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.gameplay-grid-v2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.grid-row-top {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
height: 190rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18rpx;
|
||||
min-height: 180rpx;
|
||||
}
|
||||
|
||||
/* 玩法卡片 - Claymorphism Style */
|
||||
.game-card-large {
|
||||
flex: 1;
|
||||
border-radius: $radius-xl;
|
||||
border-radius: 28rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 22rpx;
|
||||
transition: transform 0.2s;
|
||||
|
||||
/* Claymorphism 阴影 */
|
||||
box-shadow:
|
||||
12rpx 12rpx 24rpx rgba(0, 0, 0, 0.1),
|
||||
-12rpx -12rpx 24rpx rgba(255, 255, 255, 0.6),
|
||||
inset 4rpx 4rpx 8rpx rgba(255, 255, 255, 0.3),
|
||||
inset -4rpx -4rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 24rpx;
|
||||
transition: transform 0.2s ease;
|
||||
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.08);
|
||||
border: 1px solid rgba(255,255,255,0.82);
|
||||
}
|
||||
|
||||
.game-card-large:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow:
|
||||
6rpx 6rpx 12rpx rgba(0, 0, 0, 0.12),
|
||||
-6rpx -6rpx 12rpx rgba(255, 255, 255, 0.4),
|
||||
inset 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.15),
|
||||
inset -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.2);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 下排 */
|
||||
.grid-row-bottom {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
height: 130rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14rpx;
|
||||
min-height: 122rpx;
|
||||
}
|
||||
|
||||
.game-card-small {
|
||||
flex: 1;
|
||||
border-radius: $radius-lg;
|
||||
min-width: 0;
|
||||
border-radius: 24rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 16rpx;
|
||||
padding: 18rpx 14rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
/* Claymorphism 阴影 */
|
||||
box-shadow:
|
||||
8rpx 8rpx 16rpx rgba(0, 0, 0, 0.08),
|
||||
-8rpx -8rpx 16rpx rgba(255, 255, 255, 0.7),
|
||||
inset 2rpx 2rpx 4rpx rgba(255, 255, 255, 0.5),
|
||||
inset -2rpx -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
background: linear-gradient(145deg, #ffffff, #f8f8f8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
justify-content: flex-start;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.06);
|
||||
border: 1px solid rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
.game-card-small:active {
|
||||
transform: scale(0.94);
|
||||
box-shadow:
|
||||
4rpx 4rpx 8rpx rgba(0, 0, 0, 0.1),
|
||||
-4rpx -4rpx 8rpx rgba(255, 255, 255, 0.5),
|
||||
inset 4rpx 4rpx 8rpx rgba(0, 0, 0, 0.08),
|
||||
inset -4rpx -4rpx 8rpx rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(2rpx) scale(0.98);
|
||||
}
|
||||
|
||||
/* 内容样式 - 大卡片 */
|
||||
.card-content-large {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-title-large {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: #FFF;
|
||||
font-style: italic;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
|
||||
color: #fff;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.card-tag-large {
|
||||
font-size: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: $text-main;
|
||||
padding: 4rpx 14rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.card-tag-large.yellow { color: #D97706; }
|
||||
.card-tag-large.yellow { color: #b45309; }
|
||||
|
||||
.card-mascot-large {
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
bottom: -20rpx;
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
transform: rotate(10deg);
|
||||
filter: drop-shadow(0 8rpx 16rpx rgba(0,0,0,0.2));
|
||||
right: -4rpx;
|
||||
bottom: -10rpx;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
transform: rotate(8deg);
|
||||
filter: drop-shadow(0 8rpx 14rpx rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
/* 内容样式 - 小卡片 */
|
||||
.card-title-small {
|
||||
font-size: 30rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: $text-main;
|
||||
margin-bottom: 6rpx;
|
||||
z-index: 2;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.card-subtitle-small {
|
||||
font-size: 22rpx;
|
||||
color: $text-sub;
|
||||
font-size: 20rpx;
|
||||
color: rgba(15, 23, 42, 0.68);
|
||||
z-index: 2;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-icon-small {
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
bottom: -10rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
right: -6rpx;
|
||||
bottom: -6rpx;
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
opacity: 0.9;
|
||||
transform: rotate(-10deg);
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
/* 背景配色 - 优化后的渐变 */
|
||||
.card-yifan {
|
||||
background: linear-gradient(135deg, $brand-primary 0%, $brand-secondary 100%); /* 品牌橙渐变 */
|
||||
background: linear-gradient(135deg, #ff8a5c 0%, #ff6b4a 100%);
|
||||
}
|
||||
|
||||
.card-wuxian {
|
||||
background: $gradient-gold; /* 质感金渐变 */
|
||||
background: linear-gradient(135deg, #ffcf6d 0%, #ff9f43 100%);
|
||||
}
|
||||
|
||||
.card-yifan-small {
|
||||
background: linear-gradient(135deg, #ff9a6e 0%, #ff7a50 100%);
|
||||
}
|
||||
.card-yifan-small .card-title-small { color: #fff; }
|
||||
.card-yifan-small .card-subtitle-small { color: rgba(255,255,255,0.84); }
|
||||
|
||||
.card-match {
|
||||
background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); /* 柔和粉 */
|
||||
background: linear-gradient(135deg, #ff9fba 0%, #ffb8d4 100%);
|
||||
}
|
||||
.card-match .card-title-small { color: $accent-pink; }
|
||||
.card-match .card-title-large { color: #fff; }
|
||||
.card-match .card-tag-large { color: #db2777; }
|
||||
|
||||
.card-tower {
|
||||
background: linear-gradient(135deg, #FFE0CC 0%, #FFCBA4 100%); /* 品牌橙暖色 */
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
|
||||
}
|
||||
.card-tower .card-title-small { color: $brand-primary; }
|
||||
.card-tower .card-title-small { color: #5b21b6; }
|
||||
.card-tower .card-subtitle-small { color: #6d28d9; }
|
||||
|
||||
.card-more {
|
||||
background: linear-gradient(135deg, $bg-secondary 0%, #E5E7EB 100%); /* 金属灰 */
|
||||
.card-welfare {
|
||||
background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%);
|
||||
}
|
||||
.card-more .card-title-small { color: $text-sub; }
|
||||
.card-welfare .card-title-small { color: #047857; }
|
||||
.card-welfare .card-subtitle-small { color: #059669; }
|
||||
|
||||
.card-threshold {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #c7d2fe 100%);
|
||||
}
|
||||
.card-threshold .card-title-small { color: #1d4ed8; }
|
||||
.card-threshold .card-subtitle-small { color: #4338ca; }
|
||||
.card-threshold .card-icon-small {
|
||||
opacity: 0.96;
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
|
||||
/* 推荐活动列表 - Claymorphism Style */
|
||||
.activity-section {
|
||||
padding: 0 $spacing-lg;
|
||||
padding: 0 $spacing-lg 8rpx;
|
||||
animation: fadeInUp 0.6s ease-out 0.3s backwards;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@ -798,44 +841,34 @@ export default {
|
||||
.activity-grid-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
background: linear-gradient(145deg, #ffffff, #f8f8f8);
|
||||
border-radius: $radius-xl;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
/* Claymorphism 阴影 */
|
||||
box-shadow:
|
||||
10rpx 10rpx 20rpx rgba(0, 0, 0, 0.06),
|
||||
-10rpx -10rpx 20rpx rgba(255, 255, 255, 0.7),
|
||||
inset 3rpx 3rpx 6rpx rgba(255, 255, 255, 0.8),
|
||||
inset -3rpx -3rpx 6rpx rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 12rpx 30rpx rgba(15, 23, 42, 0.06);
|
||||
border: 1px solid rgba(255,255,255,0.88);
|
||||
}
|
||||
|
||||
.activity-item:active {
|
||||
transform: translateY(4rpx) scale(0.98);
|
||||
box-shadow:
|
||||
5rpx 5rpx 10rpx rgba(0, 0, 0, 0.08),
|
||||
-5rpx -5rpx 10rpx rgba(255, 255, 255, 0.5),
|
||||
inset 5rpx 5rpx 10rpx rgba(0, 0, 0, 0.05),
|
||||
inset -5rpx -5rpx 10rpx rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(2rpx) scale(0.98);
|
||||
}
|
||||
|
||||
.activity-thumb-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
padding-bottom: 96%;
|
||||
}
|
||||
|
||||
.activity-thumb {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@ -844,28 +877,27 @@ export default {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
left: 16rpx;
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255,255,255,0.86);
|
||||
color: $brand-primary;
|
||||
font-size: 18rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255,107,0,0.3);
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 24rpx;
|
||||
padding: 22rpx;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 20rpx;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -879,56 +911,54 @@ export default {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 26rpx;
|
||||
color: $accent-red;
|
||||
font-weight: 800;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity-btn-go {
|
||||
background: #1A1A1A;
|
||||
color: $accent-gold;
|
||||
font-size: 22rpx;
|
||||
font-weight: 900;
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: $radius-round;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.1);
|
||||
background: rgba(255, 107, 0, 0.10);
|
||||
color: $brand-primary;
|
||||
font-size: 20rpx;
|
||||
font-weight: 800;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
🌌 动画与高级动效
|
||||
============================================ */
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.9; }
|
||||
50% { transform: scale(1.04); opacity: 0.92; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(40rpx); }
|
||||
from { opacity: 0; transform: translateY(32rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20rpx); }
|
||||
50% { transform: translateY(-16rpx); }
|
||||
}
|
||||
|
||||
.banner-container { animation: fadeInUp 0.6s $ease-out; }
|
||||
.notice-bar-v2 { animation: fadeInUp 0.6s $ease-out 0.15s both; }
|
||||
.gameplay-section { animation: fadeInUp 0.6s $ease-out 0.3s both; }
|
||||
.activity-section { animation: fadeInUp 0.6s $ease-out 0.45s both; }
|
||||
|
||||
.brand-star { animation: pulse 2s infinite; }
|
||||
.notice-bar-v2 { animation: fadeInUp 0.6s $ease-out 0.12s both; }
|
||||
.gameplay-section { animation: fadeInUp 0.6s $ease-out 0.24s both; }
|
||||
.activity-section { animation: fadeInUp 0.6s $ease-out 0.36s both; }
|
||||
|
||||
.activity-btn-go:active {
|
||||
transform: scale(0.9);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* 兼容性修复 */
|
||||
.brand-text {
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@ -178,6 +178,12 @@
|
||||
</view>
|
||||
<text class="menu-label">{{ douyinUserId ? '已绑定' : '绑定抖音' }}</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="handleSyncDouyinOrders">
|
||||
<view class="menu-icon-box">
|
||||
<image class="menu-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSAxMmE5IDkgMCAxIDEtMi42NC02LjM2Ii8+PHBvbHlsaW5lIHBvaW50cz0iMjEgMyAyMSA5IDE1IDkiLz48cGF0aCBkPSJNMyAxMmE5IDkgMCAxIDAgMi42NCA2LjM2Ii8+PHBvbHlsaW5lIHBvaW50cz0iMyAyMSAzIDE1IDkgMTUiLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="menu-label">{{ douyinSyncing ? '同步中...' : '同步订单' }}</text>
|
||||
</view>
|
||||
<!-- #ifdef MP-TOUTIAO -->
|
||||
|
||||
<button
|
||||
@ -494,7 +500,7 @@
|
||||
<script>
|
||||
import {
|
||||
getUserInfo, getUserStats, getPointsBalance, getUserPoints, getUserCoupons, getItemCards,
|
||||
getUserTasks, getTaskProgress, getInviteRecords, modifyUser, getUserProfile, bindDouyinID, getPublicConfig
|
||||
getUserTasks, getTaskProgress, getInviteRecords, modifyUser, getUserProfile, bindDouyinID, syncMyDouyinOrders, getPublicConfig
|
||||
} from '../../api/appUser.js'
|
||||
// #ifdef MP-TOUTIAO
|
||||
import customTabBarToutiao from '@/components/app-tab-bar-toutiao.vue'
|
||||
@ -519,6 +525,7 @@ export default {
|
||||
|
||||
mobile: '', // 手机号
|
||||
douyinUserId: '', // 抖音用户ID
|
||||
douyinSyncing: false,
|
||||
customerServiceQrCode: '', // 客服二维码
|
||||
customerServiceId: '0071112x', // 抖音IM客服账号
|
||||
pointsBalance: 0,
|
||||
@ -1029,6 +1036,40 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
async handleSyncDouyinOrders() {
|
||||
if (!this.checkPhoneBound()) return
|
||||
if (this.douyinSyncing) return
|
||||
|
||||
if (!this.douyinUserId) {
|
||||
uni.showToast({
|
||||
title: '请先绑定抖音号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.douyinSyncing = true
|
||||
try {
|
||||
uni.showLoading({ title: '同步中...' })
|
||||
const data = await syncMyDouyinOrders()
|
||||
await this.loadUserInfo()
|
||||
uni.hideLoading()
|
||||
|
||||
uni.showModal({
|
||||
title: '同步完成',
|
||||
content: `抖音号:${data.douyin_user_id || this.douyinUserId}\n抓取订单:${data.total_fetched || 0} 笔\n新增订单:${data.new_orders || 0} 笔`,
|
||||
showCancel: false
|
||||
})
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: err.message || '同步失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.douyinSyncing = false
|
||||
}
|
||||
},
|
||||
toMinesweeper() {
|
||||
if (!this.checkPhoneBound()) return
|
||||
uni.navigateTo({ url: '/pages-game/game/minesweeper/index' })
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user