383 lines
12 KiB
Vue
383 lines
12 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 装饰球体 -->
|
||
<view class="orb orb-1"></view>
|
||
<view class="orb orb-2"></view>
|
||
|
||
<view class="content-wrap">
|
||
<!-- 品牌Logo -->
|
||
<view class="brand-section">
|
||
<view class="logo-box">
|
||
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
|
||
</view>
|
||
<view class="app-name">奇盒潮玩</view>
|
||
<view class="welcome-text">开启欧气之旅 ✨</view>
|
||
</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>
|
||
|
||
<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>
|
||
|
||
<view class="register-link">
|
||
<text class="register-text" @click="goToRegister">没有账号?<text class="highlight">立即注册</text></text>
|
||
</view>
|
||
</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>
|
||
</view>
|
||
|
||
<view v-if="error" class="error-toast">{{ error }}</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, 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,需要用户勾选
|
||
|
||
onMounted(() => {
|
||
try {
|
||
const saved = uni.getStorageSync('loginInfo')
|
||
if (saved && saved.account && saved.pwd) {
|
||
account.value = saved.account
|
||
pwd.value = saved.pwd
|
||
remember.value = true
|
||
}
|
||
} catch (e) {
|
||
console.error('读取本地登录信息失败', e)
|
||
}
|
||
})
|
||
|
||
function toggleRemember() {
|
||
remember.value = !remember.value
|
||
}
|
||
|
||
function toggleAgreement() {
|
||
agreementChecked.value = !agreementChecked.value
|
||
}
|
||
|
||
function goToRegister() { uni.navigateTo({ url: '/pages/register/register' }) }
|
||
|
||
function handleLogin() {
|
||
if (!agreementChecked.value) {
|
||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
||
return
|
||
}
|
||
// TODO: Implement actual username/password login logic if API available
|
||
uni.showToast({ title: '普通登录逻辑待接入', icon: 'none' })
|
||
}
|
||
|
||
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
|
||
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }
|
||
|
||
function onGetPhoneNumber(e) {
|
||
if (!agreementChecked.value) {
|
||
uni.showToast({ title: '请先阅读并同意协议', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const phoneCode = e.detail.code
|
||
console.log('login_flow start getPhoneNumber, codeExists:', !!phoneCode)
|
||
if (!phoneCode) {
|
||
uni.showToast({ title: '未授权手机号', icon: 'none' })
|
||
return
|
||
}
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: async (res) => {
|
||
try {
|
||
const loginCode = res.code
|
||
const inviterCode = uni.getStorageSync('inviter_code')
|
||
const data = await wechatLogin(loginCode, inviterCode)
|
||
const token = data && data.token
|
||
const user_id = data && data.user_id
|
||
const user_info = data || {}
|
||
|
||
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)
|
||
|
||
// 绑定手机号逻辑...
|
||
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)
|
||
}
|
||
|
||
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) {}
|
||
|
||
uni.showToast({ title: '欢迎回来', icon: 'success' })
|
||
setTimeout(() => {
|
||
uni.reLaunch({ url: '/pages/mine/index' }) // Redirect to Mine page
|
||
}, 500)
|
||
|
||
} catch (err) {
|
||
error.value = err.message || '登录失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
},
|
||
fail: () => {
|
||
error.value = '微信登录失败'
|
||
loading.value = false
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Page Container */
|
||
.container {
|
||
min-height: 100vh;
|
||
position: relative;
|
||
background: #F8F5F2; /* Cream base */
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* Orbs Background */
|
||
.orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(60rpx);
|
||
z-index: 0;
|
||
}
|
||
.orb-1 {
|
||
width: 400rpx;
|
||
height: 400rpx;
|
||
background: rgba(255, 107, 0, 0.15);
|
||
top: -100rpx;
|
||
left: -100rpx;
|
||
}
|
||
.orb-2 {
|
||
width: 500rpx;
|
||
height: 500rpx;
|
||
background: rgba(255, 215, 0, 0.1);
|
||
bottom: -150rpx;
|
||
right: -150rpx;
|
||
}
|
||
|
||
.content-wrap {
|
||
position: relative;
|
||
z-index: 1;
|
||
padding: 0 60rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Brand Section */
|
||
.brand-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 80rpx;
|
||
}
|
||
.logo-box {
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
background: #FFFFFF;
|
||
border-radius: 40rpx;
|
||
padding: 20rpx;
|
||
box-shadow: 0 20rpx 60rpx rgba(255, 107, 0, 0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 32rpx;
|
||
animation: float 6s ease-in-out infinite;
|
||
}
|
||
@keyframes float {
|
||
0% { transform: translateY(0); }
|
||
50% { transform: translateY(-20rpx); }
|
||
100% { transform: translateY(0); }
|
||
}
|
||
.logo { width: 100%; height: 100%; }
|
||
.app-name { font-size: 48rpx; font-weight: 900; color: #1A1A1A; margin-bottom: 8rpx; letter-spacing: 2rpx; }
|
||
.welcome-text { font-size: 28rpx; color: #888; letter-spacing: 4rpx; }
|
||
|
||
/* Form Styles */
|
||
.input-group {
|
||
background: #FFFFFF;
|
||
border-radius: 999rpx;
|
||
height: 100rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
margin-bottom: 32rpx;
|
||
border: 2rpx solid transparent;
|
||
transition: all 0.3s;
|
||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||
}
|
||
.input-group:focus-within {
|
||
border-color: #FF6B00;
|
||
box-shadow: 0 0 0 6rpx rgba(255, 107, 0, 0.1);
|
||
transform: translateY(-2rpx);
|
||
}
|
||
.input-icon { width: 40rpx; height: 40rpx; margin-right: 20rpx; opacity: 0.6; }
|
||
.input-icon image { width: 100%; height: 100%; }
|
||
.input-field { flex: 1; height: 100%; font-size: 30rpx; color: #333; }
|
||
.input-placeholder { color: #BBB; }
|
||
|
||
.options-row { display: flex; justify-content: space-between; margin-bottom: 60rpx; }
|
||
.remember-box { display: flex; align-items: center; }
|
||
.checkbox { width: 36rpx; height: 36rpx; border: 3rpx solid #DDD; border-radius: 8rpx; margin-right: 12rpx; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
|
||
.checkbox.checked { background: #FF6B00; border-color: #FF6B00; }
|
||
.check-mark { color: #FFF; font-size: 24rpx; font-weight: bold; }
|
||
.remember-text { font-size: 26rpx; color: #666; }
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
height: 100rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.2s;
|
||
}
|
||
.btn:active { transform: scale(0.96); }
|
||
|
||
.login-btn {
|
||
background: linear-gradient(90deg, #FF6B00 0%, #FFA500 100%);
|
||
color: #FFF;
|
||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 0, 0.3);
|
||
margin-bottom: 32rpx;
|
||
}
|
||
.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;
|
||
}
|
||
@keyframes shine {
|
||
0% { left: -100%; }
|
||
20% { left: 200%; }
|
||
100% { left: 200%; }
|
||
}
|
||
|
||
.weixin-btn {
|
||
background: #07C160;
|
||
color: #FFF;
|
||
box-shadow: 0 10rpx 30rpx rgba(7, 193, 96, 0.3);
|
||
}
|
||
.wx-icon { width: 48rpx; height: 48rpx; margin-right: 16rpx; }
|
||
|
||
/* Register Link */
|
||
.register-link { text-align: center; margin-top: 32rpx; }
|
||
.register-text { font-size: 28rpx; color: #888; }
|
||
.highlight { color: #FF6B00; font-weight: 700; margin-left: 8rpx; }
|
||
|
||
/* Agreements */
|
||
.agreements {
|
||
margin-top: 80rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.checkbox.round { border-radius: 50%; width: 32rpx; height: 32rpx; }
|
||
.checkbox-area { padding: 10rpx; }
|
||
.agreement-text { font-size: 24rpx; color: #999; margin-left: 8rpx; }
|
||
.link { color: #FF6B00; text-decoration: underline; margin: 0 4rpx; }
|
||
|
||
.error-toast {
|
||
position: fixed;
|
||
top: 100rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(255, 60, 60, 0.9);
|
||
color: #fff;
|
||
padding: 16rpx 32rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 26rpx;
|
||
z-index: 999;
|
||
}
|
||
</style> |