NEZ-3038 feat:web terminal支持个性化设置
This commit is contained in:
@@ -309,5 +309,11 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.terminal-menu {
|
||||
position: fixed;
|
||||
width: 150px;
|
||||
border-radius: 2px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
||||
"css_prefix_text": "nz-icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "7775074",
|
||||
"name": "个性化",
|
||||
"font_class": "personalization",
|
||||
"unicode": "e645",
|
||||
"unicode_decimal": 58949
|
||||
},
|
||||
{
|
||||
"icon_id": "36651290",
|
||||
"name": "Batch Synchronize",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,7 +1,32 @@
|
||||
<template>
|
||||
<div :id="'ternimalContainer'+idIndex" class="console" v-watermark="{text: waterMarkText, text1: terminal.host || '', textColor: 'rgba(215, 215, 215, 0.5)'}">
|
||||
<div :id="'ternimalContainer'+idIndex" class="console" v-watermark="{show: showWatermark,text: waterMarkText, text1: terminal.host || '', textColor: 'rgba(215, 215, 215, 0.5)'}" v-clickoutside="clickHide">
|
||||
<div :id="'terminal'+idIndex" style="height: 100%;width: 100%"></div>
|
||||
<fileDirectory :host="host" v-clickoutside="closeFileDir" :uuid="terminal.uuid" v-show="fileDirectoryShow" @close="showFileDir(false)" :fileDirectoryShow="fileDirectoryShow" ref="fileDirectory"/>
|
||||
<div
|
||||
v-if="terminal.isLogin && showMenu"
|
||||
class="terminal-menu menus"
|
||||
:style="{
|
||||
left: menuPosition.left + 'px',
|
||||
top: menuPosition.top + 'px'
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<span @click="copySelection()">{{$t('overall.duplicate')}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span @click="paste()">{{$t('project.topology.paste')}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span @click="clearTerminal()">{{$t('terminal.clear')}}</span>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div>
|
||||
<span @click="showFileDir(true)">{{$t('terminal.sftp')}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span @click="closeTerminal(true)"> {{$t('overall.close')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -23,9 +48,12 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
terminalSetting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
fontSize: {}
|
||||
},
|
||||
inject: ['terminalSetting'],
|
||||
data () {
|
||||
return {
|
||||
term: null,
|
||||
@@ -51,9 +79,21 @@ export default {
|
||||
userName: '',
|
||||
fileDirectoryShow: false,
|
||||
host: '',
|
||||
waterMarkText: 'Nezha'
|
||||
waterMarkText: 'Nezha',
|
||||
showWatermark: false,
|
||||
wordSeparator: "\\ :;~`!@#$%^&*()-=+|[]{}'\",.<>/?",
|
||||
showMenu: false,
|
||||
menuPosition: {
|
||||
left: 0,
|
||||
top: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
// computed: {
|
||||
// terminalSetting () {
|
||||
// return this.provObj.terminalSetting
|
||||
// }
|
||||
// },
|
||||
watch: {
|
||||
},
|
||||
methods: {
|
||||
@@ -99,7 +139,8 @@ export default {
|
||||
fontSize: 16,
|
||||
lineHeight: 1.2,
|
||||
allowTransparency: true,
|
||||
background: 'transparent'
|
||||
background: 'transparent',
|
||||
scrollback: this.terminalSetting.scrollbackLines
|
||||
})
|
||||
this.term.open(terminalContainer)
|
||||
this.term.focus()
|
||||
@@ -134,6 +175,7 @@ export default {
|
||||
// 连接成功onclose
|
||||
this.terminalSocket.onopen = () => {
|
||||
this.terminal.isLogin = true
|
||||
this.showMenu = false
|
||||
this.isInit = true
|
||||
this.term.focus()
|
||||
}
|
||||
@@ -146,11 +188,8 @@ export default {
|
||||
//that.term.write(data);
|
||||
} */
|
||||
})
|
||||
// 监听选中文字的事件
|
||||
this.term.on('selection', (p,a) => {
|
||||
console.log(this.term.getSelection())
|
||||
this.$copyText(this.term.getSelection())
|
||||
})
|
||||
// 个性化配置
|
||||
this.initTerminalSetting()
|
||||
// 返回
|
||||
this.terminalSocket.onmessage = function (evt) {
|
||||
let backContent = evt.data
|
||||
@@ -278,7 +317,10 @@ export default {
|
||||
})
|
||||
},
|
||||
clearTerminal () {
|
||||
this.term.reset()
|
||||
this.term.clear()
|
||||
},
|
||||
closeTerminal () {
|
||||
this.$emit('close')
|
||||
},
|
||||
enterStr (message) {
|
||||
if (this.terminalSocket && this.terminal.isLogin) {
|
||||
@@ -293,16 +335,120 @@ export default {
|
||||
// 新增历史记录
|
||||
historyChange (message) {
|
||||
this.$emit('historyChange', message)
|
||||
},
|
||||
initTerminalSetting () { // 个性化配置
|
||||
// 1 render
|
||||
this.showWatermark = this.terminalSetting.watermark
|
||||
console.log(this.terminalSetting.watermark)
|
||||
this.term.on('selection', (p, a) => {
|
||||
if (this.terminalSetting.copyOnSelect) {
|
||||
this.copySelection()
|
||||
}
|
||||
})
|
||||
// this.term.on('click', () => {
|
||||
// console.log('click')
|
||||
// console.log(this.term.getSelection())
|
||||
// })
|
||||
// this.term.on('dbclick', () => {
|
||||
// console.log('doubleclick123')
|
||||
// console.log(this.term.getSelection())
|
||||
// })
|
||||
this.term.selectionManager._isCharWordSeparator = (charData) => {
|
||||
console.log(charData)
|
||||
if (charData[2] === 0) {
|
||||
return false
|
||||
}
|
||||
return this.wordSeparator.indexOf(charData[1]) >= 0
|
||||
}
|
||||
},
|
||||
renderTerminalSetting () {
|
||||
this.showWatermark = this.terminalSetting.watermark
|
||||
this.wordSeparator = this.terminalSetting.wordSeparator
|
||||
this.term.setOption({
|
||||
scrollback: this.terminalSetting.scrollbackLines
|
||||
})
|
||||
},
|
||||
copySelection () {
|
||||
let str = this.term.getSelection()
|
||||
if (!this.terminalSetting.copyWithFormatting) {
|
||||
str = str.replace(/[\r\n]/g, '')
|
||||
}
|
||||
if (this.terminalSetting.copyTrimEnd) {
|
||||
str = str.replace(/(\s*$)/g, '')
|
||||
}
|
||||
this.$copyText(str)
|
||||
},
|
||||
dblclick () {
|
||||
if (this.term) {
|
||||
if (this.terminalSetting.copyOnSelect) {
|
||||
this.copySelection()
|
||||
}
|
||||
}
|
||||
},
|
||||
async contextmenu (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (this.terminalSetting.rightClick === 'none') {
|
||||
return
|
||||
}
|
||||
if (this.terminalSetting.rightClick === 'menu') {
|
||||
this.tiggerMenu(true, event)
|
||||
}
|
||||
if (this.terminalSetting.rightClick === 'paste') {
|
||||
if (!document.execCommand('paste')) {
|
||||
const text = await navigator.clipboard.readText()
|
||||
this.term.send(text)
|
||||
}
|
||||
}
|
||||
},
|
||||
async paste () {
|
||||
if (!document.execCommand('paste')) {
|
||||
const text = await navigator.clipboard.readText()
|
||||
this.term.send(text)
|
||||
}
|
||||
},
|
||||
tiggerMenu (flag, event) {
|
||||
this.showMenu = false
|
||||
if (flag) {
|
||||
let top = 0
|
||||
let left = 0
|
||||
if (event.clientY < document.body.clientHeight / 2) {
|
||||
top = event.clientY + 10
|
||||
} else {
|
||||
top = event.clientY - 180 - 10
|
||||
}
|
||||
if (event.clientX < document.body.clientWidth / 2) {
|
||||
left = event.clientX + 10
|
||||
} else {
|
||||
left = event.clientX - 150 - 10
|
||||
}
|
||||
this.menuPosition = {
|
||||
left: left,
|
||||
top: top
|
||||
}
|
||||
}
|
||||
this.showMenu = flag
|
||||
},
|
||||
clickHide () {
|
||||
this.showMenu = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.beforeCreate()
|
||||
console.log(this.terminalSetting)
|
||||
this.showWatermark = this.terminalSetting.watermark
|
||||
const dom = document.getElementById('ternimalContainer' + this.idIndex)
|
||||
dom.addEventListener('dblclick', this.dblclick)
|
||||
dom.addEventListener('contextmenu', this.contextmenu)
|
||||
dom.addEventListener('click', this.clickHide)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.closeSocket()
|
||||
this.term.off('selection')
|
||||
this.term.off('data')
|
||||
const dom = document.getElementById('ternimalContainer' + this.idIndex)
|
||||
dom && dom.removeEventListener('dblclick', this.dblclick)
|
||||
dom && dom.removeEventListener('contextmenu', this.contextmenu)
|
||||
dom && dom.removeEventListener('click', this.clickHide)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
<div class="personal-dropdown__username">{{name}}</div>
|
||||
<div class="personal-dropdown__name">@{{name}}</div>
|
||||
</div>
|
||||
<el-dropdown-item>
|
||||
<div id="header-to-personalization" @click="showPersonalization"><i class="nz-icon nz-icon-personalization"></i>{{$t('terminal.personal')}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<div id="header-to-logout" @click="logout"><i class="nz-icon nz-icon-exit"></i>{{$t('overall.signOut')}}</div>
|
||||
</el-dropdown-item>
|
||||
@@ -24,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<fileListState v-clickoutside="hideFileState" ref="fileListState"/>
|
||||
<webSSHNew ref="websshNew" />
|
||||
<webSSHNew ref="websshNew" :terminalSetting="terminalSetting" />
|
||||
<div class="shell-input">
|
||||
<el-input ref="shellInput" :placeholder="placeholder" size="small" v-model="message" @keyup.enter.native="sendMessage" @focus="placeholderChange('focus')" @blur="placeholderChange('blur')">
|
||||
<i slot="suffix" class="nz-icon nz-icon-history no-style-class" :class="{'active':visible}" :title="$t('terminal.history')" @click="toggleHistory"></i>
|
||||
@@ -48,6 +51,54 @@
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<!--弹窗-->
|
||||
<el-dialog :modal-append-to-body='false' :z-index="1000" :show-close="true" :visible.sync="personalization" @close="personalization = false" class="nz-dialog webshell-selectAsset" width="650px">
|
||||
<div slot="title">{{$t('webshell.selAsset')}}</div>
|
||||
<div>
|
||||
<el-form v-model="newTerminalSetting" label-width="180px" size="small" ref="terminalForm" :validate-on-rule-change="false">
|
||||
<div class="system-title">{{$t('terminal.render')}}</div>
|
||||
<el-form-item :label="$t('waterMaker')" prop="watermark">
|
||||
<el-switch v-model="newTerminalSetting.watermark" :active-value='true' :inactive-value='false' id="terminal-setting-watermark_change" size="small">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.scrollbackLines')" prop="scrollbackLines">
|
||||
<el-input-number :min="1" :precision="0" :controls="false" :placeholder="'默认25000'" v-model="newTerminalSetting.scrollbackLines" size="small"></el-input-number>
|
||||
</el-form-item>
|
||||
<div class="system-title">{{$t('terminal.mouse')}}</div>
|
||||
<el-form-item :label="$t('terminal.rightClick')" prop="rightClick">
|
||||
<el-radio-group v-model="newTerminalSetting.rightClick" size="small">
|
||||
<el-radio-button label="none">{{$t('project.topology.none')}}</el-radio-button>
|
||||
<el-radio-button label="menu">{{$t('terminal.menu')}}</el-radio-button>
|
||||
<el-radio-button label="paste">{{$t('terminal.paste')}}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.wordSeparator')" prop="wordSeparator">
|
||||
<el-input size="small" v-model="newTerminalSetting.wordSeparator"></el-input>
|
||||
</el-form-item>
|
||||
<div class="system-title">{{$t('terminal.clipboard')}}</div>
|
||||
<el-form-item :label="$t('terminal.copySelect')" prop="copyOnSelect">
|
||||
<el-switch v-model="newTerminalSetting.copyOnSelect" :active-value='true' :inactive-value='false' id="terminal-setting-watermark_change">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.copyFormatting')" prop="copyWithFormatting">
|
||||
<el-switch v-model="newTerminalSetting.copyWithFormatting" :active-value='true' :inactive-value='false' id="terminal-setting-watermark_change">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.copyTrimEnd')" prop="copyTrimEnd">
|
||||
<el-switch v-model="newTerminalSetting.copyTrimEnd" :active-value='true' :inactive-value='false' id="terminal-setting-watermark_change">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<button class="footer__btn footer__btn--light" @click="personalization = false">
|
||||
<span>{{$t('overall.cancel')}}</span>
|
||||
</button>
|
||||
<button class="footer__btn" :disabled="prevent_opt.save" @click.prevent="savePersonalization">
|
||||
<span>{{$t('overall.save')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,13 +130,15 @@ export default {
|
||||
visible: false,
|
||||
// 历史命令记录
|
||||
historyArr: [],
|
||||
personalization: false,
|
||||
newTerminalSetting: {},
|
||||
terminalSetting: {
|
||||
watermark: false,
|
||||
watermark: true,
|
||||
scrollbackLines: 25000,
|
||||
rightClick: 'menu',
|
||||
wordSeparator: "\\ :;~`!@#$%^&*()-=+|[]{}'\",.<>/?",
|
||||
copyOnSelect: false,
|
||||
copyWithFormatting: false,
|
||||
copyWithFormatting: true,
|
||||
copyTrimEnd: false
|
||||
}
|
||||
}
|
||||
@@ -110,6 +163,7 @@ export default {
|
||||
const self = this
|
||||
this.name = localStorage.getItem('nz-username')
|
||||
this.username = localStorage.getItem('nz-username')
|
||||
this.initTerminalSetting()
|
||||
window.opener.name = 'parent'
|
||||
window.onbeforeunload = () => {
|
||||
const opener = window.opener
|
||||
@@ -199,6 +253,39 @@ export default {
|
||||
isLogout: true
|
||||
})
|
||||
)
|
||||
},
|
||||
showPersonalization () {
|
||||
this.personalization = true
|
||||
this.newTerminalSetting = this.$lodash.cloneDeep(this.terminalSetting)
|
||||
},
|
||||
savePersonalization () {
|
||||
this.$put('/sys/user/preference', { terminal: JSON.stringify(this.newTerminalSetting) }).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.terminalSetting = this.$lodash.cloneDeep(this.newTerminalSetting)
|
||||
this.$refs.websshNew.renderTerminalSetting()
|
||||
this.personalization = false
|
||||
} else {
|
||||
this.$message.error(res.msg || res.error)
|
||||
}
|
||||
})
|
||||
},
|
||||
initTerminalSetting () {
|
||||
this.$get('/sys/user/preference', { key: 'terminal' }).then(res => {
|
||||
if (res.code === 200) {
|
||||
if (res.data.terminal) {
|
||||
let terminal = {}
|
||||
try {
|
||||
terminal = JSON.parse(res.data.terminal)
|
||||
} catch (e) {}
|
||||
this.terminalSetting = {
|
||||
...this.terminalSetting,
|
||||
...terminal
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg || res.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +64,14 @@
|
||||
|
||||
<terminal
|
||||
:fontSize="fontSize"
|
||||
:terminalSetting="terminalSetting"
|
||||
:terminalType="item.terminal.terminalType"
|
||||
:idIndex="index"
|
||||
:ref="'console'+index"
|
||||
:terminal="item.terminal"
|
||||
@loginFail="loginFail"
|
||||
@closeConsole="removeTab"
|
||||
@close="closeShell(item,index)"
|
||||
@refreshConsoleTitle="refreshTabTitle"
|
||||
@historyChange="(message)=>{$parent.historyChange(message)}"
|
||||
></terminal>
|
||||
@@ -200,6 +202,12 @@ export default {
|
||||
terminal,
|
||||
selectTable
|
||||
},
|
||||
props: {
|
||||
terminalSetting: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
language () { return this.$store.getters.getLanguage },
|
||||
fileList () {
|
||||
@@ -448,6 +456,7 @@ export default {
|
||||
this.$refs['console' + el.index][0].resize()
|
||||
this.$refs['console' + el.index][0].term.scrollToBottom()
|
||||
this.$refs['console' + el.index][0].focusConsole()
|
||||
this.$refs['console' + el.index][0].clickHide()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -805,6 +814,11 @@ export default {
|
||||
this.editableTabs.forEach((item, index) => {
|
||||
this.$refs['console' + index][0].enterStr(message)
|
||||
})
|
||||
},
|
||||
renderTerminalSetting () {
|
||||
this.editableTabs.forEach((item, index) => {
|
||||
this.$refs['console' + index][0].renderTerminalSetting()
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1127,25 +1127,27 @@ export const watermark = {
|
||||
bind (el, binding) {
|
||||
const text = binding.value.text
|
||||
const text1 = binding.value.text1
|
||||
const show = binding.value.show
|
||||
const font = binding.value.font || '24px Roboto-Regular'
|
||||
const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)'
|
||||
const width = binding.value.width || 400
|
||||
const height = binding.value.height || 200
|
||||
const textRotate = binding.value.textRotate || -30
|
||||
addWaterMarker(el, text, font, textColor, width, height, textRotate, text1)
|
||||
addWaterMarker(el, text, font, textColor, width, height, textRotate, show, text1)
|
||||
},
|
||||
update (el, binding) {
|
||||
const text = binding.value.text
|
||||
const text1 = binding.value.text1
|
||||
const show = binding.value.show
|
||||
const font = binding.value.font || '16px Roboto-Regular'
|
||||
const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)'
|
||||
const width = binding.value.width || 400
|
||||
const height = binding.value.height || 200
|
||||
const textRotate = binding.value.textRotate || -20
|
||||
addWaterMarker(el, text, font, textColor, width, height, textRotate, text1)
|
||||
addWaterMarker(el, text, font, textColor, width, height, textRotate, show, text1)
|
||||
},
|
||||
}
|
||||
function addWaterMarker (parentNode, text, font, textColor, width, height, textRotate, text1) {
|
||||
function addWaterMarker (parentNode, text, font, textColor, width, height, textRotate, show, text1) {
|
||||
const can = document.createElement('canvas')
|
||||
parentNode.appendChild(can)
|
||||
can.width = width
|
||||
@@ -1160,5 +1162,9 @@ function addWaterMarker (parentNode, text, font, textColor, width, height, textR
|
||||
cans.textBaseline = 'middle'
|
||||
cans.fillText(text, 200, 150)
|
||||
cans.fillText(text1, 200, 170)
|
||||
parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
|
||||
if (show) {
|
||||
parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
|
||||
} else {
|
||||
parentNode.style.backgroundImage = ''
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user