Compare commits

...
3 Commits
Author SHA1 Message Date
kevin 52c1b9de99 展开关闭标签特别卡的问题 2026-04-12 02:57:38 +08:00
kevin 5777561d9f 修复前端报错 2026-04-12 00:42:23 +08:00
kevin ded160083f 优化分页 2026-04-12 00:02:20 +08:00
3 changed files with 63 additions and 74 deletions
+2 -1
View File
@@ -111,8 +111,9 @@ export async function fetchUrlKeywordsStats() {
return data return data
} }
export async function fetchUrlKeywordsList() { export async function fetchUrlKeywordsList({ page = 1, page_size = 50 } = {}) {
const { data } = await axios.get(`${BASE}/admin/url/keywords/list`, { const { data } = await axios.get(`${BASE}/admin/url/keywords/list`, {
params: { page, page_size },
timeout: 10000, timeout: 10000,
}) })
return data return data
+2
View File
@@ -21,6 +21,8 @@ const batchAdding = ref(false)
// 爬取状态 // 爬取状态
const crawlStatus = ref(null) // { current_epoch, max_epoch, queue_length, completed_count, visited_total, is_running } const crawlStatus = ref(null) // { current_epoch, max_epoch, queue_length, completed_count, visited_total, is_running }
const backlinkStatus = ref(null)
const backlinkTriggering = ref(false)
onMounted(async () => { onMounted(async () => {
await Promise.all([loadStats(), loadWorkers(), loadBacklink(), loadPriorityStatus(), loadCrawlStatus()]) await Promise.all([loadStats(), loadWorkers(), loadBacklink(), loadPriorityStatus(), loadCrawlStatus()])
+59 -73
View File
@@ -4,11 +4,13 @@ import { fetchUrlKeywordsList, fetchUrlKeywords, fetchUrlKeywordsStats } from '.
const loading = ref(true) const loading = ref(true)
const error = ref(null) const error = ref(null)
const stats = ref({ size: 0, max_size: 10000, items: [] }) const stats = ref({ size: 0, max_size: 10000 })
const expandedUrls = ref(new Set()) const items = ref([]) // 当前页数据
const expandedUrls = ref({}) // 使用普通对象存储展开状态,避免 Set 的响应式开销
const urlKeywords = ref({}) const urlKeywords = ref({})
const loadingKeywords = ref(new Set()) const loadingKeywords = ref(new Set())
const search = ref('') const search = ref('')
const total = ref(0)
// 分页 // 分页
const currentPage = ref(1) const currentPage = ref(1)
@@ -18,7 +20,6 @@ let statsInterval = null
onMounted(async () => { onMounted(async () => {
await load() await load()
// 容量卡片每 5 秒自动刷新
statsInterval = setInterval(loadStats, 5000) statsInterval = setInterval(loadStats, 5000)
}) })
@@ -26,36 +27,38 @@ onUnmounted(() => {
if (statsInterval) clearInterval(statsInterval) if (statsInterval) clearInterval(statsInterval)
}) })
// 只刷新 stats(容量数据),不刷新列表
async function loadStats() { async function loadStats() {
try { try {
const s = await fetchUrlKeywordsStats() const s = await fetchUrlKeywordsStats()
stats.value.size = s.size || 0 stats.value.size = s.size || 0
stats.value.max_size = s.max_size || 10000 stats.value.max_size = s.max_size || 10000
} catch { } catch {
// 静默失败,不影响用户体验 // 静默
} }
} }
async function load() { async function load() {
loading.value = true loading.value = true
error.value = null error.value = null
currentPage.value = 1 // 重置到第一页
try { try {
const data = await fetchUrlKeywordsList() const data = await fetchUrlKeywordsList({ page: currentPage.value, page_size: pageSize.value })
stats.value.size = data.size || 0 stats.value.size = data.size || 0
stats.value.max_size = data.max_size || 10000 stats.value.max_size = data.max_size || 10000
// items 数组:{ url, title, snippet, keywords },反转显示(最新在前) total.value = data.total || 0
stats.value.items = (data.items || []).reverse() items.value = data.items || []
} catch (e) {
try { // 默认全部展开
const s = await fetchUrlKeywordsStats() const expanded = {}
stats.value.size = s.size || 0 items.value.forEach(item => expanded[item.url] = true)
stats.value.max_size = s.max_size || 10000 expandedUrls.value = expanded
stats.value.items = [] // 预加载当前页关键词
} catch { items.value.forEach(item => {
error.value = '无法加载缓存数据' if (item.keywords?.length) {
urlKeywords.value[item.url] = item.keywords
} }
})
} catch (e) {
error.value = '无法加载缓存数据'
console.error(e) console.error(e)
} finally { } finally {
loading.value = false loading.value = false
@@ -67,61 +70,53 @@ const usage = computed(() => {
return stats.value.size / stats.value.max_size return stats.value.size / stats.value.max_size
}) })
const filteredItems = computed(() => { const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
if (!search.value) return stats.value.items
const q = search.value.toLowerCase()
return stats.value.items.filter(item =>
item.url.toLowerCase().includes(q) ||
(item.title && item.title.toLowerCase().includes(q)) ||
(item.snippet && item.snippet.toLowerCase().includes(q))
)
})
const totalPages = computed(() => {
return Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value))
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredItems.value.slice(start, end)
})
function prevPage() { function prevPage() {
if (currentPage.value > 1) currentPage.value-- if (currentPage.value > 1) {
currentPage.value--
load()
}
} }
function nextPage() { function nextPage() {
if (currentPage.value < totalPages.value) currentPage.value++ if (currentPage.value < totalPages.value) {
currentPage.value++
load()
}
} }
function goToPage(p) { function goToPage(p) {
if (p >= 1 && p <= totalPages.value) currentPage.value = p if (p >= 1 && p <= totalPages.value && p !== currentPage.value) {
currentPage.value = p
load()
}
} }
// 搜索时回到第一页
function onSearch() { function onSearch() {
currentPage.value = 1 currentPage.value = 1
load()
} }
async function toggleKeywords(url) { async function toggleKeywords(url) {
if (expandedUrls.value.has(url)) { if (expandedUrls.value[url]) {
expandedUrls.value.delete(url) // 关闭:删除展开状态
const newExpanded = { ...expandedUrls.value }
delete newExpanded[url]
expandedUrls.value = newExpanded
return return
} }
expandedUrls.value.add(url) // 展开
expandedUrls.value = { ...expandedUrls.value, [url]: true }
if (urlKeywords.value[url]) { if (urlKeywords.value[url]) return
return
}
loadingKeywords.value.add(url) loadingKeywords.value.add(url)
try { try {
const data = await fetchUrlKeywords(url) const data = await fetchUrlKeywords(url)
urlKeywords.value[url] = data.keywords || [] urlKeywords.value[url] = data.keywords || []
} catch (e) { } catch (e) {
console.error('Failed to load keywords:', e)
urlKeywords.value[url] = [] urlKeywords.value[url] = []
} finally { } finally {
loadingKeywords.value.delete(url) loadingKeywords.value.delete(url)
@@ -214,7 +209,7 @@ function truncateSnippet(text, maxLen = 200) {
</div> </div>
<!-- Empty --> <!-- Empty -->
<div v-else-if="!stats.items.length && !search" class="bg-gray-900 border border-gray-800 rounded-xl p-12 text-center"> <div v-else-if="!items.length" class="bg-gray-900 border border-gray-800 rounded-xl p-12 text-center">
<div class="text-4xl mb-3">📭</div> <div class="text-4xl mb-3">📭</div>
<div class="text-gray-400">缓存为空</div> <div class="text-gray-400">缓存为空</div>
<div class="text-xs text-gray-600 mt-1">爬取页面时会自动填充此缓存</div> <div class="text-xs text-gray-600 mt-1">爬取页面时会自动填充此缓存</div>
@@ -224,29 +219,26 @@ function truncateSnippet(text, maxLen = 200) {
<div v-else class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden"> <div v-else class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
<div class="divide-y divide-gray-800"> <div class="divide-y divide-gray-800">
<div <div
v-for="item in paginatedItems" v-for="item in items"
:key="item.url" :key="item.url"
class="p-4 hover:bg-gray-800/40 transition-colors" class="p-4 hover:bg-gray-800/40 transition-colors cursor-pointer"
@click="toggleKeywords(item.url)"
> >
<!-- URL 标题行 --> <!-- URL 标题行 -->
<div class="flex items-start justify-between gap-3 mb-1"> <div class="flex items-start gap-2 mb-1">
<span class="shrink-0 text-gray-600 mt-0.5 text-xs select-none">
{{ expandedUrls[item.url] ? '▼' : '▶' }}
</span>
<a <a
:href="item.url" :href="item.url"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-sm text-blue-400 hover:text-blue-300 break-all line-clamp-2 flex-1" class="text-sm text-blue-400 hover:text-blue-300 break-all line-clamp-2"
:title="item.url" :title="item.url"
@click.stop
> >
{{ truncateUrl(item.url) }} {{ truncateUrl(item.url) }}
</a> </a>
<button
@click="toggleKeywords(item.url)"
class="shrink-0 text-xs text-gray-500 hover:text-blue-400 flex items-center gap-1"
>
<span>{{ expandedUrls.has(item.url) ? '▲' : '▼' }}</span>
<span v-if="loadingKeywords.has(item.url)">加载中</span>
<span v-else-if="urlKeywords[item.url] || item.keywords?.length">已缓存</span>
</button>
</div> </div>
<!-- 标题 --> <!-- 标题 -->
@@ -260,11 +252,11 @@ function truncateSnippet(text, maxLen = 200) {
</div> </div>
<!-- 关键词 --> <!-- 关键词 -->
<div v-if="expandedUrls.has(item.url)" class="mt-2"> <div v-if="expandedUrls[item.url]" class="mt-2">
<template v-if="urlKeywords[item.url]?.length || item.keywords?.length"> <template v-if="urlKeywords[item.url]?.length">
<div class="flex flex-wrap gap-1.5"> <div class="flex flex-wrap gap-1.5">
<span <span
v-for="kw in (urlKeywords[item.url] || item.keywords || [])" v-for="kw in urlKeywords[item.url]"
:key="kw.word" :key="kw.word"
class="text-xs px-2 py-0.5 rounded bg-gray-800 text-gray-300 border border-gray-700" class="text-xs px-2 py-0.5 rounded bg-gray-800 text-gray-300 border border-gray-700"
:title="`权重: ${kw.weight.toFixed(4)}`" :title="`权重: ${kw.weight.toFixed(4)}`"
@@ -274,7 +266,7 @@ function truncateSnippet(text, maxLen = 200) {
</span> </span>
</div> </div>
<div class="text-xs text-gray-600 mt-2"> <div class="text-xs text-gray-600 mt-2">
{{ (urlKeywords[item.url] || item.keywords || []).length }} 个关键词 {{ urlKeywords[item.url].length }} 个关键词
</div> </div>
</template> </template>
<span v-else-if="!loadingKeywords.has(item.url)" class="text-xs text-gray-600"> <span v-else-if="!loadingKeywords.has(item.url)" class="text-xs text-gray-600">
@@ -285,17 +277,13 @@ function truncateSnippet(text, maxLen = 200) {
</span> </span>
</div> </div>
</div> </div>
<div v-if="!paginatedItems.length" class="p-8 text-center text-gray-600">
没有找到匹配的记录
</div>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="px-4 py-3 border-t border-gray-800 flex items-center justify-between"> <div class="px-4 py-3 border-t border-gray-800 flex items-center justify-between">
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
{{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, filteredItems.length) }} / {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, total) }} /
{{ filteredItems.length }} {{ total.toLocaleString() }}
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button <button
@@ -317,9 +305,7 @@ function truncateSnippet(text, maxLen = 200) {
@click="goToPage(p)" @click="goToPage(p)"
:class="[ :class="[
'px-2 py-1 text-xs rounded', 'px-2 py-1 text-xs rounded',
p === currentPage p === currentPage ? 'bg-blue-600 text-white' : 'bg-gray-800 text-gray-400 hover:bg-gray-700'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
]" ]"
> >
{{ p }} {{ p }}