优化:表格块增加块内直接编辑所需的常用操作按钮

This commit is contained in:
jingrow 2026-06-06 12:43:23 +08:00
parent 49bc0a30e5
commit 443c359a8f
5 changed files with 317 additions and 4 deletions

View File

@ -19,6 +19,7 @@ import Toolbar from './toolbar/Toolbar.vue'
import SlashSuggestion from './menus/slash/SlashSuggestion.vue'
import LinkPanel from './menus/LinkPanel.vue'
import BubbleMenuContent from './menus/BubbleMenuContent.vue'
import TableBubbleMenu from './menus/TableBubbleMenu.vue'
import BlockOutlineRail from './menus/BlockOutlineRail.vue'
import ImageSourcePicker from '@/core/components/ImageSourcePicker.vue'
import './blockeditor.css'
@ -222,7 +223,7 @@ onBeforeUnmount(() => {
:t="t"
/>
<!-- Bubble Menu -->
<!-- Bubble Menu (text selection) -->
<BubbleMenuContent
v-if="editorRef && isEditable"
:editor-ref="editorRef"
@ -230,6 +231,13 @@ onBeforeUnmount(() => {
:open-link-panel="openLinkPanel"
/>
<!-- Table Bubble Menu (cursor in table, no selection) -->
<TableBubbleMenu
v-if="editorRef && isEditable"
:editor-ref="editorRef"
:t="t"
/>
<!-- Outline Rail -->
<div v-if="editorRef && isEditable" class="blockeditor-outline-rail-wrapper">
<BlockOutlineRail :editor="editorRef" />

View File

@ -156,10 +156,40 @@
.blockeditor-mount .ProseMirror [data-type="imageResizable"].ProseMirror-selectednode {
box-shadow: none;
}
.blockeditor-mount .ProseMirror table { width: 100%; border-collapse: collapse; margin: 8px 0; }
.blockeditor-mount .ProseMirror table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
overflow: visible;
}
.blockeditor-mount .ProseMirror table th,
.blockeditor-mount .ProseMirror table td { border: 1px solid #d1d5db; padding: 8px 12px; text-align: left; }
.blockeditor-mount .ProseMirror table th { background: #f9fafb; font-weight: 600; }
.blockeditor-mount .ProseMirror table td {
border: 1px solid #d1d5db;
padding: 8px 12px;
text-align: left;
min-width: 40px;
position: relative;
transition: background-color 0.15s ease;
}
.blockeditor-mount .ProseMirror table th {
background: #f9fafb;
font-weight: 600;
}
/* 表格可编辑状态悬浮高亮 */
.blockeditor-mount .ProseMirror table td:hover,
.blockeditor-mount .ProseMirror table th:hover {
background-color: #f3f4f6;
}
/* 表格列宽拖拽手柄提示 */
.blockeditor-mount .ProseMirror table .column-resize-handle {
background: #2383e2;
width: 2px;
opacity: 0.4;
transition: opacity 0.15s ease;
}
.blockeditor-mount .ProseMirror table .column-resize-handle:hover {
opacity: 1;
}
.blockeditor-mount .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left; color: #adb5bd; pointer-events: none; height: 0;

View File

@ -0,0 +1,236 @@
<script setup lang="ts">
/**
* TableBubbleMenu.vue 表格上下文 BubbleMenu
*
* 当光标位于表格单元格内且无选中文字时展示提供行/列增删
* 合并拆分表头切换等常用表格操作
*
* 遵循 Tiptap v3 内置 BubbleMenu 模式 BubbleMenuContent.vue 互斥
* TableBubbleMenu: selection.empty + isActive('table');
* BubbleMenuContent: !selection.empty
*/
import { ref, computed } from 'vue'
import type { Editor } from '@tiptap/core'
import { BubbleMenu } from '@tiptap/vue-3/menus'
import { useBlockCommand } from '../composables/useBlockCommand'
import ToolbarButton from '../toolbar/ToolbarButton.vue'
import ToolbarDivider from '../toolbar/ToolbarDivider.vue'
import { useAdaptiveDropdown } from '../composables/useAdaptiveDropdown'
// Props
const props = defineProps<{
editorRef: Editor | null
t: (key: string) => string
}>()
const editorRefWrapper = computed(() => props.editorRef)
const { cmd } = useBlockCommand(editorRefWrapper)
// "More" dropdown state
const moreOpen = ref(false)
const moreMenuRef = ref<HTMLElement | null>(null)
const { direction: moreDropdownDir } = useAdaptiveDropdown(moreOpen, moreMenuRef)
// BubbleMenu options
const bubbleMenuOptions = computed(() => ({
placement: 'top' as const,
offset: 8,
strategy: 'fixed' as const,
onShow: () => {
moreOpen.value = false
},
}))
/**
* 挂载到 <body> 避免被编辑器容器裁切
*/
function appendToBody(): HTMLElement {
return document.body
}
// shouldShow
/**
* 当光标位于表格内部且无选中文字时显示表格操作菜单
* BubbleMenuContent.vue 互斥后者要求 !selection.empty
*/
function shouldShow(props_: {
editor: Editor
view: any
state: any
oldState?: any
from: number
to: number
}) {
const { view, state } = props_
const { selection } = state
//
if ((selection as any).node?.type?.name === 'imageResizable') return false
// +
return selection.empty && props_.editor.isActive('table') && view.hasFocus()
}
// Command helpers
function execTableCmd(name: string) {
cmd[name]()
}
function closeMore() {
moreOpen.value = false
}
</script>
<template>
<BubbleMenu
:editor="editorRef"
:should-show="shouldShow"
:update-delay="0"
:options="bubbleMenuOptions"
:append-to="appendToBody"
class="blockeditor-table-menu"
>
<!-- Row operations -->
<ToolbarButton
icon="table_row_before"
:title="t('Insert Row Above')"
@click="execTableCmd('addRowBefore'); closeMore()"
/>
<ToolbarButton
icon="table_row_after"
:title="t('Insert Row Below')"
@click="execTableCmd('addRowAfter'); closeMore()"
/>
<ToolbarDivider />
<!-- Column operations -->
<ToolbarButton
icon="table_col_before"
:title="t('Insert Column Left')"
@click="execTableCmd('addColumnBefore'); closeMore()"
/>
<ToolbarButton
icon="table_col_after"
:title="t('Insert Column Right')"
@click="execTableCmd('addColumnAfter'); closeMore()"
/>
<ToolbarDivider />
<!-- Delete row / column -->
<ToolbarButton
icon="table_row_delete"
:title="t('Delete Row')"
@click="execTableCmd('deleteRow'); closeMore()"
/>
<ToolbarButton
icon="table_col_delete"
:title="t('Delete Column')"
@click="execTableCmd('deleteColumn'); closeMore()"
/>
<ToolbarDivider />
<!-- More: merge/split/header/delete table -->
<div class="be-tb-dropdown be-bubble-dropdown">
<ToolbarButton
icon="more_vert"
:title="t('More Table Options')"
:active="moreOpen"
@click="moreOpen = !moreOpen"
/>
<div
v-if="moreOpen"
ref="moreMenuRef"
class="be-tb-dropdown-menu"
:class="{ 'be-tb-dropdown-menu-down': moreDropdownDir === 'down' }"
>
<div
class="be-tb-dropdown-item"
@mousedown.prevent="execTableCmd('mergeOrSplit'); closeMore()"
>
{{ t('Merge / Split Cells') }}
</div>
<div
class="be-tb-dropdown-item"
@mousedown.prevent="execTableCmd('toggleHeaderRow'); closeMore()"
>
{{ t('Toggle Header Row') }}
</div>
<div
class="be-tb-dropdown-item"
@mousedown.prevent="execTableCmd('toggleHeaderColumn'); closeMore()"
>
{{ t('Toggle Header Column') }}
</div>
<div class="be-tb-dropdown-divider"></div>
<div
class="be-tb-dropdown-item be-tb-dropdown-item-danger"
@mousedown.prevent="execTableCmd('deleteTable'); closeMore()"
>
{{ t('Delete Table') }}
</div>
</div>
</div>
</BubbleMenu>
</template>
<style>
/* ── 表格 BubbleMenu 样式 ── */
.blockeditor-table-menu {
display: flex;
align-items: center;
gap: 1px;
padding: 4px 6px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
z-index: 1000;
}
.blockeditor-table-menu .be-tb-btn {
color: #6b7280;
}
.blockeditor-table-menu .be-tb-btn:hover {
background: #e5e7eb;
color: #1f2937;
}
.blockeditor-table-menu .be-tb-btn.active {
background: #2383e2;
color: #fff;
}
.blockeditor-table-menu .be-tb-divider {
background: #d1d5db;
}
/* 表格 BubbleMenu 里的 dropdown 样式继承 base仅覆盖 z-index */
.blockeditor-table-menu .be-tb-dropdown-menu {
z-index: 1001;
}
/* 危险操作样式(如删除表格) */
.be-tb-dropdown-item-danger {
color: #dc2626 !important;
}
.be-tb-dropdown-item-danger:hover {
background: #fef2f2 !important;
}
/* ── 表格单元格选中/悬浮高亮 ── */
.blockeditor-mount .ProseMirror table td.ProseMirror-selectednode,
.blockeditor-mount .ProseMirror table th.ProseMirror-selectednode {
outline: 2px solid #2383e2;
outline-offset: -1px;
}
/* ── 拖拽时隐藏表格 BubbleMenu与文字 BubbleMenu 一致) ── */
body.be-bubble-dragging .blockeditor-table-menu {
visibility: hidden !important;
pointer-events: none !important;
}
</style>

View File

@ -49,6 +49,27 @@ const ICONS = {
more_vert: '<circle cx="12" cy="5" r="2" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="2" fill="currentColor" stroke="none"/>',
chevron_right: '<path d="m9 18 6-6-6-6"/>',
enter: '<path d="m9 10-5 5 5 5"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/>',
// ── Table operations ──
/** Insert row above: horizontal row line + upward arrow */
table_row_before: '<path d="M4 16h16"/><path d="M12 4v10"/><path d="m9 7 3-3 3 3"/>',
/** Insert row below: horizontal row line + downward arrow */
table_row_after: '<path d="M4 8h16"/><path d="M12 10v10"/><path d="m9 17 3 3 3-3"/>',
/** Insert column left: vertical column line + leftward arrow */
table_col_before: '<path d="M16 4v16"/><path d="M4 12h10"/><path d="m7 9-3 3 3 3"/>',
/** Insert column right: vertical column line + rightward arrow */
table_col_after: '<path d="M8 4v16"/><path d="M10 12h10"/><path d="m17 9 3 3-3 3"/>',
/** Delete row: horizontal line with X mark */
table_row_delete: '<path d="M5 12h14"/><path d="m21 10 2 2-2 2"/><path d="M12 6v12"/>',
/** Delete column: vertical line with X mark */
table_col_delete: '<path d="M12 5v14"/><path d="m10 21 2 2-2 2"/><path d="M6 12h12"/>',
/** Merge cells: two arrows converging */
table_merge_cells: '<path d="M7 12h10"/><path d="M4 15l3-3-3-3"/><path d="M20 9l3 3-3 3"/>',
/** Split cell: arrows diverging */
table_split_cell: '<path d="M12 7v10"/><path d="M4 9l3 3-3 3"/><path d="M20 15l-3-3 3-3"/>',
/** Toggle header row */
table_header: '<path d="M4 4h16v5H4z"/><path d="M4 11h16v9H4z"/>',
} as const
/**

View File

@ -22259,6 +22259,24 @@ msgstr "在右侧插入列"
msgid "Delete Row"
msgstr "删除行"
msgid "Insert Row Above"
msgstr "在上方插入行"
msgid "Insert Column Left"
msgstr "在左侧插入列"
msgid "More Table Options"
msgstr "更多表格操作"
msgid "Merge / Split Cells"
msgstr "合并 / 拆分单元格"
msgid "Toggle Header Row"
msgstr "切换表头行"
msgid "Toggle Header Column"
msgstr "切换表头列"
msgid "Show Source Code"
msgstr "显示源代码"