feat(auth): 冷启动登录态校验 + 设备/房间列表样式打磨

- 新增 getLoginInfo(/v0/app/users/getLoginInfo)冷启动校验 token 有效性,
  http 层将 /v0/ 前缀路由到 user 服务;登录页 useDidShow 恢复 userInfo
  后落地,restoring 态防并发登录
- 详情页指标卡配色从数值移到标签(对齐 web 端 MetricCard),
  心率图标加底、设备信息卡补"设备信息"标题
- 设备列表新增"我的设备 N"分区标题,功能入口图标加圆底
- 房间列表改整卡圆角+行内分隔,删除按钮改浅红胶囊
- global.d.ts 补 png/jpg 模块声明
This commit is contained in:
ozh 2026-09-21 17:08:52 +08:00
parent 9f3ea3d82b
commit 40783ccabe
10 changed files with 213 additions and 47 deletions

View File

@ -48,3 +48,9 @@ export function wxMiniappLogin(params: WxMiniappLoginParams): Promise<WxMiniappL
{ needAuth: false }
)
}
// 冷启动登录态校验:token 有效返回登录信息,过期返回 202 由 http 层统一踢登录页。
// 响应 Content 结构(ResponseData<UserInfo>)联调实测后修正字段
export function getLoginInfo(): Promise<WxUserInfo> {
return request.get('/v0/app/users/getLoginInfo')
}

View File

@ -72,8 +72,8 @@ export async function request<T>(
if (tokenAtStart) headers.token = tokenAtStart
}
// 登录接口走 user 服务前缀,其余业务接口走 biz 前缀
const base = url.startsWith('/thirdPlatform/') ? USER_BASE : BIZ_BASE
// 登录接口与 users 系列冷启动校验接口走 user 服务前缀,其余业务接口走 biz 前缀
const base = url.startsWith('/thirdPlatform/') || url.startsWith('/v0/') ? USER_BASE : BIZ_BASE
let res: Taro.request.SuccessCallbackResult<ApiEnvelope<T>>
try {

View File

@ -8,7 +8,8 @@
margin: 24px;
padding: 40px 32px;
background: $color-bg-white;
border-radius: $radius-md;
border-radius: $radius-lg;
box-shadow: $shadow-card;
}
.sectionTitle {
@ -32,11 +33,17 @@
height: 96px;
padding: 0 32px;
background: $color-bg-gray;
border: 1px solid transparent;
border-radius: $radius-md;
font-size: 28px;
color: $color-text;
box-sizing: border-box;
margin-bottom: 32px;
&:focus {
border-color: $color-primary;
background: $color-bg-white;
}
}
// 安装房间选择:label + 横向 chip 组

View File

