120 lines
3.5 KiB
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>
|