Compare commits

...

2 Commits

Author SHA1 Message Date
stu2not
aea7a3fe42 fix: show placeholder for zero battery 2026-06-02 14:40:44 +08:00
stu2not
57bafb003d feat: support assigning child to existing device 2026-06-02 14:40:30 +08:00
4 changed files with 148 additions and 22 deletions

View File

@@ -68,6 +68,41 @@
position: relative; position: relative;
} }
.pending-device-card {
margin: 0 48px 48px;
background: #FFFFFF;
border-radius: 24px;
padding: 48px 40px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
}
.pending-device-title {
font-size: 34px;
font-weight: 700;
color: #1A1A1A;
margin-bottom: 14px;
}
.pending-device-id {
max-width: 100%;
font-size: 28px;
color: #FF8C42;
font-weight: 600;
word-break: break-all;
text-align: center;
margin-bottom: 12px;
}
.pending-device-desc {
font-size: 26px;
color: #666666;
text-align: center;
line-height: 1.5;
}
.scan-frame { .scan-frame {
background: #FFFFFF; background: #FFFFFF;
border-radius: 24px; border-radius: 24px;

View File

@@ -350,26 +350,37 @@ export default function Bind() {
<Text className='title'>{isPendingBinding ? '补全绑定' : '扫码贴卡绑定设备'}</Text> <Text className='title'>{isPendingBinding ? '补全绑定' : '扫码贴卡绑定设备'}</Text>
</View> </View>
<View className='scan-area'> {isPendingBinding ? (
<View className='scan-frame' onClick={handleScanCode}> <View className='pending-device-card'>
<View className='icon-bg orange'> <View className='icon-bg orange'>
<Image className='control-icon-img' src={require('../../assets/tab-icons/rings.png')} mode='aspectFit' /> <Image className='control-icon-img' src={require('../../assets/tab-icons/rings.png')} mode='aspectFit' />
</View> </View>
<Text className='scan-text'> <Text className='pending-device-title'></Text>
{!canEditDeviceFields ? '请先完成儿童资料' : isScanning ? '正在扫码...' : '点击扫码'} <Text className='pending-device-id'>{pendingDeviceId}</Text>
</Text> <Text className='pending-device-desc'></Text>
<Text className='scan-hint'>
{!canEditDeviceFields ? '先选儿童,再扫设备二维码' : '扫描设备二维码或条码'}
</Text>
</View> </View>
) : (
<View className='scan-area'>
<View className='scan-frame' onClick={handleScanCode}>
<View className='icon-bg orange'>
<Image className='control-icon-img' src={require('../../assets/tab-icons/rings.png')} mode='aspectFit' />
</View>
<Text className='scan-text'>
{!canEditDeviceFields ? '请先完成儿童资料' : isScanning ? '正在扫码...' : '点击扫码'}
</Text>
<Text className='scan-hint'>
{!canEditDeviceFields ? '先选儿童,再扫设备二维码' : '扫描设备二维码或条码'}
</Text>
</View>
<View className='scan-corners'> <View className='scan-corners'>
<View className='corner top-left'></View> <View className='corner top-left'></View>
<View className='corner top-right'></View> <View className='corner top-right'></View>
<View className='corner bottom-left'></View> <View className='corner bottom-left'></View>
<View className='corner bottom-right'></View> <View className='corner bottom-right'></View>
</View>
</View> </View>
</View> )}
<View className='bind-form'> <View className='bind-form'>
<View className='form-card'> <View className='form-card'>
@@ -413,12 +424,17 @@ export default function Bind() {
</View> </View>
<Text className='field-hint'> <Text className='field-hint'>
{children.length > 0 {isPendingBinding
? '可以选已有儿童,也可以在这里新建一个儿童后再绑定设备' ? children.length > 0
: '当前还没有儿童资料,请先创建一个儿童资料'} ? '可以选已有儿童,也可以新建一个儿童后关联当前设备'
: '当前还没有儿童资料,请先创建一个儿童资料后关联设备'
: children.length > 0
? '可以选已有儿童,也可以在这里新建一个儿童后再绑定设备'
: '当前还没有儿童资料,请先创建一个儿童资料'}
</Text> </Text>
</View> </View>
{!isPendingBinding && (
<View className='form-card'> <View className='form-card'>
<Text className='form-title'></Text> <Text className='form-title'></Text>
@@ -457,6 +473,7 @@ export default function Bind() {
</View> </View>
)} )}
</View> </View>
)}
<Button <Button
className='submit-btn' className='submit-btn'
@@ -476,11 +493,11 @@ export default function Bind() {
</View> </View>
<View className='tip-item'> <View className='tip-item'>
<Text className='tip-number'>2</Text> <Text className='tip-number'>2</Text>
<Text className='tip-text'></Text> <Text className='tip-text'>{isPendingBinding ? '确认当前待关联设备,再选择要关联的孩子' : '扫描设备二维码,确认设备号和序列号正确'}</Text>
</View> </View>
<View className='tip-item'> <View className='tip-item'>
<Text className='tip-number'>3</Text> <Text className='tip-number'>3</Text>
<Text className='tip-text'></Text> <Text className='tip-text'>{isPendingBinding ? '点击完成儿童关联,设备会出现在该孩子下面' : '点击发送绑卡指令,然后去设备上贴自己的卡完成确认'}</Text>
</View> </View>
</View> </View>
{systemBanner} {systemBanner}

View File

@@ -41,6 +41,14 @@ function getSignalLabel(value?: number | null): string {
return '弱' return '弱'
} }
function formatBatteryDisplay(value?: number | null): { text: string; showPercent: boolean } {
if (value === null || value === undefined) return { text: '--', showPercent: false }
const normalized = Number(value)
if (!Number.isFinite(normalized)) return { text: '--', showPercent: false }
if (normalized === 0) return { text: '*', showPercent: false }
return { text: String(normalized), showPercent: true }
}
function formatSleepRange(status: DeviceStatus | null): string { function formatSleepRange(status: DeviceStatus | null): string {
const start = status?.disable_time_start?.trim() const start = status?.disable_time_start?.trim()
const end = status?.disable_time_end?.trim() const end = status?.disable_time_end?.trim()
@@ -284,7 +292,7 @@ export default function Device() {
const childName = child.child_name || '未设置' const childName = child.child_name || '未设置'
const pageTitle = `${childName} 的伴伴` const pageTitle = `${childName} 的伴伴`
const batteryValue = deviceStatus?.power ?? deviceStatus?.battery_pct ?? null const batteryValue = deviceStatus?.power ?? deviceStatus?.battery_pct ?? null
const batteryDisplay = batteryValue === null || batteryValue === undefined ? '--' : String(batteryValue) const batteryDisplay = formatBatteryDisplay(batteryValue)
const signalLabel = getSignalLabel(deviceStatus?.signal) const signalLabel = getSignalLabel(deviceStatus?.signal)
const versionLabel = deviceStatus?.version || '--' const versionLabel = deviceStatus?.version || '--'
const coordinateLabel = formatCoordinates(deviceStatus) const coordinateLabel = formatCoordinates(deviceStatus)
@@ -307,8 +315,8 @@ export default function Device() {
</View> </View>
<View className='battery-section'> <View className='battery-section'>
<Text className='battery-percent'> <Text className='battery-percent'>
{batteryDisplay} {batteryDisplay.text}
{batteryDisplay !== '--' && <Text className='percent'>%</Text>} {batteryDisplay.showPercent && <Text className='percent'>%</Text>}
</Text> </Text>
<Text className='battery-label'></Text> <Text className='battery-label'></Text>
</View> </View>

View File

@@ -68,6 +68,7 @@ export default function Sleep() {
const [showDeleteChildModal, setShowDeleteChildModal] = useState(false) const [showDeleteChildModal, setShowDeleteChildModal] = useState(false)
const [showRoleModal, setShowRoleModal] = useState(false) const [showRoleModal, setShowRoleModal] = useState(false)
const [showFamilyModal, setShowFamilyModal] = useState(false) const [showFamilyModal, setShowFamilyModal] = useState(false)
const [showAssignDeviceModal, setShowAssignDeviceModal] = useState(false)
const [modalType, setModalType] = useState<'add' | 'edit'>('add') const [modalType, setModalType] = useState<'add' | 'edit'>('add')
const [childName, setChildName] = useState('') const [childName, setChildName] = useState('')
const [editingChildId, setEditingChildId] = useState<number | null>(null) const [editingChildId, setEditingChildId] = useState<number | null>(null)
@@ -334,9 +335,17 @@ export default function Sleep() {
const handleOpenAddChildFromSwitch = () => { const handleOpenAddChildFromSwitch = () => {
setShowChildModal(false) setShowChildModal(false)
setShowAssignDeviceModal(false)
handleOpenModal('add') handleOpenModal('add')
} }
const handleAssignExistingDevice = (targetBinding: BindingListItem) => {
if (!targetBinding?.device_id) return
setSelectedBindingDeviceId(targetBinding.device_id)
setShowAssignDeviceModal(false)
Taro.navigateTo({ url: '/pages/bind/index' })
}
const handleSubmitModal = async () => { const handleSubmitModal = async () => {
const normalizedName = childName.trim() const normalizedName = childName.trim()
if (!normalizedName) { if (!normalizedName) {
@@ -544,6 +553,11 @@ export default function Sleep() {
return return
} }
if (item.name === '关联现有设备') {
setShowAssignDeviceModal(true)
return
}
if (item.name === '绑定设备') { if (item.name === '绑定设备') {
if (!currentChild) { if (!currentChild) {
handleOpenModal('add') handleOpenModal('add')
@@ -584,6 +598,7 @@ export default function Sleep() {
} }
const parentDisplayName = parentInfo.nickname?.trim() || '家长' const parentDisplayName = parentInfo.nickname?.trim() || '家长'
const unassignedBindings = bindings.filter((item) => item.device_id && item.child_id === null)
const currentFirmwareLabel = firmwareStatus?.current_version || '--' const currentFirmwareLabel = firmwareStatus?.current_version || '--'
const latestFirmwareLabel = firmwareStatus?.latest_version || '--' const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
const currentUserId = getCurrentUserId() const currentUserId = getCurrentUserId()
@@ -637,6 +652,15 @@ export default function Sleep() {
arrow: true, arrow: true,
disabled: children.length === 0, disabled: children.length === 0,
}, },
...(unassignedBindings.length > 0
? [{
icon: require('../../assets/tab-icons/rings.png'),
iconBgClass: 'green',
name: '关联现有设备',
value: `${unassignedBindings.length} 台待关联`,
arrow: true,
}]
: []),
{ {
icon: require('../../assets/tab-icons/rings.png'), icon: require('../../assets/tab-icons/rings.png'),
iconBgClass: 'green', iconBgClass: 'green',
@@ -856,6 +880,48 @@ export default function Sleep() {
</View> </View>
)} )}
{showAssignDeviceModal && (
<View className='modal-mask' onClick={() => setShowAssignDeviceModal(false)}>
<View
className='modal-card device-switch-card'
onClick={(event) => {
event.stopPropagation()
}}
>
<Text className='modal-title'></Text>
{unassignedBindings.length === 0 ? (
<Text className='device-switch-empty'></Text>
) : (
<View className='device-switch-list'>
{unassignedBindings.map((item) => (
<View
key={item.device_id}
className='device-switch-item'
onClick={() => handleAssignExistingDevice(item)}
>
<View className='device-switch-head'>
<Text className='device-switch-id'>{item.device_id}</Text>
<Text className='device-switch-tag'></Text>
</View>
<Text className='device-switch-name'></Text>
</View>
))}
</View>
)}
<View className='device-switch-footer'>
<Text className='device-switch-add' onClick={handleOpenAddChildFromSwitch}>
+
</Text>
</View>
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={() => setShowAssignDeviceModal(false)}>
</Text>
</View>
</View>
</View>
)}
{showModal && ( {showModal && (
<View className='modal-mask'> <View className='modal-mask'>
<View className='modal-card'> <View className='modal-card'>