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:
@@ -10,6 +10,7 @@ const toMember = (backendMember = {}) => ({
|
|||||||
avatar_url: backendMember.avatar_url || '',
|
avatar_url: backendMember.avatar_url || '',
|
||||||
balance: backendMember.balance || '0.00',
|
balance: backendMember.balance || '0.00',
|
||||||
is_active: backendMember.is_active === true,
|
is_active: backendMember.is_active === true,
|
||||||
|
profile_completed: backendMember.profile_completed === true,
|
||||||
created_at: backendMember.created_at || ''
|
created_at: backendMember.created_at || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ export const authApi = {
|
|||||||
}
|
}
|
||||||
}).then(toWechatLogin)
|
}).then(toWechatLogin)
|
||||||
|
|
||||||
authStorage.setToken(loginData.access_token)
|
authStorage.setPendingToken(loginData.access_token)
|
||||||
return loginData
|
return loginData
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const toMember = (backendMember = {}) => ({
|
|||||||
avatar_url: backendMember.avatar_url || '',
|
avatar_url: backendMember.avatar_url || '',
|
||||||
balance: backendMember.balance || '0.00',
|
balance: backendMember.balance || '0.00',
|
||||||
is_active: backendMember.is_active === true,
|
is_active: backendMember.is_active === true,
|
||||||
|
profile_completed: backendMember.profile_completed === true,
|
||||||
created_at: backendMember.created_at || ''
|
created_at: backendMember.created_at || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const KEY_TOKEN = 'COMPREHENSIVE_VENUE_TOKEN'
|
|||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001'
|
const API_BASE_URL = import.meta.env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001'
|
||||||
|
|
||||||
|
let pendingToken = null
|
||||||
|
|
||||||
const normalizeError = (res, fallbackMessage = '请求服务失败') => {
|
const normalizeError = (res, fallbackMessage = '请求服务失败') => {
|
||||||
const body = res?.data || res || {}
|
const body = res?.data || res || {}
|
||||||
return {
|
return {
|
||||||
@@ -19,16 +21,41 @@ export const authStorage = {
|
|||||||
getToken() {
|
getToken() {
|
||||||
return uni.getStorageSync(KEY_TOKEN)
|
return uni.getStorageSync(KEY_TOKEN)
|
||||||
},
|
},
|
||||||
|
setPendingToken(token) {
|
||||||
|
pendingToken = token
|
||||||
|
},
|
||||||
|
getPendingToken() {
|
||||||
|
return pendingToken
|
||||||
|
},
|
||||||
|
clearPendingToken() {
|
||||||
|
pendingToken = null
|
||||||
|
},
|
||||||
clear() {
|
clear() {
|
||||||
uni.removeStorageSync(KEY_TOKEN)
|
uni.removeStorageSync(KEY_TOKEN)
|
||||||
|
pendingToken = null
|
||||||
},
|
},
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return !!uni.getStorageSync(KEY_TOKEN)
|
return !!uni.getStorageSync(KEY_TOKEN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isRedirecting = false
|
||||||
|
|
||||||
|
function redirectToLogin() {
|
||||||
|
if (isRedirecting) return
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const current = pages[pages.length - 1]
|
||||||
|
const currentRoute = current?.route || ''
|
||||||
|
if (currentRoute === 'pages/comprehensive/profile/login' || currentRoute === 'pages/comprehensive/profile/setup') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isRedirecting = true
|
||||||
|
authStorage.clear()
|
||||||
|
uni.reLaunch({ url: '/pages/comprehensive/profile/login', complete: () => { isRedirecting = false } })
|
||||||
|
}
|
||||||
|
|
||||||
export function request(options = {}) {
|
export function request(options = {}) {
|
||||||
const token = authStorage.getToken()
|
const token = authStorage.getToken() || authStorage.getPendingToken()
|
||||||
const header = {
|
const header = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(options.header || {})
|
...(options.header || {})
|
||||||
@@ -48,7 +75,9 @@ export function request(options = {}) {
|
|||||||
const body = res.data
|
const body = res.data
|
||||||
|
|
||||||
if (res.statusCode === 401 || body?.code === 40001) {
|
if (res.statusCode === 401 || body?.code === 40001) {
|
||||||
authStorage.clear()
|
redirectToLogin()
|
||||||
|
reject(normalizeError(res, ''))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
|
if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
|
||||||
@@ -76,7 +105,7 @@ export function request(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function uploadFile(options = {}) {
|
export function uploadFile(options = {}) {
|
||||||
const token = authStorage.getToken()
|
const token = authStorage.getToken() || authStorage.getPendingToken()
|
||||||
const header = {
|
const header = {
|
||||||
...(options.header || {})
|
...(options.header || {})
|
||||||
}
|
}
|
||||||
@@ -101,7 +130,9 @@ export function uploadFile(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.statusCode === 401 || body?.code === 40001) {
|
if (res.statusCode === 401 || body?.code === 40001) {
|
||||||
authStorage.clear()
|
redirectToLogin()
|
||||||
|
reject(normalizeError({ ...res, data: body }, ''))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
|
if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const useComprehensiveSessionStore = defineStore('comprehensiveVenueSessi
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
isReady: false,
|
isReady: false,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
|
needsSetup: false,
|
||||||
member: null
|
member: null
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@@ -28,12 +29,23 @@ export const useComprehensiveSessionStore = defineStore('comprehensiveVenueSessi
|
|||||||
const loginResult = await loginByWechat()
|
const loginResult = await loginByWechat()
|
||||||
const data = await authApi.wechatLogin({ code: loginResult.code })
|
const data = await authApi.wechatLogin({ code: loginResult.code })
|
||||||
this.member = data.member
|
this.member = data.member
|
||||||
this.isLoggedIn = true
|
this.needsSetup = true
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
completeLogin() {
|
||||||
|
const pendingToken = authStorage.getPendingToken()
|
||||||
|
if (pendingToken) {
|
||||||
|
authStorage.setToken(pendingToken)
|
||||||
|
authStorage.clearPendingToken()
|
||||||
|
this.isLoggedIn = true
|
||||||
|
this.needsSetup = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
authApi.logout()
|
authApi.logout()
|
||||||
this.member = null
|
this.member = null
|
||||||
this.isLoggedIn = false
|
this.isLoggedIn = false
|
||||||
|
this.needsSetup = false
|
||||||
},
|
},
|
||||||
async refreshMember() {
|
async refreshMember() {
|
||||||
if (!authStorage.isLoggedIn()) return
|
if (!authStorage.isLoggedIn()) return
|
||||||
|
|||||||
@@ -174,6 +174,18 @@
|
|||||||
"navigationBarTitleText": "个人信息"
|
"navigationBarTitleText": "个人信息"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/profile/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/profile/setup",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "完善信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/comprehensive/mall/index",
|
"path": "pages/comprehensive/mall/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="courses-page">
|
<view class="courses-page">
|
||||||
<UnauthLoginPanel
|
<block>
|
||||||
v-if="activeTab === 'membership' && !session.isLoggedIn"
|
|
||||||
@login="login"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<block v-else>
|
|
||||||
<view class="tab-bar">
|
<view class="tab-bar">
|
||||||
<view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">会员卡</view>
|
<view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">会员卡</view>
|
||||||
<view class="tab-item" :class="{ active: activeTab === 'group' }" @tap="switchTab('group')">团课</view>
|
<view class="tab-item" :class="{ active: activeTab === 'group' }" @tap="switchTab('group')">团课</view>
|
||||||
@@ -104,7 +99,6 @@ import { ticketApi } from '@/modules/comprehensive/api/ticket'
|
|||||||
import { multiplyAmount } from '@/modules/comprehensive/utils/money'
|
import { multiplyAmount } from '@/modules/comprehensive/utils/money'
|
||||||
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||||
import { formatTimeRange, todayString } from '@/modules/comprehensive/utils/date'
|
import { formatTimeRange, todayString } from '@/modules/comprehensive/utils/date'
|
||||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
|
||||||
|
|
||||||
const session = useComprehensiveSessionStore()
|
const session = useComprehensiveSessionStore()
|
||||||
const activeTab = ref('membership')
|
const activeTab = ref('membership')
|
||||||
@@ -225,11 +219,6 @@ const goDetail = (item) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
await session.login()
|
|
||||||
loadSessions(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (activeTab.value === 'tickets') {
|
if (activeTab.value === 'tickets') {
|
||||||
ticketProduct.value = null
|
ticketProduct.value = null
|
||||||
|
|||||||
@@ -1,71 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="profile-page container">
|
<view class="profile-page container">
|
||||||
<UnauthLoginPanel
|
<view v-if="!session.isLoggedIn" class="user-profile-hero" @tap="login">
|
||||||
v-if="!session.isLoggedIn"
|
<view class="hero-left">
|
||||||
@login="login"
|
<view class="hero-avatar placeholder">
|
||||||
/>
|
<text class="hero-placeholder-text">点击登录</text>
|
||||||
|
|
||||||
<block v-else>
|
|
||||||
<view class="user-profile-hero" @tap="go('/pages/comprehensive/profile/detail')">
|
|
||||||
<view class="hero-left">
|
|
||||||
<image v-if="session.member?.avatar_url" :src="session.member.avatar_url" mode="aspectFill" class="hero-avatar" />
|
|
||||||
<view v-else class="hero-avatar placeholder">
|
|
||||||
<text>{{ avatarInitial }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="hero-meta">
|
|
||||||
<text class="hero-name">{{ session.member?.nickname }}</text>
|
|
||||||
<view v-if="session.member?.phone" class="hero-sub-row">
|
|
||||||
<text class="hero-phone">{{ session.member.phone }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="hero-balance">余额 ¥{{ balanceText }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="hero-right">
|
<view class="hero-meta">
|
||||||
<view class="hero-arrow" />
|
<text class="hero-name">点击登录</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="hero-right">
|
||||||
|
<view class="hero-arrow" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="menu-group-card">
|
<view v-else class="user-profile-hero" @tap="go('/pages/comprehensive/profile/detail')">
|
||||||
<view v-for="item in menuItems" :key="item.title" class="menu-item" @tap="go(item.url)">
|
<view class="hero-left">
|
||||||
<view class="item-left">
|
<image v-if="session.member?.avatar_url" :src="session.member.avatar_url" mode="aspectFill" class="hero-avatar" />
|
||||||
<view class="item-icon-container" :class="item.theme">
|
<view v-else class="hero-avatar placeholder">
|
||||||
<view class="icon-order" v-if="item.icon === 'order'">
|
<text>{{ avatarInitial }}</text>
|
||||||
<view class="icon-order-line" />
|
|
||||||
<view class="icon-order-line short" />
|
|
||||||
</view>
|
|
||||||
<view class="icon-private" v-else-if="item.icon === 'private'">
|
|
||||||
<view class="icon-private-net" />
|
|
||||||
</view>
|
|
||||||
<view class="icon-ticket" v-else-if="item.icon === 'ticket'">
|
|
||||||
<view class="icon-ticket-dot" />
|
|
||||||
</view>
|
|
||||||
<view class="icon-profile" v-else>
|
|
||||||
<view class="icon-profile-head" />
|
|
||||||
<view class="icon-profile-body" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="menu-title">{{ item.title }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="item-right">
|
|
||||||
<view class="menu-arrow" />
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item logout" @tap="logout">
|
<view class="hero-meta">
|
||||||
<view class="item-left">
|
<text class="hero-name">{{ session.member?.nickname }}</text>
|
||||||
<view class="item-icon-container logout-theme">
|
<view v-if="session.member?.phone" class="hero-sub-row">
|
||||||
<view class="icon-logout">
|
<text class="hero-phone">{{ session.member.phone }}</text>
|
||||||
<view class="icon-logout-door" />
|
|
||||||
<view class="icon-logout-arrow" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="menu-title">退出登录</text>
|
|
||||||
</view>
|
|
||||||
<view class="item-right">
|
|
||||||
<view class="menu-arrow" />
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="hero-balance">余额 ¥{{ balanceText }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
<view class="hero-right">
|
||||||
|
<view class="hero-arrow" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-group-card">
|
||||||
|
<view v-for="item in menuItems" :key="item.title" class="menu-item" @tap="go(item.url)">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="item-icon-container" :class="item.theme">
|
||||||
|
<view class="icon-order" v-if="item.icon === 'order'">
|
||||||
|
<view class="icon-order-line" />
|
||||||
|
<view class="icon-order-line short" />
|
||||||
|
</view>
|
||||||
|
<view class="icon-private" v-else-if="item.icon === 'private'">
|
||||||
|
<view class="icon-private-net" />
|
||||||
|
</view>
|
||||||
|
<view class="icon-ticket" v-else-if="item.icon === 'ticket'">
|
||||||
|
<view class="icon-ticket-dot" />
|
||||||
|
</view>
|
||||||
|
<view class="icon-profile" v-else>
|
||||||
|
<view class="icon-profile-head" />
|
||||||
|
<view class="icon-profile-body" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="menu-title">{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<view class="menu-arrow" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="session.isLoggedIn" class="menu-item logout" @tap="logout">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="item-icon-container logout-theme">
|
||||||
|
<view class="icon-logout">
|
||||||
|
<view class="icon-logout-door" />
|
||||||
|
<view class="icon-logout-arrow" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="menu-title">退出登录</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<view class="menu-arrow" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<AgreementLinks v-if="session.isLoggedIn" class="profile-agreement-links" compact />
|
<AgreementLinks v-if="session.isLoggedIn" class="profile-agreement-links" compact />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -76,7 +83,6 @@ import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/ses
|
|||||||
import { memberApi } from '@/modules/comprehensive/api/member'
|
import { memberApi } from '@/modules/comprehensive/api/member'
|
||||||
import { formatAmount } from '@/modules/comprehensive/utils/money'
|
import { formatAmount } from '@/modules/comprehensive/utils/money'
|
||||||
import AgreementLinks from '@/components/AgreementLinks.vue'
|
import AgreementLinks from '@/components/AgreementLinks.vue'
|
||||||
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
|
||||||
|
|
||||||
const session = useComprehensiveSessionStore()
|
const session = useComprehensiveSessionStore()
|
||||||
const avatarInitial = computed(() => (session.member?.nickname || '用户').slice(0, 1))
|
const avatarInitial = computed(() => (session.member?.nickname || '用户').slice(0, 1))
|
||||||
@@ -104,8 +110,8 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const login = async () => {
|
const login = () => {
|
||||||
await session.login()
|
uni.navigateTo({ url: '/pages/comprehensive/profile/login' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
@@ -121,6 +127,65 @@ defineExpose({ onPageShow: () => {} })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.login-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 160rpx;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
border: 1rpx solid rgba(241, 245, 249, 0.9);
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
background-color: $bg-color-main;
|
||||||
|
box-shadow: 0 10rpx 40rpx rgba(15, 23, 42, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry:active {
|
||||||
|
opacity: 0.96;
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-avatar {
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-avatar.placeholder {
|
||||||
|
background: $color-primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-entry-sub {
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.user-profile-hero {
|
.user-profile-hero {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -164,6 +229,12 @@ defineExpose({ onPageShow: () => {} })
|
|||||||
background: $color-primary-light;
|
background: $color-primary-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-placeholder-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-meta {
|
.hero-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page">
|
||||||
|
<UnauthLoginPanel @login="handleLogin" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||||
|
import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue'
|
||||||
|
|
||||||
|
const session = useComprehensiveSessionStore()
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await session.login()
|
||||||
|
uni.redirectTo({ url: '/pages/comprehensive/profile/setup' })
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $bg-color-soft;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user