Răsfoiți Sursa

feat: Add a new component CodeEditor (#466)

lt5227 11 luni în urmă
părinte
comite
00989b7ac9

+ 9 - 0
mock/role/index.mock.ts

@@ -202,6 +202,14 @@ const adminList = [
             meta: {
               title: 'router.jsonEditor'
             }
+          },
+          {
+            path: 'code-editor',
+            component: 'views/Components/Editor/CodeEditor',
+            name: 'CodeEditor',
+            meta: {
+              title: 'router.codeEditor'
+            }
           }
         ]
       },
@@ -687,6 +695,7 @@ const testList: string[] = [
   '/components/editor-demo',
   '/components/editor-demo/editor',
   '/components/editor-demo/json-editor',
+  '/components/editor-demo/code-editor',
   '/components/search',
   '/components/descriptions',
   '/components/image-viewer',

+ 2 - 1
package.json

@@ -55,7 +55,8 @@
     "vue-json-pretty": "^2.4.0",
     "vue-router": "^4.3.0",
     "vue-types": "^5.1.1",
-    "xgplayer": "^3.0.14"
+    "xgplayer": "^3.0.14",
+    "monaco-editor": "^0.48.0"
   },
   "devDependencies": {
     "@commitlint/cli": "^19.2.1",

+ 3 - 0
src/components/CodeEditor/index.ts

@@ -0,0 +1,3 @@
+import CodeEditor from './src/CodeEditor.vue'
+
+export { CodeEditor }

+ 119 - 0
src/components/CodeEditor/src/CodeEditor.vue

@@ -0,0 +1,119 @@
+<script setup lang="tsx">
+import { useMonacoEditor } from '@/hooks/web/useMonacoEditor'
+import { onMounted, computed, watch, ref } from 'vue'
+import { ElSelect, ElOption, ElFormItem, ElForm } from 'element-plus'
+import { languageOptions, themeOptions } from './config/config'
+
+const props = withDefaults(
+  defineProps<{
+    width?: string | number
+    height?: string | number
+    languageSelector?: boolean
+    language?: string
+    themeSelector?: boolean
+    theme?: string
+    editorOption?: Object
+    modelValue: string
+  }>(),
+  {
+    width: '100%',
+    height: '100%',
+    languageSelector: true,
+    language: 'javascript',
+    themeSelector: true,
+    theme: 'vs-dark',
+    editorOption: () => ({}),
+    modelValue: ''
+  }
+)
+
+const emits = defineEmits<{
+  (e: 'blur'): void
+  (e: 'update:modelValue', val: string): void
+}>()
+
+const monacoEditorStyle = computed(() => {
+  return {
+    width: typeof props.width === 'string' ? props.width : props.width + 'px',
+    height: typeof props.height === 'string' ? props.height : props.height + 'px'
+  }
+})
+
+const {
+  monacoEditorRef,
+  createEditor,
+  updateVal,
+  updateOptions,
+  getEditor,
+  changeLanguage,
+  changeTheme
+} = useMonacoEditor(props.language)
+
+onMounted(() => {
+  const monacoEditor = createEditor(props.editorOption)
+  updateMonacoVal(props.modelValue)
+  monacoEditor?.onDidChangeModelContent(() => {
+    emits('update:modelValue', monacoEditor!.getValue())
+  })
+  monacoEditor?.onDidBlurEditorText(() => {
+    emits('blur')
+  })
+})
+
+watch(
+  () => props.modelValue,
+  () => {
+    updateMonacoVal(props.modelValue)
+  }
+)
+
+const localLanguage = ref(props.language)
+
+watch(localLanguage, (newLanguage) => {
+  changeLanguage(newLanguage)
+})
+
+const localTheme = ref(props.theme)
+watch(localTheme, (newTheme) => {
+  changeTheme(newTheme)
+})
+
+function updateMonacoVal(val: string) {
+  if (val !== getEditor()?.getValue()) {
+    updateVal(val)
+  }
+}
+
+defineExpose({ updateOptions })
+</script>
+
+<template>
+  <ElForm :inline="true">
+    <ElFormItem v-if="languageSelector" label="language" class="w-30% mb-5px!">
+      <ElSelect
+        v-model="localLanguage"
+        placeholder="Please select language"
+        size="small"
+        filterable
+      >
+        <ElOption
+          v-for="item in languageOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </ElSelect>
+    </ElFormItem>
+    <ElFormItem v-if="themeSelector" label="theme" class="w-30% mb-5px!">
+      <ElSelect v-model="localTheme" placeholder="Please select language" size="small" filterable>
+        <ElOption
+          v-for="item in themeOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </ElSelect>
+    </ElFormItem>
+  </ElForm>
+  <div ref="monacoEditorRef" :style="monacoEditorStyle"></div>
+</template>

+ 129 - 0
src/components/CodeEditor/src/config/config.ts

@@ -0,0 +1,129 @@
+export const languageOptions = [
+  { label: 'plaintext', value: 'plaintext' },
+  { label: 'abap', value: 'abap' },
+  { label: 'apex', value: 'apex' },
+  { label: 'azcli', value: 'azcli' },
+  { label: 'bat', value: 'bat' },
+  { label: 'bicep', value: 'bicep' },
+  { label: 'cameligo', value: 'cameligo' },
+  { label: 'clojure', value: 'clojure' },
+  { label: 'coffeescript', value: 'coffeescript' },
+  { label: 'c', value: 'c' },
+  { label: 'cpp', value: 'cpp' },
+  { label: 'csharp', value: 'csharp' },
+  { label: 'csp', value: 'csp' },
+  { label: 'css', value: 'css' },
+  { label: 'cypher', value: 'cypher' },
+  { label: 'dart', value: 'dart' },
+  { label: 'dockerfile', value: 'dockerfile' },
+  { label: 'ecl', value: 'ecl' },
+  { label: 'elixir', value: 'elixir' },
+  { label: 'flow9', value: 'flow9' },
+  { label: 'fsharp', value: 'fsharp' },
+  { label: 'freemarker2', value: 'freemarker2' },
+  {
+    label: 'freemarker2.tag-angle.interpolation-dollar',
+    value: 'freemarker2.tag-angle.interpolation-dollar'
+  },
+  {
+    label: 'freemarker2.tag-bracket.interpolation-dollar',
+    value: 'freemarker2.tag-bracket.interpolation-dollar'
+  },
+  {
+    label: 'freemarker2.tag-angle.interpolation-bracket',
+    value: 'freemarker2.tag-angle.interpolation-bracket'
+  },
+  {
+    label: 'freemarker2.tag-bracket.interpolation-bracket',
+    value: 'freemarker2.tag-bracket.interpolation-bracket'
+  },
+  {
+    label: 'freemarker2.tag-auto.interpolation-dollar',
+    value: 'freemarker2.tag-auto.interpolation-dollar'
+  },
+  {
+    label: 'freemarker2.tag-auto.interpolation-bracket',
+    value: 'freemarker2.tag-auto.interpolation-bracket'
+  },
+  { label: 'go', value: 'go' },
+  { label: 'graphql', value: 'graphql' },
+  { label: 'handlebars', value: 'handlebars' },
+  { label: 'hcl', value: 'hcl' },
+  { label: 'html', value: 'html' },
+  { label: 'ini', value: 'ini' },
+  { label: 'java', value: 'java' },
+  { label: 'javascript', value: 'javascript' },
+  { label: 'julia', value: 'julia' },
+  { label: 'kotlin', value: 'kotlin' },
+  { label: 'less', value: 'less' },
+  { label: 'lexon', value: 'lexon' },
+  { label: 'lua', value: 'lua' },
+  { label: 'liquid', value: 'liquid' },
+  { label: 'm3', value: 'm3' },
+  { label: 'markdown', value: 'markdown' },
+  { label: 'mdx', value: 'mdx' },
+  { label: 'mips', value: 'mips' },
+  { label: 'msdax', value: 'msdax' },
+  { label: 'mysql', value: 'mysql' },
+  { label: 'objective-c', value: 'objective-c' },
+  { label: 'pascal', value: 'pascal' },
+  { label: 'pascaligo', value: 'pascaligo' },
+  { label: 'perl', value: 'perl' },
+  { label: 'pgsql', value: 'pgsql' },
+  { label: 'php', value: 'php' },
+  { label: 'pla', value: 'pla' },
+  { label: 'postiats', value: 'postiats' },
+  { label: 'powerquery', value: 'powerquery' },
+  { label: 'powershell', value: 'powershell' },
+  { label: 'proto', value: 'proto' },
+  { label: 'pug', value: 'pug' },
+  { label: 'python', value: 'python' },
+  { label: 'qsharp', value: 'qsharp' },
+  { label: 'r', value: 'r' },
+  { label: 'razor', value: 'razor' },
+  { label: 'redis', value: 'redis' },
+  { label: 'redshift', value: 'redshift' },
+  { label: 'restructuredtext', value: 'restructuredtext' },
+  { label: 'ruby', value: 'ruby' },
+  { label: 'rust', value: 'rust' },
+  { label: 'sb', value: 'sb' },
+  { label: 'scala', value: 'scala' },
+  { label: 'scheme', value: 'scheme' },
+  { label: 'scss', value: 'scss' },
+  { label: 'shell', value: 'shell' },
+  { label: 'sol', value: 'sol' },
+  { label: 'aes', value: 'aes' },
+  { label: 'sparql', value: 'sparql' },
+  { label: 'sql', value: 'sql' },
+  { label: 'st', value: 'st' },
+  { label: 'swift', value: 'swift' },
+  { label: 'systemverilog', value: 'systemverilog' },
+  { label: 'verilog', value: 'verilog' },
+  { label: 'tcl', value: 'tcl' },
+  { label: 'twig', value: 'twig' },
+  { label: 'typescript', value: 'typescript' },
+  { label: 'vb', value: 'vb' },
+  { label: 'wgsl', value: 'wgsl' },
+  { label: 'xml', value: 'xml' },
+  { label: 'yaml', value: 'yaml' },
+  { label: 'json', value: 'json' }
+]
+
+export const themeOptions = [
+  {
+    label: 'vs',
+    value: 'vs'
+  },
+  {
+    label: 'vs-dark',
+    value: 'vs-dark'
+  },
+  {
+    label: 'hc-black',
+    value: 'hc-black'
+  },
+  {
+    label: 'hc-light',
+    value: 'hc-light'
+  }
+]

+ 129 - 0
src/hooks/web/useMonacoEditor.ts

@@ -0,0 +1,129 @@
+import * as monaco from 'monaco-editor'
+import { ref, nextTick, onBeforeUnmount } from 'vue'
+import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
+import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
+import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
+import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
+import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
+
+self.MonacoEnvironment = {
+  getWorker(_, label) {
+    if (label === 'json') {
+      return new jsonWorker()
+    }
+    if (label === 'css' || label === 'scss' || label === 'less') {
+      return new cssWorker()
+    }
+    if (label === 'html' || label === 'handlebars' || label === 'razor') {
+      return new htmlWorker()
+    }
+    if (label === 'typescript' || label === 'javascript') {
+      return new tsWorker()
+    }
+    return new editorWorker()
+  }
+}
+
+export function useMonacoEditor(language: string = 'javascript') {
+  // 编辑器示例
+  let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
+  // 目标元素
+  const monacoEditorRef = ref<HTMLElement>()
+
+  // 创建实例
+  function createEditor(editorOption: monaco.editor.IStandaloneEditorConstructionOptions = {}) {
+    if (!monacoEditorRef.value) return
+    monacoEditor = monaco.editor.create(monacoEditorRef.value, {
+      // 初始模型
+      model: monaco.editor.createModel('', language),
+      // 是否启用预览图
+      minimap: { enabled: true },
+      // 圆角
+      roundedSelection: true,
+      // 主题
+      theme: 'vs-dark',
+      // 主键
+      multiCursorModifier: 'ctrlCmd',
+      // 滚动条
+      scrollbar: {
+        verticalScrollbarSize: 8,
+        horizontalScrollbarSize: 8
+      },
+      // 行号
+      lineNumbers: 'on',
+      // tab大小
+      tabSize: 2,
+      //字体大小
+      fontSize: 14,
+      // 控制编辑器在用户键入、粘贴、移动或缩进行时是否应自动调整缩进
+      autoIndent: 'advanced',
+      // 自动布局
+      automaticLayout: true,
+      ...editorOption
+    })
+    return monacoEditor
+  }
+
+  // 格式化
+  async function formatDoc() {
+    await monacoEditor?.getAction('editor.action.formatDocument')?.run()
+  }
+
+  // 数据更新
+  function updateVal(val: string) {
+    nextTick(() => {
+      if (getOption(monaco.editor.EditorOption.readOnly)) {
+        updateOptions({ readOnly: false })
+      }
+      monacoEditor?.setValue(val)
+      setTimeout(async () => {
+        await formatDoc()
+      }, 10)
+    })
+  }
+
+  // 配置更新
+  function updateOptions(opt: monaco.editor.IStandaloneEditorConstructionOptions) {
+    monacoEditor?.updateOptions(opt)
+  }
+
+  // 获取配置
+  function getOption(name: monaco.editor.EditorOption) {
+    return monacoEditor?.getOption(name)
+  }
+
+  // 获取实例
+  function getEditor() {
+    return monacoEditor
+  }
+
+  function changeLanguage(newLanguage: string) {
+    const model = monacoEditor?.getModel()
+    if (model) {
+      monaco.editor.setModelLanguage(model, newLanguage)
+    }
+  }
+
+  function changeTheme(newTheme: string) {
+    monaco.editor.setTheme(newTheme)
+  }
+
+  // 页面离开 销毁
+  onBeforeUnmount(() => {
+    if (monacoEditor) {
+      monacoEditor.dispose()
+    }
+  })
+
+  return {
+    monacoEditorRef,
+    createEditor,
+    getEditor,
+    updateVal,
+    updateOptions,
+    getOption,
+    formatDoc,
+    changeLanguage,
+    changeTheme
+  }
+}

+ 4 - 1
src/locales/en.ts

@@ -153,6 +153,7 @@ export default {
     editor: 'Editor',
     richText: 'Rich text',
     jsonEditor: 'JSON Editor',
+    codeEditor: 'Code Editor',
     dialog: 'Dialog',
     imageViewer: 'Image viewer',
     descriptions: 'Descriptions',
@@ -473,7 +474,9 @@ export default {
     richText: 'Rich text',
     richTextDes: 'Secondary packaging based on wangeditor',
     jsonEditor: 'JSON Editor',
-    jsonEditorDes: 'Secondary packaging based on vue-json-pretty'
+    jsonEditorDes: 'Secondary packaging based on vue-json-pretty',
+    codeEditor: 'Code Editor',
+    codeEditorDes: 'Secondary packaging based on monaco-editor'
   },
   dialogDemo: {
     dialog: 'Dialog',

+ 4 - 1
src/locales/zh-CN.ts

@@ -151,6 +151,7 @@ export default {
     editor: '编辑器',
     richText: '富文本',
     jsonEditor: 'JSON编辑器',
+    codeEditor: '代码编辑器',
     dialog: '弹窗',
     imageViewer: '图片预览',
     descriptions: '描述',
@@ -464,7 +465,9 @@ export default {
     richText: '富文本',
     richTextDes: '基于 wangeditor 二次封装',
     jsonEditor: 'JSON编辑器',
-    jsonEditorDes: '基于 vue-json-pretty 二次封装'
+    jsonEditorDes: '基于 vue-json-pretty 二次封装',
+    codeEditor: '代码编辑器',
+    codeEditorDes: '基于 monaco-editor 二次封装'
   },
   dialogDemo: {
     dialog: '弹窗',

+ 23 - 0
src/views/Components/Editor/CodeEditor.vue

@@ -0,0 +1,23 @@
+<script setup lang="tsx">
+import { CodeEditor } from '@/components/CodeEditor'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ContentWrap } from '@/components/ContentWrap'
+import { ref } from 'vue'
+import { BaseButton } from '@/components/Button'
+import { ElDivider } from 'element-plus'
+const content = ref(
+  'public class HelloWorld {\n  public static void main(String[] args) {\n    System.out.println("Hello, World!");\n  }\n}'
+)
+const { t } = useI18n()
+
+const MonacoEditRef = ref<InstanceType<typeof CodeEditor>>()
+</script>
+<template>
+  <ContentWrap :title="t('richText.codeEditor')" :message="t('richText.codeEditorDes')">
+    <BaseButton @click="console.log(content)">控制台打印内容</BaseButton>
+    <ElDivider />
+    <div class="edit-container h-60vh">
+      <CodeEditor ref="MonacoEditRef" v-model="content" language="java" />
+    </div>
+  </ContentWrap>
+</template>