支持已绑定设备追加 NFC 卡

This commit is contained in:
stu2not
2026-06-04 09:55:35 +08:00
parent 482fbf351f
commit 9f43fcb5ae
18 changed files with 646 additions and 92 deletions

View File

@@ -8,6 +8,7 @@ import {
resolveActiveBinding,
setBindingChild,
setSelectedBindingDeviceId,
startNFCCardBind,
startNFCBind,
} from '@/services/binding'
import {
@@ -154,6 +155,8 @@ function parseBindingPayload(rawValue: string): { deviceId: string; serialNumber
export default function Bind() {
const router = useRouter()
const isAssignMode = String(router.params?.mode || '').trim() === 'assign'
const isCardMode = String(router.params?.mode || '').trim() === 'card'
const cardModeDeviceId = getRouteParam(router.params, 'device_id')
const systemBanner = useSystemBanner()
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
@@ -192,10 +195,12 @@ export default function Bind() {
const hasSelectedOrNewChild = selectedChildId !== null || Boolean(newChildName.trim())
const isPendingBinding = Boolean(pendingDeviceId)
const isAddingCard = isCardMode && Boolean(deviceId.trim())
const isPollingBind = bindStatus === SESSION_STATUS_PENDING && Boolean(bindToken)
const isExpiredBind = bindStatus === SESSION_STATUS_EXPIRED
const canEditDeviceFields = !isPendingBinding && !isPollingBind
const canSubmit = isPendingBinding ? hasSelectedOrNewChild : isExpiredBind ? true : !isPollingBind
const pageTitle = isPendingBinding ? '补全绑定' : isAddingCard ? '添加设备卡' : '扫码贴卡绑定设备'
const goDevicePage = () => {
Taro.reLaunch({ url: '/pages/device/index' })
@@ -246,7 +251,7 @@ export default function Bind() {
}
const applyLaunchQrPayload = () => {
if (isAssignMode) return false
if (isAssignMode || isCardMode) return false
const launchPayload = getLaunchQrPayload(router.params)
if (!launchPayload || appliedLaunchQrRef.current === launchPayload) return false
@@ -274,12 +279,14 @@ export default function Bind() {
setCardUUID('')
setBindExpiresAt('')
setRemainingBindSeconds(0)
setDeviceId('')
setSerialNumber('')
setBindHint('贴卡时间已过,请重新扫码并贴卡')
if (!isCardMode) {
setDeviceId('')
setSerialNumber('')
}
setBindHint(isCardMode ? '贴卡时间已过,请重新发送加卡指令' : '贴卡时间已过,请重新扫码并贴卡')
if (showToast) {
Taro.showToast({ title: '贴卡已超时,请重新扫码', icon: 'none' })
Taro.showToast({ title: isCardMode ? '贴卡已超时,请重新发送' : '贴卡已超时,请重新扫码', icon: 'none' })
}
}
@@ -318,7 +325,7 @@ export default function Bind() {
resetExpiredBindSession()
return
}
setBindHint('已发送绑卡指令,请在倒计时结束前去设备上贴自己的卡')
setBindHint(isAddingCard ? '已发送加卡指令,请在倒计时结束前去设备上贴新卡' : '已发送绑卡指令,请在倒计时结束前去设备上贴自己的卡')
schedulePoll(activeBindToken)
return
}
@@ -329,13 +336,15 @@ export default function Bind() {
if (session.status === SESSION_STATUS_COMPLETED) {
setSelectedBindingDeviceId(session.device_id)
setBindHint(
session.child_id
isAddingCard
? session.card_uuid ? `新卡添加完成,卡号 ${session.card_uuid}` : '新卡添加完成'
: session.child_id
? session.card_uuid ? `绑定完成,卡号 ${session.card_uuid}` : '绑定完成'
: session.card_uuid ? `绑定完成,卡号 ${session.card_uuid},稍后可关联孩子` : '绑定完成,稍后可关联孩子'
)
Taro.showToast({ title: '设备绑定成功', icon: 'success' })
Taro.showToast({ title: isAddingCard ? '新卡添加成功' : '设备绑定成功', icon: 'success' })
setTimeout(() => {
if (session.child_id) {
if (isAddingCard || session.child_id) {
goDevicePage()
} else {
goManagePage()
@@ -350,7 +359,7 @@ export default function Bind() {
}
if (session.status === SESSION_STATUS_FAILED) {
setBindHint('贴卡绑定失败,请重试')
setBindHint(isAddingCard ? '添加新卡失败,请确认卡未绑定到其他设备后重试' : '贴卡绑定失败,请重试')
return
}
@@ -379,7 +388,16 @@ export default function Bind() {
setChildren(currentChildren)
setSelectedChildId(currentChild?.child_id || null)
if (isAssignMode && activeBinding?.device_id && !activeBinding.child_id) {
if (isCardMode) {
const targetDeviceId = cardModeDeviceId || activeBinding?.device_id || ''
if (!targetDeviceId) {
throw new Error('当前没有可加卡的设备')
}
setPendingDeviceId(null)
setDeviceId(targetDeviceId)
setSerialNumber('')
setBindHint('确认当前设备后发送加卡指令')
} else if (isAssignMode && activeBinding?.device_id && !activeBinding.child_id) {
setPendingDeviceId(activeBinding.device_id)
setDeviceId(activeBinding.device_id)
} else {
@@ -451,10 +469,10 @@ export default function Bind() {
if (submitting || isPollingBind) return
setSubmitting(true)
Taro.showLoading({ title: isPendingBinding ? '关联中...' : '发送绑卡指令...' })
Taro.showLoading({ title: isPendingBinding ? '关联中...' : isAddingCard ? '发送加卡指令...' : '发送绑卡指令...' })
try {
const childId = await ensureChildId()
const childId = isAddingCard ? null : await ensureChildId()
if (isPendingBinding) {
if (!childId) {
@@ -472,6 +490,19 @@ export default function Bind() {
if (!deviceId.trim()) {
throw new Error('请输入设备号')
}
if (isAddingCard) {
resetBindSessionState()
const session = await startNFCCardBind({ device_id: deviceId.trim() })
setBindToken(session.bind_token)
setBindStatus(session.status)
setBindExpiresAt(session.expires_at)
setRemainingBindSeconds(getRemainingBindSeconds(session.expires_at))
setBindHint('已发送加卡指令,请在倒计时结束前去设备上贴新卡')
schedulePoll(session.bind_token)
Taro.showToast({ title: '请去设备上贴新卡', icon: 'none' })
return
}
if (!serialNumber.trim()) {
throw new Error('请输入设备序列号')
}
@@ -521,10 +552,19 @@ export default function Bind() {
<Image className='back-icon' src={require('../../assets/tab-icons/arrow-left.png')} mode='aspectFit' />
<Text className='back-text'></Text>
</View>
<Text className='title'>{isPendingBinding ? '补全绑定' : '扫码贴卡绑定设备'}</Text>
<Text className='title'>{pageTitle}</Text>
</View>
{isPendingBinding ? (
{isAddingCard ? (
<View className='pending-device-card'>
<View className='icon-bg orange'>
<Image className='control-icon-img' src={require('../../assets/tab-icons/rings.png')} mode='aspectFit' />
</View>
<Text className='pending-device-title'></Text>
<Text className='pending-device-id'>{deviceId}</Text>
<Text className='pending-device-desc'></Text>
</View>
) : isPendingBinding ? (
<View className='pending-device-card'>
<View className='icon-bg orange'>
<Image className='control-icon-img' src={require('../../assets/tab-icons/rings.png')} mode='aspectFit' />
@@ -555,6 +595,7 @@ export default function Bind() {
)}
<View className='bind-form'>
{!isAddingCard && (
<View className='form-card'>
<Text className='form-title'></Text>
@@ -621,22 +662,24 @@ export default function Bind() {
: '也可以先不创建儿童,直接绑定设备,稍后再关联孩子'}
</Text>
</View>
)}
{!isPendingBinding && (
<View className='form-card'>
<Text className='form-title'></Text>
<Text className='form-title'>{isAddingCard ? '加卡状态' : '设备信息'}</Text>
<View className='field-item'>
<Text className='field-label'></Text>
<Input
className='field-input'
value={deviceId}
disabled={!canEditDeviceFields || isPendingBinding}
placeholder={canEditDeviceFields ? '请输入设备号' : '绑定确认中'}
disabled={isAddingCard || !canEditDeviceFields || isPendingBinding}
placeholder={isAddingCard ? '当前设备' : canEditDeviceFields ? '请输入设备号' : '绑定确认中'}
onInput={(event) => setDeviceId(event.detail.value)}
/>
</View>
{!isAddingCard && (
<View className='field-item'>
<Text className='field-label'></Text>
<Input
@@ -647,6 +690,7 @@ export default function Bind() {
onInput={(event) => setSerialNumber(event.detail.value)}
/>
</View>
)}
{bindHint ? (
<View className='pending-tip'>
@@ -670,9 +714,15 @@ export default function Bind() {
className='submit-btn'
loading={submitting}
disabled={submitting || !canSubmit}
onClick={isExpiredBind && !isPendingBinding ? handleScanCode : handleSubmit}
onClick={isExpiredBind && !isPendingBinding && !isAddingCard ? handleScanCode : handleSubmit}
>
{isPendingBinding ? '完成儿童关联' : isPollingBind ? '等待贴卡确认' : isExpiredBind ? '重新扫码绑定' : '发送绑卡指令'}
{isPendingBinding
? '完成儿童关联'
: isPollingBind
? '等待贴卡确认'
: isExpiredBind
? isAddingCard ? '重新发送加卡指令' : '重新扫码绑定'
: isAddingCard ? '发送加卡指令' : '发送绑卡指令'}
</Button>
</View>
@@ -680,15 +730,15 @@ export default function Bind() {
<Text className='tips-title'></Text>
<View className='tip-item'>
<Text className='tip-number'>1</Text>
<Text className='tip-text'>{isPendingBinding ? '先选择已有儿童,或者在本页新建一个儿童' : '儿童资料可选,不选择时设备会先进入待关联状态'}</Text>
<Text className='tip-text'>{isAddingCard ? '确认当前设备号正确' : isPendingBinding ? '先选择已有儿童,或者在本页新建一个儿童' : '儿童资料可选,不选择时设备会先进入待关联状态'}</Text>
</View>
<View className='tip-item'>
<Text className='tip-number'>2</Text>
<Text className='tip-text'>{isPendingBinding ? '确认当前待关联设备,再选择要关联的孩子' : '扫描设备二维码,确认设备号和序列号正确'}</Text>
<Text className='tip-text'>{isAddingCard ? '点击发送加卡指令后,在设备上贴新卡' : isPendingBinding ? '确认当前待关联设备,再选择要关联的孩子' : '扫描设备二维码,确认设备号和序列号正确'}</Text>
</View>
<View className='tip-item'>
<Text className='tip-number'>3</Text>
<Text className='tip-text'>{isPendingBinding ? '点击完成儿童关联,设备会出现在该孩子下面' : '点击发送绑卡指令,然后在 10 分钟内去设备上贴自己的卡完成确认'}</Text>
<Text className='tip-text'>{isAddingCard ? '加卡成功后,新卡和原来的卡可以执行相同功能' : isPendingBinding ? '点击完成儿童关联,设备会出现在该孩子下面' : '点击发送绑卡指令,然后在 10 分钟内去设备上贴自己的卡完成确认'}</Text>
</View>
</View>
{systemBanner}

View File

@@ -614,6 +614,12 @@ export default function Sleep() {
return
}
if (item.name === '添加设备卡') {
if (!binding?.device_id) return
Taro.navigateTo({ url: `/pages/bind/index?mode=card&device_id=${encodeURIComponent(binding.device_id)}` })
return
}
if (item.name === 'AI 角色') {
handleOpenRoleModal()
return
@@ -713,6 +719,14 @@ export default function Sleep() {
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
arrow: true,
},
{
icon: require('../../assets/tab-icons/rings.png'),
iconBgClass: 'green',
name: '添加设备卡',
value: binding?.device_id ? '给当前设备加卡' : '未绑定',
arrow: true,
disabled: !binding,
},
{
icon: require('../../assets/tab-icons/orange-robot.png'),
iconBgClass: 'green',