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