mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 14:41:45 +08:00
feat: 优化网页终端样式
This commit is contained in:
108
web/src/components/TerminalDialog.vue
Normal file
108
web/src/components/TerminalDialog.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ExternalLink, Terminal, X } from 'lucide-vue-next'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:open', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
function openTerminalInNewTab() {
|
||||||
|
window.open('/api/terminal/', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFrameScrollbars(event: Event) {
|
||||||
|
const frame = event.currentTarget as HTMLIFrameElement | null
|
||||||
|
try {
|
||||||
|
const doc = frame?.contentDocument
|
||||||
|
if (!doc) return
|
||||||
|
|
||||||
|
const styleId = 'one-kvm-terminal-scrollbar-style'
|
||||||
|
let style = doc.getElementById(styleId) as HTMLStyleElement | null
|
||||||
|
if (!style) {
|
||||||
|
style = doc.createElement('style')
|
||||||
|
style.id = styleId
|
||||||
|
const parent = doc.head || doc.documentElement
|
||||||
|
parent.appendChild(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.textContent = `
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
.xterm-viewport {
|
||||||
|
-ms-overflow-style: none !important;
|
||||||
|
scrollbar-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html::-webkit-scrollbar,
|
||||||
|
body::-webkit-scrollbar,
|
||||||
|
.xterm-viewport::-webkit-scrollbar {
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
} catch {
|
||||||
|
// The terminal is served from the same origin; keep the dialog usable if a browser blocks access.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :open="props.open" @update:open="emit('update:open', $event)">
|
||||||
|
<DialogContent
|
||||||
|
:show-close-button="false"
|
||||||
|
class="w-[98vw] sm:w-[95vw] max-w-5xl h-[90dvh] sm:h-[85dvh] max-h-[720px] p-0 flex flex-col gap-0 overflow-hidden"
|
||||||
|
>
|
||||||
|
<DialogHeader class="px-3 sm:px-4 py-1 border-b shrink-0">
|
||||||
|
<DialogTitle class="flex h-8 items-center justify-between w-full">
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<Terminal class="h-4 w-4 shrink-0" />
|
||||||
|
<span class="truncate text-sm font-semibold">{{ t('extensions.ttyd.title') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
@click="openTerminalInNewTab"
|
||||||
|
:aria-label="t('extensions.ttyd.openInNewTab')"
|
||||||
|
:title="t('extensions.ttyd.openInNewTab')"
|
||||||
|
>
|
||||||
|
<ExternalLink class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<DialogClose as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7">
|
||||||
|
<X class="h-3.5 w-3.5" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<iframe
|
||||||
|
v-if="props.open"
|
||||||
|
src="/api/terminal/"
|
||||||
|
class="block w-full h-full border-0"
|
||||||
|
allow="clipboard-read; clipboard-write"
|
||||||
|
scrolling="no"
|
||||||
|
@load="hideFrameScrollbars"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -33,6 +33,7 @@ import InfoBar from '@/components/InfoBar.vue'
|
|||||||
import VirtualKeyboard from '@/components/VirtualKeyboard.vue'
|
import VirtualKeyboard from '@/components/VirtualKeyboard.vue'
|
||||||
import StatsSheet from '@/components/StatsSheet.vue'
|
import StatsSheet from '@/components/StatsSheet.vue'
|
||||||
import ComputerUseSheet from '@/components/ComputerUseSheet.vue'
|
import ComputerUseSheet from '@/components/ComputerUseSheet.vue'
|
||||||
|
import TerminalDialog from '@/components/TerminalDialog.vue'
|
||||||
import type { ComputerUseTimelineItem, NewComputerUseTimelineItem } from '@/types/computerUseTimeline'
|
import type { ComputerUseTimelineItem, NewComputerUseTimelineItem } from '@/types/computerUseTimeline'
|
||||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||||
import BrandMark from '@/components/BrandMark.vue'
|
import BrandMark from '@/components/BrandMark.vue'
|
||||||
@@ -61,8 +62,6 @@ import {
|
|||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Terminal,
|
|
||||||
ExternalLink,
|
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
@@ -2023,12 +2022,6 @@ function openTerminal() {
|
|||||||
showTerminalDialog.value = true
|
showTerminalDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTerminalInNewTab() {
|
|
||||||
if (!showTerminal.value) return
|
|
||||||
if (!ttydStatus.value?.running) return
|
|
||||||
window.open('/api/terminal/', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePowerShort() {
|
async function handlePowerShort() {
|
||||||
try {
|
try {
|
||||||
await atxApi.power('short')
|
await atxApi.power('short')
|
||||||
@@ -3103,37 +3096,7 @@ onUnmounted(() => {
|
|||||||
v-model:open="statsSheetOpen"
|
v-model:open="statsSheetOpen"
|
||||||
:webrtc-stats="webrtc.stats.value"
|
:webrtc-stats="webrtc.stats.value"
|
||||||
/>
|
/>
|
||||||
<Dialog v-if="showTerminal" v-model:open="showTerminalDialog">
|
<TerminalDialog v-if="showTerminal" v-model:open="showTerminalDialog" />
|
||||||
<DialogContent class="w-[98vw] sm:w-[95vw] max-w-5xl h-[90dvh] sm:h-[85dvh] max-h-[720px] p-0 flex flex-col overflow-hidden">
|
|
||||||
<DialogHeader class="px-3 sm:px-4 py-2 sm:py-3 border-b shrink-0">
|
|
||||||
<DialogTitle class="flex items-center justify-between w-full">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Terminal class="h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
<span class="text-sm sm:text-base">{{ t('extensions.ttyd.title') }}</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7 sm:h-8 sm:w-8 mr-6 sm:mr-8"
|
|
||||||
@click="openTerminalInNewTab"
|
|
||||||
:aria-label="t('extensions.ttyd.openInNewTab')"
|
|
||||||
:title="t('extensions.ttyd.openInNewTab')"
|
|
||||||
>
|
|
||||||
<ExternalLink class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div class="flex-1 min-h-0">
|
|
||||||
<iframe
|
|
||||||
v-if="showTerminalDialog"
|
|
||||||
src="/api/terminal/"
|
|
||||||
class="w-full h-full border-0"
|
|
||||||
allow="clipboard-read; clipboard-write"
|
|
||||||
scrolling="no"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Dialog v-model:open="changePasswordDialogOpen">
|
<Dialog v-model:open="changePasswordDialogOpen">
|
||||||
<DialogContent class="w-[95vw] max-w-md">
|
<DialogContent class="w-[95vw] max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { getVideoFormatState } from '@/lib/video-format-support'
|
|||||||
import { formatVideoDeviceLabel } from '@/lib/video-device-label'
|
import { formatVideoDeviceLabel } from '@/lib/video-device-label'
|
||||||
import AppLayout from '@/components/AppLayout.vue'
|
import AppLayout from '@/components/AppLayout.vue'
|
||||||
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
||||||
|
import TerminalDialog from '@/components/TerminalDialog.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -1696,10 +1697,6 @@ function openTerminal() {
|
|||||||
showTerminalDialog.value = true
|
showTerminalDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTerminalInNewTab() {
|
|
||||||
window.open('/api/terminal/', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtStatusText(status: ExtensionStatus | undefined): string {
|
function getExtStatusText(status: ExtensionStatus | undefined): string {
|
||||||
if (!status) return t('extensions.stopped')
|
if (!status) return t('extensions.stopped')
|
||||||
switch (status.state) {
|
switch (status.state) {
|
||||||
@@ -5354,37 +5351,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Terminal Dialog -->
|
<!-- Terminal Dialog -->
|
||||||
<Dialog v-model:open="showTerminalDialog">
|
<TerminalDialog v-model:open="showTerminalDialog" />
|
||||||
<DialogContent class="w-[98vw] sm:w-[95vw] max-w-5xl h-[90dvh] sm:h-[85dvh] max-h-[720px] p-0 flex flex-col overflow-hidden">
|
|
||||||
<DialogHeader class="px-3 sm:px-4 py-2 sm:py-3 border-b shrink-0">
|
|
||||||
<DialogTitle class="flex items-center justify-between w-full">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Terminal class="h-4 w-4 sm:h-5 sm:w-5" />
|
|
||||||
<span class="text-sm sm:text-base">{{ t('extensions.ttyd.title') }}</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-7 w-7 sm:h-8 sm:w-8 mr-6 sm:mr-8"
|
|
||||||
@click="openTerminalInNewTab"
|
|
||||||
:aria-label="t('extensions.ttyd.openInNewTab')"
|
|
||||||
:title="t('extensions.ttyd.openInNewTab')"
|
|
||||||
>
|
|
||||||
<ExternalLink class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div class="flex-1 min-h-0">
|
|
||||||
<iframe
|
|
||||||
v-if="showTerminalDialog"
|
|
||||||
src="/api/terminal/"
|
|
||||||
class="w-full h-full border-0"
|
|
||||||
allow="clipboard-read; clipboard-write"
|
|
||||||
scrolling="no"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<!-- Restart Confirmation Dialog -->
|
<!-- Restart Confirmation Dialog -->
|
||||||
<Dialog v-model:open="showRestartDialog">
|
<Dialog v-model:open="showRestartDialog">
|
||||||
|
|||||||
Reference in New Issue
Block a user