@tmagic/editor 1.1.0-beta.10 → 1.1.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/style.css +46 -0
  2. package/dist/tmagic-editor.mjs +862 -740
  3. package/dist/tmagic-editor.mjs.map +1 -1
  4. package/dist/tmagic-editor.umd.js +864 -739
  5. package/dist/tmagic-editor.umd.js.map +1 -1
  6. package/package.json +10 -10
  7. package/src/Editor.vue +4 -3
  8. package/src/components/ContentMenu.vue +4 -4
  9. package/src/components/Icon.vue +6 -20
  10. package/src/components/ScrollBar.vue +147 -0
  11. package/src/components/ScrollViewer.vue +89 -44
  12. package/src/components/ToolButton.vue +40 -138
  13. package/src/fields/UISelect.vue +2 -2
  14. package/src/index.ts +2 -0
  15. package/src/layouts/NavMenu.vue +156 -23
  16. package/src/layouts/sidebar/LayerMenu.vue +3 -3
  17. package/src/layouts/sidebar/LayerPanel.vue +39 -8
  18. package/src/layouts/workspace/PageBar.vue +1 -1
  19. package/src/layouts/workspace/Stage.vue +8 -90
  20. package/src/layouts/workspace/ViewerMenu.vue +3 -3
  21. package/src/layouts/workspace/Workspace.vue +133 -138
  22. package/src/services/editor.ts +17 -6
  23. package/src/services/props.ts +26 -53
  24. package/src/services/ui.ts +16 -20
  25. package/src/theme/nav-menu.scss +6 -0
  26. package/src/type.ts +26 -10
  27. package/src/utils/index.ts +1 -0
  28. package/src/utils/operator.ts +1 -1
  29. package/src/utils/props.ts +0 -3
  30. package/src/utils/scroll-viewer.ts +90 -158
  31. package/src/utils/stage.ts +91 -0
  32. package/types/Editor.vue.d.ts +7 -7
  33. package/types/components/ContentMenu.vue.d.ts +8 -8
  34. package/types/components/Icon.vue.d.ts +62 -12
  35. package/types/components/ScrollBar.vue.d.ts +83 -0
  36. package/types/components/ScrollViewer.vue.d.ts +131 -19
  37. package/types/components/ToolButton.vue.d.ts +56 -59
  38. package/types/index.d.ts +2 -0
  39. package/types/layouts/NavMenu.vue.d.ts +92 -23
  40. package/types/layouts/sidebar/LayerMenu.vue.d.ts +7 -7
  41. package/types/layouts/sidebar/LayerPanel.vue.d.ts +17 -15
  42. package/types/layouts/workspace/Workspace.vue.d.ts +54 -4
  43. package/types/services/editor.d.ts +1 -0
  44. package/types/services/props.d.ts +40 -9
  45. package/types/services/ui.d.ts +1 -1
  46. package/types/type.d.ts +23 -10
  47. package/types/utils/index.d.ts +1 -0
  48. package/types/utils/props.d.ts +0 -1
  49. package/types/utils/scroll-viewer.d.ts +16 -18
  50. package/types/utils/stage.d.ts +3 -0
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export { default as ComponentListPanel } from './layouts/sidebar/ComponentListPa
44
44
  export { default as LayerPanel } from './layouts/sidebar/LayerPanel.vue';
45
45
  export { default as PropsPanel } from './layouts/PropsPanel.vue';
46
46
  export { default as ToolButton } from './components/ToolButton.vue';
47
+ export { default as ContentMenu } from './components/ContentMenu.vue';
48
+ export { default as Icon } from './components/Icon.vue';
47
49
 
