feat(auth): 游客浏览模式 + needUserInfo 授权链路修复
- PUBLIC_PAGES 放开四个 tab 页(微信审核要求不强制授权),游客可浏览, 首页/事件/设备页未登录展示引导登录空态不再强制踢登录;mine 游客态 菜单收起只留登录入口;守卫单测同步 - needUserInfo 实测首登 Content 为 null:openId 改为可缺省的加速字段, 授权重调靠新 code 即可,登录页不再因缺 openId 阻断;补缺省分支单测 - http 层首登 Content 为 null 时先初始化再挂 message/token 字段 - 生产网关域名替换占位符(aiotcloud.celex-technology.com)
This commit is contained in:
parent
da86294fde
commit
42b6ceb69c
|
|
@ -9,7 +9,7 @@ import { ApiError, BusinessError, AuthExpiredError } from './errors'
|
||||||
// 网关分服务前缀:登录走 user 服务,其余业务接口走 biz 服务(后端约定 2026-09-17)
|
// 网关分服务前缀:登录走 user 服务,其余业务接口走 biz 服务(后端约定 2026-09-17)
|
||||||
const GATEWAY = process.env.NODE_ENV === 'development'
|
const GATEWAY = process.env.NODE_ENV === 'development'
|
||||||
? 'http://192.168.10.11:39101'
|
? 'http://192.168.10.11:39101'
|
||||||
: 'https://PROD_PLACEHOLDER' // 正式域名占位符,上线前必须替换(spec D2)
|
: 'https://aiotcloud.celex-technology.com' // 线上网关域名(2026-09-22 接入)
|
||||||
const USER_BASE = `${GATEWAY}/api/user`
|
const USER_BASE = `${GATEWAY}/api/user`
|
||||||
const BIZ_BASE = `${GATEWAY}/api/biz`
|
const BIZ_BASE = `${GATEWAY}/api/biz`
|
||||||
|
|
||||||
|
|
@ -122,13 +122,16 @@ export async function request<T>(
|
||||||
throw new BusinessError(body.Message || '业务错误', bizCode)
|
throw new BusinessError(body.Message || '业务错误', bizCode)
|
||||||
}
|
}
|
||||||
// 200 成功内的子状态(Message 携带,如 needUserInfo/noMobile),附到返回值供调用方分支处理
|
// 200 成功内的子状态(Message 携带,如 needUserInfo/noMobile),附到返回值供调用方分支处理
|
||||||
|
// 首登场景 Content 为 null(仅返回绑定凭证),需先初始化才能挂字段
|
||||||
const subState = body.Message
|
const subState = body.Message
|
||||||
if (subState && subState !== '') {
|
if (subState && subState !== '') {
|
||||||
|
if (!body.Content) body.Content = {} as T & { message?: string }
|
||||||
;(body.Content as Record<string, unknown>).message = subState
|
;(body.Content as Record<string, unknown>).message = subState
|
||||||
}
|
}
|
||||||
// 后端把登录 token 放响应头 Token(仅登录接口返回;Content.code 恒为 null)——附到返回值供登录流程落地
|
// 后端把登录 token 放响应头 Token(仅登录接口返回)——附到返回值供登录流程落地
|
||||||
const resToken = pickResponseToken(res.header)
|
const resToken = pickResponseToken(res.header)
|
||||||
if (resToken) {
|
if (resToken) {
|
||||||
|
if (!body.Content) body.Content = {} as T & { message?: string }
|
||||||
;(body.Content as Record<string, unknown>).token = resToken
|
;(body.Content as Record<string, unknown>).token = resToken
|
||||||
}
|
}
|
||||||
return body.Content
|
return body.Content
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { Navbar } from '@/components/navbar'
|
||||||
import { Loading } from '@/components/loading'
|
import { Loading } from '@/components/loading'
|
||||||
import { StateView } from '@/components/state-view'
|
import { StateView } from '@/components/state-view'
|
||||||
import { BottomSheet } from '@/components/bottom-sheet'
|
import { BottomSheet } from '@/components/bottom-sheet'
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getHomeDeviceList, type HomeDevice } from '@/api/homeDevice'
|
import { getHomeDeviceList, type HomeDevice } from '@/api/homeDevice'
|
||||||
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
|
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
|
||||||
import { getHomeList, type HomeInfo } from '@/api/home'
|
import { getHomeList, type HomeInfo } from '@/api/home'
|
||||||
|
|
@ -47,6 +49,11 @@ export default function DeviceList() {
|
||||||
const [rooms, setRooms] = useState<HomeRoom[]>([])
|
const [rooms, setRooms] = useState<HomeRoom[]>([])
|
||||||
const [filterRoomId, setFilterRoomId] = useState('')
|
const [filterRoomId, setFilterRoomId] = useState('')
|
||||||
|
|
||||||
|
// 游客浏览:未登录不拉数据,展示引导登录(微信审核要求不强制授权)
|
||||||
|
const isGuest = () =>
|
||||||
|
!store.getState().auth.token && !storage.get<string>('auth_token')
|
||||||
|
const isGuestEmpty = isGuest()
|
||||||
|
|
||||||
// 设备列表随家庭 + 房间筛选联动
|
// 设备列表随家庭 + 房间筛选联动
|
||||||
const loadDevices = useCallback(async (homeId?: string, roomId?: string) => {
|
const loadDevices = useCallback(async (homeId?: string, roomId?: string) => {
|
||||||
const targetHomeId = homeId ?? currentFamily?.homeId
|
const targetHomeId = homeId ?? currentFamily?.homeId
|
||||||
|
|
@ -109,6 +116,12 @@ export default function DeviceList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
|
if (isGuest()) {
|
||||||
|
setDeviceList([])
|
||||||
|
setLoading(false)
|
||||||
|
setLoadError(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
loadDevices(undefined, filterRoomId)
|
loadDevices(undefined, filterRoomId)
|
||||||
loadFamilies()
|
loadFamilies()
|
||||||
loadRooms()
|
loadRooms()
|
||||||
|
|
@ -124,6 +137,7 @@ export default function DeviceList() {
|
||||||
}
|
}
|
||||||
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
||||||
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
||||||
|
const goLogin = () => navigateTo(ROUTES.LOGIN)
|
||||||
|
|
||||||
const openFamilySheet = () => {
|
const openFamilySheet = () => {
|
||||||
setPendingFamily(currentFamily)
|
setPendingFamily(currentFamily)
|
||||||
|
|
@ -213,7 +227,7 @@ export default function DeviceList() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 「我的设备」分区标题:数量随筛选联动 */}
|
{/* 「我的设备」分区标题:数量随筛选联动 */}
|
||||||
{!loading && !loadError && (
|
{!loading && !loadError && !isGuestEmpty && (
|
||||||
<View className={styles.sectionTitle}>
|
<View className={styles.sectionTitle}>
|
||||||
<Text>我的设备</Text>
|
<Text>我的设备</Text>
|
||||||
<Text className={styles.sectionCount}>{deviceList.length}</Text>
|
<Text className={styles.sectionCount}>{deviceList.length}</Text>
|
||||||
|
|
@ -223,6 +237,8 @@ export default function DeviceList() {
|
||||||
{/* 设备网格 */}
|
{/* 设备网格 */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className={styles.stateWrap}><Loading /></View>
|
<View className={styles.stateWrap}><Loading /></View>
|
||||||
|
) : isGuestEmpty ? (
|
||||||
|
<StateView status="error" mode="list" text="登录后查看你的设备" retryText="去登录" onRetry={goLogin} />
|
||||||
) : loadError ? (
|
) : loadError ? (
|
||||||
<StateView status="error" onRetry={() => loadDevices()} />
|
<StateView status="error" onRetry={() => loadDevices()} />
|
||||||
) : deviceList.length === 0 ? (
|
) : deviceList.length === 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { Empty } from '@/components/empty'
|
import { Empty } from '@/components/empty'
|
||||||
|
import { store } from '@/store'
|
||||||
|
import { storage } from '@/utils/storage'
|
||||||
import { getHomeList, type HomeInfo } from '@/api/home'
|
import { getHomeList, type HomeInfo } from '@/api/home'
|
||||||
import { resolveCurrentHome } from '@/store/currentHome'
|
import { resolveCurrentHome } from '@/store/currentHome'
|
||||||
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
|
import { getRoomList, type HomeRoom } from '@/api/homeRoom'
|
||||||
|
|
@ -48,8 +50,15 @@ export default function Event() {
|
||||||
]
|
]
|
||||||
|
|
||||||
// 当前家庭 + 其房间列表:与智能设备页共享选择(resolveCurrentHome 读持久化),
|
// 当前家庭 + 其房间列表:与智能设备页共享选择(resolveCurrentHome 读持久化),
|
||||||
// 失败时保留"全部"选项不阻塞筛选
|
// 失败时保留"全部"选项不阻塞筛选;游客(未登录)跳过全部请求
|
||||||
|
const [guest, setGuest] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!store.getState().auth.token && !storage.get<string>('auth_token')) {
|
||||||
|
setGuest(true)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
getHomeList()
|
getHomeList()
|
||||||
.then(async (list) => {
|
.then(async (list) => {
|
||||||
const first = resolveCurrentHome(list)
|
const first = resolveCurrentHome(list)
|
||||||
|
|
@ -62,6 +71,7 @@ export default function Event() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
|
if (guest) return
|
||||||
// 进页首拉:home 就绪与否都调一次,home 为空时后端会报错并保持空态
|
// 进页首拉:home 就绪与否都调一次,home 为空时后端会报错并保持空态
|
||||||
if (home) loadAlarms(filters)
|
if (home) loadAlarms(filters)
|
||||||
})
|
})
|
||||||
|
|
@ -176,7 +186,16 @@ export default function Event() {
|
||||||
{/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */}
|
{/* 数据层:抽屉锚点(筛选栏下缘展开,遮罩盖住本层全部) */}
|
||||||
<View className={styles.contentArea}>
|
<View className={styles.contentArea}>
|
||||||
<View className={styles.contentBody}>
|
<View className={styles.contentBody}>
|
||||||
{alarmList.length === 0 ? (
|
{guest ? (
|
||||||
|
<>
|
||||||
|
<View className={styles.emptyArea}>
|
||||||
|
<Empty text="登录后查看告警消息" mode="message" />
|
||||||
|
</View>
|
||||||
|
<View className={styles.emptyHint}>
|
||||||
|
<Text>浏览功能无需登录,查看家人告警时再登录</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : alarmList.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
<View className={styles.emptyArea}>
|
<View className={styles.emptyArea}>
|
||||||
<Empty text="暂无告警数据" mode="message" />
|
<Empty text="暂无告警数据" mode="message" />
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,28 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 游客浏览引导登录按钮(微信审核要求:不强制授权,用户自行选择)
|
||||||
|
.guestLoginBtn {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 0 64px;
|
||||||
|
height: 80px;
|
||||||
|
background: $color-primary-grad;
|
||||||
|
border-radius: $radius-pill;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestLoginBtnPressed {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guestLoginText {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 家庭圈入口:渐变横幅卡 =====
|
// ===== 家庭圈入口:渐变横幅卡 =====
|
||||||
.familyEntry {
|
.familyEntry {
|
||||||
margin: 24px;
|
margin: 24px;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { getAlarmList, type HomeAlarm } from '@/api/homeAlarm'
|
||||||
import { store } from '@/store'
|
import { store } from '@/store'
|
||||||
import { storage } from '@/utils/storage'
|
import { storage } from '@/utils/storage'
|
||||||
import { ROUTES } from '@/constants/routes'
|
import { ROUTES } from '@/constants/routes'
|
||||||
import { navigateTo, reLaunch } from '@/utils/navigation'
|
import { navigateTo } from '@/utils/navigation'
|
||||||
import { splitAlerts } from './splitAlerts'
|
import { splitAlerts } from './splitAlerts'
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
|
@ -19,6 +19,8 @@ export default function Index() {
|
||||||
const [alertList, setAlertList] = useState<HomeAlarm[]>([])
|
const [alertList, setAlertList] = useState<HomeAlarm[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [loadError, setLoadError] = useState(false)
|
const [loadError, setLoadError] = useState(false)
|
||||||
|
// 游客浏览标记:未登录不拉数据、不弹错误,展示引导登录空态
|
||||||
|
const [guest, setGuest] = useState(false)
|
||||||
|
|
||||||
// 每次进入页面重拉默认家庭的全部告警(与事件页"全部"筛选同参数)
|
// 每次进入页面重拉默认家庭的全部告警(与事件页"全部"筛选同参数)
|
||||||
const loadAlerts = useCallback(async () => {
|
const loadAlerts = useCallback(async () => {
|
||||||
|
|
@ -41,12 +43,16 @@ export default function Index() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 冷启动直达兜底:小程序入口页不受 addInterceptor 保护,未登录时踢到登录页
|
// 每次进入页面重拉当前家庭告警;未登录(游客浏览)展示引导登录空态,不强制跳登录
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
if (!store.getState().auth.token && !storage.get<string>('auth_token')) {
|
if (!store.getState().auth.token && !storage.get<string>('auth_token')) {
|
||||||
reLaunch(ROUTES.LOGIN)
|
setAlertList([])
|
||||||
|
setLoading(false)
|
||||||
|
setLoadError(false)
|
||||||
|
setGuest(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setGuest(false)
|
||||||
loadAlerts()
|
loadAlerts()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -61,6 +67,13 @@ export default function Index() {
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className={styles.stateWrap}><Loading /></View>
|
<View className={styles.stateWrap}><Loading /></View>
|
||||||
|
) : guest ? (
|
||||||
|
<View className={styles.stateWrap}>
|
||||||
|
<Empty text="登录后查看家人动态" mode="message" subText="浏览功能无需登录,需要查看告警与设备时再登录" />
|
||||||
|
<View className={styles.guestLoginBtn} hoverClass={styles.guestLoginBtnPressed} onClick={() => navigateTo(ROUTES.LOGIN)}>
|
||||||
|
<Text className={styles.guestLoginText}>去登录</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
) : loadError ? (
|
) : loadError ? (
|
||||||
<StateView status="error" onRetry={loadAlerts} />
|
<StateView status="error" onRetry={loadAlerts} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,9 @@ export default function Login() {
|
||||||
// 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续
|
// 手机号授权暂缓(后端字段未定):提示并保持当前态,等后端确认后接续
|
||||||
throw new Error('后端要求绑定手机号,该流程暂未开通')
|
throw new Error('后端要求绑定手机号,该流程暂未开通')
|
||||||
}
|
}
|
||||||
// openId 兜底链:响应 openId → 本地缓存(needUserInfo 首次已写入)→ 当前 state
|
// needUserInfo 实测 Content 为 null(无 openId),授权重调靠新 code 即可,
|
||||||
const openIdNext =
|
// openId 仅作有则传的加速字段,缺失不再阻断
|
||||||
result.openId ?? storage.get<string>(WX_OPENID_STORAGE_KEY) ?? openId
|
setOpenId(result.openId ?? storage.get<string>(WX_OPENID_STORAGE_KEY) ?? '')
|
||||||
if (!openIdNext) throw new Error('响应缺少 openId,无法继续授权')
|
|
||||||
setOpenId(openIdNext)
|
|
||||||
setNeedAuth(true)
|
setNeedAuth(true)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
|
|
@ -85,14 +83,15 @@ export default function Login() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// needUserInfo:getUserInfo 授权用户资料(后端文档约定的授权接口)
|
// needUserInfo 二次授权:wx.getUserInfo 拿 iv/encryptedData(后端文档约定,
|
||||||
|
// getUserProfile 的资料后端不认;getUserInfo 不受 tap 上下文限制,授权弹窗由微信拉起)
|
||||||
const handleAuthUserInfo = async () => {
|
const handleAuthUserInfo = async () => {
|
||||||
if (loading || !openId) return
|
if (loading) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
applyResult(await completeUserInfoLogin(openId))
|
applyResult(await completeUserInfoLogin(openId || undefined))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 授权失败/取消:保留待授权态,用户可重试
|
// 授权/登录失败:保留待授权态,用户可重试
|
||||||
console.error('[login-page] completeUserInfoLogin 失败', e)
|
console.error('[login-page] completeUserInfoLogin 失败', e)
|
||||||
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
|
toast.error(e instanceof Error && e.message ? e.message : '授权失败')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,9 @@ export async function performWxLogin(): Promise<WxMiniappLoginResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重调登录接口的公共尾部:换新 code(一次性)→ 组装参数 → 调用 → 缓存 openId
|
// 重调登录接口的公共尾部:换新 code(一次性)→ 组装参数 → 调用 → 缓存 openId
|
||||||
|
// openId 可缺省:needUserInfo 首登响应 Content 为 null(无 openId),靠 code + 授权资料即可重调
|
||||||
async function reloginWith(
|
async function reloginWith(
|
||||||
openId: string,
|
openId: string | undefined,
|
||||||
extra: { iv?: string; encryptedData?: string; phoneCode?: string }
|
extra: { iv?: string; encryptedData?: string; phoneCode?: string }
|
||||||
): Promise<WxMiniappLoginResponse> {
|
): Promise<WxMiniappLoginResponse> {
|
||||||
let code: string
|
let code: string
|
||||||
|
|
@ -52,7 +53,7 @@ async function reloginWith(
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('获取微信登录凭证失败,请重试')
|
throw new Error('获取微信登录凭证失败,请重试')
|
||||||
}
|
}
|
||||||
const params = { appId: resolveAppId(), code, openId, ...extra }
|
const params = { appId: resolveAppId(), code, ...(openId ? { openId } : {}), ...extra }
|
||||||
const result = await wxMiniappLogin(params)
|
const result = await wxMiniappLogin(params)
|
||||||
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId)
|
||||||
return result
|
return result
|
||||||
|
|
@ -60,7 +61,7 @@ async function reloginWith(
|
||||||
|
|
||||||
// needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料
|
// needUserInfo 二次授权:getUserInfo 拿 iv/encryptedData → 重调登录接口补用户资料
|
||||||
// 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认)
|
// 后端文档约定:wx.login 返回 needUserInfo 时须用 wx.getUserInfo 授权(getUserProfile 拿的资料后端不认)
|
||||||
export async function completeUserInfoLogin(openId: string): Promise<WxMiniappLoginResponse> {
|
export async function completeUserInfoLogin(openId: string | undefined): Promise<WxMiniappLoginResponse> {
|
||||||
let profile: { iv: string; encryptedData: string }
|
let profile: { iv: string; encryptedData: string }
|
||||||
try {
|
try {
|
||||||
const r = await Taro.getUserInfo({ withCredentials: true })
|
const r = await Taro.getUserInfo({ withCredentials: true })
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,20 @@ export default function Mine() {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className={styles.menuCard}>
|
<View className={styles.menuCard}>
|
||||||
<ListRowGroup>
|
{loggedOut ? (
|
||||||
<ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} />
|
<ListRowGroup>
|
||||||
<ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} />
|
{/* 游客态:菜单收起,只留一个登录入口(微信审核要求不强制授权) */}
|
||||||
<ListRow label="手机号" value={user?.mobile || '未绑定'} arrow />
|
<ListRow label="登录后使用完整功能" arrow onClick={goLogin} />
|
||||||
<ListRow label="当前版本" value="2.8.44" />
|
</ListRowGroup>
|
||||||
<ListRow label="退出登录" arrow danger onClick={logout} />
|
) : (
|
||||||
</ListRowGroup>
|
<ListRowGroup>
|
||||||
|
<ListRow label="配置设备" arrow onClick={() => navigateTo(ROUTES.DEVICE_BLE_CONFIG)} />
|
||||||
|
<ListRow label="场所管理" arrow onClick={() => navigateTo(ROUTES.SCENE_SELECT)} />
|
||||||
|
<ListRow label="手机号" value={user?.mobile || '未绑定'} arrow />
|
||||||
|
<ListRow label="当前版本" value="2.8.44" />
|
||||||
|
<ListRow label="退出登录" arrow danger onClick={logout} />
|
||||||
|
</ListRowGroup>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,15 @@ import { ROUTES } from '@/constants/routes'
|
||||||
// 在拦截器内改写 url 会把 http 请求地址误改成页面路径(request:fail invalid url),
|
// 在拦截器内改写 url 会把 http 请求地址误改成页面路径(request:fail invalid url),
|
||||||
// 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。
|
// 故不用全局拦截器,改为在 navigation 统一出口做跳转前校验。
|
||||||
|
|
||||||
// 登录页及无需登录即可访问的页面(放行名单)
|
// 游客可浏览页面(微信审核要求:进入首页即要求授权登录会被拒,
|
||||||
export const PUBLIC_PAGES = new Set<string>([ROUTES.LOGIN])
|
// tab 页与登录页放开,操作型页面按需登录)
|
||||||
|
export const PUBLIC_PAGES = new Set<string>([
|
||||||
|
ROUTES.LOGIN,
|
||||||
|
ROUTES.HOME,
|
||||||
|
ROUTES.DEVICE_LIST,
|
||||||
|
ROUTES.EVENT,
|
||||||
|
ROUTES.MINE
|
||||||
|
])
|
||||||
|
|
||||||
// 登录页路径(navigation 守卫改投目标)
|
// 登录页路径(navigation 守卫改投目标)
|
||||||
export const ROUTES_LOGIN = ROUTES.LOGIN
|
export const ROUTES_LOGIN = ROUTES.LOGIN
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,8 @@ describe('needUserInfo 首次登录分支', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('completeUserInfoLogin 二次授权', () => {
|
describe('completeUserInfoLogin 二次授权', () => {
|
||||||
|
// 2026-09-24 定稿:内部走 wx.getUserInfo 取资料(后端不认 getUserProfile),
|
||||||
|
// openId 可缺省(needUserInfo 首登响应 Content 为 null)
|
||||||
it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => {
|
it('getUserInfo(withCredentials) → 换新 code → 带 iv/encryptedData/openId 重调登录', async () => {
|
||||||
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
|
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
|
||||||
mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' }))
|
mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-need' }))
|
||||||
|
|
@ -151,6 +153,18 @@ describe('completeUserInfoLogin 二次授权', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('openId 缺省时请求体不带 openId 字段(needUserInfo 首登无 openId 场景)', async () => {
|
||||||
|
mockGetUserInfo.mockResolvedValue({ iv: 'iv-1', encryptedData: 'enc-1' })
|
||||||
|
mockWxMiniappLogin.mockResolvedValue(result())
|
||||||
|
await completeUserInfoLogin(undefined)
|
||||||
|
expect(mockWxMiniappLogin).toHaveBeenCalledWith({
|
||||||
|
appId: 'wx-appid-live',
|
||||||
|
code: 'wx-code-1',
|
||||||
|
iv: 'iv-1',
|
||||||
|
encryptedData: 'enc-1'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => {
|
it('用户拒绝授权(errMsg 形状 reject)抛带 message 的 Error', async () => {
|
||||||
mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' })
|
mockGetUserInfo.mockRejectedValue({ errMsg: 'getUserInfo:fail auth deny' })
|
||||||
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny')
|
await expect(completeUserInfoLogin('o-need')).rejects.toThrow('getUserInfo:fail auth deny')
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,16 @@ describe('navigation 登录守卫', () => {
|
||||||
expect(mockNavigateTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
expect(mockNavigateTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('未登录 reLaunch/redirectTo/switchTab 同样被守卫', () => {
|
it('未登录 reLaunch/redirectTo 到受保护页被守卫(tab 页在游客白名单放行)', () => {
|
||||||
reLaunch(ROUTES.HOME)
|
reLaunch(ROUTES.DEVICE_ADD)
|
||||||
redirectTo(ROUTES.DEVICE_ADD)
|
redirectTo(ROUTES.FAMILY)
|
||||||
switchTab(ROUTES.EVENT)
|
|
||||||
expect(mockReLaunch).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
expect(mockReLaunch).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
||||||
expect(mockRedirectTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
expect(mockRedirectTo).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
||||||
expect(mockSwitchTab).toHaveBeenCalledWith({ url: ROUTES.LOGIN })
|
})
|
||||||
|
|
||||||
|
it('未登录 switchTab 到 tab 页:放行(游客模式白名单,微信审核要求)', () => {
|
||||||
|
switchTab(ROUTES.EVENT)
|
||||||
|
expect(mockSwitchTab).toHaveBeenCalledWith({ url: ROUTES.EVENT })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('已登录 navigateTo:原样放行', () => {
|
it('已登录 navigateTo:原样放行', () => {
|
||||||
|
|
@ -85,9 +88,13 @@ describe('navigation 登录守卫', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isProtectedPage', () => {
|
describe('isProtectedPage', () => {
|
||||||
it('登录页返回 false,其余页面返回 true', () => {
|
it('登录页与四个 tab 页返回 false(游客可浏览),其余页面返回 true', () => {
|
||||||
expect(isProtectedPage(ROUTES.LOGIN)).toBe(false)
|
expect(isProtectedPage(ROUTES.LOGIN)).toBe(false)
|
||||||
expect(isProtectedPage(ROUTES.HOME)).toBe(true)
|
expect(isProtectedPage(ROUTES.HOME)).toBe(false)
|
||||||
|
expect(isProtectedPage(ROUTES.DEVICE_LIST)).toBe(false)
|
||||||
|
expect(isProtectedPage(ROUTES.EVENT)).toBe(false)
|
||||||
|
expect(isProtectedPage(ROUTES.MINE)).toBe(false)
|
||||||
|
expect(isProtectedPage(ROUTES.FAMILY)).toBe(true)
|
||||||
expect(isProtectedPage(`${ROUTES.LOGIN}?from=xxx`)).toBe(false)
|
expect(isProtectedPage(`${ROUTES.LOGIN}?from=xxx`)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue