Files
dygou_go/frontend/src/components/ck/CKManager.vue
T
yml2213 9bff652c66 feat: 自定义重试间隔策略 + 修复按钮hover样式
- 新增重试间隔阶段配置,支持自定义时间分界点和频率
- 默认配置: 0-8秒=130ms, 8-25秒=260ms, 25-75秒=900ms
- 设置面板添加可增删的阶段配置 UI
- 修复按钮 hover 时文字变白的问题
2026-06-26 13:46:46 +08:00

526 lines
11 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 { ref, computed } from 'vue'
import { useProducts } from '@/composables/useProducts'
import { useToast } from '@/composables/useToast'
const { selectedProductId, selectedProductName, selectedProduct } = useProducts()
const { showToast } = useToast()
const App = window.go.main.App
// 状态
const loading = ref(false)
const verifyResult = ref<any>(null)
const organizeResult = ref<any>(null)
// 时间范围(默认一年)
const timeRange = ref<'year' | 'custom'>('year')
const customStart = ref('')
const customEnd = ref('')
// 计算时间戳
const timeRangeMs = computed(() => {
const now = Date.now()
if (timeRange.value === 'year') {
return {
start: Math.floor((now - 365 * 24 * 60 * 60 * 1000) / 1000),
end: Math.floor(now / 1000)
}
}
return {
start: customStart.value ? Math.floor(new Date(customStart.value).getTime() / 1000) : 0,
end: customEnd.value ? Math.floor(new Date(customEnd.value).getTime() / 1000) : Math.floor(now / 1000)
}
})
// 复验
const handleVerify = async () => {
if (!selectedProductId.value) {
showToast('提示', '请先在左侧选择商品', 'warn')
return
}
loading.value = true
try {
const result = await App.VerifyAndExport(selectedProductId.value)
verifyResult.value = result
organizeResult.value = null
if (result.success) {
showToast('复验完成', `已抢到: ${result.hitCount}, 未抢到: ${result.missCount}`, 'success')
} else {
showToast('复验失败', result.error, 'error')
}
} catch (e: any) {
showToast('复验失败', e.message, 'error')
} finally {
loading.value = false
}
}
// 导出已抢到的CK
const exportHitAccounts = async () => {
if (!verifyResult.value?.hitAccounts?.length || !selectedProductId.value) {
showToast('提示', '没有可导出的账号', 'warn')
return
}
try {
const result = await App.ExportAccountsWithProduct(
verifyResult.value.hitAccounts,
selectedProductId.value,
selectedProductName.value || ''
)
if (result.success) {
showToast('导出成功', `已导出 ${result.count} 个账号到 ${result.filePath}`, 'success')
} else {
showToast('导出失败', result.error, 'error')
}
} catch (e: any) {
showToast('导出失败', e.message, 'error')
}
}
// 整理CK
const handleOrganize = async () => {
if (!selectedProductId.value) {
showToast('提示', '请先在左侧选择商品', 'warn')
return
}
loading.value = true
try {
const { start, end } = timeRangeMs.value
const result = await App.OrganizeAccounts(selectedProductId.value, selectedProductName.value, start, end)
organizeResult.value = result
verifyResult.value = null
if (result.success) {
showToast('整理完成', `已抢到: ${result.hitCount}, 未抢到: ${result.missCount}`, 'success')
} else {
showToast('整理失败', result.error, 'error')
}
} catch (e: any) {
showToast('整理失败', e.message, 'error')
} finally {
loading.value = false
}
}
</script>
<template>
<div class="ck-manager">
<div class="ck-header">
<h2>CK 管理</h2>
<p class="ck-desc">复验抢购结果、整理账号文件</p>
</div>
<!-- 当前选中商品 -->
<div class="ck-section">
<div class="section-title">当前商品</div>
<div class="selected-product">
<template v-if="selectedProductId">
<span class="product-name">{{ selectedProductName }}</span>
<span class="product-meta">{{ selectedProduct?.score }}积分</span>
</template>
<span v-else class="product-empty">请在左侧商品列表中选择商品</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="ck-actions">
<button
class="btn btn-primary"
:disabled="loading || !selectedProductId"
@click="handleVerify"
>
{{ loading ? '查询中...' : '🔍 复验抢购结果' }}
</button>
<button
class="btn btn-success"
:disabled="loading || !selectedProductId"
@click="handleOrganize"
>
{{ loading ? '整理中...' : '📋 整理CK' }}
</button>
</div>
<!-- 时间范围(整理CK时使用) -->
<div class="ck-section">
<div class="section-title">时间范围</div>
<div class="time-range">
<label class="radio-label">
<input v-model="timeRange" type="radio" value="year">
<span>最近一年(默认)</span>
</label>
<label class="radio-label">
<input v-model="timeRange" type="radio" value="custom">
<span>自定义</span>
</label>
</div>
<div v-if="timeRange === 'custom'" class="custom-time">
<input v-model="customStart" type="date" class="form-input" placeholder="开始日期">
<span class="time-sep">至</span>
<input v-model="customEnd" type="date" class="form-input" placeholder="结束日期">
</div>
</div>
<!-- 复验结果 -->
<div v-if="verifyResult" class="ck-result">
<div class="result-header">
<span class="result-title">复验结果</span>
<button
v-if="verifyResult.hitAccounts?.length"
class="btn btn-compact btn-export"
@click="exportHitAccounts"
>
📥 导出已抢到CK
</button>
</div>
<div class="result-stats">
<div class="stat-item stat-success">
<span class="stat-label">已抢到</span>
<span class="stat-value">{{ verifyResult.hitCount }}</span>
</div>
<div class="stat-item stat-fail">
<span class="stat-label">未抢到</span>
<span class="stat-value">{{ verifyResult.missCount }}</span>
</div>
</div>
<div v-if="verifyResult.hitAccounts?.length" class="account-list">
<div class="list-title">已抢到的账号:</div>
<div class="list-items">
<span
v-for="name in verifyResult.hitAccounts"
:key="name"
class="account-tag hit"
>
{{ name }}
</span>
</div>
</div>
</div>
<!-- 整理结果 -->
<div v-if="organizeResult" class="ck-result">
<div class="result-header">
<span class="result-title">整理结果</span>
</div>
<div class="result-stats">
<div class="stat-item stat-success">
<span class="stat-label">已抢到</span>
<span class="stat-value">{{ organizeResult.hitCount }}</span>
</div>
<div class="stat-item stat-fail">
<span class="stat-label">未抢到</span>
<span class="stat-value">{{ organizeResult.missCount }}</span>
</div>
</div>
<div class="result-info">
<div v-if="organizeResult.hitCount > 0" class="info-item success">
✅ 已抢到的账号已导出到新文件
</div>
<div class="info-item">
📄 原文件已更新,只保留未抢到的账号
</div>
</div>
</div>
</div>
</template>
<style scoped>
.ck-manager {
padding: 20px;
max-width: 800px;
}
.ck-header {
margin-bottom: 24px;
}
.ck-header h2 {
font-size: 18px;
font-weight: 600;
color: var(--text-strong);
margin-bottom: 4px;
}
.ck-desc {
font-size: 13px;
color: var(--muted);
}
.ck-section {
margin-bottom: 20px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-strong);
margin-bottom: 8px;
}
.selected-product {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--field);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
min-height: 36px;
}
.selected-product.has-product {
border-color: var(--accent-border);
background: var(--accent-soft);
}
.product-name {
font-weight: 600;
color: var(--text-strong);
}
.product-meta {
font-size: 12px;
color: var(--muted);
}
.product-empty {
color: var(--muted);
font-size: 13px;
}
.form-input {
width: 100%;
height: 36px;
padding: 0 12px;
background: var(--field);
color: var(--text-strong);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
font-size: 13px;
}
.form-input:focus {
outline: 0;
border-color: var(--accent);
box-shadow: var(--focus);
}
.ck-actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.btn {
height: 36px;
padding: 0 16px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface-2);
color: var(--text);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: .12s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background: var(--surface-3);
border-color: var(--line-strong);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: #4d9aff;
border-color: #4d9aff;
}
.btn-success {
background: var(--success);
border-color: var(--success);
color: #fff;
}
.btn-success:hover {
background: #4fc95f;
border-color: #4fc95f;
}
.btn-compact {
height: 28px;
font-size: 12px;
padding: 0 10px;
}
.btn-export {
background: var(--accent-soft);
color: var(--accent);
border-color: var(--accent-border);
}
.btn-export:hover {
background: var(--accent);
color: #fff;
}
.time-range {
display: flex;
gap: 16px;
margin-bottom: 8px;
}
.radio-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 13px;
}
.radio-label input {
cursor: pointer;
}
.custom-time {
display: flex;
align-items: center;
gap: 8px;
}
.custom-time .form-input {
width: auto;
flex: 1;
}
.time-sep {
color: var(--muted);
}
.ck-result {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 16px;
margin-top: 20px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-title {
font-size: 14px;
font-weight: 600;
color: var(--text-strong);
}
.result-stats {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 20px;
background: var(--field);
border-radius: var(--radius-sm);
border: 1px solid var(--line);
min-width: 80px;
}
.stat-label {
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
}
.stat-value {
font-size: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.stat-success .stat-value {
color: var(--success);
}
.stat-fail .stat-value {
color: var(--muted);
}
.account-list {
margin-top: 12px;
}
.list-title {
font-size: 12px;
color: var(--muted);
margin-bottom: 8px;
}
.list-items {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.account-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
background: var(--surface-2);
color: var(--text);
border: 1px solid var(--line);
}
.account-tag.hit {
background: var(--success-soft);
color: var(--success);
border-color: var(--success);
}
.result-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-item {
font-size: 13px;
color: var(--text);
}
.info-item.success {
color: var(--success);
}
</style>