feat: 新增开奖加载弹窗组件并统一奖品等级显示逻辑。
This commit is contained in:
parent
75638f895b
commit
2af47b7979
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}?`,
|
||||
|
||||
@ -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,
|
||||
|
||||
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 311 KiB |
Loading…
x
Reference in New Issue
Block a user