feat(editor): 组件配置中的样式支持单独一列显示

This commit is contained in:
roymondchen 2024-12-17 20:47:33 +08:00
parent 7a8da68edb
commit 5cd6d21b2e
13 changed files with 408 additions and 152 deletions

View File

@ -134,7 +134,7 @@ import type { MApp } from '@tmagic/core';
import Framework from './layouts/Framework.vue';
import TMagicNavMenu from './layouts/NavMenu.vue';
import PropsPanel from './layouts/PropsPanel.vue';
import PropsPanel from './layouts/props-panel/PropsPanel.vue';
import Sidebar from './layouts/sidebar/Sidebar.vue';
import Workspace from './layouts/workspace/Workspace.vue';
import codeBlockService from './services/codeBlock';

View File

@ -91,7 +91,8 @@ export { default as KeyValue } from './fields/KeyValue.vue';
export { default as CodeBlockList } from './layouts/sidebar/code-block/CodeBlockList.vue';
export { default as CodeBlockListPanel } from './layouts/sidebar/code-block/CodeBlockListPanel.vue';
export { default as DataSourceConfigPanel } from './layouts/sidebar/data-source/DataSourceConfigPanel.vue';
export { default as PropsPanel } from './layouts/PropsPanel.vue';
export { default as PropsPanel } from './layouts/props-panel/PropsPanel.vue';
export { default as PropsFormPanel } from './layouts/props-panel/FormPanel.vue';
export { default as ToolButton } from './components/ToolButton.vue';
export { default as ContentMenu } from './components/ContentMenu.vue';
export { default as Icon } from './components/Icon.vue';

View File

@ -20,7 +20,7 @@
v-model:left="columnWidth.left"
v-model:right="columnWidth.right"
:min-left="65"
:min-right="20"
:min-right="320"
:min-center="100"
:width="frameworkRect?.width || 0"
@change="columnWidthChange"
@ -50,9 +50,7 @@
</template>
<template v-if="page" #right>
<TMagicScrollbar>
<slot name="props-panel"></slot>
</TMagicScrollbar>
<slot name="props-panel"></slot>
</template>
</SplitView>
@ -65,7 +63,6 @@
import { computed, inject, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
import type { MPage, MPageFragment } from '@tmagic/core';
import { TMagicScrollbar } from '@tmagic/design';
import SplitView from '@editor/components/SplitView.vue';
import type { FrameworkSlots, GetColumnWidth, PageBarSortOptions, Services } from '@editor/type';

View File

@ -1,140 +0,0 @@
<template>
<div class="m-editor-props-panel" v-show="nodes.length === 1">
<slot name="props-panel-header"></slot>
<MForm
ref="configForm"
:class="propsPanelSize"
:popper-class="`m-editor-props-panel-popper ${propsPanelSize}`"
:size="propsPanelSize"
:init-values="values"
:config="curFormConfig"
:extend-state="extendState"
@change="submit"
@error="errorHandler"
></MForm>
<TMagicButton
v-if="!disabledShowSrc"
class="m-editor-props-panel-src-icon"
circle
size="large"
title="源码"
:type="showSrc ? 'primary' : ''"
@click="showSrc = !showSrc"
>
<MIcon :icon="DocumentIcon"></MIcon>
</TMagicButton>
<CodeEditor
v-if="showSrc"
class="m-editor-props-panel-src-code"
:height="`${editorContentHeight}px`"
:init-values="values"
:options="codeOptions"
:parse="true"
@save="saveCode"
></CodeEditor>
</div>
</template>
<script lang="ts" setup>
import {
computed,
getCurrentInstance,
inject,
onBeforeUnmount,
onMounted,
ref,
useTemplateRef,
watchEffect,
} from 'vue';
import { Document as DocumentIcon } from '@element-plus/icons-vue';
import type { MNode } from '@tmagic/core';
import { TMagicButton } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, FormValue } from '@tmagic/form';
import { MForm } from '@tmagic/form';
import MIcon from '@editor/components/Icon.vue';
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
import type { PropsPanelSlots, Services } from '@editor/type';
import CodeEditor from './CodeEditor.vue';
defineSlots<PropsPanelSlots>();
defineOptions({
name: 'MEditorPropsPanel',
});
defineProps<{
disabledShowSrc?: boolean;
extendState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
}>();
const codeOptions = inject('codeOptions', {});
const emit = defineEmits(['mounted', 'submit-error', 'form-error']);
const showSrc = ref(false);
const internalInstance = getCurrentInstance();
const values = ref<FormValue>({});
const configForm = useTemplateRef<InstanceType<typeof MForm>>('configForm');
// tsFormConfig any
const curFormConfig = ref<any>([]);
const services = inject<Services>('services');
const node = computed(() => services?.editorService.get('node'));
const nodes = computed(() => services?.editorService.get('nodes') || []);
const propsPanelSize = computed(() => services?.uiService.get('propsPanelSize') || 'small');
const stage = computed(() => services?.editorService.get('stage'));
const { height: editorContentHeight } = useEditorContentHeight();
const init = async () => {
if (!node.value) {
curFormConfig.value = [];
return;
}
const type = node.value.type || (node.value.items ? 'container' : 'text');
curFormConfig.value = (await services?.propsService.getPropsConfig(type)) || [];
values.value = node.value;
};
watchEffect(init);
services?.propsService.on('props-configs-change', init);
onMounted(() => {
emit('mounted', internalInstance);
});
onBeforeUnmount(() => {
services?.propsService.off('props-configs-change', init);
});
watchEffect(() => {
if (configForm.value && stage.value) {
configForm.value.formState.stage = stage.value;
}
});
const submit = async (v: FormValue, eventData: ContainerChangeEventData) => {
try {
const values = await configForm.value?.submitForm();
services?.editorService.update(values, { changeRecords: eventData.changeRecords });
} catch (e: any) {
emit('submit-error', e);
}
};
const errorHandler = (e: any) => {
emit('form-error', e);
};
const saveCode = (values: MNode) => {
services?.editorService.update(values);
};
defineExpose({ configForm, submit });
</script>

