diff --git a/frontend/src/components/account/AccountTable.vue b/frontend/src/components/account/AccountTable.vue index a7b8e9b..fb6c3dd 100644 --- a/frontend/src/components/account/AccountTable.vue +++ b/frontend/src/components/account/AccountTable.vue @@ -39,8 +39,14 @@ const getStatusInfo = (name: string, selected: boolean) => { if (st && typeof st === 'object') { const obj = st as any - if (obj.ok) return { text: '成功', cls: 'status-success' } const msg = String(obj.message || '') + if (obj.pending) { + return { + text: msg.includes('定时') ? '待触发' : '抢购中', + cls: 'status-ready' + } + } + if (obj.ok) return { text: '成功', cls: 'status-success' } if (msg.includes('库存已耗完')) return { text: '库存耗尽', cls: 'status-warn' } if (msg.includes('兑换上限') || msg.includes('已抢到')) return { text: '已抢到', cls: 'status-success' } if (msg.includes('未绑定')) return { text: '未绑定', cls: 'status-fail' } diff --git a/frontend/src/components/console/StatusGrid.vue b/frontend/src/components/console/StatusGrid.vue index 2034b68..be92268 100644 --- a/frontend/src/components/console/StatusGrid.vue +++ b/frontend/src/components/console/StatusGrid.vue @@ -8,6 +8,8 @@ defineProps<{ const { accounts, selectedIndices, statusMap } = useAccounts() +const messageSucceeded = (message: string) => /成功|兑换上限|已抢到/.test(message) + const stats = computed(() => { const selectedNames = [...selectedIndices.value] .map(i => accounts.value[i]?.name) @@ -18,9 +20,11 @@ const stats = computed(() => { for (const name of selectedNames) { const st = statusMap.value[name] - if (st === true || (st && typeof st === 'object' && (st as any).ok)) { + const msg = st && typeof st === 'object' ? String((st as any).message || '') : '' + + if (st === true || (st && typeof st === 'object' && ((st as any).ok || messageSucceeded(msg)))) { success++ - } else if (st === false || (st && typeof st === 'object' && (st as any).message)) { + } else if (st === false || (st && typeof st === 'object' && !(st as any).pending && msg)) { fail++ } } diff --git a/frontend/src/components/layout/RightConsole.vue b/frontend/src/components/layout/RightConsole.vue index 3568591..e72123b 100644 --- a/frontend/src/components/layout/RightConsole.vue +++ b/frontend/src/components/layout/RightConsole.vue @@ -45,6 +45,14 @@ const openSettings = () => { CK 管理 + + {{ runState }} + {{ formatRemain(countdownSeconds) }} @@ -158,6 +166,26 @@ const openSettings = () => { margin-top: 2px; } +.run-state { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--muted); + background: var(--surface-2); + border: 1px solid var(--line); + border-radius: 999px; + padding: 4px 10px; + font-size: 12px; + font-weight: 500; +} + +.run-state.active { + color: var(--accent); + background: var(--accent-soft); + border-color: var(--accent-border); +} + .top-actions { display: flex; align-items: center; diff --git a/frontend/src/components/log/LogPanel.vue b/frontend/src/components/log/LogPanel.vue index 4344fc5..8035bcb 100644 --- a/frontend/src/components/log/LogPanel.vue +++ b/frontend/src/components/log/LogPanel.vue @@ -12,6 +12,8 @@ const { showToast } = useToast() const activeTab = ref<'log' | 'result' | 'stats' | 'verify'>('log') const logBodyRef = ref(null) +const messageSucceeded = (message: string) => /成功|兑换上限|已抢到/.test(message) + // 自动滚动到底部 watch(logs, async () => { await nextTick() @@ -31,9 +33,11 @@ const stats = computed(() => { for (const name of selectedNames) { const st = statusMap.value[name] - if (st === true || (st && typeof st === 'object' && (st as any).ok)) { + const msg = st && typeof st === 'object' ? String((st as any).message || '') : '' + + if (st === true || (st && typeof st === 'object' && ((st as any).ok || messageSucceeded(msg)))) { success++ - } else if (st === false || (st && typeof st === 'object' && (st as any).message)) { + } else if (st === false || (st && typeof st === 'object' && !(st as any).pending && msg)) { fail++ } } diff --git a/frontend/src/composables/useAccounts.ts b/frontend/src/composables/useAccounts.ts index e26c19b..acd12a8 100644 --- a/frontend/src/composables/useAccounts.ts +++ b/frontend/src/composables/useAccounts.ts @@ -9,6 +9,7 @@ export interface Account { export interface AccountStatus { ok?: boolean + pending?: boolean message?: string } @@ -99,9 +100,24 @@ export function useAccounts() { localStorage.removeItem('statusMap') } + const setStatuses = (statuses: Record) => { + statusMap.value = statuses + if (Object.keys(statusMap.value).length > 0) { + localStorage.setItem('statusMap', JSON.stringify(statusMap.value)) + } else { + localStorage.removeItem('statusMap') + } + } + + const mergeStatuses = (statuses: Record) => { + setStatuses({ + ...statusMap.value, + ...statuses + }) + } + const setStatus = (name: string, status: AccountStatus | boolean) => { - statusMap.value[name] = status - localStorage.setItem('statusMap', JSON.stringify(statusMap.value)) + mergeStatuses({ [name]: status }) } const selectedAccountNames = (): string[] => { @@ -110,6 +126,18 @@ export function useAccounts() { .filter(Boolean) as string[] } + const markSelectedStatus = (status: AccountStatus | boolean) => { + const next: Record = {} + selectedAccountNames().forEach(name => { + next[name] = typeof status === 'object' ? { ...status } : status + }) + mergeStatuses(next) + } + + const markSelectedPending = (message = '抢购中,等待后端结果') => { + markSelectedStatus({ pending: true, message }) + } + const buildAccountProducts = (productId: string): Record | null => { if (selectedIndices.value.size === 0) { showToast('提示', '请勾选要抢购的账号', 'warn') @@ -148,7 +176,11 @@ export function useAccounts() { queryAllScore, getScore, resetStatus, + setStatuses, + mergeStatuses, setStatus, + markSelectedStatus, + markSelectedPending, selectedAccountNames, buildAccountProducts } diff --git a/frontend/src/composables/useLogs.ts b/frontend/src/composables/useLogs.ts index 6b89124..8183fff 100644 --- a/frontend/src/composables/useLogs.ts +++ b/frontend/src/composables/useLogs.ts @@ -1,4 +1,5 @@ import { ref } from 'vue' +import { useAccounts, type AccountStatus } from './useAccounts' export interface LogEntry { id: number @@ -23,6 +24,32 @@ const results = ref>({}) const lastLogId = ref(0) const lastResultsJson = ref('') +const resultAccountName = (key: string): string => { + return String(key || '').replace(/_第\d+包$/, '') +} + +const resultSucceeded = (message: string): boolean => { + return /成功|兑换上限|已抢到/.test(String(message || '')) +} + +// 后端结果按“账号_第N包”返回,账号表按账号名展示,这里做一次归并。 +const normalizeResultStatuses = (data: Record): Record => { + const next: Record = {} + + Object.entries(data).forEach(([key, value]) => { + const name = resultAccountName(key) + const message = String(value || '') + const ok = resultSucceeded(message) + const prev = next[name] + + if (!prev || ok || !prev.ok) { + next[name] = { ok, message } + } + }) + + return next +} + export function useLogs() { const addLocalLog = (message: string, type: LogMessage['type'] = 'i') => { const now = new Date() @@ -75,11 +102,14 @@ export function useLogs() { const checkResults = async () => { try { const d = await App.GetResults() + const data = (d || {}) as Record const json = JSON.stringify(d) - if (json !== lastResultsJson.value && Object.keys(d).length > 0) { + if (json !== lastResultsJson.value && Object.keys(data).length > 0) { lastResultsJson.value = json - results.value = d + results.value = data + const { mergeStatuses } = useAccounts() + mergeStatuses(normalizeResultStatuses(data)) return true } return false @@ -90,15 +120,18 @@ export function useLogs() { const clearLogs = () => { logs.value = [] + clearLocalResults() + lastLogId.value = 0 + } + + const clearLocalResults = () => { results.value = {} lastResultsJson.value = '' - lastLogId.value = 0 } const clearResults = async () => { await App.ClearResults() - results.value = {} - lastResultsJson.value = '' + clearLocalResults() } return { @@ -108,6 +141,7 @@ export function useLogs() { fetchServerLogs, checkResults, clearLogs, + clearLocalResults, clearResults } } diff --git a/frontend/src/composables/useScheduler.ts b/frontend/src/composables/useScheduler.ts index dc54e26..71cefe9 100644 --- a/frontend/src/composables/useScheduler.ts +++ b/frontend/src/composables/useScheduler.ts @@ -2,6 +2,7 @@ import { ref } from 'vue' import { useToast } from './useToast' import { useAccounts } from './useAccounts' import { useProducts } from './useProducts' +import { useLogs } from './useLogs' const App = window.go.main.App @@ -12,8 +13,9 @@ const runState = ref('等待操作') export function useScheduler() { const { showToast } = useToast() - const { resetStatus, buildAccountProducts } = useAccounts() + const { resetStatus, buildAccountProducts, markSelectedPending, markSelectedStatus } = useAccounts() const { selectedProductId, selectedProductName } = useProducts() + const { addLocalLog, clearLocalResults } = useLogs() const setRunState = (text: string) => { runState.value = text || '等待操作' @@ -24,14 +26,23 @@ export function useScheduler() { if (!ap) return resetStatus() + clearLocalResults() + markSelectedPending('抢购中,等待后端结果') setRunState('立即抢购已提交') + addLocalLog(`立即抢购: ${Object.keys(ap).length} 个账号 -> ${selectedProductName.value || selectedProductId.value}`, 'i') showToast('抢购中', `${Object.keys(ap).length} 个账号正在抢购`, 'info', 3000) try { const d = await App.StartSeckill(ap) + if (d?.success === false) { + throw new Error(d.error || '提交失败') + } setRunState(d.msg || '已提交') + addLocalLog(d.msg || '抢购已提交', 'i') } catch (e: any) { setRunState('提交失败') + markSelectedStatus({ ok: false, message: `提交失败: ${e.message}` }) + addLocalLog('提交失败: ' + e.message, 'e') showToast('提交失败', e.message, 'error') } } @@ -58,7 +69,10 @@ export function useScheduler() { const fullTime = today + ' ' + timeStr resetStatus() + clearLocalResults() + markSelectedPending('定时配置中,等待后端确认') setRunState('定时配置中') + addLocalLog(`设置定时: ${fullTime}, 提前=${advance}ms`, 'i') try { const d = await App.SetSchedule({ @@ -66,12 +80,17 @@ export function useScheduler() { targetTime: fullTime, advance }) + if (d?.success === false) { + throw new Error(d.error || '定时设置失败') + } scheduleActive.value = true localStorage.setItem('targetTime', timeStr) localStorage.setItem('advance', String(advance)) + markSelectedPending(`定时等待触发: ${d.triggerTime || fullTime}`) setRunState(`定时已开启,触发 ${d.triggerTime}`) + addLocalLog(`定时已开启: 触发 ${d.triggerTime},剩余 ${d.remainSecond} 秒`, 's') showToast( '定时已开启', `触发时刻: ${d.triggerTime}
剩余时间: ${formatRemain(d.remainSecond)}`, @@ -81,6 +100,8 @@ export function useScheduler() { startCountdown(d.remainSecond) } catch (e: any) { setRunState('定时设置失败') + markSelectedStatus({ ok: false, message: `定时设置失败: ${e.message}` }) + addLocalLog('设置失败: ' + e.message, 'e') showToast('定时失败', e.message, 'error') } } @@ -108,6 +129,7 @@ export function useScheduler() { if (remain <= 0) { scheduleActive.value = false + markSelectedPending('抢购中,等待后端结果') setRunState('定时已触发') stopCountdown() }