48
50
  const defaultInstallOpt: InstallOptions = {
49
51
  // @todo, 自定义图片上传方法等编辑器依赖的外部选项
@@ -1,41 +1,174 @@
1
1
  <template>
2
2
  <div class="m-editor-nav-menu" :style="{ height: `${height}px` }">
3
3
  <div v-for="key in keys" :class="`menu-${key}`" :key="key" :style="`width: ${columnWidth?.[key]}px`">
4
- <tool-button :data="item" v-for="(item, index) in data[key]" :key="index"></tool-button>
4
+ <tool-button :data="item" v-for="(item, index) in buttons[key]" :key="index"></tool-button>
5
5
  </div>
6
6
  </div>
7
7
  </template>
8
8
 
9
- <script lang="ts">
10
- import { computed, defineComponent, inject, PropType } from 'vue';
9
+ <script lang="ts" setup>
10
+ import { computed, inject, markRaw } from 'vue';
11
+ import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
12
+
13
+ import { NodeType } from '@tmagic/schema';
11
14
 
12
15
  import ToolButton from '../components/ToolButton.vue';
13
- import { GetColumnWidth, MenuBarData, Services } from '../type';
16
+ import { ColumnLayout, GetColumnWidth, MenuBarData, MenuButton, MenuComponent, MenuItem, Services } from '../type';
14
17
 
15
- export default defineComponent({
16
- name: 'nav-menu',
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ data?: MenuBarData;
21
+ height?: number;
22
+ }>(),
23
+ {
24
+ data: () => ({}),
25
+ height: 35,
26
+ },
27
+ );
17
28
 
18
- components: { ToolButton },
29
+ const services = inject<Services>('services');
30
+ const uiService = services?.uiService;
19
31
 
20
- props: {
21
- data: {
22
- type: Object as PropType<MenuBarData>,
23
- default: () => ({}),
24
- },
32
+ const columnWidth = computed(() => services?.uiService.get<GetColumnWidth>('columnWidth'));
33
+ const keys = Object.values(ColumnLayout);
25
34
 
26
- height: {
27
- type: Number,
28
- },
29
- },
35
+ const showGuides = computed((): boolean => uiService?.get<boolean>('showGuides') ?? true);
36
+ const showRule = computed((): boolean => uiService?.get<boolean>('showRule') ?? true);
37
+ const zoom = computed((): number => uiService?.get<number>('zoom') ?? 1);
30
38
 
31
- setup(props) {
32
- const services = inject<Services>('services');
39
+ const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
40
+ const ctrl = isMac ? 'Command' : 'Ctrl';
33
41
 
34
- return {
35
- keys: computed(() => Object.keys(props.data) as Array<keyof MenuBarData>),
42
+ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
43
+ if (typeof item !== 'string') {
44
+ return [item];
45
+ }
46
+ const config: (MenuButton | MenuComponent)[] = [];
47
+ switch (item) {
48
+ case '/':
49
+ config.push({
50
+ type: 'divider',
51
+ className: 'divider',
52
+ });
53
+ break;
54
+ case 'zoom':
55
+ config.push(
56
+ ...getConfig('zoom-out'),
57
+ ...getConfig(`${parseInt(`${zoom.value * 100}`, 10)}%`),
58
+ ...getConfig('zoom-in'),
59
+ ...getConfig('scale-to-original'),
60
+ ...getConfig('scale-to-fit'),
61
+ );
62
+ break;
63
+ case 'delete':
64
+ config.push({
65
+ type: 'button',
66
+ className: 'delete',
67
+ icon: markRaw(Delete),
68
+ tooltip: `刪除(Delete)`,
69
+ disabled: () => services?.editorService.get('node')?.type === NodeType.PAGE,
70
+ handler: () => services?.editorService.remove(services?.editorService.get('node')),
71
+ });
72
+ break;
73
+ case 'undo':
74
+ config.push({
75
+ type: 'button',
76
+ className: 'undo',
77
+ icon: markRaw(Back),
78
+ tooltip: `后退(${ctrl}+z)`,
79
+ disabled: () => !services?.historyService.state.canUndo,
80
+ handler: () => services?.editorService.undo(),
81
+ });
82
+ break;
83
+ case 'redo':
84
+ config.push({
85
+ type: 'button',
86
+ className: 'redo',
87
+ icon: markRaw(Right),
88
+ tooltip: `前进(${ctrl}+Shift+z)`,
89
+ disabled: () => !services?.historyService.state.canRedo,
90
+ handler: () => services?.editorService.redo(),
91
+ });
92
+ break;
93
+ case 'zoom-in':
94
+ config.push({
95
+ type: 'button',
96
+ className: 'zoom-in',
97
+ icon: markRaw(ZoomIn),
98
+ tooltip: `放大(${ctrl}+=)`,
99
+ handler: () => uiService?.zoom(0.1),
100
+ });
101
+ break;
102
+ case 'zoom-out':
103
+ config.push({
104
+ type: 'button',
105
+ className: 'zoom-out',
106
+ icon: markRaw(ZoomOut),
107
+ tooltip: `縮小(${ctrl}+-)`,
108
+ handler: () => uiService?.zoom(-0.1),
109
+ });
110
+ break;
111
+ case 'scale-to-original':
112
+ config.push({
113
+ type: 'button',
114
+ className: 'scale-to-original',
115
+ icon: markRaw(ScaleToOriginal),
116
+ tooltip: `缩放到实际大小(${ctrl}+1)`,
117
+ handler: () => uiService?.set('zoom', 1),
118
+ });
119
+ break;
120
+ case 'scale-to-fit':
121
+ config.push({
122
+ type: 'button',
123
+ className: 'scale-to-fit',
124
+ icon: markRaw(FullScreen),
125
+ tooltip: `缩放以适应(${ctrl}+0)`,
126
+ handler: () => uiService?.set('zoom', uiService.calcZoom()),
127
+ });
128
+ break;
129
+ case 'rule':
130
+ config.push({
131
+ type: 'button',
132
+ className: 'rule',
133
+ icon: markRaw(Memo),
134
+ tooltip: showRule.value ? '隐藏标尺' : '显示标尺',
135
+ handler: () => uiService?.set('showRule', !showRule.value),
136
+ });
137
+ break;
138
+ case 'guides':
139
+ config.push({
140
+ type: 'button',
141
+ className: 'guides',
142
+ icon: markRaw(Grid),
143
+ tooltip: showGuides.value ? '隐藏参考线' : '显示参考线',
144
+ handler: () => uiService?.set('showGuides', !showGuides.value),
145
+ });
146
+ break;
147
+ default:
148
+ config.push({
149
+ type: 'text',
150
+ text: item,
151
+ });
152
+ }
153
+ return config;
154
+ };
36
155
 
37
- columnWidth: computed(() => services?.uiService.get<GetColumnWidth>('columnWidth')),
38
- };
39
- },
156
+ const buttons = computed(() => {
157
+ const data: {
158
+ [ColumnLayout.LEFT]: (MenuButton | MenuComponent)[];
159
+ [ColumnLayout.CENTER]: (MenuButton | MenuComponent)[];
160
+ [ColumnLayout.RIGHT]: (MenuButton | MenuComponent)[];
161
+ } = {
162
+ [ColumnLayout.LEFT]: [],
163
+ [ColumnLayout.CENTER]: [],
164
+ [ColumnLayout.RIGHT]: [],
165
+ };
166
+ keys.forEach((key) => {
167
+ const items = props.data[key] || [];
168
+ items.forEach((item) => {
169
+ data[key].push(...getConfig(item));
170
+ });
171
+ });
172
+ return data;
40
173
  });
