Files
hera-web/src/components/DeityBinding.tsx
T

101 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react'
import Taro from '@tarojs/taro'
import { Button, Image, Text, View } from '@tarojs/components'
import { deityCatalog, getDeity, isTiangongDemo, tiangongMock } from '../services/tiangong'
import { DeityId } from '../utils/tiangongMock'
import { useTiangongSnapshot } from '../hooks/useTiangongSnapshot'
import { resolveAssetUrl } from '../services/request'
import { openWechatCustomerService } from '../services/customerService'
import './DeityBinding.scss'
export function BindingGate({ onBind, lock = true }: { onBind: () => void; lock?: boolean }) {
return <View className='binding-gate' catchMove>
<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={openWechatCustomerService}>联系我们</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 [failedImage, setFailedImage] = useState('')
const working = useRef(false)
const demo = isTiangongDemo()
const art = demo ? deity.icon : resolveAssetUrl(state.iconUrl)
const canShowArt = !!art && failedImage !== art
const perform = (action: () => void) => {
if (!demo || 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={demo && mode === 'bind' ? '确认绑定神仙' : state.unlocked ? '已绑定神仙详情' : '神仙信息'}>
<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>
{demo && mode === 'view' && <Button className='binding-more' aria-label='神仙更多操作' onClick={() => setMenu(!menu)}>•••</Button>}
</View>
{demo && menu && <Button className='binding-unbind-menu' onClick={() => { setMenu(false); setConfirmUnbind(true) }}>解绑神仙</Button>}
<View className='binding-art-frame'>{canShowArt ? <Image className='binding-card-art' src={art} mode='aspectFit' onError={() => setFailedImage(art)} /> : <Text className='binding-card-image-note'>{art ? '图片加载失败' : '图片暂未配置'}</Text>}</View>
<Text className='binding-card-subtitle'>守护神仙 · {deity.name}</Text>
{demo ? <><Text className='binding-card-code'>唯一 ID:DEMO-{deityId.toUpperCase()}-001</Text><Text className='binding-card-note'>Mock 原型演示 · 不连接真实 NFC</Text></> : state.holdingId ? <Text className='binding-card-code'>持有记录:{state.holdingId}</Text> : null}
{demo && 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>
</> : state.unlocked ? <View className='binding-guarded'><View className='binding-shield'>✓</View><Text>已守护 <Text className='binding-days'>{state.days}</Text> 天</Text></View> : <Text className='binding-card-note'>请通过 NFC 绑定神仙。</Text>}
{error && <Text className='binding-error'>{error}</Text>}
</View>
<Button className='binding-card-close' aria-label='关闭神仙卡片' onClick={onClose}>×</Button>
</View>
{demo && 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()
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>
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>
}