优化前端

This commit is contained in:
yml2213
2026-06-26 10:56:25 +08:00
parent 88a386b588
commit 54b5bd90ae
39 changed files with 6796 additions and 216 deletions
@@ -0,0 +1,445 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useSettings } from '@/composables/useSettings'
const { settings, loadSettings, saveSettings } = useSettings()
const visible = ref(false)
const formData = ref({
timeApiUrl: '',
traceEnabled: false,
traceMaxBody: 0,
logLevel: 'info',
concurrency: 80,
attempts: 1
})
const open = async () => {
await loadSettings()
formData.value = { ...settings.value }
visible.value = true
}
const close = () => {
visible.value = false
}
const handleSave = async () => {
const success = await saveSettings(formData.value)
if (success) {
close()
}
}
// 暴露给父组件
defineExpose({ open, close })
</script>
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal">
<div class="modal-header">
<h3>⚙ 设置</h3>
<button class="modal-close" @click="close">×</button>
</div>
<div class="modal-body">
<!-- 时间 API -->
<div class="form-group">
<label class="form-label">时间 API 地址</label>
<input
v-model="formData.timeApiUrl"
class="form-input"
type="text"
placeholder="留空使用默认地址"
>
<div class="form-hint">用于获取斗鱼服务端时间,一般不需要修改</div>
</div>
<!-- 日志设置 -->
<div class="form-section">
<div class="form-section-title">日志设置</div>
<div class="form-group">
<div class="toggle-row">
<div>
<span class="toggle-label">Trace 日志</span>
<div class="form-hint">开启后会在 logs/ 目录生成 douyu_trace_*.log</div>
</div>
<label class="toggle">
<input v-model="formData.traceEnabled" type="checkbox">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Trace 正文上限</label>
<div class="input-with-unit">
<input
v-model.number="formData.traceMaxBody"
class="form-input"
type="number"
min="0"
step="1024"
placeholder="0"
>
<span class="input-unit">字节</span>
</div>
<div class="form-hint">0 表示不限制</div>
</div>
<div class="form-group">
<label class="form-label">日志显示等级</label>
<select v-model="formData.logLevel" class="form-select">
<option value="debug">Debug</option>
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
</div>
</div>
<!-- 抢购设置 -->
<div class="form-section">
<div class="form-section-title">抢购设置</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">并发数</label>
<input
v-model.number="formData.concurrency"
class="form-input"
type="number"
min="1"
max="500"
step="10"
>
<div class="form-hint">同时发起的请求数量,建议 50-200</div>
</div>
<div class="form-group">
<label class="form-label">尝试包数</label>
<input
v-model.number="formData.attempts"
class="form-input"
type="number"
min="1"
max="10"
step="1"
>
<div class="form-hint">每个账号生成几个抢购包,轮询使用</div>
</div>
</div>
<div class="info-card">
<div class="info-title">💡 抢购策略说明</div>
<ul class="info-list">
<li>抢购窗口:<strong>75 秒</strong>,自动循环重试</li>
<li>前 8 秒高频(180ms),之后逐渐降速</li>
<li>尝试包数 &gt; 1 时,每轮使用不同的包</li>
</ul>
</div>
<div class="info-card info-card-warn">
<div class="info-title">⚠️ 自动停止条件</div>
<ul class="info-list">
<li><strong>成功</strong> - 抢到商品</li>
<li><strong>库存耗尽</strong> - 商品本次活动库存已耗完</li>
<li><strong>兑换上限</strong> - 该账号已达本次活动兑换上限</li>
<li><strong>未绑定</strong> - 账号未绑定角色</li>
<li><strong>资格不足</strong> - 需要开通精英手册</li>
<li><strong>超时</strong> - 75秒抢购窗口结束</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" @click="close">取消</button>
<button class="btn btn-primary" @click="handleSave">保存</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.modal {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
width: 520px;
max-width: 92vw;
max-height: 85vh;
overflow-y: auto;
box-shadow: var(--shadow);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--line);
}
.modal-header h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-strong);
display: flex;
align-items: center;
gap: 8px;
}
.modal-close {
width: 32px;
height: 32px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--muted);
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: .15s;
}
.modal-close:hover {
background: var(--surface-2);
color: var(--text-strong);
}
.modal-body {
padding: 20px;
}
.form-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.form-section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-strong);
margin-bottom: 14px;
}
.form-group {
margin-bottom: 16px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 12.5px;
font-weight: 500;
color: var(--text);
margin-bottom: 6px;
}
.form-hint {
font-size: 11px;
color: var(--muted);
margin-top: 4px;
line-height: 1.5;
}
.form-row {
display: flex;
gap: 16px;
}
.form-row .form-group {
flex: 1;
}
.form-input, .form-select {
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;
transition: .15s;
}
.form-input:focus, .form-select:focus {
outline: 0;
border-color: var(--accent);
box-shadow: var(--focus);
}
.input-with-unit {
display: flex;
align-items: center;
gap: 8px;
}
.input-with-unit .form-input {
flex: 1;
}
.input-unit {
font-size: 12px;
color: var(--muted);
white-space: nowrap;
}
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.toggle-label {
font-size: 13px;
color: var(--text-strong);
font-weight: 500;
}
.toggle {
position: relative;
width: 44px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
inset: 0;
background: var(--line-strong);
border-radius: 999px;
transition: .2s;
}
.toggle-slider::before {
content: "";
position: absolute;
left: 2px;
top: 2px;
width: 20px;
height: 20px;
background: #fff;
border-radius: 50%;
transition: .2s;
}
.toggle input:checked + .toggle-slider {
background: var(--accent);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(20px);
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid var(--line);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.info-card {
margin-top: 16px;
padding: 14px;
background: var(--accent-soft);
border: 1px solid var(--accent-border);
border-radius: var(--radius-sm);
}
.info-card-warn {
background: var(--warning-soft);
border-color: var(--warning);
}
.info-card-warn .info-title {
color: var(--warning);
}
.info-title {
font-size: 12.5px;
font-weight: 600;
color: var(--accent);
margin-bottom: 10px;
}
.info-list {
margin: 0;
padding-left: 18px;
font-size: 12px;
color: var(--text);
line-height: 1.8;
}
.info-list li {
margin-bottom: 4px;
}
.info-list li:last-child {
margin-bottom: 0;
}
.info-list strong {
color: var(--text-strong);
font-weight: 600;
}
/* 动画 */
.modal-enter-active {
transition: all .2s ease;
}
.modal-leave-active {
transition: all .2s ease;
}
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal {
transform: scale(.95);
opacity: 0;
}
.modal-leave-to .modal {
transform: scale(.95);
opacity: 0;
}
</style>