View File

@ -0,0 +1,123 @@
<template>
<div class="m-editor-props-form-panel">
<slot name="props-form-panel-header"></slot>
<TMagicScrollbar>
<MForm
ref="configForm"
:class="propsPanelSize"
:popper-class="`m-editor-props-panel-popper ${propsPanelSize}`"
:label-width="labelWidth"
:label-position="labelPosition"
:size="propsPanelSize"
:init-values="values"
:config="config"
:extend-state="extendState"
@change="submit"
@error="errorHandler"
></MForm>
</TMagicScrollbar>
<TMagicButton
v-if="!disabledShowSrc"
class="m-editor-props-panel-src-icon"
circle
title="源码"
:type="showSrc ? 'primary' : ''"
@click="showSrc = !showSrc"
>
<MIcon :icon="DocumentIcon"></MIcon>
</TMagicButton>
<CodeEditor
v-if="showSrc"
class="m-editor-props-panel-src-code"
:height="`${editorContentHeight}px`"
:init-values="codeValueKey ? values[codeValueKey] : values"
:options="codeOptions"
:parse="true"
@save="saveCode"
></CodeEditor>
</div>
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, inject, onMounted, ref, useTemplateRef, watchEffect } from 'vue';
import { Document as DocumentIcon } from '@element-plus/icons-vue';
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
import { MForm } from '@tmagic/form';
import MIcon from '@editor/components/Icon.vue';
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
import type { Services } from '@editor/type';
import CodeEditor from '../CodeEditor.vue';
defineSlots<{
'props-form-panel-header'(props: {}): any;
}>();
defineOptions({
name: 'MEditorFormPanel',
});
const props = defineProps<{
config: FormConfig;
values: FormValue;
disabledShowSrc?: boolean;
labelWidth?: string;
codeValueKey?: string;
labelPosition?: string;
extendState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
}>();
const emit = defineEmits<{
submit: [values: any, eventData?: ContainerChangeEventData];
'submit-error': [e: any];
'form-error': [e: any];
mounted: [internalInstance: any];
}>();
const services = inject<Services>('services');
const codeOptions = inject('codeOptions', {});
const showSrc = ref(false);
const propsPanelSize = computed(() => services?.uiService.get('propsPanelSize') || 'small');
const { height: editorContentHeight } = useEditorContentHeight();
const stage = computed(() => services?.editorService.get('stage'));
const configForm = useTemplateRef<InstanceType<typeof MForm>>('configForm');
watchEffect(() => {
if (configForm.value) {
configForm.value.formState.stage = stage.value;
configForm.value.formState.services = services;
}
});
const internalInstance = getCurrentInstance();
onMounted(() => {
emit('mounted', internalInstance);
});
const submit = async (v: FormValue, eventData: ContainerChangeEventData) => {
try {
const values = await configForm.value?.submitForm();
emit('submit', values, eventData);
} catch (e: any) {
emit('submit-error', e);
}
};
const errorHandler = (e: any) => {
emit('form-error', e);
};
const saveCode = (values: any) => {
emit('submit', props.codeValueKey ? { [props.codeValueKey]: values } : values);
};
defineExpose({ configForm, submit });
</script>

View File

