uark_front/src/pages/home/components/ProgressBar.vue

120 lines
3.5 KiB
Vue

<template>
<div
class="space-y-2 cursor-pointer group outline-none"
role="button"
tabindex="0"
@click="handleSelect"
@keydown.enter.prevent="handleSelect"
@keydown.space.prevent="handleSelect"
>
<!-- 標題列 -->
<div class="flex items-center gap-2">
<label
class="block font-medium transition-colors duration-200 group-hover:text-brand-purple-dark cursor-pointer"
>{{ label }}</label
>
<slot name="icon" />
</div>
<!-- 進度條 -->
<div class="relative">
<progress
v-bind="$attrs"
class="progress w-full h-5 bg-brand-gray-lighter text-brand-green-light text-left [&::-webkit-progress-bar]:rounded-none [&::-webkit-progress-value]:rounded-none [&::-moz-progress-bar]:rounded-none"
:value="animatedValue"
:max="total"
role="progressbar"
:aria-valuenow="animatedValue"
aria-valuemin="0"
:aria-valuemax="total"
></progress>
<span
class="w-full absolute bottom-0 flex justify-between items-center text-[20px] font-nats pe-5 text-brand-gray group-hover:text-brand-purple-dark left-2"
>
{{ animatedValueLocale }} / {{ totalLocale }}
<span aria-hidden="true">
<svg
class="text-brand-gray/50"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</span>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps({
label: { type: String, required: true },
current: { type: Number, required: true },
total: { type: Number, required: true },
textAlign: { type: String, default: "left" },
chartKey: { type: String, required: true },
currentLegend: { type: String, required: true },
totalLegend: { type: String, required: true },
duration: { type: Number, default: 300 },
});
const emit = defineEmits(["select"]);
const animatedValue = ref(0);
const animatedValueLocale = computed(() => animatedValue.value.toLocaleString());
const totalLocale = computed(() => props.total.toLocaleString());
watch(
() => props.current,
(newVal) => {
animateValue(newVal);
},
{ immediate: true }
);
function animateValue(target) {
const start = animatedValue.value;
const diff = target - start;
const startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / props.duration, 1);
animatedValue.value = Math.round(start + diff * progress);
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
function handleSelect() {
emit("select", {
key: props.chartKey,
legends: [props.currentLegend, props.totalLegend],
titleText: `${props.currentLegend} ${animatedValueLocale.value} / ${totalLocale.value}`,
});
}
</script>
<style scoped>
progress::-webkit-progress-value {
transition: width 0.2s ease-out;
height: 20px; /* 恢復原本高度 */
}
progress::-moz-progress-bar {
transition: width 0.2s ease-out;
height: 20px; /* 恢復原本高度 */
}
</style>