优化前端

This commit is contained in:
yml2213
2026-06-26 10:56:25 +08:00
parent 88a386b588
commit 54b5bd90ae
39 changed files with 6796 additions and 216 deletions
+155
View File
@@ -0,0 +1,155 @@
import { ref } from 'vue'
import { useToast } from './useToast'
export interface Account {
index: number
name: string
cookie: string
}
export interface AccountStatus {
ok?: boolean
message?: string
}
const App = window.go.main.App
const accounts = ref<Account[]>([])
const selectedIndices = ref<Set<number>>(new Set())
const statusMap = ref<Record<string, AccountStatus | boolean>>({})
const scoreMap = ref<Record<string, string>>({})
export function useAccounts() {
const { showToast } = useToast()
const loadAccounts = async () => {
const data = await App.GetAccounts()
accounts.value = data || []
// 清理无效选择
selectedIndices.value = new Set(
[...selectedIndices.value].filter(i => i >= 0 && i < accounts.value.length)
)
saveSelection()
}
const reloadAccounts = async () => {
const data = await App.ReloadAccounts()
await loadAccounts()
showToast('账号已刷新', `共 ${data.count} 个账号`, 'success')
}
const toggleAccount = (index: number) => {
if (selectedIndices.value.has(index)) {
selectedIndices.value.delete(index)
} else {
selectedIndices.value.add(index)
}
saveSelection()
}
const toggleAll = () => {
if (selectedIndices.value.size === accounts.value.length) {
selectedIndices.value.clear()
} else {
accounts.value.forEach((_, i) => selectedIndices.value.add(i))
}
saveSelection()
}
const saveSelection = () => {
localStorage.setItem('selAccs', JSON.stringify([...selectedIndices.value]))
}
const restoreSelection = () => {
const saved = localStorage.getItem('selAccs')
if (saved) {
try {
selectedIndices.value = new Set(JSON.parse(saved))
} catch {}
}
const savedStatus = localStorage.getItem('statusMap')
if (savedStatus) {
try {
statusMap.value = JSON.parse(savedStatus)
} catch {}
}
}
const queryAllScore = async () => {
showToast('查询积分', '正在并发查询...', 'info')
const tasks = accounts.value.map(async (a) => {
try {
const d = await App.QueryScore(a.name)
const score = d.score ?? 'N/A'
scoreMap.value[a.name] = String(score)
localStorage.setItem('accScore_' + a.name, String(score))
} catch {}
})
await Promise.all(tasks)
showToast('积分查询完成', '', 'success')
}
const getScore = (name: string): string => {
return scoreMap.value[name] || localStorage.getItem('accScore_' + name) || '-'
}
const resetStatus = () => {
statusMap.value = {}
localStorage.removeItem('statusMap')
}
const setStatus = (name: string, status: AccountStatus | boolean) => {
statusMap.value[name] = status
localStorage.setItem('statusMap', JSON.stringify(statusMap.value))
}
const selectedAccountNames = (): string[] => {
return [...selectedIndices.value]
.map(i => accounts.value[i]?.name)
.filter(Boolean) as string[]
}
const buildAccountProducts = (productId: string): Record<string, string> | null => {
if (selectedIndices.value.size === 0) {
showToast('提示', '请勾选要抢购的账号', 'warn')
return null
}
if (!productId) {
showToast('提示', '请先选择商品', 'warn')
return null
}
const ap: Record<string, string> = {}
selectedIndices.value.forEach(i => {
const account = accounts.value[i]
if (account) {
ap[account.name] = productId
}
})
if (Object.keys(ap).length === 0) {
showToast('提示', '没有有效账号可提交', 'warn')
return null
}
return ap
}
return {
accounts,
selectedIndices,
statusMap,
scoreMap,
loadAccounts,
reloadAccounts,
toggleAccount,
toggleAll,
restoreSelection,
queryAllScore,
getScore,
resetStatus,
setStatus,
selectedAccountNames,
buildAccountProducts
}
}
+113
View File
@@ -0,0 +1,113 @@
import { ref } from 'vue'
export interface LogEntry {
id: number
time: string
level: string
category: string
message: string
}
export interface LogMessage {
time: string
type: 's' | 'e' | 'i' | 'w' | 'd'
message: string
category?: string
source: 'local' | 'server'
}
const App = window.go.main.App
const logs = ref<LogMessage[]>([])
const results = ref<Record<string, string>>({})
const lastLogId = ref(0)
const lastResultsJson = ref('')
export function useLogs() {
const addLocalLog = (message: string, type: LogMessage['type'] = 'i') => {
const now = new Date()
const timeStr = now.toLocaleTimeString() + '.' + String(now.getMilliseconds()).padStart(3, '0')
logs.value.push({
time: timeStr,
type,
message,
source: 'local'
})
// 限制日志数量
if (logs.value.length > 500) {
logs.value = logs.value.slice(-300)
}
}
const fetchServerLogs = async () => {
try {
const d = await App.GetLogs(lastLogId.value)
if (!d.success || !Array.isArray(d.logs) || d.logs.length === 0) return
d.logs.forEach((item: LogEntry) => {
lastLogId.value = Math.max(lastLogId.value, item.id || 0)
let type: LogMessage['type'] = 'i'
if (item.level === 'success') type = 's'
else if (item.level === 'error') type = 'e'
else if (item.level === 'warn') type = 'w'
else if (item.level === 'debug') type = 'd'
// 清理日志前缀
const cleanMsg = String(item.message || '').replace(/\[[^\]]+\]\[(info|success|warn|error)\]\s*/i, '')
logs.value.push({
time: item.time || '',
type,
message: cleanMsg,
category: item.category,
source: 'server'
})
})
// 限制日志数量
if (logs.value.length > 500) {
logs.value = logs.value.slice(-300)
}
} catch {}
}
const checkResults = async () => {
try {
const d = await App.GetResults()
const json = JSON.stringify(d)
if (json !== lastResultsJson.value && Object.keys(d).length > 0) {
lastResultsJson.value = json
results.value = d
return true
}
return false
} catch {
return false
}
}
const clearLogs = () => {
logs.value = []
results.value = {}
lastResultsJson.value = ''
lastLogId.value = 0
}
const clearResults = async () => {
await App.ClearResults()
results.value = {}
lastResultsJson.value = ''
}
return {
logs,
results,
addLocalLog,
fetchServerLogs,
checkResults,
clearLogs,
clearResults
}
}
+126
View File
@@ -0,0 +1,126 @@
import { ref, computed } from 'vue'
import { useToast } from './useToast'
export interface Product {
commodityId: string
commodityName: string
score: number
count: number
pic: string
status: number
openStatus: number
storeStatus: number
timeLimited: number
countLimited: number
eliteLimited: number
}
export interface ProductPayload {
success: boolean
source: string
products: Product[]
count: number
fetchedAt: string
ageSecond: number
stale: boolean
}
const App = window.go.main.App
const products = ref<Product[]>([])
const productInfo = ref<ProductPayload | null>(null)
const selectedProductId = ref<string | null>(null)
const selectedProductName = ref('')
const searchQuery = ref('')
const loading = ref(false)
export function useProducts() {
const { showToast } = useToast()
const filteredProducts = computed(() => {
const keyword = searchQuery.value.toLowerCase().trim()
if (!keyword) return products.value
return products.value.filter(p =>
[p.commodityName, p.commodityId, p.score, p.count].some(
v => String(v ?? '').toLowerCase().includes(keyword)
)
)
})
const selectedProduct = computed(() =>
products.value.find(p => p.commodityId === selectedProductId.value)
)
const loadProducts = async (showToastMsg = true) => {
loading.value = true
try {
const data = await App.LoadProducts(true)
products.value = Array.isArray(data.products) ? data.products : []
productInfo.value = data
// 如果选中的商品不存在了,清空选择
if (selectedProductId.value && !products.value.some(p => p.commodityId === selectedProductId.value)) {
clearSelection()
}
if (showToastMsg) {
showToast('商品已刷新', `共 ${products.value.length} 个`, 'success')
}
} catch (e: any) {
products.value = []
productInfo.value = null
clearSelection()
showToast('商品刷新失败', e.message, 'error')
} finally {
loading.value = false
}
}
const selectProduct = (id: string, name: string) => {
selectedProductId.value = id
selectedProductName.value = name
localStorage.setItem('selProd', id)
localStorage.setItem('selProdName', name)
}
const clearSelection = () => {
selectedProductId.value = null
selectedProductName.value = ''
localStorage.removeItem('selProd')
localStorage.removeItem('selProdName')
}
const setSearch = (query: string) => {
searchQuery.value = query.trim()
}
const clearSearch = () => {
searchQuery.value = ''
}
const restoreSelection = () => {
const savedId = localStorage.getItem('selProd')
const savedName = localStorage.getItem('selProdName')
if (savedId) {
selectedProductId.value = savedId
selectedProductName.value = savedName || ''
}
}
return {
products,
productInfo,
selectedProductId,
selectedProductName,
selectedProduct,
searchQuery,
filteredProducts,
loading,
loadProducts,
selectProduct,
clearSelection,
setSearch,
clearSearch,
restoreSelection
}
}
+149
View File
@@ -0,0 +1,149 @@
import { ref } from 'vue'
import { useToast } from './useToast'
import { useAccounts } from './useAccounts'
import { useProducts } from './useProducts'
const App = window.go.main.App
const scheduleActive = ref(false)
const countdownSeconds = ref(0)
const countdownTimer = ref<number | null>(null)
const runState = ref('等待操作')
export function useScheduler() {
const { showToast } = useToast()
const { resetStatus, buildAccountProducts } = useAccounts()
const { selectedProductId, selectedProductName } = useProducts()
const setRunState = (text: string) => {
runState.value = text || '等待操作'
}
const startNow = async () => {
const ap = buildAccountProducts(selectedProductId.value || '')
if (!ap) return
resetStatus()
setRunState('立即抢购已提交')
showToast('抢购中', `${Object.keys(ap).length} 个账号正在抢购`, 'info', 3000)
try {
const d = await App.StartSeckill(ap)
setRunState(d.msg || '已提交')
} catch (e: any) {
setRunState('提交失败')
showToast('提交失败', e.message, 'error')
}
}
const startSchedule = async (targetTime: string, advance: number) => {
const ap = buildAccountProducts(selectedProductId.value || '')
if (!ap) return
if (!targetTime) {
showToast('提示', '请选择目标时间', 'warn')
return
}
// 格式化时间
let timeStr = targetTime
if (timeStr.split(':').length === 2) timeStr += ':00'
const today = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(new Date()).replace(/\//g, '-')
const fullTime = today + ' ' + timeStr
resetStatus()
setRunState('定时配置中')
try {
const d = await App.SetSchedule({
accountProducts: ap,
targetTime: fullTime,
advance
})
scheduleActive.value = true
localStorage.setItem('targetTime', timeStr)
localStorage.setItem('advance', String(advance))
setRunState(`定时已开启,触发 ${d.triggerTime}`)
showToast(
'定时已开启',
`触发时刻: ${d.triggerTime}<br>剩余时间: ${formatRemain(d.remainSecond)}`,
'success',
5000
)
startCountdown(d.remainSecond)
} catch (e: any) {
setRunState('定时设置失败')
showToast('定时失败', e.message, 'error')
}
}
const cancelSchedule = async () => {
try {
await App.CancelSchedule()
} catch {}
scheduleActive.value = false
localStorage.removeItem('targetTime')
localStorage.removeItem('advance')
stopCountdown()
setRunState('定时已停止')
showToast('定时已停止', '抢购任务已取消', 'warn')
}
const startCountdown = (sec: number) => {
stopCountdown()
const endTime = Date.now() + sec * 1000
countdownTimer.value = window.setInterval(() => {
const remain = Math.max(0, Math.ceil((endTime - Date.now()) / 1000))
countdownSeconds.value = remain
if (remain <= 0) {
scheduleActive.value = false
setRunState('定时已触发')
stopCountdown()
}
}, 500)
}
const stopCountdown = () => {
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
countdownSeconds.value = 0
}
const formatRemain = (sec: number): string => {
const h = Math.floor(sec / 3600)
const m = Math.floor((sec % 3600) / 60)
const s = sec % 60
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}
const restoreSchedule = () => {
const savedTime = localStorage.getItem('targetTime')
const savedAdv = localStorage.getItem('advance')
return { savedTime, savedAdv: savedAdv ? parseInt(savedAdv) : 300 }
}
return {
scheduleActive,
countdownSeconds,
runState,
setRunState,
startNow,
startSchedule,
cancelSchedule,
formatRemain,
restoreSchedule
}
}
+65
View File
@@ -0,0 +1,65 @@
import { ref } from 'vue'
import { useToast } from './useToast'
export interface Settings {
timeApiUrl: string
traceEnabled: boolean
traceMaxBody: number
logLevel: string
concurrency: number
attempts: number
}
const App = window.go.main.App
const settings = ref<Settings>({
timeApiUrl: '',
traceEnabled: false,
traceMaxBody: 0,
logLevel: 'info',
concurrency: 80,
attempts: 1
})
export function useSettings() {
const { showToast } = useToast()
const loadSettings = async () => {
try {
const cfg = await App.GetSettings()
settings.value = {
timeApiUrl: cfg.timeApiUrl || '',
traceEnabled: !!cfg.traceEnabled,
traceMaxBody: cfg.traceMaxBody || 0,
logLevel: cfg.logLevel || 'info',
concurrency: cfg.concurrency || 80,
attempts: cfg.attempts || 1
}
} catch (e: any) {
showToast('读取设置失败', e.message, 'error')
}
}
const saveSettings = async (newSettings: Settings) => {
try {
const d = await App.SaveSettings(newSettings)
if (d.success) {
settings.value = { ...newSettings }
showToast('设置已保存', '配置已生效', 'success')
return true
} else {
showToast('保存失败', d.error || '', 'error')
return false
}
} catch (e: any) {
showToast('保存失败', e.message, 'error')
return false
}
}
return {
settings,
loadSettings,
saveSettings
}
}
+29
View File
@@ -0,0 +1,29 @@
import { ref, watch, onMounted } from 'vue'
export type Theme = 'dark' | 'light'
const theme = ref<Theme>('dark')
export function useTheme() {
const applyTheme = (t: Theme) => {
theme.value = t
document.body.dataset.theme = t
localStorage.setItem('uiTheme', t)
}
const toggleTheme = () => {
applyTheme(theme.value === 'light' ? 'dark' : 'light')
}
const initTheme = () => {
const saved = localStorage.getItem('uiTheme') as Theme | null
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
applyTheme(saved || (prefersLight ? 'light' : 'dark'))
}
return {
theme,
toggleTheme,
initTheme
}
}
+38
View File
@@ -0,0 +1,38 @@
import { ref, reactive } from 'vue'
export interface Toast {
id: number
title: string
body: string
type: 'success' | 'error' | 'warn' | 'info'
duration: number
}
const toasts = ref<Toast[]>([])
let nextId = 0
export function useToast() {
const showToast = (title: string, body: string, type: Toast['type'] = 'info', duration = 4000) => {
const id = nextId++
const toast: Toast = { id, title, body, type, duration }
toasts.value.push(toast)
// 自动移除
setTimeout(() => {
removeToast(id)
}, duration)
}
const removeToast = (id: number) => {
const index = toasts.value.findIndex(t => t.id === id)
if (index !== -1) {
toasts.value.splice(index, 1)
}
}
return {
toasts,
showToast,
removeToast
}
}