添加表格数据编辑器
This commit is contained in:
parent
0df6812fd6
commit
706ccf5fc8
|
@ -203,10 +203,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '网格参考线',
|
text: '网格参考线',
|
||||||
children: [
|
|
||||||
{ text: '网格线', handler: toggleGridLines },
|
|
||||||
{ text: '参考线' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '清空本页',
|
text: '清空本页',
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<div class="menu-item"><IconSeoFolder /> 文件</div>
|
<div class="menu-item"><IconSeoFolder /> 文件</div>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem>导出JSON文件</MenuItem>
|
<MenuItem>重置幻灯片</MenuItem>
|
||||||
<MenuItem>保存到本地</MenuItem>
|
<MenuItem>缓存幻灯片</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
<MenuItem>重做</MenuItem>
|
<MenuItem>重做</MenuItem>
|
||||||
<MenuItem>添加页面</MenuItem>
|
<MenuItem>添加页面</MenuItem>
|
||||||
<MenuItem>删除页面</MenuItem>
|
<MenuItem>删除页面</MenuItem>
|
||||||
|
<MenuItem>网格参考线</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -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>
|
|
@ -1,18 +1,63 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chart-style-panel">
|
<div class="chart-style-panel">
|
||||||
|
<Button class="full-width-btn" @click="chartDataEditorVisible = true">
|
||||||
|
<IconEdit class="btn-icon" /> 编辑图表数据
|
||||||
|
</Button>
|
||||||
<ElementOutline />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 ElementOutline from '../common/ElementOutline.vue'
|
||||||
|
import ChartDataEditor from './ChartDataEditor.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'chart-style-panel',
|
name: 'chart-style-panel',
|
||||||
components: {
|
components: {
|
||||||
ElementOutline,
|
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>
|
</script>
|
||||||
|
@ -24,4 +69,11 @@ export default defineComponent({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.full-width-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -3,7 +3,7 @@
|
||||||
<div v-if="!currentPanelComponent">
|
<div v-if="!currentPanelComponent">
|
||||||
请先选中要编辑的元素
|
请先选中要编辑的元素
|
||||||
</div>
|
</div>
|
||||||
<component :is="currentPanelComponent"></component>
|
<component v-if="handleElement" :is="currentPanelComponent"></component>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
handleElement,
|
||||||
currentPanelComponent,
|
currentPanelComponent,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue