前端显示缓存信息
This commit is contained in:
@@ -3,12 +3,14 @@ import { ref } from 'vue'
|
|||||||
import Dashboard from './views/Dashboard.vue'
|
import Dashboard from './views/Dashboard.vue'
|
||||||
import RecentCrawls from './views/RecentCrawls.vue'
|
import RecentCrawls from './views/RecentCrawls.vue'
|
||||||
import SearchView from './views/SearchView.vue'
|
import SearchView from './views/SearchView.vue'
|
||||||
|
import KeywordsCache from './views/KeywordsCache.vue'
|
||||||
|
|
||||||
const tab = ref('dashboard')
|
const tab = ref('dashboard')
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ id: 'dashboard', label: '概览', icon: '📊' },
|
{ id: 'dashboard', label: '概览', icon: '📊' },
|
||||||
{ id: 'recent', label: '最近', icon: '🕷️' },
|
{ id: 'recent', label: '最近', icon: '🕷️' },
|
||||||
|
{ id: 'keywords', label: '缓存', icon: '💾' },
|
||||||
{ id: 'search', label: '搜索', icon: '🔍' },
|
{ id: 'search', label: '搜索', icon: '🔍' },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
@@ -46,6 +48,7 @@ const nav = [
|
|||||||
<main class="flex-1 overflow-y-auto pb-16 md:pb-0">
|
<main class="flex-1 overflow-y-auto pb-16 md:pb-0">
|
||||||
<Dashboard v-if="tab === 'dashboard'" />
|
<Dashboard v-if="tab === 'dashboard'" />
|
||||||
<RecentCrawls v-else-if="tab === 'recent'" />
|
<RecentCrawls v-else-if="tab === 'recent'" />
|
||||||
|
<KeywordsCache v-else-if="tab === 'keywords'" />
|
||||||
<SearchView v-else-if="tab === 'search'" />
|
<SearchView v-else-if="tab === 'search'" />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -111,3 +111,12 @@ export async function fetchUrlKeywordsStats() {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchUrlKeywordsList() {
|
||||||
|
const { data } = await axios.get(`${BASE}/admin/url/keywords/list`, {
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import { fetchUrlKeywordsList, fetchUrlKeywords, fetchUrlKeywordsStats } from '../api.js'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref(null)
|
||||||
|
const stats = ref({ size: 0, max_size: 10000, items: [] })
|
||||||
|
const expandedUrls = ref(new Set())
|
||||||
|
const urlKeywords = ref({})
|
||||||
|
const loadingKeywords = ref(new Set())
|
||||||
|
const search = ref('')
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(50)
|
||||||
|
|
||||||
|
let statsInterval = null
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await load()
|
||||||
|
// 容量卡片每 5 秒自动刷新
|
||||||
|
statsInterval = setInterval(loadStats, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (statsInterval) clearInterval(statsInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 只刷新 stats(容量数据),不刷新列表
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const s = await fetchUrlKeywordsStats()
|
||||||
|
stats.value.size = s.size || 0
|
||||||
|
stats.value.max_size = s.max_size || 10000
|
||||||
|
} catch {
|
||||||
|
// 静默失败,不影响用户体验
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
currentPage.value = 1 // 重置到第一页
|
||||||
|
try {
|
||||||
|
const data = await fetchUrlKeywordsList()
|
||||||
|
stats.value.size = data.size || 0
|
||||||
|
stats.value.max_size = data.max_size || 10000
|
||||||
|
// items 数组:{ url, title, snippet, keywords },反转显示(最新在前)
|
||||||
|
stats.value.items = (data.items || []).reverse()
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
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)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = computed(() => {
|
||||||
|
if (!stats.value.max_size) return 0
|
||||||
|
return stats.value.size / stats.value.max_size
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
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() {
|
||||||
|
if (currentPage.value > 1) currentPage.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if (currentPage.value < totalPages.value) currentPage.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPage(p) {
|
||||||
|
if (p >= 1 && p <= totalPages.value) currentPage.value = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索时回到第一页
|
||||||
|
function onSearch() {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateUrl(url, maxLen = 80) {
|
||||||
|
if (url.length <= maxLen) return url
|
||||||
|
return url.slice(0, maxLen) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateSnippet(text, maxLen = 200) {
|
||||||
|
if (!text) return ''
|
||||||
|
if (text.length <= maxLen) return text
|
||||||
|
return text.slice(0, maxLen) + '...'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4 md:p-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-4 md:mb-6 gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl md:text-2xl font-semibold text-white mb-1">关键词缓存</h1>
|
||||||
|
<p class="text-sm text-gray-500">LRU 缓存,共 {{ stats.size.toLocaleString() }} 条记录</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- 缓存状态卡片(自动刷新) -->
|
||||||
|
<div class="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 flex items-center gap-3">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[10px] text-gray-500 uppercase">容量</span>
|
||||||
|
<span class="text-sm font-medium text-gray-300">
|
||||||
|
{{ stats.size.toLocaleString() }} / {{ stats.max_size.toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 relative">
|
||||||
|
<svg viewBox="0 0 36 36" class="w-full h-full transform -rotate-90">
|
||||||
|
<path
|
||||||
|
class="text-gray-800"
|
||||||
|
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
:class="usage > 0.9 ? 'text-red-500' : usage > 0.7 ? 'text-yellow-500' : 'text-green-500'"
|
||||||
|
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
:stroke-dasharray="`${(usage * 100).toFixed(0)}, 100`"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="absolute inset-0 flex items-center justify-center text-[8px] font-medium text-gray-400">
|
||||||
|
{{ (usage * 100).toFixed(0) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="load"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="mb-4 md:mb-5">
|
||||||
|
<div class="relative max-w-sm">
|
||||||
|
<input
|
||||||
|
v-model="search"
|
||||||
|
@input="onSearch"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索 URL、标题或摘要..."
|
||||||
|
class="w-full bg-gray-900 border border-gray-700 text-gray-200 text-sm rounded-lg pl-10 pr-4 py-2 focus:border-blue-500 focus:outline-none placeholder-gray-600"
|
||||||
|
/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">🔍</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center h-48">
|
||||||
|
<div class="text-gray-400 animate-pulse">加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div v-else-if="error" class="bg-red-900/30 border border-red-800 rounded-lg p-4 text-red-300">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty -->
|
||||||
|
<div v-else-if="!stats.items.length && !search" class="bg-gray-900 border border-gray-800 rounded-xl p-12 text-center">
|
||||||
|
<div class="text-4xl mb-3">📭</div>
|
||||||
|
<div class="text-gray-400">缓存为空</div>
|
||||||
|
<div class="text-xs text-gray-600 mt-1">爬取页面时会自动填充此缓存</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL List with Pagination -->
|
||||||
|
<div v-else class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
|
||||||
|
<div class="divide-y divide-gray-800">
|
||||||
|
<div
|
||||||
|
v-for="item in paginatedItems"
|
||||||
|
:key="item.url"
|
||||||
|
class="p-4 hover:bg-gray-800/40 transition-colors"
|
||||||
|
>
|
||||||
|
<!-- URL 标题行 -->
|
||||||
|
<div class="flex items-start justify-between gap-3 mb-1">
|
||||||
|
<a
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm text-blue-400 hover:text-blue-300 break-all line-clamp-2 flex-1"
|
||||||
|
:title="item.url"
|
||||||
|
>
|
||||||
|
{{ truncateUrl(item.url) }}
|
||||||
|
</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 v-if="item.title" class="text-sm text-gray-300 font-medium mb-1">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 摘要 -->
|
||||||
|
<div v-if="item.snippet" class="text-xs text-gray-500 leading-relaxed mb-2">
|
||||||
|
{{ truncateSnippet(item.snippet) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关键词 -->
|
||||||
|
<div v-if="expandedUrls.has(item.url)" class="mt-2">
|
||||||
|
<template v-if="urlKeywords[item.url]?.length || item.keywords?.length">
|
||||||
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
<span
|
||||||
|
v-for="kw in (urlKeywords[item.url] || item.keywords || [])"
|
||||||
|
: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-2">
|
||||||
|
共 {{ (urlKeywords[item.url] || item.keywords || []).length }} 个关键词
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else-if="!loadingKeywords.has(item.url)" class="text-xs text-gray-600">
|
||||||
|
暂无关键词
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-xs text-gray-500">
|
||||||
|
加载中...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!paginatedItems.length" class="p-8 text-center text-gray-600">
|
||||||
|
没有找到匹配的记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="px-4 py-3 border-t border-gray-800 flex items-center justify-between">
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
第 {{ (currentPage - 1) * pageSize + 1 }}-{{ Math.min(currentPage * pageSize, filteredItems.length) }} 条 /
|
||||||
|
共 {{ filteredItems.length }} 条
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
@click="prevPage"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="p in totalPages <= 5 ? totalPages : (() => {
|
||||||
|
const pages = []
|
||||||
|
const start = Math.max(1, currentPage - 2)
|
||||||
|
const end = Math.min(totalPages, start + 4)
|
||||||
|
for (let i = start; i <= end; i++) pages.push(i)
|
||||||
|
return pages
|
||||||
|
})()"
|
||||||
|
:key="p"
|
||||||
|
@click="goToPage(p)"
|
||||||
|
:class="[
|
||||||
|
'px-2 py-1 text-xs rounded',
|
||||||
|
p === currentPage
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ p }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 5 && currentPage < totalPages - 2"
|
||||||
|
class="px-1 py-1 text-xs text-gray-600"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 5 && currentPage < totalPages - 2"
|
||||||
|
@click="goToPage(totalPages)"
|
||||||
|
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ totalPages }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="nextPage"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
class="px-2 py-1 text-xs rounded bg-gray-800 text-gray-400 hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
import { fetchRecent, fetchUrlKeywords, fetchUrlKeywordsStats } from '../api.js'
|
import { fetchRecent, fetchUrlKeywords } from '../api.js'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -18,9 +18,6 @@ const expandedUrls = ref(new Set())
|
|||||||
const urlKeywords = ref({})
|
const urlKeywords = ref({})
|
||||||
const loadingKeywords = ref(new Set())
|
const loadingKeywords = ref(new Set())
|
||||||
|
|
||||||
// 关键词缓存统计
|
|
||||||
const keywordsStats = ref({ size: 0, max_size: 10000, usage: 0 })
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await load()
|
await load()
|
||||||
})
|
})
|
||||||
@@ -29,13 +26,9 @@ async function load() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const [data, stats] = await Promise.all([
|
const data = await fetchRecent(limit.value)
|
||||||
fetchRecent(limit.value),
|
|
||||||
fetchUrlKeywordsStats().catch(() => ({ size: 0, max_size: 10000, usage: 0 }))
|
|
||||||
])
|
|
||||||
items.value = data.items || []
|
items.value = data.items || []
|
||||||
total.value = data.total || 0
|
total.value = data.total || 0
|
||||||
keywordsStats.value = stats
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = '无法加载数据,可能人服务器未启动'
|
error.value = '无法加载数据,可能人服务器未启动'
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -128,37 +121,6 @@ async function toggleKeywords(url) {
|
|||||||
<p class="text-sm text-gray-500">共 {{ total.toLocaleString() }} 条记录</p>
|
<p class="text-sm text-gray-500">共 {{ total.toLocaleString() }} 条记录</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 md:gap-3">
|
<div class="flex items-center gap-2 md:gap-3">
|
||||||
<!-- 关键词缓存统计卡片 -->
|
|
||||||
<div class="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 flex items-center gap-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-[10px] text-gray-500 uppercase">关键词缓存</span>
|
|
||||||
<span class="text-sm font-medium text-gray-300">
|
|
||||||
{{ keywordsStats.size.toLocaleString() }} / {{ keywordsStats.max_size.toLocaleString() }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-8 relative">
|
|
||||||
<svg viewBox="0 0 36 36" class="w-full h-full transform -rotate-90">
|
|
||||||
<path
|
|
||||||
class="text-gray-800"
|
|
||||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
:class="keywordsStats.usage > 0.9 ? 'text-red-500' : keywordsStats.usage > 0.7 ? 'text-yellow-500' : 'text-green-500'"
|
|
||||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
:stroke-dasharray="`${(keywordsStats.usage * 100).toFixed(0)}, 100`"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="absolute inset-0 flex items-center justify-center text-[8px] font-medium text-gray-400">
|
|
||||||
{{ (keywordsStats.usage * 100).toFixed(0) }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<select
|
<select
|
||||||
v-model="limit"
|
v-model="limit"
|
||||||
@change="changeLimit(limit)"
|
@change="changeLimit(limit)"
|
||||||
|
|||||||
Reference in New Issue
Block a user