Restyle the web UI in a light theme, step 2: remaining pages
- News: show the list and the article detail side by side; the drawer becomes a persistent detail panel with a revision picker and +/- line counts. - Crawler console: one card per source instead of tabs, plus a merged live log panel filterable by source and level. - Chat: segmented Chat / CONTEXT.md switch, message cards with times, bordered tool-call rows and a context usage bar. - Login, Skills and Settings move to the panel style; diff and highlight colors follow the new palette. The market K-line area stays dark.
This commit is contained in:
1 parent
c018b3693a
commit
916a2dee73
15 files changed
+685
-477
No files matched your search
@@ -1,23 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrawlLog, SourcesInfo } from "@/apis/crawler"
|
||||
import { Monitor, RefreshRight } from "@element-plus/icons-vue"
|
||||
import { listLogs, listSources, restartCrawler } from "@/apis/crawler"
|
||||
import { useEventSource } from "@/composables/useEventSource"
|
||||
import SourcePanel from "@/pages/crawler/components/SourcePanel.vue"
|
||||
import { formatTime } from "@/utils/text"
|
||||
|
||||
const MAX_LOGS = 1000
|
||||
// 连续失败达到该次数时 TAB 标红
|
||||
// 连续失败达到该次数时卡片告警
|
||||
const FAILURE_ALERT = 3
|
||||
const RESTART_TIMEOUT_MS = 60_000
|
||||
const LEVELS = ["INFO", "WARNING", "ERROR"]
|
||||
|
||||
const info = ref<SourcesInfo>()
|
||||
const activeTab = ref<string>()
|
||||
const logs = reactive<Record<string, CrawlLog[]>>({})
|
||||
// 正在等待重新上线的来源
|
||||
const restarting = reactive(new Set<string>())
|
||||
// 日志面板筛选:空字符串为全部来源
|
||||
const logSource = ref("")
|
||||
const levels = ref([...LEVELS])
|
||||
const autoScroll = ref(true)
|
||||
const logBox = ref<HTMLElement>()
|
||||
|
||||
async function refresh() {
|
||||
info.value = await listSources()
|
||||
activeTab.value ??= info.value.sources[0]?.code
|
||||
}
|
||||
|
||||
const sourceNames = computed(() => Object.fromEntries(info.value?.sources.map(s => [s.code, s.name]) ?? []))
|
||||
|
||||
// 各来源日志按全局自增 id 合并
|
||||
const visibleLogs = computed(() => Object.values(logs).flat().filter(log => (!logSource.value || log.source_code === logSource.value)
|
||||
&& (levels.value.includes(log.level) || (log.level === "CRITICAL" && levels.value.includes("ERROR")))).sort((a, b) => a.id - b.id).slice(-MAX_LOGS))
|
||||
|
||||
watch(() => visibleLogs.value.at(-1)?.id, async () => {
|
||||
if (!autoScroll.value)
|
||||
return
|
||||
await nextTick()
|
||||
logBox.value?.scrollTo({ top: logBox.value.scrollHeight })
|
||||
})
|
||||
|
||||
function levelClass(level: string) {
|
||||
if (level === "WARNING")
|
||||
return "bg-amber-500/20 text-amber-300"
|
||||
if (level === "ERROR" || level === "CRITICAL")
|
||||
return "bg-rose-500/20 text-rose-300"
|
||||
return "bg-sky-500/20 text-sky-300"
|
||||
}
|
||||
|
||||
function appendLog(log: CrawlLog) {
|
||||
@@ -72,43 +99,73 @@ refresh().then(() => Promise.all(info.value!.sources.map(s => loadHistory(s.code
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="info" class="h-full flex flex-col gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<el-tag :type="info.manager_alive ? 'success' : 'danger'">
|
||||
{{ info.manager_alive ? "管理进程运行中" : "管理进程未运行" }}
|
||||
</el-tag>
|
||||
<el-tag :type="connected ? 'success' : 'danger'">
|
||||
{{ connected ? "日志推送已连接" : "日志推送断开" }}
|
||||
</el-tag>
|
||||
<span class="text-sm text-[var(--el-text-color-secondary)]">数据库 {{ formatBytes(info.db_size_bytes) }}</span>
|
||||
<div v-if="info" class="flex flex-col gap-4">
|
||||
<el-card shadow="never" body-class="flex flex-wrap items-center gap-x-6 gap-y-2">
|
||||
<div class="flex items-center gap-2 text-sm text-slate-900 font-semibold">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
全局状态
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500">
|
||||
采集管理进程
|
||||
<el-tag :type="info.manager_alive ? 'success' : 'danger'" size="small">
|
||||
● {{ info.manager_alive ? "运行中" : "未运行" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500">
|
||||
实时日志推送
|
||||
<el-tag :type="connected ? 'success' : 'danger'" size="small">
|
||||
● {{ connected ? "已连接" : "断开" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-slate-500">
|
||||
数据库占用
|
||||
<span class="num text-sm text-slate-900">{{ formatBytes(info.db_size_bytes) }}</span>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<el-button type="danger" :loading="restarting.size > 0" @click="restart('all')">
|
||||
<el-button type="danger" plain :icon="RefreshRight" :loading="restarting.size > 0" @click="restart('all')">
|
||||
重启全部采集
|
||||
</el-button>
|
||||
</el-card>
|
||||
|
||||
<div class="text-sm text-slate-900 font-semibold">
|
||||
采集来源({{ info.sources.length }})
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" type="border-card" class="crawler-tabs min-h-0 flex flex-1 flex-col">
|
||||
<el-tab-pane v-for="source in info.sources" :key="source.code" :name="source.code" class="h-full">
|
||||
<template #label>
|
||||
<span :class="{ 'text-red-500 font-bold': source.consecutive_failures >= FAILURE_ALERT }">
|
||||
{{ source.name }}
|
||||
</span>
|
||||
<el-badge v-if="source.consecutive_failures >= FAILURE_ALERT" :value="source.consecutive_failures" class="ml-1" />
|
||||
</template>
|
||||
<SourcePanel
|
||||
:source="source"
|
||||
:logs="logs[source.code] ?? []"
|
||||
:restarting="restarting.has(source.code)"
|
||||
@restart="restart(source.code)"
|
||||
@changed="refresh"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-4">
|
||||
<SourcePanel
|
||||
v-for="source in info.sources" :key="source.code"
|
||||
:source="source"
|
||||
:logs="logs[source.code] ?? []"
|
||||
:restarting="restarting.has(source.code)"
|
||||
:alert="source.consecutive_failures >= FAILURE_ALERT"
|
||||
@restart="restart(source.code)"
|
||||
@changed="refresh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
实时日志
|
||||
<div class="flex-1" />
|
||||
<el-select v-model="logSource" size="small" class="w-36" :empty-values="[null, undefined]">
|
||||
<el-option value="" label="全部来源" />
|
||||
<el-option v-for="s in info.sources" :key="s.code" :value="s.code" :label="s.name" />
|
||||
</el-select>
|
||||
<el-checkbox-group v-model="levels" size="small">
|
||||
<el-checkbox-button v-for="level in LEVELS" :key="level" :value="level">
|
||||
{{ level }}
|
||||
</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
<el-switch v-model="autoScroll" active-text="自动滚动" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<div ref="logBox" class="h-100 overflow-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-200 font-mono leading-6">
|
||||
<div v-for="log in visibleLogs" :key="log.id" class="whitespace-pre-wrap break-all">
|
||||
<span class="text-slate-500">{{ formatTime(log.ts) }}</span>
|
||||
<span class="mx-2 rounded px-1" :class="levelClass(log.level)">{{ log.level }}</span>
|
||||
<span class="mr-2 text-teal-300">[{{ sourceNames[log.source_code] ?? log.source_code }}]</span>{{ log.message }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.crawler-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user