| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <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}`"
- autocomplete="off"
- >
- </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' }
- })
- // 初始化图表数据:将数据格式化并填充到DOM
- 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)
- // 获取当前图表DOM中的数据,整理格式化后传递出去
- 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
- const width = originWidth + x
- const height = originHeight + y
- tempRangeSize.value = { width, height }
- }
- document.onmouseup = e => {
- isMouseDown = false
- document.onmousemove = null
- document.onmouseup = null
- const endPageX = e.pageX
- const endPageY = e.pageY
- if (startPageX === endPageX && startPageY === endPageY) return
- // 拖拽结束时,范围超过格子一半自动扩大到下一格(如拖动到一格半多的位置,会自动扩展到两格,横竖都同理)
- let width = tempRangeSize.value.width
- let height = tempRangeSize.value.height
- 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)
- 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: 12px;
- height: 12px;
- left: 0;
- top: 0;
- margin: -9px 0 0 -9px;
- cursor: nwse-resize;
- &::after {
- content: '';
- position: absolute;
- width: 4px;
- height: 12px;
- right: 0;
- top: 0;
- background-color: $themeColor;
- }
- &::before {
- content: '';
- position: absolute;
- width: 12px;
- height: 4px;
- right: 0;
- bottom: 0;
- background-color: $themeColor;
- }
- }
- 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>
|