feat(前端): 实现Seed抓取的异步交互与提示功能
- 将链接按钮改为带加载状态的交互按钮 - 添加异步请求逻辑,包含防抖和加载指示 - 实现三种状态的Toast提示(成功/无新增/错误) - 添加相关CSS样式和交互逻辑
This commit is contained in:
16
README.md
16
README.md
@@ -180,6 +180,22 @@
|
|||||||
- 生效方式:修改 `.env` 后重启服务
|
- 生效方式:修改 `.env` 后重启服务
|
||||||
- 相关代码位置:`server.js:19`(环境变量读取)、`server.js:419`(远程/本地加载逻辑)
|
- 相关代码位置:`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)
|
## 常见问题(FAQ)
|
||||||
|
|
||||||
- 修改 `seed.txt` 是否需要重启?不需要,`GET /api/seed` 会重新读取。
|
- 修改 `seed.txt` 是否需要重启?不需要,`GET /api/seed` 会重新读取。
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
const elBackTop = document.getElementById('backTop')
|
const elBackTop = document.getElementById('backTop')
|
||||||
const elSearchHint = document.getElementById('searchHint')
|
const elSearchHint = document.getElementById('searchHint')
|
||||||
const listContainer = document.querySelector('.list-container')
|
const listContainer = document.querySelector('.list-container')
|
||||||
|
const elSeedTrigger = document.getElementById('seedTrigger')
|
||||||
|
|
||||||
// Mobile Sidebar Logic
|
// Mobile Sidebar Logic
|
||||||
const toggleSidebar = document.getElementById('toggleSidebar')
|
const toggleSidebar = document.getElementById('toggleSidebar')
|
||||||
@@ -86,6 +87,32 @@
|
|||||||
toastTimer = setTimeout(() => t.classList.remove('show'), 1800)
|
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 列表
|
* @param {Array<{type:string,url:string,size:number,mtime:number}>} items 列表
|
||||||
@@ -220,6 +247,65 @@
|
|||||||
load(true)
|
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 => {
|
const io = new IntersectionObserver(entries => {
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
|
|||||||
@@ -43,9 +43,10 @@
|
|||||||
<section class="sidebar-section">
|
<section class="sidebar-section">
|
||||||
<h2>控制台</h2>
|
<h2>控制台</h2>
|
||||||
<div class="action-group">
|
<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 抓取
|
<span class="icon">⚡</span> 触发 Seed 抓取
|
||||||
</a>
|
<span class="btn-spinner" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -609,3 +609,43 @@ html, body {
|
|||||||
padding: 12px 16px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user