NEZ-3038 feat:web terminal支持个性化设置

This commit is contained in:
zhangyu
2023-08-09 16:48:09 +08:00
parent b8915bb47b
commit 527d206af8
12 changed files with 301 additions and 26 deletions

View File

@@ -309,5 +309,11 @@
text-align: center; 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

View File

@@ -5,6 +5,13 @@
"css_prefix_text": "nz-icon-", "css_prefix_text": "nz-icon-",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "7775074",
"name": "个性化",
"font_class": "personalization",
"unicode": "e645",
"unicode_decimal": 58949
},
{ {
"icon_id": "36651290", "icon_id": "36651290",
"name": "Batch Synchronize", "name": "Batch Synchronize",

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,32 @@
<template> <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> <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"/> <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> </div>
</template> </template>
<script> <script>
@@ -23,9 +48,12 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
terminalSetting: {
type: Object,
default: () => {}
},
fontSize: {} fontSize: {}
}, },
inject: ['terminalSetting'],
data () { data () {
return { return {
term: null, term: null,
@@ -51,9 +79,21 @@ export default {
userName: '', userName: '',
fileDirectoryShow: false, fileDirectoryShow: false,
host: '', host: '',
waterMarkText: 'Nezha' waterMarkText: 'Nezha',
showWatermark: false,
wordSeparator: "\\ :;~`!@#$%^&*()-=+|[]{}'\",.<>/?",
showMenu: false,
menuPosition: {
left: 0,
top: 0
}
} }
}, },
// computed: {
// terminalSetting () {
// return this.provObj.terminalSetting
// }
// },
watch: { watch: {
}, },
methods: { methods: {
@@ -99,7 +139,8 @@ export default {
fontSize: 16, fontSize: 16,
lineHeight: 1.2, lineHeight: 1.2,
allowTransparency: true, allowTransparency: true,
background: 'transparent' background: 'transparent',
scrollback: this.terminalSetting.scrollbackLines
}) })
this.term.open(terminalContainer) this.term.open(terminalContainer)
this.term.focus() this.term.focus()
@@ -134,6 +175,7 @@ export default {
// 连接成功onclose // 连接成功onclose
this.terminalSocket.onopen = () => { this.terminalSocket.onopen = () => {
this.terminal.isLogin = true this.terminal.isLogin = true
this.showMenu = false
this.isInit = true this.isInit = true
this.term.focus() this.term.focus()
} }
@@ -146,11 +188,8 @@ export default {
//that.term.write(data); //that.term.write(data);
} */ } */
}) })
// 监听选中文字的事件 // 个性化配置
this.term.on('selection', (p,a) => { this.initTerminalSetting()
console.log(this.term.getSelection())
this.$copyText(this.term.getSelection())
})
// 返回 // 返回
this.terminalSocket.onmessage = function (evt) { this.terminalSocket.onmessage = function (evt) {
let backContent = evt.data let backContent = evt.data
@@ -278,7 +317,10 @@ export default {
}) })
}, },
clearTerminal () { clearTerminal () {
this.term.reset() this.term.clear()
},
closeTerminal () {
this.$emit('close')
}, },
enterStr (message) { enterStr (message) {
if (this.terminalSocket && this.terminal.isLogin) { if (this.terminalSocket && this.terminal.isLogin) {
@@ -293,16 +335,120 @@ export default {
// 新增历史记录 // 新增历史记录
historyChange (message) { historyChange (message) {
this.$emit('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 () { mounted () {
this.beforeCreate() 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 () { beforeDestroy () {
this.closeSocket() this.closeSocket()
this.term.off('selection') this.term.off('selection')
this.term.off('data') 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> </script>

View File

@@ -15,6 +15,9 @@
<div class="personal-dropdown__username">{{name}}</div> <div class="personal-dropdown__username">{{name}}</div>
<div class="personal-dropdown__name">@{{name}}</div> <div class="personal-dropdown__name">@{{name}}</div>
</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> <el-dropdown-item>
<div id="header-to-logout" @click="logout"><i class="nz-icon nz-icon-exit"></i>{{$t('overall.signOut')}}</div> <div id="header-to-logout" @click="logout"><i class="nz-icon nz-icon-exit"></i>{{$t('overall.signOut')}}</div>
</el-dropdown-item> </el-dropdown-item>
@@ -24,7 +27,7 @@
</div> </div>
</div> </div>
<fileListState v-clickoutside="hideFileState" ref="fileListState"/> <fileListState v-clickoutside="hideFileState" ref="fileListState"/>
<webSSHNew ref="websshNew" /> <webSSHNew ref="websshNew" :terminalSetting="terminalSetting" />
<div class="shell-input"> <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')"> <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> <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> </div>
</transition> </transition>
</div> </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> </div>
</template> </template>
@@ -79,13 +130,15 @@ export default {
visible: false, visible: false,
// 历史命令记录 // 历史命令记录
historyArr: [], historyArr: [],
personalization: false,
newTerminalSetting: {},
terminalSetting: { terminalSetting: {
watermark: false, watermark: true,
scrollbackLines: 25000, scrollbackLines: 25000,
rightClick: 'menu', rightClick: 'menu',
wordSeparator: "\\ :;~`!@#$%^&*()-=+|[]{}'\",.<>/?", wordSeparator: "\\ :;~`!@#$%^&*()-=+|[]{}'\",.<>/?",
copyOnSelect: false, copyOnSelect: false,
copyWithFormatting: false, copyWithFormatting: true,
copyTrimEnd: false copyTrimEnd: false
} }
} }
@@ -110,6 +163,7 @@ export default {
const self = this const self = this
this.name = localStorage.getItem('nz-username') this.name = localStorage.getItem('nz-username')
this.username = localStorage.getItem('nz-username') this.username = localStorage.getItem('nz-username')
this.initTerminalSetting()
window.opener.name = 'parent' window.opener.name = 'parent'
window.onbeforeunload = () => { window.onbeforeunload = () => {
const opener = window.opener const opener = window.opener
@@ -199,6 +253,39 @@ export default {
isLogout: true 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)
}
})
} }
} }
} }

View File

@@ -64,12 +64,14 @@
<terminal <terminal
:fontSize="fontSize" :fontSize="fontSize"
:terminalSetting="terminalSetting"
:terminalType="item.terminal.terminalType" :terminalType="item.terminal.terminalType"
:idIndex="index" :idIndex="index"
:ref="'console'+index" :ref="'console'+index"
:terminal="item.terminal" :terminal="item.terminal"
@loginFail="loginFail" @loginFail="loginFail"
@closeConsole="removeTab" @closeConsole="removeTab"
@close="closeShell(item,index)"
@refreshConsoleTitle="refreshTabTitle" @refreshConsoleTitle="refreshTabTitle"
@historyChange="(message)=>{$parent.historyChange(message)}" @historyChange="(message)=>{$parent.historyChange(message)}"
></terminal> ></terminal>
@@ -200,6 +202,12 @@ export default {
terminal, terminal,
selectTable selectTable
}, },
props: {
terminalSetting: {
type: Object,
default: () => {}
},
},
computed: { computed: {
language () { return this.$store.getters.getLanguage }, language () { return this.$store.getters.getLanguage },
fileList () { fileList () {
@@ -448,6 +456,7 @@ export default {
this.$refs['console' + el.index][0].resize() this.$refs['console' + el.index][0].resize()
this.$refs['console' + el.index][0].term.scrollToBottom() this.$refs['console' + el.index][0].term.scrollToBottom()
this.$refs['console' + el.index][0].focusConsole() 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.editableTabs.forEach((item, index) => {
this.$refs['console' + index][0].enterStr(message) this.$refs['console' + index][0].enterStr(message)
}) })
},
renderTerminalSetting () {
this.editableTabs.forEach((item, index) => {
this.$refs['console' + index][0].renderTerminalSetting()
})
} }
}, },
watch: { watch: {

View File

@@ -1127,25 +1127,27 @@ export const watermark = {
bind (el, binding) { bind (el, binding) {
const text = binding.value.text const text = binding.value.text
const text1 = binding.value.text1 const text1 = binding.value.text1
const show = binding.value.show
const font = binding.value.font || '24px Roboto-Regular' const font = binding.value.font || '24px Roboto-Regular'
const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)' const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)'
const width = binding.value.width || 400 const width = binding.value.width || 400
const height = binding.value.height || 200 const height = binding.value.height || 200
const textRotate = binding.value.textRotate || -30 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) { update (el, binding) {
const text = binding.value.text const text = binding.value.text
const text1 = binding.value.text1 const text1 = binding.value.text1
const show = binding.value.show
const font = binding.value.font || '16px Roboto-Regular' const font = binding.value.font || '16px Roboto-Regular'
const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)' const textColor = binding.value.textColor || 'rgba(215, 215, 215, 0.5)'
const width = binding.value.width || 400 const width = binding.value.width || 400
const height = binding.value.height || 200 const height = binding.value.height || 200
const textRotate = binding.value.textRotate || -20 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') const can = document.createElement('canvas')
parentNode.appendChild(can) parentNode.appendChild(can)
can.width = width can.width = width
@@ -1160,5 +1162,9 @@ function addWaterMarker (parentNode, text, font, textColor, width, height, textR
cans.textBaseline = 'middle' cans.textBaseline = 'middle'
cans.fillText(text, 200, 150) cans.fillText(text, 200, 150)
cans.fillText(text1, 200, 170) 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 = ''
}
} }