refactor(bf-bot): 插件重构为 handlers/services/utils 分层,资源归入 res/

- 旧平铺模块(data/data_utils/database_op/image_builder*/get_bf6_data/
  gametools_bf6/bf6_data/img_utils)拆分为 handlers(绑定/查询)、
  services(fetchers/processing/query)、utils(渲染/参数)分层;
  新增 repository.py / config.py / paths.py
- 字体/图片/模板资源迁入 res/,新增 BF2042 卡面素材(bg/object_icon 等)
- browser_ext/:BF2042 扩展捕获桥(WS 直连兜底通道)源码入库
- 新增 hexi/web/src/plugins/bf_bot 前端管理页
- 新增 tests:test_bf_bot_helpers / repository / tracker_api

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
sansenhoshiandClaude Code committed 2026-10-13 17:53:06 +08:00
1 parent 614e1d7e00
commit a31aab00e8
752 files changed
+6241 -2506

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
import { useCallback, useEffect, useState } from 'react'
import { Button, Card, Spinner, toast } from '@heroui/react'
import { TrashBin } from '@gravity-ui/icons'
import { api } from '../../api/client'
import { PageHead, Pager, tdCls, thCls } from '../../components/ui'
// QQ↔EAID 绑定管理(后端 hexi/plugins/nonebot_plugin_bf_bot/web/admin.py,
// 与群里的「绑定 / 解绑」命令改的是同一份 SQLite 绑定库)
const bf = api('bf_bot')
const PER_PAGE = 10
function data(res: any) {
if (res && typeof res === 'object' && typeof res.status === 'number') {
if (res.status !== 0) throw new Error(res.msg || '请求失败')
return res.data
}
return res
}
interface BindingRow {
qq_id: string
ea_player_name: string
ea_player_id: string
ea_user_id: string
designation: string
create_time: string
}
export default function BfBotPage() {
const [rows, setRows] = useState<BindingRow[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [err, setErr] = useState('')
const [loading, setLoading] = useState(true)
const [deleting, setDeleting] = useState('')
const load = useCallback(async (p: number) => {
setLoading(true); setErr('')
try {
const d = data(await bf.get(`bindings?page=${p}&per_page=${PER_PAGE}`))
setRows((d && d.bindings) || [])
setTotal((d && d.total) || 0)
} catch (e: any) {
setErr(e.message || '加载失败')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { load(page) }, [load, page])
const remove = async (row: BindingRow) => {
if (!window.confirm(`确定解绑 ${row.qq_id}(${row.ea_player_name})?群里需要重新发「绑定 EAID」。`)) return
setDeleting(row.qq_id)
try {
const res = data(await bf.del(`bindings/${encodeURIComponent(row.qq_id)}`))
toast.success(res?.msg || '已解绑')
// 删掉本页最后一条且不在第一页时退回上一页,否则刷新当前页
if (rows.length === 1 && page > 1) setPage(page - 1)
else await load(page)
} catch (e: any) {
toast.danger(e.message || '解绑失败')
} finally {
setDeleting('')
}
}
if (loading && !rows.length) return <div className="flex justify-center py-20"><Spinner /></div>
return (
<div className="space-y-4">
<PageHead
title="BF查询"
actions={
<span className="text-sm text-slate-400">
群里发「绑定 EAID」新增绑定,这里只做查看与解绑
</span>
}
/>
{err && <p className="text-xs text-red-500">{err}</p>}
<Card className="border border-slate-200 bg-white">
<Card.Header>
<Card.Title>
绑定列表
<span className="ml-2 text-xs font-normal text-slate-400">{total} 条</span>
</Card.Title>
</Card.Header>
<Card.Content>
<table className="w-full">
<thead>
<tr>
<th className={thCls}>QQ</th>
<th className={thCls}>EA 名称</th>
<th className={thCls}>EA 双 ID</th>
<th className={thCls}>绑定时间</th>
<th className={thCls}></th>
</tr>
</thead>
<tbody>
{rows.map(r => (
<tr key={r.qq_id} className="transition-colors hover:bg-slate-50">
<td className={tdCls + ' font-mono text-xs'}>{r.qq_id}</td>
<td className={tdCls}>{r.ea_player_name || '-'}</td>
<td className={tdCls + ' font-mono text-xs'}>
{r.ea_player_id || '-'} / {r.ea_user_id || '-'}
</td>
<td className={tdCls + ' whitespace-nowrap text-xs text-slate-400'}>
{r.create_time || '-'}
</td>
<td className={tdCls + ' whitespace-nowrap text-right'}>
<Button
size="sm" variant="tertiary" className="text-red-500"
isDisabled={deleting === r.qq_id}
onPress={() => remove(r)}
>
<TrashBin className="size-4" />
{deleting === r.qq_id ? '解绑中…' : '解绑'}
</Button>
</td>
</tr>
))}
{!rows.length && (
<tr>
<td colSpan={5} className={tdCls + ' text-center text-slate-400'}>
{loading ? '加载中…' : '还没有任何绑定,群里 @机器人 发「绑定 EAID」即可'}
</td>
</tr>
)}
</tbody>
</table>
<Pager page={page} total={total} perPage={PER_PAGE} onPage={setPage} />
</Card.Content>
</Card>
</div>
)
}