This commit is contained in:
2026-04-10 00:18:21 +08:00
4 changed files with 204 additions and 25 deletions
+28
View File
@@ -47,3 +47,31 @@ export async function flushIndex() {
return data
}
export async function fetchWorkers() {
const { data } = await axios.get(`${BASE}/admin/workers`, {
timeout: 10000,
})
return data
}
export async function setWorkers(n) {
const { data } = await axios.post(`${BASE}/admin/workers`, { workers: n }, {
timeout: 10000,
})
return data
}
export async function fetchBacklink() {
const { data } = await axios.get(`${BASE}/admin/backlink`, {
timeout: 10000,
})
return data
}
export async function triggerBacklink() {
const { data } = await axios.post(`${BASE}/admin/backlink`, null, {
timeout: 10000,
})
return data
}
+169 -4
View File
@@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { fetchStats, flushIndex } from '../api.js'
import { fetchStats, flushIndex, fetchWorkers, setWorkers, fetchBacklink, triggerBacklink } from '../api.js'
const stats = ref(null)
const loading = ref(true)
@@ -8,10 +8,23 @@ const flushing = ref(false)
const error = ref(null)
let refreshInterval = null
// Workers 相关状态
const configuredWorkers = ref(0) // 设定线程数
const activeWorkers = ref(0) // 实际运行中的 goroutine 数
const workersInput = ref(0)
const workersSaving = ref(false)
// Backlink 相关状态
const backlinkStatus = ref(null) // { running, next_run, last_run?, last_error? }
const backlinkTriggering = ref(false)
onMounted(async () => {
await loadStats()
// 每5秒自动刷新
refreshInterval = setInterval(loadStats, 5000)
await Promise.all([loadStats(), loadWorkers(), loadBacklink()])
refreshInterval = setInterval(() => {
loadStats()
loadWorkers()
loadBacklink()
}, 5000)
})
onUnmounted(() => {
@@ -32,6 +45,80 @@ async function loadStats() {
}
}
async function loadWorkers() {
try {
const res = await fetchWorkers()
configuredWorkers.value = res.configured
activeWorkers.value = res.active
workersInput.value = res.configured
} catch (e) {
console.error('Failed to load workers:', e)
}
}
async function saveWorkers() {
const n = parseInt(workersInput.value, 10)
if (isNaN(n) || n < 1 || n > 500) {
error.value = '线程数必须在 1~500 之间'
return
}
workersSaving.value = true
try {
await setWorkers(n)
configuredWorkers.value = n
error.value = null
} catch (e) {
error.value = '修改线程数失败: ' + e.message
} finally {
workersSaving.value = false
}
}
async function loadBacklink() {
try {
backlinkStatus.value = await fetchBacklink()
} catch (e) {
console.error('Failed to load backlink status:', e)
}
}
async function doTriggerBacklink() {
backlinkTriggering.value = true
try {
await triggerBacklink()
// 立即刷新状态,显示 running=true
backlinkStatus.value = await fetchBacklink()
error.value = null
} catch (e) {
error.value = '触发反链计算失败: ' + e.message
} finally {
backlinkTriggering.value = false
}
}
function formatBacklinkTime(isoStr) {
if (!isoStr) return '--'
const d = new Date(isoStr)
const now = new Date()
const diffMs = d - now
const diffMin = Math.floor(diffMs / 60000)
const diffH = Math.floor(diffMin / 60)
const diffD = Math.floor(diffH / 24)
const time = d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
const date = d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
if (diffD > 0) return `${date} ${time} (${diffD}天后)`
if (diffH > 0) return `${date} ${time} (${diffH}小时后)`
if (diffMin > 0) return `${time} (${diffMin}分钟后)`
return `${time} (即将执行)`
}
function formatBacklinkLastRun(isoStr) {
if (!isoStr) return '从未执行'
const d = new Date(isoStr)
return d.toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
async function doFlush() {
flushing.value = true
try {
@@ -111,6 +198,84 @@ function langColor(lang) {
</div>
</div>
<!-- Workers Control -->
<div class="bg-gray-900 border border-gray-800 rounded-xl p-4 md:p-5 mb-6 md:mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h2 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2">爬虫线程数</h2>
<div class="flex items-baseline gap-4">
<div>
<div class="text-xs text-gray-500 mb-0.5">实际运行</div>
<div class="text-3xl font-bold" :class="activeWorkers > 0 ? 'text-green-400' : 'text-gray-500'">{{ activeWorkers }}</div>
</div>
<div class="text-gray-700 text-xl">/</div>
<div>
<div class="text-xs text-gray-500 mb-0.5">设定上限</div>
<div class="text-3xl font-bold text-white">{{ configuredWorkers }}</div>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button
class="w-9 h-9 flex items-center justify-center rounded-lg bg-gray-800 hover:bg-gray-700 text-gray-300 hover:text-white text-lg font-bold transition-colors cursor-pointer"
@click="workersInput = Math.max(1, workersInput - 1)"
:disabled="workersSaving"
></button>
<input
type="number"
v-model.number="workersInput"
min="1"
max="500"
class="w-20 h-9 text-center bg-gray-800 border border-gray-700 rounded-lg text-white text-sm focus:outline-none focus:border-blue-500 transition-colors [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
@keyup.enter="saveWorkers"
/>
<button
class="w-9 h-9 flex items-center justify-center rounded-lg bg-gray-800 hover:bg-gray-700 text-gray-300 hover:text-white text-lg font-bold transition-colors cursor-pointer"
@click="workersInput = Math.min(500, workersInput + 1)"
:disabled="workersSaving"
>+</button>
<button
class="h-9 px-4 bg-blue-700 hover:bg-blue-600 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-lg transition-colors cursor-pointer"
:disabled="workersSaving || workersInput === configuredWorkers"
@click="saveWorkers"
>
{{ workersSaving ? '保存中...' : '应用' }}
</button>
</div>
</div>
</div>
<!-- Backlink Control -->
<div v-if="backlinkStatus" class="bg-gray-900 border border-gray-800 rounded-xl p-4 md:p-5 mb-6 md:mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex-1">
<h2 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">反链计算PageRank</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div class="text-xs text-gray-500 mb-0.5">下次自动执行</div>
<div class="text-lg font-bold" :class="backlinkStatus.running ? 'text-yellow-400' : 'text-white'">
{{ backlinkStatus.running ? '计算中...' : formatBacklinkTime(backlinkStatus.next_run) }}
</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-0.5">上次完成</div>
<div class="text-lg font-bold text-white">{{ formatBacklinkLastRun(backlinkStatus.last_run) }}</div>
</div>
</div>
<div v-if="backlinkStatus.last_error" class="mt-2 text-xs text-red-400 truncate" :title="backlinkStatus.last_error">
上次错误{{ backlinkStatus.last_error }}
</div>
</div>
<button
class="h-9 px-4 bg-blue-700 hover:bg-blue-600 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-lg transition-colors cursor-pointer whitespace-nowrap"
:disabled="backlinkTriggering || backlinkStatus.running"
@click="doTriggerBacklink"
>
{{ backlinkTriggering ? '已触发...' : backlinkStatus.running ? '计算中...' : '立即执行' }}
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-5">
<!-- Domain Distribution -->
<div class="bg-gray-900 border border-gray-800 rounded-xl p-4 md:p-5">