@ -0,0 +1,142 @@
<template>
<div class="m-editor-props-panel" v-show="nodes.length === 1">
<slot name="props-panel-header"></slot>
<FormPanel
ref="propertyFormPanel"
class="m-editor-props-property-panel"
:class="{ 'show-style-panel': showStylePanel }"
:config="curFormConfig"
:values="values"
:disabledShowSrc="disabledShowSrc"
:extendState="extendState"
@submit="submit"
@submit-error="errorHandler"
@form-error="errorHandler"
@mounted="mountedHandler"
></FormPanel>
<FormPanel
v-if="showStylePanel"
class="m-editor-props-style-panel"
label-position="top"
code-value-key="style"
:config="styleFormConfig"
:values="values"
:disabledShowSrc="disabledShowSrc"
:extendState="extendState"
@submit="submit"
@submit-error="errorHandler"
@form-error="errorHandler"
>
<template #props-form-panel-header>
<div class="m-editor-props-style-panel-title">
<span>样式</span>
<div>
<TMagicButton link size="small" @click="closeStylePanelHandler"><MIcon :icon="Close"></MIcon></TMagicButton>
</div>
</div>
</template>
</FormPanel>
<TMagicButton
v-if="!showStylePanel"
class="m-editor-props-panel-style-icon"
circle
:type="showStylePanel ? 'primary' : ''"
@click="showStylePanelHandler"
>
<MIcon :icon="Sugar"></MIcon>
</TMagicButton>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, onBeforeUnmount, ref, useTemplateRef, watchEffect } from 'vue';
import { Close, Sugar } from '@element-plus/icons-vue';
import type { MNode } from '@tmagic/core';
import { TMagicButton } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, FormValue } from '@tmagic/form';
import MIcon from '@editor/components/Icon.vue';
import type { PropsPanelSlots, Services } from '@editor/type';
import { styleTabConfig } from '@editor/utils';
import FormPanel from './FormPanel.vue';
import { useStylePanel } from './use-style-panel';
defineSlots<PropsPanelSlots>();
defineOptions({
name: 'MEditorPropsPanel',
});
defineProps<{
disabledShowSrc?: boolean;
extendState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
}>();
const emit = defineEmits(['mounted', 'submit-error', 'form-error']);
const services = inject<Services>('services');
const values = ref<FormValue>({});
// tsFormConfig any
const curFormConfig = ref<any>([]);
const node = computed(() => services?.editorService.get('node'));
const nodes = computed(() => services?.editorService.get('nodes') || []);
const styleFormConfig = [
{
tabPosition: 'right',
items: styleTabConfig.items,
},
];
const init = async () => {
if (!node.value) {
curFormConfig.value = [];
return;
}
const type = node.value.type || (node.value.items ? 'container' : 'text');
curFormConfig.value = (await services?.propsService.getPropsConfig(type)) || [];
values.value = node.value;
};
watchEffect(init);
services?.propsService.on('props-configs-change', init);
onBeforeUnmount(() => {
services?.propsService.off('props-configs-change', init);
});
const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
try {
if (!v.id) {
v.id = values.value.id;
}
services?.editorService.update(v, { changeRecords: eventData?.changeRecords });
} catch (e: any) {
emit('submit-error', e);
}
};
const errorHandler = (e: any) => {
emit('form-error', e);
};
const mountedHandler = (e: any) => {
emit('mounted', e);
};
const { showStylePanel, showStylePanelHandler, closeStylePanelHandler } = useStylePanel(services);
const propertyFormPanelRef = useTemplateRef('propertyFormPanel');
defineExpose({
getFormState() {
return propertyFormPanelRef.value?.configForm?.formState;
},
submit,
});
</script>

View File

@ -0,0 +1,29 @@
import { computed } from 'vue';
import { Protocol } from '@editor/services/storage';
import { Services } from '@editor/type';
export const useStylePanel = (services?: Services) => {
const showStylePanelStorageKey = 'props-panel-show-style-panel';
const showStylePanelStorageValue = services?.storageService.getItem(showStylePanelStorageKey, {
protocol: Protocol.BOOLEAN,
});
if (typeof showStylePanelStorageValue === 'boolean') {
services?.uiService.set('showStylePanel', showStylePanelStorageValue);
}
const showStylePanel = computed(() => services?.uiService.get('showStylePanel') ?? true);
const showStylePanelHandler = () => {
services?.uiService.set('showStylePanel', true);
services?.storageService.setItem(showStylePanelStorageKey, true, { protocol: Protocol.BOOLEAN });
};
const closeStylePanelHandler = () => {
services?.uiService.set('showStylePanel', false);
services?.storageService.setItem(showStylePanelStorageKey, false, { protocol: Protocol.BOOLEAN });
};
return {
showStylePanel,
showStylePanelHandler,
closeStylePanelHandler,
};
};

View File

