Compare commits
1
Commits
f7fa7faed1
...
ded160083f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded160083f |
+2
-1
@@ -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
|
||||||
|
|||||||
+48
-68
@@ -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 items = ref([]) // 当前页数据
|
||||||
const expandedUrls = ref(new Set())
|
const expandedUrls = ref(new 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,36 @@ 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 || []
|
||||||
|
|
||||||
|
// 默认全部展开
|
||||||
|
items.value.forEach(item => expandedUrls.value.add(item.url))
|
||||||
|
// 预加载当前页关键词
|
||||||
|
items.value.forEach(item => {
|
||||||
|
if (item.keywords?.length) {
|
||||||
|
urlKeywords.value[item.url] = item.keywords
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
error.value = '无法加载缓存数据'
|
||||||
const s = await fetchUrlKeywordsStats()
|
|
||||||
stats.value.size = s.size || 0
|
|
||||||
stats.value.max_size = s.max_size || 10000
|
|
||||||
stats.value.items = []
|
|
||||||
} catch {
|
|
||||||
error.value = '无法加载缓存数据'
|
|
||||||
}
|
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -67,41 +68,32 @@ 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) {
|
||||||
@@ -112,16 +104,13 @@ async function toggleKeywords(url) {
|
|||||||
|
|
||||||
expandedUrls.value.add(url)
|
expandedUrls.value.add(url)
|
||||||
|
|
||||||
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 +203,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 +213,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.has(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>
|
||||||
|
|
||||||
<!-- 标题 -->
|
<!-- 标题 -->
|
||||||
@@ -261,10 +247,10 @@ function truncateSnippet(text, maxLen = 200) {
|
|||||||
|
|
||||||
<!-- 关键词 -->
|
<!-- 关键词 -->
|
||||||
<div v-if="expandedUrls.has(item.url)" class="mt-2">
|
<div v-if="expandedUrls.has(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 +260,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 +271,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 +299,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 }}
|
||||||
|
|||||||
Reference in New Issue
Block a user