Files
dygou_go/frontend/src/components/console/ControlStrip.vue
T

345 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onUnmounted, ref, watch } from 'vue'
import { useScheduler } from '@/composables/useScheduler'
import { useAccounts } from '@/composables/useAccounts'
import { useActivity } from '@/composables/useActivity'
import { useProducts } from '@/composables/useProducts'
import { useToast } from '@/composables/useToast'
const { scheduleActive, startNow, startFixed, startSchedule, cancelSchedule, restoreSchedule } = useScheduler()
const { reloadAccounts, queryAllScore, queryAllXiaodianAssets } = useAccounts()
const { currentActivity } = useActivity()
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
const { products, selectedProductId, loadProducts, selectProduct } = useProducts()
const { showToast } = useToast()
const fixedKeyword = ref(localStorage.getItem('xiaodianFixedKeyword') || localStorage.getItem('xiaodianWatchKeyword') || '')
const fixedAttempts = ref(Number(localStorage.getItem('xiaodianFixedAttempts') || 3))
const fixedDelayMs = ref(Number(localStorage.getItem('xiaodianFixedDelayMs') || 500))
const watchStartTime = ref(localStorage.getItem('xiaodianWatchStartTime') || '00:00:00')
const watchIntervalSec = ref(Number(localStorage.getItem('xiaodianWatchIntervalSec') || 3))
const keywordWatching = ref(false)
let keywordTimer: number | null = null
let keywordStartTimer: number | null = null
let keywordRefreshing = false
// 从本地缓存恢复定时设置
const { savedTime } = restoreSchedule()
const targetTime = ref(savedTime || '18:00:00')
const handleSchedule = () => {
startSchedule(targetTime.value)
}
const queryAssets = () => {
if (isXiaodian.value) {
queryAllXiaodianAssets()
return
}
queryAllScore()
}
const saveFixedConfig = () => {
localStorage.setItem('xiaodianFixedKeyword', fixedKeyword.value.trim())
localStorage.setItem('xiaodianWatchKeyword', fixedKeyword.value.trim())
localStorage.setItem('xiaodianFixedAttempts', String(fixedAttempts.value))
localStorage.setItem('xiaodianFixedDelayMs', String(fixedDelayMs.value))
localStorage.setItem('xiaodianWatchStartTime', watchStartTime.value)
localStorage.setItem('xiaodianWatchIntervalSec', String(watchIntervalSec.value))
}
const startFixedBuy = () => {
saveFixedConfig()
if (keywordWatching.value) stopKeywordWatch()
startFixed(fixedAttempts.value, fixedDelayMs.value)
}
const stopKeywordWatch = () => {
keywordWatching.value = false
if (keywordTimer) {
clearInterval(keywordTimer)
keywordTimer = null
}
if (keywordStartTimer) {
clearTimeout(keywordStartTimer)
keywordStartTimer = null
}
keywordRefreshing = 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) {
stopKeywordWatch()
selectProduct(hit.commodityId, hit.commodityName)
await startFixed(fixedAttempts.value, fixedDelayMs.value)
return
}
const interval = Math.max(1, Math.min(600, Number(watchIntervalSec.value) || 3)) * 1000
keywordTimer = window.setInterval(async () => {
if (!keywordWatching.value || scheduleActive.value || keywordRefreshing) return
keywordRefreshing = true
try {
await loadProducts(false)
const currentKeyword = fixedKeyword.value.trim().toLowerCase()
const currentHit = products.value.find(p => p.commodityName.toLowerCase().includes(currentKeyword))
if (currentHit) {
stopKeywordWatch()
selectProduct(currentHit.commodityId, currentHit.commodityName)
await startFixed(fixedAttempts.value, fixedDelayMs.value)
}
} finally {
keywordRefreshing = false
}
}, interval)
}
const toggleKeywordWatch = () => {
if (keywordWatching.value) {
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 {
void beginKeywordWatch()
}
}
}
watch(isXiaodian, (enabled) => {
if (!enabled && keywordWatching.value) {
stopKeywordWatch()
}
})
onUnmounted(() => {
stopKeywordWatch()
})
</script>
<template>
<div class="control-strip">
<div class="action-row">
<div class="action-group">
<button class="btn btn-danger" @click="startNow">立即抢购</button>
<button
v-if="!scheduleActive"
class="btn btn-success"
@click="handleSchedule"
>
开启定时
</button>
<button
v-else
class="btn btn-cancel"
@click="cancelSchedule"
>
停止定时
</button>
<button class="btn" @click="reloadAccounts">加载账号</button>
<button class="btn" @click="queryAssets">{{ isXiaodian ? '查询点券/碎片' : '查询积分' }}</button>
<button v-if="isXiaodian" class="btn btn-fixed" @click="startFixedBuy">固定抢购</button>
</div>
<div class="schedule-item target-item">
<label>目标</label>
<input v-model="targetTime" type="time" step="1">
</div>
</div>
<div v-if="isXiaodian" class="keyword-row">
<span class="settings-title keyword-title">关键词抢购</span>
<div class="xiaodian-settings">
<span class="settings-title">监控</span>
<div class="schedule-item">
<label>启动</label>
<input v-model="watchStartTime" type="time" step="1" @change="saveFixedConfig">
</div>
<div class="schedule-item">
<label>刷新</label>
<input v-model.number="watchIntervalSec" type="number" min="1" max="600" step="1" @change="saveFixedConfig">
<span class="input-unit">秒</span>
</div>
<span class="settings-title buy-title">抢购</span>
<div class="schedule-item fixed-keyword">
<label>关键词</label>
<input v-model="fixedKeyword" type="text" placeholder="例如:礼包" @change="saveFixedConfig">
</div>
<div class="schedule-item">
<label>次数</label>
<input v-model.number="fixedAttempts" type="number" min="1" max="100000" step="1" @change="saveFixedConfig">
</div>
<div class="schedule-item">
<label>间隔</label>
<input v-model.number="fixedDelayMs" type="number" min="0" max="60000" step="50" @change="saveFixedConfig">
<span class="input-unit">ms</span>
</div>
<button class="btn btn-watch" :class="{ watching: keywordWatching }" @click="toggleKeywordWatch">
{{ keywordWatching ? '停止监控' : '启动监控' }}
</button>
</div>
</div>
<!-- 非和平小店仅显示目标时间,保持原有定时布局。 -->
<div v-else class="schedule-bar">
<div class="schedule-item">
<label>目标</label>
<input
v-model="targetTime"
type="time"
step="1"
>
</div>
</div>
</div>
</template>
<style scoped>
.control-strip {
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.action-group {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.action-row,
.keyword-row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
width: 100%;
}
.keyword-row {
justify-content: flex-start;
padding-top: 2px;
}
.target-item {
flex: 0 0 auto;
margin-left: 4px;
}
.keyword-title {
flex: 0 0 auto;
color: var(--text-strong);
font-size: 12px;
}
.schedule-bar {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.schedule-item {
display: flex;
align-items: center;
gap: 6px;
}
.schedule-item label {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.schedule-item input {
height: 30px;
padding: 0 10px;
background: var(--field);
color: var(--text-strong);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
font-size: 12.5px;
font-variant-numeric: tabular-nums;
transition: .15s;
}
.schedule-item input:focus {
outline: 0;
border-color: var(--accent);
box-shadow: var(--focus);
}
.schedule-item input[type="time"] {
width: 108px;
text-align: center;
}
.schedule-item input[type="number"] {
width: 72px;
text-align: center;
}
.fixed-keyword input { width: 132px; }
.xiaodian-settings {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.settings-title {
color: var(--muted);
font-size: 11px;
font-weight: 600;
margin-right: 2px;
}
.buy-title { margin-left: 8px; }
.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); }
@media (max-width: 1180px) {
.control-strip {
flex-direction: column;
align-items: stretch;
}
.schedule-bar,
.action-row,
.keyword-row {
justify-content: flex-end;
}
.action-row,
.keyword-row {
align-items: flex-start;
flex-direction: column;
}
}
</style>