feat(editor): 支持拖拽调整页面顺序

This commit is contained in:
parisma 2024-06-28 15:07:45 +08:00 committed by roymondchen
parent 0ffc223459
commit 0c5485b1d0
11 changed files with 149 additions and 13 deletions

View File

@ -61,13 +61,15 @@
"keycon": "^1.4.0", "keycon": "^1.4.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moveable": "^0.53.0", "moveable": "^0.53.0",
"serialize-javascript": "^6.0.0" "serialize-javascript": "^6.0.0",
"sortablejs": "^1.15.2"
}, },
"devDependencies": { "devDependencies": {
"@types/events": "^3.0.0", "@types/events": "^3.0.0",
"@types/lodash-es": "^4.17.4", "@types/lodash-es": "^4.17.4",
"@types/node": "^18.19.0", "@types/node": "^18.19.0",
"@types/serialize-javascript": "^5.0.1", "@types/serialize-javascript": "^5.0.1",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vue/compiler-sfc": "^3.4.27", "@vue/compiler-sfc": "^3.4.27",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
@ -86,8 +88,8 @@
"@tmagic/stage": "workspace:*", "@tmagic/stage": "workspace:*",
"@tmagic/utils": "workspace:*", "@tmagic/utils": "workspace:*",
"monaco-editor": "^0.48.0", "monaco-editor": "^0.48.0",
"vue": "^3.4.27", "typescript": "*",
"typescript": "*" "vue": "^3.4.27"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"typescript": { "typescript": {

View File

@ -1,5 +1,5 @@
<template> <template>
<Framework :disabled-page-fragment="disabledPageFragment"> <Framework :disabled-page-fragment="disabledPageFragment" :page-bar-sort-options="pageBarSortOptions">
<template #header> <template #header>
<slot name="header"></slot> <slot name="header"></slot>
</template> </template>
@ -106,6 +106,7 @@
<template #page-bar><slot name="page-bar"></slot></template> <template #page-bar><slot name="page-bar"></slot></template>
<template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template> <template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template>
<template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template> <template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template>
<template #page-list-popover="{ list }"><slot name="page-list-popover" :list="list"></slot></template>
</Framework> </Framework>
</template> </template>

View File

@ -17,6 +17,7 @@ import type {
MenuBarData, MenuBarData,
MenuButton, MenuButton,
MenuComponent, MenuComponent,
PageBarSortOptions,
SideBarData, SideBarData,
StageRect, StageRect,
} from './type'; } from './type';
@ -90,6 +91,8 @@ export interface EditorProps {
/** 用于自定义组件树与画布的右键菜单 */ /** 用于自定义组件树与画布的右键菜单 */
customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[]; customContentMenu?: (menus: (MenuButton | MenuComponent)[], type: string) => (MenuButton | MenuComponent)[];
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>; extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 页面顺序拖拽配置参数 */
pageBarSortOptions?: PageBarSortOptions;
} }
export const defaultEditorProps = { export const defaultEditorProps = {

View File

@ -38,9 +38,10 @@
</slot> </slot>
<slot name="page-bar"> <slot name="page-bar">
<PageBar :disabled-page-fragment="disabledPageFragment"> <PageBar :disabled-page-fragment="disabledPageFragment" :page-bar-sort-options="pageBarSortOptions">
<template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template> <template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template>
<template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template> <template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template>
<template #page-list-popover="{ list }"><slot name="page-list-popover" :list="list"></slot></template>
</PageBar> </PageBar>
</slot> </slot>
</template> </template>
@ -63,7 +64,7 @@ import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { TMagicScrollbar } from '@tmagic/design'; import { TMagicScrollbar } from '@tmagic/design';
import SplitView from '@editor/components/SplitView.vue'; import SplitView from '@editor/components/SplitView.vue';
import type { FrameworkSlots, GetColumnWidth, Services } from '@editor/type'; import type { FrameworkSlots, GetColumnWidth, PageBarSortOptions, Services } from '@editor/type';
import { getConfig } from '@editor/utils/config'; import { getConfig } from '@editor/utils/config';
import PageBar from './page-bar/PageBar.vue'; import PageBar from './page-bar/PageBar.vue';
@ -78,6 +79,7 @@ defineOptions({
defineProps<{ defineProps<{
disabledPageFragment: boolean; disabledPageFragment: boolean;
pageBarSortOptions?: PageBarSortOptions;
}>(); }>();
const DEFAULT_LEFT_COLUMN_WIDTH = 310; const DEFAULT_LEFT_COLUMN_WIDTH = 310;

View File

@ -2,15 +2,19 @@
<div class="m-editor-page-bar-tabs"> <div class="m-editor-page-bar-tabs">
<SwitchTypeButton v-if="!disabledPageFragment" v-model="active" /> <SwitchTypeButton v-if="!disabledPageFragment" v-model="active" />
<PageBarScrollContainer :type="active"> <PageBarScrollContainer :type="active" :page-bar-sort-options="pageBarSortOptions">
<template #prepend> <template #prepend>
<AddButton :type="active"></AddButton> <AddButton :type="active"></AddButton>
<PageList :list="list">
<template #page-list-popover="{ list }"><slot name="page-list-popover" :list="list"></slot></template>
</PageList>
</template> </template>
<div <div
v-for="item in list" v-for="item in list"
class="m-editor-page-bar-item" class="m-editor-page-bar-item"
:key="item.id" :key="item.id"
:page-id="item.id"
:class="{ active: page?.id === item.id }" :class="{ active: page?.id === item.id }"
@click="switchPage(item.id)" @click="switchPage(item.id)"
> >
@ -61,11 +65,12 @@ import { Id, type MPage, type MPageFragment, NodeType } from '@tmagic/schema';
import { isPage, isPageFragment } from '@tmagic/utils'; import { isPage, isPageFragment } from '@tmagic/utils';
import ToolButton from '@editor/components/ToolButton.vue'; import ToolButton from '@editor/components/ToolButton.vue';
import type { Services } from '@editor/type'; import type { PageBarSortOptions, Services } from '@editor/type';
import { getPageFragmentList, getPageList } from '@editor/utils'; import { getPageFragmentList, getPageList } from '@editor/utils';
import AddButton from './AddButton.vue'; import AddButton from './AddButton.vue';
import PageBarScrollContainer from './PageBarScrollContainer.vue'; import PageBarScrollContainer from './PageBarScrollContainer.vue';
import PageList from './PageList.vue';
import SwitchTypeButton from './SwitchTypeButton.vue'; import SwitchTypeButton from './SwitchTypeButton.vue';
defineOptions({ defineOptions({
@ -74,6 +79,7 @@ defineOptions({
defineProps<{ defineProps<{
disabledPageFragment: boolean; disabledPageFragment: boolean;
pageBarSortOptions?: PageBarSortOptions;
}>(); }>();
const active = ref<NodeType.PAGE | NodeType.PAGE_FRAGMENT>(NodeType.PAGE); const active = ref<NodeType.PAGE | NodeType.PAGE_FRAGMENT>(NodeType.PAGE);

View File

@ -34,11 +34,12 @@ import {
type WatchStopHandle, type WatchStopHandle,
} from 'vue'; } from 'vue';
import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue'; import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
import Sortable, { SortableEvent } from 'sortablejs';
import { NodeType } from '@tmagic/schema'; import { Id, NodeType } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue'; import Icon from '@editor/components/Icon.vue';
import type { Services } from '@editor/type'; import type { PageBarSortOptions, Services } from '@editor/type';
defineOptions({ defineOptions({
name: 'MEditorPageBarScrollContainer', name: 'MEditorPageBarScrollContainer',
@ -46,23 +47,29 @@ defineOptions({
const props = defineProps<{ const props = defineProps<{
type: NodeType.PAGE | NodeType.PAGE_FRAGMENT; type: NodeType.PAGE | NodeType.PAGE_FRAGMENT;
pageBarSortOptions?: PageBarSortOptions;
}>(); }>();
const services = inject<Services>('services'); const services = inject<Services>('services');
const editorService = services?.editorService; const editorService = services?.editorService;
const uiService = services?.uiService; const uiService = services?.uiService;
const itemsContainer = ref<HTMLDivElement>(); const itemsContainer = ref<HTMLElement>();
const canScroll = ref(false); const canScroll = ref(false);
const showAddPageButton = computed(() => uiService?.get('showAddPageButton')); const showAddPageButton = computed(() => uiService?.get('showAddPageButton'));
const showPageListButton = computed(() => uiService?.get('showPageListButton'));
const itemsContainerWidth = ref(0); const itemsContainerWidth = ref(0);
const setCanScroll = () => { const setCanScroll = () => {
// //
// 37 = icon width 16 + padding 10 * 2 + border-right 1 // 37 = icon width 16 + padding 10 * 2 + border-right 1
itemsContainerWidth.value = (pageBar.value?.clientWidth || 0) - 37 * 2 - (showAddPageButton.value ? 37 : 21); itemsContainerWidth.value =
(pageBar.value?.clientWidth || 0) -
37 * 2 -
(showAddPageButton.value ? 37 : 21) -
(showPageListButton.value ? 37 : 0);
nextTick(() => { nextTick(() => {
if (itemsContainer.value) { if (itemsContainer.value) {
@ -126,6 +133,35 @@ const crateWatchLength = (length: ComputedRef<number>) =>
} else { } else {
scroll('end'); scroll('end');
} }
if (length > 1) {
const el = document.querySelector('.m-editor-page-bar-items') as HTMLElement;
let beforeDragList: Id[] = [];
const options = {
...{
dataIdAttr: 'page-id', //
onStart: async (event: SortableEvent) => {
if (typeof props.pageBarSortOptions?.beforeStart === 'function') {
await props.pageBarSortOptions.beforeStart(event, sortable);
}
beforeDragList = sortable.toArray();
},
onUpdate: async (event: SortableEvent) => {
await editorService?.sort(
beforeDragList[event.oldIndex as number],
beforeDragList[event.newIndex as number],
);
if (typeof props.pageBarSortOptions?.afterUpdate === 'function') {
await props.pageBarSortOptions.afterUpdate(event, sortable);
}
},
},
...{
...(props.pageBarSortOptions ? props.pageBarSortOptions : {}),
},
};
if (!el) return;
const sortable = new Sortable(el, options);
}
}); });
}, },
{ {

View File

@ -0,0 +1,55 @@
<template>
<div
v-if="showPageListButton"
id="m-editor-page-bar-list-icon"
class="m-editor-page-bar-item m-editor-page-bar-item-icon"
>
<TMagicPopover popper-class="page-bar-popover" placement="top" :width="160" trigger="hover">
<div>
<slot name="page-list-popover" :list="list">
<ToolButton
v-for="(item, index) in list"
:data="{
type: 'button',
text: item.devconfig?.tabName || item.name || item.id,
handler: () => switchPage(item.id),
}"
:key="index"
></ToolButton>
</slot>
</div>
<template #reference>
<TMagicIcon class="m-editor-page-list-menu-icon">
<Files></Files>
</TMagicIcon>
</template>
</TMagicPopover>
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue';
import { Files } from '@element-plus/icons-vue';
import { TMagicIcon, TMagicPopover } from '@tmagic/design';
import { Id, MPage, MPageFragment } from '@tmagic/schema';
import ToolButton from '@editor/components/ToolButton.vue';
import type { Services } from '@editor/type';
defineOptions({
name: 'MEditorPageList',
});
defineProps<{
list: MPage[] | MPageFragment[];
}>();
const services = inject<Services>('services');
const uiService = services?.uiService;
const editorService = services?.editorService;
const showPageListButton = computed(() => uiService?.get('showPageListButton'));
const switchPage = (id: Id) => {
editorService?.select(id);
};
</script>

View File

@ -47,6 +47,7 @@ const state = reactive<UiState>({
showRule: true, showRule: true,
propsPanelSize: 'small', propsPanelSize: 'small',
showAddPageButton: true, showAddPageButton: true,
showPageListButton: true,
hideSlideBar: false, hideSlideBar: false,
sideBarItems: [], sideBarItems: [],
navMenuRect: { navMenuRect: {

View File

@ -18,6 +18,19 @@
} }
} }
.m-editor-page-list-item {
display: flex;
width: 100%;
height: $--page-bar-height;
line-height: $--page-bar-height;
color: $--font-color;
z-index: 2;
overflow: hidden;
&:hover {
background-color: $--hover-color;
}
}
.m-editor-page-bar { .m-editor-page-bar {
display: flex; display: flex;
width: 100%; width: 100%;

View File

@ -18,6 +18,7 @@
import type { Component } from 'vue'; import type { Component } from 'vue';
import type EventEmitter from 'events'; import type EventEmitter from 'events';
import Sortable, { Options, SortableEvent } from 'sortablejs';
import type { PascalCasedProperties } from 'type-fest'; import type { PascalCasedProperties } from 'type-fest';
import type { ChildConfig, ColumnConfig, FilterFunction, FormConfig, FormItem, FormState, Input } from '@tmagic/form'; import type { ChildConfig, ColumnConfig, FilterFunction, FormConfig, FormItem, FormState, Input } from '@tmagic/form';
@ -56,7 +57,6 @@ import type { StageOverlayService } from './services/stageOverlay';
import type { StorageService } from './services/storage'; import type { StorageService } from './services/storage';
import type { UiService } from './services/ui'; import type { UiService } from './services/ui';
import type { UndoRedo } from './utils/undo-redo'; import type { UndoRedo } from './utils/undo-redo';
export interface FrameworkSlots { export interface FrameworkSlots {
header(props: {}): any; header(props: {}): any;
nav(props: {}): any; nav(props: {}): any;
@ -71,6 +71,7 @@ export interface FrameworkSlots {
'page-bar'(props: {}): any; 'page-bar'(props: {}): any;
'page-bar-title'(props: { page: MPage | MPageFragment }): any; 'page-bar-title'(props: { page: MPage | MPageFragment }): any;
'page-bar-popover'(props: { page: MPage | MPageFragment }): any; 'page-bar-popover'(props: { page: MPage | MPageFragment }): any;
'page-list-popover'(props: { list: MPage[] | MPageFragment[] }): any;
} }
export interface WorkspaceSlots { export interface WorkspaceSlots {
@ -239,6 +240,8 @@ export interface UiState {
propsPanelSize: 'large' | 'default' | 'small'; propsPanelSize: 'large' | 'default' | 'small';
/** 是否显示新增页面按钮 */ /** 是否显示新增页面按钮 */
showAddPageButton: boolean; showAddPageButton: boolean;
/** 是否在页面工具栏显示呼起页面列表按钮 */
showPageListButton: boolean;
/** 是否隐藏侧边栏 */ /** 是否隐藏侧边栏 */
hideSlideBar: boolean; hideSlideBar: boolean;
/** 侧边栏面板配置 */ /** 侧边栏面板配置 */
@ -762,3 +765,11 @@ export interface EventBus extends EventEmitter {
export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig; export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig;
export type PropsFormValueFunction = (data: { editorService: EditorService }) => Partial<MNode>; export type PropsFormValueFunction = (data: { editorService: EditorService }) => Partial<MNode>;
export type PartSortableOptions = Omit<Options, 'onStart' | 'onUpdate'>;
export interface PageBarSortOptions extends PartSortableOptions {
/** 在onUpdate之后调用 */
afterUpdate: (event: SortableEvent, sortable: Sortable) => void;
/** 在onStart之前调用 */
beforeStart: (event: SortableEvent, sortable: Sortable) => void;
}

6
pnpm-lock.yaml generated
View File

@ -346,6 +346,9 @@ importers:
serialize-javascript: serialize-javascript:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.2 version: 6.0.2
sortablejs:
specifier: ^1.15.2
version: 1.15.2
typescript: typescript:
specifier: '*' specifier: '*'
version: 5.4.5 version: 5.4.5
@ -365,6 +368,9 @@ importers:
'@types/serialize-javascript': '@types/serialize-javascript':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.4 version: 5.0.4
'@types/sortablejs':
specifier: ^1.15.8
version: 1.15.8
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.5(vite@5.3.1(@types/node@18.19.34)(sass@1.77.5)(terser@5.31.1))(vue@3.4.29(typescript@5.4.5)) version: 5.0.5(vite@5.3.1(@types/node@18.19.34)(sass@1.77.5)(terser@5.31.1))(vue@3.4.29(typescript@5.4.5))