From 87cb1dc3f09766ec61c94edfcb950ce3c9e75819 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 9 Aug 2026 14:06:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BA=93=E5=AD=98=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E5=B9=B6=E4=BC=98=E5=8C=96=E5=BC=80=E5=8F=91=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/console/ControlStrip.vue | 189 +++++++++++++++--- .../src/components/product/ProductCard.vue | 8 +- .../src/components/product/ProductList.vue | 7 +- internal/config/config.go | 15 +- internal/config/config_test.go | 32 +++ 5 files changed, 223 insertions(+), 28 deletions(-) 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(() => { +
+ 库存抢购 +
+ 监控 +
+ + +
+
+ + + 秒 +
+ 抢购 + {{ stockWatchTargetName }} +
+ + +
+
+ + + ms +
+ +
+
+
@@ -289,6 +414,10 @@ onUnmounted(() => { padding-top: 2px; } +.stock-watch-row { + padding-top: 0; +} + .target-item { flex: 0 0 auto; margin-left: 4px; @@ -365,6 +494,18 @@ onUnmounted(() => { .btn-fixed { border-color: var(--warning); color: var(--warning); } .btn-watch { border-color: var(--accent-border); color: var(--accent); } .btn-watch.watching { background: var(--accent-soft); } +.btn-stock-watch { border-color: var(--success); color: var(--success); } +.btn-stock-watch.watching { background: var(--success-soft); } +.stock-watch-target { + display: inline-block; + max-width: 180px; + overflow: hidden; + color: var(--text-strong); + font-size: 12px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} @media (max-width: 1180px) { .control-strip { diff --git a/frontend/src/components/product/ProductCard.vue b/frontend/src/components/product/ProductCard.vue index 899cbc3..888d894 100644 --- a/frontend/src/components/product/ProductCard.vue +++ b/frontend/src/components/product/ProductCard.vue @@ -10,6 +10,12 @@ const priceLabel = computed(() => { if (!isXiaodian.value) return '积分' return selectedProduct.value?.payLabel || '点券' }) +const displayPrice = computed(() => { + if (!selectedProduct.value) return '' + return isXiaodian.value && selectedProduct.value.payType === 1 + ? selectedProduct.value.score / 10 + : selectedProduct.value.score +}) const stockText = computed(() => { if (!selectedProduct.value) return '' if (isXiaodian.value && selectedProduct.value.stockLabel) return selectedProduct.value.stockLabel @@ -30,7 +36,7 @@ const stockIsLow = computed(() => {
{{ selectedProductName || '未选' }}