| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- <template>
- <view class="cl-progress-circle" :class="[pt.className]">
- <canvas
- class="cl-progress-circle__canvas"
- :id="canvasId"
- :style="{
- height: `${props.size}px`,
- width: `${props.size}px`
- }"
- ></canvas>
- <slot name="text">
- <cl-text
- :value="`${value}${unit}`"
- :pt="{
- className: parseClass(['absolute', pt.text?.className])
- }"
- v-if="showText"
- ></cl-text>
- </slot>
- </view>
- </template>
- <script lang="ts" setup>
- import { getColor, isDark, parseClass, parsePt, uuid } from "@/cool";
- import { computed, getCurrentInstance, onMounted, ref, watch, type PropType } from "vue";
- import type { PassThroughProps } from "../../types";
- defineOptions({
- name: "cl-progress-circle"
- });
- const props = defineProps({
- pt: {
- type: Object,
- default: () => ({})
- },
- // 数值 (0-100)
- value: {
- type: Number,
- default: 0
- },
- // 圆形大小
- size: {
- type: Number,
- default: 120
- },
- // 线条宽度
- strokeWidth: {
- type: Number,
- default: 8
- },
- // 进度条颜色
- color: {
- type: String as PropType<string | null>,
- default: null
- },
- // 底色
- unColor: {
- type: String as PropType<string | null>,
- default: null
- },
- // 是否显示文本
- showText: {
- type: Boolean,
- default: true
- },
- // 单位
- unit: {
- type: String,
- default: "%"
- },
- // 起始角度 (弧度)
- startAngle: {
- type: Number,
- default: -Math.PI / 2
- },
- // 是否顺时针
- clockwise: {
- type: Boolean,
- default: true
- },
- // 动画时长
- duration: {
- type: Number,
- default: 500
- }
- });
- const { proxy } = getCurrentInstance()!;
- // 透传样式类型定义
- type PassThrough = {
- className?: string;
- text?: PassThroughProps;
- };
- // 解析透传样式配置
- const pt = computed(() => parsePt<PassThrough>(props.pt));
- // canvas组件上下文
- let canvasCtx: CanvasContext | null = null;
- // 绘图上下文
- let drawCtx: CanvasRenderingContext2D | null = null;
- // 生成唯一的canvas ID
- const canvasId = `cl-progress-circle__${uuid()}`;
- // 当前显示值
- const value = ref(0);
- // 绘制圆形进度条
- function drawProgress() {
- if (drawCtx == null) return;
- const centerX = props.size / 2;
- const centerY = props.size / 2;
- const radius = (props.size - props.strokeWidth) / 2;
- // 清除画布
- // #ifdef APP
- drawCtx!.reset();
- // #endif
- // #ifndef APP
- drawCtx!.clearRect(0, 0, props.size, props.size);
- // #endif
- // 获取设备像素比
- const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
- // #ifndef H5
- // 设置缩放比例
- drawCtx!.scale(dpr, dpr);
- // #endif
- // 保存当前状态
- drawCtx!.save();
- // 优化的圆环绘制
- const drawCircle = (startAngle: number, endAngle: number, color: string) => {
- if (drawCtx == null) return;
- drawCtx!.beginPath();
- drawCtx!.arc(centerX, centerY, radius, startAngle, endAngle, false);
- drawCtx!.strokeStyle = color;
- drawCtx!.lineWidth = props.strokeWidth;
- drawCtx!.lineCap = "round";
- drawCtx!.lineJoin = "round";
- drawCtx!.stroke();
- };
- // 绘制底色圆环
- drawCircle(
- 0,
- 2 * Math.PI,
- props.unColor ?? (isDark.value ? getColor("surface-700") : getColor("surface-200"))
- );
- // 绘制进度圆弧
- if (value.value > 0) {
- const progress = Math.max(0, Math.min(100, value.value)) / 100;
- const endAngle = props.startAngle + (props.clockwise ? 1 : -1) * 2 * Math.PI * progress;
- drawCircle(props.startAngle, endAngle, props.color ?? getColor("primary-500"));
- }
- }
- // 动画更新数值
- function animate(targetValue: number) {
- const startValue = value.value;
- const startTime = Date.now();
- function update() {
- // 获取当前时间
- const currentTime = Date.now();
- // 计算动画经过的时间
- const elapsed = currentTime - startTime;
- // 计算动画进度
- const progress = Math.min(elapsed / props.duration, 1);
- // 缓动函数
- const easedProgress = 1 - Math.pow(1 - progress, 3);
- // 计算当前值
- value.value = Math.round(startValue + (targetValue - startValue) * easedProgress);
- // 绘制进度条
- drawProgress();
- if (progress < 1) {
- if (canvasCtx != null) {
- // @ts-ignore
- canvasCtx!.requestAnimationFrame(() => {
- update();
- });
- }
- }
- }
- update();
- }
- // 初始化画布
- function initCanvas() {
- uni.createCanvasContextAsync({
- id: canvasId,
- component: proxy,
- success: (context: CanvasContext) => {
- // 设置canvas上下文
- canvasCtx = context;
- // 获取绘图上下文
- drawCtx = context.getContext("2d")!;
- // 设置宽高
- drawCtx!.canvas.width = props.size;
- drawCtx!.canvas.height = props.size;
- // 优化渲染质量
- drawCtx!.textBaseline = "middle";
- drawCtx!.textAlign = "center";
- drawCtx!.miterLimit = 10;
- // 开始动画
- animate(props.value);
- }
- });
- }
- onMounted(() => {
- initCanvas();
- // 监听value变化
- watch(
- computed(() => props.value),
- (val: number) => {
- animate(Math.max(0, Math.min(100, val)));
- },
- {
- immediate: true
- }
- );
- watch(
- computed(() => [props.color, props.unColor, isDark.value]),
- () => {
- drawProgress();
- }
- );
- });
- defineExpose({
- animate
- });
- </script>
- <style lang="scss" scoped>
- .cl-progress-circle {
- @apply flex flex-col items-center justify-center relative;
- }
- </style>
|