1
1
<script lang="ts" setup>
2
2
import { NProgress , NSkeleton , NSpin } from ' naive-ui' ;
3
3
4
- import { computed , onMounted , onUnmounted , ref , toRefs , watch } from ' vue' ;
4
+ import {
5
+ computed ,
6
+ onBeforeUnmount ,
7
+ onMounted ,
8
+ type PropType ,
9
+ ref ,
10
+ toRefs ,
11
+ watch ,
12
+ } from ' vue' ;
5
13
6
14
import AnimatedNumber from ' ./AnimatedNumber.vue' ;
7
15
8
16
import { Rating } from ' ~/models/rating.model' ;
17
+ import { debounce } from ' ~/utils/debounce.utils' ;
9
18
import { wait } from ' ~/utils/promise.utils' ;
10
19
11
20
const props = defineProps ({
@@ -42,19 +51,91 @@ const props = defineProps({
42
51
required: false ,
43
52
default: false ,
44
53
},
54
+ editable: {
55
+ type: Boolean ,
56
+ required: false ,
57
+ default: false ,
58
+ },
59
+ container: {
60
+ type: Object as PropType <HTMLElement >,
61
+ required: false ,
62
+ },
63
+ transform: {
64
+ type: Function as PropType <(progress : number ) => number >,
65
+ required: false ,
66
+ },
45
67
});
46
68
47
- const { progress, delay } = toRefs (props );
69
+ const emit = defineEmits <{
70
+ (e : ' onEditing' , editing : boolean ): void ;
71
+ (e : ' onEdit' , progress : number ): void ;
72
+ (e : ' onEditProgress' , progress : number ): void ;
73
+ }>();
74
+
75
+ const { progress, delay, container, duration, editable, transform } = toRefs (props );
48
76
49
77
const _progress = ref (0 );
50
78
79
+ const progressRef = ref <InstanceType <typeof NProgress >>();
80
+ const containerRef = computed (() => container ?.value ?? progressRef .value ?.$el );
81
+
82
+ const editing = ref (false );
83
+ const angleProgress = ref (0 );
84
+
85
+ const debounceEmitProgress = debounce (
86
+ () => emit (' onEditProgress' , angleProgress .value ),
87
+ 500 ,
88
+ );
89
+
90
+ const editProgress = computed (() => {
91
+ if (! editing .value ) return _progress .value ;
92
+ debounceEmitProgress ();
93
+ return angleProgress .value ;
94
+ });
95
+
96
+ const progressDuration = computed (() =>
97
+ editing .value ? 100 : duration .value - delay .value ,
98
+ );
99
+
51
100
const color = computed (() => {
52
- if (_progress .value <= Rating .Bad * 10 ) return ' color-error' ;
53
- if (_progress .value <= Rating .Mediocre * 10 ) return ' color-warning' ;
54
- if (_progress .value <= Rating .Good * 10 ) return ' color-info' ;
101
+ if (editProgress .value <= Rating .Bad * 10 ) return ' color-error' ;
102
+ if (editProgress .value <= Rating .Mediocre * 10 ) return ' color-warning' ;
103
+ if (editProgress .value <= Rating .Good * 10 ) return ' color-info' ;
55
104
return ' color-primary' ;
56
105
});
57
106
107
+ const emitProgress = computed (() => {
108
+ if (! transform ?.value || typeof transform .value !== ' function' ) {
109
+ return angleProgress .value ;
110
+ }
111
+ return transform .value (angleProgress .value );
112
+ });
113
+
114
+ const onClick = () => {
115
+ if (! editable .value ) return ;
116
+ editing .value = ! editing .value ;
117
+ emit (' onEditing' , editing .value );
118
+ if (! editing .value ) emit (' onEdit' , emitProgress .value );
119
+ if (editing .value ) progressRef .value ?.$el ?.focus ();
120
+ };
121
+
122
+ const listener = (event : MouseEvent ) => {
123
+ if (! progressRef .value ?.$el ) return ;
124
+ // Get the bounding rectangle of the tracking box
125
+ const rect = progressRef .value ?.$el .getBoundingClientRect ();
126
+
127
+ // Calculate the mouse position relative to the tracking box
128
+ const mouseX = event .clientX - (rect .left + rect .width / 2 );
129
+ const mouseY = rect .top + rect .height / 2 - event .clientY ;
130
+
131
+ // Convert (x, y) to angle in radians then in degrees
132
+ let degrees = Math .atan2 (mouseX , mouseY ) * (180 / Math .PI ) - 180 ;
133
+
134
+ // Adjust angle to 0-360° range
135
+ if (degrees < 0 ) degrees += 360 ;
136
+ angleProgress .value = Math .round (degrees / 3.6 );
137
+ };
138
+
58
139
onMounted (async () => {
59
140
watch (
60
141
progress ,
@@ -64,32 +145,50 @@ onMounted(async () => {
64
145
},
65
146
{ immediate: true },
66
147
);
148
+ watch (
149
+ containerRef ,
150
+ (_new , _old ) => {
151
+ _old ?.removeEventListener (' mousemove' , listener );
152
+ if (editable .value ) _new ?.addEventListener (' mousemove' , listener );
153
+ },
154
+
155
+ { immediate: true },
156
+ );
67
157
});
68
158
69
- onUnmounted (() => {
159
+ onBeforeUnmount (() => {
70
160
_progress .value = 0 ;
161
+ containerRef .value ?.removeEventListener (' mousemove' , listener );
71
162
});
72
163
</script >
73
164
74
165
<template >
75
- <NSpin v-if =" loading" class =" spin" size =" large" >
166
+ <NSpin v-show =" loading" class =" spin" size =" large" >
76
167
<NProgress class =" progress" type =" circle" >
77
168
<NSkeleton class =" skeleton" text round />
78
169
</NProgress >
79
170
</NSpin >
80
171
<NProgress
81
- v-else
172
+ v-show =" !loading"
173
+ ref =" progressRef"
82
174
class =" progress custom-color"
175
+ :class =" { editing, editable }"
83
176
type =" circle"
84
- :percentage =" _progress "
177
+ :percentage =" editProgress "
85
178
:style =" {
86
- '--duration': `${ duration - delay }ms`,
179
+ '--duration': `${ progressDuration }ms`,
87
180
'--custom-progress-color': `var(--${ color })`,
88
181
}"
182
+ :tabindex =" editable ? 0 : undefined"
183
+ @click =" onClick"
184
+ @keydown.enter =" onClick"
185
+ @blur =" editing = false"
89
186
>
187
+ <span v-if =" editing" >{{ emitProgress }}</span >
90
188
<AnimatedNumber
189
+ v-else
91
190
:from =" from"
92
- :to =" _progress "
191
+ :to =" editProgress "
93
192
:duration =" duration"
94
193
:precision =" precision"
95
194
:disabled =" !_progress"
@@ -102,10 +201,22 @@ onUnmounted(() => {
102
201
.spin ,
103
202
.progress {
104
203
--progress-size : 3.7rem !important ;
204
+
205
+ & .editable {
206
+ cursor : grab ;
207
+ }
208
+
209
+ & .editing {
210
+ cursor : grabbing ;
211
+ }
105
212
}
106
213
107
214
.custom-color {
108
215
--n-fill-color : var (--custom-progress-color , var (--color-info )) !important ;
216
+
217
+ & :focus-visible {
218
+ outline : -webkit-focus-ring-color auto 1px ;
219
+ }
109
220
}
110
221
111
222
.spin {
0 commit comments