mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-05 00:46:50 +08:00
feat(editor): 历史记录面板支持自定义扩展 tab 并开放 Bucket/goto 配置
新增 historyListExtraTabs 配置,可在内置页面/数据源/代码块 tab 后追加业务自定义历史 tab。 导出 HistoryListBucket 供复用,GroupRow 支持配置是否允许跳转,Bucket 支持配置是否展示初始项。
This commit is contained in:
parent
818b41f07f
commit
8612311db1
@ -1508,6 +1508,55 @@ const extendFormState = async (state) => {
|
|||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## historyListExtraTabs
|
||||||
|
|
||||||
|
- **详情:**
|
||||||
|
|
||||||
|
[历史记录面板](/guide/advanced/history-list.md) 的自定义扩展 tab。
|
||||||
|
|
||||||
|
业务方可借此在历史记录面板内置的「页面 / 数据源 / 代码块」三个 tab 之后追加自定义模块的历史 tab,例如某个自定义模块维护自己的操作历史时,可在面板中增加一个独立的 tab 来展示与回滚。
|
||||||
|
|
||||||
|
- **默认值:** `[]`
|
||||||
|
|
||||||
|
- **类型:** `HistoryListExtraTab[]`
|
||||||
|
|
||||||
|
::: details 查看 HistoryListExtraTab 类型定义
|
||||||
|
<<< @/../packages/editor/src/type.ts#HistoryListExtraTab{ts}
|
||||||
|
:::
|
||||||
|
|
||||||
|
- **示例:**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
|
||||||
|
|
||||||
|
const historyListExtraTabs = [
|
||||||
|
{
|
||||||
|
name: 'my-module',
|
||||||
|
// label 支持字符串或函数,函数形式便于展示动态数量
|
||||||
|
label: () => '我的模块',
|
||||||
|
component: markRaw(MyModuleHistoryTab),
|
||||||
|
// 传入内容组件的 props
|
||||||
|
props: { foo: 'bar' },
|
||||||
|
// 内容组件的事件监听
|
||||||
|
listeners: {
|
||||||
|
goto: (cursor) => console.log(cursor),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
内容组件内部可自行通过 `useServices()` 获取 `historyService` 等服务来读取与回滚自定义模块的历史。
|
||||||
|
:::
|
||||||
|
|
||||||
## pageBarSortOptions
|
## pageBarSortOptions
|
||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
|
|||||||
@ -76,6 +76,47 @@ const menu = ref({
|
|||||||
表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select`、`code-select`、`code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。
|
表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select`、`code-select`、`code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## 扩展自定义 tab
|
||||||
|
|
||||||
|
内置的三个 tab 之外,业务方可以通过 Editor 的 [`historyListExtraTabs`](/api/editor/props.html#historylistextratabs) 在面板中追加自定义的历史 tab,追加在「页面 / 数据源 / 代码块」之后。适用于某个自定义模块维护自己的操作历史,需要在历史记录面板中独立展示与回滚的场景。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
|
||||||
|
|
||||||
|
const historyListExtraTabs = [
|
||||||
|
{
|
||||||
|
name: 'my-module',
|
||||||
|
// label 支持字符串或函数,函数形式便于展示动态数量
|
||||||
|
label: () => `我的模块 (${getMyModuleHistory().length})`,
|
||||||
|
component: markRaw(MyModuleHistoryTab),
|
||||||
|
props: { foo: 'bar' },
|
||||||
|
listeners: {
|
||||||
|
goto: (cursor) => console.log(cursor),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
每个扩展 tab 的字段说明:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `name` | 是 | tab 唯一标识,作为内部 `TMagicTabs` 的 `name` |
|
||||||
|
| `label` | 是 | tab 显示文案,支持字符串或返回字符串的函数(便于展示动态数量) |
|
||||||
|
| `component` | 是 | tab 内容区渲染的组件 |
|
||||||
|
| `props` | 否 | 传入内容组件的 props |
|
||||||
|
| `listeners` | 否 | 内容组件的事件监听 |
|
||||||
|
|
||||||
|
> 内容组件内部可自行通过 `useServices()` 拿到 `historyService` 等服务,读取并回滚自定义模块自己维护的历史。
|
||||||
|
|
||||||
## 自定义对比判断
|
## 自定义对比判断
|
||||||
|
|
||||||
差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。
|
差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。
|
||||||
|
|||||||
@ -237,6 +237,13 @@ provide('stageOptions', stageOptions);
|
|||||||
*/
|
*/
|
||||||
provide('extendFormState', props.extendFormState);
|
provide('extendFormState', props.extendFormState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel(它挂在 NavMenu 中,
|
||||||
|
* 以 markRaw component 形式渲染,无法直接通过 props 透传)。业务方可借此在历史记录
|
||||||
|
* 面板内追加自定义模块的历史 tab。
|
||||||
|
*/
|
||||||
|
provide('historyListExtraTabs', props.historyListExtraTabs);
|
||||||
|
|
||||||
provide<EventBus>('eventBus', new EventEmitter());
|
provide<EventBus>('eventBus', new EventEmitter());
|
||||||
|
|
||||||
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
|
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type {
|
|||||||
ComponentGroup,
|
ComponentGroup,
|
||||||
CustomContentMenuFunction,
|
CustomContentMenuFunction,
|
||||||
DatasourceTypeOption,
|
DatasourceTypeOption,
|
||||||
|
HistoryListExtraTab,
|
||||||
IsExpandableFunction,
|
IsExpandableFunction,
|
||||||
MenuBarData,
|
MenuBarData,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
@ -125,6 +126,8 @@ export interface EditorProps {
|
|||||||
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||||
beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
|
beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
|
||||||
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||||
|
/** 历史记录面板的自定义扩展 tab,追加在内置的页面/数据源/代码块 tab 之后 */
|
||||||
|
historyListExtraTabs?: HistoryListExtraTab[];
|
||||||
/** 页面顺序拖拽配置参数 */
|
/** 页面顺序拖拽配置参数 */
|
||||||
pageBarSortOptions?: PageBarSortOptions;
|
pageBarSortOptions?: PageBarSortOptions;
|
||||||
/** 页面搜索函数 */
|
/** 页面搜索函数 */
|
||||||
@ -145,6 +148,7 @@ export const defaultEditorProps = {
|
|||||||
disabledCodeBlock: false,
|
disabledCodeBlock: false,
|
||||||
componentGroupList: () => [],
|
componentGroupList: () => [],
|
||||||
datasourceList: () => [],
|
datasourceList: () => [],
|
||||||
|
historyListExtraTabs: () => [],
|
||||||
menu: () => ({ left: [], right: [] }),
|
menu: () => ({ left: [], right: [] }),
|
||||||
layerContentMenu: () => [],
|
layerContentMenu: () => [],
|
||||||
stageContentMenu: () => [],
|
stageContentMenu: () => [],
|
||||||
|
|||||||
@ -71,6 +71,7 @@ export { default as SplitView } from './components/SplitView.vue';
|
|||||||
export { default as Resizer } from './components/Resizer.vue';
|
export { default as Resizer } from './components/Resizer.vue';
|
||||||
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
|
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
|
||||||
export { default as CompareForm } from './components/CompareForm.vue';
|
export { default as CompareForm } from './components/CompareForm.vue';
|
||||||
|
export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
|
||||||
export { default as FloatingBox } from './components/FloatingBox.vue';
|
export { default as FloatingBox } from './components/FloatingBox.vue';
|
||||||
export { default as Tree } from './components/Tree.vue';
|
export { default as Tree } from './components/Tree.vue';
|
||||||
export { default as TreeNode } from './components/TreeNode.vue';
|
export { default as TreeNode } from './components/TreeNode.vue';
|
||||||
|
|||||||
@ -36,8 +36,13 @@
|
|||||||
<!--
|
<!--
|
||||||
初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
|
初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
|
||||||
当 bucket 内所有 group 都未 applied 时即为当前位置。
|
当 bucket 内所有 group 都未 applied 时即为当前位置。
|
||||||
|
showInitial=false 时不展示(用于没有"撤销到初始状态"语义的自定义历史,如业务模块历史)。
|
||||||
-->
|
-->
|
||||||
<InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial', bucketId)" />
|
<InitialRow
|
||||||
|
v-if="showInitial !== false"
|
||||||
|
:is-current="isInitial"
|
||||||
|
@goto-initial="$emit('goto-initial', bucketId)"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -54,29 +59,39 @@ defineOptions({
|
|||||||
name: 'MEditorHistoryListBucket',
|
name: 'MEditorHistoryListBucket',
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
/** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
|
defineProps<{
|
||||||
title: string;
|
/** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
|
||||||
/** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
|
title: string;
|
||||||
bucketId: string | number;
|
/** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
|
||||||
/** 子项 key 的命名空间前缀:`ds` 表示数据源,`cb` 表示代码块。与上层折叠状态 key 保持一致。 */
|
bucketId: string | number;
|
||||||
prefix: 'ds' | 'cb';
|
/**
|
||||||
/** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
|
* 子项 key 的命名空间前缀:内置 `ds` 表示数据源,`cb` 表示代码块;
|
||||||
groups: {
|
* 业务方复用 Bucket 时可传入自定义前缀(如 `mod`)。与上层折叠状态 key 保持一致。
|
||||||
applied: boolean;
|
*/
|
||||||
isCurrent?: boolean;
|
prefix: string;
|
||||||
opType: HistoryOpType;
|
/** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false 关闭。 */
|
||||||
steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
|
showInitial?: boolean;
|
||||||
}[];
|
/** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
|
||||||
/** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
|
groups: {
|
||||||
describeGroup: (_group: any) => string;
|
applied: boolean;
|
||||||
/** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
|
isCurrent?: boolean;
|
||||||
describeStep: (_step: any) => string;
|
opType: HistoryOpType;
|
||||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
|
||||||
isStepDiffable?: (_step: any) => boolean;
|
}[];
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
/** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
|
||||||
expanded: Record<string, boolean>;
|
describeGroup: (_group: any) => string;
|
||||||
}>();
|
/** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
|
||||||
|
describeStep: (_step: any) => string;
|
||||||
|
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
||||||
|
isStepDiffable?: (_step: any) => boolean;
|
||||||
|
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
||||||
|
expanded: Record<string, boolean>;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
showInitial: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
/** 透传子组件 GroupRow 的 toggle,由上层 panel 更新 expanded。 */
|
/** 透传子组件 GroupRow 的 toggle,由上层 panel 更新 expanded。 */
|
||||||
|
|||||||
@ -35,8 +35,8 @@
|
|||||||
<li
|
<li
|
||||||
v-for="s in subStepsDisplay"
|
v-for="s in subStepsDisplay"
|
||||||
:key="s.index"
|
:key="s.index"
|
||||||
:class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': !s.isCurrent }"
|
:class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': gotoEnabled && !s.isCurrent }"
|
||||||
:title="s.isCurrent ? '当前所在记录' : '点击跳转到该记录'"
|
:title="subStepTitle(s)"
|
||||||
@click="onSubStepClick(s)"
|
@click="onSubStepClick(s)"
|
||||||
>
|
>
|
||||||
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
||||||
@ -72,34 +72,46 @@ defineOptions({
|
|||||||
name: 'MEditorHistoryListGroupRow',
|
name: 'MEditorHistoryListGroupRow',
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
/** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */
|
defineProps<{
|
||||||
groupKey: string;
|
/** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */
|
||||||
/** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
|
groupKey: string;
|
||||||
applied: boolean;
|
/** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
|
||||||
/** 是否为合并组(即组内 step 数大于 1,由多次连续操作合并而来)。决定是否展示合并标记与可展开的子步列表。 */
|
|
||||||
merged: boolean;
|
|
||||||
/** 操作类型:`add` / `remove` / `update`,用于决定操作徽标的颜色和文案。 */
|
|
||||||
opType: HistoryOpType;
|
|
||||||
/** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
|
|
||||||
desc: string;
|
|
||||||
/** 组内的 step 总数,仅在 merged 为 true 时显示为 "合并 N 步"。 */
|
|
||||||
stepCount: number;
|
|
||||||
/** 子步列表,用于在展开状态下逐条展示每个 step 的索引、应用状态与描述文案。 */
|
|
||||||
subSteps: {
|
|
||||||
index: number;
|
|
||||||
applied: boolean;
|
applied: boolean;
|
||||||
|
/** 是否为合并组(即组内 step 数大于 1,由多次连续操作合并而来)。决定是否展示合并标记与可展开的子步列表。 */
|
||||||
|
merged: boolean;
|
||||||
|
/** 操作类型:`add` / `remove` / `update`,用于决定操作徽标的颜色和文案。 */
|
||||||
|
opType: HistoryOpType;
|
||||||
|
/** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
|
||||||
desc: string;
|
desc: string;
|
||||||
|
/** 组内的 step 总数,仅在 merged 为 true 时显示为 "合并 N 步"。 */
|
||||||
|
stepCount: number;
|
||||||
|
/** 子步列表,用于在展开状态下逐条展示每个 step 的索引、应用状态与描述文案。 */
|
||||||
|
subSteps: {
|
||||||
|
index: number;
|
||||||
|
applied: boolean;
|
||||||
|
desc: string;
|
||||||
|
isCurrent?: boolean;
|
||||||
|
diffable?: boolean;
|
||||||
|
/** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
|
||||||
|
revertable?: boolean;
|
||||||
|
}[];
|
||||||
|
/** 当前组是否处于展开状态。仅在 merged 为 true 时生效,控制子步列表是否渲染。 */
|
||||||
|
expanded: boolean;
|
||||||
|
/** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
|
||||||
isCurrent?: boolean;
|
isCurrent?: boolean;
|
||||||
diffable?: boolean;
|
/**
|
||||||
/** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
|
* 是否支持「跳转到该记录」(goto)。默认 true。
|
||||||
revertable?: boolean;
|
* 为 false 时:单步组头部与子步条目都不再可点击跳转、也不会触发 goto 事件,
|
||||||
}[];
|
* 仅保留合并组头部的展开 / 收起能力,以及查看差异、回滚等其它入口。
|
||||||
/** 当前组是否处于展开状态。仅在 merged 为 true 时生效,控制子步列表是否渲染。 */
|
*/
|
||||||
expanded: boolean;
|
gotoEnabled?: boolean;
|
||||||
/** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组),UI 高亮展示。 */
|
}>(),
|
||||||
isCurrent?: boolean;
|
{
|
||||||
}>();
|
isCurrent: false,
|
||||||
|
gotoEnabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
/**
|
/**
|
||||||
@ -129,15 +141,19 @@ const emit = defineEmits<{
|
|||||||
(_e: 'revert-step', _index: number): void;
|
(_e: 'revert-step', _index: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** 单步组:头部可点击 goto;合并组:头部可点击切换展开。当前组(isCurrent)的单步组头部不可点击。 */
|
/**
|
||||||
|
* 单步组:头部可点击 goto(需 gotoEnabled);合并组:头部可点击切换展开。
|
||||||
|
* 当前组(isCurrent)或禁用 goto 时,单步组头部不可点击。
|
||||||
|
*/
|
||||||
const isHeadClickable = computed(() => {
|
const isHeadClickable = computed(() => {
|
||||||
if (props.merged) return true;
|
if (props.merged) return true;
|
||||||
return !props.isCurrent;
|
return props.gotoEnabled && !props.isCurrent;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headTitle = computed(() => {
|
const headTitle = computed(() => {
|
||||||
if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
|
if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
|
||||||
if (props.isCurrent) return '当前所在记录';
|
if (props.isCurrent) return '当前所在记录';
|
||||||
|
if (!props.gotoEnabled) return '';
|
||||||
return '点击跳转到该记录';
|
return '点击跳转到该记录';
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -152,15 +168,23 @@ const onHeadClick = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.isCurrent) return;
|
if (props.isCurrent) return;
|
||||||
|
if (!props.gotoEnabled) return;
|
||||||
if (!props.subSteps.length) return;
|
if (!props.subSteps.length) return;
|
||||||
emit('goto', props.subSteps[0].index);
|
emit('goto', props.subSteps[0].index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
|
const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
|
||||||
if (s.isCurrent) return;
|
if (s.isCurrent) return;
|
||||||
|
if (!props.gotoEnabled) return;
|
||||||
emit('goto', s.index);
|
emit('goto', s.index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subStepTitle = (s: { isCurrent?: boolean }) => {
|
||||||
|
if (s.isCurrent) return '当前所在记录';
|
||||||
|
if (!props.gotoEnabled) return '';
|
||||||
|
return '点击跳转到该记录';
|
||||||
|
};
|
||||||
|
|
||||||
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
||||||
const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
|
const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
</component>
|
</component>
|
||||||
|
|
||||||
<component
|
<component
|
||||||
|
v-if="!disabledDataSource"
|
||||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||||
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
||||||
>
|
>
|
||||||
@ -47,6 +48,7 @@
|
|||||||
</component>
|
</component>
|
||||||
|
|
||||||
<component
|
<component
|
||||||
|
v-if="!disabledCodeBlock"
|
||||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||||
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
||||||
>
|
>
|
||||||
@ -60,6 +62,15 @@
|
|||||||
@revert-step="onCodeBlockRevert"
|
@revert-step="onCodeBlockRevert"
|
||||||
/>
|
/>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
|
<component
|
||||||
|
v-for="tab in extraTabs"
|
||||||
|
:key="tab.name"
|
||||||
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||||
|
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
|
||||||
|
>
|
||||||
|
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
|
||||||
|
</component>
|
||||||
</TMagicTabs>
|
</TMagicTabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -98,7 +109,7 @@
|
|||||||
* 各 tab 的内容拆分为独立的 SFC(PageTab / DataSourceTab / CodeBlockTab),
|
* 各 tab 的内容拆分为独立的 SFC(PageTab / DataSourceTab / CodeBlockTab),
|
||||||
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
||||||
*/
|
*/
|
||||||
import { inject, markRaw, ref, useTemplateRef } from 'vue';
|
import { computed, inject, markRaw, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { Clock, Close } from '@element-plus/icons-vue';
|
import { Clock, Close } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
||||||
@ -106,6 +117,7 @@ import type { FormState } from '@tmagic/form';
|
|||||||
|
|
||||||
import MIcon from '@editor/components/Icon.vue';
|
import MIcon from '@editor/components/Icon.vue';
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useServices } from '@editor/hooks/use-services';
|
||||||
|
import type { HistoryListExtraTab } from '@editor/type';
|
||||||
|
|
||||||
import CodeBlockTab from './CodeBlockTab.vue';
|
import CodeBlockTab from './CodeBlockTab.vue';
|
||||||
import { useHistoryList } from './composables';
|
import { useHistoryList } from './composables';
|
||||||
@ -119,14 +131,36 @@ defineOptions({
|
|||||||
|
|
||||||
const ClockIcon = markRaw(Clock);
|
const ClockIcon = markRaw(Clock);
|
||||||
const CloseIcon = markRaw(Close);
|
const CloseIcon = markRaw(Close);
|
||||||
const activeTab = ref<'page' | 'data-source' | 'code-block'>('page');
|
const activeTab = ref<string>('page');
|
||||||
|
|
||||||
/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
|
/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
||||||
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
||||||
|
|
||||||
const { editorService, dataSourceService, codeBlockService, historyService } = useServices();
|
/**
|
||||||
|
* 业务方自定义的扩展 tab,由 Editor 顶层通过 `historyListExtraTabs` 注入。
|
||||||
|
* 追加在内置「页面 / 数据源 / 代码块」三个 tab 之后,未提供时为空数组。
|
||||||
|
*/
|
||||||
|
const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
|
||||||
|
|
||||||
|
/** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
|
||||||
|
const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
|
||||||
|
|
||||||
|
const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
|
||||||
|
* 禁用后对应的历史记录 tab 不再展示。若当前激活的 tab 恰好被禁用,则回退到「页面」tab。
|
||||||
|
*/
|
||||||
|
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
||||||
|
const disabledCodeBlock = computed(() => propsService.getDisabledCodeBlock());
|
||||||
|
|
||||||
|
watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
|
||||||
|
if ((activeTab.value === 'data-source' && dsDisabled) || (activeTab.value === 'code-block' && cbDisabled)) {
|
||||||
|
activeTab.value = 'page';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
|
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
|
||||||
|
|||||||
@ -475,6 +475,29 @@ export interface SideComponent extends MenuComponent {
|
|||||||
}
|
}
|
||||||
// #endregion SideComponent
|
// #endregion SideComponent
|
||||||
|
|
||||||
|
// #region HistoryListExtraTab
|
||||||
|
/**
|
||||||
|
* 历史记录面板(HistoryListPanel)的自定义扩展 tab。
|
||||||
|
*
|
||||||
|
* 业务方可通过 Editor 的 `historyListExtraTabs` 注入额外的历史记录 tab,
|
||||||
|
* 例如某个自定义模块维护自己的操作历史时,可以在历史记录面板中增加一个
|
||||||
|
* 独立的 tab 来展示与回滚。内置的「页面 / 数据源 / 代码块」三个 tab 之后
|
||||||
|
* 会依次追加这些扩展 tab。
|
||||||
|
*/
|
||||||
|
export interface HistoryListExtraTab {
|
||||||
|
/** tab 唯一标识,作为 TMagicTabs 的 name */
|
||||||
|
name: string;
|
||||||
|
/** tab 显示文案,支持传入函数以展示动态内容(如记录数量) */
|
||||||
|
label: string | (() => string);
|
||||||
|
/** tab 内容区渲染的组件(Vue 组件或字符串标签) */
|
||||||
|
component: any;
|
||||||
|
/** 传入内容组件的 props */
|
||||||
|
props?: Record<string, any>;
|
||||||
|
/** 内容组件的事件监听 */
|
||||||
|
listeners?: Record<string, (..._args: any[]) => any>;
|
||||||
|
}
|
||||||
|
// #endregion HistoryListExtraTab
|
||||||
|
|
||||||
// #region SideItemKey
|
// #region SideItemKey
|
||||||
export enum SideItemKey {
|
export enum SideItemKey {
|
||||||
COMPONENT_LIST = 'component-list',
|
COMPONENT_LIST = 'component-list',
|
||||||
|
|||||||
@ -12,9 +12,13 @@ import historyService from '@editor/services/history';
|
|||||||
const editorService = { gotoPageStep: vi.fn(async () => 0) };
|
const editorService = { gotoPageStep: vi.fn(async () => 0) };
|
||||||
const dataSourceService = { goto: vi.fn(() => 0) };
|
const dataSourceService = { goto: vi.fn(() => 0) };
|
||||||
const codeBlockService = { goto: vi.fn(async () => 0) };
|
const codeBlockService = { goto: vi.fn(async () => 0) };
|
||||||
|
const propsService = {
|
||||||
|
getDisabledDataSource: vi.fn(() => false),
|
||||||
|
getDisabledCodeBlock: vi.fn(() => false),
|
||||||
|
};
|
||||||
|
|
||||||
vi.mock('@editor/hooks/use-services', () => ({
|
vi.mock('@editor/hooks/use-services', () => ({
|
||||||
useServices: () => ({ historyService, editorService, dataSourceService, codeBlockService }),
|
useServices: () => ({ historyService, editorService, dataSourceService, codeBlockService, propsService }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@tmagic/design', () => ({
|
vi.mock('@tmagic/design', () => ({
|
||||||
@ -251,6 +255,38 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
expect(editorService.gotoPageStep).toHaveBeenCalledWith(0);
|
expect(editorService.gotoPageStep).toHaveBeenCalledWith(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('注入 historyListExtraTabs 时追加渲染自定义 tab 内容组件', async () => {
|
||||||
|
const { default: historyListPanel } = await import('@editor/layouts/history-list/HistoryListPanel.vue');
|
||||||
|
const customTab = defineComponent({
|
||||||
|
name: 'CustomHistoryTab',
|
||||||
|
props: ['title'],
|
||||||
|
setup(p) {
|
||||||
|
return () => h('div', { class: 'custom-history-tab' }, p.title);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(historyListPanel, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
historyListExtraTabs: [
|
||||||
|
{
|
||||||
|
name: 'custom-module',
|
||||||
|
label: () => '自定义模块 (1)',
|
||||||
|
component: customTab,
|
||||||
|
props: { title: 'hello-custom' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const custom = wrapper.find('.custom-history-tab');
|
||||||
|
expect(custom.exists()).toBe(true);
|
||||||
|
expect(custom.text()).toBe('hello-custom');
|
||||||
|
});
|
||||||
|
|
||||||
test('点击数据源/代码块初始项调用对应 service.goto(id, 0)', async () => {
|
test('点击数据源/代码块初始项调用对应 service.goto(id, 0)', async () => {
|
||||||
historyService.pushDataSource('ds_x', {
|
historyService.pushDataSource('ds_x', {
|
||||||
oldSchema: null,
|
oldSchema: null,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user