magic-script-editor.vue 11 KB

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