456 lines
14 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">
<view class="glass-card">
<!-- 品牌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>
<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
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)
// 保存 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)
}
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 lang="scss" scoped>
/* Page Container */
.container {
min-height: 100vh;
position: relative;
background: $bg-secondary; /* Use secondary for better depth with orbs */
overflow: hidden;
display: flex;
flex-direction: column;
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); }
}
.content-wrap {
position: relative;
z-index: 1;
padding: 0 40rpx;
width: 100%;
box-sizing: border-box;
animation: fadeInUp 0.6s ease-out;
}
.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);
}
/* Brand Section */
.brand-section {
display: flex;
flex-direction: column;
align-items: center;
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);
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;
}
/* Form Styles */
.input-group {
background: $bg-card;
border-radius: $radius-round;
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;
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);
}
}
.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;
display: flex;
align-items: center;
justify-content: center;
font-size: $font-lg;
font-weight: 800;
position: relative;
overflow: hidden;
transition: all 0.2s;
&:active { transform: scale(0.96); }
}
.login-btn {
background: $gradient-brand;
color: $text-inverse;
box-shadow: 0 10rpx 30rpx rgba($brand-primary, 0.3);
margin-bottom: $spacing-xl;
border: none;
}
.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; /* WeChat Brand Color */
color: $text-inverse;
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; }
.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;
justify-content: center;
align-items: flex-start;
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 {
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;
}
@keyframes slideDown {
from { transform: translate(-50%, -100%); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
}
</style>