新增小程序后端代码,包括数据库、路由、服务层等。

小程序前后端打通,包括登录、注册、绑定设备、查询绑定信息,修改小朋友名称等功能。
This commit is contained in:
HycJack
2026-04-13 01:54:57 +08:00
parent 33715373b0
commit a22fcafea9
46 changed files with 3713 additions and 268 deletions

View File

@@ -6,6 +6,67 @@
padding-bottom: calc(120px + env(safe-area-inset-bottom));
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
font-size: 30px;
color: #999999;
}
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 70%;
font-size: 30px;
color: #999999;
padding: 0 48px;
.empty-title {
font-size: 36px;
color: #1A1A1A;
font-weight: 600;
margin-bottom: 12px;
}
.empty-desc {
font-size: 26px;
color: #999999;
margin-bottom: 48px;
}
.empty-input-wrap {
width: 100%;
margin-bottom: 24px;
.empty-input {
background: #F5F7FA;
border-radius: 12px;
padding: 24px;
font-size: 30px;
width: 100%;
height: 96px;
box-sizing: border-box;
text-align: center;
}
}
.empty-btn {
background: #FF8C42;
border-radius: 12px;
padding: 24px 64px;
.empty-btn-text {
color: #FFFFFF;
font-size: 32px;
font-weight: 500;
}
}
}
.icon-bg {
width: 72px;
height: 72px;

View File

@@ -1,27 +1,59 @@
import { View, Text, Button, Image } from '@tarojs/components'
import { View, Text, Button, Image, Input } from '@tarojs/components'
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { getToken } from '@/services/api'
import { getCurrentBinding, directBind } from '@/services/binding'
import { getChildren, createChild } from '@/services/child'
import './index.scss'
interface Child {
child_id: number
child_name: string
}
export default function Bind() {
const [isScanning, setIsScanning] = useState(false)
const [loading, setLoading] = useState(true)
const [children, setChildren] = useState<Child[]>([])
const [newChildName, setNewChildName] = useState('')
const [saving, setSaving] = useState(false)
useEffect(() => {
// 检查是否已登录
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
if (!isLoggedIn) {
// 未登录,跳转到登录页面
checkAuth()
}, [])
const checkAuth = async () => {
const token = getToken()
if (!token) {
Taro.redirectTo({ url: '/pages/login/index' })
return
}
// 检查是否已绑定设备
const hasDevice = Taro.getStorageSync('hasDevice')
if (hasDevice) {
// 已绑定设备,跳转到首页
Taro.reLaunch({ url: '/pages/device/index' })
try {
const childRes = await getChildren()
setChildren(childRes.items || [])
if (childRes.items?.length === 0) {
setLoading(false)
return
}
} catch (err) {
console.error('Get children failed:', err)
}
}, [])
try {
const binding = await getCurrentBinding()
if (binding) {
Taro.showToast({ title: '已有设备绑定', icon: 'none' })
setTimeout(() => {
Taro.reLaunch({ url: '/pages/device/index' })
}, 1000)
return
}
} catch (err) {
}
setLoading(false)
}
const handleScanCode = () => {
setIsScanning(true)
@@ -29,56 +61,57 @@ export default function Bind() {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => {
success: async (res) => {
setIsScanning(false)
console.log('扫码结果:', res.result)
const deviceId = res.result
// 模拟绑定设备
Taro.showLoading({ title: '绑定中...' })
setTimeout(() => {
Taro.hideLoading()
// 保存设备信息
Taro.setStorageSync('hasDevice', true)
Taro.setStorageSync('deviceInfo', {
id: res.result || 'DEVICE_' + Date.now(),
name: '小夏的伴伴',
status: 'online',
battery: 82,
bindTime: new Date().toLocaleString()
})
Taro.showToast({
title: '设备绑定成功',
icon: 'success',
duration: 2000,
complete: () => {
// 绑定成功,跳转到首页
setTimeout(() => {
Taro.reLaunch({ url: '/pages/device/index' })
}, 500)
}
})
}, 1500)
if (!deviceId) {
Taro.showToast({ title: '无效的设备码', icon: 'none' })
return
}
await doBind(deviceId)
},
fail: (err) => {
setIsScanning(false)
console.error('扫码失败:', err)
// 用户取消扫码,不显示错误
if (err.errMsg && err.errMsg.includes('cancel')) {
return
}
Taro.showToast({
title: '扫码失败,请重试',
icon: 'none'
})
Taro.showToast({ title: '扫码失败', icon: 'none' })
}
})
}
const doBind = async (deviceId: string) => {
if (children.length === 0) {
Taro.showToast({ title: '请先添加儿童', icon: 'none' })
return
}
Taro.showLoading({ title: '绑定中...' })
try {
await directBind({
device_id: deviceId,
child_id: children[0].child_id
})
Taro.hideLoading()
Taro.showToast({
title: '设备绑定成功',
icon: 'success',
duration: 1500,
complete: () => {
setTimeout(() => {
Taro.reLaunch({ url: '/pages/device/index' })
}, 500)
}
})
} catch (err: any) {
Taro.hideLoading()
Taro.showToast({ title: err.message || '绑定失败', icon: 'none' })
}
}
const handleManualInput = () => {
Taro.showModal({
title: '手动输入设备码',
@@ -86,37 +119,60 @@ export default function Bind() {
placeholderText: '请输入设备底部的设备码',
success: (res) => {
if (res.confirm && res.content) {
// 模拟绑定设备
Taro.showLoading({ title: '绑定中...' })
setTimeout(() => {
Taro.hideLoading()
Taro.setStorageSync('hasDevice', true)
Taro.setStorageSync('deviceInfo', {
id: res.content,
name: '小夏的伴伴',
status: 'online',
battery: 82,
bindTime: new Date().toLocaleString()
})
Taro.showToast({
title: '设备绑定成功',
icon: 'success',
duration: 2000,
complete: () => {
setTimeout(() => {
Taro.reLaunch({ url: '/pages/device/index' })
}, 500)
}
})
}, 1500)
doBind(res.content)
}
}
})
}
const handleAddChild = async () => {
if (!newChildName.trim()) {
Taro.showToast({ title: '请输入儿童姓名', icon: 'none' })
return
}
setSaving(true)
try {
await createChild({ child_name: newChildName.trim() })
Taro.showToast({ title: '添加成功', icon: 'success' })
const childRes = await getChildren()
setChildren(childRes.items || [])
} catch (err) {
Taro.showToast({ title: '添加失败', icon: 'none' })
} finally {
setSaving(false)
}
}
if (loading) {
return (
<View className='bind-page'>
<View className='loading'>...</View>
</View>
)
}
if (children.length === 0) {
return (
<View className='bind-page'>
<View className='empty-tip'>
<Text className='empty-title'></Text>
<Text className='empty-desc'></Text>
<View className='empty-input-wrap'>
<Input
className='empty-input'
placeholder='请输入儿童姓名'
value={newChildName}
onInput={(e) => setNewChildName(e.detail.value)}
/>
</View>
<View className='empty-btn' onClick={handleAddChild}>
<Text className='empty-btn-text'>{saving ? '保存中...' : '确认添加'}</Text>
</View>
</View>
</View>
)
}
return (
<View className='bind-page'>
<View className='bind-header'>
@@ -170,4 +226,4 @@ export default function Bind() {
</View>
</View>
)
}
}

View File

@@ -1,6 +1,7 @@
import { View, Text, Switch, Slider, Image } from '@tarojs/components'
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { getCurrentBinding } from '@/services/binding'
import './index.scss'
interface DeviceData {
@@ -20,35 +21,59 @@ export default function Device() {
const [hasDevice, setHasDevice] = useState(false)
useEffect(() => {
// 检查登录状态
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
if (!isLoggedIn) {
console.log('[Device] Checking login status...')
const token = Taro.getStorageSync('token')
console.log('[Device] Token exists:', !!token)
if (!token) {
console.log('[Device] No token, redirecting to login')
Taro.redirectTo({ url: '/pages/login/index' })
return
}
// 检查设备绑定状态
const deviceInfo = Taro.getStorageSync('deviceInfo')
const hasDeviceFlag = Taro.getStorageSync('hasDevice')
setHasDevice(!!hasDeviceFlag)
if (deviceInfo && hasDeviceFlag) {
setDevice({
id: deviceInfo.id,
name: deviceInfo.name || '小夏的伴伴',
status: deviceInfo.status || 'online',
battery: deviceInfo.battery || 82,
daysLeft: 4,
sleepEnabled: true,
sleepTime: '每天 22:00 - 07:00',
volume: 50
})
}
setIsLoading(false)
loadDeviceInfo()
}, [])
const loadDeviceInfo = async () => {
try {
const binding = await getCurrentBinding()
console.log('[Device] Got binding:', binding)
if (binding) {
setHasDevice(true)
Taro.setStorageSync('hasDevice', '1')
Taro.setStorageSync('deviceInfo', {
id: binding.device_id,
name: '伴伴设备',
status: 'online',
battery: 82
})
setDevice({
id: binding.device_id,
name: '伴伴设备',
status: 'online',
battery: 82,
daysLeft: 4,
sleepEnabled: true,
sleepTime: '每天 22:00 - 07:00',
volume: 50
})
} else {
setHasDevice(false)
Taro.removeStorageSync('hasDevice')
Taro.removeStorageSync('deviceInfo')
}
} catch (err: any) {
console.log('[Device] No binding or error:', err?.message || err)
if (err?.status === 404) {
setHasDevice(false)
Taro.removeStorageSync('hasDevice')
Taro.removeStorageSync('deviceInfo')
}
}
setIsLoading(false)
}
const handleSleepToggle = (e: any) => {
if (!device) return
setDevice({ ...device, sleepEnabled: e.detail.value })

View File

@@ -1,51 +1,118 @@
import { View, Text, Button } from '@tarojs/components'
import { useEffect } from 'react'
import Taro from '@tarojs/taro'
import { BASE_URL, request } from '@/services/api'
import './index.scss'
export default function Login() {
useEffect(() => {
// 检查是否已登录
const isLoggedIn = Taro.getStorageSync('isLoggedIn')
if (isLoggedIn) {
// 已登录,跳转到首页
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')
Taro.reLaunch({ url: '/pages/device/index' })
}
}, [])
const handleLogin = () => {
Taro.showLoading({ title: '登录中...' })
// 调用微信登录
Taro.login({
success: (loginRes) => {
console.log('微信登录成功:', loginRes)
// 模拟后端验证 - 实际项目中需要将code发送到服务器
// 这里假设登录成功
Taro.setStorageSync('isLoggedIn', true)
Taro.setStorageSync('userInfo', {
nickName: '微信家长',
avatarUrl: '',
isAdmin: true,
openid: loginRes.code
const doWeChatLogin = async () => {
try {
console.log('[Login] Starting WeChat login...')
Taro.showLoading({ title: '登录中...' })
// 1. 调用微信登录获取 code
console.log('[Login] Step 1: Calling Taro.login()...')
const loginRes = await Taro.login()
const code = loginRes.code
console.log('[Login] Got code:', code ? 'yes' : 'no')
// 2. 获取用户头像信息 (可选)
let userInfo = null
try {
console.log('[Login] Step 2: Calling Taro.getUserProfile()...')
const profileRes = await Taro.getUserProfile({
desc: '用于完善用户资料'
})
Taro.hideLoading()
// 登录成功后跳转到首页
userInfo = profileRes.userInfo
console.log('[Login] Got userInfo:', !!userInfo)
} catch (e) {
console.log('[Login] getUserProfile failed (expected if user denied):', e)
}
// 3. 调用后端登录接口 (微信登录)
console.log('[Login] Step 3: Calling backend /auth/login...')
const res = await request<{ access_token: string; expires_in: number; user_id: number }>('/auth/login', {
method: 'POST',
data: {
code: code, // 微信 code
nickname: userInfo?.nickName,
avatar_url: userInfo?.avatarUrl
}
})
console.log('[Login] Backend login success, user_id:', res.user_id)
// 4. 保存 token 和用户信息
console.log('[Login] Step 4: Saving token and user info...')
Taro.setStorageSync('token', res.access_token)
Taro.setStorageSync('user_id', res.user_id)
Taro.setStorageSync('expires_in', res.expires_in)
// 保存用户信息用于显示
if (userInfo) {
Taro.setStorageSync('userInfo', {
nickname: userInfo.nickName,
avatar_url: userInfo.avatarUrl
})
}
// 5. 如果获取到了用户信息,发送到后端更新
if (userInfo) {
console.log('[Login] Step 5: Updating user profile...')
try {
await request(`/parents/${res.user_id}`, {
method: 'PATCH',
data: {
nickname: userInfo.nickName,
avatar_url: userInfo.avatarUrl
}
})
} catch (e) {
console.log('[Login] Update user profile failed:', e)
}
}
Taro.hideLoading()
console.log('[Login] Step 6: 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' })
},
fail: (err) => {
Taro.hideLoading()
console.error('微信登录失败:', err)
}, 500)
} catch (err: any) {
Taro.hideLoading()
console.error('[Login] Login failed:', err)
// 如果是 401 错误,尝试通过创建新用户登录
if (err.status === 401) {
Taro.showToast({
title: '登录失败,请重试',
icon: 'none'
})
} else {
Taro.showToast({
title: err.message || '网络错误,请重试',
icon: 'none'
})
}
})
}
}
const handleLogin = () => {
console.log('Button clicked, starting login...')
doWeChatLogin()
}
return (
@@ -77,8 +144,7 @@ export default function Login() {
<View className='login-btn-wrapper'>
<Button
className='login-btn'
openType='getUserInfo'
onGetUserInfo={handleLogin}
onClick={handleLogin}
>
<Text className='btn-icon'>🌐</Text>
<Text className='btn-text'></Text>

View File

@@ -160,3 +160,150 @@
color: #999999;
}
}
.add-input-box {
background: #FFFFFF;
border-radius: 20px;
margin: 0 24px 24px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
.input-row {
display: flex;
flex-direction: column;
gap: 16px;
}
.input-wrapper {
.name-input {
background: #F5F7FA;
border-radius: 12px;
padding: 20px 24px;
font-size: 30px;
width: 100%;
box-sizing: border-box;
}
.input-hint {
font-size: 24px;
color: #999999;
margin-top: 12px;
display: block;
}
}
.input-btns {
display: flex;
justify-content: flex-end;
gap: 24px;
.confirm-btn {
font-size: 30px;
color: #FF8C42;
font-weight: 500;
}
.cancel-btn {
font-size: 30px;
color: #999999;
}
}
}
.menu-item.disabled {
opacity: 0.5;
&:active {
background: transparent;
}
}
.logout-section {
margin: 24px;
}
.logout-btn {
background: #FFFFFF;
border-radius: 20px;
padding: 26px 24px;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
.logout-text {
font-size: 30px;
color: #FF3B30;
font-weight: 500;
}
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #FFFFFF;
border-radius: 20px;
width: 600px;
margin: 0 48px;
overflow: hidden;
}
.modal-header {
padding: 32px 32px 16px;
text-align: center;
.modal-title {
font-size: 34px;
font-weight: 600;
color: #1A1A1A;
}
}
.modal-body {
padding: 16px 32px 32px;
display: flex;
align-items: center;
.modal-input {
background: #F5F7FA;
border-radius: 12px;
padding: 0 24px;
font-size: 34px;
width: 100%;
height: 96px;
box-sizing: border-box;
display: flex;
align-items: center;
}
}
.modal-footer {
display: flex;
border-top: 1px solid #F0F0F0;
.modal-btn {
flex: 1;
text-align: center;
padding: 24px 0;
font-size: 32px;
&.cancel {
color: #666666;
border-right: 1px solid #F0F0F0;
}
&.confirm {
color: #FF8C42;
font-weight: 500;
}
}
}

View File

@@ -1,36 +1,188 @@
import { View, Text, Image } from '@tarojs/components'
import { useState } from 'react'
import { View, Text, Image, Input } from '@tarojs/components'
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { getToken, getCurrentUserId } from '@/services/api'
import { getChildren, createChild, updateChild } from '@/services/child'
import { getCurrentBinding, unbindDevice } from '@/services/binding'
import './index.scss'
interface UserInfo {
name: string
role: string
isVerified: boolean
avatar: string
interface Child {
child_id: number
child_name: string
child_gender: number
status: number
}
interface Binding {
device_id: string
child_id: number
status: number
}
interface MenuItem {
icon: string
iconBgClass: string
name: string
value: string
value?: string
arrow?: boolean
disabled?: boolean
}
export default function Sleep() {
const [user] = useState<UserInfo>({
name: '微信家长',
role: '管理员身份',
isVerified: true,
avatar: '👤'
})
const [children, setChildren] = useState<Child[]>([])
const [binding, setBinding] = useState<Binding | null>(null)
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
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 menuItems = [
const token = getToken()
const userId = getCurrentUserId()
useEffect(() => {
if (!token) {
Taro.reLaunch({ url: '/pages/login/index' })
return
}
loadData()
}, [])
const loadData = async () => {
try {
setLoading(true)
// Get parent info
const userInfo = Taro.getStorageSync('userInfo') || {}
setParentInfo(userInfo)
// Get children
const childRes = await getChildren()
setChildren(childRes.items || [])
// Get binding
try {
const bindRes = await getCurrentBinding()
setBinding(bindRes)
} catch (err: any) {
if (err.status === 404) {
setBinding(null)
}
}
} catch (err) {
console.error('Load failed:', err)
} finally {
setLoading(false)
}
}
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
setModalType(type)
if (type === 'edit' && child) {
setChildName(child.child_name)
setEditingChildId(child.child_id)
} else {
setChildName('')
setEditingChildId(null)
}
setShowModal(true)
}
const handleModalConfirm = async () => {
if (!childName.trim()) {
Taro.showToast({ title: '请输入昵称', icon: 'none' })
return
}
try {
if (modalType === 'add') {
await createChild({ child_name: childName.trim() })
Taro.showToast({ title: '添加成功', icon: 'success' })
} else if (editingChildId) {
await updateChild(editingChildId, { child_name: childName.trim() })
Taro.showToast({ title: '修改成功', icon: 'success' })
}
setShowModal(false)
setChildName('')
setEditingChildId(null)
loadData()
} catch (err) {
Taro.showToast({ title: '操作失败', icon: 'none' })
}
}
const handleModalCancel = () => {
setShowModal(false)
setChildName('')
setEditingChildId(null)
}
const handleUnbind = () => {
if (!binding) return
Taro.showModal({
title: '解除设备绑定',
content: '确定要解除当前设备的绑定吗?',
confirmColor: '#FF8C42',
success: async (res) => {
if (res.confirm) {
try {
await unbindDevice(binding.device_id)
Taro.showToast({ title: '已解绑', icon: 'success' })
setBinding(null)
loadData()
} catch (err) {
Taro.showToast({ title: '解绑失败', icon: 'none' })
}
}
}
})
}
const handleLogout = async () => {
Taro.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
confirmColor: '#FF8C42',
success: (res) => {
if (res.confirm) {
Taro.removeStorageSync('token')
Taro.removeStorageSync('user_id')
Taro.removeStorageSync('userInfo')
Taro.reLaunch({ url: '/pages/login/index' })
}
}
})
}
const handleMenuClick = (item: MenuItem) => {
if (item.disabled) return
if (item.name === '绑定新设备') {
if (binding) {
Taro.showToast({ title: '已有设备绑定', icon: 'none' })
} else {
Taro.navigateTo({ url: '/pages/bind/index' })
}
} else if (item.name === '解除设备绑定') {
handleUnbind()
} else if (item.name.includes('儿童资料')) {
if (children.length > 0) {
handleOpenModal('edit', children[0])
} else {
handleOpenModal('add')
}
}
}
if (!token) return null
const menuItems: MenuItem[] = [
{
icon: require('../../assets/tab-icons/orange-robot.png'),
iconBgClass: 'orange',
name: '儿童资料 (用于称呼)',
value: '小夏 (6岁)',
value: children.length > 0 ? children[0].child_name : '未设置',
arrow: true
},
{
@@ -45,45 +197,13 @@ export default function Sleep() {
iconBgClass: 'red',
name: '解除设备绑定',
value: '',
arrow: true
arrow: true,
disabled: !binding
}
]
const handleMenuClick = (item: MenuItem) => {
if (item.name === '绑定新设备') {
// 跳转到绑定设备页面
Taro.navigateTo({ url: '/pages/bind/index' })
} else if (item.name === '解除设备绑定') {
Taro.showModal({
title: '解除设备绑定',
content: '确定要解除当前设备的绑定吗?解除后需要重新扫码绑定。',
confirmColor: '#FF8C42',
success: (res) => {
if (res.confirm) {
// 清除设备信息
Taro.removeStorageSync('hasDevice')
Taro.removeStorageSync('deviceInfo')
Taro.showToast({
title: '已解除绑定',
icon: 'success',
duration: 1500,
complete: () => {
setTimeout(() => {
Taro.redirectTo({ url: '/pages/bind/index' })
}, 500)
}
})
}
}
})
} else {
Taro.showToast({
title: `点击了 ${item.name}`,
icon: 'none'
})
}
}
const firstChild = children[0]
const childDisplayValue = firstChild ? firstChild.child_name : ''
return (
<View className='manage-page'>
@@ -93,50 +213,76 @@ export default function Sleep() {
<View className='user-card'>
<View className='user-avatar'>
<Text className='avatar-img'>{user.avatar}</Text>
{parentInfo.avatar_url ? (
<Image className='avatar-img' src={parentInfo.avatar_url} mode='aspectFill' />
) : (
<Text className='avatar-img'>👤</Text>
)}
</View>
<View className='user-info'>
<Text className='user-name'>{user.name}</Text>
<Text className='user-role'>{user.role}</Text>
<Text className='user-name'>{parentInfo.nickname || '家长用户'}</Text>
<Text className='user-role'>ID: {userId}</Text>
</View>
{user.isVerified && (
<View className='verified-badge'>
<Text></Text>
</View>
)}
</View>
<View className='menu-card'>
{menuItems.map((item, index) => (
<View key={index}>
<View className='menu-item' onClick={() => handleMenuClick(item)}>
<View
className={`menu-item ${item.disabled ? 'disabled' : ''}`}
onClick={() => handleMenuClick(item)}
>
<View className='menu-left'>
<View className={`icon-bg ${item.iconBgClass}`}>
<Image
className='control-icon-img'
src={item.icon}
mode='aspectFit'
/>
<Image className='control-icon-img' src={item.icon} mode='aspectFit' />
</View>
<Text className='menu-name'>{item.name}</Text>
</View>
<View className='menu-right'>
{item.value && (
<Text className='menu-value'>{item.value}</Text>
{item.name.includes('儿童资料') && (
<Text className='menu-value'>{childDisplayValue}</Text>
)}
{item.arrow && <Text className='arrow'></Text>}
</View>
</View>
{index < menuItems.length - 1 && (
<View className='divider'></View>
)}
{index < menuItems.length - 1 && <View className='divider'></View>}
</View>
))}
</View>
<View className='logout-section'>
<View className='logout-btn' onClick={handleLogout}>
<Text className='logout-text'>退</Text>
</View>
</View>
<View className='version-info'>
<Text className='version-text'> Companion V1.1.0</Text>
</View>
{/* Modal */}
{showModal && (
<View className='modal-mask'>
<View className='modal-content'>
<View className='modal-header'>
<Text className='modal-title'>{modalType === 'add' ? '添加儿童' : '修改昵称'}</Text>
</View>
<View className='modal-body'>
<Input
className='modal-input'
placeholder='请输入儿童昵称'
value={childName}
onInput={(e) => setChildName(e.detail.value)}
focus={true}
/>
</View>
<View className='modal-footer'>
<Text className='modal-btn cancel' onClick={handleModalCancel}></Text>
<Text className='modal-btn confirm' onClick={handleModalConfirm}></Text>
</View>
</View>
</View>
)}
</View>
)
}
}