DashboardHorizontalBarPanel.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <q-card class="panel-card panel-card--soft" flat>
  3. <div class="panel-title">{{ title }}</div>
  4. <div class="availability-list">
  5. <div
  6. v-for="item in decoratedItems"
  7. :key="item.label"
  8. class="availability-item"
  9. >
  10. <div class="availability-track">
  11. <div
  12. class="availability-bar"
  13. :style="{
  14. backgroundColor: item.color,
  15. }"
  16. >
  17. <div class="availability-track-meta">
  18. <div class="availability-bar-label-wrap">
  19. <span
  20. class="availability-bar-value-dot"
  21. :style="{
  22. backgroundColor: item.textColor,
  23. }"
  24. />
  25. <span class="availability-bar-label">
  26. {{ item.label }}
  27. </span>
  28. </div>
  29. <span class="availability-bar-value-wrap">
  30. <span
  31. class="availability-bar-value"
  32. >
  33. {{ item.valueLabel }}
  34. </span>
  35. </span>
  36. </div>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. <div class="availability-total-track">
  42. <div
  43. v-for="item in items"
  44. :key="`${item.label}-total`"
  45. :style="{
  46. width: `${item.percentage}%`,
  47. backgroundColor: item.color,
  48. }"
  49. class="availability-total-segment"
  50. />
  51. </div>
  52. <div v-if="totalLabel" class="availability-total">
  53. {{ totalLabel }}
  54. </div>
  55. </q-card>
  56. </template>
  57. <script setup>
  58. import { computed } from "vue";
  59. const props = defineProps({
  60. title: {
  61. type: String,
  62. required: true,
  63. },
  64. items: {
  65. type: Array,
  66. default: () => [],
  67. },
  68. totalLabel: {
  69. type: String,
  70. default: null,
  71. },
  72. });
  73. const clampChannel = (value) => Math.max(0, Math.min(255, value));
  74. const darkenColor = (hexColor, amount = 0.42) => {
  75. const hex = String(hexColor ?? "").replace("#", "");
  76. if (hex.length !== 6) {
  77. return "#173235";
  78. }
  79. const channels = [0, 2, 4].map((start) =>
  80. Number.parseInt(hex.slice(start, start + 2), 16),
  81. );
  82. const darkened = channels.map((channel) =>
  83. clampChannel(Math.round(channel * (1 - amount))),
  84. );
  85. return `#${darkened
  86. .map((channel) => channel.toString(16).padStart(2, "0"))
  87. .join("")}`;
  88. };
  89. const decoratedItems = computed(() =>
  90. props.items.map((item) => ({
  91. ...item,
  92. textColor: darkenColor(item.color),
  93. })),
  94. );
  95. </script>
  96. <style scoped lang="scss">
  97. .panel-card {
  98. padding: 18px;
  99. border-radius: 14px;
  100. background: #ffffff;
  101. border: 1px solid #d9e3e7;
  102. min-height: 300px;
  103. vertical-align: middle;
  104. }
  105. .panel-card--soft {
  106. background: #f0f3f5;
  107. }
  108. .panel-title {
  109. margin-bottom: 16px;
  110. font-size: 19px;
  111. font-weight: 400;
  112. color: #08514c;
  113. }
  114. .availability-list {
  115. display: flex;
  116. flex-direction: column;
  117. gap: 16px;
  118. }
  119. .availability-item {
  120. display: flex;
  121. flex-direction: column;
  122. gap: 10px;
  123. }
  124. .availability-track {
  125. position: relative;
  126. width: 100%;
  127. min-height: 42px;
  128. overflow: hidden;
  129. background: #dde5e8;
  130. vertical-align: middle;
  131. }
  132. .availability-bar {
  133. width: 100%;
  134. min-height: 42px;
  135. vertical-align: middle;
  136. }
  137. .availability-track-meta {
  138. display: flex;
  139. align-items: center;
  140. justify-content: space-between;
  141. gap: 12px;
  142. width: 100%;
  143. min-width: 0;
  144. min-height: 42px;
  145. padding: 0 14px;
  146. position: relative;
  147. z-index: 1;
  148. color: #173235;
  149. font-size: 13px;
  150. font-weight: 700;
  151. white-space: nowrap;
  152. vertical-align: middle;
  153. }
  154. .availability-bar-label {
  155. text-align: left;
  156. vertical-align: middle;
  157. }
  158. .availability-bar-label-wrap,
  159. .availability-bar-value-wrap {
  160. display: inline-flex;
  161. align-items: center;
  162. justify-content: flex-start;
  163. gap: 8px;
  164. min-width: 0;
  165. vertical-align: middle;
  166. }
  167. .availability-bar-value {
  168. text-align: right;
  169. overflow-wrap: anywhere;
  170. vertical-align: middle;
  171. }
  172. .availability-bar-value-dot {
  173. display: inline-flex;
  174. width: 8px;
  175. height: 8px;
  176. border-radius: 50%;
  177. vertical-align: middle;
  178. }
  179. .availability-total-track {
  180. display: flex;
  181. width: 100%;
  182. height: 18px;
  183. margin-top: 18px;
  184. overflow: hidden;
  185. background: #dde5e8;
  186. vertical-align: middle;
  187. }
  188. .availability-total-segment {
  189. height: 100%;
  190. min-width: 0;
  191. vertical-align: middle;
  192. }
  193. .availability-total {
  194. margin-top: 18px;
  195. color: #657177;
  196. font-size: 14px;
  197. text-align: center;
  198. }
  199. @media (max-width: 640px) {
  200. .availability-list {
  201. gap: 12px;
  202. }
  203. .availability-track,
  204. .availability-bar {
  205. min-height: 48px;
  206. }
  207. .availability-track-meta {
  208. flex-direction: column;
  209. align-items: flex-start;
  210. justify-content: center;
  211. gap: 8px;
  212. min-height: 48px;
  213. padding: 0 12px;
  214. font-size: 12px;
  215. white-space: normal;
  216. }
  217. .availability-bar-value-wrap {
  218. width: 100%;
  219. }
  220. .availability-bar-value {
  221. text-align: left;
  222. }
  223. }
  224. </style>