开发预览 更新于 2026-05-10

LogViewer 日志面板

终端风格的日志流面板:等宽字体、行级 level 着色、自动跟随底部、search 高亮、levels 过滤、用户滚开后出现「跳到底部」按钮。

基础用法

logsLogEntry[]{ id?, timestamp?, level?, message, source? }。当 follow=true 且当前已经在底部时,新 log push 后自动滚到底;用户主动滚到上面就会停止跟随,并在右下角显示「跳到底部」按钮,点击恢复跟随。search 字符串会在 message 中高亮。

背景 视口
09:01:35.373INFOservice auth-svc started on :8080
09:01:36.373DEBUGdb connection pool size=10
09:01:38.373INFOrequest POST /login user=alice
09:01:39.373WARNslow query detected SELECT entries took 1.2s
09:01:40.373SUCCESScache warmed 1280 entries
09:01:41.873ERRORfailed to reach upstream: ECONNREFUSED 10.0.0.4:9092
src/App.vue
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { CfLogViewer, type LogEntry } from '@chufix-design/vue';

const logs = ref<LogEntry[]>([
  { id: 1, timestamp: Date.now() - 8000, level: 'info', message: 'service auth-svc started on :8080' },
  { id: 2, timestamp: Date.now() - 7000, level: 'debug', message: 'db connection pool size=10' },
  { id: 3, timestamp: Date.now() - 5000, level: 'info', message: 'request POST /login user=alice' },
  { id: 4, timestamp: Date.now() - 4000, level: 'warn', message: 'slow query detected SELECT entries took 1.2s' },
  { id: 5, timestamp: Date.now() - 3000, level: 'success', message: 'cache warmed 1280 entries' },
  { id: 6, timestamp: Date.now() - 1500, level: 'error', message: 'failed to reach upstream: ECONNREFUSED 10.0.0.4:9092' },
]);

const search = ref('');
let nextId = 7;
let interval: ReturnType<typeof setInterval> | null = null;

onMounted(() => {
  interval = setInterval(() => {
    const levels = ['info', 'debug', 'warn', 'error', 'success'] as const;
    const lvl = levels[Math.floor(Math.random() * levels.length)];
    logs.value = [
      ...logs.value,
      {
        id: nextId++,
        timestamp: Date.now(),
        level: lvl,
        message: `tick ${nextId} from auto-feed (${lvl})`,
      },
    ];
    if (logs.value.length > 80) logs.value = logs.value.slice(-80);
  }, 1800);
});
onBeforeUnmount(() => {
  if (interval) clearInterval(interval);
});
</script>
<template>
  <div class="lv-demo">
    <input
      v-model="search"
      class="lv-demo__search"
      placeholder="搜索日志…"
    />
    <CfLogViewer :logs="logs" :search="search" :height="300" />
  </div>
