优化前端
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user