magic-script-editor.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <template>
  2. <div class="magic-script-editor">
  3. <div class="magic-empty-container" v-if="openedScripts.length === 0">
  4. <div class="magic-hot-key">
  5. <p>
  6. {{ $i('message.save') }}<em>Ctrl + S</em><br/>
  7. {{ $i('message.run') }}<em>Ctrl + Q</em><br/>
  8. {{ $i('editor.tooltip.complection')}}<em>Alt + /</em><br/>
  9. {{ $i('editor.tooltip.resume')}}<em>F8</em><br/>
  10. {{ $i('editor.tooltip.stepInto')}}<em>F6</em><br/>
  11. {{ $i('editor.tooltip.format')}}<em>Ctrl + Alt + L</em><br/>
  12. {{ $i('editor.tooltip.recent')}}<em>Ctrl + E</em>
  13. </p>
  14. </div>
  15. </div>
  16. <template v-else>
  17. <magic-tab v-model:value="selectTab" :tabs="openedScripts" className="magic-script-tab" ref="tab"
  18. :allow-close="true" @close="onClose" @change="tab => bus.$emit('open', tab)"
  19. @before-change="beforeChange" @item-contextmenu="onContextMenu">
  20. <template v-slot="{ tab }">
  21. <magic-text-icon :icon="tab.getIcon(tab.item)"/>{{ tab.item.name }}<span v-if="isUpdated(tab)">*</span>
  22. <magic-icon v-if="tab.item.lock === $LOCKED" icon="lock"/>
  23. <magic-avatar-group :users="activateUserFiles[tab.item.id] || []" :max="3" :size="20"/>
  24. </template>
  25. </magic-tab>
  26. <magic-loading :loading="loading">
  27. <div class="magic-monaco-editor-wrapper">
  28. <magic-monaco-editor ref="editor" v-model:value="selectTab.item.script" v-model:decorations="selectTab.decorations" :language="selectTab.language" :support-breakpoint="true"/>
  29. </div>
  30. </magic-loading>
  31. </template>
  32. </div>
  33. </template>
  34. <script setup>
  35. import { getCurrentInstance, inject, nextTick, onMounted, reactive, ref, toRaw, watch } from 'vue'
  36. import bus from '../../../scripts/bus.js'
  37. import request from '../../../scripts/request.js'
  38. import constants from '../../../scripts/constants.js'
  39. import Message from '../../../scripts/constants/message.js'
  40. import { Range } from 'monaco-editor'
  41. import { convertVariables } from '../../../scripts/utils.js'
  42. import RequestParameter from '../../../scripts/editor/request-parameter.js'
  43. import Socket from '../../../scripts/constants/socket.js'
  44. import store from '../../../scripts/store.js'
  45. import $i from '../../../scripts/i18n.js'
  46. const { proxy } = getCurrentInstance()
  47. const openedScripts = reactive([])
  48. const selectTab = ref({})
  49. const loading = ref(true)
  50. const editor = ref(null)
  51. const tab = ref(null)
  52. const activateUserFiles = inject('activateUserFiles')
  53. const javaTypes = {
  54. 'String': 'java.lang.String',
  55. 'Integer': 'java.lang.Integer',
  56. 'Double': 'java.lang.Double',
  57. 'Long': 'java.lang.Long',
  58. 'Byte': 'java.lang.Byte',
  59. 'Short': 'java.lang.Short',
  60. 'Float': 'java.lang.Float',
  61. 'MultipartFile': 'org.springframework.web.multipart.MultipartFile',
  62. 'MultipartFiles': 'java.util.List',
  63. }
  64. RequestParameter.setEnvironment(() => {
  65. const env = {}
  66. const item = selectTab.value?.item
  67. const append = array => array && Array.isArray(array) && array.forEach(it => {
  68. if(it && typeof it.name === 'string' && it.dataType){
  69. env[it.name] = javaTypes[it.dataType] || 'java.lang.Object'
  70. }
  71. })
  72. if(item){
  73. append(item?.parameters)
  74. append(item?.paths)
  75. }
  76. return env
  77. })
  78. // 关闭tab
  79. const onClose = tab => {
  80. let index = openedScripts.findIndex(it => it === tab)
  81. openedScripts.splice(index, 1)
  82. if(tab === selectTab.value){
  83. let len = openedScripts.length
  84. if(index < len){
  85. bus.$emit(Message.OPEN, openedScripts[index])
  86. } else if(len > 0) {
  87. bus.$emit(Message.OPEN, openedScripts[index - 1])
  88. }
  89. }
  90. bus.$emit(Message.CLOSE, tab.item)
  91. // 没有打开的文件时
  92. if(openedScripts.length === 0){
  93. // 设置标题为空
  94. bus.$emit(Message.OPEN_EMPTY)
  95. selectTab.value = {}
  96. }
  97. }
  98. watch(openedScripts, (newVal) => {
  99. store.set(constants.RECENT_OPENED_TAB, newVal.filter(it => it.item?.id).map(it => it.item.id))
  100. })
  101. // 执行保存
  102. const doSave = (flag) => {
  103. const opened = selectTab.value
  104. if(opened && opened.item){
  105. const item = selectTab.value.processSave(opened.item)
  106. Object.keys(item).forEach(key => opened.item[key] = item[key])
  107. return request.sendJson(`/resource/file/${selectTab.value.type}/save?auto=${flag ? 0 : 1}`, item).success((id) => {
  108. const msg = `${opened.title}「${opened.path()}」`
  109. if(id) {
  110. bus.status('message.saveSuccess', true, msg)
  111. opened.tmpObject = JSON.parse(JSON.stringify(item))
  112. if(opened.item.id !== id){
  113. bus.loading(1)
  114. }
  115. opened.item.id = id
  116. }else{
  117. bus.status('message.saveFailed', false, msg)
  118. proxy.$alert($i('message.saveFailed', msg))
  119. }
  120. })
  121. }
  122. }
  123. // 执行测试
  124. const doTest = () => selectTab.value.doTest(selectTab.value)
  125. const doContinue = step => {
  126. if(selectTab.value.debuging){
  127. editor.value.removedDecorations(selectTab.value.debugDecorations)
  128. selectTab.value.debuging = false
  129. selectTab.value.variables = null
  130. const breakpoints = (selectTab.value.decorations || []).filter(it => it.options.linesDecorationsClassName === 'breakpoints').map(it => it.range.startLineNumber).join('|')
  131. bus.send(Socket.RESUME_BREAKPOINT, [selectTab.value.item.id, (step === true ? '1' : '0'), breakpoints].join(','))
  132. }
  133. }
  134. // tab 右键菜单事件
  135. const onContextMenu = (event, item, index) => {
  136. const menus = [{
  137. label: $i('editor.tab.close'),
  138. divided: true,
  139. onClick(){
  140. onClose(item)
  141. }
  142. },{
  143. label: $i('editor.tab.closeOther'),
  144. divided: true,
  145. onClick(){
  146. [...openedScripts].forEach((it, i) => i != index && onClose(it))
  147. }
  148. },{
  149. label: $i('editor.tab.closeLeft'),
  150. onClick(){
  151. [...openedScripts].forEach((it, i) => i < index && onClose(it))
  152. }
  153. },{
  154. label: $i('editor.tab.closeRight'),
  155. divided: true,
  156. onClick(){
  157. [...openedScripts].forEach((it, i) => i > index && onClose(it))
  158. }
  159. },{
  160. label: $i('editor.tab.closeAll'),
  161. onClick(){
  162. [...openedScripts].forEach(it => onClose(it))
  163. }
  164. }]
  165. proxy.$contextmenu({ menus, event})
  166. }
  167. // 判断是否有变动
  168. const isUpdated = (tab) => Object.keys(tab.tmpObject || {}).some(k => {
  169. const v1 = tab.tmpObject[k]
  170. const v2 = tab.item[k]
  171. if(v1 === v2 || k === 'properties' || k === 'responseBody' || k === 'responseBodyDefinition'){
  172. return false
  173. }
  174. return (typeof v1 === 'object' || typeof v2 === 'object') ? JSON.stringify(v1) !== JSON.stringify(v2) : v1 !== v2
  175. })
  176. // 退出登录
  177. bus.$on(Message.LOGOUT, () => [...openedScripts].forEach(tab => onClose(tab)))
  178. // 执行删除时
  179. bus.$on(Message.DELETE_FILE, item => {
  180. const index = openedScripts.findIndex(it => it.item === item)
  181. if(index > -1){
  182. onClose(openedScripts[index])
  183. }
  184. })
  185. // 重新加载资源完毕时
  186. bus.$on(Message.RELOAD_RESOURCES_FINISH, ()=> {
  187. openedScripts.filter(opened => opened.item && opened.item.id).forEach(opened => request.sendGet(`/resource/file/${opened.item.id}`).success(data => {
  188. bus.status('message.getDetailSuccess', true, `${opened.title}「${opened.path()}」`)
  189. Object.keys(data).forEach(key => opened.item[key] = data[key])
  190. }))
  191. })
  192. // 登录响应
  193. bus.$event(Socket.LOGIN_RESPONSE, () => {
  194. if(selectTab.value){
  195. bus.send(Socket.SET_FILE_ID, selectTab.value.item?.id || '0')
  196. }
  197. })
  198. const beforeChange = tab => {
  199. if(tab && editor.value){
  200. tab.scrollTop = editor.value.getScrollTop()
  201. }
  202. }
  203. // 打开文件
  204. bus.$on(Message.OPEN, opened => {
  205. let find = openedScripts.find(it => it.item === opened.item || (it.item.id && it.item.id === opened.item.id))
  206. bus.send(Socket.SET_FILE_ID, opened.item.id || '0')
  207. if(find){
  208. selectTab.value = find
  209. loading.value = false
  210. nextTick(() => editor.value.setScrollTop(find.scrollTop || 0))
  211. } else {
  212. openedScripts.push(opened)
  213. selectTab.value = opened
  214. if(opened.item.id && !opened.item.script){
  215. loading.value = true
  216. request.sendGet(`/resource/file/${opened.item.id}`).success(data => {
  217. bus.status('message.getDetail', true, `${opened.title}「${opened.path()}」`)
  218. Object.keys(data).forEach(key => opened.item[key] = data[key])
  219. opened.tmpObject = JSON.parse(JSON.stringify(opened.processSave(data)))
  220. loading.value = false
  221. nextTick(() => editor.value.setScrollTop(0))
  222. })
  223. } else {
  224. opened.tmpObject = JSON.parse(JSON.stringify(opened.processSave(opened.item)))
  225. loading.value = false
  226. nextTick(() => editor.value.setScrollTop(0))
  227. }
  228. }
  229. if(selectTab.value.decorations && selectTab.value.decorations.length > 0){
  230. nextTick(() => {
  231. const decorations = toRaw(selectTab.value.decorations)
  232. selectTab.value.debugDecorations = editor.value.appendDecoration(decorations)
  233. .map((it, index) => decorations[index].options?.className === 'debug-line' ? it : null)
  234. .filter(it => it !== null) || []
  235. })
  236. }
  237. nextTick(() => tab.value && tab.value.scrollIntoView(opened))
  238. })
  239. // 保存事件
  240. bus.$on(Message.DO_SAVE, doSave)
  241. // 测试事件
  242. bus.$on(Message.DO_TEST, () => {
  243. const opened = selectTab.value
  244. if(opened && opened.item && opened.runnable && opened.doTest && opened.running !== true){
  245. if(constants.AUTO_SAVE && opened.item.lock !== '1'){
  246. doSave().end(successed => successed && doTest())
  247. } else {
  248. doTest()
  249. }
  250. }
  251. })
  252. // 进入断点
  253. bus.$event(Socket.BREAKPOINT, ([ scriptId, { range, variables } ]) => {
  254. // 如果切换或已关闭
  255. if(selectTab.value?.item?.id !== scriptId){
  256. const opened = openedScripts.find(it => it.item.id === scriptId)
  257. if(opened){
  258. // 切换tab
  259. bus.$emit(Message.OPEN, opened)
  260. }else{
  261. // TODO 重新打开
  262. }
  263. }
  264. nextTick(() => {
  265. selectTab.value.variables = convertVariables(variables)
  266. selectTab.value.debuging = true
  267. selectTab.value.debugDecorations = [editor.value.appendDecoration([{
  268. range: new Range(range[0], 1, range[0], 1),
  269. options: {
  270. isWholeLine: true,
  271. inlineClassName: 'debug-line',
  272. className: 'debug-line'
  273. }
  274. }])]
  275. bus.$emit(Message.SWITCH_TOOLBAR, 'debug')
  276. })
  277. })
  278. // 恢复断点
  279. bus.$on(Message.DEBUG_CONTINUE, doContinue)
  280. // 断点单步运行
  281. bus.$on(Message.DEBUG_SETPINTO, () => doContinue(true))
  282. // 执行出现异常
  283. bus.$event(Socket.EXCEPTION, ([ [scriptId, message, line] ]) => {
  284. if(selectTab.value?.item?.id === scriptId){
  285. const range = new Range(line[0], line[2], line[1], line[3] + 1)
  286. const instance = editor.value.getInstance()
  287. const decorations = instance.deltaDecorations([], [{
  288. range,
  289. options: {
  290. hoverMessage: {
  291. value: message
  292. },
  293. inlineClassName: 'squiggly-error'
  294. }
  295. }])
  296. instance.revealRangeInCenter(range)
  297. instance.focus()
  298. if(constants.DECORATION_TIMEOUT >= 0){
  299. setTimeout(() => instance.deltaDecorations(decorations, []), constants.DECORATION_TIMEOUT)
  300. }
  301. }
  302. })
  303. const emit = defineEmits(['onLoad'])
  304. onMounted(() => emit('onLoad'))
  305. </script>
  306. <style scoped>
  307. .magic-script-editor{
  308. flex: 1;
  309. overflow: hidden;
  310. position: relative;
  311. }
  312. .magic-script-editor .magic-monaco-editor-wrapper{
  313. position: absolute;
  314. top: 30px;
  315. left: 0;
  316. right: 0;
  317. bottom: 0;
  318. }
  319. .magic-empty-container{
  320. flex: 1;
  321. position: relative;
  322. width: 100%;
  323. height: 100%;
  324. background: var(--empty-background-color);
  325. }
  326. .magic-hot-key{
  327. position: absolute;
  328. top: 50%;
  329. margin-top: -105px;
  330. text-align: center;
  331. color: var(--empty-color);
  332. font-size: 16px;
  333. width: 100%;
  334. }
  335. .magic-hot-key p{
  336. display: inline-block;
  337. text-align: left;
  338. line-height: 30px;
  339. }
  340. .magic-hot-key p em{
  341. margin-left: 15px;
  342. font-style: normal;
  343. color: var(--empty-key-color);
  344. }
  345. .magic-monaco-editor{
  346. position: absolute;
  347. top: 0;
  348. bottom: 0;
  349. left:0;
  350. right: 0;
  351. overflow: visible !important;
  352. }
  353. .magic-script-editor :deep(.magic-avatar-group){
  354. margin-left: 10px;
  355. }
  356. </style>