41
174
  </script>
@@ -9,7 +9,7 @@ import { Delete, DocumentCopy, Files, Plus } from '@element-plus/icons-vue';
9
9
  import { NodeType } from '@tmagic/schema';
10
10
 
11
11
  import ContentMenu from '../../components/ContentMenu.vue';
12
- import type { ComponentGroup, MenuButton, MenuItem, Services } from '../../type';
12
+ import type { ComponentGroup, MenuButton, MenuComponent, Services } from '../../type';
13
13
 
14
14
  export default defineComponent({
15
15
  components: { ContentMenu },
@@ -22,7 +22,7 @@ export default defineComponent({
22
22
  const isPage = computed(() => node.value?.type === NodeType.PAGE);
23
23
  const componentList = computed(() => services?.componentListService.getList() || []);
24
24
 
25
- const layerContentMenu = inject<MenuItem[]>('layerContentMenu', []);
25
+ const layerContentMenu = inject<(MenuComponent | MenuButton)[]>('layerContentMenu', []);
26
26
 
27
27
  const createMenuItems = (group: ComponentGroup): MenuButton[] =>
28
28
  group.items.map((component) => ({
@@ -77,7 +77,7 @@ export default defineComponent({
77
77
 
78
78
  return {
79
79
  menu,
80
- menuData: computed<MenuItem[]>(() => [
80
+ menuData: computed<(MenuButton | MenuComponent)[]>(() => [
81
81
  {
82
82
  type: 'button',
83
83
  text: '新增',
@@ -15,6 +15,7 @@
15
15
  v-if="values.length"
16
16
  ref="tree"
17
17
  node-key="id"
18
+ empty-text="页面空荡荡的"
18
19
  draggable
19
20
  :default-expanded-keys="expandedKeys"
20
21
  :load="loadItems"
@@ -29,7 +30,8 @@
29
30
  @node-click="clickHandler"
30
31
  @node-contextmenu="contextmenu"
31
32
  @node-drag-end="handleDragEnd"
32
- empty-text="页面空荡荡的"
33
+ @node-collapse="handleCollapse"
34
+ @node-expand="handleExpand"
33
35
  >
34
36
  <template #default="{ node, data }">
35
37
  <div
@@ -60,7 +62,7 @@ import { computed, defineComponent, inject, Ref, ref, watchEffect } from 'vue';
60
62
  import type { ElTree } from 'element-plus';
61
63
  import { throttle } from 'lodash-es';
62
64
 
63
- import type { MNode, MPage } from '@tmagic/schema';
65
+ import type { Id, MNode, MPage } from '@tmagic/schema';
64
66
  import { MContainer, NodeType } from '@tmagic/schema';
65
67
  import StageCore from '@tmagic/stage';
66
68
 
@@ -122,22 +124,41 @@ const useDrop = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorServi
122
124
  });
123
125
 
124
126
  const useStatus = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorService?: EditorService) => {
125
- const highlightNode = ref<MNode>();
126
127
  const node = ref<MNode>();
127
128
  const page = computed(() => editorService?.get('page'));
129
+ const expandedKeys = new Map<Id, Id>();
130
+
131
+ const expandNodes = () => {
132
+ expandedKeys.forEach((key) => {
133
+ if (!tree.value) return;
134
+ tree.value.getNode(key)?.expand();
135
+ });
136
+ };
128
137
 
129
138
  watchEffect(() => {
130
139
  if (!tree.value) return;
131
- node.value = editorService?.get('node');
132
- node.value && tree.value.setCurrentKey(node.value.id, true);
140
+ if (!editorService) return;
141
+ node.value = editorService.get('node');
142
+
143
+ if (!node.value) return;
133
144
 
134
- const parent = editorService?.get('parent');
145
+ tree.value.setCurrentKey(node.value.id, true);
146
+
147
+ const parent = editorService.get('parent');
135
148
  if (!parent?.id) return;
136
149
 
137
150
  const treeNode = tree.value.getNode(parent.id);
138
151
  treeNode?.updateChildren();
139
152
 
140
- highlightNode.value = editorService?.get('highlightNode');
153
+ setTimeout(() => {
154
+ tree.value &&
155
+ Object.entries(tree.value.store.nodesMap).forEach(([id, node]) => {
156
+ if (node.expanded && node.data.items) {
157
+ expandedKeys.set(id, id);
158
+ }
159
+ });
160
+ expandNodes();
161
+ });
141
162
  });
142
163
 
143
164
  return {
@@ -153,9 +174,19 @@ const useStatus = (tree: Ref<InstanceType<typeof ElTree> | undefined>, editorSer
153
174
  resolve([]);
154
175
  },
155
176
 
156
- highlightNode,
177
+ highlightNode: computed(() => editorService?.get('highlightNode')),
157
178
  clickNode: node,
158
179
  expandedKeys: computed(() => (node.value ? [node.value.id] : [])),
180
+
181
+ handleCollapse: (data: MNode) => {
182
+ expandedKeys.delete(data.id);
183
+ },
184
+
185
+ handleExpand: (data: MNode) => {
186
+ const parent = editorService?.getParentById(data.id);
187
+ if (!parent?.id) return;
188
+ expandedKeys.set(parent.id, parent.id);
189
+ },
159
190
  };
160
191
  };
161
192
 
@@ -10,7 +10,7 @@
10
10
  <div class="m-editor-page-bar-title">
11
11
  <slot name="page-bar-title" :page="item">
12
12
  <el-tooltip effect="dark" placement="top-start" :content="item.name">
13
- <span>{{ item.name }}</span>
13
+ <span>{{ item.name || item.id }}</span>
14
14
  </el-tooltip>
15
15
  </slot>
16
16
  </div>
@@ -4,12 +4,14 @@
4
4
  ref="stageWrap"
5
5
  :width="stageRect?.width"
6
6
  :height="stageRect?.height"
7
+ :wrap-width="stageContainerRect?.width"
8
+ :wrap-height="stageContainerRect?.height"
7
9
  :zoom="zoom"
8
10
  >
9
11
  <div
10
12
  class="m-editor-stage-container"
11
13
  ref="stageContainer"
12
- :style="`transform: scale(${zoom})`"
14
+ :style="`transform: scale(${zoom});`"
13
15
  @contextmenu="contextmenuHandler"
14
16
  @drop="dropHandler"
15
17
  @dragover="dragoverHandler"
@@ -25,25 +27,11 @@ import { computed, inject, markRaw, onMounted, onUnmounted, ref, toRaw, watch, w
25
27
  import { cloneDeep } from 'lodash-es';
26
28
 
27
29
  import type { MApp, MContainer, MNode, MPage } from '@tmagic/schema';
28
- import StageCore, {
29
- calcValueByFontsize,
30
- getOffset,
31
- GuidesType,
32
- Runtime,
33
- SortEventData,
34
- UpdateEventData,
35
- } from '@tmagic/stage';
30
+ import StageCore, { calcValueByFontsize, getOffset, Runtime } from '@tmagic/stage';
36
31
 
37
32
  import ScrollViewer from '../../components/ScrollViewer.vue';
38
- import {
39
- H_GUIDE_LINE_STORAGE_KEY,
40
- Layout,
41
- Services,
42
- StageOptions,
43
- StageRect,
44
- V_GUIDE_LINE_STORAGE_KEY,
45
- } from '../../type';
46
- import { getGuideLineFromCache } from '../../utils';
33
+ import { Layout, Services, StageOptions, StageRect } from '../../type';
34
+ import { useStage } from '../../utils/stage';
47
35
 
48
36
  import ViewerMenu from './ViewerMenu.vue';
49
37
 
@@ -59,94 +47,24 @@ const menu = ref<InstanceType<typeof ViewerMenu>>();
59
47
 
60
48
  const isMultiSelect = computed(() => services?.editorService.get('nodes')?.length > 1);
61
49
  const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
62
- const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
50
+ const stageContainerRect = computed(() => services?.uiService.get<StageRect>('stageContainerRect'));
63
51
  const root = computed(() => services?.editorService.get<MApp>('root'));
64
52
  const page = computed(() => services?.editorService.get<MPage>('page'));
65
53
  const zoom = computed(() => services?.uiService.get<number>('zoom') || 1);
66
54
  const node = computed(() => services?.editorService.get<MNode>('node'));
67
55
 
68
- const getGuideLineKey = (key: string) => `${key}_${root.value?.id}_${page.value?.id}`;
69
-
70
56
  watchEffect(() => {
71
57
  if (stage) return;
72
58
 
73
59
  if (!stageContainer.value) return;
74
60
  if (!(stageOptions?.runtimeUrl || stageOptions?.render) || !root.value) return;
75
61
 
76
- stage = new StageCore({
77
- render: stageOptions.render,
78
- runtimeUrl: stageOptions.runtimeUrl,
79
- zoom: zoom.value,
80
- autoScrollIntoView: stageOptions.autoScrollIntoView,
81
- isContainer: stageOptions.isContainer,
82
- containerHighlightClassName: stageOptions.containerHighlightClassName,
83
- containerHighlightDuration: stageOptions.containerHighlightDuration,
84
- containerHighlightType: stageOptions.containerHighlightType,
85
- canSelect: (el, event, stop) => {
86
- const elCanSelect = stageOptions.canSelect(el);
87
- // 在组件联动过程中不能再往下选择,返回并触发 ui-select
88
- if (uiSelectMode.value && elCanSelect && event.type === 'mousedown') {
89
- document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
90
- return stop();
91
- }
92
-
93
- return elCanSelect;
94
- },
95
- moveableOptions: stageOptions.moveableOptions,
96
- updateDragEl: stageOptions.updateDragEl,
97
- });
62
+ stage = useStage(stageOptions);
98
63
 
99
64
  services?.editorService.set('stage', markRaw(stage));
100
65
 
101
66
  stage?.mount(stageContainer.value);
102
67
 
103
- stage.mask.setGuides([
104
- getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY)),
105
- getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY)),
106
- ]);
107
-
108
- stage?.on('select', (el: HTMLElement) => {
109
- services?.editorService.select(el.id);
110
- });
111
-
112
- stage?.on('highlight', (el: HTMLElement) => {
113
- services?.editorService.highlight(el.id);
114
- });
115
-
116
- stage?.on('multiSelect', (els: HTMLElement[]) => {
117
- services?.editorService.multiSelect(els.map((el) => el.id));
118
- });
119
-
120
- stage?.on('update', (ev: UpdateEventData) => {
121
- if (ev.parentEl) {
122
- for (const data of ev.data) {
123
- services?.editorService.moveToContainer({ id: data.el.id, style: data.style }, ev.parentEl.id);
124
- }
125
- return;
126
- }
127
-
128
- services?.editorService.update(ev.data.map((data) => ({ id: data.el.id, style: data.style })));
129
- });
130
-
131
- stage?.on('sort', (ev: SortEventData) => {
132
- services?.editorService.sort(ev.src, ev.dist);
133
- });
134
-
135
- stage?.on('changeGuides', (e) => {
136
- services?.uiService.set('showGuides', true);
137
-
138
- if (!root.value || !page.value) return;
139
-
140
- const storageKey = getGuideLineKey(
141
- e.type === GuidesType.HORIZONTAL ? H_GUIDE_LINE_STORAGE_KEY : V_GUIDE_LINE_STORAGE_KEY,
142
- );
143
- if (e.guides.length) {
144
- globalThis.localStorage.setItem(storageKey, JSON.stringify(e.guides));
145
- } else {
146
- globalThis.localStorage.removeItem(storageKey);
147
- }
148
- });
149
-
150
68
  if (!node.value?.id) return;
151
69
  stage?.on('runtime-ready', (rt) => {
152
70
  runtime = rt;
@@ -12,7 +12,7 @@ import { isPage } from '@tmagic/utils';
12
12
 
13
13
  import ContentMenu from '../../components/ContentMenu.vue';
14
14
  import storageService from '../../services/storage';
15
- import { LayerOffset, Layout, MenuItem, Services } from '../../type';
15
+ import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '../../type';
16
16
  import { COPY_STORAGE_KEY } from '../../utils/editor';
17
17
 
18
18
  const props = withDefaults(defineProps<{ isMultiSelect?: boolean }>(), { isMultiSelect: false });
@@ -28,9 +28,9 @@ const nodes = computed(() => editorService?.get<MNode[]>('nodes'));
28
28
  const parent = computed(() => editorService?.get('parent'));
29
29
  const stage = computed(() => editorService?.get<StageCore>('stage'));
30
30
 
31
- const stageContentMenu = inject<MenuItem[]>('stageContentMenu', []);
31
+ const stageContentMenu = inject<(MenuButton | MenuComponent)[]>('stageContentMenu', []);
32
32
 
33
- const menuData = reactive<MenuItem[]>([
33
+ const menuData = reactive<(MenuButton | MenuComponent)[]>([
34
34
  {
35
35
  type: 'button',
36
36
  text: '水平居中',