@drax/dashboard-vue 3.17.0 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/DashboardConfig/DashboardCardEditor.vue +1 -1
- package/src/components/DashboardView/DashboardView.vue +16 -2
- package/src/components/GroupByCard/GroupByCard.vue +12 -0
- package/src/components/GroupByCard/renders/GroupByLinesRender.vue +324 -0
- package/src/components/GroupByCard/renders/GroupByPieRender.vue +12 -2
- package/src/components/GroupByCard/renders/GroupByTableRender.vue +62 -40
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.18.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"vue-tsc": "^3.2.4",
|
|
47
47
|
"vuetify": "^3.11.8"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "c7709f3912ba1f8ae6a7e78376e17d50b32cfa7d"
|
|
50
50
|
}
|
|
@@ -314,7 +314,7 @@ const {
|
|
|
314
314
|
clearable class="mb-3"></v-select>
|
|
315
315
|
</v-col>
|
|
316
316
|
<v-col cols="12" md="6">
|
|
317
|
-
<v-select v-model="form.groupBy!.render" :items="['pie', 'bars', 'table', 'gallery']"
|
|
317
|
+
<v-select v-model="form.groupBy!.render" :items="['pie', 'bars', 'lines', 'table', 'gallery']"
|
|
318
318
|
label="Render visual" variant="outlined" density="compact" hide-details="auto"
|
|
319
319
|
class="mb-3"></v-select>
|
|
320
320
|
</v-col>
|
|
@@ -21,9 +21,14 @@ const {dashboard} = defineProps({
|
|
|
21
21
|
:md="card?.layout?.md || 12"
|
|
22
22
|
:lg="card?.layout?.lg || 12"
|
|
23
23
|
>
|
|
24
|
-
<v-card
|
|
24
|
+
<v-card
|
|
25
|
+
class="dashboard-card"
|
|
26
|
+
:variant="card?.layout?.cardVariant || 'elevated' "
|
|
27
|
+
hover
|
|
28
|
+
:height="card?.layout?.height || 300"
|
|
29
|
+
>
|
|
25
30
|
<v-card-title>{{card?.title}}</v-card-title>
|
|
26
|
-
<v-card-text >
|
|
31
|
+
<v-card-text class="dashboard-card__content">
|
|
27
32
|
<paginate-card v-if="card?.type === 'paginate'" :card="card" />
|
|
28
33
|
<group-by-card v-else-if="card?.type === 'groupBy'" :card="card" />
|
|
29
34
|
</v-card-text>
|
|
@@ -36,5 +41,14 @@ const {dashboard} = defineProps({
|
|
|
36
41
|
</template>
|
|
37
42
|
|
|
38
43
|
<style scoped>
|
|
44
|
+
.dashboard-card {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
}
|
|
39
48
|
|
|
49
|
+
.dashboard-card__content {
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-height: 0;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
40
54
|
</style>
|
|
@@ -6,6 +6,7 @@ import GroupByTableRender from "./renders/GroupByTableRender.vue";
|
|
|
6
6
|
import GroupByPieRender from "./renders/GroupByPieRender.vue";
|
|
7
7
|
import GroupByBarsRender from "./renders/GroupByBarsRender.vue";
|
|
8
8
|
import GroupByGalleryRender from "./renders/GroupByGalleryRender.vue";
|
|
9
|
+
import GroupByLinesRender from "./renders/GroupByLinesRender.vue";
|
|
9
10
|
import {ref, onMounted} from "vue";
|
|
10
11
|
|
|
11
12
|
|
|
@@ -38,18 +39,21 @@ watch(() => card, async () => {
|
|
|
38
39
|
:data="data"
|
|
39
40
|
:headers="groupByHeaders"
|
|
40
41
|
:fields="cardEntityFields"
|
|
42
|
+
:date-format="card?.groupBy?.dateFormat"
|
|
41
43
|
/>
|
|
42
44
|
|
|
43
45
|
<group-by-pie-render v-else-if="card?.groupBy?.render === 'pie'"
|
|
44
46
|
:data="data"
|
|
45
47
|
:headers="groupByHeaders"
|
|
46
48
|
:fields="cardEntityFields"
|
|
49
|
+
:date-format="card?.groupBy?.dateFormat"
|
|
47
50
|
/>
|
|
48
51
|
|
|
49
52
|
<group-by-bars-render v-else-if="card?.groupBy?.render === 'bars'"
|
|
50
53
|
:data="data"
|
|
51
54
|
:headers="groupByHeaders"
|
|
52
55
|
:fields="cardEntityFields"
|
|
56
|
+
:date-format="card?.groupBy?.dateFormat"
|
|
53
57
|
:show-legend="false"
|
|
54
58
|
/>
|
|
55
59
|
|
|
@@ -57,9 +61,17 @@ watch(() => card, async () => {
|
|
|
57
61
|
:data="data"
|
|
58
62
|
:headers="groupByHeaders"
|
|
59
63
|
:fields="cardEntityFields"
|
|
64
|
+
:date-format="card?.groupBy?.dateFormat"
|
|
60
65
|
:show-legend="false"
|
|
61
66
|
/>
|
|
62
67
|
|
|
68
|
+
<group-by-lines-render v-else-if="card?.groupBy?.render === 'lines'"
|
|
69
|
+
:data="data"
|
|
70
|
+
:headers="groupByHeaders"
|
|
71
|
+
:fields="cardEntityFields"
|
|
72
|
+
:date-format="card?.groupBy?.dateFormat"
|
|
73
|
+
/>
|
|
74
|
+
|
|
63
75
|
</template>
|
|
64
76
|
|
|
65
77
|
<style scoped>
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import {computed, ref, onMounted, onUnmounted, watch} from "vue";
|
|
4
|
+
import {useDateFormat} from "@drax/common-vue"
|
|
5
|
+
import type {IDraxDateFormatUnit} from "@drax/common-share";
|
|
6
|
+
import type {IEntityCrudField} from "@drax/crud-share";
|
|
7
|
+
|
|
8
|
+
const {formatDateByUnit} = useDateFormat()
|
|
9
|
+
|
|
10
|
+
const {data, fields, dateFormat} = defineProps({
|
|
11
|
+
data: {type: Array as PropType<any[]>, required: false},
|
|
12
|
+
headers: {type: Array as PropType<any[]>, required: false},
|
|
13
|
+
fields: {type: Array as PropType<IEntityCrudField[]>, required: false},
|
|
14
|
+
dateFormat: {type: String as PropType<IDraxDateFormatUnit>, required: false, default: 'day'},
|
|
15
|
+
showLegend: {type: Boolean, default: true},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
19
|
+
|
|
20
|
+
const colors = [
|
|
21
|
+
'#2563eb', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
|
|
22
|
+
'#06b6d4', '#ec4899', '#84cc16', '#f97316', '#14b8a6'
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const isNumericValue = (value: unknown) => typeof value === 'number' && Number.isFinite(value)
|
|
26
|
+
|
|
27
|
+
const formatFieldValue = (fieldName: string, value: any) => {
|
|
28
|
+
const field = fields?.find((item) => item.name === fieldName)
|
|
29
|
+
|
|
30
|
+
if (value === null || value === undefined) return 'N/A'
|
|
31
|
+
|
|
32
|
+
if (field?.type === 'date') {
|
|
33
|
+
return formatDateByUnit(value, dateFormat)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (['ref', 'object'].includes(field?.type || '') && field?.refDisplay && value) {
|
|
37
|
+
return value[field.refDisplay] || 'N/A'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
41
|
+
return JSON.stringify(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return String(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const xField = computed(() => {
|
|
48
|
+
if (!data?.length) return null
|
|
49
|
+
|
|
50
|
+
const sample = data.find((item) => item && typeof item === 'object')
|
|
51
|
+
if (!sample) return null
|
|
52
|
+
|
|
53
|
+
return Object.keys(sample).find((key) => key !== 'count' && !isNumericValue(sample[key])) || null
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const xFieldConfig = computed(() => {
|
|
57
|
+
if (!xField.value) return null
|
|
58
|
+
return fields?.find((field) => field.name === xField.value) || null
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const seriesKeys = computed(() => {
|
|
62
|
+
if (!data?.length) return ['count']
|
|
63
|
+
|
|
64
|
+
const sample = data.find((item) => item && typeof item === 'object')
|
|
65
|
+
if (!sample) return ['count']
|
|
66
|
+
|
|
67
|
+
const keys = Object.keys(sample).filter((key) => key !== xField.value && isNumericValue(sample[key]))
|
|
68
|
+
return keys.length > 0 ? keys : ['count']
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const seriesMeta = computed(() => {
|
|
72
|
+
return seriesKeys.value.map((key, index) => {
|
|
73
|
+
const field = fields?.find((item) => item.name === key)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
key,
|
|
77
|
+
label: field?.label || key,
|
|
78
|
+
color: colors[index % colors.length]
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const buildItemLabel = (item: Record<string, any>) => {
|
|
84
|
+
const labelParts: string[] = []
|
|
85
|
+
|
|
86
|
+
Object.keys(item).forEach((key) => {
|
|
87
|
+
if (seriesKeys.value.includes(key)) return
|
|
88
|
+
|
|
89
|
+
const formattedValue = formatFieldValue(key, item[key])
|
|
90
|
+
|
|
91
|
+
if (formattedValue && formattedValue !== 'N/A') {
|
|
92
|
+
labelParts.push(formattedValue)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return labelParts.length > 0 ? labelParts.join(' - ') : 'N/A'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chartData = computed(() => {
|
|
100
|
+
if (!data?.length || !xField.value) return []
|
|
101
|
+
|
|
102
|
+
const normalized = data.map((item, index) => {
|
|
103
|
+
const rawX = item?.[xField.value as string]
|
|
104
|
+
const xLabel = formatFieldValue(xField.value as string, rawX)
|
|
105
|
+
const fullLabel = buildItemLabel(item)
|
|
106
|
+
const xValue = xFieldConfig.value?.type === 'date' && rawX ? new Date(rawX).getTime() : index
|
|
107
|
+
|
|
108
|
+
const seriesValues = Object.fromEntries(
|
|
109
|
+
seriesMeta.value.map((series) => [series.key, Number(item?.[series.key] || 0)])
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
rawX,
|
|
114
|
+
xLabel,
|
|
115
|
+
fullLabel,
|
|
116
|
+
xValue,
|
|
117
|
+
seriesValues
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return normalized.sort((left, right) => {
|
|
122
|
+
if (xFieldConfig.value?.type === 'date') {
|
|
123
|
+
return left.xValue - right.xValue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return left.xLabel.localeCompare(right.xLabel, undefined, {numeric: true, sensitivity: 'base'})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const maxValue = computed(() => {
|
|
131
|
+
if (!chartData.value.length) return 0
|
|
132
|
+
|
|
133
|
+
const values = chartData.value.flatMap((item) => seriesMeta.value.map((series) => item.seriesValues[series.key] || 0))
|
|
134
|
+
return Math.max(...values, 0)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const yAxisTicks = computed(() => {
|
|
138
|
+
const steps = 5
|
|
139
|
+
const currentMax = maxValue.value
|
|
140
|
+
|
|
141
|
+
if (currentMax <= 0) {
|
|
142
|
+
return [0, 1, 2, 3, 4, 5]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Array.from({length: steps + 1}, (_, index) => {
|
|
146
|
+
return (currentMax / steps) * index
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const drawLineChart = () => {
|
|
151
|
+
if (!canvasRef.value || !chartData.value.length || !xField.value) return
|
|
152
|
+
|
|
153
|
+
const canvas = canvasRef.value
|
|
154
|
+
const ctx = canvas.getContext('2d')
|
|
155
|
+
if (!ctx) return
|
|
156
|
+
|
|
157
|
+
const parentWidth = canvas.parentElement?.clientWidth || 640
|
|
158
|
+
const width = parentWidth
|
|
159
|
+
const height = 340
|
|
160
|
+
const padding = {top: 24, right: 24, bottom: 72, left: 56}
|
|
161
|
+
|
|
162
|
+
canvas.width = width
|
|
163
|
+
canvas.height = height
|
|
164
|
+
|
|
165
|
+
const chartWidth = width - padding.left - padding.right
|
|
166
|
+
const chartHeight = height - padding.top - padding.bottom
|
|
167
|
+
|
|
168
|
+
ctx.clearRect(0, 0, width, height)
|
|
169
|
+
ctx.textBaseline = 'middle'
|
|
170
|
+
|
|
171
|
+
yAxisTicks.value.forEach((tick) => {
|
|
172
|
+
const y = padding.top + chartHeight - ((tick / Math.max(maxValue.value, 5)) * chartHeight)
|
|
173
|
+
|
|
174
|
+
ctx.beginPath()
|
|
175
|
+
ctx.moveTo(padding.left, y)
|
|
176
|
+
ctx.lineTo(padding.left + chartWidth, y)
|
|
177
|
+
ctx.strokeStyle = '#e5e7eb'
|
|
178
|
+
ctx.lineWidth = 1
|
|
179
|
+
ctx.stroke()
|
|
180
|
+
|
|
181
|
+
ctx.fillStyle = '#6b7280'
|
|
182
|
+
ctx.font = '11px sans-serif'
|
|
183
|
+
ctx.textAlign = 'right'
|
|
184
|
+
ctx.fillText(Number.isInteger(tick) ? String(tick) : tick.toFixed(1), padding.left - 8, y)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
ctx.beginPath()
|
|
188
|
+
ctx.moveTo(padding.left, padding.top)
|
|
189
|
+
ctx.lineTo(padding.left, padding.top + chartHeight)
|
|
190
|
+
ctx.lineTo(padding.left + chartWidth, padding.top + chartHeight)
|
|
191
|
+
ctx.strokeStyle = '#9ca3af'
|
|
192
|
+
ctx.lineWidth = 1.25
|
|
193
|
+
ctx.stroke()
|
|
194
|
+
|
|
195
|
+
const getPointX = (index: number) => {
|
|
196
|
+
if (chartData.value.length === 1) return padding.left + (chartWidth / 2)
|
|
197
|
+
return padding.left + ((chartWidth / (chartData.value.length - 1)) * index)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const getPointY = (value: number) => {
|
|
201
|
+
const safeMax = Math.max(maxValue.value, 5)
|
|
202
|
+
return padding.top + chartHeight - ((value / safeMax) * chartHeight)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
seriesMeta.value.forEach((series) => {
|
|
206
|
+
ctx.beginPath()
|
|
207
|
+
|
|
208
|
+
chartData.value.forEach((item, index) => {
|
|
209
|
+
const x = getPointX(index)
|
|
210
|
+
const y = getPointY(item.seriesValues[series.key] || 0)
|
|
211
|
+
|
|
212
|
+
if (index === 0) {
|
|
213
|
+
ctx.moveTo(x, y)
|
|
214
|
+
} else {
|
|
215
|
+
ctx.lineTo(x, y)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
ctx.strokeStyle = series.color
|
|
220
|
+
ctx.lineWidth = 2.5
|
|
221
|
+
ctx.stroke()
|
|
222
|
+
|
|
223
|
+
chartData.value.forEach((item, index) => {
|
|
224
|
+
const x = getPointX(index)
|
|
225
|
+
const y = getPointY(item.seriesValues[series.key] || 0)
|
|
226
|
+
const valueLabel = String(item.seriesValues[series.key] || 0)
|
|
227
|
+
|
|
228
|
+
ctx.beginPath()
|
|
229
|
+
ctx.arc(x, y, 4, 0, 2 * Math.PI)
|
|
230
|
+
ctx.fillStyle = '#ffffff'
|
|
231
|
+
ctx.fill()
|
|
232
|
+
ctx.lineWidth = 2
|
|
233
|
+
ctx.strokeStyle = series.color
|
|
234
|
+
ctx.stroke()
|
|
235
|
+
|
|
236
|
+
ctx.fillStyle = series.color
|
|
237
|
+
ctx.font = '11px sans-serif'
|
|
238
|
+
ctx.textAlign = 'center'
|
|
239
|
+
ctx.fillText(valueLabel, x, y - 12)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
chartData.value.forEach((item, index) => {
|
|
244
|
+
const x = getPointX(index)
|
|
245
|
+
|
|
246
|
+
ctx.beginPath()
|
|
247
|
+
ctx.moveTo(x, padding.top + chartHeight)
|
|
248
|
+
ctx.lineTo(x, padding.top + chartHeight + 6)
|
|
249
|
+
ctx.strokeStyle = '#9ca3af'
|
|
250
|
+
ctx.lineWidth = 1
|
|
251
|
+
ctx.stroke()
|
|
252
|
+
|
|
253
|
+
ctx.save()
|
|
254
|
+
ctx.translate(x, padding.top + chartHeight + 18)
|
|
255
|
+
ctx.rotate(-Math.PI / 4)
|
|
256
|
+
ctx.fillStyle = '#4b5563'
|
|
257
|
+
ctx.font = '12px sans-serif'
|
|
258
|
+
ctx.textAlign = 'right'
|
|
259
|
+
const maxLabelLength = 36
|
|
260
|
+
const label = item.fullLabel.length > maxLabelLength
|
|
261
|
+
? `${item.fullLabel.slice(0, maxLabelLength)}...`
|
|
262
|
+
: item.fullLabel
|
|
263
|
+
ctx.fillText(label, 0, 0)
|
|
264
|
+
ctx.restore()
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
watch(chartData, () => {
|
|
269
|
+
setTimeout(drawLineChart, 100)
|
|
270
|
+
}, {deep: true})
|
|
271
|
+
|
|
272
|
+
onMounted(() => {
|
|
273
|
+
drawLineChart()
|
|
274
|
+
window.addEventListener('resize', drawLineChart)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
onUnmounted(() => {
|
|
278
|
+
window.removeEventListener('resize', drawLineChart)
|
|
279
|
+
})
|
|
280
|
+
</script>
|
|
281
|
+
|
|
282
|
+
<template>
|
|
283
|
+
<div class="line-chart-container">
|
|
284
|
+
<div v-if="!data || data.length === 0" class="empty-state">
|
|
285
|
+
<v-icon size="64" color="grey-lighten-1">mdi-chart-line</v-icon>
|
|
286
|
+
<p class="text-grey-lighten-1 mt-4">No hay datos para mostrar</p>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<div v-else-if="!xField" class="empty-state">
|
|
290
|
+
<v-icon size="64" color="grey-lighten-1">mdi-axis-x-arrow</v-icon>
|
|
291
|
+
<p class="text-grey-lighten-1 mt-4">Se necesita al menos un campo categórico o de fecha para el eje X</p>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<template v-else>
|
|
295
|
+
<div class="chart-wrapper">
|
|
296
|
+
<canvas ref="canvasRef"></canvas>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
</template>
|
|
300
|
+
</div>
|
|
301
|
+
</template>
|
|
302
|
+
|
|
303
|
+
<style scoped>
|
|
304
|
+
.line-chart-container {
|
|
305
|
+
width: 100%;
|
|
306
|
+
min-height: 360px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.chart-wrapper {
|
|
310
|
+
position: relative;
|
|
311
|
+
width: 100%;
|
|
312
|
+
min-height: 340px;
|
|
313
|
+
overflow-x: auto;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.empty-state {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
align-items: center;
|
|
320
|
+
justify-content: center;
|
|
321
|
+
min-height: 300px;
|
|
322
|
+
text-align: center;
|
|
323
|
+
}
|
|
324
|
+
</style>
|
|
@@ -173,13 +173,23 @@ const drawPieChart = () => {
|
|
|
173
173
|
ctx.fillStyle = segment.color
|
|
174
174
|
ctx.fill()
|
|
175
175
|
|
|
176
|
+
const textX = lineEndX + (isRightSide ? 6 : -6)
|
|
177
|
+
|
|
176
178
|
ctx.fillStyle = '#374151'
|
|
177
179
|
ctx.font = '500 12px sans-serif'
|
|
178
180
|
ctx.textAlign = isRightSide ? 'left' : 'right'
|
|
179
181
|
ctx.fillText(
|
|
180
182
|
segment.shortLabel,
|
|
181
|
-
|
|
182
|
-
lineEndY
|
|
183
|
+
textX,
|
|
184
|
+
lineEndY - 7
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
ctx.fillStyle = '#6b7280'
|
|
188
|
+
ctx.font = '11px sans-serif'
|
|
189
|
+
ctx.fillText(
|
|
190
|
+
`${segment.value}`,
|
|
191
|
+
textX,
|
|
192
|
+
lineEndY + 8
|
|
183
193
|
)
|
|
184
194
|
|
|
185
195
|
currentAngle = endAngle
|
|
@@ -29,52 +29,57 @@ const getPercentage = (count: number) => {
|
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
31
|
<template>
|
|
32
|
-
<div>
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<template
|
|
44
|
-
v-for="field in fields"
|
|
45
|
-
:key="field.name"
|
|
46
|
-
v-slot:[`item.${field.name}`]="{ value }"
|
|
32
|
+
<div class="table-render">
|
|
33
|
+
<div class="table-render__scroll">
|
|
34
|
+
<v-data-table
|
|
35
|
+
:headers="headers"
|
|
36
|
+
:items="data"
|
|
37
|
+
density="compact"
|
|
38
|
+
:items-per-page="-1"
|
|
39
|
+
hide-default-footer
|
|
40
|
+
height="100%"
|
|
41
|
+
:fixed-header="true"
|
|
42
|
+
class="table-render__table"
|
|
47
43
|
>
|
|
48
|
-
<template v-
|
|
49
|
-
{{value ? value[field.refDisplay] : '-' }}
|
|
50
|
-
</template>
|
|
44
|
+
<template v-slot:bottom></template>
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
<!-- Slot para personalizar la visualización de cada campo -->
|
|
47
|
+
<template
|
|
48
|
+
v-for="field in fields"
|
|
49
|
+
:key="field.name"
|
|
50
|
+
v-slot:[`item.${field.name}`]="{ value }"
|
|
51
|
+
>
|
|
52
|
+
<template v-if="['ref','object'].includes(field.type) && field.refDisplay">
|
|
53
|
+
{{value ? value[field.refDisplay] : '-' }}
|
|
54
|
+
</template>
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
<template v-else-if="field.type === 'date'">
|
|
57
|
+
{{ formatDateByUnit(value, dateFormat) }}
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<template v-else-if="field.type === 'number'">
|
|
61
|
+
{{ value.toLocaleString('es-ar') }}
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<template v-else>
|
|
65
|
+
{{ value }}
|
|
66
|
+
</template>
|
|
59
67
|
|
|
60
|
-
<template v-else>
|
|
61
|
-
{{ value }}
|
|
62
68
|
</template>
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</v-data-table>
|
|
70
|
+
<!-- Formato especial para el count con porcentaje -->
|
|
71
|
+
<template v-slot:item.count="{ item }">
|
|
72
|
+
<div class="d-flex align-center justify-end ga-2">
|
|
73
|
+
<v-chip color="primary" size="small" variant="flat">
|
|
74
|
+
{{ item.count }}
|
|
75
|
+
</v-chip>
|
|
76
|
+
<span class="text-caption text-grey text-left percentage-text">
|
|
77
|
+
({{ getPercentage(item.count) }}%)
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</v-data-table>
|
|
82
|
+
</div>
|
|
78
83
|
|
|
79
84
|
<!-- Fila de totales -->
|
|
80
85
|
<v-divider class="my-2"></v-divider>
|
|
@@ -89,6 +94,23 @@ const getPercentage = (count: number) => {
|
|
|
89
94
|
</template>
|
|
90
95
|
|
|
91
96
|
<style scoped>
|
|
97
|
+
.table-render {
|
|
98
|
+
height: 100%;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
min-height: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.table-render__scroll {
|
|
105
|
+
flex: 1;
|
|
106
|
+
min-height: 0;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.table-render__table {
|
|
111
|
+
height: 100%;
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
.percentage-text {
|
|
93
115
|
display: inline-block;
|
|
94
116
|
min-width: 45px;
|