mirror of
https://github.com/netfun2000/rttys_zhaojh329.git
synced 2026-02-27 09:53:24 +08:00
ui: migrate all Vue components from Options API to Composition API
Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Generated
+413
-397
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
"element-plus": "^2.10.1",
|
"element-plus": "^2.10.1",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.16",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-clipboard2": "^0.3.3",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.5",
|
"vue-i18n": "^11.1.5",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="content" class="content" :style="{top: axis.y + 'px', left: axis.x + 'px'}" v-if="visibility">
|
<div ref="content" class="content" :style="{top: axis.y + 'px', left: axis.x + 'px'}" v-if="model">
|
||||||
<template v-for="(item, index) in menus" :key="item.name">
|
<template v-for="(item, index) in menus" :key="item.name">
|
||||||
<a @click="onMenuClick(item.name)" :style="{'text-decoration': item.underline ? 'underline' : 'none'}">
|
<a @click="onMenuClick(item.name)" :style="{'text-decoration': item.underline ? 'underline' : 'none'}">
|
||||||
{{item.caption || item.name}}
|
{{item.caption || item.name}}
|
||||||
@@ -9,75 +9,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { reactive, watch, nextTick, onBeforeUnmount, useTemplateRef } from 'vue'
|
||||||
name: 'ContextMenu',
|
|
||||||
props: {
|
|
||||||
menus: Array
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
visibility: false,
|
|
||||||
axis: {x: 0, y: 0}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
visibility(val) {
|
|
||||||
if (!val)
|
|
||||||
document.removeEventListener('mousedown', this.close)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close(e) {
|
|
||||||
const el = this.$refs.content
|
|
||||||
|
|
||||||
if (e.clientX >= this.axis.x && e.clientX <= this.axis.x + el.clientWidth &&
|
defineProps({
|
||||||
e.clientY >= this.axis.y && e.clientY <= this.axis.y + el.clientHeight) {
|
menus: Array
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
this.visibility = false
|
const model = defineModel()
|
||||||
},
|
|
||||||
show(e) {
|
|
||||||
document.addEventListener('mousedown', this.close)
|
|
||||||
|
|
||||||
this.axis = {x: e.clientX, y: e.clientY}
|
const emit = defineEmits(['click'])
|
||||||
this.visibility = true
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
const content = useTemplateRef('content')
|
||||||
const el = this.$refs.content
|
const axis = reactive({ x: 0, y: 0 })
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect()
|
const close = (e) => {
|
||||||
const viewportWidth = window.innerWidth
|
const el = content.value
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
let x = e.clientX
|
if (e.clientX >= axis.x && e.clientX <= axis.x + el.clientWidth &&
|
||||||
let y = e.clientY
|
e.clientY >= axis.y && e.clientY <= axis.y + el.clientHeight) {
|
||||||
|
return
|
||||||
if (x + rect.width > viewportWidth) {
|
|
||||||
x = viewportWidth - rect.width - 15
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y + rect.height > viewportHeight) {
|
|
||||||
y = viewportHeight - rect.height - 15
|
|
||||||
}
|
|
||||||
|
|
||||||
x = Math.max(15, x)
|
|
||||||
y = Math.max(15, y)
|
|
||||||
|
|
||||||
this.axis = {x, y}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onMenuClick(name) {
|
|
||||||
this.visibility = false
|
|
||||||
this.$emit('click', name)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
document.removeEventListener('mousedown', this.close)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const show = (clientX, clientY) => {
|
||||||
|
document.addEventListener('mousedown', close)
|
||||||
|
|
||||||
|
axis.x = clientX
|
||||||
|
axis.y = clientY
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const el = content.value
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
|
||||||
|
let x = clientX
|
||||||
|
let y = clientY
|
||||||
|
|
||||||
|
if (x + rect.width > viewportWidth) {
|
||||||
|
x = viewportWidth - rect.width - 15
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + rect.height > viewportHeight) {
|
||||||
|
y = viewportHeight - rect.height - 15
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(15, x)
|
||||||
|
y = Math.max(15, y)
|
||||||
|
|
||||||
|
axis.x = x
|
||||||
|
axis.y = y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMenuClick = (name) => {
|
||||||
|
model.value = null
|
||||||
|
emit('click', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => model.value, (val) => {
|
||||||
|
if (!val)
|
||||||
|
document.removeEventListener('mousedown', close)
|
||||||
|
else
|
||||||
|
show(val.x, val.y)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('mousedown', close))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+141
-130
@@ -53,145 +53,156 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, reactive, computed, nextTick, useTemplateRef } from 'vue'
|
||||||
name: 'RttyCmd',
|
import { useI18n } from 'vue-i18n'
|
||||||
props: {
|
import { ElMessage } from 'element-plus'
|
||||||
selection: Array
|
import axios from 'axios'
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cmdModal: false,
|
|
||||||
inputParamVisible: false,
|
|
||||||
inputParamValue: '',
|
|
||||||
cmdStatus: {
|
|
||||||
total: 0,
|
|
||||||
modal: false,
|
|
||||||
execing: 0,
|
|
||||||
fail: 0,
|
|
||||||
respModal: false,
|
|
||||||
responses: []
|
|
||||||
},
|
|
||||||
cmdData: {
|
|
||||||
username: '',
|
|
||||||
cmd: '',
|
|
||||||
params: [],
|
|
||||||
currentParam: '',
|
|
||||||
wait: 30
|
|
||||||
},
|
|
||||||
cmdRuleValidate: {
|
|
||||||
username: [{required: true, message: this.$t('username is required')}],
|
|
||||||
cmd: [{required: true, message: this.$t('command is required')}],
|
|
||||||
wait: [{validator: (rule, value, callback) => {
|
|
||||||
if (!value) {
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isInteger(value) || value < 0 || value > 30) {
|
const { t } = useI18n()
|
||||||
callback(new Error(this.$t('must be an integer between 0 and 30')))
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
const props = defineProps({
|
||||||
}}]
|
selection: Array
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const cmdForm = useTemplateRef('cmdForm')
|
||||||
|
const inputParam = useTemplateRef('inputParam')
|
||||||
|
|
||||||
|
const cmdModal = ref(false)
|
||||||
|
const inputParamVisible = ref(false)
|
||||||
|
const inputParamValue = ref('')
|
||||||
|
|
||||||
|
const cmdStatus = reactive({
|
||||||
|
total: 0,
|
||||||
|
modal: false,
|
||||||
|
execing: 0,
|
||||||
|
fail: 0,
|
||||||
|
respModal: false,
|
||||||
|
responses: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const cmdData = reactive({
|
||||||
|
username: '',
|
||||||
|
cmd: '',
|
||||||
|
params: [],
|
||||||
|
currentParam: '',
|
||||||
|
wait: 30
|
||||||
|
})
|
||||||
|
|
||||||
|
const cmdRuleValidate = {
|
||||||
|
username: [{required: true, message: t('username is required')}],
|
||||||
|
cmd: [{required: true, message: t('command is required')}],
|
||||||
|
wait: [{validator: (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
|
||||||
computed: {
|
if (!Number.isInteger(value) || value < 0 || value > 30) {
|
||||||
cmdStatusPercent() {
|
callback(new Error(t('must be an integer between 0 and 30')))
|
||||||
if (this.cmdStatus.total === 0)
|
|
||||||
return 0
|
|
||||||
return (this.cmdStatus.total - this.cmdStatus.execing) / this.cmdStatus.total * 100
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
showCmdForm() {
|
|
||||||
if (this.selection.length < 1) {
|
|
||||||
this.$message.error(this.$t('Please select the devices you want to operate'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.cmdModal = true
|
|
||||||
},
|
|
||||||
delCmdParam(tag) {
|
|
||||||
this.cmdData.params.splice(this.cmdData.params.indexOf(tag), 1)
|
|
||||||
},
|
|
||||||
showInputParam() {
|
|
||||||
this.inputParamVisible = true
|
|
||||||
this.$nextTick(() => {
|
|
||||||
(this.$refs.inputParam).focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleInputParamConfirm() {
|
|
||||||
const value = this.inputParamValue
|
|
||||||
if (value) {
|
|
||||||
this.cmdData.params.push(value)
|
|
||||||
}
|
|
||||||
this.inputParamVisible = false
|
|
||||||
this.inputParamValue = ''
|
|
||||||
},
|
|
||||||
doCmd() {
|
|
||||||
(this.$refs['cmdForm']).validate(valid => {
|
|
||||||
if (valid) {
|
|
||||||
const selection = this.selection.filter(item => item.proto > 4)
|
|
||||||
|
|
||||||
this.cmdModal = false
|
callback()
|
||||||
this.cmdStatus.modal = true
|
}}]
|
||||||
this.cmdStatus.total = selection.length
|
}
|
||||||
this.cmdStatus.execing = selection.length
|
|
||||||
this.cmdStatus.fail = 0
|
|
||||||
this.cmdStatus.responses = []
|
|
||||||
|
|
||||||
selection.forEach(item => {
|
const cmdStatusPercent = computed(() => {
|
||||||
const data = {
|
if (cmdStatus.total === 0)
|
||||||
username: this.cmdData.username,
|
return 0
|
||||||
cmd: this.cmdData.cmd,
|
return (cmdStatus.total - cmdStatus.execing) / cmdStatus.total * 100
|
||||||
params: this.cmdData.params
|
})
|
||||||
|
|
||||||
|
const showCmdForm = () => {
|
||||||
|
if (props.selection.length < 1) {
|
||||||
|
ElMessage.error(t('Please select the devices you want to operate'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmdModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const delCmdParam = (tag) => cmdData.params.splice(cmdData.params.indexOf(tag), 1)
|
||||||
|
|
||||||
|
const showInputParam = () => {
|
||||||
|
inputParamVisible.value = true
|
||||||
|
nextTick(() => inputParam.value.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputParamConfirm = () => {
|
||||||
|
const value = inputParamValue.value
|
||||||
|
if (value) {
|
||||||
|
cmdData.params.push(value)
|
||||||
|
}
|
||||||
|
inputParamVisible.value = false
|
||||||
|
inputParamValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const doCmd = () => {
|
||||||
|
cmdForm.value.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
const selection = props.selection.filter(item => item.proto > 4)
|
||||||
|
|
||||||
|
cmdModal.value = false
|
||||||
|
cmdStatus.modal = true
|
||||||
|
cmdStatus.total = selection.length
|
||||||
|
cmdStatus.execing = selection.length
|
||||||
|
cmdStatus.fail = 0
|
||||||
|
cmdStatus.responses = []
|
||||||
|
|
||||||
|
selection.forEach(item => {
|
||||||
|
const data = {
|
||||||
|
username: cmdData.username,
|
||||||
|
cmd: cmdData.cmd,
|
||||||
|
params: cmdData.params
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post(`/cmd/${item.id}?group=${item.group}&wait=${cmdData.wait}`, data).then((response) => {
|
||||||
|
if (cmdData.wait === 0) {
|
||||||
|
cmdStatus.responses.push({
|
||||||
|
err: 0,
|
||||||
|
msg: '',
|
||||||
|
id: item.id,
|
||||||
|
code: 0,
|
||||||
|
stdout: '',
|
||||||
|
stderr: ''
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const resp = response.data
|
||||||
|
|
||||||
|
if (resp.err && resp.err !== 0) {
|
||||||
|
cmdStatus.fail++
|
||||||
|
resp.stdout = ''
|
||||||
|
resp.stderr = ''
|
||||||
|
} else {
|
||||||
|
resp.stdout = window.atob(resp.stdout || '')
|
||||||
|
resp.stderr = window.atob(resp.stderr || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.axios.post(`/cmd/${item.id}?group=${item.group}&wait=${this.cmdData.wait}`, data).then((response) => {
|
resp.id = item.id
|
||||||
if (this.cmdData.wait === 0) {
|
cmdStatus.responses.push(resp)
|
||||||
this.cmdStatus.responses.push({
|
}
|
||||||
err: 0,
|
cmdStatus.execing--
|
||||||
msg: '',
|
})
|
||||||
id: item.id,
|
|
||||||
code: 0,
|
|
||||||
stdout: '',
|
|
||||||
stderr: ''
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const resp = response.data
|
|
||||||
|
|
||||||
if (resp.err && resp.err !== 0) {
|
|
||||||
this.cmdStatus.fail++
|
|
||||||
resp.stdout = ''
|
|
||||||
resp.stderr = ''
|
|
||||||
} else {
|
|
||||||
resp.stdout = window.atob(resp.stdout || '')
|
|
||||||
resp.stderr = window.atob(resp.stderr || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.id = item.id
|
|
||||||
this.cmdStatus.responses.push(resp)
|
|
||||||
}
|
|
||||||
this.cmdStatus.execing--
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
|
||||||
resetCmdData() {
|
|
||||||
(this.$refs['cmdForm']).resetFields()
|
|
||||||
},
|
|
||||||
ignoreCmdResp() {
|
|
||||||
this.cmdStatus.execing = 0
|
|
||||||
this.cmdStatus.respModal = true
|
|
||||||
this.cmdStatus.modal = false
|
|
||||||
},
|
|
||||||
showCmdResp() {
|
|
||||||
this.cmdStatus.modal = false
|
|
||||||
if (this.cmdStatus.responses.length > 0)
|
|
||||||
this.cmdStatus.respModal = true
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetCmdData = () => cmdForm.value.resetFields()
|
||||||
|
|
||||||
|
const ignoreCmdResp = () => {
|
||||||
|
cmdStatus.execing = 0
|
||||||
|
cmdStatus.respModal = true
|
||||||
|
cmdStatus.modal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCmdResp = () => {
|
||||||
|
cmdStatus.modal = false
|
||||||
|
if (cmdStatus.responses.length > 0)
|
||||||
|
cmdStatus.respModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
showCmdForm,
|
||||||
|
resetCmdData
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const startPos = ref(0)
|
|||||||
const startSizes = ref([])
|
const startSizes = ref([])
|
||||||
const containerRef = ref(null)
|
const containerRef = ref(null)
|
||||||
|
|
||||||
function initializePanelSizes() {
|
const initializePanelSizes = () => {
|
||||||
if (!props.config.panels) return
|
if (!props.config.panels) return
|
||||||
|
|
||||||
if (panelSizes.value.length !== props.config.panels.length) {
|
if (panelSizes.value.length !== props.config.panels.length) {
|
||||||
@@ -42,7 +42,7 @@ function initializePanelSizes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPanelStyle(index) {
|
const getPanelStyle = (index) => {
|
||||||
if (!props.config.panels) return {}
|
if (!props.config.panels) return {}
|
||||||
|
|
||||||
initializePanelSizes()
|
initializePanelSizes()
|
||||||
@@ -64,7 +64,7 @@ function getPanelStyle(index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startResize(index, event) {
|
const startResize = (index, event) => {
|
||||||
isResizing.value = true
|
isResizing.value = true
|
||||||
resizingIndex.value = index
|
resizingIndex.value = index
|
||||||
startPos.value = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
startPos.value = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||||
@@ -78,7 +78,7 @@ function startResize(index, event) {
|
|||||||
document.body.style.userSelect = 'none'
|
document.body.style.userSelect = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleResizeMove(event) {
|
const handleResizeMove = (event) => {
|
||||||
if (!isResizing.value) return
|
if (!isResizing.value) return
|
||||||
|
|
||||||
const currentPos = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
const currentPos = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||||
@@ -107,7 +107,7 @@ function handleResizeMove(event) {
|
|||||||
panelSizes.value = newSizes
|
panelSizes.value = newSizes
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopResize() {
|
const stopResize = () => {
|
||||||
isResizing.value = false
|
isResizing.value = false
|
||||||
resizingIndex.value = -1
|
resizingIndex.value = -1
|
||||||
containerRef.value = null
|
containerRef.value = null
|
||||||
@@ -120,17 +120,9 @@ function stopResize() {
|
|||||||
emit('resize')
|
emit('resize')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSplitPanel(panelId, position) {
|
const handleSplitPanel = (panelId, position) => emit('split', panelId, position)
|
||||||
emit('split', panelId, position)
|
const handleClosePanel = (panelId) => emit('close', panelId)
|
||||||
}
|
const handleResize = () => emit('resize')
|
||||||
|
|
||||||
function handleClosePanel(panelId) {
|
|
||||||
emit('close', panelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResize() {
|
|
||||||
emit('resize')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+338
-326
@@ -1,29 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-container">
|
<div class="terminal-container">
|
||||||
<div ref="terminal" class="terminal" @contextmenu.prevent="showContextmenu"></div>
|
<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-dialog v-model="fileCtx.modal" :title="$t('Upload file to device')" @close="onUploadDialogClosed" :width="400">
|
||||||
<el-upload :before-upload="beforeUpload" action="#">
|
<el-upload :before-upload="beforeUpload" action="#">
|
||||||
<el-button type="primary">{{ $t("Select file") }}</el-button>
|
<el-button type="primary">{{ $t("Select file") }}</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<p v-if="file.file !== null"> {{ file.file.name }}</p>
|
<p v-if="fileCtx.file !== null"> {{ fileCtx.file.name }}</p>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="file.modal = false">{{ $t('Cancel') }}</el-button>
|
<el-button @click="fileCtx.modal = false">{{ $t('Cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="doUploadFile">{{ $t('OK') }}</el-button>
|
<el-button type="primary" @click="doUploadFile">{{ $t('OK') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<contextmenu ref="contextmenu" :menus="contextmenus" @click="onContextmenuClick"/>
|
<ContextMenu v-model="contextmenuPos" :menus="contextmenus" @click="onContextmenuClick"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onUnmounted, nextTick, useTemplateRef } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import useClipboard from 'vue-clipboard3'
|
||||||
import { Terminal } from '@xterm/xterm'
|
import { Terminal } from '@xterm/xterm'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
|
|
||||||
import OverlayAddon from '../xterm-addon/xterm-addon-overlay'
|
import OverlayAddon from '../xterm-addon/xterm-addon-overlay'
|
||||||
import Contextmenu from '../components/ContextMenu.vue'
|
import ContextMenu from '../components/ContextMenu.vue'
|
||||||
|
|
||||||
import { ElLoading } from 'element-plus'
|
|
||||||
|
|
||||||
const LoginErrorOffline = 4000
|
const LoginErrorOffline = 4000
|
||||||
const LoginErrorBusy = 4001
|
const LoginErrorBusy = 4001
|
||||||
@@ -35,333 +38,342 @@ const ReadFileBlkSize = 63 * 1024
|
|||||||
|
|
||||||
const AckBlkSize = 4 * 1024
|
const AckBlkSize = 4 * 1024
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'RttyTerm',
|
devid: String,
|
||||||
components: {
|
panelId: String
|
||||||
'Contextmenu': Contextmenu
|
})
|
||||||
},
|
|
||||||
props: {
|
|
||||||
devid: String,
|
|
||||||
panelId: String
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
contextmenus: [
|
|
||||||
{name: 'copy', caption: this.$t('Copy - Ctrl+Insert')},
|
|
||||||
{name: 'paste', caption: this.$t('Paste - Shift+Insert')},
|
|
||||||
{name: 'clear', caption: this.$t('Clear Scrollback')},
|
|
||||||
{name: 'font+', caption: this.$t('font+')},
|
|
||||||
{name: 'font-', caption: this.$t('font-')},
|
|
||||||
{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')}
|
|
||||||
],
|
|
||||||
file: {
|
|
||||||
modal: false,
|
|
||||||
accepted: false,
|
|
||||||
file: null,
|
|
||||||
offset: 0,
|
|
||||||
fr: new FileReader(),
|
|
||||||
name: '',
|
|
||||||
chunks: []
|
|
||||||
},
|
|
||||||
disposables: [],
|
|
||||||
socket: null,
|
|
||||||
term: null,
|
|
||||||
fitAddon: null,
|
|
||||||
unack: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
showContextmenu(e) {
|
|
||||||
this.$refs.contextmenu.show(e)
|
|
||||||
},
|
|
||||||
onContextmenuClick(name) {
|
|
||||||
if (name === 'copy') {
|
|
||||||
const text = this.term.getSelection()
|
|
||||||
if (text) {
|
|
||||||
this.$copyText(text).then(() => {
|
|
||||||
this.$message.success(this.$t('Copied to clipboard'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (name === 'paste') {
|
|
||||||
this.pasteFromClipboard()
|
|
||||||
} else if (name === 'clear') {
|
|
||||||
this.term.clear()
|
|
||||||
} else if (name === 'font+') {
|
|
||||||
this.updateFontSize(1)
|
|
||||||
} else if (name === 'font-') {
|
|
||||||
this.updateFontSize(-1)
|
|
||||||
} else if (name === 'upload') {
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.term.focus()
|
const emit = defineEmits(['split', 'close'])
|
||||||
},
|
|
||||||
async pasteFromClipboard() {
|
|
||||||
try {
|
|
||||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
|
||||||
this.$message.info(this.$t('Please use shortcut "Shift+Insert"'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await navigator.clipboard.readText()
|
const router = useRouter()
|
||||||
if (text) {
|
const { t } = useI18n()
|
||||||
this.sendTermData(text)
|
const { toClipboard } = useClipboard()
|
||||||
this.$message.success(this.$t('Pasted from clipboard'))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'NotAllowedError') {
|
|
||||||
this.$alert(this.$t('clipboard_instructions'), this.$t('Clipboard Permission Required'),
|
|
||||||
{
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.$message.info(this.$t('Please use shortcut "Shift+Insert"'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateFontSize(size) {
|
|
||||||
this.term.options.fontSize += size
|
|
||||||
this.fitAddon.fit()
|
|
||||||
},
|
|
||||||
onUploadDialogClosed() {
|
|
||||||
this.term.focus()
|
|
||||||
if (this.file.accepted)
|
|
||||||
return
|
|
||||||
this.file.file = null
|
|
||||||
const msg = {type: 'fileCanceled'}
|
|
||||||
this.socket.send(JSON.stringify(msg))
|
|
||||||
},
|
|
||||||
beforeUpload(file) {
|
|
||||||
this.file.file = file
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
submitUploadFile() {
|
|
||||||
this.$refs.upload.submit()
|
|
||||||
},
|
|
||||||
sendFileInfo(file) {
|
|
||||||
const msg = {type: 'fileInfo', size: file.size, name: file.name}
|
|
||||||
this.socket.send(JSON.stringify(msg))
|
|
||||||
},
|
|
||||||
readFileBlob(fr, file, offset, size) {
|
|
||||||
const blob = file.slice(offset, offset + size)
|
|
||||||
fr.readAsArrayBuffer(blob)
|
|
||||||
},
|
|
||||||
doUploadFile() {
|
|
||||||
if (!this.file.file) {
|
|
||||||
this.onUploadDialogClosed()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.term.focus()
|
const terminal = useTemplateRef('terminal')
|
||||||
|
const contextmenuPos = ref(null)
|
||||||
|
|
||||||
if (this.file.size > 0xffffffff) {
|
const contextmenus = [
|
||||||
this.$message.error(this.$t('The file you will upload is too large(> 4294967295 Byte)'))
|
{name: 'copy', caption: t('Copy - Ctrl+Insert')},
|
||||||
return
|
{name: 'paste', caption: t('Paste - Shift+Insert')},
|
||||||
}
|
{name: 'clear', caption: t('Clear Scrollback')},
|
||||||
|
{name: 'font+', caption: t('font+')},
|
||||||
|
{name: 'font-', caption: t('font-')},
|
||||||
|
{name: 'upload', caption: t('Upload file') + ' - rtty -R'},
|
||||||
|
{name: 'download', caption: t('Download file') + ' - rtty -S file'},
|
||||||
|
{name: 'split-left', caption: t('split-left')},
|
||||||
|
{name: 'split-right', caption: t('split-right')},
|
||||||
|
{name: 'split-up', caption: t('split-up')},
|
||||||
|
{name: 'split-down', caption: t('split-down')},
|
||||||
|
{name: 'close', caption: t('Close')},
|
||||||
|
{name: 'about', caption: t('About')}
|
||||||
|
]
|
||||||
|
|
||||||
this.file.accepted = true
|
const fileCtx = reactive({
|
||||||
this.file.modal = false
|
modal: false,
|
||||||
|
accepted: false,
|
||||||
|
file: null,
|
||||||
|
offset: 0,
|
||||||
|
fr: new FileReader(),
|
||||||
|
name: '',
|
||||||
|
chunks: []
|
||||||
|
})
|
||||||
|
|
||||||
this.sendFileInfo(this.file.file)
|
let disposables = []
|
||||||
|
let socket = null
|
||||||
|
let term = null
|
||||||
|
let fitAddon = null
|
||||||
|
let unack = 0
|
||||||
|
|
||||||
if (this.file.size === 0) {
|
const copyText = async(text) => {
|
||||||
this.sendFileData(null)
|
try {
|
||||||
return
|
await toClipboard(text)
|
||||||
}
|
return Promise.resolve()
|
||||||
|
} catch (err) {
|
||||||
this.file.offset = 0
|
return Promise.reject(err)
|
||||||
|
|
||||||
const fr = this.file.fr
|
|
||||||
|
|
||||||
fr.onload = e => {
|
|
||||||
this.file.offset += e.loaded
|
|
||||||
this.sendFileData(new Uint8Array(fr.result))
|
|
||||||
}
|
|
||||||
this.readFileBlob(fr, this.file.file, this.file.offset, ReadFileBlkSize)
|
|
||||||
},
|
|
||||||
sendTermData(data) {
|
|
||||||
this.socket.send(new Uint8Array([0, ...new TextEncoder().encode(data)]))
|
|
||||||
},
|
|
||||||
sendFileData(data) {
|
|
||||||
let b
|
|
||||||
|
|
||||||
if (data !== null)
|
|
||||||
b = new Uint8Array([1, MsgTypeFileData, ...data])
|
|
||||||
else
|
|
||||||
b = new Uint8Array([1, MsgTypeFileData])
|
|
||||||
|
|
||||||
this.socket.send(b)
|
|
||||||
},
|
|
||||||
fitTerm() {
|
|
||||||
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({
|
|
||||||
cursorBlink: true,
|
|
||||||
fontSize: 16
|
|
||||||
})
|
|
||||||
this.term = term
|
|
||||||
|
|
||||||
const fitAddon = new FitAddon()
|
|
||||||
this.fitAddon = fitAddon
|
|
||||||
term.loadAddon(fitAddon)
|
|
||||||
|
|
||||||
const overlayAddon = new OverlayAddon()
|
|
||||||
term.loadAddon(overlayAddon)
|
|
||||||
|
|
||||||
term.open(this.$refs['terminal'])
|
|
||||||
term.focus()
|
|
||||||
|
|
||||||
this.disposables.push(term.onData(data => this.sendTermData(data)))
|
|
||||||
this.disposables.push(term.onBinary(data => this.sendTermData(data)))
|
|
||||||
|
|
||||||
this.disposables.push(term.onResize(size => {
|
|
||||||
const msg = {type: 'winsize', cols: size.cols, rows: size.rows}
|
|
||||||
this.socket.send(JSON.stringify(msg))
|
|
||||||
overlayAddon.show(term.cols + 'x' + term.rows)
|
|
||||||
}))
|
|
||||||
|
|
||||||
window.addEventListener('rtty-resize', this.fitTerm)
|
|
||||||
this.fitTerm()
|
|
||||||
},
|
|
||||||
dispose() {
|
|
||||||
this.disposables.forEach(d => d.dispose())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const loading = ElLoading.service({
|
|
||||||
lock: true,
|
|
||||||
text: this.$t('Requesting device to create terminal...'),
|
|
||||||
background: '#555',
|
|
||||||
customClass: 'rtty-loading'
|
|
||||||
})
|
|
||||||
|
|
||||||
const group = this.$route.query.group ?? ''
|
|
||||||
|
|
||||||
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'
|
|
||||||
|
|
||||||
const socket = new WebSocket(protocol + location.host + `/connect/${this.devid}?group=${group}`)
|
|
||||||
socket.binaryType = 'arraybuffer'
|
|
||||||
this.socket = socket
|
|
||||||
|
|
||||||
socket.addEventListener('close', (ev) => {
|
|
||||||
loading.close()
|
|
||||||
|
|
||||||
if (ev.code === LoginErrorOffline) {
|
|
||||||
this.$router.push('/error/offline')
|
|
||||||
} else if (ev.code === LoginErrorBusy) {
|
|
||||||
this.$router.push('/error/full')
|
|
||||||
} else if (ev.code === LoginErrorTimeout) {
|
|
||||||
this.$router.push('/error/timeout')
|
|
||||||
} else {
|
|
||||||
this.closed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener('error', () => {
|
|
||||||
loading.close()
|
|
||||||
|
|
||||||
let href = `/connect/${this.devid}`
|
|
||||||
if (group)
|
|
||||||
href += `?group=${group}`
|
|
||||||
window.location.href = href
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener('message', ev => {
|
|
||||||
const data = ev.data
|
|
||||||
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
const msg = JSON.parse(data)
|
|
||||||
if (msg.type === 'login') {
|
|
||||||
loading.close()
|
|
||||||
this.openTerm()
|
|
||||||
} else if (msg.type === 'sendfile') {
|
|
||||||
this.file.name = msg.name
|
|
||||||
this.file.chunks = []
|
|
||||||
socket.send(JSON.stringify({type: 'fileAck'}))
|
|
||||||
} else if (msg.type === 'recvfile') {
|
|
||||||
this.file.modal = true
|
|
||||||
this.file.file = null
|
|
||||||
this.file.accepted = false
|
|
||||||
this.term.blur()
|
|
||||||
} else if (msg.type === 'fileAck') {
|
|
||||||
if (this.file.file && this.file.offset < this.file.file.size)
|
|
||||||
this.readFileBlob(this.file.fr, this.file.file, this.file.offset, ReadFileBlkSize)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data = new Uint8Array(ev.data)
|
|
||||||
|
|
||||||
if (data[0] === 0) {
|
|
||||||
this.unack += data.length - 1
|
|
||||||
this.term.write(data.slice(1))
|
|
||||||
|
|
||||||
if (this.unack > AckBlkSize) {
|
|
||||||
const msg = {type: 'ack', ack: this.unack}
|
|
||||||
socket.send(JSON.stringify(msg))
|
|
||||||
this.unack = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (data.length === 1) {
|
|
||||||
const blob = new Blob(this.file.chunks)
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = this.file.name
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.file.chunks = []
|
|
||||||
document.body.removeChild(a)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
}, 100)
|
|
||||||
} else {
|
|
||||||
this.file.chunks.push(data.slice(1))
|
|
||||||
socket.send(JSON.stringify({type: 'fileAck'}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
window.removeEventListener('rtty-resize', this.fitTerm)
|
|
||||||
|
|
||||||
this.dispose()
|
|
||||||
if (this.term)
|
|
||||||
this.term.dispose()
|
|
||||||
|
|
||||||
if (this.socket)
|
|
||||||
this.socket.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showContextmenu = (e) => contextmenuPos.value = { x: e.clientX, y: e.clientY }
|
||||||
|
|
||||||
|
const onContextmenuClick = (name) => {
|
||||||
|
if (name === 'copy') {
|
||||||
|
const text = term.getSelection()
|
||||||
|
if (text)
|
||||||
|
copyText(text).then(() => ElMessage.success(t('Copied to clipboard')))
|
||||||
|
} else if (name === 'paste') {
|
||||||
|
pasteFromClipboard()
|
||||||
|
} else if (name === 'clear') {
|
||||||
|
term.clear()
|
||||||
|
} else if (name === 'font+') {
|
||||||
|
updateFontSize(1)
|
||||||
|
} else if (name === 'font-') {
|
||||||
|
updateFontSize(-1)
|
||||||
|
} else if (name === 'upload') {
|
||||||
|
ElMessage.success(t('Please execute command "rtty -R" in current terminal!'))
|
||||||
|
} else if (name === 'download') {
|
||||||
|
ElMessage.success(t('Please execute command "rtty -S file" in current terminal!'))
|
||||||
|
} else if (name === 'split-left') {
|
||||||
|
emit('split', props.panelId, 'left')
|
||||||
|
} else if (name === 'split-right') {
|
||||||
|
emit('split', props.panelId, 'right')
|
||||||
|
} else if (name === 'split-up') {
|
||||||
|
emit('split', props.panelId, 'up')
|
||||||
|
} else if (name === 'split-down') {
|
||||||
|
emit('split', props.panelId, 'down')
|
||||||
|
} else if (name === 'close') {
|
||||||
|
emit('close', props.panelId)
|
||||||
|
} else if (name === 'about') {
|
||||||
|
window.open('https://github.com/zhaojh329/rtty')
|
||||||
|
}
|
||||||
|
|
||||||
|
term.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pasteFromClipboard = async() => {
|
||||||
|
try {
|
||||||
|
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||||
|
ElMessage.info(t('Please use shortcut "Shift+Insert"'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await navigator.clipboard.readText()
|
||||||
|
if (text) {
|
||||||
|
sendTermData(text)
|
||||||
|
ElMessage.success(t('Pasted from clipboard'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'NotAllowedError') {
|
||||||
|
ElMessageBox.alert(t('clipboard_instructions'), t('Clipboard Permission Required'), {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage.info(t('Please use shortcut "Shift+Insert"'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFontSize = (size) => {
|
||||||
|
term.options.fontSize += size
|
||||||
|
fitAddon.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUploadDialogClosed = () => {
|
||||||
|
term.focus()
|
||||||
|
if (fileCtx.accepted)
|
||||||
|
return
|
||||||
|
fileCtx.file = null
|
||||||
|
const msg = {type: 'fileCanceled'}
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
fileCtx.file = file
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendFileInfo = (file) => {
|
||||||
|
const msg = {type: 'fileInfo', size: file.size, name: file.name}
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFileBlob = (fr, file, offset, size) => {
|
||||||
|
const blob = file.slice(offset, offset + size)
|
||||||
|
fr.readAsArrayBuffer(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doUploadFile = () => {
|
||||||
|
if (!fileCtx.file) {
|
||||||
|
onUploadDialogClosed()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
term.focus()
|
||||||
|
|
||||||
|
if (fileCtx.file.size > 0xffffffff) {
|
||||||
|
ElMessage.error(t('The file you will upload is too large(> 4294967295 Byte)'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileCtx.accepted = true
|
||||||
|
fileCtx.modal = false
|
||||||
|
|
||||||
|
sendFileInfo(fileCtx.file)
|
||||||
|
|
||||||
|
if (fileCtx.file.size === 0) {
|
||||||
|
sendFileData(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileCtx.offset = 0
|
||||||
|
|
||||||
|
const fr = fileCtx.fr
|
||||||
|
|
||||||
|
fr.onload = e => {
|
||||||
|
fileCtx.offset += e.loaded
|
||||||
|
sendFileData(new Uint8Array(fr.result))
|
||||||
|
}
|
||||||
|
readFileBlob(fr, fileCtx.file, fileCtx.offset, ReadFileBlkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendTermData = (data) => socket.send(new Uint8Array([0, ...new TextEncoder().encode(data)]))
|
||||||
|
|
||||||
|
const sendFileData = (data) => {
|
||||||
|
let b
|
||||||
|
|
||||||
|
if (data !== null)
|
||||||
|
b = new Uint8Array([1, MsgTypeFileData, ...data])
|
||||||
|
else
|
||||||
|
b = new Uint8Array([1, MsgTypeFileData])
|
||||||
|
|
||||||
|
socket.send(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fitTerm = () => nextTick(() => fitAddon.fit())
|
||||||
|
|
||||||
|
const closed = () => {
|
||||||
|
if (term)
|
||||||
|
term.write('\n\n\r\x1B[1;3;31mConnection is closed.\x1B[0m')
|
||||||
|
dispose()
|
||||||
|
emit('close', props.panelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openTerm = () => {
|
||||||
|
term = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
fontSize: 16
|
||||||
|
})
|
||||||
|
|
||||||
|
const fitAddonInstance = new FitAddon()
|
||||||
|
fitAddon = fitAddonInstance
|
||||||
|
term.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
const overlayAddon = new OverlayAddon()
|
||||||
|
term.loadAddon(overlayAddon)
|
||||||
|
|
||||||
|
term.open(terminal.value)
|
||||||
|
term.focus()
|
||||||
|
|
||||||
|
disposables.push(term.onData(data => sendTermData(data)))
|
||||||
|
disposables.push(term.onBinary(data => sendTermData(data)))
|
||||||
|
|
||||||
|
disposables.push(term.onResize(size => {
|
||||||
|
const msg = {type: 'winsize', cols: size.cols, rows: size.rows}
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
overlayAddon.show(term.cols + 'x' + term.rows)
|
||||||
|
}))
|
||||||
|
|
||||||
|
window.addEventListener('rtty-resize', fitTerm)
|
||||||
|
fitTerm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispose = () => disposables.forEach(d => d.dispose())
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: t('Requesting device to create terminal...'),
|
||||||
|
background: '#555',
|
||||||
|
customClass: 'rtty-loading'
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const group = route.query.group ?? ''
|
||||||
|
|
||||||
|
const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'
|
||||||
|
|
||||||
|
socket = new WebSocket(protocol + location.host + `/connect/${props.devid}?group=${group}`)
|
||||||
|
socket.binaryType = 'arraybuffer'
|
||||||
|
|
||||||
|
socket.addEventListener('close', (ev) => {
|
||||||
|
loading.close()
|
||||||
|
|
||||||
|
if (ev.code === LoginErrorOffline) {
|
||||||
|
router.push('/error/offline')
|
||||||
|
} else if (ev.code === LoginErrorBusy) {
|
||||||
|
router.push('/error/full')
|
||||||
|
} else if (ev.code === LoginErrorTimeout) {
|
||||||
|
router.push('/error/timeout')
|
||||||
|
} else {
|
||||||
|
closed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.addEventListener('error', () => {
|
||||||
|
loading.close()
|
||||||
|
|
||||||
|
let href = `/connect/${props.devid}`
|
||||||
|
if (group)
|
||||||
|
href += `?group=${group}`
|
||||||
|
window.location.href = href
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.addEventListener('message', ev => {
|
||||||
|
const data = ev.data
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const msg = JSON.parse(data)
|
||||||
|
if (msg.type === 'login') {
|
||||||
|
loading.close()
|
||||||
|
openTerm()
|
||||||
|
} else if (msg.type === 'sendfile') {
|
||||||
|
fileCtx.name = msg.name
|
||||||
|
fileCtx.chunks = []
|
||||||
|
socket.send(JSON.stringify({type: 'fileAck'}))
|
||||||
|
} else if (msg.type === 'recvfile') {
|
||||||
|
fileCtx.modal = true
|
||||||
|
fileCtx.file = null
|
||||||
|
fileCtx.accepted = false
|
||||||
|
term.blur()
|
||||||
|
} else if (msg.type === 'fileAck') {
|
||||||
|
if (fileCtx.file && fileCtx.offset < fileCtx.file.size)
|
||||||
|
readFileBlob(fileCtx.fr, fileCtx.file, fileCtx.offset, ReadFileBlkSize)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const data = new Uint8Array(ev.data)
|
||||||
|
|
||||||
|
if (data[0] === 0) {
|
||||||
|
unack += data.length - 1
|
||||||
|
term.write(data.slice(1))
|
||||||
|
|
||||||
|
if (unack > AckBlkSize) {
|
||||||
|
const msg = {type: 'ack', ack: unack}
|
||||||
|
socket.send(JSON.stringify(msg))
|
||||||
|
unack = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data.length === 1) {
|
||||||
|
const blob = new Blob(fileCtx.chunks)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = fileCtx.name
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fileCtx.chunks = []
|
||||||
|
document.body.removeChild(a)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
fileCtx.chunks.push(data.slice(1))
|
||||||
|
socket.send(JSON.stringify({type: 'fileAck'}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('rtty-resize', fitTerm)
|
||||||
|
|
||||||
|
dispose()
|
||||||
|
if (term)
|
||||||
|
term.dispose()
|
||||||
|
|
||||||
|
if (socket)
|
||||||
|
socket.close()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+98
-100
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="modal" :title="$t('Access your devices\'s Web')" width="300">
|
<el-dialog v-model="model" :title="$t('Access your devices\'s Web')" width="300">
|
||||||
<el-form ref="form" :label-width="80" label-position="left" :model="formData" :rules="ruleValidate">
|
<el-form ref="form" :label-width="80" label-position="left" :model="formData" :rules="ruleValidate">
|
||||||
<el-form-item :label="$t('Proto')" prop="proto">
|
<el-form-item :label="$t('Proto')" prop="proto">
|
||||||
<el-radio-group v-model="formData.proto">
|
<el-radio-group v-model="formData.proto">
|
||||||
@@ -19,119 +19,117 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="modal = false">{{ $t('Cancel') }}</el-button>
|
<el-button @click="model = false">{{ $t('Cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="open">{{ $t('OK') }}</el-button>
|
<el-button type="primary" @click="open">{{ $t('OK') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { reactive, useTemplateRef } from 'vue'
|
||||||
name: 'RttyWeb',
|
import { useI18n } from 'vue-i18n'
|
||||||
data() {
|
import { ElMessage } from 'element-plus'
|
||||||
return {
|
|
||||||
modal: false,
|
|
||||||
formData: {
|
|
||||||
proto: 'http',
|
|
||||||
ipaddr: '',
|
|
||||||
port: null,
|
|
||||||
path: ''
|
|
||||||
},
|
|
||||||
ruleValidate: {
|
|
||||||
ipaddr: [{validator: (rule, value, callback) => {
|
|
||||||
if (!value) {
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isValidIP(value)) {
|
const props = defineProps({
|
||||||
callback(new Error(this.$t('Invalid IP address')))
|
dev: Object
|
||||||
}
|
})
|
||||||
|
|
||||||
callback()
|
const { t } = useI18n()
|
||||||
}}],
|
|
||||||
port: [{validator: (rule, value, callback) => {
|
|
||||||
if (!value) {
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Number.isInteger(value) || value < 1 || value > 65536) {
|
const form = useTemplateRef('form')
|
||||||
callback(new Error(this.$t('Invalid port')))
|
const model = defineModel()
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
const formData = reactive({
|
||||||
}}],
|
proto: 'http',
|
||||||
path: [{validator: (rule, value, callback) => {
|
ipaddr: '',
|
||||||
if (!value) {
|
port: null,
|
||||||
callback()
|
path: ''
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (!value.startsWith('/')) {
|
const isValidIP = (addr) => {
|
||||||
callback(new Error(this.$t('Must start with /')))
|
const ipv4Pattern = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
|
||||||
}
|
return new RegExp(ipv4Pattern).test(addr)
|
||||||
|
}
|
||||||
|
|
||||||
callback()
|
const ruleValidate = {
|
||||||
}}]
|
ipaddr: [{validator: (rule, value, callback) => {
|
||||||
},
|
if (!value) {
|
||||||
group: '',
|
callback()
|
||||||
devid: '',
|
return
|
||||||
devProto: null
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
show(dev) {
|
|
||||||
this.group = dev.group
|
|
||||||
this.devid = dev.id
|
|
||||||
this.devProto = dev.proto
|
|
||||||
this.formData.proto = 'http'
|
|
||||||
this.formData.ipaddr = ''
|
|
||||||
this.formData.port = null
|
|
||||||
this.formData.path = ''
|
|
||||||
this.modal = true
|
|
||||||
},
|
|
||||||
isValidIP(addr) {
|
|
||||||
const ipv4Pattern = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
|
|
||||||
return new RegExp(ipv4Pattern).test(addr)
|
|
||||||
},
|
|
||||||
open() {
|
|
||||||
this.$refs.form.validate(valid => {
|
|
||||||
if (!valid)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (this.devProto < 4 && this.formData.proto === 'https') {
|
if (!isValidIP(value)) {
|
||||||
this.$message.error(this.$t('Your device\'s rtty does not support https proxy, please upgrade it.'))
|
callback(new Error(t('Invalid IP address')))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.modal = false
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const proto = this.formData.proto
|
|
||||||
let ipaddr = this.formData.ipaddr
|
|
||||||
let port = this.formData.port
|
|
||||||
let path = this.formData.path
|
|
||||||
|
|
||||||
if (!ipaddr)
|
|
||||||
ipaddr = '127.0.0.1'
|
|
||||||
|
|
||||||
if (!port)
|
|
||||||
port = proto === 'https' ? 443 : 80
|
|
||||||
|
|
||||||
if (!path)
|
|
||||||
path = '/'
|
|
||||||
|
|
||||||
const addr = encodeURIComponent(`${ipaddr}:${port}${path}`)
|
|
||||||
|
|
||||||
if (this.group)
|
|
||||||
window.open(`/web2/${this.group}/${this.devid}/${proto}/${addr}`)
|
|
||||||
else
|
|
||||||
window.open(`/web/${this.devid}/${proto}/${addr}`)
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
callback()
|
||||||
|
}}],
|
||||||
|
port: [{validator: (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(value) || value < 1 || value > 65536) {
|
||||||
|
callback(new Error(t('Invalid port')))
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}}],
|
||||||
|
path: [{validator: (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
callback(new Error(t('Must start with /')))
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
form.value.validate(valid => {
|
||||||
|
if (!valid)
|
||||||
|
return
|
||||||
|
|
||||||
|
const dev = props.dev
|
||||||
|
|
||||||
|
if (dev.proto < 4 && formData.proto === 'https') {
|
||||||
|
ElMessage.error(t('Your device\'s rtty does not support https proxy, please upgrade it.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
model.value = false
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const proto = formData.proto
|
||||||
|
let ipaddr = formData.ipaddr
|
||||||
|
let port = formData.port
|
||||||
|
let path = formData.path
|
||||||
|
|
||||||
|
if (!ipaddr)
|
||||||
|
ipaddr = '127.0.0.1'
|
||||||
|
|
||||||
|
if (!port)
|
||||||
|
port = proto === 'https' ? 443 : 80
|
||||||
|
|
||||||
|
if (!path)
|
||||||
|
path = '/'
|
||||||
|
|
||||||
|
const addr = encodeURIComponent(`${ipaddr}:${port}${path}`)
|
||||||
|
|
||||||
|
const group = dev.group
|
||||||
|
const devid = dev.id
|
||||||
|
|
||||||
|
if (group)
|
||||||
|
window.open(`/web2/${group}/${devid}/${proto}/${addr}`)
|
||||||
|
else
|
||||||
|
window.open(`/web/${devid}/${proto}/${addr}`)
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import VueClipboard from 'vue-clipboard2'
|
|
||||||
import VueAxios from 'vue-axios'
|
import VueAxios from 'vue-axios'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
@@ -14,7 +13,6 @@ import ElementPlus from './element-plus'
|
|||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(VueClipboard)
|
|
||||||
app.use(VueAxios, axios)
|
app.use(VueAxios, axios)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|||||||
+30
-32
@@ -8,40 +8,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { Warning as WarningIcon } from '@vicons/ionicons5'
|
import { Warning as WarningIcon } from '@vicons/ionicons5'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n()
|
||||||
name: 'Error',
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
err: String
|
err: String
|
||||||
},
|
})
|
||||||
components: {
|
|
||||||
WarningIcon
|
const title = computed(() => {
|
||||||
},
|
const err = props.err
|
||||||
computed: {
|
if (err === 'offline')
|
||||||
title() {
|
return t('Device Unavailable')
|
||||||
const err = this.err
|
else if (err === 'full')
|
||||||
if (err === 'offline')
|
return t('Terminal Session Limit Reached')
|
||||||
return this.$t('Device Unavailable')
|
else if (err === 'timeout')
|
||||||
else if (err === 'full')
|
return t('Device Response Timeout')
|
||||||
return this.$t('Terminal Session Limit Reached')
|
return ''
|
||||||
else if (err === 'timeout')
|
})
|
||||||
return this.$t('Device Response Timeout')
|
|
||||||
return ''
|
const message = computed(() => {
|
||||||
},
|
const err = props.err
|
||||||
message() {
|
if (err === 'offline')
|
||||||
const err = this.err
|
return t('The device is currently offline. Please check the device status and try again.')
|
||||||
if (err === 'offline')
|
else if (err === 'full')
|
||||||
return this.$t('The device is currently offline. Please check the device status and try again.')
|
return t('The maximum number of concurrent terminal sessions has been reached. Please try again later.')
|
||||||
else if (err === 'full')
|
else if (err === 'timeout')
|
||||||
return this.$t('The maximum number of concurrent terminal sessions has been reached. Please try again later.')
|
return t('The device did not respond to the terminal session request within the expected time. Please check the device status and try again.')
|
||||||
else if (err === 'timeout')
|
return ''
|
||||||
return this.$t('The device did not respond to the terminal session request within the expected time. Please check the device status and try again.')
|
})
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+109
-109
@@ -47,130 +47,130 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-main>
|
</el-main>
|
||||||
<RttyCmd ref="rttyCmd" :selection="selection"/>
|
<RttyCmd ref="rttyCmd" :selection="selection"/>
|
||||||
<RttyWeb ref="rttyWeb"/>
|
<RttyWeb v-model="web.modal" :dev="web.dev"/>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted, useTemplateRef } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { InternetExplorer as IEIcon } from '@vicons/fa'
|
import { InternetExplorer as IEIcon } from '@vicons/fa'
|
||||||
import { Terminal as TerminalIcon } from '@vicons/ionicons5'
|
import { Terminal as TerminalIcon } from '@vicons/ionicons5'
|
||||||
import RttyCmd from '../components/RttyCmd.vue'
|
import RttyCmd from '../components/RttyCmd.vue'
|
||||||
import RttyWeb from '../components/RttyWeb.vue'
|
import RttyWeb from '../components/RttyWeb.vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
const router = useRouter()
|
||||||
name: 'Home',
|
|
||||||
components: {
|
|
||||||
IEIcon,
|
|
||||||
TerminalIcon,
|
|
||||||
RttyCmd,
|
|
||||||
RttyWeb
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
group: '',
|
|
||||||
groups: [],
|
|
||||||
filterString: '',
|
|
||||||
loading: true,
|
|
||||||
devlists: [],
|
|
||||||
filteredDevices: [],
|
|
||||||
selection: [],
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pagedevlists() {
|
|
||||||
return this.filteredDevices.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatTime(t) {
|
|
||||||
let ts = t || 0
|
|
||||||
let tm = 0
|
|
||||||
let th = 0
|
|
||||||
let td = 0
|
|
||||||
|
|
||||||
if (ts > 59) {
|
const rttyCmd = useTemplateRef('rttyCmd')
|
||||||
tm = Math.floor(ts / 60)
|
|
||||||
ts = ts % 60
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tm > 59) {
|
const group = ref('')
|
||||||
th = Math.floor(tm / 60)
|
const groups = ref([])
|
||||||
tm = tm % 60
|
const filterString = ref('')
|
||||||
}
|
const loading = ref(true)
|
||||||
|
const devlists = ref([])
|
||||||
|
const filteredDevices = ref([])
|
||||||
|
const selection = ref([])
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const web = reactive({
|
||||||
|
modal: false,
|
||||||
|
dev: null
|
||||||
|
})
|
||||||
|
|
||||||
if (th > 23) {
|
const pagedevlists = computed(() => {
|
||||||
td = Math.floor(th / 24)
|
return filteredDevices.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
|
||||||
th = th % 24
|
})
|
||||||
}
|
|
||||||
|
|
||||||
let s = ''
|
const formatTime = (t) => {
|
||||||
|
let ts = t || 0
|
||||||
|
let tm = 0
|
||||||
|
let th = 0
|
||||||
|
let td = 0
|
||||||
|
|
||||||
if (td > 0)
|
if (ts > 59) {
|
||||||
s = `${td}d `
|
tm = Math.floor(ts / 60)
|
||||||
|
ts = ts % 60
|
||||||
return s + `${th}h ${tm}m ${ts}s`
|
|
||||||
},
|
|
||||||
handlePageChange(page, size) {
|
|
||||||
this.currentPage = page
|
|
||||||
this.pageSize = size
|
|
||||||
},
|
|
||||||
handleLogout() {
|
|
||||||
this.axios.get('/signout').then(() => {
|
|
||||||
this.$router.push('/login')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleSearch() {
|
|
||||||
this.filteredDevices = this.devlists.filter((d) => {
|
|
||||||
const filterString = this.filterString.toLowerCase()
|
|
||||||
return d.id.toLowerCase().indexOf(filterString) > -1 || d.description.toLowerCase().indexOf(filterString) > -1
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getGroups() {
|
|
||||||
this.axios.get('/groups').then(res => {
|
|
||||||
this.groups = res.data
|
|
||||||
if (this.groups.indexOf(this.group) === -1)
|
|
||||||
this.group = this.groups[0]
|
|
||||||
this.getDevices()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getDevices() {
|
|
||||||
this.axios.get(`/devs?group=${this.group}`).then(res => {
|
|
||||||
this.loading = false
|
|
||||||
this.devlists = res.data
|
|
||||||
this.selection = []
|
|
||||||
this.handleSearch()
|
|
||||||
}).catch(() => {
|
|
||||||
this.$router.push('/login')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleRefresh() {
|
|
||||||
this.loading = true
|
|
||||||
setTimeout(() => {
|
|
||||||
this.getGroups()
|
|
||||||
}, 500)
|
|
||||||
},
|
|
||||||
handleSelection(selection) {
|
|
||||||
this.selection = selection
|
|
||||||
},
|
|
||||||
connectDevice(devid) {
|
|
||||||
let url = `/rtty/${devid}`
|
|
||||||
if (this.group)
|
|
||||||
url += `?group=${this.group}`
|
|
||||||
window.open(url)
|
|
||||||
},
|
|
||||||
connectDeviceWeb(dev) {
|
|
||||||
this.$refs.rttyWeb.show(dev)
|
|
||||||
},
|
|
||||||
showCmdForm() {
|
|
||||||
this.$refs.rttyCmd.showCmdForm()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getGroups()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tm > 59) {
|
||||||
|
th = Math.floor(tm / 60)
|
||||||
|
tm = tm % 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if (th > 23) {
|
||||||
|
td = Math.floor(th / 24)
|
||||||
|
th = th % 24
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = ''
|
||||||
|
|
||||||
|
if (td > 0)
|
||||||
|
s = `${td}d `
|
||||||
|
|
||||||
|
return s + `${th}h ${tm}m ${ts}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page, size) => {
|
||||||
|
currentPage.value = page
|
||||||
|
pageSize.value = size
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
axios.get('/signout').then(() => {
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
filteredDevices.value = devlists.value.filter((d) => {
|
||||||
|
const filterStr = filterString.value.toLowerCase()
|
||||||
|
return d.id.toLowerCase().indexOf(filterStr) > -1 || d.description.toLowerCase().indexOf(filterStr) > -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGroups = () => {
|
||||||
|
axios.get('/groups').then(res => {
|
||||||
|
groups.value = res.data
|
||||||
|
if (groups.value.indexOf(group.value) === -1)
|
||||||
|
group.value = groups.value[0]
|
||||||
|
getDevices()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDevices = () => {
|
||||||
|
axios.get(`/devs?group=${group.value}`).then(res => {
|
||||||
|
loading.value = false
|
||||||
|
devlists.value = res.data
|
||||||
|
selection.value = []
|
||||||
|
handleSearch()
|
||||||
|
}).catch(() => {
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loading.value = true
|
||||||
|
setTimeout(() => getGroups(), 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelection = (sel) => selection.value = sel
|
||||||
|
|
||||||
|
const connectDevice = (devid) => {
|
||||||
|
let url = `/rtty/${devid}`
|
||||||
|
if (group.value)
|
||||||
|
url += `?group=${group.value}`
|
||||||
|
window.open(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectDeviceWeb = (dev) => {
|
||||||
|
web.dev = dev
|
||||||
|
web.modal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCmdForm = () => rttyCmd.value.showCmdForm()
|
||||||
|
|
||||||
|
onMounted(() => getGroups())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
+23
-21
@@ -14,29 +14,31 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, reactive } from 'vue'
|
||||||
data() {
|
import { useRouter } from 'vue-router'
|
||||||
return {
|
import { useI18n } from 'vue-i18n'
|
||||||
loading: false,
|
import { ElMessage } from 'element-plus'
|
||||||
formValue: {
|
import axios from 'axios'
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleSubmit() {
|
|
||||||
const params = {
|
|
||||||
password: this.formValue.password
|
|
||||||
}
|
|
||||||
|
|
||||||
this.axios.post('/signin', params).then(() => {
|
const { t } = useI18n()
|
||||||
this.$router.push('/')
|
const router = useRouter()
|
||||||
}).catch(() => {
|
|
||||||
this.$message.error(this.$t('Signin Fail! password wrong.'))
|
const loading = ref(false)
|
||||||
})
|
const formValue = reactive({
|
||||||
}
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const params = {
|
||||||
|
password: formValue.password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
axios.post('/signin', params).then(() => {
|
||||||
|
router.push('/')
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error(t('Signin Fail! password wrong.'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+8
-12
@@ -36,7 +36,7 @@ const terms = computed(() => {
|
|||||||
return ids
|
return ids
|
||||||
})
|
})
|
||||||
|
|
||||||
function moveTerminalsToPool() {
|
const moveTerminalsToPool = () => {
|
||||||
const pool = document.getElementById('terminal-pool')
|
const pool = document.getElementById('terminal-pool')
|
||||||
|
|
||||||
terms.value.forEach(termId => {
|
terms.value.forEach(termId => {
|
||||||
@@ -45,7 +45,7 @@ function moveTerminalsToPool() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveTerminalsToPlaceholders() {
|
const moveTerminalsToPlaceholders = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
terms.value.forEach(termId => {
|
terms.value.forEach(termId => {
|
||||||
const term = document.querySelector(`[data-terminal-id="${termId}"]`)
|
const term = document.querySelector(`[data-terminal-id="${termId}"]`)
|
||||||
@@ -59,23 +59,19 @@ function moveTerminalsToPlaceholders() {
|
|||||||
|
|
||||||
onMounted(() => moveTerminalsToPlaceholders())
|
onMounted(() => moveTerminalsToPlaceholders())
|
||||||
|
|
||||||
function handleResize() {
|
const handleResize = () => dispatchEventRttyResize()
|
||||||
dispatchEventRttyResize()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
function dispatchEventRttyResize() {
|
const dispatchEventRttyResize = () => window.dispatchEvent(new CustomEvent('rtty-resize'))
|
||||||
window.dispatchEvent(new CustomEvent('rtty-resize'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClosePanel(panelId) {
|
const handleClosePanel = (panelId) => {
|
||||||
moveTerminalsToPool()
|
moveTerminalsToPool()
|
||||||
deletePanel(rootConfig.value, panelId, 0)
|
deletePanel(rootConfig.value, panelId, 0)
|
||||||
moveTerminalsToPlaceholders()
|
moveTerminalsToPlaceholders()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePanel(config, panelId, index, parent) {
|
const deletePanel = (config, panelId, index, parent) => {
|
||||||
if (parent && config.id === panelId) {
|
if (parent && config.id === panelId) {
|
||||||
parent.panels.splice(index, 1)
|
parent.panels.splice(index, 1)
|
||||||
if (parent.panels.length === 1) {
|
if (parent.panels.length === 1) {
|
||||||
@@ -90,13 +86,13 @@ function deletePanel(config, panelId, index, parent) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSplitPanel(panelId, direction) {
|
const handleSplitPanel = (panelId, direction) => {
|
||||||
moveTerminalsToPool()
|
moveTerminalsToPool()
|
||||||
splitPanel(rootConfig.value, panelId, 0, direction)
|
splitPanel(rootConfig.value, panelId, 0, direction)
|
||||||
moveTerminalsToPlaceholders()
|
moveTerminalsToPlaceholders()
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitPanel(config, panelId, index, position, parent) {
|
const splitPanel = (config, panelId, index, position, parent) => {
|
||||||
if (config.id === panelId) {
|
if (config.id === panelId) {
|
||||||
const direction = (position === 'left' || position === 'right') ? 'horizontal' : 'vertical'
|
const direction = (position === 'left' || position === 'right') ? 'horizontal' : 'vertical'
|
||||||
const newId = 'panel-' + Math.random().toString(36).substring(2, 10)
|
const newId = 'panel-' + Math.random().toString(36).substring(2, 10)
|
||||||
|
|||||||
Reference in New Issue
Block a user