From 6d8be28225903a9e105335121777d3d968a3c565 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 6 Jul 2026 20:30:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20CK=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=92=8C=E7=9B=91=E6=8E=A7=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- cmd/huya-go/main.go | 108 +++- frontend/src/App.tsx | 1081 +++++++++++++++++++++----------- frontend/src/styles.css | 464 ++++++++++---- frontend/wailsjs/go/models.ts | 22 + internal/gui/app.go | 210 +++++-- internal/gui/app_test.go | 46 ++ internal/huya/http_client.go | 4 + internal/huya/wss_client.go | 5 + internal/sniper/sniper.go | 675 ++++++++++++++++++-- internal/sniper/sniper_test.go | 255 ++++++++ 11 files changed, 2305 insertions(+), 568 deletions(-) create mode 100644 internal/gui/app_test.go create mode 100644 internal/sniper/sniper_test.go diff --git a/.gitignore b/.gitignore index 70627c8..4bd2770 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /bin/ /frontend/node_modules/ *.test -build/ \ No newline at end of file +build/ +frontend/dist/ \ No newline at end of file diff --git a/cmd/huya-go/main.go b/cmd/huya-go/main.go index a5705cb..b0f0863 100644 --- a/cmd/huya-go/main.go +++ b/cmd/huya-go/main.go @@ -24,15 +24,27 @@ func main() { cookieFile = flag.String("cookie-file", "", "Cookie 文件,一行一个账号") sid = flag.Int("sid", 0, "活动 SID") pid = flag.Int("pid", 0, "奖品 PID") + pids = flag.String("pids", "", "多个奖品 PID,逗号分隔") name = flag.String("name", "", "按奖品名包含匹配") + names = flag.String("names", "", "多个奖品名,逗号分隔") + autoSelect = flag.Bool("auto", false, "monitor 自动选择列表里的可兑奖品") loops = flag.Int("loops", 1, "monitor 循环次数") delayMS = flag.Int("delay", 150, "monitor 每轮间隔毫秒") concurrency = flag.Int("concurrency", 5, "monitor/exchange 并发账号数") - stopAfterSuccess = flag.Bool("stop-after-success", true, "monitor 成功后停止") + stopAfterSuccess = flag.Bool("stop-after-success", false, "monitor 任意账号成功后停止全部") + accountStop = flag.Bool("account-stop", true, "monitor 单账号成功后停止该账号该奖品") at = flag.String("at", "", "定时开始时间,支持 RFC3339 或 2006-01-02 15:04:05") + leadMS = flag.Int("lead", 2500, "monitor 相对 -at 提前启动毫秒") timeoutSec = flag.Int("timeout", 15, "单次请求超时秒数") transport = flag.String("transport", "http", "传输层:http/wss") wssHost = flag.String("wss-host", huya.DefaultWSSHost, "WSS host,来自 launch.queryHttpDns 或抓包") + prewarm = flag.Bool("prewarm", true, "monitor 开始前预热账号连接") + directExchange = flag.Bool("direct-exchange", true, "monitor 已知 PID 时跳过列表直接兑换") + listBefore = flag.Bool("list-before-exchange", false, "monitor 即使已知 PID 也先查询列表") + shareList = flag.Bool("share-list", true, "monitor 多账号共享同一轮奖品列表") + refreshSuccess = flag.Bool("refresh-after-success", true, "monitor 成功后异步刷新积分和记录") + freqBackoffMS = flag.Int("freq-backoff", 300, "操作频繁后的账号退避毫秒") + errorBackoffMS = flag.Int("error-backoff", 300, "请求异常后的账号退避毫秒") verbose = flag.Bool("v", false, "输出调试日志") ) flag.Parse() @@ -65,22 +77,25 @@ func main() { defer closeClient() ctx := context.Background() + var startAt time.Time if *at != "" { when, err := parseTime(*at) if err != nil { exitErr(err) } - waitUntil(ctx, when) + startAt = when } switch *cmd { case "list": + waitForCommand(ctx, startAt) resp, err := client.GetActPrizeList(ctx, accounts[0].UID, accounts[0].Cookie, int32(*sid)) if err != nil { exitErr(err) } printJSON(resp) case "score": + waitForCommand(ctx, startAt) results := make([]map[string]any, 0, len(accounts)) for _, account := range accounts { resp, err := client.QueryUserScore(ctx, account.UID, account.Cookie, int32(*sid)) @@ -93,6 +108,7 @@ func main() { } printJSON(results) case "records": + waitForCommand(ctx, startAt) results := make([]map[string]any, 0, len(accounts)) for _, account := range accounts { resp, err := client.GetUserPrizeRecords(ctx, account.UID, account.Cookie, int32(*sid)) @@ -107,24 +123,42 @@ func main() { if *pid <= 0 { exitErr(errors.New("exchange 必须提供 -pid")) } + waitForCommand(ctx, startAt) runExchange(ctx, client, accounts, int32(*sid), int32(*pid), *concurrency) case "monitor": - if *pid <= 0 && strings.TrimSpace(*name) == "" { + targets := parseTargets(*pid, *pids, *name, *names, *autoSelect) + if len(targets) == 0 { exitErr(errors.New("monitor 必须提供 -pid 或 -name")) } runner := sniper.Runner{Client: client} var outMu sync.Mutex - err := runner.Run(ctx, accounts, sniper.Target{PID: int32(*pid), Name: strings.TrimSpace(*name)}, sniper.Config{ - SID: int32(*sid), - Loops: *loops, - Delay: time.Duration(*delayMS) * time.Millisecond, - Concurrency: *concurrency, - StopAfterSuccess: *stopAfterSuccess, - }, func(result sniper.Result) { + emit := func(result sniper.Result) { outMu.Lock() defer outMu.Unlock() _ = json.NewEncoder(os.Stdout).Encode(result) - }) + } + cfg := sniper.Config{ + SID: int32(*sid), + Loops: *loops, + Delay: time.Duration(*delayMS) * time.Millisecond, + Concurrency: *concurrency, + StopAfterSuccess: *stopAfterSuccess, + StopAccountAfterSuccess: *accountStop, + Prewarm: false, + DirectExchange: *directExchange, + ListBeforeExchange: *listBefore, + SharePrizeList: *shareList, + RefreshAfterSuccess: *refreshSuccess, + TooFrequentBackoff: time.Duration(*freqBackoffMS) * time.Millisecond, + ErrorBackoff: time.Duration(*errorBackoffMS) * time.Millisecond, + } + if *prewarm { + if err := runner.Prewarm(ctx, accounts, cfg, emit); err != nil && !errors.Is(err, context.Canceled) { + exitErr(err) + } + } + waitForMonitor(ctx, startAt, time.Duration(*leadMS)*time.Millisecond) + err := runner.RunTargets(ctx, accounts, targets, cfg, emit) if err != nil && !errors.Is(err, context.Canceled) { exitErr(err) } @@ -133,6 +167,20 @@ func main() { } } +func waitForCommand(ctx context.Context, startAt time.Time) { + if !startAt.IsZero() { + waitUntil(ctx, startAt) + } +} + +func waitForMonitor(ctx context.Context, startAt time.Time, lead time.Duration) { + if startAt.IsZero() { + return + } + when := startAt.Add(-lead) + waitUntil(ctx, when) +} + func runExchange(ctx context.Context, client huya.ActivityClient, accounts []sniper.Account, sid, pid int32, concurrency int) { if concurrency <= 0 { concurrency = 1 @@ -189,6 +237,44 @@ func loadCookies(cookie, file string) ([]string, error) { return out, nil } +func parseTargets(pid int, pids, name, names string, autoSelect bool) []sniper.Target { + targets := make([]sniper.Target, 0) + name = strings.TrimSpace(name) + if pid > 0 || name != "" { + targets = append(targets, sniper.Target{PID: int32(pid), Name: name}) + } + if autoSelect { + targets = append(targets, sniper.Target{Auto: true}) + } + for _, text := range splitList(pids) { + value, err := strconv.Atoi(text) + if err == nil && value > 0 { + targets = append(targets, sniper.Target{PID: int32(value)}) + } + } + for _, text := range splitList(names) { + if text == "auto" || text == "自动" || text == "自动选择" { + targets = append(targets, sniper.Target{Auto: true}) + continue + } + targets = append(targets, sniper.Target{Name: text}) + } + return targets +} + +func splitList(text string) []string { + replacer := strings.NewReplacer(",", ",", "\n", ",", "\t", ",", " ", ",") + parts := strings.Split(replacer.Replace(text), ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + item := strings.TrimSpace(part) + if item != "" { + out = append(out, item) + } + } + return out +} + func parseTime(text string) (time.Time, error) { text = strings.TrimSpace(text) if text == "" { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7b8571f..5814730 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,14 +1,11 @@ -import { useEffect, useMemo, useState } from 'react' -import type { ReactNode } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import type { Key, ReactNode } from 'react' import { App as AntApp, Button, - Col, - Divider, Form, Input, InputNumber, - Row, Select, Space, Statistic, @@ -22,13 +19,13 @@ import { import type { ColumnsType } from 'antd/es/table' import { ClipboardList, + ClipboardPaste, ListChecks, Play, RefreshCw, - Search, + Save, ShoppingCart, Square, - Users, } from 'lucide-react' import { DefaultConfig, @@ -42,9 +39,9 @@ import { StartMonitor, StopMonitor, } from '../wailsjs/go/gui/App' -import { EventsOn } from '../wailsjs/runtime/runtime' +import { ClipboardGetText, EventsOn } from '../wailsjs/runtime/runtime' -const storageKey = 'huya-go.gui.config' +const storageKey = 'huya-go.gui.config.v2' type UiConfig = { transport: string @@ -52,11 +49,22 @@ type UiConfig = { sid: number pid: number name: string + targetText: string cookieText: string + selectedAccountLines: number[] loops: number delayMs: number concurrency: number stopAfterSuccess: boolean + stopAccountAfterSuccess: boolean + prewarm: boolean + directExchange: boolean + sharePrizeList: boolean + autoSelect: boolean + refreshAfterSuccess: boolean + leadMs: number + frequentBackoffMs: number + errorBackoffMs: number timeoutSec: number startAt: string } @@ -76,6 +84,7 @@ type LogEntry = { type PrizeInfo = { id: number + sid?: number name: string score?: number new_score?: number @@ -83,6 +92,7 @@ type PrizeInfo = { left_num?: number status?: number percent?: string + img?: string } type ScoreResult = { @@ -145,9 +155,16 @@ type MonitorResult = { key?: string loop?: number uid: number + target_index?: number + targetIndex?: number action?: string + category?: string + message?: string + done?: boolean prize?: PrizeInfo exchange?: ExchangeResp + score?: ScoreResult['resp'] + records?: RecordResult['resp'] error?: string } @@ -156,7 +173,7 @@ type RecordRow = UserPrizeInfo & { uid: number } -type SavedConfig = Omit +type SavedConfig = Omit function getExchangeInfo(resp?: ExchangeResp): ExchangeInfo { return resp?.exchange_info || resp?.exchangeInfo || {} @@ -166,34 +183,40 @@ function getPreCondition(resp?: ExchangeResp): ActPreCondition { return resp?.act_pre_condition || resp?.actPreCondition || {} } -function statusTag(status?: number, error?: string): ReactNode { - if (error) { - return 失败 - } - if (status === 200) { - return 成功 - } - if (status) { - return {status} - } - return 等待 +function priceOf(row?: PrizeInfo): number { + if (!row) return 0 + return row.new_score || row.score || 0 } -function pickConfig(values: Partial): UiConfig { - return { - transport: values.transport || 'http', - wssHost: values.wssHost || '', - sid: Number(values.sid || 0), - pid: Number(values.pid || 0), - name: values.name || '', - cookieText: values.cookieText || '', - loops: Number(values.loops || 1), - delayMs: Number(values.delayMs || 150), - concurrency: Number(values.concurrency || 1), - stopAfterSuccess: Boolean(values.stopAfterSuccess), - timeoutSec: Number(values.timeoutSec || 15), - startAt: values.startAt || '', +function leftOf(row?: PrizeInfo): number { + if (!row) return 0 + return row.left_num || 0 +} + +function statusTag(status?: number, error?: string): ReactNode { + if (error) return 失败 + if (status === 200) return 成功 + if (status) return {status} + return 待处理 +} + +function categoryTag(category?: string): ReactNode { + const map: Record = { + ready: { color: 'blue', label: '预热' }, + success: { color: 'success', label: '成功' }, + not_found: { color: 'default', label: '未命中' }, + not_open: { color: 'processing', label: '未开放' }, + too_frequent: { color: 'warning', label: '频控' }, + not_enough: { color: 'error', label: '不足' }, + already_done: { color: 'purple', label: '已兑' }, + off_shelf: { color: 'error', label: '下架' }, + sold_out: { color: 'error', label: '抢完' }, + failed: { color: 'warning', label: '失败' }, + error: { color: 'error', label: '异常' }, } + if (!category) return 待处理 + const item = map[category] || { color: 'default', label: category } + return {item.label} } function readSavedConfig(): Partial { @@ -204,17 +227,106 @@ function readSavedConfig(): Partial { } } +function normalizeCookieText(text: string): string { + const normalized = text + .replace(/\r\n?/g, '\n') + .replace(/\\r\\n|\\r|\\n/g, '\n') + + const withLineBreaks = + normalized.includes('\n') || !/\/r\/n|\/r/.test(normalized) + ? normalized + : normalized.replace(/\/r\/n|\/r|\/n/g, '\n') + + return withLineBreaks + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .join('\n') +} + +function pickConfig(values: Partial): UiConfig { + return { + transport: values.transport || 'http', + wssHost: values.wssHost || '', + sid: Number(values.sid || 0), + pid: Number(values.pid || 0), + name: values.name || '', + targetText: values.targetText || '', + cookieText: values.cookieText || '', + selectedAccountLines: values.selectedAccountLines || [], + loops: Number(values.loops || 1), + delayMs: Number(values.delayMs || 150), + concurrency: Number(values.concurrency || 1), + stopAfterSuccess: Boolean(values.stopAfterSuccess), + stopAccountAfterSuccess: values.stopAccountAfterSuccess !== false, + prewarm: values.prewarm !== false, + directExchange: values.directExchange !== false, + sharePrizeList: values.sharePrizeList !== false, + autoSelect: Boolean(values.autoSelect), + refreshAfterSuccess: values.refreshAfterSuccess !== false, + leadMs: Number(values.leadMs || 0), + frequentBackoffMs: Number(values.frequentBackoffMs || 300), + errorBackoffMs: Number(values.errorBackoffMs || 300), + timeoutSec: Number(values.timeoutSec || 15), + startAt: values.startAt || '', + } +} + +function normalizeExchangeResult(row: ExchangeResult, index: number): MonitorResult { + const message = + row.error || + row.resp?.msg || + getPreCondition(row.resp).tip || + row.resp?.order_id || + row.resp?.orderId || + '' + const category = + row.error + ? 'error' + : row.resp?.status === 200 + ? 'success' + : message.includes('积分') && message.includes('不足') + ? 'not_enough' + : 'failed' + + return { + key: `exchange-${row.uid}-${index}-${Date.now()}`, + loop: 0, + uid: row.uid, + action: 'exchange', + category, + message, + exchange: row.resp, + error: row.error, + } +} + +function normalizeMonitorResult(row: MonitorResult): MonitorResult { + return { + ...row, + key: `monitor-${row.loop || 0}-${row.uid}-${Date.now()}-${Math.random()}`, + } +} + export default function Dashboard() { const { message } = AntApp.useApp() const [form] = Form.useForm() + const previewedCookieText = useRef('') + const watchedPID = Form.useWatch('pid', form) + const watchedName = Form.useWatch('name', form) + const watchedCookieText = Form.useWatch('cookieText', form) const [loading, setLoading] = useState('') const [running, setRunning] = useState(false) const [accounts, setAccounts] = useState([]) + const [selectedAccountLines, setSelectedAccountLines] = useState([]) const [prizes, setPrizes] = useState([]) + const [selectedPrizeKeys, setSelectedPrizeKeys] = useState([]) + const [productSearch, setProductSearch] = useState('') const [scores, setScores] = useState([]) const [records, setRecords] = useState([]) const [results, setResults] = useState([]) const [logs, setLogs] = useState([]) + const [now, setNow] = useState(new Date()) useEffect(() => { let mounted = true @@ -241,26 +353,91 @@ export default function Dashboard() { 'gui:monitor-state', (state: { running?: boolean; message?: string }) => { setRunning(Boolean(state?.running)) - if (state?.message) { - message.info(state.message) - } + if (state?.message) message.info(state.message) }, ) + const tick = window.setInterval(() => setNow(new Date()), 250) return () => { mounted = false offLog() offResult() offState() + window.clearInterval(tick) } }, [form, message]) - const config = () => pickConfig(form.getFieldsValue(true)) + useEffect(() => { + if ((watchedCookieText || '') === previewedCookieText.current) return + setAccounts([]) + setSelectedAccountLines([]) + setPrizes([]) + setSelectedPrizeKeys([]) + }, [watchedCookieText]) - const previewAccounts = async () => { - const items = (await PreviewAccounts(form.getFieldValue('cookieText') || '')) as AccountPreview[] - setAccounts(items || []) - } + const validAccounts = useMemo(() => accounts.filter((item) => item.valid), [accounts]) + const selectedAccountCount = selectedAccountLines.length + const selectedPrizeID = + selectedPrizeKeys.length === 1 ? Number(selectedPrizeKeys[0]) || undefined : undefined + + const scoreMap = useMemo(() => { + const map = new Map() + scores.forEach((item) => map.set(item.uid, item)) + return map + }, [scores]) + + const latestResultByUID = useMemo(() => { + const map = new Map() + results.forEach((item) => { + if (!map.has(item.uid)) map.set(item.uid, item) + }) + return map + }, [results]) + + const successCount = useMemo( + () => new Set(results.filter((item) => item.category === 'success').map((item) => item.uid)).size, + [results], + ) + const errorCount = useMemo( + () => + results.filter((item) => + ['error', 'failed', 'not_enough', 'off_shelf', 'sold_out'].includes(item.category || ''), + ).length, + [results], + ) + const pendingCount = Math.max(selectedAccountCount - successCount, 0) + + const currentPrize = useMemo(() => { + const fromSelection = prizes.find((item) => item.id === selectedPrizeID) + if (fromSelection) return fromSelection + const pid = Number(watchedPID || 0) + return prizes.find((item) => item.id === pid) + }, [prizes, selectedPrizeID, watchedPID]) + + const filteredPrizes = useMemo(() => { + const keyword = productSearch.trim().toLowerCase() + if (!keyword) return prizes + return prizes.filter( + (item) => + item.name.toLowerCase().includes(keyword) || + String(item.id).includes(keyword) || + String(priceOf(item)).includes(keyword), + ) + }, [prizes, productSearch]) + + const prizeOptions = useMemo( + () => + prizes.map((item) => ({ + value: item.id, + label: `${item.name} · ${item.id} · ${priceOf(item)}积分`, + })), + [prizes], + ) + + const config = () => ({ + ...pickConfig(form.getFieldsValue(true)), + selectedAccountLines: selectedAccountLines.map(Number).filter((line) => line > 0), + }) const runAction = async ( key: string, @@ -279,6 +456,51 @@ export default function Dashboard() { } } + const previewAccounts = async (cookieText?: string) => { + const text = cookieText ?? form.getFieldValue('cookieText') ?? '' + previewedCookieText.current = text + const items = (await PreviewAccounts(text)) as AccountPreview[] + setAccounts(items || []) + setSelectedAccountLines((items || []).filter((item) => item.valid).map((item) => item.line)) + } + + const importCookiesFromClipboard = async () => { + setLoading('clipboard') + try { + const clipboardText = await ClipboardGetText() + const cookieText = normalizeCookieText(clipboardText || '') + if (!cookieText) { + message.warning('剪贴板没有可导入的 CK') + return + } + form.setFieldsValue({ cookieText }) + await previewAccounts(cookieText) + message.success(`已从剪贴板导入 ${cookieText.split('\n').length} 条 CK`) + } catch (err: unknown) { + message.error(`剪贴板导入失败: ${String(err)}`) + } finally { + setLoading('') + } + } + + const queryPrizes = () => { + if (!String(form.getFieldValue('cookieText') || '').trim()) { + message.warning('Cookie 不能为空') + return + } + runAction('list', () => ListPrizes(config()), (data) => { + const result = data as { prizes?: PrizeInfo[] } + const nextPrizes = result?.prizes || [] + setPrizes(nextPrizes) + setSelectedPrizeKeys((prev) => + prev.filter((key) => nextPrizes.some((item) => item.id === Number(key))), + ) + if (nextPrizes.length === 0) { + message.warning('当前 CK 未返回可选商品') + } + }) + } + const saveSettings = () => { const values = config() const saved: SavedConfig = { @@ -287,10 +509,20 @@ export default function Dashboard() { sid: values.sid, pid: values.pid, name: values.name, + targetText: values.targetText, loops: values.loops, delayMs: values.delayMs, concurrency: values.concurrency, stopAfterSuccess: values.stopAfterSuccess, + stopAccountAfterSuccess: values.stopAccountAfterSuccess, + prewarm: values.prewarm, + directExchange: values.directExchange, + sharePrizeList: values.sharePrizeList, + autoSelect: values.autoSelect, + refreshAfterSuccess: values.refreshAfterSuccess, + leadMs: values.leadMs, + frequentBackoffMs: values.frequentBackoffMs, + errorBackoffMs: values.errorBackoffMs, timeoutSec: values.timeoutSec, startAt: values.startAt, } @@ -298,19 +530,104 @@ export default function Dashboard() { message.success('已保存') } - const prizeColumns: ColumnsType = useMemo( + const applyPrizeSelection = (keys: Key[]) => { + const normalized = keys.map(Number).filter((item) => item > 0) + setSelectedPrizeKeys(normalized) + const selected = prizes.filter((item) => normalized.includes(item.id)) + if (selected.length === 0) { + form.setFieldsValue({ pid: 0, name: '', targetText: '' }) + return + } + form.setFieldsValue({ + pid: selected[0].id, + name: selected[0].name, + targetText: selected.map((item) => String(item.id)).join('\n'), + autoSelect: false, + }) + } + + const choosePrize = (pid?: number) => applyPrizeSelection(pid ? [pid] : []) + + const startMonitor = (scheduled: boolean) => { + const values = config() + if (!scheduled) values.startAt = '' + setResults([]) + return runAction('monitor', () => StartMonitor(values), () => setRunning(true)) + } + + const exchangeNow = () => { + const values = config() + if (values.pid <= 0) { + message.warning('请选择商品或填写 PID') + return + } + setResults([]) + return runAction('exchange', () => Exchange(values), (data) => { + const items = (data || []) as ExchangeResult[] + setResults(items.map(normalizeExchangeResult)) + }) + } + + const selectAllAccounts = () => { + setSelectedAccountLines(validAccounts.map((item) => item.line)) + } + + const clearAccounts = () => { + setSelectedAccountLines([]) + } + + const accountColumns: ColumnsType = useMemo( () => [ - { title: 'PID', dataIndex: 'id', width: 92 }, - { title: '名称', dataIndex: 'name', minWidth: 220, ellipsis: true }, + { + title: '账号', + width: 150, + render: (_, row) => ( +
+ {row.uid || `第${row.line}行`} +
{row.valid ? '已加入任务' : row.error || '未选择'}
+
+ ), + }, + { + title: '凭据', + width: 90, + render: (_, row) => (row.valid ? 已加载 : 无效), + }, + { + title: '商品', + width: 150, + ellipsis: true, + render: () => currentPrize?.name || watchedName || '未选择', + }, { title: '积分', - width: 90, - render: (_, row) => row.new_score || row.score || 0, + width: 84, + render: (_, row) => scoreMap.get(row.uid)?.score ?? '-', }, + { + title: '状态', + width: 98, + render: (_, row) => categoryTag(latestResultByUID.get(row.uid)?.category), + }, + { + title: '最后结果', + ellipsis: true, + render: (_, row) => { + const result = latestResultByUID.get(row.uid) + return result?.error || result?.message || result?.exchange?.msg || '' + }, + }, + ], + [currentPrize?.name, latestResultByUID, scoreMap, watchedName], + ) + + const prizeColumns: ColumnsType = useMemo( + () => [ + { title: 'num', dataIndex: 'id', width: 92 }, + { title: 'name', dataIndex: 'name', minWidth: 220, ellipsis: true }, + { title: '剩余', width: 84, render: (_, row) => leftOf(row) || '-' }, { title: '总量', dataIndex: 'num', width: 84 }, - { title: '剩余', dataIndex: 'left_num', width: 84 }, - { title: '状态', dataIndex: 'status', width: 82 }, - { title: '比例', dataIndex: 'percent', width: 88 }, + { title: '价格', width: 84, render: (_, row) => priceOf(row) || '-' }, ], [], ) @@ -319,15 +636,8 @@ export default function Dashboard() { () => [ { title: 'UID', dataIndex: 'uid', width: 140 }, { title: '可用积分', dataIndex: 'score', width: 110 }, - { - title: '状态', - width: 90, - render: (_, row) => statusTag(row.resp?.status, row.error), - }, - { - title: '消息', - render: (_, row) => row.error || row.resp?.msg || '', - }, + { title: '状态', width: 90, render: (_, row) => statusTag(row.resp?.status, row.error) }, + { title: '消息', render: (_, row) => row.error || row.resp?.msg || '' }, ], [], ) @@ -336,19 +646,11 @@ export default function Dashboard() { 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 || '', - }) + rows.push({ key: `${item.uid}-empty`, uid: item.uid, name: item.error || item.resp?.msg || '' }) 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 @@ -358,11 +660,7 @@ export default function Dashboard() { () => [ { 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: '积分', width: 90, render: (_, row) => row.new_score || row.score || 0 }, { title: '状态', dataIndex: 'status', width: 84 }, { title: '时间', dataIndex: 'create_time', width: 130 }, ], @@ -372,23 +670,20 @@ export default function Dashboard() { const resultColumns: ColumnsType = useMemo( () => [ { title: '轮次', dataIndex: 'loop', width: 70 }, - { title: 'UID', dataIndex: 'uid', width: 140 }, - { title: '动作', dataIndex: 'action', width: 120 }, + { title: 'UID', dataIndex: 'uid', width: 132 }, + { title: '动作', dataIndex: 'action', width: 112 }, + { title: '分类', width: 90, render: (_, row) => categoryTag(row.category) }, { title: '奖品', minWidth: 220, ellipsis: true, render: (_, row) => row.prize?.name || getExchangeInfo(row.exchange).desc || '', }, - { - title: '状态', - width: 90, - render: (_, row) => statusTag(row.exchange?.status, row.error), - }, { title: '消息', render: (_, row) => row.error || + row.message || row.exchange?.msg || getPreCondition(row.exchange).tip || row.exchange?.order_id || @@ -399,272 +694,369 @@ export default function Dashboard() { [], ) - return ( -
-