feat(小程序): 修复运行时与登录链路
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
import './app.scss'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
function App(props) {
|
||||
useLaunch(() => {
|
||||
console.log('App launched.')
|
||||
})
|
||||
|
||||
function App(props: PropsWithChildren) {
|
||||
return props.children
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface AppConfig {
|
||||
}
|
||||
|
||||
export const APP_CONFIG: AppConfig = {
|
||||
appEnv: __APP_ENV__,
|
||||
apiBaseUrl: __API_BASE_URL__,
|
||||
appEnv: typeof __APP_ENV__ === 'undefined' ? 'development' : __APP_ENV__,
|
||||
apiBaseUrl: typeof __API_BASE_URL__ === 'undefined' ? 'http://127.0.0.1:8001' : __API_BASE_URL__,
|
||||
}
|
||||
|
||||
export const API_BASE_URL = APP_CONFIG.apiBaseUrl
|
||||
|
||||
@@ -1,101 +1,77 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { wechatLogin } from '@/services/auth'
|
||||
import { getToken, wechatLogin } from '@/services/auth'
|
||||
import './index.scss'
|
||||
|
||||
export default function Login() {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[Login] Page mounted, checking token...')
|
||||
const token = Taro.getStorageSync('token')
|
||||
console.log('[Login] Token exists:', !!token)
|
||||
if (token) {
|
||||
console.log('[Login] Already logged in, redirecting to device page')
|
||||
if (getToken()) {
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const doWeChatLogin = async () => {
|
||||
try {
|
||||
console.log('[Login] Starting WeChat login...')
|
||||
Taro.showLoading({ title: '登录中...' })
|
||||
const handleLogin = async () => {
|
||||
if (submitting) return
|
||||
|
||||
// 1. 调用微信登录获取 code
|
||||
console.log('[Login] Step 1: Calling Taro.login()...')
|
||||
setSubmitting(true)
|
||||
Taro.showLoading({ title: '登录中...' })
|
||||
|
||||
try {
|
||||
const loginRes = await Taro.login()
|
||||
const code = loginRes.code
|
||||
console.log('[Login] Got code:', code ? 'yes' : 'no')
|
||||
if (!code) {
|
||||
if (!loginRes.code) {
|
||||
throw new Error('未获取到微信登录凭证')
|
||||
}
|
||||
|
||||
// 2. 获取用户头像信息 (可选)
|
||||
let userInfo = null
|
||||
let userInfo:
|
||||
| {
|
||||
nickName?: string
|
||||
avatarUrl?: string
|
||||
}
|
||||
| null = null
|
||||
|
||||
try {
|
||||
console.log('[Login] Step 2: Calling Taro.getUserProfile()...')
|
||||
const profileRes = await Taro.getUserProfile({
|
||||
desc: '用于完善用户资料'
|
||||
desc: '用于完善家长资料展示',
|
||||
})
|
||||
userInfo = profileRes.userInfo
|
||||
console.log('[Login] Got userInfo:', !!userInfo)
|
||||
} catch (e) {
|
||||
console.log('[Login] getUserProfile failed (expected if user denied):', e)
|
||||
userInfo = profileRes.userInfo || null
|
||||
} catch (error) {
|
||||
console.log('[login] getUserProfile skipped:', error)
|
||||
}
|
||||
|
||||
// 3. 调用后端登录接口 (微信登录)
|
||||
console.log('[Login] Step 3: Calling backend /auth/login...')
|
||||
const res = await wechatLogin({
|
||||
code,
|
||||
await wechatLogin({
|
||||
code: loginRes.code,
|
||||
nickname: userInfo?.nickName,
|
||||
avatar_url: userInfo?.avatarUrl
|
||||
avatar_url: userInfo?.avatarUrl,
|
||||
})
|
||||
console.log('[Login] Backend login success, user_id:', res.user_id)
|
||||
|
||||
// 4. 保存用户信息用于显示
|
||||
console.log('[Login] Step 4: Saving user info...')
|
||||
if (userInfo) {
|
||||
Taro.setStorageSync('userInfo', {
|
||||
nickname: userInfo.nickName,
|
||||
avatar_url: userInfo.avatarUrl
|
||||
avatar_url: userInfo.avatarUrl,
|
||||
})
|
||||
} else {
|
||||
Taro.removeStorageSync('userInfo')
|
||||
}
|
||||
|
||||
Taro.hideLoading()
|
||||
console.log('[Login] Step 5: Login complete, showing success toast')
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
|
||||
// 6. 跳转到首页
|
||||
console.log('[Login] Redirecting to /pages/device/index...')
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({ url: '/pages/device/index' })
|
||||
}, 500)
|
||||
|
||||
} catch (err: any) {
|
||||
}, 300)
|
||||
} catch (error: any) {
|
||||
console.error('[login] failed:', error)
|
||||
Taro.showToast({
|
||||
title: error?.message || '登录失败,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
console.error('[Login] Login failed:', err)
|
||||
|
||||
// 401 通常表示微信 code 失效或已被使用,提示用户重试登录
|
||||
if (err.status === 401) {
|
||||
Taro.showToast({
|
||||
title: '登录失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: err.message || '网络错误,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
console.log('Button clicked, starting login...')
|
||||
doWeChatLogin()
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='login-page'>
|
||||
<View className='login-header'>
|
||||
@@ -123,12 +99,9 @@ export default function Login() {
|
||||
</View>
|
||||
|
||||
<View className='login-btn-wrapper'>
|
||||
<Button
|
||||
className='login-btn'
|
||||
onClick={handleLogin}
|
||||
>
|
||||
<Button className='login-btn' loading={submitting} disabled={submitting} onClick={handleLogin}>
|
||||
<Text className='btn-icon'>🌐</Text>
|
||||
<Text className='btn-text'>微信一键登录</Text>
|
||||
<Text className='btn-text'>{submitting ? '登录中...' : '微信一键登录'}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { API_BASE_URL } from '@/config/env'
|
||||
import { getCurrentUserId, getToken, handleUnauthorized } from './session'
|
||||
|
||||
const BASE_URL = API_BASE_URL
|
||||
const REQUEST_TIMEOUT_MS = 10000
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(public status: number, message: string) {
|
||||
@@ -9,6 +11,16 @@ class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorDetail(data: any): string {
|
||||
if (!data) return 'Request failed'
|
||||
if (typeof data.detail === 'string') return data.detail
|
||||
if (Array.isArray(data.detail)) {
|
||||
return data.detail.map((item: any) => String(item?.msg || item)).join('; ')
|
||||
}
|
||||
if (typeof data.message === 'string') return data.message
|
||||
return 'Request failed'
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
url: string,
|
||||
options: {
|
||||
@@ -17,40 +29,42 @@ async function request<T>(
|
||||
headers?: Record<string, string>
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const token = Taro.getStorageSync('token')
|
||||
console.log(`[API] ${options.method || 'GET'} ${url}`, token ? 'with token' : 'no token')
|
||||
|
||||
const token = getToken()
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await Taro.request({
|
||||
url: `${BASE_URL}${url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: headers,
|
||||
})
|
||||
let response
|
||||
try {
|
||||
response = await Taro.request({
|
||||
url: `${BASE_URL}${url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: headers,
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
})
|
||||
} catch (error: any) {
|
||||
const message = String(error?.errMsg || error?.message || '')
|
||||
const detail = message.includes('timeout')
|
||||
? '请求超时,请检查后端服务是否可访问'
|
||||
: '网络请求失败,请检查后端地址和网络连接'
|
||||
throw new ApiError(0, detail)
|
||||
}
|
||||
|
||||
if (response.statusCode === 401) {
|
||||
handleUnauthorized({ redirect: !url.startsWith('/auth/') })
|
||||
}
|
||||
|
||||
console.log(`[API] Response status:`, response.statusCode)
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
const detail = response.data?.detail || 'Request failed'
|
||||
throw new ApiError(response.statusCode, detail)
|
||||
throw new ApiError(response.statusCode, getErrorDetail(response.data))
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data as T
|
||||
}
|
||||
|
||||
export { request, ApiError, BASE_URL }
|
||||
|
||||
export function getToken(): string {
|
||||
return Taro.getStorageSync('token') || ''
|
||||
}
|
||||
|
||||
export function getCurrentUserId(): number {
|
||||
return Taro.getStorageSync('user_id') || 0
|
||||
}
|
||||
export { getCurrentUserId, getToken }
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
import {
|
||||
clearLoginSession,
|
||||
getCurrentUserId as getStoredCurrentUserId,
|
||||
getToken as getStoredToken,
|
||||
saveLoginSession,
|
||||
} from './session'
|
||||
import { request } from './api'
|
||||
|
||||
export interface WechatLoginPayload {
|
||||
@@ -25,40 +29,19 @@ export interface Parent {
|
||||
status: number
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
export async function wechatLogin(data: WechatLoginPayload): Promise<LoginSession> {
|
||||
const session = await request<LoginSession>('/auth/login', {
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
|
||||
Taro.setStorageSync('token', session.access_token)
|
||||
Taro.setStorageSync('token_type', session.token_type)
|
||||
Taro.setStorageSync('user_id', session.user_id)
|
||||
Taro.setStorageSync('expires_in', session.expires_in)
|
||||
|
||||
saveLoginSession(session)
|
||||
return session
|
||||
}
|
||||
|
||||
// 创建家长
|
||||
export async function createParent(data: {
|
||||
openid: string
|
||||
unionid?: string
|
||||
nickname?: string
|
||||
avatar_url?: string
|
||||
}): Promise<Parent> {
|
||||
return request<Parent>('/parents', {
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取家长信息
|
||||
export async function getParent(userId: number): Promise<Parent> {
|
||||
return request<Parent>(`/parents/${userId}`)
|
||||
}
|
||||
|
||||
// 更新家长信息
|
||||
export async function updateParent(
|
||||
userId: number,
|
||||
data: { nickname?: string; avatar_url?: string; phone?: string }
|
||||
@@ -69,20 +52,14 @@ export async function updateParent(
|
||||
})
|
||||
}
|
||||
|
||||
// 获取 token
|
||||
export function getToken(): string {
|
||||
return Taro.getStorageSync('token')
|
||||
return getStoredToken()
|
||||
}
|
||||
|
||||
// 清除 token
|
||||
export function clearToken(): void {
|
||||
Taro.removeStorageSync('token')
|
||||
Taro.removeStorageSync('token_type')
|
||||
Taro.removeStorageSync('user_id')
|
||||
Taro.removeStorageSync('expires_in')
|
||||
clearLoginSession()
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
export function getCurrentUserId(): number {
|
||||
return Taro.getStorageSync('user_id') || 0
|
||||
return getStoredCurrentUserId()
|
||||
}
|
||||
|
||||
72
banban-mini/src/services/session.ts
Normal file
72
banban-mini/src/services/session.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const TOKEN_KEY = 'token'
|
||||
const TOKEN_TYPE_KEY = 'token_type'
|
||||
const USER_ID_KEY = 'user_id'
|
||||
const EXPIRES_IN_KEY = 'expires_in'
|
||||
const TOKEN_EXPIRES_AT_KEY = 'token_expires_at'
|
||||
const LOGIN_PAGE_URL = '/pages/login/index'
|
||||
|
||||
let redirectingToLogin = false
|
||||
|
||||
export interface StoredLoginSession {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
user_id: number
|
||||
}
|
||||
|
||||
export function saveLoginSession(session: StoredLoginSession): void {
|
||||
const expiresAt = Date.now() + Math.max(session.expires_in, 0) * 1000
|
||||
Taro.setStorageSync(TOKEN_KEY, session.access_token)
|
||||
Taro.setStorageSync(TOKEN_TYPE_KEY, session.token_type)
|
||||
Taro.setStorageSync(USER_ID_KEY, session.user_id)
|
||||
Taro.setStorageSync(EXPIRES_IN_KEY, session.expires_in)
|
||||
Taro.setStorageSync(TOKEN_EXPIRES_AT_KEY, expiresAt)
|
||||
}
|
||||
|
||||
export function clearLoginSession(): void {
|
||||
Taro.removeStorageSync(TOKEN_KEY)
|
||||
Taro.removeStorageSync(TOKEN_TYPE_KEY)
|
||||
Taro.removeStorageSync(USER_ID_KEY)
|
||||
Taro.removeStorageSync(EXPIRES_IN_KEY)
|
||||
Taro.removeStorageSync(TOKEN_EXPIRES_AT_KEY)
|
||||
}
|
||||
|
||||
function getTokenExpiresAt(): number {
|
||||
const rawValue = Number(Taro.getStorageSync(TOKEN_EXPIRES_AT_KEY) || 0)
|
||||
return Number.isFinite(rawValue) ? rawValue : 0
|
||||
}
|
||||
|
||||
export function getToken(): string {
|
||||
const token = Taro.getStorageSync(TOKEN_KEY) || ''
|
||||
if (!token) return ''
|
||||
|
||||
const expiresAt = getTokenExpiresAt()
|
||||
if (expiresAt > 0 && Date.now() >= expiresAt) {
|
||||
clearLoginSession()
|
||||
return ''
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
export function getCurrentUserId(): number {
|
||||
if (!getToken()) return 0
|
||||
return Number(Taro.getStorageSync(USER_ID_KEY) || 0)
|
||||
}
|
||||
|
||||
export function handleUnauthorized(options: { redirect?: boolean } = {}): void {
|
||||
const { redirect = true } = options
|
||||
clearLoginSession()
|
||||
|
||||
if (!redirect || redirectingToLogin) return
|
||||
|
||||
redirectingToLogin = true
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({ url: LOGIN_PAGE_URL })
|
||||
setTimeout(() => {
|
||||
redirectingToLogin = false
|
||||
}, 600)
|
||||
}, 120)
|
||||
}
|
||||
Reference in New Issue
Block a user