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

View File

@ -1,5 +1,5 @@
<template>
<Framework :disabled-page-fragment="disabledPageFragment">
<Framework :disabled-page-fragment="disabledPageFragment" :page-bar-sort-options="pageBarSortOptions">
<template #header>
<slot name="header"></slot>
</template>
@ -106,6 +106,7 @@
<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-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>
</template>

View File

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

View File

@ -38,9 +38,10 @@
</slot>
<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-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>
</slot>
</template>
@ -63,7 +64,7 @@ import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { TMagicScrollbar } from '@tmagic/design';
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 PageBar from './page-bar/PageBar.vue';
@ -78,6 +79,7 @@ defineOptions({
defineProps<{
disabledPageFragment: boolean;
pageBarSortOptions?: PageBarSortOptions;
}>();
const DEFAULT_LEFT_COLUMN_WIDTH = 310;

View File

@ -2,15 +2,19 @@
<div class="m-editor-page-bar-tabs">
<SwitchTypeButton v-if="!disabledPageFragment" v-model="active" />
<PageBarScrollContainer :type="active">
<PageBarScrollContainer :type="active" :page-bar-sort-options="pageBarSortOptions">
<template #prepend>
<AddButton :type="active"></AddButton>
<PageList :list="list">
<template #page-list-popover="{ list }"><slot name="page-list-popover" :list="list"></slot></template>
</PageList>
</template>
<div
v-for="item in list"
class="m-editor-page-bar-item"
:key="item.id"
:page-id="item.id"
:class="{ active: page?.id === 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 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 AddButton from './AddButton.vue';
import PageBarScrollContainer from './PageBarScrollContainer.vue';
import PageList from './PageList.vue';
import SwitchTypeButton from './SwitchTypeButton.vue';
defineOptions({
@ -74,6 +79,7 @@ defineOptions({
defineProps<{
disabledPageFragment: boolean;
pageBarSortOptions?: PageBarSortOptions;
}>();
const active = ref<NodeType.PAGE | NodeType.PAGE_FRAGMENT>(NodeType.PAGE);

View File

@ -34,11 +34,12 @@ import {
type WatchStopHandle,
} from '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 type { Services } from '@editor/type';
import type { PageBarSortOptions, Services } from '@editor/type';
defineOptions({
name: 'MEditorPageBarScrollContainer',
@ -46,23 +47,29 @@ defineOptions({
const props = defineProps<{
type: NodeType.PAGE | NodeType.PAGE_FRAGMENT;
pageBarSortOptions?: PageBarSortOptions;
}>();
const services = inject<Services>('services');
const editorService = services?.editorService;
const uiService = services?.uiService;
const itemsContainer = ref<HTMLDivElement>();
const itemsContainer = ref<HTMLElement>();
const canScroll = ref(false);
const showAddPageButton = computed(() => uiService?.get('showAddPageButton'));
const showPageListButton = computed(() => uiService?.get('showPageListButton'));
const itemsContainerWidth = ref(0);
const setCanScroll = () => {
//
// 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(() => {
if (itemsContainer.value) {
@ -126,6 +133,35 @@ const crateWatchLength = (length: ComputedRef<number>) =>
} else {
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,
propsPanelSize: 'small',
showAddPageButton: true,
showPageListButton: true,
hideSlideBar: false,
sideBarItems: [],
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 {
display: flex;
width: 100%;

View File

@ -18,6 +18,7 @@
import type { Component } from 'vue';
import type EventEmitter from 'events';
import Sortable, { Options, SortableEvent } from 'sortablejs';
import type { PascalCasedProperties } from 'type-fest';
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 { UiService } from './services/ui';
import type { UndoRedo } from './utils/undo-redo';
export interface FrameworkSlots {
header(props: {}): any;
nav(props: {}): any;
@ -71,6 +71,7 @@ export interface FrameworkSlots {
'page-bar'(props: {}): any;
'page-bar-title'(props: { page: MPage | MPageFragment }): any;
'page-bar-popover'(props: { page: MPage | MPageFragment }): any;
'page-list-popover'(props: { list: MPage[] | MPageFragment[] }): any;
}
export interface WorkspaceSlots {
@ -239,6 +240,8 @@ export interface UiState {
propsPanelSize: 'large' | 'default' | 'small';
/** 是否显示新增页面按钮 */
showAddPageButton: boolean;
/** 是否在页面工具栏显示呼起页面列表按钮 */
showPageListButton: boolean;
/** 是否隐藏侧边栏 */
hideSlideBar: boolean;
/** 侧边栏面板配置 */
@ -762,3 +765,11 @@ export interface EventBus extends EventEmitter {
export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig;
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:
specifier: ^6.0.0
version: 6.0.2
sortablejs:
specifier: ^1.15.2
version: 1.15.2
typescript:
specifier: '*'
version: 5.4.5
@ -365,6 +368,9 @@ importers:
'@types/serialize-javascript':
specifier: ^5.0.1
version: 5.0.4
'@types/sortablejs':
specifier: ^1.15.8
version: 1.15.8
'@vitejs/plugin-vue':
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))