Initial commit
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
<script setup>
|
||||
import { useDisplay, useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
donutColors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 75
|
||||
}
|
||||
})
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
const display = useDisplay()
|
||||
const cardRef = ref(null)
|
||||
const cardWidth = ref(0)
|
||||
|
||||
const updateCardWidth = () => {
|
||||
if (cardRef.value) {
|
||||
cardWidth.value = cardRef.value.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCardWidth()
|
||||
window.addEventListener('resize', updateCardWidth)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateCardWidth)
|
||||
})
|
||||
|
||||
const chartSeries = computed(() => [props.progress, 100 - props.progress])
|
||||
|
||||
const processColors = (colors) => {
|
||||
const currentTheme = vuetifyTheme.current.value.colors
|
||||
return colors.map(color => {
|
||||
if (typeof color === 'string' && currentTheme[color]) {
|
||||
return currentTheme[color]
|
||||
}
|
||||
return color
|
||||
})
|
||||
}
|
||||
|
||||
const chartSize = computed(() => {
|
||||
const width = cardWidth.value || 300
|
||||
|
||||
let size
|
||||
if (width < 250) {
|
||||
size = Math.max(120, width * 0.5)
|
||||
} else if (width < 350) {
|
||||
size = Math.max(150, width * 0.5)
|
||||
} else if (width < 450) {
|
||||
size = Math.max(180, width * 0.45)
|
||||
} else if (width < 600) {
|
||||
size = Math.max(200, width * 0.4)
|
||||
} else if (width < 800) {
|
||||
size = Math.max(240, width * 0.35)
|
||||
} else {
|
||||
size = Math.min(300, width * 0.32)
|
||||
}
|
||||
|
||||
if (display.xs.value) size = Math.max(size, 120)
|
||||
if (display.sm.value) size = Math.max(size, 160)
|
||||
if (display.md.value) size = Math.max(size, 200)
|
||||
|
||||
return { width: size, height: size }
|
||||
})
|
||||
|
||||
const textSizes = computed(() => {
|
||||
const width = cardWidth.value || 300
|
||||
|
||||
if (width < 250 || display.xs.value) {
|
||||
return {
|
||||
title: 'text-body-2',
|
||||
subtitle: 'text-caption',
|
||||
mainNumber: 'text-h6',
|
||||
percentage: 'text-body-2'
|
||||
}
|
||||
} else if (width < 350 || display.sm.value) {
|
||||
return {
|
||||
title: 'text-body-1',
|
||||
subtitle: 'text-body-2',
|
||||
mainNumber: 'text-h5',
|
||||
percentage: 'text-body-2'
|
||||
}
|
||||
} else if (width < 450) {
|
||||
return {
|
||||
title: 'text-h6',
|
||||
subtitle: 'text-body-2',
|
||||
mainNumber: 'text-h4',
|
||||
percentage: 'text-body-1'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: display.md.value ? 'text-h5' : 'text-h4',
|
||||
subtitle: 'text-body-1',
|
||||
mainNumber: display.lg.value ? 'text-h2' : 'text-h3',
|
||||
percentage: 'text-body-1'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
const currentTheme = vuetifyTheme.current.value.colors
|
||||
const variableTheme = vuetifyTheme.current.value.variables
|
||||
const width = cardWidth.value || 300
|
||||
|
||||
const defaultDonutColors = [
|
||||
currentTheme.success,
|
||||
]
|
||||
|
||||
const usedDonutColors = props.donutColors.length
|
||||
? processColors(props.donutColors)
|
||||
: defaultDonutColors
|
||||
|
||||
const headingColor = `rgba(${hexToRgb(currentTheme['on-background'])},${variableTheme['high-emphasis-opacity']})`
|
||||
const backgroundCircleColor = `rgba(${hexToRgb(usedDonutColors[0])}, 0.15)`
|
||||
|
||||
const chartSizeValue = chartSize.value.width
|
||||
const valueFontSize = width < 250 ? '1.1rem' :
|
||||
width < 350 ? '1.3rem' :
|
||||
width < 450 ? '1.5rem' :
|
||||
chartSizeValue < 160 ? '1.3rem' :
|
||||
chartSizeValue < 200 ? '1.6rem' :
|
||||
chartSizeValue < 250 ? '1.8rem' : '2rem'
|
||||
|
||||
const labelFontSize = width < 250 ? '0.8rem' :
|
||||
width < 350 ? '0.9rem' :
|
||||
chartSizeValue < 160 ? '0.9rem' :
|
||||
chartSizeValue < 200 ? '1rem' : '1.1rem'
|
||||
|
||||
const donutSize = width < 300 ? '65%' :
|
||||
width < 400 ? '70%' :
|
||||
chartSizeValue < 180 ? '70%' :
|
||||
chartSizeValue < 220 ? '75%' : '80%'
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
type: 'donut',
|
||||
sparkline: {
|
||||
enabled: false
|
||||
},
|
||||
},
|
||||
labels: ['Progress', 'Remaining'],
|
||||
colors: [...usedDonutColors, backgroundCircleColor],
|
||||
stroke: {
|
||||
width: 0
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: 'dark',
|
||||
style: {
|
||||
fontSize: width < 300 ? '12px' : '14px'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
},
|
||||
},
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
startAngle: -90,
|
||||
endAngle: 270,
|
||||
donut: {
|
||||
size: donutSize,
|
||||
background: 'transparent',
|
||||
labels: {
|
||||
show: true,
|
||||
value: {
|
||||
show: true,
|
||||
fontSize: valueFontSize,
|
||||
fontFamily: 'Public Sans',
|
||||
color: headingColor,
|
||||
fontWeight: 600,
|
||||
offsetY: chartSizeValue < 140 ? -2 : 0,
|
||||
formatter(val) {
|
||||
return `${Math.round(val)}%`
|
||||
},
|
||||
},
|
||||
name: {
|
||||
show: false
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
showAlways: true,
|
||||
color: usedDonutColors[0],
|
||||
fontSize: labelFontSize,
|
||||
label: 'Progress',
|
||||
fontFamily: 'Public Sans',
|
||||
fontWeight: 500,
|
||||
formatter() {
|
||||
return `${props.progress}%`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 280,
|
||||
options: {
|
||||
chart: {
|
||||
width: 120,
|
||||
height: 120
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '65%',
|
||||
labels: {
|
||||
value: {
|
||||
fontSize: '1.1rem',
|
||||
offsetY: -2
|
||||
},
|
||||
total: {
|
||||
fontSize: '0.8rem'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 400,
|
||||
options: {
|
||||
chart: {
|
||||
width: 150,
|
||||
height: 150
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '70%',
|
||||
labels: {
|
||||
value: {
|
||||
fontSize: '1.3rem',
|
||||
offsetY: -1
|
||||
},
|
||||
total: {
|
||||
fontSize: '0.9rem'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
chart: {
|
||||
width: 180,
|
||||
height: 180
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '75%',
|
||||
labels: {
|
||||
value: {
|
||||
fontSize: '1.5rem'
|
||||
},
|
||||
total: {
|
||||
fontSize: '1rem'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 1024,
|
||||
options: {
|
||||
chart: {
|
||||
width: 240,
|
||||
height: 240
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '80%',
|
||||
labels: {
|
||||
value: {
|
||||
fontSize: '1.8rem'
|
||||
},
|
||||
total: {
|
||||
fontSize: '1.1rem'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const cardBackgroundStyle = computed(() => {
|
||||
const defaultDonutColors = [
|
||||
vuetifyTheme.current.value.colors.success,
|
||||
]
|
||||
|
||||
const colors = props.donutColors.length ? processColors(props.donutColors) : defaultDonutColors
|
||||
|
||||
const createGradientColor = (color, opacity = 0.08) => {
|
||||
if (color.includes('rgba')) {
|
||||
return color.replace(/[\d\.]+\)$/g, `${opacity})`)
|
||||
}
|
||||
if (color.startsWith('#')) {
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
if (color.includes('rgb')) {
|
||||
return color.replace('rgb', 'rgba').replace(')', `, ${opacity})`)
|
||||
}
|
||||
return `rgba(${hexToRgb(color)}, ${opacity})`
|
||||
}
|
||||
|
||||
const gradientColors = colors.map((color, index) =>
|
||||
createGradientColor(color, index === 0 ? 0.12 : 0.04)
|
||||
)
|
||||
|
||||
return {
|
||||
background: `linear-gradient(135deg,
|
||||
${gradientColors[0]} 0%,
|
||||
${gradientColors[1] || gradientColors[0]} 50%,
|
||||
${gradientColors[2] || gradientColors[0]} 100%)`
|
||||
}
|
||||
})
|
||||
|
||||
const layoutClasses = computed(() => {
|
||||
const width = cardWidth.value || 300
|
||||
|
||||
return {
|
||||
cardText: width < 250 ? 'pa-2' :
|
||||
width < 350 ? 'pa-3' :
|
||||
width < 500 ? 'pa-4' : 'pa-5',
|
||||
textSection: width < 250 ? 'me-2' :
|
||||
width < 350 ? 'me-3' :
|
||||
width < 500 ? 'me-4' : 'me-5',
|
||||
chartSection: 'flex-shrink-0',
|
||||
iconSize: width < 250 ? 12 :
|
||||
width < 350 ? 14 :
|
||||
width < 500 ? 16 : 18,
|
||||
spacing: width < 300 ? 'mb-2' :
|
||||
width < 450 ? 'mb-3' : 'mb-4'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
ref="cardRef"
|
||||
class="overflow-visible"
|
||||
:style="cardBackgroundStyle"
|
||||
>
|
||||
<VCardText
|
||||
class="d-flex align-center justify-space-between"
|
||||
:class="layoutClasses.cardText"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-column flex-grow-1"
|
||||
:class="layoutClasses.textSection"
|
||||
>
|
||||
<div :class="layoutClasses.spacing">
|
||||
<h5
|
||||
class="text-no-wrap font-weight-medium"
|
||||
:class="textSizes.title"
|
||||
style="line-height: 1.2;"
|
||||
>
|
||||
Generated Leads
|
||||
</h5>
|
||||
<div
|
||||
class="text-medium-emphasis"
|
||||
:class="textSizes.subtitle"
|
||||
style="line-height: 1.1;"
|
||||
>
|
||||
Monthly Report
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
class="font-weight-bold"
|
||||
:class="[textSizes.mainNumber, layoutClasses.spacing]"
|
||||
style="line-height: 1.1;"
|
||||
>
|
||||
4,350
|
||||
</h3>
|
||||
<div class="d-flex align-center">
|
||||
<VIcon
|
||||
icon="tabler-chevron-up"
|
||||
:color="(props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0]"
|
||||
:size="layoutClasses.iconSize"
|
||||
class="me-1"
|
||||
/>
|
||||
<span
|
||||
class="font-weight-medium"
|
||||
:class="textSizes.percentage"
|
||||
:style="{ color: (props.donutColors.length ? processColors(props.donutColors) : [vuetifyTheme.current.value.colors.success])[0] }"
|
||||
>
|
||||
15.8%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-center justify-center"
|
||||
:class="layoutClasses.chartSection"
|
||||
>
|
||||
<VueApexCharts
|
||||
:options="chartOptions"
|
||||
:series="chartSeries"
|
||||
:height="chartSize.height"
|
||||
:width="chartSize.width"
|
||||
/>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user