fix: 优化小程序家庭邀请和设备详情
This commit is contained in:
@@ -14,10 +14,13 @@ import {
|
||||
} from '@/services/binding'
|
||||
import {
|
||||
Child,
|
||||
CHILD_NAME_RULE_MESSAGE,
|
||||
createChild,
|
||||
getChildren,
|
||||
sanitizeChildNameInput,
|
||||
resolveChildSelection,
|
||||
setSelectedChildId as setStoredSelectedChildId,
|
||||
validateChildNameInput,
|
||||
} from '@/services/child'
|
||||
import { useSystemBanner } from '@/components/system-banner/use-system-banner'
|
||||
|
||||
@@ -444,6 +447,10 @@ export default function Bind() {
|
||||
|
||||
const childName = newChildName.trim()
|
||||
if (!childName) return null
|
||||
const childNameError = validateChildNameInput(childName)
|
||||
if (childNameError) {
|
||||
throw new Error(childNameError)
|
||||
}
|
||||
|
||||
const child = await createChild({ child_name: childName })
|
||||
setChildren((currentChildren) => [child, ...currentChildren])
|
||||
@@ -500,7 +507,7 @@ export default function Bind() {
|
||||
|
||||
if (isPendingBinding) {
|
||||
if (!childId) {
|
||||
throw new Error('请先选择宝贝或填写新的宝贝昵称')
|
||||
throw new Error('请先选择宝贝或填写新的宝贝姓名')
|
||||
}
|
||||
await setBindingChild(pendingDeviceId!, { child_id: childId })
|
||||
setSelectedBindingDeviceId(pendingDeviceId)
|
||||
@@ -658,18 +665,20 @@ export default function Bind() {
|
||||
)}
|
||||
|
||||
<View className='field-item'>
|
||||
<Text className='field-label'>{isPendingBinding ? '新建宝贝昵称' : '新建宝贝昵称(可选)'}</Text>
|
||||
<Text className='field-label'>{isPendingBinding ? '新建宝贝姓名' : '新建宝贝姓名(可选)'}</Text>
|
||||
<Input
|
||||
className='field-input'
|
||||
value={newChildName}
|
||||
placeholder={
|
||||
isPendingBinding
|
||||
? children.length > 0 ? '不选已有宝贝时,可填写新昵称' : '请输入宝贝昵称'
|
||||
: children.length > 0 ? '不选已有宝贝时,可填写新昵称' : '可不填,稍后再关联宝贝'
|
||||
? children.length > 0 ? '不选已有宝贝时,可填写2-5位简体中文姓名(暂不支持数字和英文)' : CHILD_NAME_RULE_MESSAGE
|
||||
: children.length > 0 ? '不选已有宝贝时,可填写2-5位简体中文姓名(暂不支持数字和英文)' : '可不填,稍后再关联宝贝'
|
||||
}
|
||||
maxlength={5}
|
||||
onInput={(event) => {
|
||||
setNewChildName(event.detail.value)
|
||||
if (event.detail.value) {
|
||||
const nextChildName = sanitizeChildNameInput(event.detail.value)
|
||||
setNewChildName(nextChildName)
|
||||
if (nextChildName) {
|
||||
setSelectedChildId(null)
|
||||
setStoredSelectedChildId(null)
|
||||
}
|
||||
|
||||
@@ -672,8 +672,9 @@
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #F3F3F3;
|
||||
|
||||
@@ -682,14 +683,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detail-row-highlight {
|
||||
display: block;
|
||||
padding-top: 22px;
|
||||
padding-bottom: 24px;
|
||||
|
||||
.detail-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
text-align: left;
|
||||
color: #FF8C42;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
flex-shrink: 0;
|
||||
width: 180px;
|
||||
font-size: 28px;
|
||||
line-height: 1.45;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
max-width: 60%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
font-size: 28px;
|
||||
line-height: 1.45;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
color: #1A1A1A;
|
||||
|
||||
@@ -342,7 +342,7 @@ export default function Device() {
|
||||
<Text className='empty-subtitle'>
|
||||
{hasPendingDevice
|
||||
? '这台设备还没有关联宝贝,关联后即可查看定位、聊天和设备状态'
|
||||
: '绑定时可以选择已有宝贝,或直接新建宝贝昵称'}
|
||||
: '绑定时可以选择已有宝贝,或直接新建宝贝姓名'}
|
||||
</Text>
|
||||
<View className='empty-btn' onClick={() => Taro.navigateTo({ url: hasPendingDevice ? '/pages/bind/index?mode=assign' : '/pages/bind/index' })}>
|
||||
<Text className='empty-btn-text'>{hasPendingDevice ? '关联宝贝' : '添加设备'}</Text>
|
||||
@@ -620,30 +620,34 @@ export default function Device() {
|
||||
|
||||
{isDetailExpanded && (
|
||||
<View className='detail-body'>
|
||||
<View className='detail-row detail-row-highlight'>
|
||||
<Text className='detail-label'>当前坐标</Text>
|
||||
<Text className='detail-value'>{coordinateLabel}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>位置上报时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.location_updated_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>服务端时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.server_time)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>设备状态时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.settings_updated_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>设备 ID</Text>
|
||||
<Text className='detail-value'>{binding.device_id}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>当前宝贝</Text>
|
||||
<Text className='detail-value'>{childName}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>当前设备</Text>
|
||||
<Text className='detail-value'>{binding.device_id}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>绑定时间</Text>
|
||||
<Text className='detail-value'>{formatTime(binding.bound_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>当前坐标</Text>
|
||||
<Text className='detail-value'>{coordinateLabel}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>状态更新时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.settings_updated_at)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>定位更新时间</Text>
|
||||
<Text className='detail-value'>{formatTime(deviceStatus?.location_updated_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1043,15 +1043,6 @@
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.family-copy-action {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
font-size: 26px;
|
||||
color: #FF8C42;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.family-danger-action {
|
||||
background: #FFF1F2;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,16 @@ import {
|
||||
updateBindingCardName,
|
||||
unbindDevice,
|
||||
} from '@/services/binding'
|
||||
import { Child, clearSelectedChildId, deleteChild, setSelectedChildId, updateChild } from '@/services/child'
|
||||
import {
|
||||
Child,
|
||||
CHILD_NAME_RULE_MESSAGE,
|
||||
clearSelectedChildId,
|
||||
deleteChild,
|
||||
sanitizeChildNameInput,
|
||||
setSelectedChildId,
|
||||
updateChild,
|
||||
validateChildNameInput,
|
||||
} from '@/services/child'
|
||||
import {
|
||||
createFamilyInvitation,
|
||||
FamilyMember,
|
||||
@@ -427,16 +436,6 @@ export default function Sleep() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyFamilyInvite = () => {
|
||||
if (!familyInvitePath) return
|
||||
Taro.setClipboardData({
|
||||
data: familyInvitePath,
|
||||
success: () => {
|
||||
Taro.showToast({ title: '邀请路径已复制', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveFamilyMember = (member: FamilyMember) => {
|
||||
if (!binding?.device_id || member.is_owner || isUpdatingFamily) return
|
||||
|
||||
@@ -657,8 +656,9 @@ export default function Sleep() {
|
||||
|
||||
const handleSubmitModal = async () => {
|
||||
const normalizedName = childName.trim()
|
||||
if (!normalizedName) {
|
||||
Taro.showToast({ title: '请输入宝贝昵称', icon: 'none' })
|
||||
const childNameError = validateChildNameInput(normalizedName)
|
||||
if (childNameError) {
|
||||
Taro.showToast({ title: childNameError, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1442,14 +1442,15 @@ export default function Sleep() {
|
||||
{showModal && (
|
||||
<View className='modal-mask'>
|
||||
<View className='modal-card'>
|
||||
<Text className='modal-title'>修改宝贝昵称</Text>
|
||||
<Text className='modal-title'>修改宝贝姓名</Text>
|
||||
<View className='modal-input-wrap'>
|
||||
<Input
|
||||
className='modal-input'
|
||||
value={childName}
|
||||
placeholder='请输入宝贝昵称'
|
||||
placeholder={CHILD_NAME_RULE_MESSAGE}
|
||||
focus
|
||||
onInput={(event) => setChildName(event.detail.value)}
|
||||
maxlength={5}
|
||||
onInput={(event) => setChildName(sanitizeChildNameInput(event.detail.value))}
|
||||
/>
|
||||
</View>
|
||||
<View className='modal-actions'>
|
||||
@@ -1615,9 +1616,6 @@ export default function Sleep() {
|
||||
<Text className='family-invite-tip'>
|
||||
邀请有效期至 {familyInviteExpiresAt ? familyInviteExpiresAt.replace('T', ' ').slice(0, 16) : '24 小时内'}
|
||||
</Text>
|
||||
<Text className='family-copy-action' onClick={handleCopyFamilyInvite}>
|
||||
复制邀请路径
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
|
||||
@@ -2,6 +2,9 @@ import Taro from '@tarojs/taro'
|
||||
import { request } from './api'
|
||||
|
||||
const SELECTED_CHILD_ID_KEY = 'selectedChildId'
|
||||
const CHILD_NAME_PATTERN = /^[\u4e00-\u9fa5]{2,5}$/
|
||||
|
||||
export const CHILD_NAME_RULE_MESSAGE = '请输入2-5位简体中文姓名(暂不支持数字和英文)'
|
||||
|
||||
function buildQuery(params: Record<string, string | number | undefined | null>): string {
|
||||
return Object.entries(params)
|
||||
@@ -10,6 +13,21 @@ function buildQuery(params: Record<string, string | number | undefined | null>):
|
||||
.join('&')
|
||||
}
|
||||
|
||||
export function normalizeChildNameInput(value?: string | null): string {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
export function sanitizeChildNameInput(value?: string | null): string {
|
||||
return normalizeChildNameInput(value).replace(/[^\u4e00-\u9fa5]/g, '').slice(0, 5)
|
||||
}
|
||||
|
||||
export function validateChildNameInput(value?: string | null): string {
|
||||
const childName = normalizeChildNameInput(value)
|
||||
if (!childName) return '请输入宝贝姓名'
|
||||
if (!CHILD_NAME_PATTERN.test(childName)) return CHILD_NAME_RULE_MESSAGE
|
||||
return ''
|
||||
}
|
||||
|
||||
export interface Child {
|
||||
child_id: number
|
||||
child_name: string
|
||||
@@ -61,9 +79,16 @@ export async function createChild(data: {
|
||||
child_gender?: number
|
||||
child_birthday?: string
|
||||
}): Promise<Child> {
|
||||
const childName = normalizeChildNameInput(data.child_name)
|
||||
const childNameError = validateChildNameInput(childName)
|
||||
if (childNameError) throw new Error(childNameError)
|
||||
|
||||
return request<Child>('/banban/children', {
|
||||
method: 'POST',
|
||||
data,
|
||||
data: {
|
||||
...data,
|
||||
child_name: childName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,9 +108,17 @@ export async function updateChild(
|
||||
childId: number,
|
||||
data: { child_name?: string; child_gender?: number; child_birthday?: string }
|
||||
): Promise<Child> {
|
||||
const nextData = { ...data }
|
||||
if (nextData.child_name !== undefined) {
|
||||
const childName = normalizeChildNameInput(nextData.child_name)
|
||||
const childNameError = validateChildNameInput(childName)
|
||||
if (childNameError) throw new Error(childNameError)
|
||||
nextData.child_name = childName
|
||||
}
|
||||
|
||||
return request<Child>(`/banban/children/${childId}`, {
|
||||
method: 'PATCH',
|
||||
data,
|
||||
data: nextData,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user