main
vue 299 lines 6.23 KB
Raw
1 <template>
2 <n-card content-class="p-0!" :hoverable="clickable" :class="{ 'group cursor-pointer': clickable }" size="small">
3 <div
4 class="card-header border-border flex items-center justify-between gap-4 overflow-hidden border-b px-4 py-3"
5 :class="{ 'group-hover:text-primary transition-colors': clickable }"
6 >
7 <div class="truncate text-base">{{ title }}</div>
8 <Icon v-if="clickable" :name="icon || 'carbon:arrow-up-right'" />
9 </div>
10
11 <div class="card-content flex flex-col gap-3">
12 <div v-if="barValues.length" class="bars flex">
13 <div
14 v-for="item of barValues"
15 :key="JSON.stringify(item)"
16 :class="item.status"
17 class="bar"
18 :style="{ width: `${item.percentage}%` }"
19 >
20 <div class="fill"></div>
21 </div>
22 </div>
23 <div class="list flex flex-col">
24 <div
25 v-for="item of listValues"
26 :key="JSON.stringify(item)"
27 class="item group/item flex items-center gap-3"
28 :class="[item.status]"
29 >
30 <div
31 class="label flex items-center gap-2 truncate"
32 :class="{ 'cursor-pointer': selectable }"
33 @click="emit('select', item)"
34 >
35 <span class="badge"></span>
36 <span
37 class="truncate font-mono"
38 :class="{ 'group-hover/item:text-primary transition-colors': selectable }"
39 >
40 {{ item.label }}
41 </span>
42 <Icon
43 v-if="selectable"
44 :name="icon || 'carbon:arrow-up-right'"
45 class="group-hover/item:text-primary transition-colors"
46 />
47 </div>
48 <div class="divider grow"></div>
49 <div class="value flex gap-3 font-mono whitespace-nowrap">
50 <span v-if="!item.isTotal" class="opacity-50">{{ item.percentage }}%</span>
51 <strong>{{ item.value }}</strong>
52 </div>
53 </div>
54 <div v-if="!listValues.length" class="item group/item muted flex items-center gap-3">
55 <div class="label flex items-center gap-2 truncate">
56 <span class="badge"></span>
57 <span class="truncate font-mono">No data</span>
58 </div>
59 <div class="divider grow"></div>
60 </div>
61 </div>
62 </div>
63 </n-card>
64 </template>
65
66 <script setup lang="ts">
67 import _round from "lodash/round"
68 import { NCard } from "naive-ui"
69 import { computed } from "vue"
70 import Icon from "@/components/common/Icon.vue"
71
72 export interface ItemProps {
73 value: number
74 label: string
75 isTotal?: boolean
76 status?: "success" | "warning" | "error" | "muted" | "primary" | "default" | "info"
77 }
78
79 interface ItemPropsExt extends ItemProps {
80 percentage: number
81 }
82
83 const {
84 title,
85 values,
86 showTotal = true,
87 showZeroItems,
88 clickable,
89 selectable,
90 icon
91 } = defineProps<{
92 title: string
93 values: ItemProps[]
94 icon?: string
95 showTotal?: boolean
96 showZeroItems?: boolean
97 clickable?: boolean
98 selectable?: boolean
99 }>()
100
101 const emit = defineEmits<{
102 select: [item: ItemProps]
103 }>()
104
105 const totItem = computed(
106 () =>
107 values.find(item => item.isTotal) || {
108 value: values.reduce((acc, cur) => {
109 return acc + cur.value
110 }, 0),
111 isTotal: true,
112 label: "Total"
113 }
114 )
115
116 const sanitizedValues = computed<ItemPropsExt[]>(() => {
117 const list: ItemPropsExt[] = values
118 .filter(o => !o.isTotal)
119 .map(o => ({ ...o, percentage: _round((o.value / totItem.value.value) * 100, 2) }))
120 .filter(o => o.value || (!o.value && showZeroItems))
121
122 return [{ ...totItem.value, percentage: 100 }, ...list]
123 })
124
125 const listValues = computed<ItemPropsExt[]>(() =>
126 sanitizedValues.value.filter(o => !o.isTotal || (showTotal && o.isTotal))
127 )
128
129 const barValues = computed<ItemPropsExt[]>(() => sanitizedValues.value.filter(o => !o.isTotal && o.percentage))
130 </script>
131
132 <style scoped lang="scss">
133 @use "sass:list";
134
135 .n-card {
136 overflow: hidden;
137
138 .card-content {
139 padding: 12px 16px;
140
141 .bars {
142 height: 10px;
143
144 .bar {
145 padding: 0 2px;
146
147 &:first-child {
148 padding-left: 0;
149 }
150 &:last-child {
151 padding-right: 0;
152 }
153
154 .fill {
155 border-radius: var(--border-radius-small);
156 background-color: var(--fg-default-color);
157 height: 100%;
158 width: 100%;
159 }
160
161 @for $i from 2 through 10 {
162 $opacities: (1, 0.8, 0.6, 0.4, 0.2);
163 $idx: (($i - 1) % 5) + 1;
164 &:nth-child(#{$i}) {
165 .fill {
166 opacity: list.nth($opacities, $idx);
167 }
168 }
169 }
170
171 &.success {
172 .fill {
173 background-color: var(--success-color);
174 opacity: 1;
175 }
176 }
177 &.warning {
178 .fill {
179 background-color: var(--warning-color);
180 opacity: 1;
181 }
182 }
183 &.error {
184 .fill {
185 background-color: var(--error-color);
186 opacity: 1;
187 }
188 }
189 &.muted {
190 .fill {
191 background-color: var(--fg-secondary-color);
192 opacity: 1;
193 }
194 }
195 &.info {
196 .fill {
197 background-color: var(--info-color);
198 opacity: 1;
199 }
200 }
201 &.primary {
202 .fill {
203 background-color: var(--primary-color);
204 opacity: 1;
205 }
206 }
207 &.default {
208 .fill {
209 background-color: var(--fg-default-color);
210 opacity: 1;
211 }
212 }
213 }
214 }
215
216 .list {
217 font-size: 13px;
218 gap: 4px;
219
220 .item {
221 line-height: 1;
222 padding: 3px 4px;
223
224 .badge {
225 height: 10px;
226 width: 10px;
227 min-width: 10px;
228 border-radius: var(--border-radius-small);
229 background-color: var(--fg-default-color);
230 }
231
232 .divider {
233 height: 1px;
234 background-color: rgba(var(--border-color-rgb) / 0.7);
235 }
236
237 @for $i from 2 through 10 {
238 $opacities: (1, 0.8, 0.6, 0.4, 0.2);
239 $idx: (($i - 1) % 5) + 1;
240 &:nth-child(#{$i}) {
241 .badge {
242 opacity: list.nth($opacities, $idx);
243 }
244 }
245 }
246
247 &.success {
248 .badge {
249 background-color: var(--success-color);
250 opacity: 1;
251 }
252 }
253 &.warning {
254 .badge {
255 background-color: var(--warning-color);
256 opacity: 1;
257 }
258 }
259 &.error {
260 .badge {
261 background-color: var(--error-color);
262 opacity: 1;
263 }
264 }
265 &.muted {
266 .badge {
267 background-color: var(--fg-secondary-color);
268 opacity: 1;
269 }
270 }
271 &.info {
272 .badge {
273 background-color: var(--info-color);
274 opacity: 1;
275 }
276 }
277 &.default {
278 .badge {
279 background-color: var(--fg-default-color);
280 opacity: 1;
281 }
282 }
283 &.primary {
284 .badge {
285 background-color: var(--primary-color);
286 opacity: 1;
287 }
288 }
289 }
290 }
291 }
292
293 &.hovered {
294 &:hover {
295 border-color: var(--primary-color);
296 }
297 }
298 }
299 </style>