diff --git a/frontend/src/components/console/ControlStrip.vue b/frontend/src/components/console/ControlStrip.vue index 54bec23..2fb82aa 100644 --- a/frontend/src/components/console/ControlStrip.vue +++ b/frontend/src/components/console/ControlStrip.vue @@ -18,7 +18,7 @@ const { } = useAccounts() const { currentActivity } = useActivity() const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian') -const { products, selectedProductId, loadProducts, selectProduct } = useProducts() +const { products, selectedProductId, selectedProductName, loadProducts, selectProduct } = useProducts() const { showToast } = useToast() const fixedKeyword = ref(localStorage.getItem('xiaodianFixedKeyword') || localStorage.getItem('xiaodianWatchKeyword') || '') const fixedAttempts = ref(Number(localStorage.getItem('xiaodianFixedAttempts') || 3)) @@ -29,6 +29,16 @@ const keywordWatching = ref(false) let keywordTimer: number | null = null let keywordStartTimer: number | null = null let keywordRefreshing = false +const stockWatchStartTime = ref(localStorage.getItem('xiaodianStockWatchStartTime') || '00:00:00') +const stockWatchIntervalSec = ref(Number(localStorage.getItem('xiaodianStockWatchIntervalSec') || 3)) +const stockWatchAttempts = ref(Number(localStorage.getItem('xiaodianStockWatchAttempts') || 3)) +const stockWatchDelayMs = ref(Number(localStorage.getItem('xiaodianStockWatchDelayMs') || 500)) +const stockWatching = ref(false) +const stockWatchTarget = ref<{ id: string; name: string } | null>(null) +const stockWatchTargetName = computed(() => stockWatchTarget.value?.name || selectedProductName.value || '请先在左侧选择商品') +let stockWatchTimer: number | null = null +let stockWatchStartTimer: number | null = null +let stockRefreshing = false // 从本地缓存恢复定时设置 const { savedTime } = restoreSchedule() @@ -81,9 +91,17 @@ const saveFixedConfig = () => { localStorage.setItem('xiaodianWatchIntervalSec', String(watchIntervalSec.value)) } +const saveStockWatchConfig = () => { + localStorage.setItem('xiaodianStockWatchStartTime', stockWatchStartTime.value) + localStorage.setItem('xiaodianStockWatchIntervalSec', String(stockWatchIntervalSec.value)) + localStorage.setItem('xiaodianStockWatchAttempts', String(stockWatchAttempts.value)) + localStorage.setItem('xiaodianStockWatchDelayMs', String(stockWatchDelayMs.value)) +} + const startFixedBuy = () => { saveFixedConfig() - if (keywordWatching.value) stopKeywordWatch() + stopKeywordWatch() + stopStockWatch() void startFixed(fixedAttempts.value, fixedDelayMs.value) } @@ -100,12 +118,25 @@ const stopKeywordWatch = () => { keywordRefreshing = false } +const stopStockWatch = () => { + stockWatching.value = false + if (stockWatchTimer) { + clearInterval(stockWatchTimer) + stockWatchTimer = null + } + if (stockWatchStartTimer) { + clearTimeout(stockWatchStartTimer) + stockWatchStartTimer = null + } + stockRefreshing = false +} + const beginKeywordWatch = async () => { if (!keywordWatching.value || !isXiaodian.value) return await loadProducts(true) const keyword = fixedKeyword.value.trim().toLowerCase() const hit = products.value.find(p => p.commodityName.toLowerCase().includes(keyword)) - if (hit) { + if (hit && !scheduleActive.value && !seckillRunning.value) { stopKeywordWatch() selectProduct(hit.commodityId, hit.commodityName) await startFixed(fixedAttempts.value, fixedDelayMs.value) @@ -113,7 +144,7 @@ const beginKeywordWatch = async () => { } const interval = Math.max(1, Math.min(600, Number(watchIntervalSec.value) || 3)) * 1000 keywordTimer = window.setInterval(async () => { - if (!keywordWatching.value || scheduleActive.value || keywordRefreshing) return + if (!keywordWatching.value || scheduleActive.value || seckillRunning.value || keywordRefreshing) return keywordRefreshing = true try { await loadProducts(false) @@ -135,38 +166,102 @@ const toggleKeywordWatch = () => { stopKeywordWatch() return } - keywordWatching.value = !keywordWatching.value - if (keywordWatching.value) { - saveFixedConfig() - if (!fixedKeyword.value.trim()) { - stopKeywordWatch() - showToast('请输入关键词', '设置商品关键词后再开始监控', 'warn') - return - } - const [hours, minutes, seconds] = watchStartTime.value.split(':').map(Number) - const target = new Date() - target.setHours(hours || 0, minutes || 0, seconds || 0, 0) - const waitMs = Math.max(0, target.getTime() - Date.now()) - if (waitMs > 0) { - showToast('监控已设置', `${watchStartTime.value} 开始监控`, 'info', 3000) - keywordStartTimer = window.setTimeout(() => { - keywordStartTimer = null - void beginKeywordWatch() - }, waitMs) - } else { + if (!fixedKeyword.value.trim()) { + showToast('请输入关键词', '设置商品关键词后再开始监控', 'warn') + return + } + saveFixedConfig() + stopStockWatch() + keywordWatching.value = true + const [hours, minutes, seconds] = watchStartTime.value.split(':').map(Number) + const target = new Date() + target.setHours(hours || 0, minutes || 0, seconds || 0, 0) + const waitMs = Math.max(0, target.getTime() - Date.now()) + if (waitMs > 0) { + showToast('监控已设置', `${watchStartTime.value} 开始关键词监控`, 'info', 3000) + keywordStartTimer = window.setTimeout(() => { + keywordStartTimer = null void beginKeywordWatch() + }, waitMs) + } else { + void beginKeywordWatch() + } +} + +const tryStartStockBuy = async () => { + const target = stockWatchTarget.value + if (!target) return false + if (scheduleActive.value || seckillRunning.value) return false + const product = products.value.find(item => item.commodityId === target.id) + // count 为 -1 时代表库存未公开,不能把未知状态误判为可抢。 + if (!product || product.count <= 0) return false + + stopStockWatch() + selectProduct(product.commodityId, product.commodityName) + const stockText = product.stockLabel || `库存 ${product.count}` + showToast('检测到库存', `${product.commodityName} ${stockText},开始固定抢购`, 'info', 3000) + await startFixed(stockWatchAttempts.value, stockWatchDelayMs.value) + return true +} + +const beginStockWatch = async () => { + if (!stockWatching.value || !isXiaodian.value || !stockWatchTarget.value) return + await loadProducts(true) + if (await tryStartStockBuy()) return + + const interval = Math.max(1, Math.min(600, Number(stockWatchIntervalSec.value) || 3)) * 1000 + stockWatchTimer = window.setInterval(async () => { + if (!stockWatching.value || scheduleActive.value || seckillRunning.value || stockRefreshing) return + stockRefreshing = true + try { + await loadProducts(false) + await tryStartStockBuy() + } finally { + stockRefreshing = false } + }, interval) +} + +const toggleStockWatch = () => { + if (stockWatching.value) { + stopStockWatch() + return + } + const product = products.value.find(item => item.commodityId === selectedProductId.value) + if (!product) { + showToast('请选择商品', '请先在左侧选择要监控库存的商品', 'warn') + return + } + + saveStockWatchConfig() + stopKeywordWatch() + stockWatchTarget.value = { id: product.commodityId, name: product.commodityName } + stockWatching.value = true + const [hours, minutes, seconds] = stockWatchStartTime.value.split(':').map(Number) + const target = new Date() + target.setHours(hours || 0, minutes || 0, seconds || 0, 0) + const waitMs = Math.max(0, target.getTime() - Date.now()) + if (waitMs > 0) { + showToast('库存监控已设置', `${stockWatchTarget.value.name} 将在 ${stockWatchStartTime.value} 开始监控`, 'info', 3000) + stockWatchStartTimer = window.setTimeout(() => { + stockWatchStartTimer = null + void beginStockWatch() + }, waitMs) + } else { + void beginStockWatch() } } watch(isXiaodian, (enabled) => { - if (!enabled && keywordWatching.value) { + if (!enabled) { stopKeywordWatch() + stopStockWatch() } }) onUnmounted(() => { stopKeywordWatch() + stopStockWatch() unsubscribeSchedulerEvents() }) @@ -242,6 +337,36 @@ onUnmounted(() => { +