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",
|
||||
"vue": "^3.5.16",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-clipboard2": "^0.3.3",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<a @click="onMenuClick(item.name)" :style="{'text-decoration': item.underline ? 'underline' : 'none'}">
|
||||
{{item.caption || item.name}}
|
||||
@@ -9,75 +9,77 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
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
|
||||
<script setup>
|
||||
import { reactive, watch, nextTick, onBeforeUnmount, useTemplateRef } from 'vue'
|
||||
|
||||
if (e.clientX >= this.axis.x && e.clientX <= this.axis.x + el.clientWidth &&
|
||||
e.clientY >= this.axis.y && e.clientY <= this.axis.y + el.clientHeight) {
|
||||
return
|
||||
}
|
||||
defineProps({
|
||||
menus: Array
|
||||
})
|
||||
|
||||
this.visibility = false
|
||||
},
|
||||
show(e) {
|
||||
document.addEventListener('mousedown', this.close)
|
||||
const model = defineModel()
|
||||
|
||||
this.axis = {x: e.clientX, y: e.clientY}
|
||||
this.visibility = true
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.content
|
||||
if (!el) return
|
||||
const content = useTemplateRef('content')
|
||||
const axis = reactive({ x: 0, y: 0 })
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const close = (e) => {
|
||||
const el = content.value
|
||||
|
||||
let x = e.clientX
|
||||
let y = e.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)
|
||||
|
||||
this.axis = {x, y}
|
||||
})
|
||||
},
|
||||
onMenuClick(name) {
|
||||
this.visibility = false
|
||||
this.$emit('click', name)
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('mousedown', this.close)
|
||||
if (e.clientX >= axis.x && e.clientX <= axis.x + el.clientWidth &&
|
||||
e.clientY >= axis.y && e.clientY <= axis.y + el.clientHeight) {
|
||||
return
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+141
-130
@@ -53,145 +53,156 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RttyCmd',
|
||||
props: {
|
||||
selection: Array
|
||||
},
|
||||
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
|
||||
}
|
||||
<script setup>
|
||||
import { ref, reactive, computed, nextTick, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
if (!Number.isInteger(value) || value < 0 || value > 30) {
|
||||
callback(new Error(this.$t('must be an integer between 0 and 30')))
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
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: {
|
||||
cmdStatusPercent() {
|
||||
if (this.cmdStatus.total === 0)
|
||||
return 0
|
||||
return (this.cmdStatus.total - this.cmdStatus.execing) / this.cmdStatus.total * 100
|
||||
|
||||
if (!Number.isInteger(value) || value < 0 || value > 30) {
|
||||
callback(new Error(t('must be an integer between 0 and 30')))
|
||||
}
|
||||
},
|
||||
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
|
||||
this.cmdStatus.modal = true
|
||||
this.cmdStatus.total = selection.length
|
||||
this.cmdStatus.execing = selection.length
|
||||
this.cmdStatus.fail = 0
|
||||
this.cmdStatus.responses = []
|
||||
callback()
|
||||
}}]
|
||||
}
|
||||
|
||||
selection.forEach(item => {
|
||||
const data = {
|
||||
username: this.cmdData.username,
|
||||
cmd: this.cmdData.cmd,
|
||||
params: this.cmdData.params
|
||||
const cmdStatusPercent = computed(() => {
|
||||
if (cmdStatus.total === 0)
|
||||
return 0
|
||||
return (cmdStatus.total - cmdStatus.execing) / cmdStatus.total * 100
|
||||
})
|
||||
|
||||
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) => {
|
||||
if (this.cmdData.wait === 0) {
|
||||
this.cmdStatus.responses.push({
|
||||
err: 0,
|
||||
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--
|
||||
})
|
||||
})
|
||||
}
|
||||
resp.id = item.id
|
||||
cmdStatus.responses.push(resp)
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -33,7 +33,7 @@ const startPos = ref(0)
|
||||
const startSizes = ref([])
|
||||
const containerRef = ref(null)
|
||||
|
||||
function initializePanelSizes() {
|
||||
const initializePanelSizes = () => {
|
||||
if (!props.config.panels) return
|
||||
|
||||
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 {}
|
||||
|
||||
initializePanelSizes()
|
||||
@@ -64,7 +64,7 @@ function getPanelStyle(index) {
|
||||
}
|
||||
}
|
||||
|
||||
function startResize(index, event) {
|
||||
const startResize = (index, event) => {
|
||||
isResizing.value = true
|
||||
resizingIndex.value = index
|
||||
startPos.value = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||
@@ -78,7 +78,7 @@ function startResize(index, event) {
|
||||
document.body.style.userSelect = 'none'
|
||||
}
|
||||
|
||||
function handleResizeMove(event) {
|
||||
const handleResizeMove = (event) => {
|
||||
if (!isResizing.value) return
|
||||
|
||||
const currentPos = props.config.direction === 'horizontal' ? event.clientX : event.clientY
|
||||
@@ -107,7 +107,7 @@ function handleResizeMove(event) {
|
||||
panelSizes.value = newSizes
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
const stopResize = () => {
|
||||
isResizing.value = false
|
||||
resizingIndex.value = -1
|
||||
containerRef.value = null
|
||||
@@ -120,17 +120,9 @@ function stopResize() {
|
||||
emit('resize')
|
||||
}
|
||||
|
||||
function handleSplitPanel(panelId, position) {
|
||||
emit('split', panelId, position)
|
||||
}
|
||||
|
||||
function handleClosePanel(panelId) {
|
||||
emit('close', panelId)
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
emit('resize')
|
||||
}
|
||||
const handleSplitPanel = (panelId, position) => emit('split', panelId, position)
|
||||
const handleClosePanel = (panelId) => emit('close', panelId)
|
||||
const handleResize = () => emit('resize')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+338
-326
@@ -1,29 +1,32 @@
|
||||
<template>
|
||||
<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-dialog v-model="fileCtx.modal" :title="$t('Upload file to device')" @close="onUploadDialogClosed" :width="400">
|
||||
<el-upload :before-upload="beforeUpload" action="#">
|
||||
<el-button type="primary">{{ $t("Select file") }}</el-button>
|
||||
</el-upload>
|
||||
<p v-if="file.file !== null"> {{ file.file.name }}</p>
|
||||
<p v-if="fileCtx.file !== null"> {{ fileCtx.file.name }}</p>
|
||||
<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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<contextmenu ref="contextmenu" :menus="contextmenus" @click="onContextmenuClick"/>
|
||||
<ContextMenu v-model="contextmenuPos" :menus="contextmenus" @click="onContextmenuClick"/>
|
||||
</div>
|
||||
</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 { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
import OverlayAddon from '../xterm-addon/xterm-addon-overlay'
|
||||
import Contextmenu from '../components/ContextMenu.vue'
|
||||
|
||||
import { ElLoading } from 'element-plus'
|
||||
import ContextMenu from '../components/ContextMenu.vue'
|
||||
|
||||
const LoginErrorOffline = 4000
|
||||
const LoginErrorBusy = 4001
|
||||
@@ -35,333 +38,342 @@ const ReadFileBlkSize = 63 * 1024
|
||||
|
||||
const AckBlkSize = 4 * 1024
|
||||
|
||||
export default {
|
||||
name: 'RttyTerm',
|
||||
components: {
|
||||
'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')
|
||||
}
|
||||
const props = defineProps({
|
||||
devid: String,
|
||||
panelId: String
|
||||
})
|
||||
|
||||
this.term.focus()
|
||||
},
|
||||
async pasteFromClipboard() {
|
||||
try {
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
this.$message.info(this.$t('Please use shortcut "Shift+Insert"'))
|
||||
return
|
||||
}
|
||||
const emit = defineEmits(['split', 'close'])
|
||||
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
this.sendTermData(text)
|
||||
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
|
||||
}
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { toClipboard } = useClipboard()
|
||||
|
||||
this.term.focus()
|
||||
const terminal = useTemplateRef('terminal')
|
||||
const contextmenuPos = ref(null)
|
||||
|
||||
if (this.file.size > 0xffffffff) {
|
||||
this.$message.error(this.$t('The file you will upload is too large(> 4294967295 Byte)'))
|
||||
return
|
||||
}
|
||||
const contextmenus = [
|
||||
{name: 'copy', caption: t('Copy - Ctrl+Insert')},
|
||||
{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
|
||||
this.file.modal = false
|
||||
const fileCtx = reactive({
|
||||
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) {
|
||||
this.sendFileData(null)
|
||||
return
|
||||
}
|
||||
|
||||
this.file.offset = 0
|
||||
|
||||
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 copyText = async(text) => {
|
||||
try {
|
||||
await toClipboard(text)
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+98
-100
@@ -1,5 +1,5 @@
|
||||
<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-item :label="$t('Proto')" prop="proto">
|
||||
<el-radio-group v-model="formData.proto">
|
||||
@@ -19,119 +19,117 @@
|
||||
</el-form>
|
||||
<template #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>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RttyWeb',
|
||||
data() {
|
||||
return {
|
||||
modal: false,
|
||||
formData: {
|
||||
proto: 'http',
|
||||
ipaddr: '',
|
||||
port: null,
|
||||
path: ''
|
||||
},
|
||||
ruleValidate: {
|
||||
ipaddr: [{validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
<script setup>
|
||||
import { reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
if (!this.isValidIP(value)) {
|
||||
callback(new Error(this.$t('Invalid IP address')))
|
||||
}
|
||||
const props = defineProps({
|
||||
dev: Object
|
||||
})
|
||||
|
||||
callback()
|
||||
}}],
|
||||
port: [{validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
if (!Number.isInteger(value) || value < 1 || value > 65536) {
|
||||
callback(new Error(this.$t('Invalid port')))
|
||||
}
|
||||
const form = useTemplateRef('form')
|
||||
const model = defineModel()
|
||||
|
||||
callback()
|
||||
}}],
|
||||
path: [{validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const formData = reactive({
|
||||
proto: 'http',
|
||||
ipaddr: '',
|
||||
port: null,
|
||||
path: ''
|
||||
})
|
||||
|
||||
if (!value.startsWith('/')) {
|
||||
callback(new Error(this.$t('Must start with /')))
|
||||
}
|
||||
const 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)
|
||||
}
|
||||
|
||||
callback()
|
||||
}}]
|
||||
},
|
||||
group: '',
|
||||
devid: '',
|
||||
devProto: null
|
||||
const ruleValidate = {
|
||||
ipaddr: [{validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
},
|
||||
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') {
|
||||
this.$message.error(this.$t('Your device\'s rtty does not support https proxy, please upgrade it.'))
|
||||
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)
|
||||
})
|
||||
if (!isValidIP(value)) {
|
||||
callback(new Error(t('Invalid IP address')))
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import VueAxios from 'vue-axios'
|
||||
import axios from 'axios'
|
||||
import App from './App.vue'
|
||||
@@ -14,7 +13,6 @@ import ElementPlus from './element-plus'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(VueClipboard)
|
||||
app.use(VueAxios, axios)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
+30
-32
@@ -8,40 +8,38 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { Warning as WarningIcon } from '@vicons/ionicons5'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
name: 'Error',
|
||||
props: {
|
||||
err: String
|
||||
},
|
||||
components: {
|
||||
WarningIcon
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
const err = this.err
|
||||
if (err === 'offline')
|
||||
return this.$t('Device Unavailable')
|
||||
else if (err === 'full')
|
||||
return this.$t('Terminal Session Limit Reached')
|
||||
else if (err === 'timeout')
|
||||
return this.$t('Device Response Timeout')
|
||||
return ''
|
||||
},
|
||||
message() {
|
||||
const err = this.err
|
||||
if (err === 'offline')
|
||||
return this.$t('The device is currently offline. Please check the device status and try again.')
|
||||
else if (err === 'full')
|
||||
return this.$t('The maximum number of concurrent terminal sessions has been reached. Please try again later.')
|
||||
else if (err === 'timeout')
|
||||
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 ''
|
||||
}
|
||||
}
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
err: String
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
const err = props.err
|
||||
if (err === 'offline')
|
||||
return t('Device Unavailable')
|
||||
else if (err === 'full')
|
||||
return t('Terminal Session Limit Reached')
|
||||
else if (err === 'timeout')
|
||||
return t('Device Response Timeout')
|
||||
return ''
|
||||
})
|
||||
|
||||
const message = computed(() => {
|
||||
const err = props.err
|
||||
if (err === 'offline')
|
||||
return t('The device is currently offline. Please check the device status and try again.')
|
||||
else if (err === 'full')
|
||||
return t('The maximum number of concurrent terminal sessions has been reached. Please try again later.')
|
||||
else if (err === 'timeout')
|
||||
return 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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+109
-109
@@ -47,130 +47,130 @@
|
||||
</el-card>
|
||||
</el-main>
|
||||
<RttyCmd ref="rttyCmd" :selection="selection"/>
|
||||
<RttyWeb ref="rttyWeb"/>
|
||||
<RttyWeb v-model="web.modal" :dev="web.dev"/>
|
||||
</el-container>
|
||||
</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 { Terminal as TerminalIcon } from '@vicons/ionicons5'
|
||||
import RttyCmd from '../components/RttyCmd.vue'
|
||||
import RttyWeb from '../components/RttyWeb.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
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
|
||||
const router = useRouter()
|
||||
|
||||
if (ts > 59) {
|
||||
tm = Math.floor(ts / 60)
|
||||
ts = ts % 60
|
||||
}
|
||||
const rttyCmd = useTemplateRef('rttyCmd')
|
||||
|
||||
if (tm > 59) {
|
||||
th = Math.floor(tm / 60)
|
||||
tm = tm % 60
|
||||
}
|
||||
const group = ref('')
|
||||
const groups = ref([])
|
||||
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) {
|
||||
td = Math.floor(th / 24)
|
||||
th = th % 24
|
||||
}
|
||||
const pagedevlists = computed(() => {
|
||||
return filteredDevices.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)
|
||||
})
|
||||
|
||||
let s = ''
|
||||
const formatTime = (t) => {
|
||||
let ts = t || 0
|
||||
let tm = 0
|
||||
let th = 0
|
||||
let td = 0
|
||||
|
||||
if (td > 0)
|
||||
s = `${td}d `
|
||||
|
||||
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 (ts > 59) {
|
||||
tm = Math.floor(ts / 60)
|
||||
ts = ts % 60
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+23
-21
@@ -14,29 +14,31 @@
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formValue: {
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSubmit() {
|
||||
const params = {
|
||||
password: this.formValue.password
|
||||
}
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
this.axios.post('/signin', params).then(() => {
|
||||
this.$router.push('/')
|
||||
}).catch(() => {
|
||||
this.$message.error(this.$t('Signin Fail! password wrong.'))
|
||||
})
|
||||
}
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
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>
|
||||
|
||||
|
||||
+8
-12
@@ -36,7 +36,7 @@ const terms = computed(() => {
|
||||
return ids
|
||||
})
|
||||
|
||||
function moveTerminalsToPool() {
|
||||
const moveTerminalsToPool = () => {
|
||||
const pool = document.getElementById('terminal-pool')
|
||||
|
||||
terms.value.forEach(termId => {
|
||||
@@ -45,7 +45,7 @@ function moveTerminalsToPool() {
|
||||
})
|
||||
}
|
||||
|
||||
function moveTerminalsToPlaceholders() {
|
||||
const moveTerminalsToPlaceholders = () => {
|
||||
nextTick(() => {
|
||||
terms.value.forEach(termId => {
|
||||
const term = document.querySelector(`[data-terminal-id="${termId}"]`)
|
||||
@@ -59,23 +59,19 @@ function moveTerminalsToPlaceholders() {
|
||||
|
||||
onMounted(() => moveTerminalsToPlaceholders())
|
||||
|
||||
function handleResize() {
|
||||
dispatchEventRttyResize()
|
||||
}
|
||||
const handleResize = () => dispatchEventRttyResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
function dispatchEventRttyResize() {
|
||||
window.dispatchEvent(new CustomEvent('rtty-resize'))
|
||||
}
|
||||
const dispatchEventRttyResize = () => window.dispatchEvent(new CustomEvent('rtty-resize'))
|
||||
|
||||
function handleClosePanel(panelId) {
|
||||
const handleClosePanel = (panelId) => {
|
||||
moveTerminalsToPool()
|
||||
deletePanel(rootConfig.value, panelId, 0)
|
||||
moveTerminalsToPlaceholders()
|
||||
}
|
||||
|
||||
function deletePanel(config, panelId, index, parent) {
|
||||
const deletePanel = (config, panelId, index, parent) => {
|
||||
if (parent && config.id === panelId) {
|
||||
parent.panels.splice(index, 1)
|
||||
if (parent.panels.length === 1) {
|
||||
@@ -90,13 +86,13 @@ function deletePanel(config, panelId, index, parent) {
|
||||
return false
|
||||
}
|
||||
|
||||
function handleSplitPanel(panelId, direction) {
|
||||
const handleSplitPanel = (panelId, direction) => {
|
||||
moveTerminalsToPool()
|
||||
splitPanel(rootConfig.value, panelId, 0, direction)
|
||||
moveTerminalsToPlaceholders()
|
||||
}
|
||||
|
||||
function splitPanel(config, panelId, index, position, parent) {
|
||||
const 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)
|
||||
|
||||
Reference in New Issue
Block a user