Przeglądaj źródła

添加表格数据编辑器

pipipi-pikachu 5 lat temu
rodzic
commit
706ccf5fc8

+ 0 - 4
src/views/Editor/Canvas/index.vue

@@ -203,10 +203,6 @@ export default defineComponent({
         },
         {
           text: '网格参考线',
-          children: [
-            { text: '网格线', handler: toggleGridLines },
-            { text: '参考线' },
-          ],
         },
         {
           text: '清空本页',

+ 3 - 2
src/views/Editor/EditorHeader/index.vue

@@ -5,8 +5,8 @@
         <div class="menu-item"><IconSeoFolder /> 文件</div>
         <template #overlay>
           <Menu>
-            <MenuItem>导出JSON文件</MenuItem>
-            <MenuItem>保存到本地</MenuItem>
+            <MenuItem>重置幻灯片</MenuItem>
+            <MenuItem>缓存幻灯片</MenuItem>
           </Menu>
         </template>
       </Dropdown>
@@ -18,6 +18,7 @@
             <MenuItem>重做</MenuItem>
             <MenuItem>添加页面</MenuItem>
             <MenuItem>删除页面</MenuItem>
+            <MenuItem>网格参考线</MenuItem>
           </Menu>
         </template>
       </Dropdown>

+ 296 - 0
src/views/Editor/Toolbar/ElementStylePanel/ChartDataEditor.vue

@@ -0,0 +1,296 @@
+<template>
+  <div class="chart-data-editor">
+    <div class="editor-content">
+      <div class="range-box">
+        <div 
+          class="temp-range" 
+          :style="{
+            width: tempRangeSize.width + 'px',
+            height: tempRangeSize.height + 'px',
+          }"
+        ></div>
+        <div 
+          :class="['range-line', line.type]" 
+          v-for="line in rangeLines" 
+          :key="line.type" 
+          :style="line.style"
+        ></div>
+        <div 
+          class="resizable" 
+          :style="resizablePointStyle"
+          @mousedown.stop="changeSelectRange($event)"
+        ></div>
+      </div>
+      <table>
+        <tbody>
+          <tr v-for="rowIndex in 30" :key="rowIndex">
+            <td v-for="colIndex in 7" :key="colIndex" :class="{ 'head': colIndex === 1 && rowIndex <= selectedRange[1] }">
+              <input 
+                :class="['item', { 'selected': rowIndex <= selectedRange[1] && colIndex <= selectedRange[0] }]"
+                :id="`cell-${rowIndex - 1}-${colIndex - 1}`"
+              >
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <div class="btns">
+      <Button class="btn" @click="closeEditor()">取消</Button>
+      <Button type="primary" class="btn" @click="getTableData()">确认</Button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ChartData } from '@/types/slides'
+import { computed, defineComponent, onMounted, PropType, ref } from 'vue'
+
+const CELL_WIDTH = 100
+const CELL_HEIGHT = 32
+
+export default defineComponent({
+  name: 'chart-data-editor',
+  props: {
+    data: {
+      type: Object as PropType<ChartData>,
+      required: true,
+    }
+  },
+  setup(props, { emit }) {
+    const selectedRange = ref([0, 0])
+    const tempRangeSize = ref({ width: 0, height: 0 })
+
+    const rangeLines = computed(() => {
+      const width = selectedRange.value[0] * CELL_WIDTH
+      const height = selectedRange.value[1] * CELL_HEIGHT
+      return [
+        { type: 't', style: {width: width + 'px'} },
+        { type: 'b', style: {top: height + 'px', width: width + 'px'} },
+        { type: 'l', style: {height: height + 'px'} },
+        { type: 'r', style: {left: width + 'px', height: height + 'px'} },
+      ]
+    })
+
+    const resizablePointStyle = computed(() => {
+      const width = selectedRange.value[0] * CELL_WIDTH
+      const height = selectedRange.value[1] * CELL_HEIGHT
+      return { left: width + 'px', top: height + 'px' }
+    })
+
+    const initData = () => {
+      const _data: string[][] = []
+
+      const { labels, series } = props.data
+      const rowCount = labels.length
+      const colCount = series.length
+
+      for(let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+        const row = [labels[rowIndex]]
+        for(let colIndex = 0; colIndex < colCount; colIndex++) {
+          row.push(series[colIndex][rowIndex] + '')
+        }
+        _data.push(row)
+      }
+
+      for(let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+        for(let colIndex = 0; colIndex < colCount + 1; colIndex++) {
+          const inputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
+          if(!inputRef) continue
+          inputRef.value = _data[rowIndex][colIndex] + ''
+        }
+      }
+
+      selectedRange.value = [colCount + 1, rowCount]
+    }
+
+    onMounted(initData)
+
+    const getTableData = () => {
+      const [col, row] = selectedRange.value
+
+      const labels: string[] = []
+      const series: number[][] = []
+
+      for(let rowIndex = 0; rowIndex < row; rowIndex++) {
+        let labelsItem = `类别${rowIndex + 1}`
+        const labelInputRef = document.querySelector(`#cell-${rowIndex}-0`) as HTMLInputElement
+        if(labelInputRef && labelInputRef.value) labelsItem = labelInputRef.value
+        labels.push(labelsItem)
+      }
+
+      for(let colIndex = 1; colIndex < col; colIndex++) {
+        const seriesItem = []
+        for(let rowIndex = 0; rowIndex < row; rowIndex++) {
+          const valueInputRef = document.querySelector(`#cell-${rowIndex}-${colIndex}`) as HTMLInputElement
+          let value = 0
+          if(valueInputRef && valueInputRef.value && !!(+valueInputRef.value)) {
+            value = +valueInputRef.value
+          }
+          seriesItem.push(value)
+        }
+        series.push(seriesItem)
+      }
+      const data = { labels, series }
+      emit('save', data)
+    }
+
+    const closeEditor = () => emit('close')
+
+    const changeSelectRange = (e: MouseEvent) => {
+      let isMouseDown = true
+
+      const startPageX = e.pageX
+      const startPageY = e.pageY
+
+      const originWidth = selectedRange.value[0] * CELL_WIDTH
+      const originHeight = selectedRange.value[1] * CELL_HEIGHT
+
+      document.onmousemove = e => {
+        if(!isMouseDown) return
+
+        const currentPageX = e.pageX
+        const currentPageY = e.pageY
+
+        const x = currentPageX - startPageX
+        const y = currentPageY - startPageY
+
+        let width = originWidth + x
+        let height = originHeight + y
+
+        if(width % CELL_WIDTH > CELL_WIDTH * 0.5) width = width + (CELL_WIDTH - width % CELL_WIDTH)
+        if(height % CELL_HEIGHT > CELL_HEIGHT * 0.5) height = height + (CELL_HEIGHT - height % CELL_HEIGHT)
+
+        tempRangeSize.value = { width, height }
+      }
+
+      document.onmouseup = () => {
+        isMouseDown = false
+        document.onmousemove = null
+        document.onmouseup = null
+
+        const { width, height } = tempRangeSize.value
+
+        let row = Math.round(height / CELL_HEIGHT)
+        let col = Math.round(width / CELL_WIDTH)
+
+        if(row < 3) row = 3
+        if(col < 2) col = 2
+
+        selectedRange.value = [col, row]
+        tempRangeSize.value = { width: 0, height: 0 }
+      }
+    }
+
+    return {
+      tempRangeSize,
+      rangeLines,
+      resizablePointStyle,
+      changeSelectRange,
+      selectedRange,
+      getTableData,
+      closeEditor,
+    }
+  },
+})
+</script>
+
+<style lang="scss" scoped>
+.chart-data-editor {
+  width: 600px;
+  position: relative;
+}
+.editor-content {
+  width: 100%;
+  height: 360px;
+  overflow: overlay;
+  position: relative;
+  border-right: 1px solid #ccc;
+  border-bottom: 1px solid #ccc;
+}
+.range-box {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  user-select: none;
+}
+.temp-range {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 0;
+  height: 0;
+  background-color: rgba($color: #888, $alpha: .3);
+}
+.range-line {
+  position: absolute;
+  width: 0;
+  height: 0;
+  left: 0;
+  top: 0;
+  border: 0 solid $themeColor;
+
+  &.t {
+    border-top-width: 1px;
+  }
+  &.b {
+    border-bottom-width: 1px;
+  }
+  &.l {
+    border-left-width: 1px;
+  }
+  &.r {
+    border-right-width: 1px;
+  }
+}
+.resizable {
+  position: absolute;
+  width: 8px;
+  height: 8px;
+  left: 0;
+  top: 0;
+  margin: -4px 0 0 -4px;
+  background-color: $themeColor;
+  cursor: nwse-resize;
+}
+table {
+  width: 100%;
+  height: 100%;
+  user-select: none;
+  table-layout: fixed;
+
+  td {
+    text-align: center;
+    border: 1px solid #ccc;
+    vertical-align: middle;
+    width: 100px;
+    height: 32px;
+
+    &.head {
+      background-color: rgba($color: $themeColor, $alpha: .1);
+    }
+  }
+  .item {
+    width: 100%;
+    height: 100%;
+    border: 0;
+    outline: 0;
+    font-size: 13px;
+    text-align: center;
+    background-color: transparent;
+
+    &.selected {
+      background-color: rgba($color: $themeColor, $alpha: .02);
+    }
+  }
+}
+.btns {
+  margin-top: 10px;
+  text-align: right;
+
+  .btn {
+    margin-left: 10px;
+  }
+}
+</style>

+ 53 - 1
src/views/Editor/Toolbar/ElementStylePanel/ChartStylePanel.vue

@@ -1,18 +1,63 @@
 <template>
   <div class="chart-style-panel">
+    <Button class="full-width-btn" @click="chartDataEditorVisible = true">
+      <IconEdit class="btn-icon" /> 编辑图表数据
+    </Button>
     <ElementOutline />
+
+    <Modal
+      v-model:visible="chartDataEditorVisible" 
+      :footer="null" 
+      centered
+      :closable="false"
+      :width="648"
+      destroyOnClose
+    >
+      <ChartDataEditor 
+        :data="handleElement.data"
+        @close="chartDataEditorVisible = false"
+        @save="value => updateData(value)"
+      />
+    </Modal>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { computed, defineComponent, Ref, ref } from 'vue'
+import { useStore } from 'vuex'
+import { MutationTypes, State } from '@/store'
+import { ChartData, PPTChartElement } from '@/types/slides'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
 
 import ElementOutline from '../common/ElementOutline.vue'
+import ChartDataEditor from './ChartDataEditor.vue'
 
 export default defineComponent({
   name: 'chart-style-panel',
   components: {
     ElementOutline,
+    ChartDataEditor,
+  },
+  setup() {
+    const store = useStore<State>()
+    const handleElement: Ref<PPTChartElement> = computed(() => store.getters.handleElement)
+
+    const chartDataEditorVisible = ref(false)
+
+    const { addHistorySnapshot } = useHistorySnapshot()
+
+    const updateData = (data: ChartData) => {
+      chartDataEditorVisible.value = false
+      const props = { data }
+      store.commit(MutationTypes.UPDATE_ELEMENT, { id: handleElement.value.id, props })
+      addHistorySnapshot()
+    }
+
+    return {
+      chartDataEditorVisible,
+      handleElement,
+      updateData,
+    }
   },
 })
 </script>
@@ -24,4 +69,11 @@ export default defineComponent({
   align-items: center;
   margin-bottom: 10px;
 }
+.full-width-btn {
+  width: 100%;
+  margin-bottom: 10px;
+}
+.btn-icon {
+  margin-right: 3px;
+}
 </style>

+ 2 - 1
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -3,7 +3,7 @@
     <div v-if="!currentPanelComponent">
       请先选中要编辑的元素
     </div>
-    <component :is="currentPanelComponent"></component>
+    <component v-if="handleElement" :is="currentPanelComponent"></component>
   </div>
 </template>
 
@@ -39,6 +39,7 @@ export default defineComponent({
     })
 
     return {
+      handleElement,
       currentPanelComponent,
     }
   },