<template>
<tiny-form
ref="createDataForm"
label-position="top"
class="create-form"
:model="state.createData"
:rules="rules"
validate-type="text"
:inline-message="true"
>
<tiny-form-item label="变量名" prop="name" class="var">
<tiny-input
v-model="state.createData.name"
placeholder="只能包含数字字母及下划线"
@change="$emit('nameInput', $event)"
></tiny-input>
</tiny-form-item>
<tiny-form-item label="初始值类型" class="var-type-item">
<tiny-radio-group v-model="state.variableType" :options="VAR_TYPES"></tiny-radio-group>
</tiny-form-item>
<tiny-collapse v-model="state.activeName">
<tiny-collapse-item :title="INIT" name="initValue">
<tiny-form-item>
<monaco-editor
ref="variableEditor"
class="variable-editor"
:value="editorCode"
:showFormatBtn="true"
:options="state.editorOptions"
@editorDidMount="editorDidMount"
@fullscreenChange="fullscreenChange"
>
<template #buttons>
<editor-i18n-tool ref="i18nToolRef" @confirm="insertContent"></editor-i18n-tool>
</template>
<template #fullscreenHead>
<state-fullscreen-head :title="INIT" @close="cancel"></state-fullscreen-head>
</template>
<template #fullscreenFooter>
<div class="fullscreen-footer-content">
<state-tips></state-tips>
</div>
</template>
</monaco-editor>
<state-tips></state-tips>
</tiny-form-item>
</tiny-collapse-item>
<tiny-collapse-item :title="GETTER" :name="GETTER">
<tiny-form-item>
<monaco-editor ref="getterEditor" class="variable-editor" :options="options" :value="state.getterEditorValue">
<template #fullscreenHead>
<state-fullscreen-head :title="GETTER" @close="cancel"></state-fullscreen-head>
</template>
<template #fullscreenFooter>
<div class="fullscreen-footer-content">
<div class="tips">
<pre>{{ getterExample }}</pre>
</div>
</div>
</template>
</monaco-editor>
<div class="tips">
<pre>{{ getterExample }}</pre>
</div>
</tiny-form-item>
</tiny-collapse-item>
<tiny-collapse-item :title="SETTER" :name="SETTER">
<tiny-form-item>
<monaco-editor ref="setterEditor" class="variable-editor" :options="options" :value="state.setterEditorValue">
<template #fullscreenHead>
<state-fullscreen-head :title="SETTER" @close="cancel"></state-fullscreen-head>
</template>
<template #fullscreenFooter>
<div class="fullscreen-footer-content">
<div class="tips">
<pre>{{ setterExample }}</pre>
</div>
</div>
</template>
</monaco-editor>
<div class="tips">
<pre>{{ setterExample }}</pre>
</div>
</tiny-form-item>
</tiny-collapse-item>
</tiny-collapse>
</tiny-form>
</template>
<script lang="ts">
/* metaService: engine.plugins.state.CreateVariable */
import { reactive, ref, computed, watch, onBeforeUnmount } from 'vue'
import {
Form,
FormItem,
Input,
RadioGroup,
Collapse as TinyCollapse,
CollapseItem as TinyCollapseItem
} from '@opentiny/vue'
import { MonacoEditor } from '@opentiny/tiny-engine-common'
import { verifyJsVarName } from '@opentiny/tiny-engine-common/js/verification'
import { initCompletion } from '@opentiny/tiny-engine-common/js/completion'
import * as Monaco from 'monaco-editor'
import { validateMonacoEditorData } from './js/common'
import EditorI18nTool from './EditorI18nTool.vue'
import StateTips from './StateTips.vue'
import StateFullscreenHead from './StateFullscreenHead.vue'
export default {
components: {
MonacoEditor,
StateTips,
StateFullscreenHead,
TinyForm: Form,
TinyFormItem: FormItem,
TinyInput: Input,
TinyRadioGroup: RadioGroup,
EditorI18nTool,
TinyCollapse,
TinyCollapseItem
},
props: {
createData: {
type: Object
},
dataSource: {
type: Object
},
flag: {
type: String
},
updateKey: {
type: String
}
},
emits: ['nameInput', 'close'],
setup(props, { emit }) {
const INIT = '初始值'
const GETTER = 'getter'
const SETTER = 'setter'
const variableEditor = ref(null)
const getterEditor = ref(null)
const setterEditor = ref(null)
const i18nToolRef = ref(null)
const getEditor = () => variableEditor.value
const isAccessorData = (data) => [data?.accessor?.getter?.type, data?.accessor?.setter?.type].includes('JSFunction')
const getPropsCreateData = () => ({
name: '',
...props.createData,
variable: isAccessorData(props.createData.variable)
? props.createData.variable.defaultValue
: props.createData.variable
})
const DEFAULT_GETTER = 'function getter() {}'
const DEFAULT_SETTER = 'function setter() {}'
const LANG_TYPES = {
JSON: 'json',
JS: 'javascript'
}
const VAR_TYPES = [
{ text: 'JSON类型', label: LANG_TYPES.JSON },
{ text: 'JS表达式类型', label: LANG_TYPES.JS }
]
const getVarType = () => (props.createData.variable?.type === 'JSExpression' ? LANG_TYPES.JS : LANG_TYPES.JSON)
const state = reactive({
errorMessage: '',
activeName: ['initValue', 'getter', 'setter'],
createData: getPropsCreateData(),
variableType: getVarType(),
getterEditorValue: props.createData.variable?.accessor?.getter?.value || DEFAULT_GETTER,
setterEditorValue: props.createData.variable?.accessor?.setter?.value || DEFAULT_SETTER,
editorOptions: {
language: getVarType(),
lineNumbers: true,
// 禁用滚动条边边一直显示的边框
overviewRulerBorder: false,
renderLineHighlightOnlyWhenFocus: true,
quickSuggestions: false, // 快速提示禁用,避免调用其他模块提供的函数,因为变量是最先初始化
suggest: {
showFields: false,
showFunctions: false
}
},
completionProvider: null
})
const changeLanguage = (language) => {
state.editorOptions.language = language
Monaco.editor.setModelLanguage(variableEditor.value.getEditor().getModel(), language)
}
watch(
() => props.createData.variable,
() => {
state.errorMessage = ''
state.getterEditorValue = props.createData.variable?.accessor?.getter?.value || DEFAULT_GETTER
state.setterEditorValue = props.createData.variable?.accessor?.setter?.value || DEFAULT_SETTER
state.variableType = getVarType()
if (state.editorOptions.language !== state.variableType) {
changeLanguage(state.variableType)
}
}
)
watch(
() => [props.createData.name, props.createData.variable],
() => {
state.createData = getPropsCreateData()
}
)
watch(
() => props.createData.name,
() => {
variableEditor.value?.switchFullScreen(false)
getterEditor.value?.switchFullScreen(false)
setterEditor.value?.switchFullScreen(false)
}
)
watch(
() => state.variableType,
() => {
changeLanguage(state.variableType)
}
)
const validate = () => {
if (state.errorMessage) {
return { success: false, message: state.errorMessage }
}
// JS表达式不进行校验
if (state.variableType === LANG_TYPES.JS) {
return { success: true }
}
return validateMonacoEditorData(variableEditor.value, '初始数据')
}
const getDefaultValue = () => {
// JS表达式使用字符串值,不进行解析
if (state.variableType === LANG_TYPES.JS) {
return { type: 'JSExpression', value: getEditor().getEditor().getValue() }
}
return getEditor().getValue()
}
const getFormData = () => {
const defaultValue = getDefaultValue()
const getter = getterEditor.value.getEditor().getValue()
const setter = setterEditor.value.getEditor().getValue()
if (!getter && !setter) return defaultValue
const result = { defaultValue }
if (getter && getter !== DEFAULT_GETTER) {
result.accessor = { ...result.accessor, getter: { type: 'JSFunction', value: getter } }
}
if (setter && setter !== DEFAULT_SETTER) {
result.accessor = { ...result.accessor, setter: { type: 'JSFunction', value: setter } }
}
// 没有设置 getter setter,需要直接返回 defaultValue
if (!result.accessor) {
return defaultValue
}
return result
}
const validateName = (rule, name, callback) => {
state.errorMessage = ''
if (!name) {
state.errorMessage = '输入内容不能为空'
} else if (!verifyJsVarName(name)) {
state.errorMessage = ' state 属性名称只能以字母或下划线开头且仅包含数字字母及下划线'
} else if (
Object.keys(props.dataSource).includes(name) &&
(props.flag !== 'update' || name !== props.updateKey)
) {
state.errorMessage = '已存在同名 state 属性'
}
if (state.errorMessage) {
callback(new Error(state.errorMessage))
} else {
callback()
}
}
const rules = {
name: { validator: validateName, required: true }
}
const editorCode = computed(() => {
const { type, value } = state.createData.variable || {}
if (type === 'JSExpression') {
return value
}
return JSON.stringify(state.createData.variable, null, 2)
})
const addContextMenu = (editorInstance, id, label, handler) => {
editorInstance.addAction({
id,
label,
precondition: null,
keybindingContext: null,
contextMenuGroupId: 'navigation',
contextMenuOrder: 1,
run: handler
})
}
const editorDidMount = (editorInstance) => {
addContextMenu(editorInstance, 'addI18n', '插入词条', () => {
i18nToolRef.value.state.showPopover = true
})
// 支持对象类型数据或表达式,不显示语法校验报错
const diagnosticsOptions = variableEditor.value.editor
.getMonaco()
.languages?.typescript?.javascriptDefaults.getDiagnosticsOptions()
variableEditor.value.editor.getMonaco().languages?.typescript?.javascriptDefaults.setDiagnosticsOptions({
...diagnosticsOptions,
noSyntaxValidation: true,
noSemanticValidation: true
})
if (variableEditor.value) {
state.completionProvider = initCompletion(
variableEditor.value.editor.getMonaco(),
variableEditor.value.editor.getEditor()?.getModel(),
(item) => item.label !== 'this.state' && !item.label.startsWith('this.state.')
)
}
}
const fullscreenChange = () => {
i18nToolRef.value.state.showPopover = false
}
onBeforeUnmount(() => {
state.completionProvider?.forEach((provider) => {
provider.dispose()
})
})
const insertContent = (insertText = '') => {
const monacoEditor = getEditor().editor.getEditor()
const selection = monacoEditor.getSelection()
const range = new Monaco.Range(
selection?.startLineNumber || 1,
selection?.startColumn || 1,
selection?.endLineNumber || 1,
selection?.endColumn || 1
)
monacoEditor.executeEdits('', [{ range, text: insertText }])
getEditor().formatCode()
monacoEditor.focus()
}
const cancel = () => {
emit('close')
}
const createDataForm = ref(null)
const validateForm = () => {
return new Promise((resolve) => {
createDataForm.value.validate((valid) => {
if (valid) {
resolve()
}
})
})
}
const clearValidateForm = () => {
createDataForm.value?.clearValidate()
}
const options = {
lineNumbers: true,
language: 'javascript',
// 禁用滚动条边边一直显示的边框
overviewRulerBorder: false,
renderLineHighlightOnlyWhenFocus: true
}
const getterExample =
'function getter() {\r\n // this.state.name = `${this.props.firstName} ${this.props.lastName}`\r\n}'
const setterExample =
"function setter() {\r\n // const [firstName, lastName] = this.state.name.split(' ')\r\n // this.emit('update:firstName', firstName)\r\n // this.emit('update:lastName', lastName)\r\n}"
return {
INIT,
GETTER,
SETTER,
state,
VAR_TYPES,
variableEditor,
getterEditor,
setterEditor,
i18nToolRef,
editorCode,
rules,
options,
getterExample,
setterExample,
getEditor,
validateName,
editorDidMount,
validate,
getFormData,
insertContent,
fullscreenChange,
cancel,
validateForm,
createDataForm,
clearValidateForm
}
}
}
</script>
<style lang="less" scoped>
.create-form {
height: calc(100% - 45px);
overflow-y: auto;
.tips {
font-size: 12px;
line-height: 18px;
margin-top: 8px;
border-radius: 4px;
padding: 8px 14px;
background: var(--te-state-tip-bg-color);
color: var(--te-state-tip-text-color);
& > pre {
font-family: Consolas, 'Courier New', monospace;
}
}
:deep(.toolbar) {
position: absolute;
z-index: 99;
right: 12px;
}
.var {
padding: 12px 12px 0 12px;
}
.var-type-item {
padding: 0 12px;
}
.tiny-form-item:not(:last-child) {
margin-bottom: 12px;
}
:deep(.tiny-form-item__label) {
color: var(--te-state-common-label-text-color);
}
.label-left-wrap {
color: var(--te-state-common-label-text-color);
display: flex;
}
:deep(.tiny-collapse-item__wrap) {
padding: 0 12px;
.tiny-collapse-item__content {
padding: 0;
.tiny-form-item:first-child {
padding-bottom: 12px;
}
}
}
}
.create-content-description {
font-size: 12px;
color: var(--te-state-common-text-color-emphasize);
margin-left: 8px;
cursor: pointer;
}
.variable-editor {
height: 270px;
}
</style>