11.2. Переход дашборда на SVG
Переносим все канвасы fabric на https://svgjs.dev/docs/3.0/
Ветку уже создал 50-11-2-dashboard-to-svg Вроде их только 4
Ниже TimelineCalendarGraph.vue выхлоп от chatgpt который может использовать что-то несуществующее или неправильно использовать, нужно привести в порядок
<template>
<div ref="canvasWrapper" class="canvas">
<svg ref="canvas" width="100%" height="800px"></svg>
</div>
</template>
<script>
import { SVG } from '@svgdotjs/svg.js';
import throttle from 'lodash/throttle';
import moment from 'moment';
import { formatDurationString } from '@/utils/time';
const headerHeight = 20;
const columns = 7;
const rowHeight = 120;
export default {
name: 'TimelineCalendarGraph',
props: {
start: {
type: String,
required: true,
},
end: {
type: String,
required: true,
},
timePerDay: {
type: Object,
required: true,
},
},
mounted() {
this.draw();
window.addEventListener('resize', this.onResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize);
},
methods: {
formatDuration: formatDurationString,
draw: throttle(function () {
const width = this.$refs.canvasWrapper.offsetWidth;
const columnWidth = width / 7;
const startOfMonth = moment(this.start, 'YYYY-MM-DD').startOf('month');
const endOfMonth = moment(this.start, 'YYYY-MM-DD').endOf('month');
const firstDay = startOfMonth.clone().startOf('isoWeek');
const lastDay = endOfMonth.clone().endOf('isoWeek');
const rows = lastDay.diff(firstDay, 'weeks') + 1;
// Clear canvas
// this.$refs.canvas.innerHTML = '';
SVG(this.$refs.canvas).clear();
// Background
SVG(this.$refs.canvas)
.rect(width - 1, rows * rowHeight - 1)
.move(0, headerHeight)
.radius(20)
.fill('#FAFAFA')
.stroke({ color: '#DFE5ED', width: 1 });
// Column headers
for (let column = 0; column < columns; column++) {
const date = firstDay.clone().locale(this.$i18n.locale).add(column, 'days');
SVG(this.$refs.canvas)
.text(date.format('dddd').toUpperCase())
.move(column * columnWidth, 0)
.width(columnWidth)
.height(headerHeight)
.font({
family: 'Nunito, sans-serif',
size: 10,
weight: 600,
fill: '#2E2EF9',
})
}
const { timePerDay } = this;
for (let row = 0; row < rows; row++) {
for (let column = 0; column < columns; column++) {
const cellLeft = column * columnWidth;
const cellTop = headerHeight + row * rowHeight;
const date = firstDay.clone().add(row * columns + column, 'days');
const isInSelection = date.diff(this.start) >= 0 && date.diff(this.end) <= 0;
const isInSelectedMonth = date.diff(startOfMonth) >= 0 && date.diff(endOfMonth) <= 0;
// Selected cell background
if (isInSelection) {
SVG(this.$refs.canvas)
.rect(columnWidth - 2, rowHeight - 2)
.move(cellLeft + 1, cellTop + 1)
.fill('#F4F4FF');
}
// Date label
SVG(this.$refs.canvas)
.text(date.format('D'))
.move(cellLeft, cellTop + 10)
.width(columnWidth - 13)
.height(rowHeight)
.font({
family: 'Nunito, sans-serif',
size: 15,
weight: isInSelection ? 600 : 400,
fill: isInSelection ? '#2E2EF9' : isInSelectedMonth ? '#59566E' : '#B1B1BE',
});
// Worked time label
const dateKey = date.format('YYYY-MM-DD');
if (timePerDay[dateKey]) {
SVG(this.$refs.canvas)
.text(this.formatDuration(timePerDay[dateKey]))
.move(cellLeft + 13, cellTop + rowHeight - 30)
.width(columnWidth)
.height(rowHeight)
.font({
family: 'Nunito, sans-serif',
size: 15,
weight: isInSelection ? 600 : 400,
fill: '#59566E',
});
}
// Selected cell bottom border
if (isInSelection) {
SVG(this.$refs.canvas)
.line(0, 0, columnWidth, 0)
.move(columnWidth * column, cellTop + rowHeight - 3)
.stroke({ color: '#2E2EF9', width: 3 });
}
}
}
// Horizontal grid lines
for (let row = 1; row < rows; row++) {
SVG(this.$refs.canvas)
.line(0, 0, width, 0)
.move(0, rowHeight * row + headerHeight)
.stroke({ color: '#DFE5ED', width: 1 });
}
// Vertical grid lines
for (let column = 1; column < columns; column++) {
SVG(this.$refs.canvas)
.line(0, 0, 0, rowHeight * rows)
.move(columnWidth * column, 20)
.stroke({ color: '#DFE5ED', width: 1 });
}
}),
onResize: throttle(function () {
this.draw();
}, 0),
},
watch: {
start() {
this.onResize();
},
end() {
this.onResize();
},
timePerDay() {
this.draw();
},
},
};
</script>
<style lang="scss" scoped>
.canvas svg {
box-sizing: content-box;
}
</style>
GPT-4
<template>
<div ref="svgWrapper" class="svg-container"></div>
</template>
<script>
import { SVG } from '@svgdotjs/svg.js';
import moment from 'moment';
import { formatDurationString } from '@/utils/time';
const headerHeight = 20;
const columns = 7;
const rowHeight = 120;
export default {
name: 'TimelineCalendarGraph',
props: {
start: {
type: String,
required: true,
},
end: {
type: String,
required: true,
},
timePerDay: {
type: Object,
required: true,
},
},
mounted() {
this.drawSVG();
window.addEventListener('resize', this.drawSVG);
},
beforeDestroy() {
window.removeEventListener('resize', this.drawSVG);
},
methods: {
formatDuration: formatDurationString,
drawSVG() {
if (!this.$refs.svgWrapper) {
return;
}
// Clear existing SVG to redraw
this.$refs.svgWrapper.innerHTML = '';
const draw = SVG().addTo(this.$refs.svgWrapper).size('100%', '100%');
const width = this.$refs.svgWrapper.clientWidth;
const columnWidth = width / columns;
const startOfMonth = moment(this.start, 'YYYY-MM-DD').startOf('month');
const endOfMonth = moment(this.start, 'YYYY-MM-DD').endOf('month');
const firstDay = startOfMonth.clone().startOf('isoWeek');
const lastDay = endOfMonth.clone().endOf('isoWeek');
const rows = lastDay.diff(firstDay, 'weeks') + 1;
const height = headerHeight + rowHeight * rows;
// Set the height dynamically based on content
draw.size(width, height);
// Background
draw.rect(width, rows * rowHeight).attr({
fill: '#FAFAFA',
stroke: '#DFE5ED',
'stroke-width': 1,
x: 0,
y: headerHeight,
rx: 20,
ry: 20,
});
// Column headers and cells
for (let row = 0; row < rows; row++) {
for (let column = 0; column < columns; column++) {
const cellLeft = column * columnWidth;
const cellTop = headerHeight + row * rowHeight;
const date = firstDay.clone().add(row * columns + column, 'days');
const isInSelection = date.diff(this.start) >= 0 && date.diff(this.end) <= 0;
const isInSelectedMonth = date.diff(startOfMonth) >= 0 && date.diff(endOfMonth) <= 0;
// Selected cell background
if (isInSelection) {
draw.rect(columnWidth - 2, rowHeight - 2).attr({
fill: '#F4F4FF',
x: cellLeft + 1,
y: cellTop + 1,
});
}
// Date label
draw.text(date.format('D'))
.attr({
x: cellLeft,
y: cellTop + 10,
fontSize: 15,
fontWeight: isInSelection ? 600 : 400,
fill: isInSelection ? '#2E2EF9' : isInSelectedMonth ? '#59566E' : '#B1B1BE',
})
.dx(columnWidth - 13);
// Worked time label
const dateKey = date.format('YYYY-MM-DD');
if (this.timePerDay[dateKey]) {
draw.text(this.formatDuration(this.timePerDay[dateKey])).attr({
x: cellLeft + 13,
y: cellTop + rowHeight - 30,
fontSize: 15,
fontWeight: isInSelection ? 600 : 400,
fill: '#59566E',
});
}
}
}
// Grid lines
for (let row = 1; row < rows; row++) {
draw.line(0, rowHeight * row + headerHeight, width, rowHeight * row + headerHeight).stroke({
color: '#DFE5ED',
width: 1,
});
}
for (let column = 1; column < columns; column++) {
draw.line(columnWidth * column, headerHeight, columnWidth * column, height).stroke({
color: '#DFE5ED',
width: 1,
});
}
},
},
watch: {
start: 'drawSVG',
end: 'drawSVG',
timePerDay: 'drawSVG',
},
};
</script>
<style scoped>
.svg-container {
width: 100%;
/* Adjust the height as necessary or make it dynamic */
height: auto;
box-sizing: border-box;
}
</style>
Edited by Danil Shapilov