mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat(editor): 支持拖拽调整页面顺序
This commit is contained in:
parent
0ffc223459
commit
0c5485b1d0
@ -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": {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
|
55
packages/editor/src/layouts/page-bar/PageList.vue
Normal file
55
packages/editor/src/layouts/page-bar/PageList.vue
Normal 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>
|
@ -47,6 +47,7 @@ const state = reactive<UiState>({
|
||||
showRule: true,
|
||||
propsPanelSize: 'small',
|
||||
showAddPageButton: true,
|
||||
showPageListButton: true,
|
||||
hideSlideBar: false,
|
||||
sideBarItems: [],
|
||||
navMenuRect: {
|
||||
|
@ -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%;
|
||||
|
@ -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
6
pnpm-lock.yaml
generated
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user