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 { getActivityIssueRewards } from '@/api/appUser'
|
||||||
import { normalizeRewards, groupRewardsByLevel } from '@/utils/activity'
|
import { normalizeRewards, groupRewardsByLevel } from '@/utils/activity'
|
||||||
import { cleanUrl } from '@/utils/format'
|
import { cleanUrl } from '@/utils/format'
|
||||||
import { getRewardCacheItem, setRewardCache, isFresh } from '@/utils/cache'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 奖励数据管理
|
* 奖励数据管理
|
||||||
@ -27,27 +26,15 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多期的奖励数据(带缓存)
|
* 获取多期的奖励数据 (无缓存)
|
||||||
* @param {Array} issueList - 期列表
|
* @param {Array} issueList - 期列表
|
||||||
*/
|
*/
|
||||||
async function fetchRewardsForIssues(issueList) {
|
async function fetchRewardsForIssues(issueList) {
|
||||||
const activityId = activityIdRef?.value || activityIdRef
|
const activityId = activityIdRef?.value || activityIdRef
|
||||||
if (!activityId) return
|
if (!activityId) return
|
||||||
|
|
||||||
const list = issueList || []
|
const toFetch = issueList || []
|
||||||
const toFetch = []
|
if (toFetch.length === 0) return
|
||||||
|
|
||||||
// 先从缓存加载
|
|
||||||
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
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -59,7 +46,6 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
|
|||||||
if (!issueId) return
|
if (!issueId) return
|
||||||
const value = res.status === 'fulfilled' ? normalizeRewards(res.value, cleanUrl) : []
|
const value = res.status === 'fulfilled' ? normalizeRewards(res.value, cleanUrl) : []
|
||||||
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
|
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
|
||||||
setRewardCache(activityId, issueId, value)
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('fetchRewardsForIssues error', e)
|
console.error('fetchRewardsForIssues error', e)
|
||||||
@ -76,19 +62,12 @@ export function useRewards(activityIdRef, currentIssueIdRef) {
|
|||||||
const activityId = activityIdRef?.value || activityIdRef
|
const activityId = activityIdRef?.value || activityIdRef
|
||||||
if (!activityId || !issueId) return
|
if (!activityId || !issueId) return
|
||||||
|
|
||||||
// 先检查缓存
|
|
||||||
const cached = getRewardCacheItem(activityId, issueId)
|
|
||||||
if (cached) {
|
|
||||||
rewardsMap.value = { ...rewardsMap.value, [issueId]: cached }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getActivityIssueRewards(activityId, issueId)
|
const res = await getActivityIssueRewards(activityId, issueId)
|
||||||
const value = normalizeRewards(res, cleanUrl)
|
const value = normalizeRewards(res, cleanUrl)
|
||||||
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
|
rewardsMap.value = { ...rewardsMap.value, [issueId]: value }
|
||||||
setRewardCache(activityId, issueId, value)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('fetchRewardsForIssue error', e)
|
console.error('fetchRewardsForIssue error', e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -135,7 +135,8 @@ export default {
|
|||||||
banners: [],
|
banners: [],
|
||||||
activities: [],
|
activities: [],
|
||||||
selectedGroupName: '',
|
selectedGroupName: '',
|
||||||
bannerIndex: 0
|
bannerIndex: 0,
|
||||||
|
isHomeLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -169,24 +170,15 @@ export default {
|
|||||||
return Array.isArray(this.activities) ? this.activities : []
|
return Array.isArray(this.activities) ? this.activities : []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShow() {
|
onLoad() {
|
||||||
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 (_) {}
|
|
||||||
this.loadHomeData()
|
this.loadHomeData()
|
||||||
},
|
},
|
||||||
|
onShow() {
|
||||||
|
// 只有非首次进入或数据为空时才触发刷新,避免 onLoad/onShow 双重触发
|
||||||
|
if (this.activities.length === 0 && !this.isHomeLoading) {
|
||||||
|
this.loadHomeData()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onBannerChange(e) {
|
onBannerChange(e) {
|
||||||
this.bannerIndex = e.detail.current
|
this.bannerIndex = e.detail.current
|
||||||
@ -256,28 +248,23 @@ export default {
|
|||||||
return parts.join(' · ')
|
return parts.join(' · ')
|
||||||
},
|
},
|
||||||
async loadHomeData() {
|
async loadHomeData() {
|
||||||
// Notices
|
if (this.isHomeLoading) return
|
||||||
|
this.isHomeLoading = true
|
||||||
|
// 同时发起请求,大幅提升首屏加载速度
|
||||||
try {
|
try {
|
||||||
const nData = await this.apiGet('/api/app/notices')
|
const [nData, bData, acData] = await Promise.all([
|
||||||
this.notices = this.normalizeNotices(nData)
|
this.apiGet('/api/app/notices').catch(() => null),
|
||||||
} catch (e) {
|
this.apiGet('/api/app/banners').catch(() => null),
|
||||||
this.notices = []
|
this.apiGet('/api/app/activities').catch(() => null)
|
||||||
}
|
])
|
||||||
|
|
||||||
// Banners
|
if (nData) this.notices = this.normalizeNotices(nData)
|
||||||
try {
|
if (bData) this.banners = this.normalizeBanners(bData)
|
||||||
const bData = await this.apiGet('/api/app/banners')
|
if (acData) this.activities = this.normalizeActivities(acData)
|
||||||
this.banners = this.normalizeBanners(bData)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.banners = []
|
console.error('Home data load failed', e)
|
||||||
}
|
} finally {
|
||||||
|
this.isHomeLoading = false
|
||||||
// Activities
|
|
||||||
try {
|
|
||||||
const acData = await this.apiGet('/api/app/activities')
|
|
||||||
this.activities = this.normalizeActivities(acData)
|
|
||||||
} catch (e) {
|
|
||||||
this.activities = []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBannerTap(b) {
|
onBannerTap(b) {
|
||||||
|
|||||||
@ -1,105 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="page-login">
|
||||||
<!-- 装饰球体 -->
|
<!-- 全局背景装饰 -->
|
||||||
<view class="orb orb-1"></view>
|
<view class="bg-decoration"></view>
|
||||||
<view class="orb orb-2"></view>
|
|
||||||
|
|
||||||
<view class="content-wrap">
|
<view class="content-wrap">
|
||||||
<view class="glass-card">
|
<view class="login-card glass-card anim-fade-in-up">
|
||||||
<!-- 品牌Logo -->
|
<!-- 品牌标识项 -->
|
||||||
<view class="brand-section">
|
<view class="brand-section">
|
||||||
<view class="logo-box">
|
<view class="logo-wrapper anim-float">
|
||||||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
<image class="logo-img" src="/static/logo.png" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
<view class="welcome-text">开启欧气之旅 ✨</view>
|
<text class="brand-title">获取手机号</text>
|
||||||
|
<text class="brand-desc">
|
||||||
|
为保障您的权益并提供精准的发货服务,我们需要获取您的手机号码作为登录标识。
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 登录方式区域 -->
|
||||||
<!-- #ifdef MP-TOUTIAO -->
|
<view class="auth-section">
|
||||||
<view class="login-form">
|
<!-- 微信一键登录 (微信小程序环境) -->
|
||||||
<view class="input-group">
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<view class="input-icon">
|
<view class="auth-btn-row">
|
||||||
<image src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNBMEExQTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjAgMjF2LTJhNCA0IDAgMCAwLTQtNEg4YTQgNCAwIDAgMC00IDR2MiIgLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiIC8+PC9zdmc+" mode="aspectFit"></image>
|
<button class="btn-refuse" @tap="handleRefuse">拒绝</button>
|
||||||
</view>
|
<button
|
||||||
<input
|
class="btn-allow anim-scale"
|
||||||
type="text"
|
open-type="getPhoneNumber"
|
||||||
v-model="account"
|
:disabled="loading"
|
||||||
class="input-field"
|
@getphonenumber="onGetPhoneNumber"
|
||||||
placeholder="请输入账号"
|
>
|
||||||
placeholder-class="input-placeholder"
|
{{ loading ? '处理中...' : '允许' }}
|
||||||
/>
|
<view class="btn-shine"></view>
|
||||||
|
</button>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
<!-- #endif -->
|
|
||||||
|
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- 协议勾选 -->
|
||||||
<view class="weixin-login-box">
|
<view class="agreement-wrap">
|
||||||
<button class="btn weixin-btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">
|
<view class="checkbox-container" @tap="toggleAgreement">
|
||||||
<image class="wx-icon" src="/static/logo.png" mode="aspectFit"></image> <!-- 应该用微信图标,暂时用logo代替或SVG -->
|
<view class="custom-checkbox" :class="{ 'is-checked': agreementChecked }">
|
||||||
<text>微信一键登录</text>
|
<text class="check-icon" v-if="agreementChecked">✓</text>
|
||||||
</button>
|
</view>
|
||||||
</view>
|
<view class="agreement-content">
|
||||||
<!-- #endif -->
|
我已阅读并同意 <text class="link-text" @tap.stop="toUserAgreement">《用户协议》</text> 和 <text class="link-text" @tap.stop="toPurchaseAgreement">《隐私政策》</text>
|
||||||
|
</view>
|
||||||
<!-- 协议区 -->
|
|
||||||
<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>
|
</view>
|
||||||
</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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
|
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const needBindPhone = ref(false)
|
|
||||||
const account = ref("")
|
const account = ref("")
|
||||||
const pwd = ref("")
|
const pwd = ref("")
|
||||||
const remember = ref(false)
|
const agreementChecked = ref(false)
|
||||||
const agreementChecked = ref(false) // 默认为false,需要用户勾选
|
const process = { env: { NODE_ENV: 'development' } } // Polyfill for template if needed
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
try {
|
try {
|
||||||
@ -107,28 +87,40 @@ onMounted(() => {
|
|||||||
if (saved && saved.account && saved.pwd) {
|
if (saved && saved.account && saved.pwd) {
|
||||||
account.value = saved.account
|
account.value = saved.account
|
||||||
pwd.value = saved.pwd
|
pwd.value = saved.pwd
|
||||||
remember.value = true
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
console.error('读取本地登录信息失败', e)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleRemember() {
|
|
||||||
remember.value = !remember.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAgreement() {
|
function toggleAgreement() {
|
||||||
agreementChecked.value = !agreementChecked.value
|
agreementChecked.value = !agreementChecked.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRefuse() {
|
||||||
|
uni.showToast({ title: '需要授权手机号才能体验完整功能', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
if (!agreementChecked.value) {
|
if (!agreementChecked.value) {
|
||||||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||||
return
|
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' }) }
|
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
|
||||||
@ -136,17 +128,17 @@ function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchas
|
|||||||
|
|
||||||
function onGetPhoneNumber(e) {
|
function onGetPhoneNumber(e) {
|
||||||
if (!agreementChecked.value) {
|
if (!agreementChecked.value) {
|
||||||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
|
||||||
|
uni.vibrateShort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const phoneCode = e.detail.code
|
const phoneCode = e.detail.code
|
||||||
if (!phoneCode) {
|
if (!phoneCode) {
|
||||||
uni.showToast({ title: '未授权手机号', icon: 'none' })
|
uni.showToast({ title: '需要授权手机号以完成注册', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
|
||||||
|
|
||||||
uni.login({
|
uni.login({
|
||||||
provider: 'weixin',
|
provider: 'weixin',
|
||||||
@ -159,51 +151,56 @@ function onGetPhoneNumber(e) {
|
|||||||
const user_id = data && data.user_id
|
const user_id = data && data.user_id
|
||||||
const user_info = data || {}
|
const user_info = data || {}
|
||||||
|
|
||||||
|
// 保存用户信息及 Token
|
||||||
uni.setStorageSync('user_info', user_info)
|
uni.setStorageSync('user_info', user_info)
|
||||||
if (token) uni.setStorageSync('token', token)
|
if (token) uni.setStorageSync('token', token)
|
||||||
if (user_id) uni.setStorageSync('user_id', user_id)
|
if (user_id) uni.setStorageSync('user_id', user_id)
|
||||||
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
|
if (user_info.avatar) uni.setStorageSync('avatar', user_info.avatar)
|
||||||
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
|
if (user_info.nickname) uni.setStorageSync('nickname', user_info.nickname)
|
||||||
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
|
if (user_info.invite_code) uni.setStorageSync('invite_code', user_info.invite_code)
|
||||||
|
|
||||||
// 保存 openid 用于支付等场景
|
// 保存 openid 用于支付等场景
|
||||||
const openid = data && (data.openid || data.open_id)
|
const openid = data && (data.openid || data.open_id)
|
||||||
if (openid) uni.setStorageSync('openid', openid)
|
if (openid) uni.setStorageSync('openid', openid)
|
||||||
|
|
||||||
// 绑定手机号逻辑...
|
// 若还没绑定手机号,则进行绑定;已绑定则跳过,大幅提升登录速度
|
||||||
try {
|
const isBound = user_info.phone || user_info.phone_number || user_info.mobile
|
||||||
await new Promise(r => setTimeout(r, 600))
|
if (!isBound) {
|
||||||
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
|
try {
|
||||||
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
|
await bindPhone(user_id, phoneCode)
|
||||||
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
|
uni.setStorageSync('phone_bound', true)
|
||||||
} catch (bindErr) {
|
} catch (e) {
|
||||||
// 忽略绑定失败,允许静默失败或重试逻辑
|
console.warn('Bind phone failed', e)
|
||||||
console.warn('Bind phone failed', bindErr)
|
}
|
||||||
|
} else {
|
||||||
|
uni.setStorageSync('phone_bound', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.setStorageSync('phone_bound', true)
|
// 性能优化:后台静默获取非关键数据,不阻塞页面跳转
|
||||||
|
Promise.all([
|
||||||
// 获取用户信息
|
getUserStats(user_id).then(stats => {
|
||||||
try {
|
if (stats) uni.setStorageSync('user_stats', stats)
|
||||||
const stats = await getUserStats(user_id)
|
}).catch(()=>{}),
|
||||||
uni.setStorageSync('user_stats', stats)
|
getPointsBalance(user_id).then(balance => {
|
||||||
const balance = await getPointsBalance(user_id)
|
const b = balance && balance.balance !== undefined ? balance.balance : balance
|
||||||
const b = balance && balance.balance !== undefined ? balance.balance : balance
|
uni.setStorageSync('points_balance', b)
|
||||||
uni.setStorageSync('points_balance', b)
|
}).catch(()=>{})
|
||||||
} catch(e) {}
|
])
|
||||||
|
|
||||||
uni.showToast({ title: '欢迎回来', icon: 'success' })
|
uni.showToast({ title: '欧气加持,登录成功!', icon: 'none', duration: 1000 })
|
||||||
|
|
||||||
|
// 缩短延迟,极致响应
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.reLaunch({ url: '/pages/mine/index' }) // Redirect to Mine page
|
uni.reLaunch({ url: '/pages/mine/index' })
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || '登录失败'
|
uni.showToast({ title: err.message || '登录异常', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: () => {
|
||||||
error.value = '微信登录失败'
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -211,251 +208,258 @@ function onGetPhoneNumber(e) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
/* Page Container */
|
.page-login {
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
position: relative;
|
|
||||||
background: $bg-secondary; /* Use secondary for better depth with orbs */
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
padding: 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
/* 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); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrap {
|
.content-wrap {
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 0 40rpx;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
position: relative;
|
||||||
animation: fadeInUp 0.6s ease-out;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.login-card {
|
||||||
background: $bg-glass;
|
padding: 80rpx 40rpx;
|
||||||
backdrop-filter: blur(30rpx);
|
text-align: center;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Brand Section */
|
/* 品牌区 */
|
||||||
.brand-section {
|
.brand-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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;
|
margin-bottom: 60rpx;
|
||||||
}
|
}
|
||||||
.logo-box {
|
|
||||||
width: 160rpx;
|
.auth-btn-row {
|
||||||
height: 160rpx;
|
|
||||||
background: $bg-card;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
box-shadow: 0 12rpx 30rpx rgba($brand-primary, 0.2);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 30rpx;
|
||||||
justify-content: center;
|
margin-bottom: 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Styles */
|
.btn-refuse {
|
||||||
.input-group {
|
flex: 1;
|
||||||
background: $bg-card;
|
|
||||||
border-radius: $radius-round;
|
|
||||||
height: 100rpx;
|
height: 100rpx;
|
||||||
display: flex;
|
border-radius: 50rpx;
|
||||||
align-items: center;
|
background: #f1f3f5;
|
||||||
padding: 0 32rpx;
|
color: $text-sub;
|
||||||
margin-bottom: $spacing-xl;
|
font-size: 32rpx;
|
||||||
border: 2rpx solid transparent;
|
font-weight: 600;
|
||||||
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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
border: none;
|
||||||
background: rgba(255,255,255,0.5);
|
&::after { border: none; }
|
||||||
|
|
||||||
&.checked {
|
|
||||||
background: $brand-primary;
|
|
||||||
border-color: $brand-primary;
|
|
||||||
box-shadow: 0 4rpx 10rpx rgba($brand-primary, 0.3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.check-mark { color: $text-inverse; font-size: $font-sm; font-weight: bold; }
|
|
||||||
.remember-text { font-size: 26rpx; color: $text-sub; }
|
|
||||||
|
|
||||||
/* Buttons */
|
.btn-allow {
|
||||||
.btn {
|
flex: 1.2;
|
||||||
height: 96rpx;
|
height: 100rpx;
|
||||||
border-radius: $radius-round;
|
border-radius: 50rpx;
|
||||||
|
background: $gradient-brand;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: $font-lg;
|
box-shadow: $shadow-warm;
|
||||||
font-weight: 800;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
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;
|
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;
|
.anim-fade-in-up {
|
||||||
color: $text-inverse;
|
animation: fadeInUp 0.8s $ease-out both;
|
||||||
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
|
|
||||||
margin-bottom: $spacing-xl;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-login-btn {
|
.anim-float {
|
||||||
background: #555;
|
animation: float 4s ease-in-out infinite;
|
||||||
color: #fff;
|
|
||||||
margin-bottom: $spacing-xl;
|
|
||||||
border: none;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anim-scale {
|
||||||
|
transition: transform 0.2s $ease-out;
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-shine {
|
.btn-shine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: -100%;
|
top: 0; left: -100%;
|
||||||
width: 50%; height: 100%;
|
width: 50%; height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
transform: skewX(-20deg);
|
transform: skewX(-25deg);
|
||||||
animation: shine 3s infinite;
|
animation: shine 4s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shine {
|
@keyframes shine {
|
||||||
0% { left: -100%; }
|
0% { left: -150%; }
|
||||||
20% { left: 200%; }
|
30% { left: 150%; }
|
||||||
100% { left: 200%; }
|
100% { left: 150%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.weixin-btn {
|
@keyframes float {
|
||||||
background: #07C160; /* WeChat Brand Color */
|
0%, 100% { transform: translateY(0); }
|
||||||
color: $text-inverse;
|
50% { transform: translateY(-15rpx); }
|
||||||
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
.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; }
|
.form-container {
|
||||||
.register-text { font-size: $font-md; color: $text-sub; }
|
|
||||||
.highlight { color: $brand-primary; font-weight: 700; margin-left: 8rpx; }
|
|
||||||
|
|
||||||
/* Agreements */
|
|
||||||
.agreements {
|
|
||||||
margin-top: 60rpx;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
gap: 30rpx;
|
||||||
padding: 0 20rpx;
|
|
||||||
}
|
}
|
||||||
.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 {
|
.u-input-wrap {
|
||||||
position: fixed;
|
background: #f8f8fa;
|
||||||
top: 100rpx;
|
border-radius: 50rpx;
|
||||||
left: 50%;
|
height: 90rpx;
|
||||||
transform: translateX(-50%);
|
padding: 0 40rpx;
|
||||||
background: rgba($uni-color-error, 0.9);
|
display: flex;
|
||||||
color: $text-inverse;
|
align-items: center;
|
||||||
padding: 16rpx 32rpx;
|
border: 1px solid transparent;
|
||||||
border-radius: 12rpx;
|
&:focus-within {
|
||||||
font-size: 26rpx;
|
border-color: rgba($brand-primary, 0.3);
|
||||||
z-index: 999;
|
background: #fff;
|
||||||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.2);
|
}
|
||||||
animation: slideDown 0.3s ease-out;
|
|
||||||
}
|
}
|
||||||
@keyframes slideDown {
|
|
||||||
from { transform: translate(-50%, -100%); opacity: 0; }
|
.u-input {
|
||||||
to { transform: translate(-50%, 0); opacity: 1; }
|
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>
|
</style>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
||||||
<view class="price-row">
|
<view class="price-row">
|
||||||
<view class="points-wrap">
|
<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>
|
<text class="points-unit">积分</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -74,7 +74,7 @@ async function onRedeem() {
|
|||||||
return
|
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({
|
uni.showModal({
|
||||||
title: '确认兑换',
|
title: '确认兑换',
|
||||||
content: `是否消耗 ${points} 积分兑换 ${p.title || p.name}?`,
|
content: `是否消耗 ${points} 积分兑换 ${p.title || p.name}?`,
|
||||||
|
|||||||
@ -143,7 +143,7 @@ function normalizeItems(list, kind) {
|
|||||||
image: cleanUrl(i.main_image || i.image || ''),
|
image: cleanUrl(i.main_image || i.image || ''),
|
||||||
title: i.name || i.title || '',
|
title: i.name || i.title || '',
|
||||||
price: i.price || i.discount_value || 0,
|
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
|
stock: i.in_stock ? 99 : 0, // Simplified stock check if returned as bool
|
||||||
discount_value: i.discount_value || 0,
|
discount_value: i.discount_value || 0,
|
||||||
min_spend: i.min_spend || 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