magic-script-editor.vue 11 KB

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