Files
hejiu/pages/login/login.vue
T
cg 99723497fa rename(app): 将应用名称从"干杯日记"更改为"碰盏日记"
- 替换所有文件中的品牌名称引用,包括App.vue、pages.json、uni.scss等
- 更新全局样式、导航栏标题和组件中的文案显示
- 修改分享功能中的应用名称显示
- 调整聊天页面的输入栏键盘适配逻辑
- 在多个页面组件中添加事件发射声明以支持交互功能
2026-07-21 22:20:08 +08:00

477 lines
11 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="themeClass" class="page-container login-page">
<!-- 顶部品牌区 -->
<view class="brand-area">
<view class="brand-glow"></view>
<view class="brand-icon">
<text class="brand-emoji">🍻</text>
</view>
<text class="brand-name">碰盏日记</text>
<text class="brand-slogan">让每一杯酒都有迹可循</text>
</view>
<!-- 登录区域 -->
<view class="login-area">
<!-- 理性饮酒须知 -->
<view v-if="showNotice" class="notice-card card">
<view class="notice-header">
<text class="notice-icon">📋</text>
<text class="notice-title">理性饮酒须知</text>
</view>
<view class="notice-content">
<text class="notice-item"> 过量饮酒有害健康请理性记录</text>
<text class="notice-item"> 本应用不鼓励未成年人使用</text>
<text class="notice-item"> 不涉及任何酒类销售与推荐</text>
<text class="notice-item"> 您的数据仅用于个人记录</text>
</view>
<button class="btn-primary notice-btn" @click="acceptNotice">我已了解</button>
</view>
<!-- 主登录区域 -->
<view v-else class="login-actions">
<!-- 头像 + 昵称填写微信新规范 -->
<view class="profile-form card">
<view class="form-title-row">
<text class="form-title">完善你的资料</text>
</view>
<!-- 头像选择 -->
<view class="avatar-row">
<text class="form-label">头像</text>
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image v-if="avatarUrl" class="avatar-preview" :src="avatarUrl" mode="aspectFill"></image>
<view v-else class="avatar-placeholder">
<text class="avatar-placeholder-icon">👤</text>
</view>
<text class="avatar-edit">{{ avatarUrl ? '换头像' : '选头像' }}</text>
</button>
</view>
<!-- 昵称输入 -->
<view class="nickname-row">
<text class="form-label">昵称</text>
<input
type="nickname"
class="nickname-input"
v-model="nickname"
placeholder="点击获取微信昵称"
@blur="onNicknameBlur"
/>
</view>
</view>
<!-- 确认登录 -->
<button class="btn-cta login-btn" :loading="loginLoading" :disabled="!agreed || loginLoading" @click="handleLogin">
<text class="login-btn-text">{{ loginLoading ? '登录中...' : '完成并进入' }}</text>
</button>
<!-- 协议勾选 -->
<view class="agreement-check" @click="agreed = !agreed">
<view class="check-box" :class="{ 'check-box-active': agreed }">
<text v-if="agreed" class="check-mark"></text>
</view>
<text class="agreement-check-text">我已阅读并同意</text>
<text class="agreement-check-text text-amber" @click.stop="openAgreement('user')">用户协议</text>
<text class="agreement-check-text"></text>
<text class="agreement-check-text text-amber" @click.stop="openAgreement('privacy')">隐私政策</text>
</view>
</view>
</view>
<!-- 底部协议非登录状态时显示 -->
<view v-if="showNotice" class="agreement">
<text class="text-tiny">登录即代表同意</text>
<text class="text-tiny text-amber">用户协议</text>
<text class="text-tiny"></text>
<text class="text-tiny text-amber">隐私政策</text>
</view>
</view>
</template>
<script>
import themeMixin from '../../common/theme-mixin'
import client, { saveAuthTokens } from '../../common/api'
export default {
mixins: [themeMixin],
data() {
return {
showNotice: true,
loginLoading: false,
agreed: false,
avatarUrl: '',
nickname: ''
}
},
onShareAppMessage() {
return {
title: '碰盏日记 - 让每一杯酒都有迹可循',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '碰盏日记 - 让每一杯酒都有迹可循',
query: ''
}
},
methods: {
acceptNotice() {
this.showNotice = false
},
// ===== 微信新规范:头像选择 =====
onChooseAvatar(e) {
if (e.detail && e.detail.avatarUrl) {
this.avatarUrl = e.detail.avatarUrl
}
},
// ===== 微信新规范:昵称输入 =====
onNicknameBlur(e) {
// 用户输入内容会自动更新到 v-model,这里可以做额外处理
if (e.detail && e.detail.value) {
this.nickname = e.detail.value
}
},
// ===== 确认登录 =====
async handleLogin() {
if (this.loginLoading) return
if (!this.agreed) {
uni.showToast({ title: '请先同意用户协议和隐私政策', icon: 'none', duration: 2000 })
return
}
if (!this.nickname.trim()) {
uni.showToast({ title: '请先填写昵称', icon: 'none', duration: 2000 })
return
}
this.loginLoading = true
try {
// 1. 获取微信登录 code
const loginRes = await this.wxLogin()
if (!loginRes || !loginRes.code) {
throw new Error('获取微信code失败')
}
// 2. 调用后端微信登录接口
const loginParams = {
code: loginRes.code,
nickName: this.nickname.trim() || '',
avatarUrl: this.avatarUrl || ''
}
const resp = await client.WechatLogin(loginParams)
// 3. 保存 token 和用户信息
saveAuthTokens(resp)
if (resp.user) {
uni.setStorageSync('user_info', JSON.stringify(resp.user))
}
uni.setStorageSync('is_logged_in', 'true')
uni.setStorageSync('is_guest', 'false')
uni.setStorageSync('is_first_launch', 'false')
this.finishLogin()
} catch (e) {
console.warn('后端登录失败,降级为本地模式:', e)
uni.showToast({ title: e.msg || '登录失败,使用本地模式', icon: 'none', duration: 2000 })
// 降级为本地登录
const userInfo = {
id: `user_${Date.now()}`,
nickname: this.nickname.trim() || '酒友',
avatar: this.avatarUrl || '',
joinedAt: new Date().toISOString().slice(0, 10),
preferences: null
}
uni.setStorageSync('user_info', JSON.stringify(userInfo))
uni.setStorageSync('is_logged_in', 'true')
uni.setStorageSync('is_first_launch', 'false')
this.finishLogin()
} finally {
this.loginLoading = false
}
},
// 微信login获取code
wxLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => resolve(res),
fail: err => reject(err)
})
})
},
finishLogin() {
uni.setStorageSync('is_logged_in', 'true')
uni.setStorageSync('is_first_launch', 'false')
uni.switchTab({ url: '/pages/index/index' })
},
// ===== 协议查看 =====
openAgreement(type) {
const title = type === 'user' ? '用户协议' : '隐私政策'
uni.showToast({ title: `${title}页面开发中`, icon: 'none' })
}
}
}
</script>
<style lang="scss" scoped>
.login-page {
display: flex;
flex-direction: column;
height: 100vh;
padding: $sp-lg;
}
.brand-area {
flex: 0.6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding-top: 60rpx;
}
.brand-glow {
position: absolute;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(232,168,56,0.15) 0%, transparent 70%);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
filter: blur(30rpx);
}
.brand-icon {
width: 140rpx;
height: 140rpx;
background: $bg-card;
border-radius: $radius-lg;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-md;
margin-bottom: $sp-md;
position: relative;
z-index: 2;
}
.brand-emoji {
font-size: 64rpx;
}
.brand-name {
font-size: $fs-3xl;
font-weight: $fw-black;
color: $text-primary;
letter-spacing: -2rpx;
margin-bottom: $sp-xs;
}
.brand-slogan {
font-size: $fs-sm;
color: $text-secondary;
}
.login-area {
padding-bottom: $sp-lg;
}
/* 理性饮酒须知 */
.notice-card {
padding: $sp-lg;
}
.notice-header {
display: flex;
align-items: center;
gap: $sp-sm;
margin-bottom: $sp-md;
}
.notice-icon {
font-size: $fs-lg;
}
.notice-title {
font-size: $fs-base;
font-weight: $fw-bold;
}
.notice-content {
margin-bottom: $sp-md;
}
.notice-item {
display: block;
font-size: $fs-xs;
color: $text-secondary;
line-height: $lh-normal;
}
.notice-btn {
width: 100%;
}
/* 资料表单 */
.profile-form {
padding: $sp-lg;
margin-bottom: 0;
}
.form-title-row {
margin-bottom: $sp-md;
}
.form-title {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.avatar-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: $sp-sm 0;
border-bottom: 2rpx solid var(--border-micro, rgba(255, 255, 255, 0.05));
}
.avatar-btn {
display: flex;
align-items: center;
gap: $sp-sm;
background: none;
border: none;
padding: 0;
margin: 0;
line-height: normal;
&::after { display: none; }
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: $radius-full;
border: 3rpx solid $amber;
flex-shrink: 0;
}
.avatar-placeholder {
width: 80rpx;
height: 80rpx;
border-radius: $radius-full;
border: 3rpx dashed rgba(232,168,56,0.4);
background: $bg-elevated;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-placeholder-icon {
font-size: 40rpx;
opacity: 0.5;
}
.avatar-edit {
font-size: $fs-sm;
color: $amber;
}
.nickname-row {
display: flex;
align-items: center;
gap: $sp-lg;
padding: $sp-sm 0;
}
.form-label {
font-size: $fs-base;
color: $text-secondary;
flex-shrink: 0;
min-width: 80rpx;
}
.nickname-input {
flex: 1;
font-size: $fs-base;
color: $text-primary;
padding: $sp-xs 0;
}
/* 登录按钮 */
.login-actions {
display: flex;
flex-direction: column;
gap: $sp-md;
}
.login-btn {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
&[disabled] {
opacity: 0.5;
}
}
.login-btn-text {
font-size: $fs-lg;
font-weight: $fw-bold;
}
/* 协议勾选 */
.agreement-check {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $sp-xs;
padding-top: $sp-xs;
}
.check-box {
width: 36rpx;
height: 36rpx;
border-radius: $radius-sm;
border: 2rpx solid $text-tertiary;
display: flex;
align-items: center;
justify-content: center;
transition: all $duration-fast $ease-out;
flex-shrink: 0;
}
.check-box-active {
background: $amber;
border-color: $amber;
}
.check-mark {
font-size: $fs-xs;
color: $text-on-amber;
font-weight: $fw-bold;
}
.agreement-check-text {
font-size: $fs-xs;
color: $text-tertiary;
}
/* 底部协议 */
.agreement {
display: flex;
justify-content: center;
gap: $sp-xs;
padding: $sp-sm 0;
}
</style>