mirror of
https://github.com/veops/oneterm.git
synced 2026-06-03 19:30:03 +08:00
feat(ui):add rdp&vnc (#15)
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"enquire.js": "^2.1.6",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"guacamole-common-js": "1.4.0-a",
|
||||
"is-buffer": "^2.0.5",
|
||||
"jquery": "^3.6.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
|
||||
@@ -38,7 +38,13 @@ export default {
|
||||
return enUS
|
||||
},
|
||||
isOpsFullScreen() {
|
||||
return ['cmdb_screen', 'oneterm_terminal', 'oneterm_replay'].includes(this.$route.name)
|
||||
return [
|
||||
'cmdb_screen',
|
||||
'oneterm_terminal',
|
||||
'oneterm_replay',
|
||||
'oneterm_guacamole',
|
||||
'oneterm_replay_guacamole',
|
||||
].includes(this.$route.name)
|
||||
},
|
||||
isOpsOnlyTopMenu() {
|
||||
return ['fullscreen_index', 'setting_person', 'notice_center'].includes(this.$route.name)
|
||||
|
||||
@@ -7,9 +7,13 @@ export function closeConnect(session_id) {
|
||||
})
|
||||
}
|
||||
|
||||
export function postConnectIsRight(asset_id, account_id, protocol) {
|
||||
export function postConnectIsRight(asset_id, account_id, protocol, query = null) {
|
||||
let url = `/oneterm/v1/connect/${asset_id}/${account_id}/${protocol}`
|
||||
if (query) {
|
||||
url = `${url}?${query}`
|
||||
}
|
||||
return axios({
|
||||
url: `/oneterm/v1/connect/${asset_id}/${account_id}/${protocol}`,
|
||||
url,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,6 +140,14 @@ const oneterm_en = {
|
||||
param: 'Param',
|
||||
before: 'Before',
|
||||
after: 'After',
|
||||
},
|
||||
guacamole: {
|
||||
play: 'click to play',
|
||||
idle: 'Initializing...',
|
||||
connecting: 'Connecting...',
|
||||
waiting: 'Waiting...',
|
||||
connected: 'Connected',
|
||||
disconnect: 'Disconnect'
|
||||
}
|
||||
}
|
||||
export default oneterm_en
|
||||
|
||||
@@ -140,6 +140,14 @@ const oneterm_zh = {
|
||||
param: '属性',
|
||||
before: '操作前',
|
||||
after: '操作后',
|
||||
},
|
||||
guacamole: {
|
||||
play: '点击播放',
|
||||
idle: '正在初始化中...',
|
||||
connecting: '正在努力连接中...',
|
||||
waiting: '正在等待服务器响应...',
|
||||
connected: '连接成功',
|
||||
disconnect: '连接已关闭'
|
||||
}
|
||||
}
|
||||
export default oneterm_zh
|
||||
|
||||
@@ -97,12 +97,26 @@ const genOnetermRoutes = () => {
|
||||
component: () => import('../views/terminal'),
|
||||
meta: { title: '终端', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/oneterm/guacamole/:asset_id/:account_id/:protocol',
|
||||
name: 'oneterm_guacamole',
|
||||
hidden: true,
|
||||
component: () => import('../views/terminal/guacamoleClient.vue'),
|
||||
meta: { title: '终端', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/oneterm/replay/:session_id',
|
||||
name: 'oneterm_replay',
|
||||
hidden: true,
|
||||
component: () => import('../views/replay'),
|
||||
meta: { title: '回放', icon: 'ops-itsm-servicedesk', selectedIcon: 'ops-itsm-servicedesk-selected', keepAlive: false }
|
||||
meta: { title: '回放', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/oneterm/replay/guacamole/:session_id',
|
||||
name: 'oneterm_replay_guacamole',
|
||||
hidden: true,
|
||||
component: () => import('../views/replay/guacamoleReplay.vue'),
|
||||
meta: { title: '回放', keepAlive: false }
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,3 +17,32 @@ export const setLocalStorage = (name, param) => {
|
||||
}
|
||||
localStorage.setItem(name, JSON.stringify(storageData))
|
||||
}
|
||||
|
||||
class Strings {
|
||||
hasText = function (text) {
|
||||
return !(text === undefined || text === null || text.length === 0)
|
||||
}
|
||||
|
||||
zeroPad = function zeroPad(num, minLength) {
|
||||
let str = num.toString()
|
||||
while (str.length < minLength) { str = '0' + str }
|
||||
return str
|
||||
};
|
||||
}
|
||||
|
||||
export const strings = new Strings()
|
||||
|
||||
class Times {
|
||||
formatTime = function formatTime(millis) {
|
||||
const totalSeconds = Math.floor(millis / 1000)
|
||||
|
||||
// Split into seconds and minutes
|
||||
const seconds = totalSeconds % 60
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
|
||||
// Format seconds and minutes as MM:SS
|
||||
return strings.zeroPad(minutes, 2) + ':' + strings.zeroPad(seconds, 2)
|
||||
};
|
||||
}
|
||||
|
||||
export const times = new Times()
|
||||
|
||||
@@ -118,15 +118,13 @@ export default {
|
||||
return this.$t(`oneterm.assetList.editAsset`)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
getNodeList().then((res) => {
|
||||
this.nodeList = res?.data?.list || []
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
setAsset(asset, type) {
|
||||
this.visible = true
|
||||
this.type = type
|
||||
getNodeList().then((res) => {
|
||||
this.nodeList = res?.data?.list || []
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
const {
|
||||
id = null,
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
prop="type_id"
|
||||
:style="{ display: 'flex', alignItems: 'center' }"
|
||||
>
|
||||
<CMDBTypeSelect v-model="syncForm.type_id" selectType="ci_type" @select="changeTypeId" />
|
||||
<CMDBTypeSelect v-model="syncForm.type_id" selectType="ci_type" @change="changeTypeId" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item :label="$t('oneterm.fieldMap')">
|
||||
<div v-for="item in fieldMap" :key="item.id">
|
||||
@@ -105,7 +105,7 @@
|
||||
</treeselect>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="item.error" style="color:red">{{ `${$t(`placeholder2`)}` }}</span>
|
||||
<span v-if="item.error" style="color: red">{{ `${$t(`placeholder2`)}` }}</span>
|
||||
</div>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item :label="$t('oneterm.filter')" class="cmdb-value-filter">
|
||||
@@ -215,7 +215,7 @@ export default {
|
||||
setNode(node, type) {
|
||||
this.visible = true
|
||||
this.type = type
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(async () => {
|
||||
const params = {}
|
||||
if (node?.id) {
|
||||
params.no_self_child = node.id
|
||||
@@ -247,26 +247,26 @@ export default {
|
||||
enable,
|
||||
frequency,
|
||||
}
|
||||
|
||||
this.changeTypeId({ id: type_id })
|
||||
this.fieldMap =
|
||||
JSON.stringify(mapping) === '{}'
|
||||
? [
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'name', label: this.$t('oneterm.name') },
|
||||
},
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'ip', label: 'IP' },
|
||||
},
|
||||
]
|
||||
: Object.keys(mapping).map((key) => {
|
||||
return {
|
||||
field_name: mapping[key],
|
||||
attribute: { value: key, label: this.fieldMapObj[key] },
|
||||
}
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
this.fieldMap =
|
||||
JSON.stringify(mapping) === '{}'
|
||||
? [
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'name', label: this.$t('oneterm.name') },
|
||||
},
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'ip', label: 'IP' },
|
||||
},
|
||||
]
|
||||
: Object.keys(mapping).map((key) => {
|
||||
return {
|
||||
field_name: mapping[key],
|
||||
attribute: { value: key, label: this.fieldMapObj[key] },
|
||||
}
|
||||
})
|
||||
})
|
||||
this.filterExp = filters
|
||||
this.$nextTick(() => {
|
||||
this.$refs.filterComp.visibleChange(true, false)
|
||||
@@ -276,12 +276,24 @@ export default {
|
||||
this.$refs.accessAuth.setValues(access_auth)
|
||||
})
|
||||
},
|
||||
async changeTypeId({ id }) {
|
||||
async changeTypeId(id) {
|
||||
this.fieldMap = [
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'name', label: this.$t('oneterm.name') },
|
||||
},
|
||||
{
|
||||
field_name: undefined,
|
||||
attribute: { value: 'ip', label: 'IP' },
|
||||
},
|
||||
]
|
||||
if (id) {
|
||||
await getCITypeAttributesById(id).then((res) => {
|
||||
const { attributes } = res
|
||||
this.attributes = attributes
|
||||
})
|
||||
} else {
|
||||
this.attributes = []
|
||||
}
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
|
||||
@@ -94,11 +94,15 @@ export default {
|
||||
if (valid) {
|
||||
this.handleCancel()
|
||||
const { account_id, protocol } = this.loginForm
|
||||
postConnectIsRight(this.asset_id, account_id, protocol).then((res) => {
|
||||
if (res?.data?.session_id) {
|
||||
window.open(`/oneterm/terminal?session_id=${res?.data?.session_id}`, '_blank')
|
||||
}
|
||||
})
|
||||
if (protocol.includes('rdp') || protocol.includes('vnc')) {
|
||||
window.open(`/oneterm/guacamole/${this.asset_id}/${account_id}/${protocol}`, '_blank')
|
||||
} else {
|
||||
postConnectIsRight(this.asset_id, account_id, protocol).then((res) => {
|
||||
if (res?.data?.session_id) {
|
||||
window.open(`/oneterm/terminal?session_id=${res?.data?.session_id}`, '_blank')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
<a-form-model-item :label="$t('oneterm.protocol')">
|
||||
<div class="protocol-box" v-for="(pro, index) in protocols" :key="pro.id">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="pro.value" style="width: 100px">
|
||||
<a-select v-model="pro.value" style="width: 100px" @change="(value) => changeProValue(value, index)">
|
||||
<a-select-option value="ssh">
|
||||
ssh
|
||||
</a-select-option>
|
||||
<a-select-option value="rdp">
|
||||
rdp
|
||||
</a-select-option>
|
||||
<a-select-option value="vnc">
|
||||
vnc
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input :placeholder="$t('oneterm.assetList.protocolPlaceholder')" v-model="pro.label" style="width: calc(100% - 100px)" />
|
||||
<a-input
|
||||
:placeholder="$t('oneterm.assetList.protocolPlaceholder')"
|
||||
v-model="pro.label"
|
||||
style="width: calc(100% - 100px)"
|
||||
/>
|
||||
</a-input-group>
|
||||
<a-space>
|
||||
<a @click="addPro"><a-icon type="plus-circle"/></a>
|
||||
@@ -64,6 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getGatewayList } from '../../../api/gateway'
|
||||
export default {
|
||||
@@ -105,6 +116,19 @@ export default {
|
||||
}))
|
||||
: [{ id: uuidv4(), value: 'ssh', label: '22' }]
|
||||
},
|
||||
changeProValue(value, index) {
|
||||
const _pro = _.cloneDeep(this.protocols[index])
|
||||
if (value === 'rdp') {
|
||||
_pro.label = '3389'
|
||||
}
|
||||
if (value === 'vnc') {
|
||||
_pro.label = '5900'
|
||||
}
|
||||
if (value === 'ssh') {
|
||||
_pro.label = '22'
|
||||
}
|
||||
this.$set(this.protocols, index, _pro)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
241
oneterm-ui/src/modules/oneterm/views/replay/guacamoleReplay.vue
Normal file
241
oneterm-ui/src/modules/oneterm/views/replay/guacamoleReplay.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="oneterm-guacamole-replay-wrapper">
|
||||
<div class="oneterm-guacamole-replay" id="display"></div>
|
||||
<span
|
||||
v-if="waiting"
|
||||
@click="
|
||||
() => {
|
||||
waiting = false
|
||||
handlePlayPause()
|
||||
}
|
||||
"
|
||||
>{{ $t('oneterm.guacamole.play') }}</span
|
||||
>
|
||||
<a-row
|
||||
v-else
|
||||
class="oneterm-guacamole-replay-progress"
|
||||
type="flex"
|
||||
justify="space-around"
|
||||
align="middle"
|
||||
:gutter="[5, 5]"
|
||||
>
|
||||
<a-col><a-icon @click="handlePlayPause" :type="playBtnIcon"/></a-col>
|
||||
<a-col flex="1">
|
||||
<a-slider
|
||||
:tip-formatter="formatter"
|
||||
:max="max"
|
||||
:value="percent"
|
||||
@change="handleProgressChange"
|
||||
/></a-col>
|
||||
<a-col>
|
||||
<a-select
|
||||
size="small"
|
||||
:style="{ width: '90px' }"
|
||||
:value="speed"
|
||||
@change="
|
||||
(value) => {
|
||||
speed = value
|
||||
if (value === 1) {
|
||||
stopSpeedUp()
|
||||
} else {
|
||||
startSpeedUp()
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<a-select-option :value="1">
|
||||
1倍速
|
||||
</a-select-option>
|
||||
<a-select-option :value="1.25">
|
||||
1.25倍速
|
||||
</a-select-option>
|
||||
<a-select-option :value="1.5">
|
||||
1.5倍速
|
||||
</a-select-option>
|
||||
<a-select-option :value="2">
|
||||
2倍速
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col>{{ position }}/ {{ duration }}</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Guacamole from 'guacamole-common-js'
|
||||
import { times } from '../../utils'
|
||||
|
||||
export default {
|
||||
name: 'GuacamoleReplay',
|
||||
data() {
|
||||
return {
|
||||
recording: null,
|
||||
percent: 0,
|
||||
speed: 1,
|
||||
max: 0,
|
||||
position: '00:00',
|
||||
duration: '00:00',
|
||||
waiting: true,
|
||||
timer: null,
|
||||
playBtnIcon: 'play-circle',
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
if (this.recording) {
|
||||
this.recording.disconnect()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const { session_id } = this.$route.params
|
||||
const RECORDING_URL = `/api/oneterm/v1/session/replay/${session_id}`
|
||||
const tunnel = new Guacamole.StaticHTTPTunnel(RECORDING_URL)
|
||||
tunnel.onstatechange = this.onTunnelStateChange
|
||||
const recording = new Guacamole.SessionRecording(tunnel)
|
||||
const recordingDisplay = recording.getDisplay()
|
||||
const display = document.getElementById('display')
|
||||
display.appendChild(recordingDisplay.getElement())
|
||||
recording.connect()
|
||||
|
||||
// If playing, the play/pause button should read "Pause"
|
||||
recording.onplay = () => {
|
||||
this.playBtnIcon = 'pause-circle'
|
||||
}
|
||||
|
||||
// If paused, the play/pause button should read "Play"
|
||||
recording.onpause = () => {
|
||||
this.playBtnIcon = 'play-circle'
|
||||
}
|
||||
|
||||
recording.onseek = (millis) => {
|
||||
this.percent = millis
|
||||
this.position = times.formatTime(millis)
|
||||
}
|
||||
|
||||
recording.onprogress = (millis) => {
|
||||
this.max = millis
|
||||
this.duration = times.formatTime(millis)
|
||||
}
|
||||
|
||||
this.recording = recording
|
||||
},
|
||||
onTunnelStateChange(state) {
|
||||
switch (state) {
|
||||
case Guacamole.Tunnel.State.OPEN:
|
||||
this.handlePlayPause()
|
||||
break
|
||||
case Guacamole.Tunnel.State.CLOSED:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
handlePlayPause() {
|
||||
if (this.percent === this.max) {
|
||||
// 重播
|
||||
this.percent = 0
|
||||
this.recording.seek(0, () => {
|
||||
this.recording.play()
|
||||
this.startSpeedUp()
|
||||
})
|
||||
}
|
||||
if (!this.recording.isPlaying()) {
|
||||
this.recording.play()
|
||||
this.startSpeedUp()
|
||||
} else {
|
||||
this.recording.pause()
|
||||
this.stopSpeedUp()
|
||||
this.$message.info('暂停')
|
||||
}
|
||||
},
|
||||
startSpeedUp() {
|
||||
this.stopSpeedUp()
|
||||
if (this.speed === 1) {
|
||||
return
|
||||
}
|
||||
if (!this.recording.isPlaying()) {
|
||||
return
|
||||
}
|
||||
const add_time = 100
|
||||
const delay = 1000 / (1000 / add_time) / (this.speed - 1)
|
||||
|
||||
const max = this.recording.getDuration()
|
||||
const current = this.recording.getPosition()
|
||||
if (current >= max) {
|
||||
return
|
||||
}
|
||||
this.recording.seek(current + add_time, () => {
|
||||
this.timer = setTimeout(this.startSpeedUp, delay)
|
||||
})
|
||||
},
|
||||
stopSpeedUp() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
},
|
||||
handleProgressChange(value) {
|
||||
this.recording.seek(value, () => {
|
||||
console.log('complete')
|
||||
})
|
||||
},
|
||||
formatter(value) {
|
||||
return `${times.formatTime(value)}`
|
||||
},
|
||||
onWindowResize() {
|
||||
const { recording } = this
|
||||
const width = recording.getDisplay().getWidth()
|
||||
const height = recording.getDisplay().getHeight()
|
||||
|
||||
const winWidth = window.innerWidth
|
||||
const winHeight = window.innerHeight - 40
|
||||
|
||||
const scaleW = winWidth / width
|
||||
const scaleH = winHeight / height
|
||||
|
||||
const scale = Math.min(scaleW, scaleH)
|
||||
if (!scale) {
|
||||
return
|
||||
}
|
||||
recording.getDisplay().scale(scale)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.oneterm-guacamole-replay-wrapper {
|
||||
background: black;
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.oneterm-guacamole-replay-progress {
|
||||
width: 100%;
|
||||
background: black;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
.oneterm-guacamole-replay-wrapper,
|
||||
.oneterm-guacamole-replay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -230,10 +230,22 @@ export default {
|
||||
return time
|
||||
},
|
||||
openReplay(row) {
|
||||
window.open(`/oneterm/replay/${row.session_id}`, '_blank')
|
||||
if (row.protocol.includes('rdp') || row.protocol.includes('vnc')) {
|
||||
window.open(`/oneterm/replay/guacamole/${row.session_id}`, '_blank')
|
||||
} else {
|
||||
window.open(`/oneterm/replay/${row.session_id}`, '_blank')
|
||||
}
|
||||
},
|
||||
openMonitor(row) {
|
||||
window.open(`/oneterm/terminal?session_id=${row.session_id}&&is_monitor=true`, '_blank')
|
||||
if (row.protocol.includes('rdp') || row.protocol.includes('vnc')) {
|
||||
const { asset_id, account_id, protocol } = row
|
||||
window.open(
|
||||
`/oneterm/guacamole/${asset_id}/${account_id}/${protocol}?session_id=${row.session_id}&&is_monitor=true`,
|
||||
'_blank'
|
||||
)
|
||||
} else {
|
||||
window.open(`/oneterm/terminal?session_id=${row.session_id}&&is_monitor=true`, '_blank')
|
||||
}
|
||||
},
|
||||
disconnect(row) {
|
||||
this.loading = true
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="oneterm-guacamole" id="display"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Guacamole from 'guacamole-common-js'
|
||||
import { postConnectIsRight } from '../../api/connect'
|
||||
|
||||
const STATE_IDLE = 0
|
||||
const STATE_CONNECTING = 1
|
||||
const STATE_WAITING = 2
|
||||
const STATE_CONNECTED = 3
|
||||
const STATE_DISCONNECTING = 4
|
||||
const STATE_DISCONNECTED = 5
|
||||
|
||||
export default {
|
||||
name: 'GuacamoleClient',
|
||||
data() {
|
||||
return {
|
||||
client: null,
|
||||
session_id: null,
|
||||
is_monitor: this.$route.query.is_monitor,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
const guacamoleClient = document.getElementById('display')
|
||||
const { asset_id, account_id, protocol } = this.$route.params
|
||||
const { session_id, is_monitor } = this.$route.query
|
||||
if (session_id) {
|
||||
this.session_id = session_id
|
||||
this.init()
|
||||
} else {
|
||||
postConnectIsRight(
|
||||
asset_id,
|
||||
account_id,
|
||||
protocol,
|
||||
`w=${guacamoleClient.clientWidth}&h=${guacamoleClient.clientHeight}&dpi=${96}`
|
||||
).then((res) => {
|
||||
this.session_id = res?.data?.session_id
|
||||
if (this.session_id) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
if (this.client) {
|
||||
this.client.disconnect()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const { session_id, is_monitor } = this
|
||||
const tunnel = new Guacamole.WebSocketTunnel(
|
||||
is_monitor
|
||||
? `ws://${document.location.host}/api/oneterm/v1/connect/monitor/${session_id}`
|
||||
: `ws://${document.location.host}/api/oneterm/v1/connect/${session_id}`
|
||||
)
|
||||
const client = new Guacamole.Client(tunnel)
|
||||
|
||||
// 处理从虚拟机收到的剪贴板内容
|
||||
// client.onclipboard = handleClipboardReceived
|
||||
|
||||
// 处理客户端的状态变化事件
|
||||
client.onstatechange = (state) => {
|
||||
this.onClientStateChange(state)
|
||||
}
|
||||
|
||||
client.onerror = this.onError
|
||||
client.ondisconnect = this.onDisconnect
|
||||
tunnel.onerror = this.onError
|
||||
|
||||
// Get display div from document
|
||||
const displayEle = document.getElementById('display')
|
||||
|
||||
// Add client to display div
|
||||
const element = client.getDisplay().getElement()
|
||||
displayEle.appendChild(element)
|
||||
|
||||
client.connect(`w=${displayEle.clientWidth}&h=${displayEle.clientHeight}&dpi=96`)
|
||||
const display = client.getDisplay()
|
||||
display.onresize = function(width, height) {
|
||||
display.scale(Math.min(window.innerHeight / display.getHeight(), window.innerWidth / display.getHeight()))
|
||||
}
|
||||
|
||||
const sink = new Guacamole.InputSink()
|
||||
displayEle.appendChild(sink.getElement())
|
||||
sink.focus()
|
||||
|
||||
const keyboard = new Guacamole.Keyboard(sink.getElement())
|
||||
|
||||
keyboard.onkeydown = (keysym) => {
|
||||
client.sendKeyEvent(1, keysym)
|
||||
if (keysym === 65288) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
keyboard.onkeyup = (keysym) => {
|
||||
client.sendKeyEvent(0, keysym)
|
||||
}
|
||||
|
||||
const sinkFocus = _.debounce(() => {
|
||||
sink.focus()
|
||||
})
|
||||
|
||||
const mouse = new Guacamole.Mouse(element)
|
||||
|
||||
mouse.onmousedown = mouse.onmouseup = function(mouseState) {
|
||||
sinkFocus()
|
||||
if (!is_monitor) {
|
||||
client.sendMouseState(mouseState)
|
||||
}
|
||||
}
|
||||
mouse.onmousemove = function(mouseState) {
|
||||
sinkFocus()
|
||||
client.getDisplay().showCursor(false)
|
||||
mouseState.x = mouseState.x / display.getScale()
|
||||
mouseState.y = mouseState.y / display.getScale()
|
||||
if (!is_monitor) {
|
||||
client.sendMouseState(mouseState)
|
||||
}
|
||||
}
|
||||
|
||||
const touch = new Guacamole.Mouse.Touchpad(element) // or Guacamole.Touchscreen
|
||||
|
||||
touch.onmousedown = touch.onmousemove = touch.onmouseup = function(state) {
|
||||
if (!is_monitor) {
|
||||
client.sendMouseState(state)
|
||||
}
|
||||
}
|
||||
this.client = client
|
||||
},
|
||||
onClientStateChange(state) {
|
||||
console.log(state)
|
||||
const key = 'message'
|
||||
switch (state) {
|
||||
case STATE_IDLE:
|
||||
this.$message.destroy(key)
|
||||
this.$message.loading({ content: this.$t('oneterm.guacamole.idle'), duration: 0, key: key })
|
||||
break
|
||||
case STATE_CONNECTING:
|
||||
this.$message.destroy(key)
|
||||
this.$message.loading({ content: this.$t('oneterm.guacamole.connecting'), duration: 0, key: key })
|
||||
break
|
||||
case STATE_WAITING:
|
||||
this.$message.destroy(key)
|
||||
this.$message.loading({ content: this.$t('oneterm.guacamole.waiting'), duration: 0, key: key })
|
||||
break
|
||||
case STATE_CONNECTED:
|
||||
this.$message.destroy(key)
|
||||
this.$message.success({ content: this.$t('oneterm.guacamole.connected'), duration: 3, key: key })
|
||||
// 向后台发送请求,更新会话的状态
|
||||
// sessionApi.connect(sessionId)
|
||||
break
|
||||
case STATE_DISCONNECTING:
|
||||
break
|
||||
case STATE_DISCONNECTED:
|
||||
if (this.client) {
|
||||
this.client.disconnect()
|
||||
}
|
||||
const guacamoleClient = document.getElementById('display')
|
||||
guacamoleClient.innerHTML = ''
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
onWindowResize() {
|
||||
const { client } = this
|
||||
const width = client.getDisplay().getWidth()
|
||||
const height = client.getDisplay().getHeight()
|
||||
|
||||
const winWidth = window.innerWidth
|
||||
const winHeight = window.innerHeight
|
||||
|
||||
const scaleW = winWidth / width
|
||||
const scaleH = winHeight / height
|
||||
|
||||
const scale = Math.min(scaleW, scaleH)
|
||||
if (!scale) {
|
||||
return
|
||||
}
|
||||
client.getDisplay().scale(scale)
|
||||
},
|
||||
onError(status) {
|
||||
if (status.code > 1000) {
|
||||
this.$message.info({
|
||||
content: decodeURIComponent(
|
||||
atob(status.message)
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||
})
|
||||
.join('')
|
||||
),
|
||||
})
|
||||
} else if (status.code < 1000 && status.message) {
|
||||
this.$message.info(status.message)
|
||||
}
|
||||
},
|
||||
onDisconnect() {
|
||||
console.log(2222)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.oneterm-guacamole {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.oneterm-guacamole > div {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@@ -63,7 +63,8 @@ export default {
|
||||
this.websocket = new WebSocket(
|
||||
is_monitor
|
||||
? `${protocol}://${document.location.host}/api/oneterm/v1/connect/monitor/${session_id}?w=${this.term.cols}&h=${this.term.rows}`
|
||||
: `${protocol}://${document.location.host}/api/oneterm/v1/connect/${session_id}?w=${this.term.cols}&h=${this.term.rows}`
|
||||
: `${protocol}://${document.location.host}/api/oneterm/v1/connect/${session_id}?w=${this.term.cols}&h=${this.term.rows}`,
|
||||
['Sec-WebSocket-Protocol']
|
||||
)
|
||||
this.websocket.onopen = this.websocketOpen()
|
||||
this.websocket.onmessage = this.getMessage
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
<vxe-column :title="$t(`oneterm.sessionTable.clientIp`)" field="client_ip"> </vxe-column>
|
||||
<vxe-column :title="$t(`oneterm.protocol`)" field="protocol"> </vxe-column>
|
||||
<vxe-column :title="$t(`operation`)" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<template #default="{row}">
|
||||
<a-space>
|
||||
<a-tooltip :title="$t(`login`)">
|
||||
<a @click="openTerminal(row)"><ops-icon type="oneterm-login" /></a>
|
||||
<a @click="openTerminal(row)"><ops-icon type="oneterm-login"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t(`oneterm.switchAccount`)">
|
||||
<a @click="openLogin(row)"><ops-icon type="oneterm-switch" /></a>
|
||||
<a @click="openLogin(row)"><ops-icon type="oneterm-switch"/></a>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user