<template> <div class="magic-script-editor"> <div class="magic-empty-container" v-if="openedScripts.length === 0"> <div class="magic-hot-key"> <p> {{ $i('message.save') }}<em>Ctrl + S</em><br/> {{ $i('message.run') }}<em>Ctrl + Q</em><br/> {{ $i('editor.tooltip.complection')}}<em>Alt + /</em><br/> {{ $i('editor.tooltip.resume')}}<em>F8</em><br/> {{ $i('editor.tooltip.stepInto')}}<em>F6</em><br/> {{ $i('editor.tooltip.format')}}<em>Ctrl + Alt + L</em><br/> {{ $i('editor.tooltip.recent')}}<em>Ctrl + E</em> </p> </div> </div> <template v-else> <magic-tab v-model:value="selectTab" :tabs="openedScripts" className="magic-script-tab" ref="tab" :allow-close="true" @close="onClose" @change="tab => bus.$emit('open', tab)" @before-change="beforeChange" @item-contextmenu="onContextMenu"> <template v-slot="{ tab }"> <magic-text-icon :icon="tab.getIcon(tab.item)"/>{{ tab.item.name }}<span v-if="isUpdated(tab)">*</span> <magic-icon v-if="tab.item.lock === $LOCKED" icon="lock"/> <magic-avatar-group :users="activateUserFiles[tab.item.id] || []" :max="3" :size="20"/> </template> </magic-tab> <magic-loading :loading="loading"> <div class="magic-monaco-editor-wrapper"> <magic-monaco-editor ref="editor" v-model:value="selectTab.item.script" v-model:decorations="selectTab.decorations" :language="selectTab.language" :support-breakpoint="true"/> </div> </magic-loading> </template> </div> </template> <script setup> import { getCurrentInstance, inject, nextTick, onMounted, reactive, ref, toRaw, watch } from 'vue' import bus from '../../../scripts/bus.js' import request from '../../../scripts/request.js' import constants from '../../../scripts/constants.js' import Message from '../../../scripts/constants/message.js' import { Range } from 'monaco-editor' import { convertVariables } from '../../../scripts/utils.js' import RequestParameter from '../../../scripts/editor/request-parameter.js' import Socket from '../../../scripts/constants/socket.js' import store from '../../../scripts/store.js' import $i from '../../../scripts/i18n.js' const { proxy } = getCurrentInstance() const openedScripts = reactive([]) const selectTab = ref({}) const loading = ref(true) const editor = ref(null) const tab = ref(null) const activateUserFiles = inject('activateUserFiles') const javaTypes = { 'String': 'java.lang.String', 'Integer': 'java.lang.Integer', 'Double': 'java.lang.Double', 'Long': 'java.lang.Long', 'Byte': 'java.lang.Byte', 'Short': 'java.lang.Short', 'Float': 'java.lang.Float', 'MultipartFile': 'org.springframework.web.multipart.MultipartFile', 'MultipartFiles': 'java.util.List', } RequestParameter.setEnvironment(() => { const env = {} const item = selectTab.value?.item const append = array => array && Array.isArray(array) && array.forEach(it => { if(it && typeof it.name === 'string' && it.dataType){ env[it.name] = javaTypes[it.dataType] || 'java.lang.Object' } }) if(item){ append(item?.parameters) append(item?.paths) } return env }) // 关闭tab const onClose = tab => { let index = openedScripts.findIndex(it => it === tab) openedScripts.splice(index, 1) if(tab === selectTab.value){ let len = openedScripts.length if(index < len){ bus.$emit(Message.OPEN, openedScripts[index]) } else if(len > 0) { bus.$emit(Message.OPEN, openedScripts[index - 1]) } } bus.$emit(Message.CLOSE, tab.item) // 没有打开的文件时 if(openedScripts.length === 0){ // 设置标题为空 bus.$emit(Message.OPEN_EMPTY) selectTab.value = {} } } watch(openedScripts, (newVal) => { store.set(constants.RECENT_OPENED_TAB, newVal.filter(it => it.item?.id).map(it => it.item.id)) }) // 执行保存 const doSave = (flag) => { const opened = selectTab.value if(opened && opened.item){ const item = selectTab.value.processSave(opened.item) Object.keys(item).forEach(key => opened.item[key] = item[key]) return request.sendJson(`/resource/file/${selectTab.value.type}/save?auto=${flag ? 0 : 1}`, item).success((id) => { const msg = `${opened.title}「${opened.path()}」` if(id) { bus.status('message.saveSuccess', true, msg) opened.tmpObject = JSON.parse(JSON.stringify(item)) if(opened.item.id !== id){ bus.loading(1) } opened.item.id = id }else{ bus.status('message.saveFailed', false, msg) proxy.$alert($i('message.saveFailed', msg)) } }) } } // 执行测试 const doTest = () => selectTab.value.doTest(selectTab.value) const doContinue = step => { if(selectTab.value.debuging){ editor.value.removedDecorations(selectTab.value.debugDecorations) selectTab.value.debuging = false selectTab.value.variables = null const breakpoints = (selectTab.value.decorations || []).filter(it => it.options.linesDecorationsClassName === 'breakpoints').map(it => it.range.startLineNumber).join('|') bus.send(Socket.RESUME_BREAKPOINT, [selectTab.value.item.id, (step === true ? '1' : '0'), breakpoints].join(',')) } } // tab 右键菜单事件 const onContextMenu = (event, item, index) => { const menus = [{ label: $i('editor.tab.close'), divided: true, onClick(){ onClose(item) } },{ label: $i('editor.tab.closeOther'), divided: true, onClick(){ [...openedScripts].forEach((it, i) => i != index && onClose(it)) } },{ label: $i('editor.tab.closeLeft'), onClick(){ [...openedScripts].forEach((it, i) => i < index && onClose(it)) } },{ label: $i('editor.tab.closeRight'), divided: true, onClick(){ [...openedScripts].forEach((it, i) => i > index && onClose(it)) } },{ label: $i('editor.tab.closeAll'), onClick(){ [...openedScripts].forEach(it => onClose(it)) } }] proxy.$contextmenu({ menus, event}) } // 判断是否有变动 const isUpdated = (tab) => Object.keys(tab.tmpObject || {}).some(k => { const v1 = tab.tmpObject[k] const v2 = tab.item[k] if(v1 === v2 || k === 'properties' || k === 'responseBody' || k === 'responseBodyDefinition'){ return false } return (typeof v1 === 'object' || typeof v2 === 'object') ? JSON.stringify(v1) !== JSON.stringify(v2) : v1 !== v2 }) // 退出登录 bus.$on(Message.LOGOUT, () => [...openedScripts].forEach(tab => onClose(tab))) // 执行删除时 bus.$on(Message.DELETE_FILE, item => { const index = openedScripts.findIndex(it => it.item === item) if(index > -1){ onClose(openedScripts[index]) } }) // 重新加载资源完毕时 bus.$on(Message.RELOAD_RESOURCES_FINISH, ()=> { openedScripts.filter(opened => opened.item && opened.item.id).forEach(opened => request.sendGet(`/resource/file/${opened.item.id}`).success(data => { bus.status('message.getDetailSuccess', true, `${opened.title}「${opened.path()}」`) Object.keys(data).forEach(key => opened.item[key] = data[key]) })) }) // 登录响应 bus.$event(Socket.LOGIN_RESPONSE, () => { if(selectTab.value){ bus.send(Socket.SET_FILE_ID, selectTab.value.item?.id || '0') } }) const beforeChange = tab => { if(tab && editor.value){ tab.scrollTop = editor.value.getScrollTop() } } // 打开文件 bus.$on(Message.OPEN, opened => { let find = openedScripts.find(it => it.item === opened.item || (it.item.id && it.item.id === opened.item.id)) bus.send(Socket.SET_FILE_ID, opened.item.id || '0') if(find){ selectTab.value = find loading.value = false nextTick(() => editor.value.setScrollTop(find.scrollTop || 0)) } else { openedScripts.push(opened) selectTab.value = opened if(opened.item.id && !opened.item.script){ loading.value = true request.sendGet(`/resource/file/${opened.item.id}`).success(data => { bus.status('message.getDetail', true, `${opened.title}「${opened.path()}」`) Object.keys(data).forEach(key => opened.item[key] = data[key]) opened.tmpObject = JSON.parse(JSON.stringify(opened.processSave(data))) loading.value = false nextTick(() => editor.value.setScrollTop(0)) }) } else { opened.tmpObject = JSON.parse(JSON.stringify(opened.processSave(opened.item))) loading.value = false nextTick(() => editor.value.setScrollTop(0)) } } if(selectTab.value.decorations && selectTab.value.decorations.length > 0){ nextTick(() => { const decorations = toRaw(selectTab.value.decorations) selectTab.value.debugDecorations = editor.value.appendDecoration(decorations) .map((it, index) => decorations[index].options?.className === 'debug-line' ? it : null) .filter(it => it !== null) || [] }) } nextTick(() => tab.value && tab.value.scrollIntoView(opened)) }) // 保存事件 bus.$on(Message.DO_SAVE, doSave) // 测试事件 bus.$on(Message.DO_TEST, () => { const opened = selectTab.value if(opened && opened.item && opened.runnable && opened.doTest && opened.running !== true){ if(constants.AUTO_SAVE && opened.item.lock !== '1'){ doSave().end(successed => successed && doTest()) } else { doTest() } } }) // 进入断点 bus.$event(Socket.BREAKPOINT, ([ scriptId, { range, variables } ]) => { // 如果切换或已关闭 if(selectTab.value?.item?.id !== scriptId){ const opened = openedScripts.find(it => it.item.id === scriptId) if(opened){ // 切换tab bus.$emit(Message.OPEN, opened) }else{ // TODO 重新打开 } } nextTick(() => { selectTab.value.variables = convertVariables(variables) selectTab.value.debuging = true selectTab.value.debugDecorations = [editor.value.appendDecoration([{ range: new Range(range[0], 1, range[0], 1), options: { isWholeLine: true, inlineClassName: 'debug-line', className: 'debug-line' } }])] bus.$emit(Message.SWITCH_TOOLBAR, 'debug') }) }) // 恢复断点 bus.$on(Message.DEBUG_CONTINUE, doContinue) // 断点单步运行 bus.$on(Message.DEBUG_SETPINTO, () => doContinue(true)) // 执行出现异常 bus.$event(Socket.EXCEPTION, ([ [scriptId, message, line] ]) => { if(selectTab.value?.item?.id === scriptId){ const range = new Range(line[0], line[2], line[1], line[3] + 1) const instance = editor.value.getInstance() const decorations = instance.deltaDecorations([], [{ range, options: { hoverMessage: { value: message }, inlineClassName: 'squiggly-error' } }]) instance.revealRangeInCenter(range) instance.focus() if(constants.DECORATION_TIMEOUT >= 0){ setTimeout(() => instance.deltaDecorations(decorations, []), constants.DECORATION_TIMEOUT) } } }) const emit = defineEmits(['onLoad']) onMounted(() => emit('onLoad')) </script> <style scoped> .magic-script-editor{ flex: 1; overflow: hidden; position: relative; } .magic-script-editor .magic-monaco-editor-wrapper{ position: absolute; top: 30px; left: 0; right: 0; bottom: 0; } .magic-empty-container{ flex: 1; position: relative; width: 100%; height: 100%; background: var(--empty-background-color); } .magic-hot-key{ position: absolute; top: 50%; margin-top: -105px; text-align: center; color: var(--empty-color); font-size: 16px; width: 100%; } .magic-hot-key p{ display: inline-block; text-align: left; line-height: 30px; } .magic-hot-key p em{ margin-left: 15px; font-style: normal; color: var(--empty-key-color); } .magic-monaco-editor{ position: absolute; top: 0; bottom: 0; left:0; right: 0; overflow: visible !important; } .magic-script-editor :deep(.magic-avatar-group){ margin-left: 10px; } </style>