@ -82,7 +82,8 @@ export class WebStorage extends BaseService {
case Protocol.NUMBER:
return Number(item);
case Protocol.BOOLEAN:
return Boolean(item);
if (item === 'true') return true;
if (item === 'false') return false;
default:
return item;
}

View File

@ -29,6 +29,7 @@ import BaseService from './BaseService';
const state = reactive<UiState>({
uiSelectMode: false,
showSrc: false,
showStylePanel: true,
zoom: 1,
stageContainerRect: {
width: 0,

View File

@ -12,3 +12,5 @@ $sidebar-heder-background-color: $theme-color;
$sidebar-content-background-color: #ffffff;
$page-bar-height: 32px;
$props-style-panel-width: 300px;

View File

@ -1,5 +1,78 @@
@use "common/var" as *;
.m-editor-props-panel {
padding: 0 10px 50px 10px;
height: 100%;
position: relative;
--props-style-panel-width: 300px;
.m-editor-props-form-panel {
padding-bottom: 10px;
position: relative;
height: 100%;
box-sizing: border-box;
.tmagic-design-scrollbar {
height: 100%;
}
}
.m-editor-props-property-panel {
&.show-style-panel {
padding-right: var(--props-style-panel-width);
.m-editor-props-panel-src-icon {
right: calc(15px + var(--props-style-panel-width));
}
}
.tmagic-design-form {
padding-right: 10px;
padding-left: 10px;
> .m-container-tab {
> .tmagic-design-tabs {
> .el-tabs__content {
padding-top: 55px;
}
> .el-tabs__header.is-top {
position: absolute;
top: 0;
width: 100%;
background: #fff;
z-index: 2;
}
}
}
}
}
.m-editor-props-style-panel {
position: absolute;
width: var(--props-style-panel-width);
right: 0;
top: 0;
background: #fff;
z-index: 12;
$style-panel-title-height: 38px;
.tmagic-design-scrollbar {
height: calc(100% - $style-panel-title-height - 1px);
}
.m-editor-props-style-panel-title {
text-align: center;
font-size: 14px;
font-weight: 600;
padding: 0 5px;
height: $style-panel-title-height;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid $border-color;
}
}
.m-editor-props-panel-src-icon {
position: absolute;
@ -8,6 +81,13 @@
z-index: 30;
}
.m-editor-props-panel-style-icon {
position: absolute;
right: 15px;
bottom: 60px;
z-index: 30;
}
.m-editor-props-panel-src-code.magic-code-editor {
position: absolute;
left: 0;

View File

@ -231,6 +231,8 @@ export interface UiState {
uiSelectMode: boolean;
/** 是否显示整个配置源码, true: 显示, false: 不显示默认为false */
showSrc: boolean;
/** 是否将样式配置单独一列显示, true: 显示, false: 不显示默认为true */
showStylePanel: boolean;
/** 画布显示放大倍数,默认为 1 */
zoom: number;
/** 画布容器的宽高 */

View File

@ -18,6 +18,7 @@
*/
import { NODE_CONDS_KEY } from '@tmagic/core';
import { tMagicMessage } from '@tmagic/design';
import type { FormConfig, FormState, TabPaneConfig } from '@tmagic/form';
export const arrayOptions = [
@ -41,6 +42,7 @@ export const numberOptions = [
export const styleTabConfig: TabPaneConfig = {
title: '样式',
display: ({ services }: any) => !(services.uiService.get('showStylePanel') ?? true),
items: [
{
name: 'style',
@ -53,6 +55,7 @@ export const styleTabConfig: TabPaneConfig = {
type: 'data-source-field-select',
name: 'position',
text: '固定定位',
labelPosition: 'left',
checkStrictly: false,
dataSourceFieldType: ['string'],
fieldConfig: {
@ -213,7 +216,7 @@ export const styleTabConfig: TabPaneConfig = {
checkStrictly: false,
dataSourceFieldType: ['string'],
fieldConfig: {
type: 'text',
type: 'img-upload',
},
},
{
@ -344,11 +347,13 @@ export const advancedTabConfig: TabPaneConfig = {
{
name: 'created',
text: 'created',
labelPosition: 'top',
type: 'code-select',
},
{
name: 'mounted',
text: 'mounted',
labelPosition: 'top',
type: 'code-select',
},
],
@ -389,8 +394,21 @@ export const fillConfig = (config: FormConfig = [], labelWidth = '80px'): FormCo
// 组件id必须要有
{
name: 'id',
type: 'display',
text: 'id',
text: 'ID',
type: 'text',
disabled: true,
append: {
type: 'button',
text: '复制',
handler: async (vm, { model }) => {
try {
await navigator.clipboard.writeText(`${model.id}`);
tMagicMessage.success('已复制');
} catch (err) {
tMagicMessage.error('复制失败');
}
},
},
},
{
name: 'name',