feat(gui): 优化双时间日志呈现与账号兑换记录弹窗明细查看
- 参考斗鱼重构日志双时间展示:本地时间与虎牙估算时间拆分,青色高亮 Tag 醒目显示 H:xx:xx:xx.xxx,排版自适应不折行;新增一键复制带双时间格式日志与清空功能 - 增强账号兑换记录交互:账号列表新增专属「记录」按钮,点击弹出专属兑换记录明细 Modal(支持查看全部历史订单、缩略图、积分、格式化时间,单号一键复制) - 优化顶部批量「兑换记录」流程:查询完成后自动切至「兑换记录」Tab(由原'复盘'重命名),并显示总记录条数与友好空状态提示 - 验证:Vite 前端打包与 Go 全套单测均干净通过
This commit is contained in:
+318
-22
@@ -23,6 +23,7 @@ import {
|
||||
ClipboardPaste,
|
||||
Copy,
|
||||
FolderOpen,
|
||||
History,
|
||||
ListChecks,
|
||||
Pencil,
|
||||
Play,
|
||||
@@ -747,6 +748,14 @@ export default function Dashboard() {
|
||||
const [productSearch, setProductSearch] = useState('')
|
||||
const [scores, setScores] = useState<ScoreResult[]>([])
|
||||
const [records, setRecords] = useState<RecordResult[]>([])
|
||||
const [panelTab, setPanelTab] = useState<string>('logs')
|
||||
const [recordModalData, setRecordModalData] = useState<{
|
||||
open: boolean
|
||||
account?: AccountPreview
|
||||
loading: boolean
|
||||
records: UserPrizeInfo[]
|
||||
error?: string
|
||||
}>({ open: false, loading: false, records: [] })
|
||||
const [results, setResults] = useState<MonitorResult[]>([])
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [now, setNow] = useState(new Date())
|
||||
@@ -963,6 +972,12 @@ export default function Dashboard() {
|
||||
return map
|
||||
}, [results])
|
||||
|
||||
const recordsByUID = useMemo(() => {
|
||||
const map = new Map<number, RecordResult>()
|
||||
records.forEach((item) => map.set(item.uid, item))
|
||||
return map
|
||||
}, [records])
|
||||
|
||||
// 用服务端兑换记录兜底,界面刷新后也能看出该账号是否已抢到当前奖品。
|
||||
const recordHitByUID = useMemo(() => {
|
||||
const map = new Map<number, UserPrizeInfo>()
|
||||
@@ -1485,6 +1500,58 @@ export default function Dashboard() {
|
||||
setSelectedAccountLines([])
|
||||
}
|
||||
|
||||
const viewAccountRecords = async (account: AccountPreview) => {
|
||||
const cached = recordsByUID.get(account.uid)
|
||||
if (cached && cached.records && cached.records.length > 0) {
|
||||
setRecordModalData({
|
||||
open: true,
|
||||
account,
|
||||
loading: false,
|
||||
records: cached.records,
|
||||
error: cached.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setRecordModalData({
|
||||
open: true,
|
||||
account,
|
||||
loading: true,
|
||||
records: cached?.records || [],
|
||||
error: undefined,
|
||||
})
|
||||
|
||||
try {
|
||||
const values = config()
|
||||
const singleConfig = {
|
||||
...values,
|
||||
selectedAccountLines: [account.line],
|
||||
}
|
||||
const data = await GetRecords(singleConfig)
|
||||
const res = (data || [])[0]
|
||||
const fetchedRecords = res?.records || []
|
||||
setRecordModalData({
|
||||
open: true,
|
||||
account,
|
||||
loading: false,
|
||||
records: fetchedRecords,
|
||||
error:
|
||||
res?.error || (res?.resp?.status && res.resp.status !== 200 ? res.resp.msg : undefined),
|
||||
})
|
||||
if (res) {
|
||||
setRecords((prev) => [...prev.filter((p) => p.uid !== account.uid), res])
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setRecordModalData({
|
||||
open: true,
|
||||
account,
|
||||
loading: false,
|
||||
records: [],
|
||||
error: String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const accountColumns: ColumnsType<AccountPreview> = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -1598,12 +1665,33 @@ export default function Dashboard() {
|
||||
return result?.error || result?.message || result?.exchange?.msg || ''
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '兑换记录',
|
||||
width: 104,
|
||||
render: (_, row) => {
|
||||
const rec = recordsByUID.get(row.uid)
|
||||
const count = rec?.records?.length || 0
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<History size={13} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
viewAccountRecords(row)
|
||||
}}
|
||||
>
|
||||
{count > 0 ? `记录 (${count})` : '记录'}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
currentPrize?.name,
|
||||
exchangeByUID,
|
||||
latestResultByUID,
|
||||
recordHitByUID,
|
||||
recordsByUID,
|
||||
running,
|
||||
scoreMap,
|
||||
watchedName,
|
||||
@@ -1694,15 +1782,30 @@ export default function Dashboard() {
|
||||
[],
|
||||
)
|
||||
|
||||
const totalRecordCount = useMemo(() => {
|
||||
return records.reduce((sum, item) => sum + (item.records?.length || 0), 0)
|
||||
}, [records])
|
||||
|
||||
const recordRows = useMemo<RecordRow[]>(() => {
|
||||
const rows: RecordRow[] = []
|
||||
records.forEach((item) => {
|
||||
if (!item.records?.length) {
|
||||
rows.push({ key: `${item.uid}-empty`, uid: item.uid, name: item.error || item.resp?.msg || '' })
|
||||
const emptyMsg =
|
||||
item.error ||
|
||||
(item.resp?.status && item.resp.status !== 200 ? item.resp.msg : '(无兑换记录)')
|
||||
rows.push({
|
||||
key: `${item.uid}-empty`,
|
||||
uid: item.uid,
|
||||
name: emptyMsg,
|
||||
})
|
||||
return
|
||||
}
|
||||
item.records.forEach((record, index) => {
|
||||
rows.push({ key: `${item.uid}-${index}-${record.id || 'record'}`, uid: item.uid, ...record })
|
||||
rows.push({
|
||||
key: `${item.uid}-${index}-${record.id || 'record'}`,
|
||||
uid: item.uid,
|
||||
...record,
|
||||
})
|
||||
})
|
||||
})
|
||||
return rows
|
||||
@@ -1711,13 +1814,63 @@ export default function Dashboard() {
|
||||
const recordColumns: ColumnsType<RecordRow> = useMemo(
|
||||
() => [
|
||||
{ title: 'UID', dataIndex: 'uid', width: 140 },
|
||||
{ title: '名称', dataIndex: 'name', minWidth: 220, ellipsis: true },
|
||||
{ title: '积分', width: 90, render: (_, row) => row.new_score || row.score || 0 },
|
||||
{ title: '状态', dataIndex: 'status', width: 84 },
|
||||
{ title: '订单号', dataIndex: 'order_id', minWidth: 200, ellipsis: true },
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
minWidth: 200,
|
||||
ellipsis: true,
|
||||
render: (name, row) => (
|
||||
<Space orientation="horizontal" size={6}>
|
||||
{row.img ? (
|
||||
<img
|
||||
src={row.img}
|
||||
alt={name}
|
||||
style={{ width: 24, height: 24, borderRadius: 4, objectFit: 'cover' }}
|
||||
/>
|
||||
) : null}
|
||||
<span>{name}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
width: 90,
|
||||
render: (_, row) =>
|
||||
row.new_score || row.score ? `${row.new_score || row.score} 分` : '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 84,
|
||||
render: (status) =>
|
||||
status === 200 || !status ? <Tag color="success">成功</Tag> : <Tag>{status}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'order_id',
|
||||
minWidth: 220,
|
||||
render: (orderId) =>
|
||||
orderId ? (
|
||||
<div className="order-id-cell">
|
||||
<span>{orderId}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Copy size={12} />}
|
||||
title="复制单号"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(orderId)
|
||||
message.success('已复制单号')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{ title: '时间', width: 160, render: (_, row) => formatUnixTime(row.create_time) },
|
||||
],
|
||||
[],
|
||||
[message],
|
||||
)
|
||||
|
||||
const resultColumns: ColumnsType<MonitorResult> = useMemo(
|
||||
@@ -1781,6 +1934,26 @@ export default function Dashboard() {
|
||||
options={logFilterOptions}
|
||||
/>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<Copy size={13} />}
|
||||
disabled={visibleLogs.length === 0}
|
||||
onClick={() => {
|
||||
const text = visibleLogs
|
||||
.map(
|
||||
(l) =>
|
||||
`${l.time} [${l.category}][${l.level}]${l.account ? `[账号:${l.account}]` : ''} ${l.message}`,
|
||||
)
|
||||
.join('\n')
|
||||
navigator.clipboard.writeText(text)
|
||||
message.success(`已复制 ${visibleLogs.length} 条日志`)
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button size="small" disabled={logs.length === 0} onClick={() => setLogs([])}>
|
||||
清空
|
||||
</Button>
|
||||
<Tooltip title="开启后记录逐条网络请求(debug 级)">
|
||||
<Switch
|
||||
size="small"
|
||||
@@ -1829,7 +2002,7 @@ export default function Dashboard() {
|
||||
},
|
||||
{
|
||||
key: 'records',
|
||||
label: '复盘',
|
||||
label: `兑换记录${totalRecordCount > 0 ? ` (${totalRecordCount})` : ''}`,
|
||||
children: (
|
||||
<Table
|
||||
size="small"
|
||||
@@ -1838,6 +2011,7 @@ export default function Dashboard() {
|
||||
dataSource={recordRows}
|
||||
pagination={{ pageSize: 12, showSizeChanger: false }}
|
||||
scroll={{ x: 740, y: 520 }}
|
||||
locale={{ emptyText: '暂无兑换记录,可勾选账号后点击上方「兑换记录」按钮或账号行的「记录」按钮进行查询' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -2049,9 +2223,13 @@ export default function Dashboard() {
|
||||
message.warning('请先选择账号')
|
||||
return
|
||||
}
|
||||
runAction('records', () => GetRecords(values), (data) =>
|
||||
setRecords((data || []) as RecordResult[]),
|
||||
)
|
||||
runAction('records', () => GetRecords(values), (data) => {
|
||||
const list = (data || []) as RecordResult[]
|
||||
setRecords(list)
|
||||
setPanelTab('records')
|
||||
const total = list.reduce((sum, item) => sum + (item.records?.length || 0), 0)
|
||||
message.success(`查询完成:${list.length} 个账号,共 ${total} 条兑换记录`)
|
||||
})
|
||||
}}
|
||||
>
|
||||
兑换记录
|
||||
@@ -2116,7 +2294,7 @@ export default function Dashboard() {
|
||||
|
||||
<div className="side-log-panel">
|
||||
<div className="panel-tools">
|
||||
<Tabs items={panelTabs} size="small" />
|
||||
<Tabs activeKey={panelTab} onChange={setPanelTab} items={panelTabs} size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -2338,6 +2516,107 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<span>兑换记录明细</span>
|
||||
{recordModalData.account ? (
|
||||
<Tag color="blue">UID: {recordModalData.account.uid}</Tag>
|
||||
) : null}
|
||||
{!recordModalData.loading ? (
|
||||
<Tag color={recordModalData.records.length > 0 ? 'green' : 'default'}>
|
||||
共 {recordModalData.records.length} 条
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
open={recordModalData.open}
|
||||
width="min(860px, calc(100vw - 48px))"
|
||||
footer={null}
|
||||
onCancel={() => setRecordModalData((prev) => ({ ...prev, open: false }))}
|
||||
>
|
||||
{recordModalData.loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '36px 0' }}>
|
||||
<Typography.Text type="secondary">正在向虎牙服务器查询该账号兑换记录...</Typography.Text>
|
||||
</div>
|
||||
) : recordModalData.error ? (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<Tag color="error">查询失败</Tag>
|
||||
<span>{recordModalData.error}</span>
|
||||
</div>
|
||||
) : recordModalData.records.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '36px 0' }}>
|
||||
<Typography.Text type="secondary">暂无兑换记录(该账号在当前活动下尚无成功订单)</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
size="small"
|
||||
className="record-modal-table"
|
||||
rowKey={(r, idx) => `${r.id || idx}-${r.order_id || ''}`}
|
||||
dataSource={recordModalData.records}
|
||||
pagination={{ pageSize: 8, showSizeChanger: false }}
|
||||
columns={[
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'name',
|
||||
render: (name, r) => (
|
||||
<Space orientation="horizontal" size={8}>
|
||||
{r.img ? (
|
||||
<img
|
||||
src={r.img}
|
||||
alt={name}
|
||||
style={{ width: 32, height: 32, borderRadius: 4, objectFit: 'cover' }}
|
||||
/>
|
||||
) : null}
|
||||
<div>
|
||||
<strong>{name || '未知商品'}</strong>
|
||||
{r.prize_id ? <div className="muted">PID: {r.prize_id}</div> : null}
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '消耗积分',
|
||||
width: 110,
|
||||
render: (_, r) => <Tag color="orange">{r.new_score || r.score || 0} 积分</Tag>,
|
||||
},
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'order_id',
|
||||
width: 260,
|
||||
render: (orderId) =>
|
||||
orderId ? (
|
||||
<div className="order-id-cell">
|
||||
<span>{orderId}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Copy size={12} />}
|
||||
title="复制单号"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(orderId)
|
||||
message.success('已复制单号')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '兑换时间',
|
||||
width: 170,
|
||||
render: (_, r) => formatUnixTime(r.create_time),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 80,
|
||||
render: () => <Tag color="success">成功</Tag>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -2543,19 +2822,36 @@ function padNumber(value: number, size = 2): string {
|
||||
return String(value).padStart(size, '0')
|
||||
}
|
||||
|
||||
function parseLogTime(rawTime?: string): { local: string; huya?: string } {
|
||||
if (!rawTime) return { local: '' }
|
||||
const match = rawTime.match(/^(.*?)\(H(.*?)\)$/)
|
||||
if (match) {
|
||||
return { local: match[1], huya: match[2] }
|
||||
}
|
||||
return { local: rawTime }
|
||||
}
|
||||
|
||||
function LogView({ logs }: { logs: LogEntry[] }) {
|
||||
return (
|
||||
<div className="log-view">
|
||||
{logs.map((item) => (
|
||||
<div className={`log-line level-${item.level}`} key={item.id}>
|
||||
<span>{item.time}</span>
|
||||
<Tag>{item.level}</Tag>
|
||||
<code>
|
||||
{item.category ? `[${item.category}]` : ''}
|
||||
{item.account ? `[账号:${item.account}]` : ''} {item.message}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
{logs.map((item) => {
|
||||
const { local, huya } = parseLogTime(item.time)
|
||||
return (
|
||||
<div className={`log-line level-${item.level}`} key={item.id}>
|
||||
<span className="log-local-time">{local}</span>
|
||||
{huya ? (
|
||||
<Tag color="cyan" className="log-huya-tag">
|
||||
H:{huya}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Tag className="log-level-tag">{item.level}</Tag>
|
||||
<code>
|
||||
{item.category ? `[${item.category}]` : ''}
|
||||
{item.account ? `[账号:${item.account}]` : ''} {item.message}
|
||||
</code>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+43
-3
@@ -452,20 +452,48 @@ body {
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 54px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-height: 23px;
|
||||
color: #4b5565;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
.log-line .log-local-time {
|
||||
color: #64748b;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-line .log-huya-tag {
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-line .log-level-tag {
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
line-height: 18px;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-line code {
|
||||
color: #1677ff;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.log-line.level-error code {
|
||||
@@ -476,6 +504,18 @@ body {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.record-modal-table {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.order-id-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stats-pane {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
+6
-1
@@ -422,6 +422,8 @@ func (a *App) GetRecords(cfg Config) ([]RecordResult, error) {
|
||||
defer closeClient()
|
||||
|
||||
results := make([]RecordResult, len(accounts))
|
||||
totalRecords := 0
|
||||
var recordMu sync.Mutex
|
||||
a.runConcurrent(accounts, cfg.Concurrency, func(i int, account sniper.Account) {
|
||||
ctx, cancel := requestContext(cfg.TimeoutSec)
|
||||
defer cancel()
|
||||
@@ -429,10 +431,13 @@ func (a *App) GetRecords(cfg Config) ([]RecordResult, error) {
|
||||
item := RecordResult{UID: account.UID, Resp: resp, Error: errorString(err)}
|
||||
if resp != nil {
|
||||
item.Records = resp.Records
|
||||
recordMu.Lock()
|
||||
totalRecords += len(resp.Records)
|
||||
recordMu.Unlock()
|
||||
}
|
||||
results[i] = item
|
||||
})
|
||||
a.addLog("info", "查询兑换记录完成: accounts=%d", len(accounts))
|
||||
a.addLog("info", "查询兑换记录完成: accounts=%d 总记录=%d", len(accounts), totalRecords)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user