mirror of
https://github.com/netfun2000/rttys_zhaojh329.git
synced 2026-02-27 09:53:24 +08:00
ui: add terminal window splitting functionality
- Add horizontal and vertical split options to context menu - Support recursive splitting for complex terminal layouts Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="splitter-panel-root">
|
||||
<div v-if="!config.panels" :key="config.id" :id="config.id" class="rtty-placeholder"></div>
|
||||
<div v-else class="rtty-splitter" :class="config.direction">
|
||||
<div v-for="(panel, index) in config.panels" :key="panel.id || `panel-${index}`" class="splitter-panel" :style="getPanelStyle(index)">
|
||||
<RttySplitter :config="panel" :devid="devid" @split="handleSplitPanel" @close="handleClosePanel" @resize="handleResize"/>
|
||||
<div v-if="index < config.panels.length - 1" class="splitter-bar" :class="config.direction" @mousedown="startResize(index, $event)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
devid: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['split', 'close', 'resize'])
|
||||
|
||||
const panelSizes = ref([])
|
||||
const isResizing = ref(false)
|
||||
const resizingIndex = ref(-1)
|
||||
const startPos = ref(0)
|
||||
const startSizes = ref([])
|
||||
const containerRef = ref(null)
|
||||
|
||||
function initializePanelSizes() {
|
||||
if (!props.config.panels) return
|
||||
|
||||
if (panelSizes.value.length !== props.config.panels.length) {
|
||||
const defaultSize = 100 / props.config.panels.length
|
||||
panelSizes.value = new Array(props.config.panels.length).fill(defaultSize)
|
||||
}
|
||||
}
|
||||
|
||||
function getPanelStyle(index) {
|
||||
if (!props.config.panels) return {}
|
||||
|
||||
initializePanelSizes()
|
||||
|
||||
const size = panelSizes.value[index] || (100 / props.config.panels.length)
|
||||
|
||||
if (props.config.direction === 'horizontal') {
|
||||
return {
|
||||
width: `${size}%`,
|
||||
height: '100%',
|
||||
position: 'relative'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
height: `${size}%`,
|
||||
width: '100%',
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startResize(index, event) {
|
||||
isResizing.value = true
|
||||
resizingIndex.value = index
|
||||
startPos.value = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||
startSizes.value = [...panelSizes.value]
|
||||
containerRef.value = event.target.closest('.rtty-splitter')
|
||||
|
||||
document.addEventListener('mousemove', handleResizeMove)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
|
||||
event.preventDefault()
|
||||
document.body.style.userSelect = 'none'
|
||||
}
|
||||
|
||||
function handleResizeMove(event) {
|
||||
if (!isResizing.value) return
|
||||
|
||||
const currentPos = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||
const delta = currentPos - startPos.value
|
||||
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
|
||||
const containerSize = props.config.direction === 'horizontal' ? container.clientWidth : container.clientHeight
|
||||
|
||||
const deltaPercent = (delta / containerSize) * 100
|
||||
|
||||
const newSizes = [...startSizes.value]
|
||||
const leftIndex = resizingIndex.value
|
||||
const rightIndex = leftIndex + 1
|
||||
|
||||
const minSize = 5
|
||||
const maxLeftDecrease = Math.max(0, newSizes[leftIndex] - minSize)
|
||||
const maxRightDecrease = Math.max(0, newSizes[rightIndex] - minSize)
|
||||
|
||||
const actualDelta = Math.max(-maxLeftDecrease, Math.min(maxRightDecrease, deltaPercent))
|
||||
|
||||
newSizes[leftIndex] = startSizes.value[leftIndex] + actualDelta
|
||||
newSizes[rightIndex] = startSizes.value[rightIndex] - actualDelta
|
||||
|
||||
panelSizes.value = newSizes
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isResizing.value = false
|
||||
resizingIndex.value = -1
|
||||
containerRef.value = null
|
||||
|
||||
document.removeEventListener('mousemove', handleResizeMove)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
|
||||
document.body.style.userSelect = ''
|
||||
|
||||
emit('resize')
|
||||
}
|
||||
|
||||
function handleSplitPanel(panelId, position) {
|
||||
emit('split', panelId, position)
|
||||
}
|
||||
|
||||
function handleClosePanel(panelId) {
|
||||
emit('close', panelId)
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
emit('resize')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splitter-panel-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rtty-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rtty-splitter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rtty-splitter.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.rtty-splitter.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitter-panel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitter-bar {
|
||||
position: absolute;
|
||||
background-color: #dcdfe6;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.splitter-bar:hover {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.splitter-bar.horizontal {
|
||||
top: 0;
|
||||
right: -2px;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.splitter-bar.vertical {
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="terminal-container">
|
||||
<div ref="terminal" class="terminal" @contextmenu.prevent="showContextmenu"></div>
|
||||
<el-dialog v-model="file.modal" :title="$t('Upload file to device')" @close="onUploadDialogClosed" :width="400">
|
||||
<el-upload :before-upload="beforeUpload" action="#">
|
||||
@@ -44,7 +44,8 @@ export default {
|
||||
'Contextmenu': Contextmenu
|
||||
},
|
||||
props: {
|
||||
devid: String
|
||||
devid: String,
|
||||
panelId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -55,6 +56,11 @@ export default {
|
||||
{name: 'font', caption: this.$t('Font Size')},
|
||||
{name: 'upload', caption: this.$t('Upload file') + ' - rtty -R'},
|
||||
{name: 'download', caption: this.$t('Download file') + ' - rtty -S file'},
|
||||
{name: 'split-left', caption: this.$t('split-left')},
|
||||
{name: 'split-right', caption: this.$t('split-right')},
|
||||
{name: 'split-up', caption: this.$t('split-up')},
|
||||
{name: 'split-down', caption: this.$t('split-down')},
|
||||
{name: 'close', caption: this.$t('Close')},
|
||||
{name: 'about', caption: this.$t('About')}
|
||||
],
|
||||
font: {
|
||||
@@ -71,7 +77,6 @@ export default {
|
||||
chunks: []
|
||||
},
|
||||
disposables: [],
|
||||
resizeDelay: null,
|
||||
socket: null,
|
||||
term: null,
|
||||
fitAddon: null,
|
||||
@@ -100,6 +105,16 @@ export default {
|
||||
this.$message.success(this.$t('Please execute command "rtty -R" in current terminal!'))
|
||||
} else if (name === 'download') {
|
||||
this.$message.success(this.$t('Please execute command "rtty -S file" in current terminal!'))
|
||||
} else if (name === 'split-left') {
|
||||
this.$emit('split', this.panelId, 'left')
|
||||
} else if (name === 'split-right') {
|
||||
this.$emit('split', this.panelId, 'right')
|
||||
} else if (name === 'split-up') {
|
||||
this.$emit('split', this.panelId, 'up')
|
||||
} else if (name === 'split-down') {
|
||||
this.$emit('split', this.panelId, 'down')
|
||||
} else if (name === 'close') {
|
||||
this.$emit('close', this.panelId)
|
||||
} else if (name === 'about') {
|
||||
window.open('https://github.com/zhaojh329/rtty')
|
||||
}
|
||||
@@ -209,18 +224,13 @@ export default {
|
||||
this.socket.send(b)
|
||||
},
|
||||
fitTerm() {
|
||||
this.$nextTick(() => {
|
||||
if (this.resizeDelay)
|
||||
clearTimeout(this.resizeDelay)
|
||||
this.resizeDelay = setTimeout(() => {
|
||||
this.fitAddon.fit()
|
||||
}, 200)
|
||||
})
|
||||
this.$nextTick(() => this.fitAddon.fit())
|
||||
},
|
||||
closed() {
|
||||
if (this.term)
|
||||
this.term.write('\n\n\r\x1B[1;3;31mConnection is closed.\x1B[0m')
|
||||
this.dispose()
|
||||
this.$emit('close', this.panelId)
|
||||
},
|
||||
openTerm() {
|
||||
const term = new Terminal({
|
||||
@@ -248,12 +258,7 @@ export default {
|
||||
overlayAddon.show(term.cols + 'x' + term.rows)
|
||||
}))
|
||||
|
||||
window.addEventListener('resize', this.fitTerm)
|
||||
|
||||
this.disposables.push({
|
||||
dispose: () => window.removeEventListener('resize', this.fitTerm)
|
||||
})
|
||||
|
||||
window.addEventListener('rtty-resize', this.fitTerm)
|
||||
this.fitTerm()
|
||||
},
|
||||
dispose() {
|
||||
@@ -356,6 +361,8 @@ export default {
|
||||
})
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('rtty-resize', this.fitTerm)
|
||||
|
||||
this.dispose()
|
||||
if (this.term)
|
||||
this.term.dispose()
|
||||
@@ -367,9 +374,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.terminal-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
margin: 5px;
|
||||
height: calc(100vh - 10px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport) {
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
"Font Size": "Font Size",
|
||||
"Upload file": "Upload file",
|
||||
"Download file": "Download file",
|
||||
"split-left": "Split: Left",
|
||||
"split-right": "Split: Right",
|
||||
"split-up": "Split: Up",
|
||||
"split-down": "Split: Down",
|
||||
"Close": "Close",
|
||||
"About": "About",
|
||||
"Please execute command \"rtty -R\" in current terminal!": "Please execute command \"rtty -R\" in current terminal!",
|
||||
"Please execute command \"rtty -S file\" in current terminal!": "Please execute command \"rtty -S file\" in current terminal!",
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
"Font Size": "字体大小",
|
||||
"Upload file": "上传文件",
|
||||
"Download file": "下载文件",
|
||||
"split-left": "拆分窗格: 左",
|
||||
"split-right": "拆分窗格: 右",
|
||||
"split-up": "拆分窗格: 上",
|
||||
"split-down": "拆分窗格: 下",
|
||||
"Close": "关闭",
|
||||
"About": "关于",
|
||||
"Please execute command \"rtty -R\" in current terminal!": "请在当前终端中执行命令 \"rtty -R\"",
|
||||
"Please execute command \"rtty -S file\" in current terminal!": "请在当前终端中执行命令 \"rtty -S file\"",
|
||||
|
||||
+122
-1
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<RttyTerm :devid="devid"/>
|
||||
<RttySplitter :devid="devid" :config="rootConfig" @split="handleSplitPanel" @close="handleClosePanel" @resize="handleResize" class="splitter-root"/>
|
||||
<div id="terminal-pool">
|
||||
<RttyTerm v-for="id in terms" :key="id" :data-terminal-id="id" :devid="devid" :panel-id="id" @split="handleSplitPanel" @close="handleClosePanel"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RttySplitter from '../components/RttySplitter.vue'
|
||||
import RttyTerm from '../components/RttyTerm.vue'
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
|
||||
defineProps({
|
||||
devid: {
|
||||
@@ -12,4 +17,120 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const paneRootID = 'rtty-panel-root'
|
||||
|
||||
const rootConfig = ref({
|
||||
id: paneRootID
|
||||
})
|
||||
|
||||
const terms = computed(() => {
|
||||
const ids = []
|
||||
const traverse = (node) => {
|
||||
if (node.id) {
|
||||
ids.push(node.id)
|
||||
} else if (node.panels) {
|
||||
node.panels.forEach(traverse)
|
||||
}
|
||||
}
|
||||
traverse(rootConfig.value)
|
||||
return ids
|
||||
})
|
||||
|
||||
function moveTerminalsToPool() {
|
||||
const pool = document.getElementById('terminal-pool')
|
||||
|
||||
terms.value.forEach(termId => {
|
||||
const term = document.querySelector(`[data-terminal-id="${termId}"]`)
|
||||
pool.appendChild(term)
|
||||
})
|
||||
}
|
||||
|
||||
function moveTerminalsToPlaceholders() {
|
||||
nextTick(() => {
|
||||
terms.value.forEach(termId => {
|
||||
const term = document.querySelector(`[data-terminal-id="${termId}"]`)
|
||||
const placeholder = document.getElementById(termId)
|
||||
placeholder.appendChild(term)
|
||||
})
|
||||
|
||||
setTimeout(() => dispatchEventRttyResize(), 100)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => moveTerminalsToPlaceholders())
|
||||
|
||||
function handleResize() {
|
||||
dispatchEventRttyResize()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
function dispatchEventRttyResize() {
|
||||
window.dispatchEvent(new CustomEvent('rtty-resize'))
|
||||
}
|
||||
|
||||
function handleClosePanel(panelId) {
|
||||
moveTerminalsToPool()
|
||||
deletePanel(rootConfig.value, panelId, 0)
|
||||
moveTerminalsToPlaceholders()
|
||||
}
|
||||
|
||||
function deletePanel(config, panelId, index, parent) {
|
||||
if (parent && config.id === panelId) {
|
||||
parent.panels.splice(index, 1)
|
||||
if (parent.panels.length === 1) {
|
||||
parent.id = parent.panels[0].id
|
||||
delete parent.direction
|
||||
delete parent.panels
|
||||
}
|
||||
return true
|
||||
} else if (config.panels) {
|
||||
return config.panels.some((panel, i) => deletePanel(panel, panelId, i, config))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleSplitPanel(panelId, direction) {
|
||||
moveTerminalsToPool()
|
||||
splitPanel(rootConfig.value, panelId, 0, direction)
|
||||
moveTerminalsToPlaceholders()
|
||||
}
|
||||
|
||||
function splitPanel(config, panelId, index, position, parent) {
|
||||
if (config.id === panelId) {
|
||||
const direction = (position === 'left' || position === 'right') ? 'horizontal' : 'vertical'
|
||||
const newId = 'panel-' + Math.random().toString(36).substring(2, 10)
|
||||
|
||||
if (parent && (parent.direction === direction || parent.panels.length < 2)) {
|
||||
parent.direction = direction
|
||||
if (position === 'right' || position === 'down')
|
||||
index++
|
||||
parent.panels.splice(index, 0, { id: newId })
|
||||
} else {
|
||||
if (position === 'right' || position === 'down') {
|
||||
config.panels = [
|
||||
{ id: config.id },
|
||||
{ id: newId }
|
||||
]
|
||||
} else {
|
||||
config.panels = [
|
||||
{ id: newId },
|
||||
{ id: config.id }
|
||||
]
|
||||
}
|
||||
config.direction = direction
|
||||
delete config.id
|
||||
}
|
||||
return true
|
||||
} else if (config.panels) {
|
||||
return config.panels.some((panel, i) => splitPanel(panel, panelId, i, position, config))
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.splitter-root {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user