feat: 新增开奖加载弹窗组件并统一奖品等级显示逻辑。

This commit is contained in:
邹方成 2025-12-27 22:50:51 +08:00
parent 75638f895b
commit 2af47b7979
6 changed files with 340 additions and 370 deletions

View File

@ -5,7 +5,6 @@ import { ref, computed, watch } from 'vue'
import { getActivityIssueRewards } from '@/api/appUser'
import { normalizeRewards, groupRewardsByLevel } from '@/utils/activity'
import { cleanUrl } from '@/utils/format'
import { getRewardCacheItem, setRewardCache, isFresh } from '@/utils/cache'
/**
* 奖励数据管理
@ -27,27 +26,15 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
})
/**
* 获取多期的奖励数据带缓存
* 获取多期的奖励数据 (无缓存)
* @param {Array} issueList - 期列表
*/
async function fetchRewardsForIssues(issueList) {
const activityId = activityIdRef?.value || activityIdRef
if (!activityId) return
const list = issueList || []
const toFetch = []
// 先从缓存加载
list.forEach(issue => {
const cached = getRewardCacheItem(activityId, issue.id)
if (cached) {
rewardsMap.value = { ...rewardsMap.value, [issue.id]: cached }
} else {
toFetch.push(issue)
}
})
if (!toFetch.length) return
const toFetch = issueList || []
if (toFetch.length === 0) return
loading.value = true
try {
@ -59,7 +46,6 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
if (!issueId) return
const value = res.status === 'fulfilled' ? normalizeRewards(res.value, cleanUrl) : []
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
setRewardCache(activityId, issueId, value)
})
} catch (e) {
console.error('fetchRewardsForIssues error', e)
@ -76,19 +62,12 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
const activityId = activityIdRef?.value || activityIdRef
if (!activityId || !issueId) return
// 先检查缓存
const cached = getRewardCacheItem(activityId, issueId)
if (cached) {
rewardsMap.value = { ...rewardsMap.value, [issueId]: cached }
return
}
loading.value = true
try {
const res = await getActivityIssueRewards(activityId, issueId)
const value = normalizeRewards(res, cleanUrl)
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
setRewardCache(activityId, issueId, value)
} catch (e) {
console.error('fetchRewardsForIssue error', e)
} finally {

View File

@ -135,7 +135,8 @@ export default {
banners: [],
activities: [],
selectedGroupName: '',
bannerIndex: 0
bannerIndex: 0,
isHomeLoading: false
}
},
computed: {
@ -169,24 +170,15 @@ export default {
return Array.isArray(this.activities) ? this.activities : []
}
},
onShow() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
}
try { console.log('home onShow', { token: !!token, phoneBound }) } catch (_) {}
onLoad() {
this.loadHomeData()
},
onShow() {
// onLoad/onShow
if (this.activities.length === 0 && !this.isHomeLoading) {
this.loadHomeData()
}
},
methods: {
onBannerChange(e) {
this.bannerIndex = e.detail.current
@ -256,28 +248,23 @@ export default {
return parts.join(' · ')
},
async loadHomeData() {
// Notices
if (this.isHomeLoading) return
this.isHomeLoading = true
//
try {
const nData = await this.apiGet('/api/app/notices')
this.notices = this.normalizeNotices(nData)
} catch (e) {
this.notices = []
}
const [nData, bData, acData] = await Promise.all([
this.apiGet('/api/app/notices').catch(() => null),
this.apiGet('/api/app/banners').catch(() => null),
this.apiGet('/api/app/activities').catch(() => null)
])
// Banners
try {
const bData = await this.apiGet('/api/app/banners')
this.banners = this.normalizeBanners(bData)
if (nData) this.notices = this.normalizeNotices(nData)
if (bData) this.banners = this.normalizeBanners(bData)
if (acData) this.activities = this.normalizeActivities(acData)
} catch (e) {
this.banners = []
}
// Activities
try {
const acData = await this.apiGet('/api/app/activities')
this.activities = this.normalizeActivities(acData)
} catch (e) {
this.activities = []
console.error('Home data load failed', e)
} finally {
this.isHomeLoading = false
}
},
onBannerTap(b) {

View File

@ -1,105 +1,85 @@
<template>
<view class="container">
<!-- 装饰球体 -->
<view class="orb orb-1"></view>
<view class="orb orb-2"></view>
<view class="page-login">
<!-- 全局背景装饰 -->
<view class="bg-decoration"></view>
<view class="content-wrap">
<view class="glass-card">
<!-- 品牌Logo -->
<view class="login-card glass-card anim-fade-in-up">
<!-- 品牌标识项 -->
<view class="brand-section">
<view class="logo-box">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
<view class="logo-wrapper anim-float">
<image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
</view>
<view class="welcome-text">开启欧气之旅 </view>
<text class="brand-title">获取手机号</text>
<text class="brand-desc">
为保障您的权益并提供精准的发货服务我们需要获取您的手机号码作为登录标识
</text>
</view>
<!-- 登录表单 -->
<!-- #ifdef MP-TOUTIAO -->
<view class="login-form">
<view class="input-group">
<view class="input-icon">
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMjF2LTJhNCA0IDAgMCAwLTQtNEg4YTQgNCAwIDAgMC00IDR2MiIgLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<input
type="text"
v-model="account"
class="input-field"
placeholder="请输入账号"
placeholder-class="input-placeholder"
/>
<!-- 登录方式区域 -->
<view class="auth-section">
<!-- 微信一键登录 (微信小程序环境) -->
<!-- #ifdef MP-WEIXIN -->
<view class="auth-btn-row">
<button class="btn-refuse" @tap="handleRefuse">拒绝</button>
<button
class="btn-allow anim-scale"
open-type="getPhoneNumber"
:disabled="loading"
@getphonenumber="onGetPhoneNumber"
>
{{ loading ? '处理中...' : '允许' }}
<view class="btn-shine"></view>
</button>
</view>
<!-- #endif -->
<!-- 其他登录 (其他环境或手机号验证码) -->
<!-- #ifndef MP-WEIXIN -->
<view class="form-container">
<view class="u-input-wrap">
<input type="text" v-model="account" placeholder="请输入手机号/账号" class="u-input" />
</view>
<view class="u-input-wrap">
<input type="password" v-model="pwd" placeholder="请输入密码" class="u-input" />
</view>
<button class="btn-primary-large" @tap="handleLogin">立即登录</button>
</view>
<!-- #endif -->
<view class="input-group">
<view class="input-icon">
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSIzIiB5PS“xMSIgd2lkdGg9“MTgiIGhlaWdodD0iMTEiIHJ4PSIyIiByeT0iMiIgLz48cGF0aCBkPSJNNyAxMVY3YTUgNSAwIDAgMSAxMCAwdjQiIC8+PC9zdmc+" mode="aspectFit"></image>
</view>
<input
type="password"
v-model="pwd"
class="input-field"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
</view>
<view class="options-row">
<view class="remember-box" @click="toggleRemember">
<view class="checkbox" :class="{ checked: remember }">
<view class="check-mark" v-if="remember"></view>
</view>
<text class="remember-text">记住密码</text>
</view>
</view>
<button class="btn login-btn" @click="handleLogin">
<text class="btn-text">立即登录</text>
<view class="btn-shine"></view>
</button>
<!-- Test Login Button -->
<button class="btn test-login-btn" @click="handleTestLogin">
<text class="btn-text">测试账号登录 (Dev)</text>
</button>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="weixin-login-box">
<button class="btn weixin-btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">
<image class="wx-icon" src="/static/logo.png" mode="aspectFit"></image> <!-- 应该用微信图标暂时用logo代替或SVG -->
<text>微信一键登录</text>
</button>
</view>
<!-- #endif -->
<!-- 协议区 -->
<view class="agreements">
<view class="checkbox-area">
<view class="checkbox round" :class="{ checked: agreementChecked }" @click="toggleAgreement"></view>
</view>
<view class="agreement-text">
登录即代表同意 <text class="link" @tap="toUserAgreement">用户协议</text> & <text class="link" @tap="toPurchaseAgreement">隐私政策</text>
<!-- 协议勾选 -->
<view class="agreement-wrap">
<view class="checkbox-container" @tap="toggleAgreement">
<view class="custom-checkbox" :class="{ 'is-checked': agreementChecked }">
<text class="check-icon" v-if="agreementChecked"></text>
</view>
<view class="agreement-content">
我已阅读并同意 <text class="link-text" @tap.stop="toUserAgreement">用户协议</text> <text class="link-text" @tap.stop="toPurchaseAgreement">隐私政策</text>
</view>
</view>
</view>
</view>
<view v-if="error" class="error-toast">{{ error }}</view>
<!-- 页脚品牌标识 -->
<view class="page-footer">
<text class="footer-copy">Copyright © 2025 柯大鸭潮玩科技</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
const loading = ref(false)
const error = ref('')
const needBindPhone = ref(false)
const account = ref("")
const pwd = ref("")
const remember = ref(false)
const agreementChecked = ref(false) // false
const agreementChecked = ref(false)
const process = { env: { NODE_ENV: 'development' } } // Polyfill for template if needed
onMounted(() => {
try {
@ -107,28 +87,40 @@ onMounted(() => {
if (saved && saved.account && saved.pwd) {
account.value = saved.account
pwd.value = saved.pwd
remember.value = true
}
} catch (e) {
console.error('读取本地登录信息失败', e)
}
} catch (e) {}
})
function toggleRemember() {
remember.value = !remember.value
}
function toggleAgreement() {
agreementChecked.value = !agreementChecked.value
}
function handleRefuse() {
uni.showToast({ title: '需要授权手机号才能体验完整功能', icon: 'none' })
}
function handleLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
return
}
// TODO: Implement actual username/password login logic if API available
uni.showToast({ title: '普通登录逻辑待接入', icon: 'none' })
uni.showToast({ title: '密码登录正在维护中', icon: 'none' })
}
function handleTestLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
return
}
account.value = 'demo'
pwd.value = '123456'
uni.showLoading({ title: '测试登录中' })
setTimeout(() => {
uni.hideLoading()
uni.setStorageSync('token', 'mock_token_dev')
uni.setStorageSync('user_id', 999)
uni.reLaunch({ url: '/pages/mine/index' })
}, 1000)
}
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
@ -136,17 +128,17 @@ function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchas
function onGetPhoneNumber(e) {
if (!agreementChecked.value) {
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
uni.vibrateShort()
return
}
const phoneCode = e.detail.code
if (!phoneCode) {
uni.showToast({ title: '未授权手机号', icon: 'none' })
uni.showToast({ title: '需要授权手机号以完成注册', icon: 'none' })
return
}
loading.value = true
error.value = ''
uni.login({
provider: 'weixin',
@ -159,51 +151,56 @@ function onGetPhoneNumber(e) {
const user_id = data && data.user_id
const user_info = data || {}
// Token
uni.setStorageSync('user_info', user_info)
if (token) uni.setStorageSync('token', token)
if (user_id) uni.setStorageSync('user_id', user_id)
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
// openid
const openid = data && (data.openid || data.open_id)
if (openid) uni.setStorageSync('openid', openid)
// ...
try {
await new Promise(r => setTimeout(r, 600))
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
} catch (bindErr) {
//
console.warn('Bind phone failed', bindErr)
//
const isBound = user_info.phone || user_info.phone_number || user_info.mobile
if (!isBound) {
try {
await bindPhone(user_id, phoneCode)
uni.setStorageSync('phone_bound', true)
} catch (e) {
console.warn('Bind phone failed', e)
}
} else {
uni.setStorageSync('phone_bound', true)
}
uni.setStorageSync('phone_bound', true)
//
try {
const stats = await getUserStats(user_id)
uni.setStorageSync('user_stats', stats)
const balance = await getPointsBalance(user_id)
const b = balance && balance.balance !== undefined ? balance.balance : balance
uni.setStorageSync('points_balance', b)
} catch(e) {}
//
Promise.all([
getUserStats(user_id).then(stats => {
if (stats) uni.setStorageSync('user_stats', stats)
}).catch(()=>{}),
getPointsBalance(user_id).then(balance => {
const b = balance && balance.balance !== undefined ? balance.balance : balance
uni.setStorageSync('points_balance', b)
}).catch(()=>{})
])
uni.showToast({ title: '欢迎回来', icon: 'success' })
uni.showToast({ title: '欧气加持,登录成功!', icon: 'none', duration: 1000 })
//
setTimeout(() => {
uni.reLaunch({ url: '/pages/mine/index' }) // Redirect to Mine page
uni.reLaunch({ url: '/pages/mine/index' })
}, 500)
} catch (err) {
error.value = err.message || '登录失败'
uni.showToast({ title: err.message || '登录异常', icon: 'none' })
} finally {
loading.value = false
}
},
fail: () => {
error.value = '微信登录失败'
loading.value = false
}
})
@ -211,251 +208,258 @@ function onGetPhoneNumber(e) {
</script>
<style lang="scss" scoped>
/* Page Container */
.container {
.page-login {
min-height: 100vh;
position: relative;
background: $bg-secondary; /* Use secondary for better depth with orbs */
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Orbs Background */
.bg-decoration {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80rpx); /* Increased blur for smoother look */
opacity: 0.6;
}
.orb-1 {
width: 500rpx;
height: 500rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.4), transparent 70%);
top: -100rpx;
left: -100rpx;
animation: float 8s ease-in-out infinite;
}
.orb-2 {
width: 600rpx;
height: 600rpx;
background: radial-gradient(circle, rgba($accent-gold, 0.3), transparent 70%);
bottom: -150rpx;
right: -150rpx;
animation: float 10s ease-in-out infinite reverse;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30rpx, 40rpx); }
padding: 40rpx;
box-sizing: border-box;
}
.content-wrap {
position: relative;
z-index: 1;
padding: 0 40rpx;
width: 100%;
box-sizing: border-box;
animation: fadeInUp 0.6s ease-out;
position: relative;
z-index: 10;
}
.glass-card {
background: $bg-glass;
backdrop-filter: blur(30rpx);
border-radius: 40rpx;
padding: 60rpx 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.8);
.login-card {
padding: 80rpx 40rpx;
text-align: center;
}
/* Brand Section */
/* 品牌区 */
.brand-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 80rpx;
}
.logo-wrapper {
width: 180rpx;
height: 180rpx;
background: #fff;
border-radius: 46rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-warm;
margin-bottom: 40rpx;
padding: 24rpx;
box-sizing: border-box;
overflow: hidden;
border: 4rpx solid rgba($brand-primary, 0.05);
.logo-img {
width: 100%;
height: 100%;
}
}
.brand-info {
text-align: center;
}
.brand-title {
font-size: 36rpx;
font-weight: 800;
color: $text-main;
margin-bottom: 24rpx;
display: block;
}
.brand-desc {
font-size: 26rpx;
color: $text-sub;
line-height: 1.6;
text-align: center;
padding: 0 20rpx;
}
/* 登录操作区 */
.auth-section {
margin-top: 100rpx;
margin-bottom: 60rpx;
}
.logo-box {
width: 160rpx;
height: 160rpx;
background: $bg-card;
border-radius: 40rpx;
padding: 20rpx;
box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2);
.auth-btn-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $spacing-xl;
animation: pulse 3s infinite;
}
.logo { width: 100%; height: 100%; }
.app-name {
font-size: 44rpx;
font-weight: 900;
color: $text-main;
margin-bottom: 12rpx;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.05);
}
.welcome-text {
font-size: 26rpx;
color: $text-sub;
letter-spacing: 4rpx;
text-transform: uppercase;
opacity: 0.8;
gap: 30rpx;
margin-bottom: 0;
}
/* Form Styles */
.input-group {
background: $bg-card;
border-radius: $radius-round;
.btn-refuse {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
padding: 0 32rpx;
margin-bottom: $spacing-xl;
border: 2rpx solid transparent;
transition: all 0.3s;
box-shadow: $shadow-sm;
&.glass-input {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.5);
&:focus-within {
background: $bg-card;
border-color: $brand-primary;
box-shadow: 0 0 0 4rpx rgba($brand-primary, 0.15);
transform: translateY(-2rpx);
}
}
}
.input-icon { width: 40rpx; height: 40rpx; margin-right: 20rpx; opacity: 1; }
.input-icon image { width: 100%; height: 100%; }
.input-field { flex: 1; height: 100%; font-size: 30rpx; color: $text-main; font-weight: 500; }
.input-placeholder { color: $text-tertiary; font-weight: 400; }
.options-row { display: flex; justify-content: space-between; margin-bottom: 60rpx; padding: 0 10rpx; }
.remember-box { display: flex; align-items: center; }
.checkbox {
width: 36rpx; height: 36rpx;
border: 3rpx solid $border-color;
border-radius: 10rpx;
margin-right: 12rpx;
border-radius: 50rpx;
background: #f1f3f5;
color: $text-sub;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background: rgba(255,255,255,0.5);
&.checked {
background: $brand-primary;
border-color: $brand-primary;
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
}
border: none;
&::after { border: none; }
}
.check-mark { color: $text-inverse; font-size: $font-sm; font-weight: bold; }
.remember-text { font-size: 26rpx; color: $text-sub; }
/* Buttons */
.btn {
height: 96rpx;
border-radius: $radius-round;
.btn-allow {
flex: 1.2;
height: 100rpx;
border-radius: 50rpx;
background: $gradient-brand;
color: #fff;
font-size: 32rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-lg;
font-weight: 800;
box-shadow: $shadow-warm;
position: relative;
overflow: hidden;
border: none;
&::after { border: none; }
}
.dev-login {
margin-top: 40rpx;
font-size: 24rpx;
color: $text-sub;
text-decoration: underline;
opacity: 0.5;
}
/* 协议区 */
.agreement-wrap {
margin-top: 40rpx;
}
.checkbox-container {
display: flex;
align-items: flex-start;
justify-content: center;
}
.custom-checkbox {
width: 32rpx;
height: 32rpx;
border: 2rpx solid $text-tertiary;
border-radius: 50%;
margin-right: 16rpx;
margin-top: 4rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
&.is-checked {
background: $brand-primary;
border-color: $brand-primary;
}
.check-icon {
color: #fff;
font-size: 20rpx;
font-weight: bold;
}
}
.agreement-content {
font-size: 24rpx;
color: $text-sub;
line-height: 1.5;
text-align: left;
max-width: 500rpx;
.link-text {
color: $brand-primary;
font-weight: 600;
}
}
/* 页脚 */
.page-footer {
margin-top: 100rpx;
text-align: center;
&:active { transform: scale(0.96); }
.footer-copy {
font-size: 20rpx;
color: rgba($text-tertiary, 0.6);
letter-spacing: 1rpx;
}
}
.login-btn {
background: $gradient-brand;
color: $text-inverse;
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
margin-bottom: $spacing-xl;
border: none;
/* 动画增强 */
.anim-fade-in-up {
animation: fadeInUp 0.8s $ease-out both;
}
.test-login-btn {
background: #555;
color: #fff;
margin-bottom: $spacing-xl;
border: none;
font-size: 28rpx;
.anim-float {
animation: float 4s ease-in-out infinite;
}
.anim-scale {
transition: transform 0.2s $ease-out;
&:active {
transform: scale(0.96);
}
}
.btn-shine {
position: absolute;
top: 0; left: -100%;
width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transform: skewX(-20deg);
animation: shine 3s infinite;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transform: skewX(-25deg);
animation: shine 4s infinite;
}
@keyframes shine {
0% { left: -100%; }
20% { left: 200%; }
100% { left: 200%; }
0% { left: -150%; }
30% { left: 150%; }
100% { left: 150%; }
}
.weixin-btn {
background: #07C160; /* WeChat Brand Color */
color: $text-inverse;
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
border: none;
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-15rpx); }
}
.wx-icon { width: 48rpx; height: 48rpx; margin-right: 16rpx; filter: brightness(100); } /* Make logo white if it's the logo, but ideally it should be WeChat icon */
/* Register Link */
.register-link { text-align: center; margin-top: $spacing-xl; }
.register-text { font-size: $font-md; color: $text-sub; }
.highlight { color: $brand-primary; font-weight: 700; margin-left: 8rpx; }
/* Agreements */
.agreements {
margin-top: 60rpx;
/* 其他表单 */
.form-container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0 20rpx;
flex-direction: column;
gap: 30rpx;
}
.checkbox.round { border-radius: 50%; width: 32rpx; height: 32rpx; margin-top: 4rpx; }
.checkbox-area { padding-right: 12rpx; }
.agreement-text {
font-size: $font-sm;
color: $text-tertiary;
line-height: 1.5;
text-align: left;
}
.link { color: $brand-primary; text-decoration: none; font-weight: 600; margin: 0 4rpx; }
.error-toast {
position: fixed;
top: 100rpx;
left: 50%;
transform: translateX(-50%);
background: rgba($uni-color-error, 0.9);
color: $text-inverse;
padding: 16rpx 32rpx;
border-radius: 12rpx;
font-size: 26rpx;
z-index: 999;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.2);
animation: slideDown 0.3s ease-out;
.u-input-wrap {
background: #f8f8fa;
border-radius: 50rpx;
height: 90rpx;
padding: 0 40rpx;
display: flex;
align-items: center;
border: 1px solid transparent;
&:focus-within {
border-color: rgba($brand-primary, 0.3);
background: #fff;
}
}
@keyframes slideDown {
from { transform: translate(-50%, -100%); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
.u-input {
flex: 1;
font-size: 28rpx;
}
.btn-primary-large {
background: $gradient-brand;
color: #fff;
height: 90rpx;
border-radius: 45rpx;
border: none;
font-weight: 700;
box-shadow: $shadow-warm;
}
</style>

View File

@ -8,7 +8,7 @@
<view class="title">{{ detail.title || detail.name || '-' }}</view>
<view class="price-row">
<view class="points-wrap">
<text class="points-val">{{ detail.points_required || (detail.price ? Math.floor(detail.price / 100) : 0) }}</text>
<text class="points-val">{{ (detail.points_required ? Math.floor(detail.points_required / 100) : 0) || (detail.price ? Math.floor(detail.price / 100) : 0) }}</text>
<text class="points-unit">积分</text>
</view>
</view>
@ -74,7 +74,7 @@ async function onRedeem() {
return
}
const points = detail.value.points_required || (detail.value.price ? Math.floor(detail.value.price / 100) : 0)
const points = (detail.value.points_required ? Math.floor(detail.value.points_required / 100) : 0) || (detail.value.price ? Math.floor(detail.value.price / 100) : 0)
uni.showModal({
title: '确认兑换',
content: `是否消耗 ${points} 积分兑换 ${p.title || p.name}`,

View File

@ -143,7 +143,7 @@ function normalizeItems(list, kind) {
image: cleanUrl(i.main_image || i.image || ''),
title: i.name || i.title || '',
price: i.price || i.discount_value || 0,
points: i.points_required || (i.price ? Math.floor(i.price / 100) : (i.discount_value ? Math.floor(i.discount_value / 100) : 0)),
points: i.points_required ? Math.floor(i.points_required / 100) : (i.price ? Math.floor(i.price / 100) : (i.discount_value ? Math.floor(i.discount_value / 100) : 0)),
stock: i.in_stock ? 99 : 0, // Simplified stock check if returned as bool
discount_value: i.discount_value || 0,
min_spend: i.min_spend || 0,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 311 KiB