feat(前端): 实现Seed抓取的异步交互与提示功能

- 将链接按钮改为带加载状态的交互按钮
- 添加异步请求逻辑,包含防抖和加载指示
- 实现三种状态的Toast提示(成功/无新增/错误)
- 添加相关CSS样式和交互逻辑
This commit is contained in:
2025-12-14 16:51:46 +08:00
parent a06c07470d
commit 5bf2c1e80d
4 changed files with 145 additions and 2 deletions

View File

@@ -180,6 +180,22 @@
- 生效方式:修改 `.env` 后重启服务
- 相关代码位置:`server.js:19`(环境变量读取)、`server.js:419`(远程/本地加载逻辑)
## 前端交互更新Seed抓取
- 禁止跳转到 `/api/seed`,前端通过 `fetch` 异步触发 Seed 抓取
- 成功无新增:黄色提示 `已执行Seed抓取无新增数据`
- 成功有新增:绿色提示 `已执行Seed抓取发现 ${新增数量} 条新数据`
- 错误:红色提示 `请求失败,请稍后重试`
- Toast右下角非模态自动隐藏 3 秒;颜色由代码控制
- 加载:请求超过 500ms 时显示按钮内微型旋转指示,结束后自动隐藏
- 防抖:点击 300ms 防抖,避免重复请求与误触
- 代码位置:
- 触发按钮:`public/index.html:46``id="seedTrigger"`
- 异步调用与提示:`public/app.js:275``runSeed`)、`public/app.js:97``toastBR`
- 加载指示逻辑:`public/app.js:258``showSeedSpinner`500ms 延迟显示)
- 计算新增数量:`public/app.js:267``calcNewCount`
- 样式:`public/styles.css:614``.toast-br`)、`public/styles.css:636``.btn-spinner`)、`public/styles.css:648``.btn.is-loading`
## 常见问题FAQ
- 修改 `seed.txt` 是否需要重启?不需要,`GET /api/seed` 会重新读取。

View File

@@ -14,6 +14,7 @@
const elBackTop = document.getElementById('backTop')
const elSearchHint = document.getElementById('searchHint')
const listContainer = document.querySelector('.list-container')
const elSeedTrigger = document.getElementById('seedTrigger')
// Mobile Sidebar Logic
const toggleSidebar = document.getElementById('toggleSidebar')
@@ -86,6 +87,32 @@
toastTimer = setTimeout(() => t.classList.remove('show'), 1800)
}
let toastBrTimer
/**
* 底部右侧Toast提示非模态
* @param {'success'|'info'|'error'} type 提示类型
* @param {string} msg 提示文本
* @param {number} duration 显示时长毫秒
*/
const toastBR = (type, msg, duration = 3000) => {
let t = document.querySelector('.toast-br')
if (!t) {
t = document.createElement('div')
t.className = 'toast-br'
document.body.appendChild(t)
}
t.textContent = msg
const map = {
success: 'rgba(52,199,89,0.9)', // 绿色
info: 'rgba(255,204,0,0.9)', // 黄色(无变化)
error: 'rgba(255,59,48,0.9)' // 红色
}
t.style.background = map[type] || map.info
t.classList.add('show')
clearTimeout(toastBrTimer)
toastBrTimer = setTimeout(() => t.classList.remove('show'), duration)
}
/**
* 渲染缓存条目
* @param {Array<{type:string,url:string,size:number,mtime:number}>} items 列表
@@ -220,6 +247,65 @@
load(true)
})
// Seed触发异步调用并提示新增数量
let seedBusy = false
let seedDebounceTimer
let seedSpinnerTimer
/**
* 显示/隐藏Seed按钮上的加载指示器
* @param {boolean} show 是否显示
*/
const showSeedSpinner = show => {
if (!elSeedTrigger) return
elSeedTrigger.classList.toggle('is-loading', !!show)
}
/**
* 计算新增条目数量
* @param {{results:Array<{skipped:boolean,error?:string}>}} data 接口返回数据
* @returns {number}
*/
const calcNewCount = data => {
const arr = Array.isArray(data?.results) ? data.results : []
return arr.filter(x => !x.skipped && !x.error).length
}
/**
* 触发Seed抓取带防抖、加载延时与错误处理
* @returns {Promise<void>}
*/
const runSeed = async () => {
if (seedBusy) return
seedBusy = true
if (elSeedTrigger) elSeedTrigger.disabled = true
// 超过500ms才显示加载指示
seedSpinnerTimer = setTimeout(() => showSeedSpinner(true), 500)
try {
const resp = await fetch('/api/seed', { method: 'GET' })
const data = await resp.json()
const newCount = calcNewCount(data)
if (newCount > 0) {
toastBR('success', `已执行Seed抓取发现 ${newCount} 条新数据`)
} else {
toastBR('info', '已执行Seed抓取无新增数据')
}
// 刷新列表
load(true)
} catch (e) {
toastBR('error', '请求失败,请稍后重试')
} finally {
clearTimeout(seedSpinnerTimer)
showSeedSpinner(false)
seedBusy = false
if (elSeedTrigger) elSeedTrigger.disabled = false
}
}
if (elSeedTrigger) {
elSeedTrigger.addEventListener('click', () => {
if (seedBusy) return
clearTimeout(seedDebounceTimer)
seedDebounceTimer = setTimeout(runSeed, 300)
})
}
// 虚拟滚动/无限加载
const io = new IntersectionObserver(entries => {
entries.forEach(e => {

View File

@@ -43,9 +43,10 @@
<section class="sidebar-section">
<h2>控制台</h2>
<div class="action-group">
<a class="btn primary full-width" href="/api/seed" target="_blank">
<button id="seedTrigger" class="btn primary full-width" type="button">
<span class="icon"></span> 触发 Seed 抓取
</a>
<span class="btn-spinner" aria-hidden="true"></span>
</button>
</div>
</section>

View File

@@ -609,3 +609,43 @@ html, body {
padding: 12px 16px;
}
}
/* Toast (Bottom Right, Non-modal) */
.toast-br {
position: fixed;
bottom: 24px;
right: 24px;
transform: translateY(0);
background: rgba(0,0,0,0.8);
color: #fff;
padding: 10px 16px;
border-radius: 20px;
font-size: 13px;
box-shadow: var(--shadow-md);
opacity: 0;
transition: all 0.3s ease;
pointer-events: none;
z-index: 2000;
backdrop-filter: blur(10px);
}
.toast-br.show {
opacity: 1;
}
/* Button inner loading spinner (shown when .btn has .is-loading) */
.btn-spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: none;
}
.btn.is-loading .btn-spinner {
display: inline-block;
}
.btn.is-loading {
opacity: 0.9;
cursor: not-allowed;
}