@ -13,11 +13,12 @@
.heartCard {
margin: 24px;
padding: 32px;
background: $color-bg-white;
background: linear-gradient(135deg, #fff 0%, #fdf7f6 100%);
border-radius: $radius-lg;
box-shadow: $shadow-card;
display: flex;
align-items: flex-start;
border: 1px solid rgba(245, 34, 45, 0.06);
}
.heartLeft {
@ -27,7 +28,22 @@
min-width: 200px;
}
.heartIcon { font-size: 72px; color: $color-danger; line-height: 1; }
// 心形图标:浅红圆底衬托,弱化大面积实色
.heartIconWrap {
width: 88px;
height: 88px;
border-radius: $radius-circle;
background: $color-danger-light-bg;
display: flex;
align-items: center;
justify-content: center;
}
.heartIcon {
font-size: 48px;
color: $color-danger;
line-height: 1;
}
.heartValue {
margin-top: 16px;
@ -87,21 +103,45 @@
width: 48.5%;
box-sizing: border-box;
margin-bottom: 20px;
padding: 24px;
padding: 28px 24px;
background: $color-bg-white;
border-radius: $radius-md;
box-shadow: $shadow-card;
}
.metricLabel { font-size: 26px; color: $color-text-secondary; }
// 指标名左侧小色条:六卡各自的语义色,替代大数字整体染色
.metricLabel {
display: flex;
align-items: center;
font-size: 26px;
color: $color-text-secondary;
&::before {
content: '';
width: 8px;
height: 24px;
border-radius: $radius-pill;
margin-right: 12px;
background: $color-text-placeholder;
}
}
.metricLabelBlue::before { background: $color-info; }
.metricLabelOrange::before { background: $color-warning; }
.metricLabelGreen::before { background: $color-success; }
.metricLabelRed::before { background: $color-danger; }
.metricRow {
margin-top: 12px;
margin-top: 14px;
display: flex;
align-items: baseline;
}
.metricValue { font-size: 44px; font-weight: 700; }
.metricValue {
font-size: 44px;
font-weight: 700;
color: $color-text;
}
.metricBlue { color: $color-primary-deep; }
.metricOrange { color: #fa8c16; }
@ -114,6 +154,15 @@
color: $color-text-tertiary;
}
// 设备信息卡分区标题
.infoTitle {
display: block;
margin: 24px 24px 16px;
font-size: 30px;
font-weight: 600;
color: $color-text;
}
.infoCard {
margin: 4px 24px 24px;
}

View File

@ -127,7 +127,9 @@ export default function Detail() {
<View className={styles.heartCard}>
<View className={styles.heartLeft}>
<Text className={styles.heartIcon}>❤</Text>
<View className={styles.heartIconWrap}>
<Text className={styles.heartIcon}>❤</Text>
</View>
<Text className={styles.heartValue}>{fmt(signs?.heartRate)}</Text>
<Text className={styles.heartUnit}>BPM</Text>
<View className={`${styles.badge} ${online ? styles.badgeOn : styles.badgeOff}`}>
@ -146,45 +148,46 @@ export default function Detail() {
<View className={styles.metricGrid}>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>呼吸率</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelBlue}`}>呼吸率</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricBlue}`}>{fmt(signs?.respiratoryRate)}</Text>
<Text className={styles.metricValue}>{fmt(signs?.respiratoryRate)}</Text>
<Text className={styles.metricUnit}>/min</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>体动</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelOrange}`}>体动</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricOrange}`}>{fmt(signs?.bodyMovement)}</Text>
<Text className={styles.metricValue}>{fmt(signs?.bodyMovement)}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>运动状态</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelGreen}`}>运动状态</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricGreen}`}>{MOVE_STATE_MAP[signs?.moveState ?? -1] ?? '--'}</Text>
<Text className={styles.metricValue}>{MOVE_STATE_MAP[signs?.moveState ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>在床状态</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelRed}`}>在床状态</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricRed}`}>{ONBED_STATUS_MAP[signs?.onbedStatus ?? -1] ?? '--'}</Text>
<Text className={styles.metricValue}>{ONBED_STATUS_MAP[signs?.onbedStatus ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>人体状态</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelBlue}`}>人体状态</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricBlue}`}>{BODY_STATUS_MAP[signs?.bodyStatus ?? -1] ?? '--'}</Text>
<Text className={styles.metricValue}>{BODY_STATUS_MAP[signs?.bodyStatus ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={styles.metricLabel}>人体位置</Text>
<Text className={`${styles.metricLabel} ${styles.metricLabelGreen}`}>人体位置</Text>
<View className={styles.metricRow}>
<Text className={`${styles.metricValue} ${styles.metricGreen}`}>{fmt(signs?.bodyPosition)}</Text>
<Text className={styles.metricValue}>{fmt(signs?.bodyPosition)}</Text>
<Text className={styles.metricUnit}>分米</Text>
</View>
</View>
</View>
<Text className={styles.infoTitle}>设备信息</Text>
<View className={styles.infoCard}>
<ListRowGroup>
<ListRow label="设备型号" value={deviceModel || '-'} />

View File

@ -15,7 +15,7 @@
// 绿色衬底:占位高度与浮卡上浮量(-40px)匹配,卡片叠在它上面而不是压进导航栏
.heroSpace {
height: 64px;
background: #2ccb98;
background: linear-gradient(180deg, #2ccb98 0%, #35d6a5 100%);
margin-bottom: -40px;
position: relative;
z-index: 1;
@ -27,7 +27,7 @@
padding: 8px 32px 32px;
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
box-shadow: 0 8px 24px rgba(44, 203, 152, 0.12);
position: relative;
z-index: 2;
}
@ -38,6 +38,10 @@
align-items: center;
justify-content: space-between;
border-bottom: 1px solid $color-border;
&:last-of-type {
border-bottom: none;
}
}
.familyLabel {
@ -75,7 +79,7 @@
// 房间管理 / 添加设备 两张并排功能卡
.funcRow {
display: flex;
padding: 24px;
padding: 24px 24px 0;
gap: 24px;
}
@ -86,20 +90,30 @@
border-radius: $radius-md;
box-shadow: $shadow-card;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
.funcCardPressed {
background: $color-bg-press;
}
// 左侧圆角浅绿图标底:延续「左图标色块」卡片语言
.funcIconWrap {
width: 88px;
height: 88px;
border-radius: $radius-md;
background: $color-primary-light-bg;
display: flex;
align-items: center;
justify-content: center;
}
.funcIcon {
font-size: 56px;
font-size: 48px;
color: $color-primary;
line-height: 1;
margin-bottom: 12px;
}
.funcText {
@ -113,7 +127,7 @@
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
padding: 0 24px 24px;
padding: 24px 24px 8px;
gap: 16px;
}
@ -121,16 +135,40 @@
flex-shrink: 0;
padding: 12px 32px;
background: $color-bg-white;
border: 1px solid $color-border;
border-radius: $radius-pill;
box-shadow: $shadow-card;
font-size: 26px;
color: $color-text-secondary;
}
.roomChipActive {
background: $color-primary-grad;
border-color: transparent;
}
.roomChipTextActive {
color: #ffffff;
font-weight: 600;
}
// 「我的设备」分区标题:筛选条与网格之间的视觉锚点
.sectionTitle {
display: flex;
align-items: center;
padding: 24px 24px 16px;
font-size: 30px;
font-weight: 600;
color: $color-text;
.sectionCount {
margin-left: 12px;
padding: 2px 16px;
background: $color-primary-light-bg;
border-radius: $radius-pill;
font-size: 22px;
font-weight: 500;
color: $color-primary-deep;
}
}
// 设备网格
@ -144,7 +182,7 @@
.card {
width: 48.5%;
margin-bottom: 24px;
padding: 36px 20px 24px;
padding: 36px 20px 28px;
box-sizing: border-box;
background: $color-bg-white;
border-radius: $radius-lg;
@ -152,6 +190,11 @@
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid transparent;
&:active {
border-color: $color-primary-light-border;
}
}
// 上半区(图标/名称/状态)点击进详情,下方解绑按钮独立命中
@ -166,13 +209,14 @@
width: 108px;
height: 108px;
padding: 14px;
border-radius: $radius-circle;
border-radius: 32px;
background: $color-primary-grad;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
box-sizing: border-box;
box-shadow: 0 6px 16px rgba(44, 203, 152, 0.25);
}
.iconOffline { background: $color-bg-disabled; }
@ -228,13 +272,14 @@
.sheetItem {
padding: 24px 32px;
background: $color-bg-gray;
border: 2px solid transparent;
border-radius: $radius-md;
margin-bottom: 16px;
}
.sheetItemActive {
background: $color-primary-light-bg;
border: 2px solid $color-primary;
border-color: $color-primary;
}
.sheetHead {

View File

@ -182,11 +182,15 @@ export default function DeviceList() {
{/* 房间管理 + 添加设备 两张并排 */}
<View className={styles.funcRow}>
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goRoom}>
<Text className={styles.funcIcon}>⌂</Text>
<View className={styles.funcIconWrap}>
<Text className={styles.funcIcon}>⌂</Text>
</View>
<Text className={styles.funcText}>房间管理</Text>
</View>
<View className={styles.funcCard} hoverClass={styles.funcCardPressed} onClick={goAdd}>
<Text className={styles.funcIcon}>+</Text>
<View className={styles.funcIconWrap}>
<Text className={styles.funcIcon}>+</Text>
</View>
<Text className={styles.funcText}>添加设备</Text>
</View>
</View>
@ -215,6 +219,14 @@ export default function DeviceList() {
</View>
)}
{/* 「我的设备」分区标题:数量随筛选联动 */}
{!loading && !loadError && (
<View className={styles.sectionTitle}>
<Text>我的设备</Text>
<Text className={styles.sectionCount}>{deviceList.length}</Text>
</View>
)}
{/* 设备网格 */}
{loading ? (
<View className={styles.stateWrap}><Loading /></View>

View File

@ -4,6 +4,7 @@ import { useRouter, useDidShow } from '@tarojs/taro'
import { useAppDispatch, useAppSelector } from '@/store/hooks'
import { setAuth } from '@/store/authSlice'
import { performWxLogin, completeUserInfoLogin, getLoginSubState } from './wxLoginFlow'
import { getLoginInfo } from '@/api/auth'
import { useToast } from '@/hooks/useToast'
import { ROUTES } from '@/constants/routes'
import { reLaunch } from '@/utils/navigation'
@ -20,6 +21,8 @@ export default function Login() {
// 注册中间态:needUserInfo(待用户资料授权)→ null(初始/完成)
const [needAuth, setNeedAuth] = useState(false)
const [openId, setOpenId] = useState('')
// 冷启动校验进行中:阻止 useDidShow 与按钮登录并发
const [restoring, setRestoring] = useState(false)
// 登录后落地页:携带 redirect 参数时优先回跳(守卫踢登录/分享卡直达场景),否则进首页
const afterLoginUrl = () => {
@ -28,9 +31,20 @@ export default function Login() {
return target || ROUTES.HOME
}
// 已有 token 直接进首页(本期不做 checkLogin 校验,spec D5)
// 冷启动 token 有效性校验:有效则恢复 userInfo(内存态,重启即失)后进首页;
// 无效 getLoginInfo 返回 202,http 层统一 clearAuth + 踢回本页,此处无需分支
useDidShow(() => {
if (token && !loading) reLaunch(afterLoginUrl())
if (!token || loading || restoring) return
setRestoring(true)
getLoginInfo()
.then((userInfo) => {
dispatch(setAuth({ token, userInfo }))
return reLaunch(afterLoginUrl())
})
.catch(() => {
// 202 分支 http 层已踢回本页(reLaunch 目标即本页,幂等);其余异常保持登录页可手动登录
})
.finally(() => setRestoring(false))
})
// 完整登录态校验与落地;未完成时推进授权态
@ -61,7 +75,7 @@ export default function Login() {
}
const handleWxLogin = async () => {
if (loading) return
if (loading || restoring) return
setLoading(true)
try {
applyResult(await performWxLogin())
@ -102,7 +116,7 @@ export default function Login() {
onClick={needAuth ? handleAuthUserInfo : handleWxLogin}
>
<Text className={styles.submitText}>
{loading ? '登录中...' : needAuth ? '授权用户信息,继续' : '微信一键登录'}
{loading ? '登录中...' : restoring ? '恢复登录...' : needAuth ? '授权用户信息,继续' : '微信一键登录'}
</Text>
</View>
</View>

View File

@ -11,16 +11,26 @@
align-items: center;
}
.list { margin-top: 20px; }
// 房间列表:整体一张圆角卡,行内分隔
.list {
margin: 24px;
background: $color-bg-white;
border-radius: $radius-lg;
box-shadow: $shadow-card;
overflow: hidden;
}
.item {
min-height: 104px;
min-height: 112px;
padding: 16px 32px;
margin-bottom: 2px;
background: $color-bg-white;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid $color-border;
&:last-of-type {
border-bottom: none;
}
}
// 名称 + 场景标签组:点击进编辑
@ -50,20 +60,24 @@
color: $color-primary-deep;
}
// 删除操作:胶囊浅红底,弱化破坏性但保持可识别
.del {
flex-shrink: 0;
margin-left: 24px;
font-size: 26px;
padding: 8px 24px;
background: $color-danger-light-bg;
border-radius: $radius-pill;
font-size: 24px;
color: $color-danger;
}
// 添加房间:虚线边框卡,与家庭圈添加卡风格一致
.addBtn {
margin: 24px 24px 0;
margin: 24px;
height: 104px;
border: 1px dashed $color-primary-light-border;
border: 2px dashed $color-primary-light-border;
border-radius: $radius-md;
background: $color-bg-white;
background: $color-primary-light-bg;
display: flex;
align-items: center;
justify-content: center;
@ -104,10 +118,16 @@
height: 88px;
padding: 0 32px;
background: $color-bg-gray;
border: 1px solid transparent;
border-radius: $radius-md;
font-size: 28px;
color: $color-text;
box-sizing: border-box;
&:focus {
border-color: $color-primary;
background: $color-bg-white;
}
}
.senceGrid {

10
src/types/global.d.ts vendored
View File

@ -2,3 +2,13 @@ declare module '*.module.scss' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.png' {
const src: string
export default src
}
declare module '*.jpg' {
const src: string
export default src
}