2026-10-17 22:44:09 +08:00
|
|
|
|
import { useRef, useState } from 'react'
|
|
|
|
|
|
import Taro from '@tarojs/taro'
|
|
|
|
|
|
import { Button, Image, Text, View } from '@tarojs/components'
|
2026-10-18 23:35:28 +08:00
|
|
|
|
import { deityCatalog, getDeity, isTiangongDemo, tiangongMock } from '../services/tiangong'
|
2026-10-17 22:44:09 +08:00
|
|
|
|
import { DeityId } from '../utils/tiangongMock'
|
|
|
|
|
|
import { useTiangongSnapshot } from '../hooks/useTiangongSnapshot'
|
|
|
|
|
|
import './DeityBinding.scss'
|
|
|
|
|
|
|
|
|
|
|
|
export function BindingGate({ onBind, lock = true }: { onBind: () => void; lock?: boolean }) {
|
2026-10-18 23:35:28 +08:00
|
|
|
|
return <View className='binding-gate' catchMove>
|
2026-10-17 22:44:09 +08:00
|
|
|
|
<View className='binding-gate-content'>
|
|
|
|
|
|
{lock && <View className='binding-lock'><View /></View>}
|
|
|
|
|
|
<Text>须绑定神仙</Text><Text>开启你的守护之旅</Text>
|
|
|
|
|
|
<View className='binding-gate-actions'>
|
|
|
|
|
|
<Button className='binding-primary' onClick={onBind}>绑定神仙</Button>
|
|
|
|
|
|
<Button className='binding-outline' onClick={() => Taro.showToast({ title: '企业微信入口待配置', icon: 'none' })}>联系我们</Button>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function DeityCardDialog({ deityId, mode = 'view', onClose }: { deityId: DeityId; mode?: 'bind' | 'view'; onClose: () => void }) {
|
|
|
|
|
|
const snapshot = useTiangongSnapshot()
|
|
|
|
|
|
const deity = getDeity(deityId)!
|
|
|
|
|
|
const state = snapshot.deities[deityId]
|
|
|
|
|
|
const [menu, setMenu] = useState(false)
|
|
|
|
|
|
const [confirmUnbind, setConfirmUnbind] = useState(false)
|
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
const working = useRef(false)
|
2026-10-18 23:35:28 +08:00
|
|
|
|
if (!isTiangongDemo()) return <View className='binding-dialog-mask' catchMove><View className='binding-picker' role='dialog' aria-label='神仙信息'>
|
|
|
|
|
|
<Text className='binding-confirm-title'>{deity.name}</Text>
|
|
|
|
|
|
<Image className='binding-card-art' src={deity.icon} mode='aspectFit' />
|
|
|
|
|
|
<Text className='binding-picker-note'>{state.unlocked ? `已守护 ${state.days} 天 · 持有记录 ${state.holdingId}` : '请通过 NFC 绑定神仙。此处不模拟建立真实绑定关系。'}</Text>
|
|
|
|
|
|
<Button className='binding-outline binding-picker-cancel' onClick={onClose}>关闭</Button>
|
|
|
|
|
|
</View></View>
|
2026-10-17 22:44:09 +08:00
|
|
|
|
const perform = (action: () => void) => {
|
|
|
|
|
|
if (working.current) return
|
|
|
|
|
|
working.current = true
|
|
|
|
|
|
try { action(); onClose() } catch (cause) { setError(cause instanceof Error ? cause.message : '模拟操作失败') }
|
|
|
|
|
|
finally { working.current = false }
|
|
|
|
|
|
}
|
|
|
|
|
|
return <View className='binding-dialog-mask' catchMove>
|
|
|
|
|
|
<View className='binding-dialog-stack'>
|
|
|
|
|
|
<View className='binding-gold-card' role='dialog' aria-label={mode === 'bind' ? '确认绑定神仙' : '已绑定神仙详情'}>
|
|
|
|
|
|
<View className='binding-card-hook' /><View className='binding-card-corner top' /><View className='binding-card-corner bottom' />
|
|
|
|
|
|
<View className='binding-card-title-row'><Text className='binding-card-spark'>✦</Text><Text className='binding-card-name'>{deity.name}</Text><Text className='binding-card-spark'>✦</Text>
|
|
|
|
|
|
{mode === 'view' && <Button className='binding-more' aria-label='神仙更多操作' onClick={() => setMenu(!menu)}>•••</Button>}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{menu && <Button className='binding-unbind-menu' onClick={() => { setMenu(false); setConfirmUnbind(true) }}>解绑神仙</Button>}
|
|
|
|
|
|
<View className='binding-art-frame'><Image className='binding-card-art' src={deity.icon} mode='aspectFit' /></View>
|
|
|
|
|
|
<Text className='binding-card-subtitle'>守护神仙 · {deity.name}</Text>
|
|
|
|
|
|
<Text className='binding-card-code'>唯一 ID:DEMO-{deityId.toUpperCase()}-001</Text>
|
|
|
|
|
|
<Text className='binding-card-note'>Mock 原型演示 · 不连接真实 NFC</Text>
|
|
|
|
|
|
{mode === 'bind' ? <>
|
|
|
|
|
|
<Text className='binding-card-warning'>每位神仙只能绑定一个微信号哦</Text>
|
|
|
|
|
|
<Button className='binding-primary binding-card-confirm' onClick={() => perform(() => {
|
|
|
|
|
|
tiangongMock.invite(deityId)
|
|
|
|
|
|
Taro.showToast({ title: '模拟绑定成功', icon: 'none' })
|
|
|
|
|
|
})}>确认绑定</Button>
|
|
|
|
|
|
</> : <View className='binding-guarded'><View className='binding-shield'>✓</View><Text>已守护 <Text className='binding-days'>{state.days}</Text> 天</Text></View>}
|
|
|
|
|
|
{error && <Text className='binding-error'>{error}</Text>}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Button className='binding-card-close' aria-label='关闭神仙卡片' onClick={onClose}>×</Button>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{confirmUnbind && <View className='binding-confirm-mask'>
|
|
|
|
|
|
<View className='binding-confirm-panel' role='dialog' aria-label='解绑神仙确认'>
|
|
|
|
|
|
<Text className='binding-confirm-title'><Text className='binding-alert'>ⓘ</Text> 确定要解绑吗?</Text>
|
|
|
|
|
|
<View className='binding-confirm-copy'>
|
|
|
|
|
|
<Text>• 解绑后「我的」里面不再展示该神仙</Text>
|
|
|
|
|
|
<Text>• 该神仙今年还剩 <Text className='binding-quota'>{state.remainingUnbinds} 次</Text> 解绑机会</Text>
|
|
|
|
|
|
<Text>• 用神仙碰触手机 NFC 可再次绑定</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{state.remainingUnbinds === 0 && <Text className='binding-error'>该神仙今年的解绑次数已用尽,暂不可解绑。</Text>}
|
|
|
|
|
|
<Text className='binding-demo-note'>本次仅模拟;取消不会扣减次数。</Text>
|
|
|
|
|
|
<View className='binding-confirm-actions'>
|
|
|
|
|
|
<Button className={`binding-primary ${state.remainingUnbinds === 0 ? 'binding-disabled' : ''}`} disabled={state.remainingUnbinds === 0} onClick={() => perform(() => {
|
|
|
|
|
|
tiangongMock.unbind(deityId)
|
|
|
|
|
|
Taro.showToast({ title: '模拟解绑成功', icon: 'none' })
|
|
|
|
|
|
})}>确定解绑</Button>
|
|
|
|
|
|
<Button className='binding-outline' onClick={() => { setConfirmUnbind(false); setError('') }}>取消</Button>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{error && <Text className='binding-error'>{error}</Text>}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function DeityBindingFlow({ initialDeity, onClose }: { initialDeity?: DeityId; onClose: () => void }) {
|
|
|
|
|
|
const [selected, setSelected] = useState<DeityId | null>(initialDeity || null)
|
|
|
|
|
|
const snapshot = useTiangongSnapshot()
|
2026-10-18 23:35:28 +08:00
|
|
|
|
if (!isTiangongDemo()) return <View className='binding-dialog-mask' catchMove><View className='binding-picker' role='dialog' aria-label='绑定神仙说明'><Text className='binding-confirm-title'>碰一碰,绑定神仙</Text><Text className='binding-picker-note'>请通过 NFC 绑定神仙,本次不在页面中模拟真实设备绑定。</Text><Button className='binding-outline binding-picker-cancel' onClick={onClose}>关闭</Button></View></View>
|
2026-10-17 22:44:09 +08:00
|
|
|
|
if (selected) return <DeityCardDialog deityId={selected} mode='bind' onClose={onClose} />
|
|
|
|
|
|
const available = deityCatalog.filter(item => !snapshot.deities[item.id].unlocked)
|
|
|
|
|
|
return <View className='binding-dialog-mask' catchMove><View className='binding-picker' role='dialog' aria-label='模拟 NFC 识别'>
|
|
|
|
|
|
<Text className='binding-confirm-title'>碰一碰,绑定神仙</Text>
|
|
|
|
|
|
<Text className='binding-picker-note'>Mock 演示:选择要模拟识别的神仙。不会开启 NFC、连接设备或绑定真实微信号。</Text>
|
|
|
|
|
|
<View className='binding-picker-grid'>{available.map(deity => <Button key={deity.id} className='binding-picker-item' aria-label={`模拟识别${deity.name}`} onClick={() => setSelected(deity.id)}><Image className='binding-picker-art' src={deity.icon} mode='aspectFit' /><Text>{deity.name}</Text></Button>)}</View>
|
|
|
|
|
|
{!available.length && <Text className='binding-picker-note'>当前示例中的神仙均已绑定。</Text>}
|
|
|
|
|
|
<Button className='binding-outline binding-picker-cancel' onClick={onClose}>取消</Button>
|
|
|
|
|
|
</View></View>
|
|
|
|
|
|
}
|