fix: logs viewer scoller
This commit is contained in:
@@ -22,8 +22,9 @@ password =
|
|||||||
dbname = baihu
|
dbname = baihu
|
||||||
# 数据库文件路径 (仅 sqlite)
|
# 数据库文件路径 (仅 sqlite)
|
||||||
path = data/baihu.db
|
path = data/baihu.db
|
||||||
# 数据库 DSN (仅 mysql/mariadb, 如果设置则优先使用。注意:需同时将 type 设置为 mysql)
|
# 数据库 DSN (仅 mysql/postgres, 如果设置则优先使用。注意:需同时将 type 设置为 mysql 或 postgres)
|
||||||
# 例如: user:password@unix(/var/run/mysqld/mysqld.sock)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
# 例如 (MySQL): user:password@unix(/var/run/mysqld/mysqld.sock)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
# 例如 (Postgres): postgres://user:password@localhost:5432/dbname?sslmode=disable
|
||||||
dsn =
|
dsn =
|
||||||
# 表前缀
|
# 表前缀
|
||||||
table_prefix = baihu_
|
table_prefix = baihu_
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
| `BH_DB_PASSWORD` | database.password | 数据库密码 | - |
|
| `BH_DB_PASSWORD` | database.password | 数据库密码 | - |
|
||||||
| `BH_DB_NAME` | database.dbname | 数据库库名 | baihu |
|
| `BH_DB_NAME` | database.dbname | 数据库库名 | baihu |
|
||||||
| `BH_DB_PATH` | database.path | SQLite 物理文件存储路径 | ./data/baihu.db |
|
| `BH_DB_PATH` | database.path | SQLite 物理文件存储路径 | ./data/baihu.db |
|
||||||
| `BH_DB_DSN` | database.dsn | 数据库 DSN (仅 mysql, 优先级高。**需同时设置 type=mysql**) | - |
|
| `BH_DB_DSN` | database.dsn | 数据库 DSN (仅 mysql/postgres, 优先级高。**需对应设置 type**) | - |
|
||||||
| `BH_DB_TABLE_PREFIX` | database.table_prefix | 数据库表前缀 | baihu_ |
|
| `BH_DB_TABLE_PREFIX` | database.table_prefix | 数据库表前缀 | baihu_ |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -46,9 +46,10 @@ url_prefix = /baihu
|
|||||||
[database]
|
[database]
|
||||||
type = sqlite
|
type = sqlite
|
||||||
path = /app/data/baihu.db
|
path = /app/data/baihu.db
|
||||||
# mysql 连接示例 (Unix Socket):
|
# 数据库连接示例 (Unix Socket / DSN):
|
||||||
# 注意:使用 dsn 时,type 必须设为 mysql
|
# 注意:使用 dsn 时,type 必须设为 mysql 或 postgres
|
||||||
# dsn = user:password@unix(/var/run/mysqld/mysqld.sock)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
# dsn = user:password@unix(/var/run/mysqld/mysqld.sock)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
# dsn = postgres://user:password@localhost:5432/dbname?sslmode=disable
|
||||||
table_prefix = baihu_
|
table_prefix = baihu_
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ func Init(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
dialector = mysql.Open(dsn)
|
dialector = mysql.Open(dsn)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
|
dsn := cfg.DSN
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
|
||||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName)
|
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName)
|
||||||
|
}
|
||||||
dialector = postgres.Open(dsn)
|
dialector = postgres.Open(dsn)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported database type: %s", cfg.Type)
|
return fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import Ansi from 'ansi-to-vue3'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
|
||||||
import '@xterm/xterm/css/xterm.css'
|
|
||||||
|
|
||||||
const lightTheme = {
|
|
||||||
background: '#f4f4f5', // zinc-100
|
|
||||||
foreground: '#18181b', // zinc-900
|
|
||||||
cursor: '#18181b',
|
|
||||||
selectionBackground: 'rgba(0, 0, 0, 0.15)',
|
|
||||||
}
|
|
||||||
|
|
||||||
const darkTheme = {
|
|
||||||
background: '#09090b', // zinc-950
|
|
||||||
foreground: '#e4e4e7', // zinc-200
|
|
||||||
cursor: '#e4e4e7',
|
|
||||||
selectionBackground: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -32,109 +16,76 @@ const props = withDefaults(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const terminalRef = ref<HTMLDivElement | null>(null)
|
const scrollContainerRef = ref<HTMLDivElement | null>(null)
|
||||||
let terminal: Terminal | null = null
|
const lightBackgroundClass = 'bg-zinc-100 text-zinc-900'
|
||||||
let fitAddon: FitAddon | null = null
|
const darkBackgroundClass = 'bg-zinc-950 text-zinc-300' // 稍微调亮一点暗色模式文字
|
||||||
const lightBackgroundClass = 'terminal-theme-light'
|
|
||||||
const darkBackgroundClass = 'terminal-theme-dark'
|
|
||||||
|
|
||||||
function getTheme() {
|
// 自动滚动到底部逻辑
|
||||||
return props.theme === 'dark' ? darkTheme : lightTheme
|
const scrollToBottom = () => {
|
||||||
|
if (props.autoScroll && scrollContainerRef.value) {
|
||||||
|
scrollContainerRef.value.scrollTop = scrollContainerRef.value.scrollHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTerminal() {
|
watch(() => props.content, () => {
|
||||||
if (!terminalRef.value) return
|
|
||||||
|
|
||||||
terminal = new Terminal({
|
|
||||||
fontSize: props.fontSize,
|
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
|
||||||
theme: getTheme(),
|
|
||||||
allowProposedApi: true,
|
|
||||||
convertEol: true,
|
|
||||||
disableStdin: true,
|
|
||||||
cursorBlink: false,
|
|
||||||
rows: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
fitAddon = new FitAddon()
|
|
||||||
terminal.loadAddon(fitAddon)
|
|
||||||
terminal.open(terminalRef.value)
|
|
||||||
|
|
||||||
if (props.content) {
|
|
||||||
terminal.write(props.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
fitAddon?.fit()
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.content, (newContent, oldContent) => {
|
|
||||||
if (!terminal) return
|
|
||||||
|
|
||||||
if (newContent.length < oldContent.length) {
|
|
||||||
terminal.clear()
|
|
||||||
terminal.write(newContent)
|
|
||||||
} else {
|
|
||||||
const appended = newContent.slice(oldContent.length)
|
|
||||||
terminal.write(appended)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.autoScroll) {
|
if (props.autoScroll) {
|
||||||
terminal.scrollToBottom()
|
nextTick(scrollToBottom)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.theme, () => {
|
|
||||||
if (terminal) {
|
|
||||||
terminal.options.theme = getTheme()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function handleResize() {
|
|
||||||
fitAddon?.fit()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', handleResize)
|
if (props.autoScroll) {
|
||||||
setTimeout(initTerminal, 100)
|
setTimeout(scrollToBottom, 50)
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
if (terminal) {
|
|
||||||
terminal.dispose()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
fit: () => fitAddon?.fit(),
|
fit: () => { },
|
||||||
clear: () => terminal?.clear()
|
clear: () => { },
|
||||||
|
scrollToBottom
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="terminalRef"
|
ref="scrollContainerRef"
|
||||||
class="w-full h-full min-h-0"
|
class="w-full h-full overflow-y-auto overflow-x-auto log-container native-scroll"
|
||||||
:class="theme === 'dark' ? darkBackgroundClass : lightBackgroundClass"
|
:class="theme === 'dark' ? darkBackgroundClass : lightBackgroundClass"
|
||||||
/>
|
:style="{ fontSize: fontSize + 'px' }"
|
||||||
|
><div class="inline-block min-w-full p-3 font-mono leading-normal whitespace-pre text-left"><Ansi>{{ content }}</Ansi></div></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.terminal-theme-light {
|
.native-scroll {
|
||||||
background-color: #f4f4f5;
|
scrollbar-gutter: stable;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-theme-dark {
|
/* 强制覆盖 ansi-to-vue3 可能生成的 code 样式 */
|
||||||
background-color: #09090b;
|
:deep(code) {
|
||||||
|
background: transparent !important;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.xterm) {
|
/* 滚动条美化 */
|
||||||
padding: 8px;
|
.native-scroll::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
}
|
}
|
||||||
:deep(.xterm-viewport),
|
|
||||||
:deep(.xterm-screen) {
|
.native-scroll::-webkit-scrollbar-thumb {
|
||||||
background-color: inherit !important;
|
background: rgba(128, 128, 128, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-scroll::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, watch, nextTick } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { TASK_STATUS, TASK_TYPE } from '@/constants'
|
import { TASK_STATUS, TASK_TYPE } from '@/constants'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -175,11 +175,6 @@ async function selectLog(log: TaskLog) {
|
|||||||
wsContent.value = event.data
|
wsContent.value = event.data
|
||||||
} else {
|
} else {
|
||||||
wsContent.value += event.data
|
wsContent.value += event.data
|
||||||
// 自动滚动到底部
|
|
||||||
nextTick(() => {
|
|
||||||
const pre = document.querySelector('.log-pre')
|
|
||||||
if (pre) pre.scrollTop = pre.scrollHeight
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ onUnmounted(() => {
|
|||||||
<div
|
<div
|
||||||
class="bg-background rounded-lg shadow-lg flex flex-col w-full sm:w-[90vw] md:w-[80vw] max-w-5xl h-[90vh] sm:h-[85vh]">
|
class="bg-background rounded-lg shadow-lg flex flex-col w-full sm:w-[90vw] md:w-[80vw] max-w-5xl h-[90vh] sm:h-[85vh]">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:flex-row sm:items-center justify-between px-3 sm:px-4 py-2 sm:py-3 border-b shrink-0 gap-2">
|
class="flex items-center justify-between px-3 sm:px-4 py-2 sm:py-3 border-b shrink-0 gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<span class="text-sm font-medium truncate">{{ title }}</span>
|
<span class="text-sm font-medium truncate" :title="title">{{ title }}</span>
|
||||||
<div v-if="status"
|
<div v-if="status"
|
||||||
class="flex items-center gap-1.5 px-2 py-0.5 rounded text-[10px] font-bold uppercase transition-colors shrink-0"
|
class="flex items-center gap-1.5 px-2 py-0.5 rounded text-[10px] font-bold uppercase transition-colors shrink-0"
|
||||||
:class="status === 'success' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
|
:class="status === 'success' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
|
||||||
@@ -72,8 +72,8 @@ onUnmounted(() => {
|
|||||||
{{ status === 'success' ? '成功' : status === 'failed' ? '失败' : '执行中' }}
|
{{ status === 'success' ? '成功' : status === 'failed' ? '失败' : '执行中' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @click="close">
|
<Button variant="ghost" size="icon" class="h-8 w-8 sm:h-7 sm:w-7" @click="close">
|
||||||
<X class="h-4 w-4" />
|
<X class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user