Terminal.vue 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <script setup lang="ts">
  2. import '@xterm/xterm/css/xterm.css'
  3. import { Terminal } from '@xterm/xterm'
  4. import { FitAddon } from '@xterm/addon-fit'
  5. import _ from 'lodash'
  6. import ws from '@/lib/websocket'
  7. import use2FAModal from '@/components/TwoFA/use2FAModal'
  8. import twoFA from '@/api/2fa'
  9. let term: Terminal | null
  10. let ping: NodeJS.Timeout
  11. const router = useRouter()
  12. const websocket = shallowRef()
  13. const lostConnection = ref(false)
  14. onMounted(() => {
  15. twoFA.secure_session_status()
  16. const otpModal = use2FAModal()
  17. otpModal.open().then(secureSessionId => {
  18. websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
  19. nextTick(() => {
  20. initTerm()
  21. websocket.value.onmessage = wsOnMessage
  22. websocket.value.onopen = wsOnOpen
  23. websocket.value.onerror = () => {
  24. lostConnection.value = true
  25. }
  26. websocket.value.onclose = () => {
  27. lostConnection.value = true
  28. }
  29. })
  30. }).catch(() => {
  31. if (window.history.length > 1)
  32. router.go(-1)
  33. else
  34. router.push('/')
  35. })
  36. })
  37. interface Message {
  38. Type: number
  39. Data: string | null | { Cols: number; Rows: number }
  40. }
  41. const fitAddon = new FitAddon()
  42. const fit = _.throttle(() => {
  43. fitAddon.fit()
  44. }, 50)
  45. function initTerm() {
  46. term = new Terminal({
  47. convertEol: true,
  48. fontSize: 14,
  49. cursorStyle: 'block',
  50. scrollback: 1000,
  51. theme: {
  52. background: '#000',
  53. },
  54. })
  55. term.loadAddon(fitAddon)
  56. term.open(document.getElementById('terminal')!)
  57. setTimeout(() => {
  58. fitAddon.fit()
  59. }, 60)
  60. window.addEventListener('resize', fit)
  61. term.focus()
  62. term.onData(key => {
  63. const order: Message = {
  64. Data: key,
  65. Type: 1,
  66. }
  67. sendMessage(order)
  68. })
  69. term.onBinary(data => {
  70. sendMessage({ Type: 1, Data: data })
  71. })
  72. term.onResize(data => {
  73. sendMessage({ Type: 2, Data: { Cols: data.cols, Rows: data.rows } })
  74. })
  75. }
  76. function sendMessage(data: Message) {
  77. websocket.value.send(JSON.stringify(data))
  78. }
  79. function wsOnMessage(msg: { data: string | Uint8Array }) {
  80. term!.write(msg.data)
  81. }
  82. function wsOnOpen() {
  83. ping = setInterval(() => {
  84. sendMessage({ Type: 3, Data: null })
  85. }, 30000)
  86. }
  87. onUnmounted(() => {
  88. window.removeEventListener('resize', fit)
  89. clearInterval(ping)
  90. term?.dispose()
  91. websocket.value?.close()
  92. })
  93. </script>
  94. <template>
  95. <ACard :title="$gettext('Terminal')">
  96. <AAlert
  97. v-if="lostConnection"
  98. class="mb-6"
  99. type="error"
  100. show-icon
  101. :message="$gettext('Connection lost, please refresh the page.')"
  102. />
  103. <div
  104. id="terminal"
  105. class="console"
  106. />
  107. </ACard>
  108. </template>
  109. <style lang="less" scoped>
  110. .console {
  111. min-height: calc(100vh - 300px);
  112. :deep(.terminal) {
  113. padding: 10px;
  114. }
  115. :deep(.xterm-viewport) {
  116. border-radius: 5px;
  117. }
  118. }
  119. </style>