Compare commits
1
Commits
d03644e69f
...
422033fc6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422033fc6d |
@@ -96,3 +96,11 @@ export async function fetchCrawlStatus() {
|
|||||||
return data
|
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>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
import { fetchRecent } from '../api.js'
|
import { fetchRecent, fetchUrlKeywords } from '../api.js'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -13,6 +13,11 @@ let refreshInterval = null
|
|||||||
const limits = [20, 50, 100, 200]
|
const limits = [20, 50, 100, 200]
|
||||||
const limit = ref(50)
|
const limit = ref(50)
|
||||||
|
|
||||||
|
// 关键词展开状态和缓存
|
||||||
|
const expandedUrls = ref(new Set())
|
||||||
|
const urlKeywords = ref({})
|
||||||
|
const loadingKeywords = ref(new Set())
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load()
|
await load()
|
||||||
})
|
})
|
||||||
@@ -81,6 +86,30 @@ function topLang(language) {
|
|||||||
const sorted = Object.entries(language).sort((a, b) => b[1] - a[1])
|
const sorted = Object.entries(language).sort((a, b) => b[1] - a[1])
|
||||||
return sorted[0]
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<div class="text-xs text-gray-600 mt-0.5 break-all line-clamp-1">{{ item.url }}</div>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="item.description" class="text-xs text-gray-500 mt-1 line-clamp-1">{{ item.description }}</div>
|
<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>
|
||||||
<td class="px-5 py-3.5">
|
<td class="px-5 py-3.5">
|
||||||
<span class="text-gray-400 text-xs font-mono">{{ item.domain }}</span>
|
<span class="text-gray-400 text-xs font-mono">{{ item.domain }}</span>
|
||||||
@@ -202,17 +261,16 @@ function topLang(language) {
|
|||||||
|
|
||||||
<!-- Mobile Cards -->
|
<!-- Mobile Cards -->
|
||||||
<div class="md:hidden divide-y divide-gray-800">
|
<div class="md:hidden divide-y divide-gray-800">
|
||||||
<a
|
<div
|
||||||
v-for="item in filtered"
|
v-for="item in filtered"
|
||||||
:key="item.url"
|
:key="item.url"
|
||||||
:href="item.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="block p-4 hover:bg-gray-800/40 transition-colors"
|
class="block p-4 hover:bg-gray-800/40 transition-colors"
|
||||||
>
|
>
|
||||||
|
<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="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="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>
|
||||||
|
<div class="flex items-center gap-2 text-xs mb-2">
|
||||||
<span class="text-gray-400 font-mono">{{ item.domain }}</span>
|
<span class="text-gray-400 font-mono">{{ item.domain }}</span>
|
||||||
<template v-if="topLang(item.language)">
|
<template v-if="topLang(item.language)">
|
||||||
<span
|
<span
|
||||||
@@ -223,7 +281,36 @@ function topLang(language) {
|
|||||||
</template>
|
</template>
|
||||||
<span class="text-gray-600 ml-auto">{{ fmtTime(item.crawled_at) }}</span>
|
<span class="text-gray-600 ml-auto">{{ fmtTime(item.crawled_at) }}</span>
|
||||||
</div>
|
</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 v-if="!filtered.length" class="p-8 text-center text-gray-600">
|
||||||
没有找到匹配的记录
|
没有找到匹配的记录
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user