383 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>