支持绑定卡片管理和名称编辑

This commit is contained in:
stu2not
2026-06-08 10:48:30 +08:00
parent d33212613b
commit dcff337f43
6 changed files with 541 additions and 1 deletions

View File

@@ -328,6 +328,123 @@
}
}
.bound-cards-card {
margin: 0 24px 24px;
border-radius: 20px;
background: #FFFFFF;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
overflow: hidden;
&.disabled {
opacity: 0.72;
}
}
.bound-cards-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px;
background: #F8FAFC;
}
.bound-cards-heading {
flex: 1;
min-width: 0;
}
.bound-cards-title {
display: block;
font-size: 30px;
font-weight: 700;
color: #1F2937;
}
.bound-cards-subtitle {
display: block;
margin-top: 8px;
font-size: 24px;
line-height: 1.5;
color: #6B7280;
word-break: break-all;
}
.bound-cards-count {
flex-shrink: 0;
padding: 8px 16px;
border-radius: 999px;
font-size: 24px;
font-weight: 700;
color: #14532D;
background: #DCFCE7;
}
.bound-card-list {
padding: 4px 24px 20px;
}
.bound-card-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 18px 0;
border-top: 1px dashed #E5E7EB;
&:first-child {
border-top: none;
}
}
.bound-card-main {
flex: 1;
min-width: 0;
}
.bound-card-name {
display: block;
font-size: 28px;
line-height: 1.45;
font-weight: 700;
color: #1F2937;
word-break: break-all;
&.empty {
font-weight: 600;
color: #94A3B8;
}
}
.bound-card-uuid {
display: block;
margin-top: 6px;
font-size: 22px;
line-height: 1.45;
color: #6B7280;
word-break: break-all;
}
.bound-card-edit {
flex-shrink: 0;
padding: 10px 18px;
border-radius: 14px;
background: #FFF2E7;
font-size: 24px;
font-weight: 700;
color: #FF8C42;
}
.bound-cards-empty {
padding: 24px;
}
.bound-cards-empty-text {
font-size: 24px;
line-height: 1.6;
color: #9CA3AF;
}
.menu-item.disabled {
opacity: 0.45;
}
@@ -410,6 +527,18 @@
color: #FF8C42;
font-weight: 600;
}
&.disabled {
color: #CBD5E1;
}
}
.card-name-modal-tip {
display: block;
margin-top: 14px;
font-size: 24px;
line-height: 1.5;
color: #94A3B8;
}
.device-switch-card {

View File

@@ -6,9 +6,13 @@ import { DEVICE_UNAVAILABLE_MESSAGE } from '@/services/api'
import { getCurrentUserId } from '@/services/session'
import {
BindingListItem,
BindingCardItem,
BindingCardGroupItem,
clearSelectedBindingDeviceId,
getBindingCardGroups,
loadCurrentChildBindingContext,
setSelectedBindingDeviceId,
updateBindingCardName,
unbindDevice,
} from '@/services/binding'
import { Child, clearSelectedChildId, createChild, deleteChild, setSelectedChildId, updateChild } from '@/services/child'
@@ -53,6 +57,8 @@ export default function Sleep() {
const [firmwareStatus, setFirmwareStatus] = useState<DeviceFirmwareStatus | null>(null)
const [deviceRole, setDeviceRole] = useState<DeviceCurrentRole | null>(null)
const [roles, setRoles] = useState<DeviceRoleSummary[]>([])
const [currentCardGroup, setCurrentCardGroup] = useState<BindingCardGroupItem | null>(null)
const [isLoadingCards, setIsLoadingCards] = useState(false)
const [isLoadingFirmware, setIsLoadingFirmware] = useState(false)
const [isUpdatingFirmware, setIsUpdatingFirmware] = useState(false)
const [isLoadingRoles, setIsLoadingRoles] = useState(false)
@@ -69,9 +75,13 @@ export default function Sleep() {
const [showRoleModal, setShowRoleModal] = useState(false)
const [showFamilyModal, setShowFamilyModal] = useState(false)
const [showAssignDeviceModal, setShowAssignDeviceModal] = useState(false)
const [showCardNameModal, setShowCardNameModal] = useState(false)
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
const [childName, setChildName] = useState('')
const [editingChildId, setEditingChildId] = useState<number | null>(null)
const [editingCard, setEditingCard] = useState<BindingCardItem | null>(null)
const [cardName, setCardName] = useState('')
const [isUpdatingCardName, setIsUpdatingCardName] = useState(false)
const [parentInfo, setParentInfo] = useState<Parent | null>(null)
const [isAuthorizingPhone, setIsAuthorizingPhone] = useState(false)
@@ -107,6 +117,7 @@ export default function Sleep() {
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
void loadCurrentDeviceCards(nextBinding?.device_id)
} catch (error: any) {
console.error('[manage] load failed:', error)
Taro.showToast({
@@ -215,6 +226,31 @@ export default function Sleep() {
}
}
const loadCurrentDeviceCards = async (deviceId?: string | null) => {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
setCurrentCardGroup(null)
setIsLoadingCards(false)
return
}
setIsLoadingCards(true)
try {
const nextCardGroups = await getBindingCardGroups()
const nextCurrentGroup = nextCardGroups.items.find((item) => item.device_id === normalizedDeviceId) || null
setCurrentCardGroup(nextCurrentGroup)
} catch (error: any) {
console.error('[manage] card groups load failed:', error)
setCurrentCardGroup(null)
Taro.showToast({
title: error?.message || '卡片信息加载失败',
icon: 'none',
})
} finally {
setIsLoadingCards(false)
}
}
const handleOpenFamilyModal = () => {
if (!binding?.device_id) {
Taro.showToast({ title: '当前没有可用设备', icon: 'none' })
@@ -338,6 +374,7 @@ export default function Sleep() {
void loadFirmwareStatus(nextBinding?.device_id)
void loadRoleData(nextBinding?.device_id)
void loadFamilyData(nextBinding?.device_id)
void loadCurrentDeviceCards(nextBinding?.device_id)
setShowChildModal(false)
Taro.showToast({
title: '已切换当前孩子',
@@ -351,6 +388,48 @@ export default function Sleep() {
handleOpenModal('add')
}
const handleOpenCardNameModal = (card: BindingCardItem) => {
setEditingCard(card)
setCardName(card.card_name || '')
setShowCardNameModal(true)
}
const handleCloseCardNameModal = () => {
if (isUpdatingCardName) return
setShowCardNameModal(false)
setEditingCard(null)
setCardName('')
}
const handleSubmitCardName = async () => {
if (!editingCard || isUpdatingCardName) return
const normalizedName = cardName.trim()
if (normalizedName.length > 64) {
Taro.showToast({ title: '卡片名称最多 64 个字', icon: 'none' })
return
}
setIsUpdatingCardName(true)
try {
await updateBindingCardName(editingCard.card_id, {
card_name: normalizedName || null,
})
Taro.showToast({ title: '已保存', icon: 'success' })
setShowCardNameModal(false)
setEditingCard(null)
setCardName('')
await loadCurrentDeviceCards(binding?.device_id)
} catch (error: any) {
Taro.showToast({
title: error?.message || '卡片名称保存失败',
icon: 'none',
})
} finally {
setIsUpdatingCardName(false)
}
}
const handleAssignExistingDevice = (targetBinding: BindingListItem) => {
if (!targetBinding?.device_id) return
setSelectedBindingDeviceId(targetBinding.device_id)
@@ -654,6 +733,12 @@ export default function Sleep() {
const latestFirmwareLabel = firmwareStatus?.latest_version || '--'
const currentUserId = getCurrentUserId()
const isFamilyOwner = familyInfo?.current_user_role === 'owner'
const canEditCardName = Boolean(
binding?.device_id &&
currentCardGroup?.device_id === binding.device_id &&
familyInfo?.device_id === binding.device_id &&
isFamilyOwner
)
const canUpdateFirmware = Boolean(binding?.device_id && firmwareStatus?.can_update && !isLoadingFirmware && !isUpdatingFirmware)
const firmwareActionText = isLoadingFirmware ? '查询中' : isUpdatingFirmware ? '发送中' : '更新'
const roleValue = !binding?.device_id
@@ -832,6 +917,50 @@ export default function Sleep() {
</View>
</View>
<View className={`bound-cards-card ${binding?.device_id ? '' : 'disabled'}`}>
<View className='bound-cards-header'>
<View className='bound-cards-heading'>
<Text className='bound-cards-title'></Text>
<Text className='bound-cards-subtitle'>{binding?.device_id || '当前未绑定设备'}</Text>
</View>
<Text className='bound-cards-count'>
{isLoadingCards ? '查询中' : `${currentCardGroup?.card_count || 0}`}
</Text>
</View>
{!binding?.device_id ? (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'> UUID</Text>
</View>
) : isLoadingCards ? (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'>...</Text>
</View>
) : currentCardGroup?.cards.length ? (
<View className='bound-card-list'>
{currentCardGroup.cards.map((card) => (
<View className='bound-card-item' key={`${currentCardGroup.device_id}-${card.card_id}`}>
<View className='bound-card-main'>
<Text className={`bound-card-name ${card.card_name ? '' : 'empty'}`}>
{card.card_name || '未命名卡片'}
</Text>
<Text className='bound-card-uuid'>{card.card_uuid}</Text>
</View>
{canEditCardName && (
<Text className='bound-card-edit' onClick={() => handleOpenCardNameModal(card)}>
</Text>
)}
</View>
))}
</View>
) : (
<View className='bound-cards-empty'>
<Text className='bound-cards-empty-text'></Text>
</View>
)}
</View>
<View className='menu-card'>
{menuItems.map((item, index) => (
<View key={index}>
@@ -1018,6 +1147,32 @@ export default function Sleep() {
</View>
</View>
)}
{showCardNameModal && (
<View className='modal-mask'>
<View className='modal-card'>
<Text className='modal-title'></Text>
<View className='modal-input-wrap'>
<Input
className='modal-input'
value={cardName}
placeholder='例如 奶奶的卡'
focus
disabled={isUpdatingCardName}
onInput={(event) => setCardName(event.detail.value)}
/>
</View>
<Text className='card-name-modal-tip'></Text>
<View className='modal-actions'>
<Text className='modal-action cancel' onClick={handleCloseCardNameModal}>
</Text>
<Text className={`modal-action confirm ${isUpdatingCardName ? 'disabled' : ''}`} onClick={handleSubmitCardName}>
{isUpdatingCardName ? '保存中...' : '确认'}
</Text>
</View>
</View>
</View>
)}
{showRoleModal && (
<View className='modal-mask' onClick={() => setShowRoleModal(false)}>
<View

View File

@@ -28,6 +28,29 @@ export interface BindingListResponse {
next_cursor?: number | null
}
export interface BindingCardItem {
card_id: number
card_uuid: string
card_name?: string | null
status: number
updated_at?: string | null
}
export interface BindingCardGroupItem {
device_id: string
child_id: number | null
child_name?: string | null
bound_at: string
card_count: number
cards: BindingCardItem[]
}
export interface BindingCardGroupListResponse {
items: BindingCardGroupItem[]
total_devices: number
total_cards: number
}
export interface BindingChildContext {
currentChild: Child | null
currentBinding: Binding | null
@@ -111,6 +134,20 @@ export async function getBindings(cursor?: number, limit: number = 20): Promise<
return request<BindingListResponse>(`/banban/bindings?${query}`)
}
export async function getBindingCardGroups(): Promise<BindingCardGroupListResponse> {
return request<BindingCardGroupListResponse>('/banban/bindings/cards')
}
export async function updateBindingCardName(
cardId: number,
data: { card_name?: string | null }
): Promise<BindingCardItem> {
return request<BindingCardItem>(`/banban/bindings/cards/${cardId}`, {
method: 'PATCH',
data,
})
}
export async function getBinding(deviceId: string): Promise<Binding | null> {
try {
return await request<Binding>(`/banban/bindings/${deviceId}`)