CodeEditor.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <script setup lang="ts">
  2. import type { editor } from 'monaco-editor'
  3. import { nextTick, onMounted, ref, watch } from 'vue'
  4. import * as monaco from 'monaco-editor'
  5. import { IconButton } from '@repo/ui'
  6. const props = withDefaults(
  7. defineProps<{
  8. allowFullscreen?: boolean
  9. autoToggleTheme?: boolean
  10. bordered?: boolean
  11. config?: editor.IStandaloneEditorConstructionOptions
  12. language?: string
  13. lineNumbers?: 'off' | 'on'
  14. modelValue?: any
  15. readOnly?: boolean
  16. theme?: 'hc-black' | 'vs-dark' | 'vs-light'
  17. valueFormat?: string
  18. height?: string
  19. appendTo?: string | HTMLElement
  20. }>(),
  21. {
  22. allowFullscreen: true,
  23. config: () => ({
  24. minimap: {
  25. enabled: false
  26. },
  27. selectOnLineNumbers: true
  28. }),
  29. autoToggleTheme: true,
  30. language: 'json',
  31. lineNumbers: 'on',
  32. readOnly: false,
  33. theme: 'vs-light',
  34. valueFormat: 'string',
  35. height: '120px'
  36. }
  37. )
  38. // 动态切换主题
  39. const theme = ref('dark')
  40. const emit = defineEmits(['update:modelValue', 'update:language'])
  41. const isFullScreen = ref(false)
  42. const fullScreenStyle = `position: fixed;
  43. top: 0;
  44. left: 0;
  45. right: 0;
  46. bottom: 0;
  47. z-index: 2999;`
  48. const editContainer = ref<HTMLElement | null>(null)
  49. let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
  50. /**
  51. * 设置文本
  52. * @param text
  53. */
  54. function setValue(text: string) {
  55. monacoEditor?.setValue(text || '')
  56. }
  57. /**
  58. * 光标处插入文本
  59. * @param text
  60. */
  61. function insertText(text: string) {
  62. // 获取光标位置
  63. const position = monacoEditor?.getPosition()
  64. // 未获取到光标位置信息
  65. if (!position) {
  66. return
  67. }
  68. // 插入
  69. monacoEditor?.executeEdits('', [
  70. {
  71. range: new monaco.Range(
  72. position.lineNumber,
  73. position.column,
  74. position.lineNumber,
  75. position.column
  76. ),
  77. text
  78. }
  79. ])
  80. // 设置新的光标位置
  81. monacoEditor?.setPosition({
  82. ...position,
  83. column: position.column + text.length
  84. })
  85. // 重新聚焦
  86. monacoEditor?.focus()
  87. }
  88. onMounted(() => {
  89. monacoEditor = monaco.editor.create(editContainer.value as HTMLElement, {
  90. value: getValue(),
  91. ...props.config,
  92. automaticLayout: true,
  93. language: props.language,
  94. lineNumbers: props.lineNumbers,
  95. readOnly: props.readOnly,
  96. scrollBeyondLastLine: false,
  97. theme: props.theme
  98. })
  99. function handleToggleTheme() {
  100. if (theme.value === 'dark') {
  101. monaco.editor.setTheme('vs-dark')
  102. } else {
  103. monaco.editor.setTheme('vs-light')
  104. }
  105. }
  106. // 自动切换主题
  107. if (props.autoToggleTheme) {
  108. watch(
  109. () => theme,
  110. () => {
  111. nextTick(() => handleToggleTheme())
  112. },
  113. {
  114. immediate: true
  115. }
  116. )
  117. }
  118. // 获取值
  119. function getValue() {
  120. // valueFormat 为json 格式,需要转换处理
  121. if (props.valueFormat === 'json' && props.modelValue) {
  122. return JSON.stringify(props.modelValue, null, 2)
  123. }
  124. return props.modelValue ?? ''
  125. }
  126. // 监听值变化
  127. monacoEditor.onDidChangeModelContent(() => {
  128. const currenValue = monacoEditor?.getValue()
  129. // valueFormat 为json 格式,需要转换处理
  130. if (props.valueFormat === 'json' && currenValue) {
  131. emit('update:modelValue', JSON.parse(currenValue))
  132. return
  133. }
  134. emit('update:modelValue', currenValue ?? '')
  135. })
  136. })
  137. defineExpose({
  138. insertText,
  139. setValue
  140. })
  141. </script>
  142. <template>
  143. <Teleport :disabled="!appendTo" :to="appendTo">
  144. <div ref="editContainer" :class="{ bordered: props.bordered }"
  145. :style="isFullScreen ? fullScreenStyle : { height: props.height }"
  146. class="code-editor w-full h-full relative">
  147. <div class="z-999 text-$epic-text-helper absolute right-4 top-2 cursor-pointer text-xl"
  148. @click="isFullScreen = !isFullScreen" v-if="props.allowFullscreen">
  149. <IconButton v-if="!isFullScreen" icon="lucide:fullscreen" link />
  150. <IconButton v-else icon="material-symbols-light:fullscreen-exit-rounded" link />
  151. </div>
  152. </div>
  153. </Teleport>
  154. </template>
  155. <style lang="less" scoped>
  156. .code-editor {
  157. width: 100%;
  158. min-height: 150px;
  159. :deep(.monaco-editor) {
  160. height: 100%;
  161. }
  162. &.bordered {
  163. border: 1px solid var(--epic-border-color);
  164. }
  165. }
  166. </style>