diff --git a/packages/editor/package.json b/packages/editor/package.json index 21d8169c..8814cc56 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -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": { diff --git a/packages/editor/src/Editor.vue b/packages/editor/src/Editor.vue index 2d28641e..15c34a36 100644 --- a/packages/editor/src/Editor.vue +++ b/packages/editor/src/Editor.vue @@ -1,5 +1,5 @@ diff --git a/packages/editor/src/editorProps.ts b/packages/editor/src/editorProps.ts index bbaf9709..e377e6e8 100644 --- a/packages/editor/src/editorProps.ts +++ b/packages/editor/src/editorProps.ts @@ -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 | Promise>; + /** 页面顺序拖拽配置参数 */ + pageBarSortOptions?: PageBarSortOptions; } export const defaultEditorProps = { diff --git a/packages/editor/src/layouts/Framework.vue b/packages/editor/src/layouts/Framework.vue index 07a0d643..801c2e9c 100644 --- a/packages/editor/src/layouts/Framework.vue +++ b/packages/editor/src/layouts/Framework.vue @@ -38,9 +38,10 @@ - + + @@ -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; diff --git a/packages/editor/src/layouts/page-bar/PageBar.vue b/packages/editor/src/layouts/page-bar/PageBar.vue index b60031ef..8e798e96 100644 --- a/packages/editor/src/layouts/page-bar/PageBar.vue +++ b/packages/editor/src/layouts/page-bar/PageBar.vue @@ -2,15 +2,19 @@
- +
@@ -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); diff --git a/packages/editor/src/layouts/page-bar/PageBarScrollContainer.vue b/packages/editor/src/layouts/page-bar/PageBarScrollContainer.vue index a25e61ec..18042930 100644 --- a/packages/editor/src/layouts/page-bar/PageBarScrollContainer.vue +++ b/packages/editor/src/layouts/page-bar/PageBarScrollContainer.vue @@ -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'); const editorService = services?.editorService; const uiService = services?.uiService; -const itemsContainer = ref(); +const itemsContainer = ref(); 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) => } 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); + } }); }, { diff --git a/packages/editor/src/layouts/page-bar/PageList.vue b/packages/editor/src/layouts/page-bar/PageList.vue new file mode 100644 index 00000000..546142f2 --- /dev/null +++ b/packages/editor/src/layouts/page-bar/PageList.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/editor/src/services/ui.ts b/packages/editor/src/services/ui.ts index 5bbffbed..5088b97c 100644 --- a/packages/editor/src/services/ui.ts +++ b/packages/editor/src/services/ui.ts @@ -47,6 +47,7 @@ const state = reactive({ showRule: true, propsPanelSize: 'small', showAddPageButton: true, + showPageListButton: true, hideSlideBar: false, sideBarItems: [], navMenuRect: { diff --git a/packages/editor/src/theme/page-bar.scss b/packages/editor/src/theme/page-bar.scss index 0586c0cb..590eeba1 100644 --- a/packages/editor/src/theme/page-bar.scss +++ b/packages/editor/src/theme/page-bar.scss @@ -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%; diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index af33dcbe..b3c03b79 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -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; + +export type PartSortableOptions = Omit; +export interface PageBarSortOptions extends PartSortableOptions { + /** 在onUpdate之后调用 */ + afterUpdate: (event: SortableEvent, sortable: Sortable) => void; + /** 在onStart之前调用 */ + beforeStart: (event: SortableEvent, sortable: Sortable) => void; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e16a28f..e3d8692b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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))