main
vue 231 lines 4.65 KB
Raw
1 <template>
2 <n-card content-class="p-0!" :class="{ hovered }">
3 <div class="flex flex-col overflow-hidden">
4 <div class="card-header flex items-center justify-between gap-4">
5 <div class="title flex grow items-center gap-2">
6 <span class="truncate">{{ title }}</span>
7 <Icon v-if="hovered" :name="ArrowRightIcon" :size="12" />
8 </div>
9 <div class="icon">
10 <slot name="icon"></slot>
11 </div>
12 </div>
13 <div class="card-content flex flex-col gap-3">
14 <div class="bars flex">
15 <div
16 v-for="item of barValues"
17 :key="JSON.stringify(item)"
18 :class="item.status"
19 class="bar"
20 :style="{ width: `${item.percentage}%` }"
21 >
22 <div class="fill"></div>
23 </div>
24 </div>
25 <div class="list flex flex-col">
26 <div
27 v-for="item of listValues"
28 :key="JSON.stringify(item)"
29 class="item flex items-center gap-3"
30 :class="item.status"
31 >
32 <div class="label flex items-center gap-2 truncate">
33 <span class="badge"></span>
34 <span class="truncate font-mono">{{ item.label }}</span>
35 </div>
36 <div class="divider grow"></div>
37 <div class="value flex gap-3 font-mono whitespace-nowrap">
38 <span v-if="!item.isTotal" class="opacity-50">{{ item.percentage }}%</span>
39 <strong>{{ item.value }}</strong>
40 </div>
41 </div>
42 </div>
43 </div>
44 </div>
45 </n-card>
46 </template>
47
48 <script setup lang="ts">
49 import _round from "lodash/round"
50 import { NCard } from "naive-ui"
51 import { computed } from "vue"
52 import Icon from "@/components/common/Icon.vue"
53
54 export interface ItemProps {
55 value: number
56 label: string
57 isTotal?: boolean
58 status?: "success" | "warning" | "error" | "muted" | "primary"
59 }
60
61 interface ItemPropsExt extends ItemProps {
62 percentage: number
63 }
64
65 const {
66 title,
67 values,
68 showTotal = true,
69 showZeroItems,
70 hovered
71 } = defineProps<{
72 title: string
73 values: ItemProps[]
74 showTotal?: boolean
75 showZeroItems?: boolean
76 hovered?: boolean
77 }>()
78
79 const ArrowRightIcon = "carbon:arrow-right"
80 const totItem = computed(
81 () =>
82 values.find(item => item.isTotal) || {
83 value: values.reduce((acc, cur) => {
84 return acc + cur.value
85 }, 0),
86 isTotal: true,
87 label: "Total"
88 }
89 )
90
91 const sanitizedValues = computed<ItemPropsExt[]>(() => {
92 const list: ItemPropsExt[] = values
93 .filter(o => !o.isTotal)
94 .map(o => ({ ...o, percentage: _round((o.value / totItem.value.value) * 100, 2) }))
95 .filter(o => o.value || (!o.value && showZeroItems))
96
97 return [{ ...totItem.value, percentage: 100 }, ...list]
98 })
99
100 const listValues = computed<ItemPropsExt[]>(() =>
101 sanitizedValues.value.filter(o => !o.isTotal || (showTotal && o.isTotal))
102 )
103
104 const barValues = computed<ItemPropsExt[]>(() => sanitizedValues.value.filter(o => !o.isTotal && o.percentage))
105 </script>
106
107 <style scoped lang="scss">
108 .n-card {
109 overflow: hidden;
110
111 .card-header {
112 border-bottom: 1px solid var(--border-color);
113 overflow: hidden;
114 padding: 10px 16px;
115
116 .title {
117 font-size: 16px;
118 text-overflow: ellipsis;
119 white-space: nowrap;
120 overflow: hidden;
121 }
122 }
123
124 .card-content {
125 padding: 10px 16px;
126
127 .bars {
128 height: 10px;
129
130 .bar {
131 padding: 0 2px;
132
133 &:first-child {
134 padding-left: 0;
135 }
136 &:last-child {
137 padding-right: 0;
138 }
139
140 .fill {
141 border-radius: var(--border-radius-small);
142 background-color: var(--fg-default-color);
143 height: 100%;
144 width: 100%;
145 }
146
147 &.success {
148 .fill {
149 background-color: var(--success-color);
150 }
151 }
152 &.warning {
153 .fill {
154 background-color: var(--warning-color);
155 }
156 }
157 &.error {
158 .fill {
159 background-color: var(--error-color);
160 }
161 }
162 &.muted {
163 .fill {
164 background-color: var(--fg-secondary-color);
165 }
166 }
167 &.primary {
168 .fill {
169 background-color: var(--primary-color);
170 }
171 }
172 }
173 }
174
175 .list {
176 font-size: 13px;
177 gap: 4px;
178
179 .item {
180 line-height: 1;
181 padding: 3px 4px;
182
183 .badge {
184 height: 10px;
185 width: 10px;
186 min-width: 10px;
187 border-radius: var(--border-radius-small);
188 background-color: var(--fg-default-color);
189 }
190
191 .divider {
192 height: 1px;
193 background-color: var(--border-color);
194 }
195
196 &.success {
197 .badge {
198 background-color: var(--success-color);
199 }
200 }
201 &.warning {
202 .badge {
203 background-color: var(--warning-color);
204 }
205 }
206 &.error {
207 .badge {
208 background-color: var(--error-color);
209 }
210 }
211 &.muted {
212 .badge {
213 background-color: var(--fg-secondary-color);
214 }
215 }
216 &.primary {
217 .badge {
218 background-color: var(--primary-color);
219 }
220 }
221 }
222 }
223 }
224
225 &.hovered {
226 &:hover {
227 border-color: var(--primary-color);
228 }
229 }
230 }
231 </style>