Bläddra i källkod

可编辑表格组件开发

pipipi-pikachu 5 år sedan
förälder
incheckning
6ccbea6cc1
2 ändrade filer med 475 tillägg och 0 borttagningar
  1. 15 0
      src/App.vue
  2. 460 0
      src/components/EditableTable.vue

+ 15 - 0
src/App.vue

@@ -1,6 +1,10 @@
 <template>
   <Editor v-if="!screening" />
   <Screen v-else />
+
+  <div class="test">
+    <EditableTable />
+  </div>
 </template>
 
 <script lang="ts">
@@ -10,12 +14,14 @@ import { MutationTypes, ActionTypes, State } from '@/store'
 
 import Editor from './views/Editor/index.vue'
 import Screen from './views/Screen/index.vue'
+import EditableTable from '@/components/EditableTable.vue'
 
 export default defineComponent({
   name: 'app',
   components: {
     Editor,
     Screen,
+    EditableTable,
   },
   setup() {
     const store = useStore<State>()
@@ -37,4 +43,13 @@ export default defineComponent({
 #app {
   height: 100%;
 }
+.test {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: #fff;
+  padding: 10px;
+}
 </style>

+ 460 - 0
src/components/EditableTable.vue

@@ -0,0 +1,460 @@
+<template>
+  <div 
+    class="editable-table"
+    :style="{ width: width + 'px' }"
+  >
+    <table>
+      <colgroup>
+        <col span="1" v-for="(width, index) in colWidths" :key="index" :width="width">
+      </colgroup>
+      <tbody>
+        <tr
+          v-for="(rowCells, rowIndex) in tableCells" 
+          :key="rowIndex"
+        >
+          <td 
+            class="cell"
+            :class="{
+              'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
+              'active': activedCell === `${rowIndex}_${colIndex}`,
+            }"
+            v-for="(cell, colIndex) in rowCells"
+            :key="cell.id"
+            :rowspan="cell.rowspan"
+            :colspan="cell.colspan"
+            :data-cell-index="`${rowIndex}_${colIndex}`"
+            v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
+            @mousedown="$event => handleCellMousedown($event, rowIndex, colIndex)"
+            @mouseenter="handleCellMouseenter(rowIndex, colIndex)"
+            v-contextmenu="el => contextmenus(el)"
+          >
+            <div 
+              class="cell-text" 
+              :contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
+            ></div>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+
+<script lang="ts">
+import { createRandomCode } from '@/utils/common'
+import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
+import { ContextmenuItem } from './Contextmenu/types'
+
+interface TableCells {
+  id: string;
+  colspan: number;
+  rowspan: number;
+  text: string;
+  style?: {
+    color?: string;
+    bgColor?: string;
+    fontSize?: number;
+    fontName?: string;
+    bold?: boolean;
+    italic?: boolean;
+    align?: string;
+  };
+}
+
+export default defineComponent({
+  name: 'editable-table',
+  setup() {
+    const tableCells = ref<TableCells[][]>([
+      [
+        { id: '1', colspan: 1, rowspan: 1, text: '' },
+        { id: '2', colspan: 1, rowspan: 1, text: '' },
+        { id: '3', colspan: 1, rowspan: 1, text: '' },
+        { id: '4', colspan: 1, rowspan: 1, text: '' },
+        { id: '5', colspan: 1, rowspan: 1, text: '' },
+      ],
+      [
+        { id: '6', colspan: 1, rowspan: 1, text: '' },
+        { id: '7', colspan: 1, rowspan: 1, text: '' },
+        { id: '8', colspan: 1, rowspan: 1, text: '' },
+        { id: '9', colspan: 1, rowspan: 1, text: '' },
+        { id: '10', colspan: 1, rowspan: 1, text: '' },
+      ],
+      [
+        { id: '11', colspan: 1, rowspan: 1, text: '' },
+        { id: '12', colspan: 1, rowspan: 1, text: '' },
+        { id: '13', colspan: 1, rowspan: 1, text: '' },
+        { id: '14', colspan: 1, rowspan: 1, text: '' },
+        { id: '15', colspan: 1, rowspan: 1, text: '' },
+      ],
+    ])
+    const width = 800
+    const colWidths = ref([160, 160, 160, 160, 160])
+    const isStartSelect = ref(false)
+    const startCell = ref<number[]>([])
+    const endCell = ref<number[]>([])
+
+    const hideCells = computed(() => {
+      const hideCells = []
+      
+      for(let i = 0; i < tableCells.value.length; i++) {
+        const rowCells = tableCells.value[i]
+
+        for(let j = 0; j < rowCells.length; j++) {
+          const cell = rowCells[j]
+          
+          if(cell.colspan > 1 || cell.rowspan > 1) {
+            for(let row = i; row < i + cell.rowspan; row++) {
+              for(let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) {
+                hideCells.push(`${row}_${col}`)
+              }
+            }
+          }
+        }
+      }
+      return hideCells
+    })
+
+    const selectedCells = computed(() => {
+      if(!startCell.value.length) return []
+      const [startX, startY] = startCell.value
+
+      if(!endCell.value.length) return [`${startX}_${startY}`]
+      const [endX, endY] = endCell.value
+
+      if(startX === endX && startY === endY) return [`${startX}_${startY}`]
+
+      const selectedCells = []
+
+      const minX = Math.min(startX, endX)
+      const minY = Math.min(startY, endY)
+      const maxX = Math.max(startX, endX)
+      const maxY = Math.max(startY, endY)
+
+      for(let i = 0; i < tableCells.value.length; i++) {
+        const rowCells = tableCells.value[i]
+        for(let j = 0; j < rowCells.length; j++) {
+          if(i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)
+        }
+      }
+      return selectedCells
+    })
+
+    const activedCell = computed(() => {
+      if(selectedCells.value.length > 1) return null
+      return selectedCells.value[0]
+    })
+
+    const selectedRange = computed(() => {
+      if(!startCell.value.length) return null
+      const [startX, startY] = startCell.value
+
+      if(!endCell.value.length) return { row: [startX, startX], col: [startY, startY] }
+      const [endX, endY] = endCell.value
+
+      if(startX === endX && startY === endY) return { row: [startX, startX], col: [startY, startY] }
+
+      const minX = Math.min(startX, endX)
+      const minY = Math.min(startY, endY)
+      const maxX = Math.max(startX, endX)
+      const maxY = Math.max(startY, endY)
+
+      return {
+        row: [minX, maxX],
+        col: [minY, maxY],
+      }
+    })
+
+    const handleMouseup = () => isStartSelect.value = false
+
+    const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
+      if(e.which !== 1) return
+
+      endCell.value = []
+      isStartSelect.value = true
+      startCell.value = [rowIndex, colIndex]
+    }
+
+    const handleCellMouseenter = (rowIndex: number, colIndex: number) => {
+      if(!isStartSelect.value) return
+      endCell.value = [rowIndex, colIndex]
+    }
+
+    onMounted(() => {
+      document.addEventListener('mouseup', handleMouseup)
+    })
+    onUnmounted(() => {
+      document.removeEventListener('mouseup', handleMouseup)
+    })
+
+    const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
+
+    const removeSelectedCells = () => {
+      startCell.value = []
+      endCell.value = []
+    }
+
+    const selectCol = (index: number) => {
+      const maxRow = tableCells.value.length - 1
+      startCell.value = [0, index]
+      endCell.value = [maxRow, index]
+    }
+
+    const selectRow = (index: number) => {
+      const maxCol = tableCells.value[index].length - 1
+      startCell.value = [index, 0]
+      endCell.value = [index, maxCol]
+    }
+
+    const selectAll = () => {
+      const maxRow = tableCells.value.length - 1
+      const maxCol = tableCells.value[maxRow].length - 1
+      startCell.value = [0, 0]
+      endCell.value = [maxRow, maxCol]
+    }
+
+    const deleteRow = (rowIndex: number) => {
+      const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
+
+      const targetCells = tableCells.value[rowIndex]
+      const hideCellsPos = []
+      for(let i = 0; i < targetCells.length; i++) {
+        if(isHideCell(rowIndex, i)) hideCellsPos.push(i)
+      }
+      
+      for(const pos of hideCellsPos) {
+        for(let i = rowIndex; i >= 0; i--) {
+          if(!isHideCell(i, pos)) {
+            _tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1
+            break
+          }
+        }
+      }
+
+      _tableCells.splice(rowIndex, 1)
+      tableCells.value = _tableCells
+    }
+
+    const deleteCol = (colIndex: number) => {
+      const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
+
+      const hideCellsPos = []
+      for(let i = 0; i < tableCells.value.length; i++) {
+        if(isHideCell(i, colIndex)) hideCellsPos.push(i)
+      }
+
+      for(const pos of hideCellsPos) {
+        for(let i = colIndex; i >= 0; i--) {
+          if(!isHideCell(pos, i)) {
+            _tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1
+            break
+          }
+        }
+      }
+
+      tableCells.value = _tableCells.map(item => {
+        item.splice(colIndex, 1)
+        return item
+      })
+    }
+    
+    const insertRow = (selectedIndex: number, rowIndex: number) => {
+      const rowCells: TableCells[] = []
+      for(let i = 0; i < tableCells.value[0].length; i++) {
+        rowCells.push({
+          colspan: 1,
+          rowspan: 1,
+          text: '',
+          id: createRandomCode(),
+        })
+      }
+
+      tableCells.value.splice(rowIndex, 0, rowCells)
+    }
+
+    const insertCol = (selectedIndex: number, colIndex: number) => {
+      tableCells.value = tableCells.value.map(item => {
+        const cell = {
+          colspan: 1,
+          rowspan: 1,
+          text: '',
+          id: createRandomCode(),
+        }
+        item.splice(colIndex, 0, cell)
+        return item
+      })
+    }
+    
+    const mergeCells = () => {
+      const [startX, startY] = startCell.value
+      const [endX, endY] = endCell.value
+
+      const minX = Math.min(startX, endX)
+      const minY = Math.min(startY, endY)
+      const maxX = Math.max(startX, endX)
+      const maxY = Math.max(startY, endY)
+
+      const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
+      
+      _tableCells[minX][minY].rowspan = maxX - minX + 1
+      _tableCells[minX][minY].colspan = maxY - minY + 1
+
+      tableCells.value = _tableCells
+      removeSelectedCells()
+    }
+
+    const splitCells = (rowIndex: number, colIndex: number) => {
+      const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
+      _tableCells[rowIndex][colIndex].rowspan = 1
+      _tableCells[rowIndex][colIndex].colspan = 1
+
+      tableCells.value = _tableCells
+      removeSelectedCells()
+    }
+
+    const getEffectiveTableCells = () => {
+      const effectiveTableCells = []
+
+      for(let i = 0; i < tableCells.value.length; i++) {
+        const rowCells = tableCells.value[i]
+        const _rowCells = []
+        for(let j = 0; j < rowCells.length; j++) {
+          if(!isHideCell(i, j)) _rowCells.push(rowCells[j])
+        }
+        if(_rowCells.length) effectiveTableCells.push(_rowCells)
+      }
+
+      return effectiveTableCells
+    }
+
+    const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
+      const cellIndex = el.dataset.cellIndex as string
+      const rowIndex = +cellIndex.split('_')[0]
+      const colIndex = +cellIndex.split('_')[1]
+
+      if(!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {
+        startCell.value = [rowIndex, colIndex]
+        endCell.value = []
+      }
+
+      const isMultiSelected = selectedCells.value.length > 1
+
+      const targetCell = tableCells.value[rowIndex][colIndex]
+      const canSplit = targetCell.rowspan > 1 || targetCell.colspan > 1
+
+      const effectiveTableCells = getEffectiveTableCells()
+      const canDeleteRow = effectiveTableCells.length > 1
+      const canDeleteCol = effectiveTableCells[0].length > 1
+
+      return [
+        {
+          text: '插入列',
+          children: [
+            { text: '到左侧', handler: () => insertCol(colIndex, colIndex) },
+            { text: '到右侧', handler: () => insertCol(colIndex, colIndex + 1) },
+          ],
+        },
+        {
+          text: '插入行',
+          children: [
+            { text: '到上方', handler: () => insertRow(rowIndex, rowIndex) },
+            { text: '到下方', handler: () => insertRow(rowIndex, rowIndex + 1) },
+          ],
+        },
+        {
+          text: '删除列',
+          disable: !canDeleteCol,
+          handler: () => deleteCol(colIndex),
+        },
+        {
+          text: '删除行',
+          disable: !canDeleteRow,
+          handler: () => deleteRow(rowIndex),
+        },
+        { divider: true },
+        {
+          text: '合并单元格',
+          hide: !isMultiSelected,
+          handler: mergeCells,
+        },
+        {
+          text: '取消合并单元格',
+          hide: isMultiSelected || !canSplit,
+          handler: () => splitCells(rowIndex, colIndex),
+        },
+        {
+          text: '选中全部单元格',
+          handler: selectAll,
+        },
+      ]
+    }
+
+
+    return {
+      width,
+      tableCells,
+      colWidths,
+      hideCells,
+      selectedCells,
+      activedCell,
+      selectedRange,
+      handleCellMousedown,
+      handleCellMouseenter,
+      selectCol,
+      selectRow,
+      contextmenus,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.editable-table {
+  position: relative;
+}
+table {
+  width: 100%;
+  position: relative;
+  table-layout: fixed;
+  border-collapse: collapse;
+  border-spacing: 0;
+  word-wrap: break-word;
+
+  .cell {
+    padding: 5px;
+    position: relative;
+    white-space: normal;
+    word-wrap: break-word;
+    vertical-align: middle;
+    border: 1px solid #d9d9d9;
+    cursor: default;
+
+    &.selected::after {
+      content: '';
+      width: 100%;
+      height: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+      background-color: rgba($color: #888, $alpha: .1);
+    }
+  }
+
+  .cell-text {
+    min-height: 22px;
+    border: 0;
+    outline: 0;
+    line-height: 1.5;
+    font-size: 14px;
+    user-select: none;
+    cursor: text;
+
+    &.active {
+      user-select: text;
+    }
+
+    ::selection {
+      background-color: rgba(27, 110, 232, 0.3);
+      color: inherit;
+    }
+  }
+}
+</style>