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

395 lines
9.7 KiB
Vue
Raw 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 text-line-clamp">{{
value === '' ? buttonLabel : value
}}</span>
<svg-icon class="edit-icon" name="to-edit"></svg-icon>
</div>
<tiny-button v-else class="edit-btn" @click="open">
<slot name="icon"></slot>
<svg-icon
name="page-schema"
v-if="[buttonText[locale], buttonText].includes(EDIT_CODE_TEXT)"
class="edit-btn-icon"
></svg-icon>
{{ 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>
<div class="source-code-split-panel-wrapper">
<tiny-split v-model="editorState.splitWidth">
<template #left>
<monaco-editor
ref="editor"
class="source-code-content"
:value="value"
:options="options"
@editorDidMount="editorDidMount"
></monaco-editor>
</template>
<template #right>
<div v-if="editorTipsDemo" class="source-code-split-panel-wrapper-right">
<div class="header-tips-demo-content lowcode-scrollbar-thin">
<span>{{ $t('common.exampleCode') }}</span>
<pre><code>{{ editorTipsDemo }}</code></pre>
</div>
</div>
</template>
</tiny-split>
</div>
<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 lang="ts">
import { computed, nextTick, reactive, ref, watchEffect } from 'vue'
import { Button, DialogBox, Split } from '@opentiny/vue'
import VueMonaco from './VueMonaco.vue'
import { formatString } from '../js/ast'
import i18n from '../js/i18n'
export default {
components: {
TinySplit: Split,
MonacoEditor: VueMonaco,
TinyButton: Button,
TinyDialogBox: DialogBox
},
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', 'close'],
setup(props, { emit }) {
const { locale } = i18n.global
const editorState = reactive({
show: false,
created: false,
errorMsg: '',
showEditorDemo: false,
splitWidth: props.tips?.demo ? 0.7 : 1
})
const value = ref('')
const editor = ref<any>(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)
const EDIT_CODE_TEXT = '编辑代码'
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: any) {
editorState.errorMsg = error
}
}
return jsonData
}
const editorDidMount = (monacoInstance: { onDidChangeModelContent: any; getValue: () => any }) => {
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') {
value = new Func(`return ${content}`)()
} else {
value = typeof props.modelValue === 'string' ? content : JSON.parse(content)
}
} catch (error) {
/* empty */
}
emit('update:modelValue', value)
}
close()
}
return {
save,
close,
open,
formatCode,
editorDidMount,
buttonLabel,
titleLabel,
editorTipsTitle,
editorTipsDemo,
editor,
editorState,
value,
EDIT_CODE_TEXT,
options: {
language: props.language,
minimap: {
enabled: false
}
},
locale
}
}
}
</script>
<style lang="less" scoped>
.editor-wrap {
width: 100%;
display: flex;
text-align: center;
.tiny-button.tiny-button.edit-btn {
color: var(--te-component-common-text-color-primary);
border-color: var(--te-component-common-border-color);
flex: 1;
text-align: center;
margin-right: 0;
&:hover {
border-color: var(--te-component-common-border-color-active);
}
&:focus {
border-color: var(--te-component-common-border-color-active);
}
.edit-btn-icon {
font-size: 14px;
margin-right: 4px;
vertical-align: text-top;
color: var(--te-component-common-icon-color);
}
}
}
.btn-box {
display: flex;
justify-content: flex-end;
width: 100%;
&:has(.format-btn) {
justify-content: space-between;
}
}
.full-width {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 24px;
padding: 4px;
padding-left: 10px;
padding-right: 12px;
border: 1px solid var(--te-component-common-border-color);
border-radius: var(--te-base-border-radius-1);
&:hover {
border-color: var(--te-component-common-border-color-divider);
}
.text-content {
--ellipsis-line: 1;
}
&.empty-color {
color: var(--te-component-common-text-color-weaken);
}
.edit-icon {
margin-left: 4px;
flex-shrink: 0;
cursor: pointer;
color: var(--te-component-common-text-color-secondary);
}
}
.text-line-clamp {
-webkit-line-clamp: 1;
}
:deep(.tiny-dialog-box__header) {
padding-bottom: 15px;
}
:deep(.tiny-dialog-box__title) {
font-size: 14px;
}
.source-code {
height: 50vh;
display: flex;
flex-direction: column;
.header-tips-container {
display: flex;
height: 17px;
margin-bottom: 10px;
color: var(--te-component-common-text-color-primary);
.header-tips-title {
color: var(--te-component-common-text-color-weaken);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.source-code-content {
overflow-y: auto;
height: 100%;
flex: 1;
}
.source-code-split-panel-wrapper {
height: 100%;
:deep(.tiny-split-trigger) {
width: 2px;
}
&-right {
padding: 16px 0 0 16px;
pre {
margin: 8px;
}
code {
font-family: Microsoft YaHei, Microsoft YaHei-Normal;
color: var(--te-component-common-text-color-weaken);
}
}
}
.error-msg {
margin-top: 8px;
color: var(--te-component-common-error-color);
font-weight: bold;
}
}
</style>