perf: 表格行列操作优化,气泡菜单补充

This commit is contained in:
pipipi-pikachu
2026-06-07 15:34:57 +08:00
parent 43e357b235
commit a79c17ceff
6 changed files with 143 additions and 101 deletions
+2
View File
@@ -17,6 +17,7 @@ declare module 'vue' {
'ICustom:filePptist': typeof import('~icons/custom/file-pptist')['default']
'ICustom:left': typeof import('~icons/custom/left')['default']
'ICustom:right': typeof import('~icons/custom/right')['default']
'IIconParkOutline:add': typeof import('~icons/icon-park-outline/add')['default']
'IIconParkOutline:addText': typeof import('~icons/icon-park-outline/add-text')['default']
'IIconParkOutline:alignBottom': typeof import('~icons/icon-park-outline/align-bottom')['default']
'IIconParkOutline:alignHorizontally': typeof import('~icons/icon-park-outline/align-horizontally')['default']
@@ -109,6 +110,7 @@ declare module 'vue' {
'IIconParkOutline:previewOpen': typeof import('~icons/icon-park-outline/preview-open')['default']
'IIconParkOutline:quote': typeof import('~icons/icon-park-outline/quote')['default']
'IIconParkOutline:radarChart': typeof import('~icons/icon-park-outline/radar-chart')['default']
'IIconParkOutline:reduce': typeof import('~icons/icon-park-outline/reduce')['default']
'IIconParkOutline:refresh': typeof import('~icons/icon-park-outline/refresh')['default']
'IIconParkOutline:right': typeof import('~icons/icon-park-outline/right')['default']
'IIconParkOutline:rotate': typeof import('~icons/icon-park-outline/rotate')['default']
+9 -1
View File
@@ -2,6 +2,7 @@ import mitt, { type Emitter } from 'mitt'
export const enum EmitterEvents {
RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND',
TABLE_COMMAND = 'TABLE_COMMAND',
SYNC_RICH_TEXT_ATTRS_TO_STORE = 'SYNC_RICH_TEXT_ATTRS_TO_STORE',
OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR',
OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR',
@@ -17,8 +18,15 @@ export interface RichTextCommand {
action: RichTextAction | RichTextAction[]
}
export interface TableCommand {
targetId: string
command: 'insert-row' | 'insert-col' | 'delete-row' | 'delete-col'
position?: 'before' | 'after'
}
type Events = {
[EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand
[EmitterEvents.TABLE_COMMAND]: TableCommand
[EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE]: void
[EmitterEvents.OPEN_CHART_DATA_EDITOR]: void
[EmitterEvents.OPEN_LATEX_EDITOR]: void
@@ -26,4 +34,4 @@ type Events = {
const emitter: Emitter<Events> = mitt<Events>()
export default emitter
export default emitter
@@ -10,6 +10,35 @@
</button>
</Popover>
<BorderPanel />
<div class="divider"></div>
<Popover trigger="click">
<template #content>
<div class="table-command-menu">
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'before')">上方插入行</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'after')">下方插入行</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'before')">左侧插入列</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'after')">右侧插入列</PopoverMenuItem>
</div>
</template>
<button class="toolbar-btn">
<i-icon-park-outline:add class="icon" />
<span>添加</span>
</button>
</Popover>
<Popover trigger="click">
<template #content>
<div class="table-command-menu">
<PopoverMenuItem center @click="emitTableCommand('delete-row')">删除行</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('delete-col')">删除列</PopoverMenuItem>
</div>
</template>
<button class="toolbar-btn">
<i-icon-park-outline:reduce class="icon" />
<span>删除</span>
</button>
</Popover>
</div>
</template>
@@ -18,13 +47,15 @@ import { computed, type Ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTTableElement, TableCell, TableCellStyle } from '@/types/slides'
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import BorderPanel from './BorderPanel.vue'
import Popover from '@/components/Popover.vue'
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
defineProps<{
const props = defineProps<{
elementInfo: PPTTableElement
}>()
@@ -49,6 +80,14 @@ const cellBackcolor = computed(() => {
const { addHistorySnapshot } = useHistorySnapshot()
const emitTableCommand = (command: TableCommand['command'], position?: TableCommand['position']) => {
emitter.emit(EmitterEvents.TABLE_COMMAND, {
targetId: props.elementInfo.id,
command,
position,
})
}
const updateCellBackcolor = (backcolor: string) => {
const el = handleTableElement.value
if (!el || el.type !== 'table') return
@@ -105,4 +144,14 @@ const updateCellBackcolor = (backcolor: string) => {
margin-left: 5px;
}
}
.divider {
width: 1px;
height: 18px;
background-color: $borderColor;
margin: 0 4px;
flex-shrink: 0;
}
.table-command-menu {
width: 100px;
}
</style>
@@ -113,20 +113,32 @@
<Divider />
<div class="row">
<div style="width: 40%;"></div>
<div class="set-count" style="width: 60%;">
<Button class="btn" :disabled="rowCount <= 1" @click="setTableRow(rowCount - 1)"><i-icon-park-outline:minus /></Button>
<div class="count-text">{{rowCount}}</div>
<Button class="btn" :disabled="rowCount >= 30" @click="setTableRow(rowCount + 1)"><i-icon-park-outline:plus /></Button>
</div>
<div style="width: 40%;">操作</div>
<ButtonGroup style="width: 60%;" passive>
<Button first style="flex: 1;" @click="emitTableCommand('insert-row', 'after')">添加行</Button>
<Popover trigger="click">
<template #content>
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'before')">上方添加</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('insert-row', 'after')">下方添加</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('delete-row')">删除行</PopoverMenuItem>
</template>
<Button last class="popover-btn"><i-icon-park-outline:down /></Button>
</Popover>
</ButtonGroup>
</div>
<div class="row">
<div style="width: 40%;"></div>
<div class="set-count" style="width: 60%;">
<Button class="btn" :disabled="colCount <= 1" @click="setTableCol(colCount - 1)"><i-icon-park-outline:minus /></Button>
<div class="count-text">{{colCount}}</div>
<Button class="btn" :disabled="colCount >= 30" @click="setTableCol(colCount + 1)"><i-icon-park-outline:plus /></Button>
</div>
<div style="width: 40%;">操作</div>
<ButtonGroup style="width: 60%;" passive>
<Button first style="flex: 1;" @click="emitTableCommand('insert-col', 'after')">添加列</Button>
<Popover trigger="click">
<template #content>
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'before')">左侧添加</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('insert-col', 'after')">右侧添加</PopoverMenuItem>
<PopoverMenuItem center @click="emitTableCommand('delete-col')">删除列</PopoverMenuItem>
</template>
<Button last class="popover-btn"><i-icon-park-outline:down /></Button>
</Popover>
</ButtonGroup>
</div>
<Divider />
@@ -185,10 +197,10 @@
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { nanoid } from 'nanoid'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTTableElement, TableCell, TableCellStyle, TableTheme, TextAlign, TextAlignVertical } from '@/types/slides'
import { FONTS } from '@/configs/font'
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import ElementOutline from '../common/ElementOutline.vue'
@@ -206,6 +218,7 @@ import RadioGroup from '@/components/RadioGroup.vue'
import Select from '@/components/Select.vue'
import SelectGroup from '@/components/SelectGroup.vue'
import Popover from '@/components/Popover.vue'
import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
const slidesStore = useSlidesStore()
const { handleElement, handleElementId, selectedTableCells: selectedCells } = storeToRefs(useMainStore())
@@ -230,22 +243,12 @@ const textAttrs = ref({
const theme = ref<TableTheme>()
const hasTheme = ref(false)
const rowCount = ref(0)
const colCount = ref(0)
const minRowCount = ref(0)
const minColCount = ref(0)
watch(handleElement, () => {
if (!handleElement.value || handleElement.value.type !== 'table') return
theme.value = handleElement.value.theme
hasTheme.value = !!theme.value
rowCount.value = handleElement.value.data.length
colCount.value = handleElement.value.data[0].length
minRowCount.value = handleElement.value.data.length
minColCount.value = handleElement.value.data[0].length
}, { deep: true, immediate: true })
const { addHistorySnapshot } = useHistorySnapshot()
@@ -349,67 +352,12 @@ const toggleTheme = (checked: boolean) => {
}
}
// 设置表格行数
const setTableRow = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const rowCount = _handleElement.data.length
if (value > rowCount) {
const newTableCells: TableCell[][] = []
for (let i = 0; i < value - rowCount; i++) {
const rowCells: TableCell[] = []
for (let j = 0; j < colCount.value; j++) {
rowCells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
}
newTableCells.push(rowCells)
}
const tableCells: TableCell[][] = JSON.parse(JSON.stringify(_handleElement.data))
tableCells.push(...newTableCells)
updateElement({ data: tableCells })
}
else {
const tableCells: TableCell[][] = _handleElement.data.slice(0, value)
updateElement({ data: tableCells })
}
}
// 设置表格列数
const setTableCol = (value: number) => {
const _handleElement = handleElement.value as PPTTableElement
const colCount = _handleElement.data[0].length
let tableCells = _handleElement.data
let colSizeList = _handleElement.colWidths.map(item => item * _handleElement.width)
if (value > colCount) {
tableCells = tableCells.map(item => {
const cells: TableCell[] = []
for (let i = 0; i < value - colCount; i++) {
cells.push({ id: nanoid(10), colspan: 1, rowspan: 1, text: '' })
}
item.push(...cells)
return item
})
const newColSizeList: number[] = new Array(value - colCount).fill(100)
colSizeList.push(...newColSizeList)
}
else {
tableCells = tableCells.map(item => item.slice(0, value))
colSizeList = colSizeList.slice(0, value)
}
const width = colSizeList.reduce((a, b) => a + b)
const colWidths = colSizeList.map(item => item / width)
const props = {
width,
data: tableCells,
colWidths,
}
updateElement(props)
const emitTableCommand = (command: TableCommand['command'], position?: TableCommand['position']) => {
emitter.emit(EmitterEvents.TABLE_COMMAND, {
targetId: handleElementId.value,
command,
position,
})
}
</script>
@@ -426,19 +374,7 @@ const setTableCol = (value: number) => {
.switch-wrapper {
text-align: right;
}
.set-count {
display: flex;
justify-content: center;
align-items: center;
.btn {
padding: 0 8px;
}
.count-text {
flex: 1;
text-align: center;
margin: 0 8px;
}
.popover-btn {
padding: 0 3px;
}
</style>
@@ -70,6 +70,8 @@ import { useMainStore } from '@/store'
import type { PPTElementOutline, TableCell, TableTheme } from '@/types/slides'
import type { ContextmenuItem } from '@/components/Contextmenu/types'
import { KEYS } from '@/configs/hotkey'
import emitter, { EmitterEvents, type TableCommand } from '@/utils/emitter'
import message from '@/utils/message'
import { getCellStyle, getTextStyle, formatText } from './utils'
import useHideCells from './useHideCells'
import useSubThemeColor from './useSubThemeColor'
@@ -77,6 +79,7 @@ import useSubThemeColor from './useSubThemeColor'
import CustomTextarea from './CustomTextarea.vue'
const props = withDefaults(defineProps<{
elementId: string
data: TableCell[][]
width: number
cellMinHeight: number
@@ -659,6 +662,49 @@ const checkCanDeleteRowOrCol = () => {
return { canDeleteRow, canDeleteCol }
}
const getCommandTargetCell = () => {
const selectedCell = selectedCells.value.find(cell => !hideCells.value.includes(cell))
if (!selectedCell) return null
const [rowIndex, colIndex] = selectedCell.split('_').map(Number)
return { rowIndex, colIndex }
}
const execTableCommand = (payload: TableCommand) => {
if (payload.targetId !== props.elementId) return
const targetCell = getCommandTargetCell()
const isBefore = payload.position === 'before'
if (payload.command === 'insert-row') {
const rowIndex = targetCell ? (isBefore ? targetCell.rowIndex : targetCell.rowIndex + 1) : (isBefore ? 0 : tableCells.value.length)
return insertRow(rowIndex)
}
if (payload.command === 'insert-col') {
const colCount = tableCells.value[0]?.length || 0
const colIndex = targetCell ? (isBefore ? targetCell.colIndex : targetCell.colIndex + 1) : (isBefore ? 0 : colCount)
return insertCol(colIndex)
}
const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()
if (payload.command === 'delete-row') {
if (!canDeleteRow) return message.warning('表格至少保留一行')
return deleteRow(targetCell ? targetCell.rowIndex : tableCells.value.length - 1)
}
if (payload.command === 'delete-col') {
if (!canDeleteCol) return message.warning('表格至少保留一列')
const colCount = tableCells.value[0]?.length || 1
return deleteCol(targetCell ? targetCell.colIndex : colCount - 1)
}
}
onMounted(() => {
emitter.on(EmitterEvents.TABLE_COMMAND, execTableCommand)
})
onUnmounted(() => {
emitter.off(EmitterEvents.TABLE_COMMAND, execTableCommand)
})
// 检查是否可以合并或拆分
// 必须多选才可以合并
// 必须单选且所选单元格为合并单元格才可以拆分
@@ -835,4 +881,4 @@ table {
z-index: 2;
cursor: col-resize;
}
</style>
</style>
@@ -19,6 +19,7 @@
>
<EditableTable
@mousedown.stop
:elementId="elementInfo.id"
:data="elementInfo.data"
:width="elementInfo.width"
:cellMinHeight="elementInfo.cellMinHeight"