集成家长短信通知

This commit is contained in:
stu2not
2026-06-03 15:14:01 +08:00
parent c4d977ce99
commit 482fbf351f
13 changed files with 1027 additions and 23 deletions

View File

@@ -44,6 +44,7 @@
.user-info {
flex: 1;
min-width: 0;
.user-name {
font-size: 34px;
@@ -51,6 +52,15 @@
color: #1A1A1A;
display: block;
}
.user-phone {
display: block;
margin-top: 8px;
font-size: 24px;
line-height: 1.4;
color: #666666;
word-break: break-all;
}
}
.verified-badge {
@@ -64,6 +74,32 @@
font-weight: 500;
}
}
.phone-auth-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 150px;
height: 60px;
margin: 0;
padding: 0;
border-radius: 14px;
background: #FF8C42;
color: #FFFFFF;
font-size: 24px;
font-weight: 600;
line-height: 60px;
&::after {
border: none;
}
&[disabled] {
background: #CBD5E1;
color: #FFFFFF;
}
}
}
.menu-card {

View File

@@ -1,7 +1,7 @@
import { View, Text, Image, Input, Button } from '@tarojs/components'
import { useState } from 'react'
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
import { clearToken, getToken } from '@/services/auth'
import { authorizeParentPhone, clearToken, getParent, getToken, Parent } from '@/services/auth'
import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
import { getCurrentUserId } from '@/services/session'
import {
@@ -72,7 +72,8 @@ export default function Sleep() {
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
const [childName, setChildName] = useState('')
const [editingChildId, setEditingChildId] = useState<number | null>(null)
const [parentInfo, setParentInfo] = useState<{ nickname?: string; avatar_url?: string }>({})
const [parentInfo, setParentInfo] = useState<Parent | null>(null)
const [isAuthorizingPhone, setIsAuthorizingPhone] = useState(false)
useDidShow(() => {
void loadData()
@@ -95,12 +96,14 @@ export default function Sleep() {
setLoading(true)
try {
const context = await loadCurrentChildBindingContext()
const currentUserId = getCurrentUserId()
const currentParent = currentUserId ? await loadCurrentParent(currentUserId) : null
const nextBinding = (context.currentBinding as BindingListItem | null) || null
setChildren(context.children)
setBindings(context.bindings as BindingListItem[])
setCurrentChild(context.currentChild)
setBinding(nextBinding)
setParentInfo(Taro.getStorageSync('userInfo') || {})
setParentInfo(currentParent)
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
@@ -115,6 +118,15 @@ export default function Sleep() {
}
}
const loadCurrentParent = async (userId: number): Promise<Parent | null> => {
try {
return await getParent(userId)
} catch (error) {
console.warn('[manage] parent profile load failed:', error)
return null
}
}
const currentChildName = currentChild?.child_name || '未设置'
const loadFirmwareStatus = async (deviceId?: string | null) => {
@@ -526,6 +538,45 @@ export default function Sleep() {
})
}
const handleAuthorizePhone = async (event: any) => {
if (isAuthorizingPhone) return
const phoneCode = String(event?.detail?.code || '').trim()
if (!phoneCode) {
const errMsg = String(event?.detail?.errMsg || '')
Taro.showToast({
title: errMsg.includes('deny') || errMsg.includes('fail') ? '未授权手机号' : '未获取到手机号授权',
icon: 'none',
})
return
}
setIsAuthorizingPhone(true)
Taro.showLoading({ title: '保存中...' })
try {
const parent = await authorizeParentPhone(phoneCode)
setParentInfo(parent)
const cachedUserInfo = (Taro.getStorageSync('userInfo') || {}) as {
nickname?: string
avatar_url?: string
}
Taro.setStorageSync('userInfo', {
...cachedUserInfo,
nickname: parent.nickname || cachedUserInfo.nickname,
avatar_url: parent.avatar_url || cachedUserInfo.avatar_url,
})
Taro.showToast({ title: '短信通知已开启', icon: 'success' })
} catch (error: any) {
console.error('[manage] phone authorization failed:', error)
Taro.showToast({
title: error?.message || '手机号保存失败',
icon: 'none',
})
} finally {
Taro.hideLoading()
setIsAuthorizingPhone(false)
}
}
const handleMenuClick = (item: MenuItem) => {
if (item.disabled) return
@@ -589,7 +640,9 @@ export default function Sleep() {
)
}
const parentDisplayName = parentInfo.nickname?.trim() || '家长'
const parentDisplayName = parentInfo?.nickname?.trim() || '家长'
const parentPhone = String(parentInfo?.phone || '').trim()
const parentPhoneLabel = parentPhone ? `${parentPhone.slice(0, 3)}****${parentPhone.slice(-4)}` : '未开启'
const unassignedBindings = bindings.filter((item) => item.device_id && item.child_id === null)
const currentFirmwareLabel = firmwareStatus?.current_version || '--'
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
@@ -694,7 +747,7 @@ export default function Sleep() {
<View className='user-card'>
<View className='user-avatar'>
{parentInfo.avatar_url ? (
{parentInfo?.avatar_url ? (
<Image className='avatar-img image-avatar' src={parentInfo.avatar_url} mode='aspectFill' />
) : (
<Text className='avatar-img'>👤</Text>
@@ -702,10 +755,23 @@ export default function Sleep() {
</View>
<View className='user-info'>
<Text className='user-name'>{parentDisplayName}</Text>
<Text className='user-phone'>{parentPhoneLabel}</Text>
</View>
<View className='verified-badge'>
<Text>{currentChild ? '已选择当前孩子' : '未选择孩子'}</Text>
</View>
{parentPhone ? (
<View className='verified-badge'>
<Text></Text>
</View>
) : (
<Button
className='phone-auth-btn'
openType='getPhoneNumber'
loading={isAuthorizingPhone}
disabled={isAuthorizingPhone}
onGetPhoneNumber={handleAuthorizePhone}
>
</Button>
)}
</View>
<View className='summary-card'>

View File

@@ -45,7 +45,7 @@ export async function getParent(userId: number): Promise<Parent> {
export async function updateParent(
userId: number,
data: { nickname?: string; avatar_url?: string; phone?: string }
data: { nickname?: string; avatar_url?: string }
): Promise<Parent> {
return request<Parent>(`/banban/parents/${userId}`, {
method: 'PATCH',
@@ -53,6 +53,13 @@ export async function updateParent(
})
}
export async function authorizeParentPhone(code: string): Promise<Parent> {
return request<Parent>('/banban/parents/me/phone', {
method: 'POST',
data: { code },
})
}
export function getToken(): string {
return getStoredToken()
}