</template>
<style scoped>
.lv-demo {
  display: grid;
  gap: 8px;
}
.lv-demo__search {
  height: 30px;
  padding: 0 10px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.lv-demo__search:focus {
  outline: none;
  border-color: var(--accent-1);
}
</style>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { CfLogViewer } from '@chufix-design/vue';

const logs = ref<LogEntry[]>([
  { id: 1, timestamp: Date.now() - 8000, level: 'info', message: 'service auth-svc started on :8080' },
  { id: 2, timestamp: Date.now() - 7000, level: 'debug', message: 'db connection pool size=10' },
  { id: 3, timestamp: Date.now() - 5000, level: 'info', message: 'request POST /login user=alice' },
  { id: 4, timestamp: Date.now() - 4000, level: 'warn', message: 'slow query detected SELECT entries took 1.2s' },
  { id: 5, timestamp: Date.now() - 3000, level: 'success', message: 'cache warmed 1280 entries' },
  { id: 6, timestamp: Date.now() - 1500, level: 'error', message: 'failed to reach upstream: ECONNREFUSED 10.0.0.4:9092' },
]);

const search = ref('');
let nextId = 7;
let interval= null;

onMounted(() => {
  interval = setInterval(() => {
    const levels = ['info', 'debug', 'warn', 'error', 'success'];
    const lvl = levels[Math.floor(Math.random() * levels.length)];
    logs.value = [
      ...logs.value,
      {
        id: nextId++,
        timestamp: Date.now(),
        level: lvl,
        message: `tick ${nextId} from auto-feed (${lvl})`,
      },
    ];
    if (logs.value.length > 80) logs.value = logs.value.slice(-80);
  }, 1800);
});
onBeforeUnmount(() => {
  if (interval) clearInterval(interval);
});
</script>
<template>
  <div class="lv-demo">
    <input
      v-model="search"
      class="lv-demo__search"
      placeholder="搜索日志…"
    />
    <CfLogViewer :logs="logs" :search="search" :height="300" />
  </div>
</template>
<style scoped>
.lv-demo {
  display: grid;
  gap: 8px;
}
.lv-demo__search {
  height: 30px;
  padding: 0 10px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.lv-demo__search:focus {
  outline: none;
  border-color: var(--accent-1);
}
</style>
import { useEffect, useState } from 'react';
import { CfLogViewer } from '@chufix-design/react';

export default function Demo() {
  const [logs, setLogs] = useState<LogEntry[]>([
    { id: 1, timestamp: Date.now() - 8000, level: 'info', message: 'service auth-svc started on :8080' },
    { id: 2, timestamp: Date.now() - 7000, level: 'debug', message: 'db connection pool size=10' },
    { id: 3, timestamp: Date.now() - 5000, level: 'info', message: 'request POST /login user=alice' },
    { id: 4, timestamp: Date.now() - 4000, level: 'warn', message: 'slow query detected SELECT entries took 1.2s' },
    { id: 5, timestamp: Date.now() - 3000, level: 'success', message: 'cache warmed 1280 entries' },
    { id: 6, timestamp: Date.now() - 1500, level: 'error', message: 'failed to reach upstream: ECONNREFUSED 10.0.0.4:9092' },
  ]);

  const [search, setSearch] = useState('');
  let nextId = 7;
  let interval: ReturnType<typeof setInterval> | null = null;

  useEffect(() => {
    interval = setInterval(() => {
      const levels = ['info', 'debug', 'warn', 'error', 'success'] as const;
      const lvl = levels[Math.floor(Math.random() * levels.length)];
      setLogs([
        ...logs,
        {
          id: nextId++,
          timestamp: Date.now(),
          level: lvl,
          message: `tick ${nextId} from auto-feed (${lvl})`,
        },
      ]);
      if (logs.length > 80) setLogs(logs.slice(-80));
    }, 1800);
  });
  onBeforeUnmount(() => {
    if (interval) clearInterval(interval);
  });
  return (
    <>
      <div className="lv-demo">
          <input value={search} onChange={setSearch} className="lv-demo__search" placeholder="搜索日志…" />
          <CfLogViewer logs={logs} search={search} height={300} />
        </div>
    </>
  );
}
import { useEffect, useState } from 'react';
import { CfLogViewer } from '@chufix-design/react';

export default function Demo() {
  const [logs, setLogs] = useState<LogEntry[]>([
    { id: 1, timestamp: Date.now() - 8000, level: 'info', message: 'service auth-svc started on :8080' },
    { id: 2, timestamp: Date.now() - 7000, level: 'debug', message: 'db connection pool size=10' },
    { id: 3, timestamp: Date.now() - 5000, level: 'info', message: 'request POST /login user=alice' },
    { id: 4, timestamp: Date.now() - 4000, level: 'warn', message: 'slow query detected SELECT entries took 1.2s' },
    { id: 5, timestamp: Date.now() - 3000, level: 'success', message: 'cache warmed 1280 entries' },
    { id: 6, timestamp: Date.now() - 1500, level: 'error', message: 'failed to reach upstream: ECONNREFUSED 10.0.0.4:9092' },
  ]);

  const [search, setSearch] = useState('');
  let nextId = 7;
  let interval= null;

  useEffect(() => {
    interval = setInterval(() => {
      const levels = ['info', 'debug', 'warn', 'error', 'success'];
      const lvl = levels[Math.floor(Math.random() * levels.length)];
      setLogs([
        ...logs,
        {
          id: nextId++,
          timestamp: Date.now(),
          level: lvl,
          message: `tick ${nextId} from auto-feed (${lvl})`,
        },
      ]);
      if (logs.length > 80) setLogs(logs.slice(-80));
    }, 1800);
  });
  onBeforeUnmount(() => {
    if (interval) clearInterval(interval);
  });
  return (
    <>
      <div className="lv-demo">
          <input value={search} onChange={setSearch} className="lv-demo__search" placeholder="搜索日志…" />
          <CfLogViewer logs={logs} search={search} height={300} />
        </div>
    </>
  );
}

API

Props

属性类型默认说明
logsLogEntry[]日志数组
followbooleantrue自动跟随底部
searchstring高亮关键字
heightnumber | string320面板高
showTimestampbooleantrue显示时间戳
showLevelbooleantrue显示 level 标签
showSourcebooleanfalse显示 source 字段
levelsLogLevel[]过滤;不传显示全部

反馈与讨论

LogViewer 日志面板 的讨论

0
0 / 600
正在加载评论...