tiny-engine/packages/common/component/MetaCodeEditor.vue

372 lines
9.9 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="editor-wrap">
<slot :open="open">
<div v-if="buttonShowContent" :class="['full-width', { 'empty-color': value === '' }]" @click="open">
<span class="text-content text-ellipsis-multiple">{{ value === '' ? buttonLabel : value }}</span>
<svg-icon class="edit-icon" name="edit"></svg-icon>
</div>
<tiny-button v-else class="edit-btn" @click="open">
{{ buttonLabel }}
</tiny-button>
</slot>
<tiny-dialog-box
v-model:visible="editorState.show"
:title="titleLabel"
width="50vw"
class="meta-code-editor-dialog-box"
append-to-body
:close-on-click-modal="false"
>
<div class="source-code">
<div v-if="editorTipsTitle" class="header-tips-container">
<span class="header-tips-title" :title="editorTipsTitle">{{ editorTipsTitle }}</span>
<div
v-if="editorTipsDemo"
class="header-tips-showdemo"
@click="editorState.showEditorDemo = !editorState.showEditorDemo"
>
<span>{{ editorState.showEditorDemo ? $t('common.collapseExample') : $t('common.expandExample') }}</span>
<icon-chevron-up v-if="editorState.showEditorDemo" class="collapse-icon"></icon-chevron-up>
<icon-chevron-down v-else class="collapse-icon"></icon-chevron-down>
</div>
</div>
<div v-if="editorState.showEditorDemo" class="header-tips-demo">
<div class="header-tips-demo-content lowcode-scrollbar-thin">
<pre><code>{{ editorTipsDemo }}</code></pre>
</div>
</div>
<monaco-editor
ref="editor"
class="source-code-content"
:value="value"
:options="options"
@editorDidMount="editorDidMount"
></monaco-editor>
<div v-if="showErrorMsg" class="error-msg">{{ editorState.errorMsg }}</div>
</div>
<template #footer>
<div class="btn-box">
<tiny-button
v-if="language === 'json' && showFormatBtn"
class="format-btn"
plain
type="danger"
@click="formatCode"
>
{{ $t('common.format') }}
</tiny-button>
<div>
<tiny-button @click="close">{{ $t('common.cancel') }}</tiny-button>
<tiny-button type="primary" @click="save">{{ $t('common.save') }}</tiny-button>
</div>
</div>
</template>
</tiny-dialog-box>
</div>
</template>
<script>
import { reactive, ref, computed, watchEffect, nextTick } from 'vue'
import { Button, DialogBox } from '@opentiny/vue'
import { iconChevronDown, iconChevronUp } from '@opentiny/vue-icon'
import VueMonaco from './VueMonaco.vue'
import i18n from '@opentiny/tiny-engine-controller/js/i18n'
import { formatString } from '@opentiny/tiny-engine-controller/js/ast'
export default {
components: {
MonacoEditor: VueMonaco,
TinyButton: Button,
TinyDialogBox: DialogBox,
IconChevronDown: iconChevronDown(),
IconChevronUp: iconChevronUp()
},
props: {
buttonText: {
type: [String, Object],
default: '编辑代码'
},
modelValue: {
type: [String, Object, Array],
default: ''
},
buttonShowContent: {
type: Boolean,
default: false
},
title: {
type: [String, Object],
default: ''
},
language: {
type: String,
default: 'javascript'
},
dataType: String,
single: {
type: Boolean,
default: false
},
theme: {
type: String
},
showFormatBtn: {
type: Boolean,
default: true
},
showErrorMsg: {
type: Boolean,
default: true
},
tips: {
// 代码编辑器上方提示title显示简短的文字描述demo为显示的示例点击 “展开示例” 可查看
type: Object,
default: () => ({ title: '', demo: '' })
}
},
emits: ['save', 'open'],
setup(props, { emit }) {
const { locale } = i18n.global
const editorState = reactive({
show: false,
created: false,
errorMsg: '',
showEditorDemo: false
})
const value = ref('')
const editor = ref(null)
const buttonLabel = computed(() => props.buttonText?.[locale.value] ?? props.buttonText)
const titleLabel = computed(() => props.title?.[locale.value] ?? props.title)
const editorTipsTitle = computed(() => props.tips?.title?.[locale.value] ?? props.tips?.title)
const editorTipsDemo = computed(() => props.tips?.demo?.[locale.value] ?? props.tips?.demo)
watchEffect(() => {
const { modelValue, dataType } = props
const val = dataType ? modelValue?.value || '' : modelValue
value.value = typeof val === 'string' ? val : JSON.stringify(val, null, 2)
})
// 关闭编辑器
const close = () => {
editorState.show = false
emit('close')
}
// 打开编辑器
const open = () => {
if (!editorState.created) {
editorState.created = true
}
editorState.show = true
emit('open')
nextTick(() => window.dispatchEvent(new Event('resize')))
}
const parseContent = (content = editor.value?.getEditor().getValue()) => {
let jsonData
if (props.language === 'json' && content) {
try {
jsonData = JSON.parse(content)
editorState.errorMsg = ''
} catch (error) {
editorState.errorMsg = error
}
}
return jsonData
}
const editorDidMount = (monacoInstance) => {
monacoInstance.onDidChangeModelContent(() => {
const newValue = monacoInstance.getValue()
parseContent(newValue)
})
}
const formatCode = () => {
let jsonStr = editor.value?.getEditor().getValue()
if (jsonStr) {
try {
jsonStr = formatString(jsonStr, 'json')
editor.value?.getEditor().setValue(jsonStr)
} catch (error) {
/* empty */
}
}
}
// 保存编辑器内容
const save = () => {
const { language, dataType, single } = props
const content = formatString(editor.value?.getEditor().getValue(), language)
emit('save', { content })
if (!single) {
let value = content
const Func = Function
try {
if (dataType) {
value = value === '' ? '' : { type: dataType, value }
} else if (language === 'json') {
// eslint-disable-next-line no-new-func
value = new Func(`return ${content}`)()
} else {
value = typeof props.modelValue === 'string' ? content : JSON.parse(content)
}
} catch (error) {
/* empty */
}
emit('update:modelValue', value)
}
close()
}
const getTheme = () => {
const defaultTheme = window?.TinyGlobalConfig?.theme || 'light'
return (props.theme || defaultTheme)?.includes('dark') ? 'vs-dark' : 'vs'
}
return {
save,
close,
open,
formatCode,
editorDidMount,
buttonLabel,
titleLabel,
editorTipsTitle,
editorTipsDemo,
editor,
editorState,
value,
options: {
theme: getTheme(),
tabSize: 2,
language: props.language,
autoIndent: true,
formatOnPaste: true,
automaticLayout: true,
roundedSelection: true,
minimap: {
enabled: false
}
}
}
}
}
</script>
<style lang="less" scoped>
.editor-wrap {
width: 100%;
.edit-btn {
color: var(--ti-lowcode-meta-codeEditor-color);
border-color: var(--ti-lowcode-meta-codeEditor-border-color);
&:hover {
color: var(--ti-lowcode-meta-codeEditor-hover-color);
border-color: var(--ti-lowcode-meta-codeEditor-border-hover-color);
}
}
}
.btn-box {
display: flex;
justify-content: flex-end;
&:has(.format-btn) {
justify-content: space-between;
}
}
.full-width {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 32px;
padding: 4px 8px;
border: 1px solid var(--ti-lowcode-meta-codeEditor-border-color);
border-radius: 6px;
&:hover {
border-color: var(--ti-lowcode-meta-codeEditor-border-hover-color);
}
.text-content {
--ellipsis-line: 1;
}
&.empty-color {
color: var(--ti-lowcode-common-text-desc-color);
}
.edit-icon {
margin-left: 4px;
flex-shrink: 0;
cursor: pointer;
color: var(--ti-lowcode-common-text-main-color);
}
}
.source-code {
height: 50vh;
display: flex;
flex-direction: column;
.header-tips-container {
display: flex;
height: 17px;
margin-bottom: 10px;
color: var(--ti-lowcode-meta-code-editor-header-tips-container-color);
.header-tips-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.header-tips-showdemo {
display: flex;
align-items: center;
margin-left: 10px;
white-space: nowrap;
cursor: pointer;
&:hover {
.collapse-icon {
color: var(--ti-lowcode-meta-code-editor-header-collapse-icon-hover-color);
}
}
.collapse-icon {
margin-left: 4px;
color: var(--ti-lowcode-meta-code-editor-header-collapse-icon-color);
}
}
}
.header-tips-demo {
overflow: hidden;
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 8px;
color: var(--ti-lowcode-meta-code-editor-header-tips-demo-color);
background-color: var(--ti-lowcode-meta-code-editor-header-tips-demo-bg-color);
.header-tips-demo-content {
max-height: 98px;
overflow-y: auto;
}
pre {
margin: 0px;
}
code {
font-family: Consolas, Menlo, Monaco, Courier New, monospace, serif;
}
}
.source-code-content {
overflow-y: auto;
flex: 1;
border: 1px solid var(--ti-lowcode-meta-code-editor-source-code-content-border-color);
}
.error-msg {
margin-top: 8px;
color: var(--ti-lowcode-meta-code-editor-err-msg-color);
font-weight: bold;
}
}
</style>