| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- <script setup lang="ts">
- import { nextTick, onMounted, reactive, ref, watch, onBeforeUnmount, computed } from 'vue'
- import { useLocalEditorTheme } from "@/utils/useLocalEditorTheme"
- import type { editor } from 'monaco-editor'
- import { ElMessage } from "element-plus"
- import * as monaco from 'monaco-editor'
- import { IconButton } from '@repo/ui'
- import { debounce } from "lodash-es"
- interface CodeEditorType {
- // 是否展示工具栏
- tools?: boolean
- // 是否展示全屏工具
- allowFullscreen?: boolean
- // 是否展示复制工具
- copyCode?: boolean
- // 是否自动切换主题
- autoToggleTheme?: boolean
- // 挂载DOM
- appendTo?: string | HTMLElement
- // 内容
- modelValue?: any
- // 语法
- language?: string
- // 主题
- theme?: 'vs-light' | 'vs-dark'
- // 是否只读
- readOnly?: boolean
- // 是否显示行号
- lineNumbers?: 'on' | 'off'
- // 设置代码模块高度,总高度多40px
- height?: number
- // 返回结果为string,也可转化为json
- formatValue?: 'string' | 'json'
- // 插件内部配置
- config?: editor.IStandaloneEditorConstructionOptions
- }
- const props = withDefaults(defineProps<CodeEditorType>(),
- {
- //默认配置
- tools: true,
- copyCode: true,
- readOnly: false,
- allowFullscreen: true,
- autoToggleTheme: true,
- language: 'javascript',
- lineNumbers: 'on',
- theme: 'vs-light',
- formatValue: 'string',
- height: 150,
- config: () => ({
- minimap: { enabled: false },
- selectOnLineNumbers: true
- }),
- }
- )
- const emit = defineEmits(['update:modelValue', 'update:language'])
- let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
- let model: editor.ITextModel | null = null
- const editContainer = ref<HTMLElement>()
- const isFullScreen = ref(false)
- const {
- monacoTheme,
- themeClass,
- toggleTheme
- } = useLocalEditorTheme({
- defaultTheme: props.theme,
- autoToggleTheme: props.autoToggleTheme
- })
- let componentConfig = reactive({
- language: props.language
- })
- const languageSource = [
- { id: 'javascript', name: 'javascript' },
- { id: 'python', name: 'python' },
- { id: 'json', name: 'json' }
- ]
- /**
- * @description: formatValue为json 格式,需要转换处理
- * @return
- */
- const formatValue = (value: any) => {
- if (props.formatValue === 'json') {
- return JSON.stringify(value ?? {}, null, 2)
- }
- return value ?? ''
- }
- onMounted(() => {
- // 处理代码转换
- model = monaco.editor.createModel(
- formatValue(props.modelValue),
- componentConfig.language
- )
- // 接入配置
- monacoEditor = monaco.editor.create(editContainer.value!, {
- model,
- wordWrap: 'on',
- automaticLayout: true,
- theme: monacoTheme.value,
- readOnly: props.readOnly,
- lineNumbers: props.lineNumbers,
- ...props.config,
- })
- // 添加空格
- monacoEditor.onKeyDown((e) => {
- if (e.keyCode === monaco.KeyCode.Space) {
- e.preventDefault();
- monacoEditor?.trigger('keyboard', 'type', { text: ' ' });
- }
- });
- // 监听代码输入
- monacoEditor.onDidChangeModelContent(updateModelValue)
- })
- // 读取传值数据
- watch(() => props.modelValue, (value) => {
- nextTick(() => {
- if (!model) return
- const next = formatValue(value)
- if (model.getValue() !== next) {
- model.setValue(next)
- }
- })
- }, { immediate: true })
- // 切换语法,触发回调
- watch(() => componentConfig.language, (lang) => {
- nextTick(() => {
- if (!model) return
- monaco.editor.setModelLanguage(model, lang)
- emit('update:language', lang)
- })
- }, { immediate: true })
- // 监听全屏, 重新计算layout
- watch(isFullScreen, () => {
- nextTick(() => {
- monacoEditor?.layout()
- })
- })
- /**
- * @description: 发送回调(formatValue:json,需要转换处理)
- * @return {*}
- */
- const updateModelValue = debounce(() => {
- if (!model) return
- const value = model.getValue()
- if (props.formatValue === 'json') {
- try {
- emit('update:modelValue', JSON.parse(value))
- } catch {
- return ElMessage.warning('JSON 语法错误')
- }
- } else {
- emit('update:modelValue', value)
- }
- }, 1000)
- /**
- * @description: 复制代码到粘贴板
- * @return {*}
- */
- const copyCode = () => {
- const text = model?.getValue()
- if (!text) return
- try {
- navigator.clipboard.writeText(text).then(() => {
- ElMessage.success('复制成功!')
- })
- } catch {
- const textarea = document.createElement('textarea')
- textarea.value = text
- document.body.appendChild(textarea)
- textarea.select()
- document.execCommand('copy')
- textarea.remove()
- ElMessage.success('复制成功!')
- }
- }
- /**
- * @description: 设置全屏样式
- * @return {*}
- */
- const fullScreenStyle = computed(() => {
- if (isFullScreen.value) return {}
- return {
- height: `${props.height + 40}px`
- }
- })
- /**
- * @description: 切换主题, 并设置对应样式
- * @return {*}
- */
- const toolTipClass = computed(() => {
- return `editor-tooltip editor-tooltip--${themeClass.value}`
- })
- const onToggleTheme = () => {
- toggleTheme()
- }
- /**
- * 设置文本
- * @param text
- */
- function setValue(text: string) {
- monacoEditor?.setValue(text || '')
- }
- /**
- * 光标处插入文本
- * @param text
- */
- function insertText(text: string) {
- // 获取光标位置
- const position = monacoEditor?.getPosition()
- // 未获取到光标位置信息
- if (!position) {
- return
- }
- // 插入
- monacoEditor?.executeEdits('', [
- {
- range: new monaco.Range(
- position.lineNumber,
- position.column,
- position.lineNumber,
- position.column
- ),
- text
- }
- ])
- // 设置新的光标位置
- monacoEditor?.setPosition({
- ...position,
- column: position.column + text.length
- })
- // 重新聚焦
- monacoEditor?.focus()
- }
- // 销毁model
- onBeforeUnmount(() => {
- monacoEditor?.dispose()
- model?.dispose()
- })
- defineExpose({
- insertText,
- setValue
- })
- </script>
- <template>
- <Teleport :disabled="!appendTo" :to="appendTo">
- <div class="monacoEditor !m-0" :class="[themeClass, { 'is-fullscreen': isFullScreen }]"
- :style="fullScreenStyle">
- <div class="tools h-[33px] flex items-center justify-between gap-2" v-if="props.tools">
- <!-- 语法切换 -->
- <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="切换语法">
- <div class="w-1/3">
- <ElSelect v-model="componentConfig.language">
- <ElOption v-for="value in languageSource" :label="value.name" :value="value.id" />
- </ElSelect>
- </div>
- </ElTooltip>
- <div class="flex-1 flex items-center justify-end gap-1">
- <!-- 放大/缩小 -->
- <ElTooltip :effect="themeClass" placement="top" :popper-class="toolTipClass"
- :content="!isFullScreen ? '放大' : '恢复正常'">
- <div class="cursor-pointer text-xl text-center px-2" @click="isFullScreen = !isFullScreen"
- v-if="props.allowFullscreen">
- <IconButton :icon="isFullScreen ? 'lucide:minimize' : 'lucide:fullscreen'" link
- class="fullscreen" />
- </div>
- </ElTooltip>
- <!-- copy -->
- <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="复制">
- <div class="copy text-center px-2" @click="copyCode" v-if="props.copyCode">
- <IconButton icon="lucide:copy" link class="copyIcon"></IconButton>
- </div>
- </ElTooltip>
- <!-- 主题 -->
- <ElTooltip :effect="themeClass" :popper-class="toolTipClass" placement="top" content="主题">
- <div class="copy text-center px-2" @click="onToggleTheme">
- <IconButton :icon="themeClass === 'dark' ? 'lucide:moon' : 'lucide:sun'" link
- class="themeIcon">
- </IconButton>
- </div>
- </ElTooltip>
- </div>
- </div>
- <!-- 编辑器 -->
- <div class="editor-wrapper">
- <div ref="editContainer" class="code-editor"></div>
- </div>
- </div>
- </Teleport>
- </template>
- <style lang="less">
- .editor-tooltip {
- border-radius: 6px;
- font-size: 12px;
- padding: 6px 10px;
- }
- // light
- .editor-tooltip--light {
- background-color: #ffffff !important;
- color: #1e1e1e !important;
- border: 1px solid #e5e7eb;
- .el-popper__arrow::before {
- background: #ffffff !important;
- border: 1px solid #e5e7eb;
- }
- }
- // dark
- .editor-tooltip--dark {
- background-color: #1e1e1e !important;
- color: #ffffff !important;
- .el-popper__arrow::before {
- background: #1e1e1e !important;
- }
- }
- </style>
- <style lang="less" scoped>
- .monacoEditor {
- border: 1px solid #eee;
- display: flex;
- flex-direction: column;
- transition: height 0.25s ease, inset 0.25s ease, background-color 0.2s;
- &.is-fullscreen {
- position: absolute;
- inset: 0;
- z-index: 1000;
- }
- .tools {
- border-bottom: 1px solid #eeeeee83;
- padding-bottom: 6px;
- ::v-deep(.el-select .el-select__wrapper) {
- border: none;
- box-shadow: none;
- background-color: #ffff;
- }
- ::v-deep(.el-select .el-select__placeholder) {
- color: #1e1e1e;
- }
- }
- .editor-wrapper {
- flex: 1;
- overflow: hidden;
- }
- .code-editor {
- width: 100%;
- height: 100%;
- ::v-deep(.monaco-editor) {
- height: 100%;
- background-color: #ffffff;
- }
- &.bordered {
- border: 1px solid var(--epic-border-color);
- }
- }
- }
- .light {
- background-color: #ffffff;
- color: #1e1e1e;
- &:hover {
- color: #1e1e1e;
- }
- .tools {
- ::v-deep(.el-select .el-select__wrapper) {
- border: none;
- box-shadow: none;
- background-color: #ffffff;
- }
- ::v-deep(.el-select .el-select__placeholder) {
- color: #1e1e1e;
- }
- .fullscreen,
- .fullscreen-exit,
- .themeIcon,
- .copyIcon {
- color: #1e1e1e;
- }
- }
- }
- .dark {
- background-color: #1e1e1e;
- color: #ffffff;
- &:hover {
- color: #ffffff;
- }
- .tools {
- ::v-deep(.el-select .el-select__wrapper) {
- border: none;
- box-shadow: none;
- background-color: #1e1e1e;
- }
- ::v-deep(.el-select .el-select__placeholder) {
- color: #ffffff;
- }
- .fullscreen,
- .fullscreen-exit,
- .themeIcon,
- .copyIcon {
- color: #ffffff;
- }
- }
- }
- </style>
|