chore: opt fonts
This commit is contained in:
Generated
+20
@@ -8,6 +8,8 @@
|
||||
"name": "web-ui",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/geist-mono": "^5.2.7",
|
||||
"@fontsource/geist-sans": "^5.2.5",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@guolao/vue-monaco-editor": "^1.6.0",
|
||||
@@ -576,6 +578,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/geist-mono": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/geist-mono/-/geist-mono-5.2.7.tgz",
|
||||
"integrity": "sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/geist-sans": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/geist-sans/-/geist-sans-5.2.5.tgz",
|
||||
"integrity": "sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/inter": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/geist-mono": "^5.2.7",
|
||||
"@fontsource/geist-sans": "^5.2.5",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@guolao/vue-monaco-editor": "^1.6.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
@@ -22,6 +22,7 @@ const emit = defineEmits<{
|
||||
disconnected: []
|
||||
success: []
|
||||
failed: []
|
||||
'status-change': [status: { text: string; type: 'success' | 'error' | 'info' } | null]
|
||||
}>()
|
||||
|
||||
const terminalRef = ref<HTMLDivElement | null>(null)
|
||||
@@ -33,14 +34,21 @@ let inputBuffer = ''
|
||||
let commandHistory: string[] = []
|
||||
let historyIndex = -1
|
||||
|
||||
const statusMessage = ref<{ text: string; type: 'success' | 'error' | 'info' } | null>(null)
|
||||
|
||||
watch(statusMessage, (newVal) => {
|
||||
emit('status-change', newVal)
|
||||
})
|
||||
|
||||
function initTerminal(forceConnect = false) {
|
||||
if (!terminalRef.value) return
|
||||
|
||||
// 清理旧终端
|
||||
// 清理旧终端与 DOM 内容
|
||||
if (terminal) {
|
||||
terminal.dispose()
|
||||
terminal = null
|
||||
}
|
||||
terminalRef.value.innerHTML = ''
|
||||
|
||||
// 确保旧的 WebSocket 完全关闭
|
||||
if (ws) {
|
||||
@@ -52,6 +60,7 @@ function initTerminal(forceConnect = false) {
|
||||
|
||||
inputBuffer = ''
|
||||
isPtyMode = false
|
||||
statusMessage.value = { text: '正在初始化终端...', type: 'info' }
|
||||
|
||||
terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
@@ -160,12 +169,12 @@ function connectWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket(wsUrl)
|
||||
} catch {
|
||||
terminal?.writeln('\x1b[31m无法创建 WebSocket 连接\x1b[0m')
|
||||
statusMessage.value = { text: '无法创建 WebSocket 连接', type: 'error' }
|
||||
return
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
terminal?.writeln('\x1b[32m已连接到终端\x1b[0m')
|
||||
statusMessage.value = { text: '已连接到终端', type: 'success' }
|
||||
emit('connected')
|
||||
|
||||
// 如果有初始命令,延迟发送(PTY 会自动回显命令)
|
||||
@@ -205,24 +214,17 @@ function connectWebSocket() {
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
terminal?.writeln('')
|
||||
terminal?.writeln('\x1b[31m连接已断开\x1b[0m')
|
||||
statusMessage.value = { text: '连接已断开', type: 'error' }
|
||||
emit('disconnected')
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
terminal?.writeln('\x1b[31m连接错误\x1b[0m')
|
||||
statusMessage.value = { text: '连接错误', type: 'error' }
|
||||
}
|
||||
}
|
||||
|
||||
function reconnect() {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
}
|
||||
inputBuffer = ''
|
||||
isPtyMode = false
|
||||
terminal?.clear()
|
||||
connectWebSocket()
|
||||
initTerminal(true)
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
|
||||
@@ -111,7 +111,7 @@ onMounted(() => {
|
||||
<!-- Application Card: The main floating surface -->
|
||||
<div
|
||||
class="flex h-full w-full bg-background relative transition-all duration-500 overflow-hidden
|
||||
2xl:max-w-[1800px] 2xl:max-h-[92vh] 2xl:rounded-[2.5rem]
|
||||
2xl:max-w-[1800px] 2xl:max-h-[92vh] 2xl:rounded-[1.25rem]
|
||||
2xl:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.25),0_0_0_1px_rgba(255,255,255,0.05)]
|
||||
2xl:ring-1 2xl:ring-slate-900/5 dark:2xl:ring-white/10
|
||||
2xl:border 2xl:border-slate-200/60 dark:2xl:border-white/5
|
||||
|
||||
@@ -109,7 +109,7 @@ onMounted(loadLogs)
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="flex items-center gap-2 w-full lg:w-auto lg:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filterUsername" placeholder="搜索用户名..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
|
||||
@@ -169,7 +169,7 @@ function onDialogClose(open: boolean) {
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="flex items-center gap-2 w-full lg:w-auto lg:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
|
||||
@@ -166,7 +166,7 @@ function onDialogClose(open: boolean) {
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:ml-auto">
|
||||
<div class="flex items-center gap-2 w-full lg:w-auto lg:ml-auto">
|
||||
<div class="relative w-full sm:w-60 group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filters.keyword" placeholder="搜索标题或内容..." class="h-9 pl-9 w-full text-sm bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
|
||||
@@ -331,9 +331,9 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
||||
<p class="text-muted-foreground text-sm">管理和调度自动化执行任务</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row flex-wrap gap-3 w-full lg:flex-1 lg:justify-end">
|
||||
<div class="flex flex-col sm:flex-row flex-wrap gap-3 w-full lg:w-auto lg:ml-auto lg:justify-end">
|
||||
<!-- 搜索与标签 -->
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2 w-full sm:flex-1 sm:w-auto text-sm">
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2 w-full sm:flex-1 lg:flex-none lg:w-auto text-sm">
|
||||
<div class="relative w-full sm:flex-1 lg:max-w-[240px] group">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background"
|
||||
|
||||
@@ -28,6 +28,11 @@ function reconnect() {
|
||||
isReconnecting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const statusState = ref<{ text: string; type: 'success' | 'error' | 'info' } | null>(null)
|
||||
function handleStatusChange(status: any) {
|
||||
statusState.value = status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -58,13 +63,33 @@ function reconnect() {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect"
|
||||
:disabled="isReconnecting" title="重新连接">
|
||||
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': isReconnecting }" />
|
||||
</Button>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 终端状态指示 (右上角) -->
|
||||
<div v-if="statusState" class="flex items-center gap-1.5 transition-all animate-in fade-in slide-in-from-right-1">
|
||||
<div :class="[
|
||||
'w-1.5 h-1.5 rounded-full',
|
||||
statusState.type === 'success' ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]' :
|
||||
statusState.type === 'error' ? 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]' :
|
||||
'bg-blue-400 animate-pulse'
|
||||
]" />
|
||||
<span :class="[
|
||||
'text-[11px] font-medium transition-colors',
|
||||
statusState.type === 'success' ? 'text-green-500/90' :
|
||||
statusState.type === 'error' ? 'text-red-500/90' :
|
||||
'text-blue-400/90'
|
||||
]">
|
||||
{{ statusState.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-gray-400 hover:text-white" @click="reconnect"
|
||||
:disabled="isReconnecting" title="重新连接">
|
||||
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': isReconnecting }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 border border-[#3c3c3c] border-t-0 rounded-b-md overflow-hidden bg-[#1e1e1e]">
|
||||
<XTerminal ref="terminalRef" :font-size="13" />
|
||||
<XTerminal ref="terminalRef" :font-size="13" @status-change="handleStatusChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user