Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422033fc6d |
@@ -96,3 +96,11 @@ export async function fetchCrawlStatus() {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchUrlKeywords(url) {
|
||||
const { data } = await axios.get(`${BASE}/admin/url/keywords`, {
|
||||
params: { url },
|
||||
timeout: 5000,
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { fetchRecent } from '../api.js'
|
||||
import { fetchRecent, fetchUrlKeywords } from '../api.js'
|
||||
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
@@ -13,6 +13,11 @@ let refreshInterval = null
|
||||
const limits = [20, 50, 100, 200]
|
||||
const limit = ref(50)
|
||||
|
||||
// 关键词展开状态和缓存
|
||||
const expandedUrls = ref(new Set())
|
||||
const urlKeywords = ref({})
|
||||
const loadingKeywords = ref(new Set())
|
||||
|
||||
onMounted(async () => {
|
||||
await load()
|
||||
})
|
||||
@@ -81,6 +86,30 @@ function topLang(language) {
|
||||
const sorted = Object.entries(language).sort((a, b) => b[1] - a[1])
|
||||
return sorted[0]
|
||||
}
|
||||
|
||||
async function toggleKeywords(url) {
|
||||
if (expandedUrls.value.has(url)) {
|
||||
expandedUrls.value.delete(url)
|
||||
return
|
||||
}
|
||||
|
||||
expandedUrls.value.add(url)
|
||||
|
||||
if (urlKeywords.value[url]) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingKeywords.value.add(url)
|
||||
try {
|
||||
const data = await fetchUrlKeywords(url)
|
||||
urlKeywords.value[url] = data.keywords || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load keywords:', e)
|
||||
urlKeywords.value[url] = []
|
||||
} finally {
|
||||
loadingKeywords.value.delete(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -170,6 +199,36 @@ function topLang(language) {
|
||||
<div class="text-xs text-gray-600 mt-0.5 break-all line-clamp-1">{{ item.url }}</div>
|
||||
</a>
|
||||
<div v-if="item.description" class="text-xs text-gray-500 mt-1 line-clamp-1">{{ item.description }}</div>
|
||||
<!-- 关键词展开/折叠 -->
|
||||
<button
|
||||
@click.prevent="toggleKeywords(item.url)"
|
||||
class="mt-2 text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1"
|
||||
>
|
||||
<span>{{ expandedUrls.has(item.url) ? '▼' : '▶' }} 关键词</span>
|
||||
<span v-if="loadingKeywords.has(item.url)" class="text-gray-500">加载中...</span>
|
||||
</button>
|
||||
<!-- 关键词列表 -->
|
||||
<div v-if="expandedUrls.has(item.url)" class="mt-2">
|
||||
<template v-if="urlKeywords[item.url]?.length">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="kw in urlKeywords[item.url]"
|
||||
:key="kw.word"
|
||||
class="text-xs px-2 py-0.5 rounded bg-gray-800 text-gray-300 border border-gray-700"
|
||||
:title="`权重: ${kw.weight.toFixed(4)}`"
|
||||
>
|
||||
{{ kw.word }}
|
||||
<span class="text-gray-500 text-[10px] ml-0.5">{{ kw.weight.toFixed(2) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">
|
||||
共 {{ urlKeywords[item.url].length }} 个关键词
|
||||
</div>
|
||||
</template>
|
||||
<span v-else-if="!loadingKeywords.has(item.url)" class="text-xs text-gray-600">
|
||||
暂无关键词(服务重启后缓存已清空)
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-3.5">
|
||||
<span class="text-gray-400 text-xs font-mono">{{ item.domain }}</span>
|
||||
@@ -202,17 +261,16 @@ function topLang(language) {
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="md:hidden divide-y divide-gray-800">
|
||||
<a
|
||||
<div
|
||||
v-for="item in filtered"
|
||||
:key="item.url"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="block p-4 hover:bg-gray-800/40 transition-colors"
|
||||
>
|
||||
<div class="font-medium text-gray-200 text-sm line-clamp-2 mb-1">{{ item.title || '(无标题)' }}</div>
|
||||
<div class="text-xs text-gray-500 break-all line-clamp-1 mb-2">{{ item.url }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<a :href="item.url" target="_blank" rel="noopener noreferrer" class="block">
|
||||
<div class="font-medium text-gray-200 text-sm line-clamp-2 mb-1">{{ item.title || '(无标题)' }}</div>
|
||||
<div class="text-xs text-gray-500 break-all line-clamp-1 mb-2">{{ item.url }}</div>
|
||||
</a>
|
||||
<div class="flex items-center gap-2 text-xs mb-2">
|
||||
<span class="text-gray-400 font-mono">{{ item.domain }}</span>
|
||||
<template v-if="topLang(item.language)">
|
||||
<span
|
||||
@@ -223,7 +281,36 @@ function topLang(language) {
|
||||
</template>
|
||||
<span class="text-gray-600 ml-auto">{{ fmtTime(item.crawled_at) }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<!-- 移动端关键词展开 -->
|
||||
<button
|
||||
@click.prevent="toggleKeywords(item.url)"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 mb-2"
|
||||
>
|
||||
<span>{{ expandedUrls.has(item.url) ? '▼' : '▶' }} 关键词</span>
|
||||
<span v-if="loadingKeywords.has(item.url)" class="text-gray-500">加载中...</span>
|
||||
</button>
|
||||
<div v-if="expandedUrls.has(item.url)" class="mb-2">
|
||||
<template v-if="urlKeywords[item.url]?.length">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="kw in urlKeywords[item.url]"
|
||||
:key="kw.word"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded bg-gray-800 text-gray-300 border border-gray-700"
|
||||
:title="`权重: ${kw.weight.toFixed(4)}`"
|
||||
>
|
||||
{{ kw.word }}
|
||||
<span class="text-gray-500 text-[9px] ml-0.5">{{ kw.weight.toFixed(2) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-600 mt-1">
|
||||
共 {{ urlKeywords[item.url].length }} 个
|
||||
</div>
|
||||
</template>
|
||||
<span v-else-if="!loadingKeywords.has(item.url)" class="text-xs text-gray-600">
|
||||
暂无关键词
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!filtered.length" class="p-8 text-center text-gray-600">
|
||||
没有找到匹配的记录
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user