feat: 登录注册流程重构 - 登录页+完善信息页+401跳转

- 新增 profile/login.vue 登录页(UnauthLoginPanel)
- 新增 profile/setup.vue 完善信息页(头像/昵称/手机号)
- profile/index 未登录态显示头像占位+点击登录+菜单列表
- 登录后始终跳转setup页,completeLogin后才持久化token
- auth.js 登录后用pendingToken(内存),不检查profile_completed
- request.js 401时reLaunch到登录页,不显示toast,防重复跳转
- 约课页移除UnauthLoginPanel,未登录直接显示列表
- session.js 新增needsSetup状态和completeLogin方法
This commit is contained in:
gqt
2026-07-13 21:09:07 +08:00
parent b06cf69942
commit 5487ece142
9 changed files with 489 additions and 79 deletions
@@ -0,0 +1,265 @@
<template>
<view class="setup-page">
<view class="setup-card">
<view class="avatar-row">
<button class="avatar-picker" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image v-if="avatarUrl" :src="avatarUrl" mode="aspectFill" class="avatar-img" />
<view v-else class="avatar-placeholder">
<text class="avatar-placeholder-text">选择头像</text>
</view>
</button>
</view>
<view class="field-row">
<text class="field-label">昵称</text>
<input
type="nickname"
class="field-input"
placeholder="请输入昵称"
:value="nickname"
@blur="onNicknameBlur"
/>
</view>
<view class="field-row">
<text class="field-label">手机号</text>
<view class="phone-area">
<text v-if="phone" class="phone-text">{{ phone }}</text>
<button class="phone-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhone">
{{ phone ? '更换' : '获取' }}
</button>
</view>
</view>
<button
class="submit-button"
:class="{ disabled: submitting || !canSubmit }"
:disabled="submitting || !canSubmit"
@tap="onSubmit"
>
{{ submitting ? '提交中...' : '完成' }}
</button>
<view class="home-link" @tap="goHome">返回首页</view>
</view>
<AgreementLinks ref="agreementLinksRef" :show-entry="false" />
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { memberApi } from '@/modules/comprehensive/api/member'
import { authStorage } from '@/modules/comprehensive/api/auth'
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
import AgreementLinks from '@/components/AgreementLinks.vue'
const session = useComprehensiveSessionStore()
const avatarUrl = ref(session.member?.avatar_url || '')
const nickname = ref(session.member?.nickname || '')
const phone = ref(session.member?.phone || '')
const submitting = ref(false)
const canSubmit = computed(() => !!avatarUrl.value && !!nickname.value.trim())
const onChooseAvatar = async (e) => {
if (!e.detail.avatarUrl) return
try {
const data = await memberApi.uploadAvatar(e.detail.avatarUrl)
avatarUrl.value = data.avatar_url
} catch {
avatarUrl.value = e.detail.avatarUrl
}
}
const onNicknameBlur = (e) => {
nickname.value = e.detail.value || ''
}
const onGetPhone = async (e) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') return
try {
const member = await memberApi.bindMemberPhone(e.detail.code)
phone.value = member.phone || ''
} catch {
uni.showToast({ title: '获取手机号失败', icon: 'none' })
}
}
const onSubmit = async () => {
if (!canSubmit.value || submitting.value) return
submitting.value = true
try {
await memberApi.updateMemberProfile({
nickname: nickname.value.trim(),
avatar_url: avatarUrl.value
})
session.completeLogin()
uni.showToast({ title: '设置成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/profile/index' })
}, 500)
} catch {
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
const goHome = () => {
authStorage.clear()
session.member = null
session.needsSetup = false
uni.switchTab({ url: '/pages/home/index' })
}
</script>
<style lang="scss" scoped>
.setup-page {
min-height: 100vh;
padding: 0 $spacing-lg 48rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background: $bg-color-soft;
}
.setup-card {
width: 100%;
max-width: 560rpx;
display: flex;
flex-direction: column;
align-items: stretch;
gap: $spacing-md;
}
.avatar-row {
display: flex;
justify-content: center;
margin-bottom: $spacing-sm;
}
.avatar-picker {
width: 144rpx;
height: 144rpx;
padding: 0;
border-radius: 50%;
overflow: hidden;
background: $bg-color-hover;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.avatar-picker::after {
border: none;
}
.avatar-img {
width: 144rpx;
height: 144rpx;
border-radius: 50%;
}
.avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.avatar-placeholder-text {
font-size: 20rpx;
color: $text-color-light;
}
.field-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-md;
height: 96rpx;
padding: 0 $spacing-md;
border-radius: $border-radius-base;
background: $bg-color-main;
}
.field-label {
flex-shrink: 0;
color: $text-color-muted;
font-size: 28rpx;
font-weight: 700;
width: 100rpx;
text-align: left;
}
.field-input {
flex: 1;
text-align: right;
font-size: 28rpx;
color: $text-color-main;
}
.phone-area {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: $spacing-sm;
}
.phone-text {
color: $text-color-main;
font-size: 28rpx;
font-weight: 700;
}
.phone-btn {
font-size: 24rpx;
font-weight: 700;
color: $color-primary;
background: $color-primary-light;
padding: 8rpx 24rpx;
border-radius: $border-radius-sm;
line-height: 1.5;
}
.phone-btn::after {
border: none;
}
.submit-button {
width: 100%;
height: 88rpx;
border-radius: $border-radius-base;
color: #FFFFFF;
background-color: $text-color-main;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacing-sm;
}
.submit-button::after {
border: none;
}
.submit-button.disabled,
.submit-button[disabled] {
color: #FFFFFF;
background-color: $text-color-light;
}
.home-link {
text-align: center;
color: $text-color-muted;
font-size: 26rpx;
padding: $spacing-sm 0;
}
</style>