v-uni-app-ui 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/components/config/css/basic.scss +19 -0
- package/components/config/interface/basic-type.js +16 -0
- package/components/config/interface/components-interface.ts +0 -0
- package/components/config/interface/monitor/components/input-monitor.js +0 -0
- package/components/config/interface/monitor/property-monitor.ts +136 -0
- package/components/config/interface/props/basic-props.ts +88 -0
- package/components/config/interface/props/components/button-props.ts +85 -0
- package/components/config/interface/props/components/input-props.ts +69 -0
- package/components/config/interface/props/props-tools.ts +64 -0
- package/components/config/style/basic.js +346 -0
- package/components/config/style/component-registry.js +142 -0
- package/components/config/style/components/button-style.js +160 -0
- package/components/config/style/components/input-style.js +98 -0
- package/components/config/style/components-style.js +622 -0
- package/components/config/style/property-mapper.js +377 -0
- package/components/config/style/pseudo-processor.js +213 -0
- package/components/config.js +3 -3
- package/components/icon/iconfont.css +87 -0
- package/components/icon/iconfont.js +1 -0
- package/components/icon/iconfont.json +135 -0
- package/components/icon/iconfont.ttf +0 -0
- package/components/icon/iconfont.woff +0 -0
- package/components/icon/iconfont.woff2 +0 -0
- package/components/model/native/v-button/v-button.vue +81 -273
- package/components/model/native/v-input/v-input.vue +132 -321
- package/components/utils/event-modifiers.ts +139 -0
- package/components/utils/validator.ts +451 -0
- package/index.js +372 -0
- package/package.json +12 -4
- package/components/model/native/v-text-button/v-text-button.vue +0 -139
|
@@ -1,352 +1,163 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<view :class="
|
|
3
|
-
<view :
|
|
2
|
+
<view :id="props.id" :class="computedClasses" :style="computedStyle">
|
|
3
|
+
<view :style="leftSlotStyle" class="left-slot">
|
|
4
4
|
<slot name="left">
|
|
5
|
-
<
|
|
5
|
+
<view class="left-icon" v-if="props.isIcon" :style="iconStyle">
|
|
6
|
+
<view class="iconfont icon-url"></view>
|
|
7
|
+
<view class="icon-fill" :style="iconFillStyle"></view>
|
|
8
|
+
</view>
|
|
6
9
|
</slot>
|
|
7
10
|
</view>
|
|
8
|
-
<view :class="
|
|
11
|
+
<view :style="inputStyle" class="center-input">
|
|
9
12
|
<input
|
|
10
|
-
|
|
11
|
-
:
|
|
12
|
-
|
|
13
|
-
{ 'v-input--disabled': disabled },
|
|
14
|
-
{ 'input-focused': focused },
|
|
15
|
-
`v-input--border--${borderModel}`,
|
|
16
|
-
`v-input--right--combination--${combinationConfig.position}`
|
|
17
|
-
]"
|
|
13
|
+
v-model="inputValue"
|
|
14
|
+
:type="computedInputType"
|
|
15
|
+
:style="inputFontStyle"
|
|
18
16
|
:placeholder="currentPlaceholder"
|
|
19
|
-
:
|
|
20
|
-
:
|
|
21
|
-
:
|
|
22
|
-
:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
:disabled="props.disabled"
|
|
18
|
+
:placeholder-style="toCSS(inputPlaceholderStyle)"
|
|
19
|
+
:cursor="props.cursor"
|
|
20
|
+
:selection-start="selectionStart"
|
|
21
|
+
:selection-end="selectionEnd"
|
|
22
|
+
:maxlength="props.maxlength"
|
|
23
|
+
:random-number="props.randomNumber"
|
|
24
|
+
:cursor-color="props.cursorColor"
|
|
25
|
+
:focus="props.focus"
|
|
26
|
+
:auto-blur="props.autoBlur"
|
|
27
|
+
:password="props.type === 'password'"
|
|
28
|
+
:cursor-spacing="props.cursorSpacing"
|
|
29
|
+
@focus="focus(true)"
|
|
30
|
+
@blur="focus(false)"
|
|
27
31
|
/>
|
|
28
|
-
<view v-if="showCounter && maxlength" class="input-counter">{{ currentLength }}/{{ maxlength }}</view>
|
|
29
32
|
</view>
|
|
30
|
-
<view :
|
|
31
|
-
<slot name="right">
|
|
32
|
-
<button>{{ combinationConfig.buttonText }}</button>
|
|
33
|
-
</slot>
|
|
33
|
+
<view :style="rightSlotStyle" class="right-slot">
|
|
34
|
+
<slot name="right"></slot>
|
|
34
35
|
</view>
|
|
35
36
|
</view>
|
|
36
37
|
</template>
|
|
37
38
|
|
|
38
39
|
<script setup lang="ts">
|
|
39
|
-
import { ref,
|
|
40
|
+
import { ref, onUnmounted, computed } from 'vue';
|
|
41
|
+
import inputProps from '@/components/config/interface/props/components/input-props';
|
|
42
|
+
import { useComponentStyle } from '@/components/config/style/components-style';
|
|
43
|
+
import { usePropertyMonitor } from '@/components/config/interface/monitor/property-monitor';
|
|
44
|
+
|
|
45
|
+
const props = defineProps(inputProps);
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
input: [value: string];
|
|
48
|
+
blur: [event: FocusEvent];
|
|
49
|
+
focus: [event: FocusEvent];
|
|
50
|
+
'update:value': [value: any];
|
|
51
|
+
'update:placeholderConfig': [value: any];
|
|
52
|
+
'update:disabled': [value: any];
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const { style: computedStyle, classes: computedClasses, sonStyle, toCSS, focus } = useComponentStyle('input', props);
|
|
56
|
+
const getSonStyle = (element: string): Record<string, string | number> => {
|
|
57
|
+
return sonStyle.value[element as keyof typeof sonStyle.value] || {};
|
|
58
|
+
};
|
|
59
|
+
const inputPlaceholderStyle = computed(() => {
|
|
60
|
+
return Object.assign(getSonStyle('placeholder'), props.placeholderStyle);
|
|
61
|
+
});
|
|
62
|
+
const inputStyle = computed(() => getSonStyle('input'));
|
|
63
|
+
const inputFontStyle = computed(() => {
|
|
64
|
+
const { width, height, margin, padding, ...fontStyles } = inputStyle.value;
|
|
65
|
+
return fontStyles;
|
|
66
|
+
});
|
|
67
|
+
const leftSlotStyle = computed(() => getSonStyle('leftSlot'));
|
|
68
|
+
const rightSlotStyle = computed(() => getSonStyle('rightSlot'));
|
|
69
|
+
const iconStyle = computed(() => getSonStyle('icon'));
|
|
70
|
+
const iconFillStyle = computed(() => getSonStyle('iconFill'));
|
|
71
|
+
const computedInputType = computed(() => {
|
|
72
|
+
const typeMap: Record<string, string> = {
|
|
73
|
+
text: 'text',
|
|
74
|
+
id: 'idcard',
|
|
75
|
+
password: 'text',
|
|
76
|
+
'safe-password': 'safe-password',
|
|
77
|
+
number: 'number',
|
|
78
|
+
digit: 'digit',
|
|
79
|
+
numeric: 'numeric',
|
|
80
|
+
decimal: 'decimal',
|
|
81
|
+
email: 'email',
|
|
82
|
+
phone: 'tel',
|
|
83
|
+
url: 'url',
|
|
84
|
+
textarea: 'text', // uni-app 的 input 不支持 textarea,需用单独的 textarea 组件,此处降级为 text
|
|
85
|
+
name: 'nickname',
|
|
86
|
+
search: 'search',
|
|
87
|
+
none: 'none'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return typeMap[props.type] || 'text';
|
|
91
|
+
});
|
|
92
|
+
const currentPlaceholderIndex = ref(0);
|
|
93
|
+
const currentPlaceholder = computed(() => {
|
|
94
|
+
const placeholder = props.placeholder;
|
|
95
|
+
if (!placeholder || placeholder.length === 0) return '';
|
|
96
|
+
const index = currentPlaceholderIndex.value % placeholder.length;
|
|
97
|
+
return placeholder[index];
|
|
98
|
+
});
|
|
40
99
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
position: String;
|
|
44
|
-
buttonText: String;
|
|
45
|
-
}
|
|
100
|
+
// 定时器管理
|
|
101
|
+
let placeholderTimer: ReturnType<typeof setInterval> | null = null;
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* placeholderTimeNumber 更换提示词耗时 默认:8秒
|
|
53
|
-
* maxlength 最大输入值 默认:null 不限制
|
|
54
|
-
* disabled 是否禁用 默认:false 可选值:true禁用 false不禁用
|
|
55
|
-
* type 文本框类型 默认:text 可选值:text、password、number
|
|
56
|
-
* showCounter 是否显示统计文字 默认:false 可选值:true显示统计文字 false不显示统计文字
|
|
57
|
-
* borderModel 边框模式 默认:all 可选值:all普通边框 nont无边框模式 bottom底部边框模式 top上边框模式 left左边框模式 right右边框模式 ends左右两端边框模式 up-down上下边框模式
|
|
58
|
-
* combinationConfig 组合组件
|
|
59
|
-
* isShow:是否显示插槽 默认值:false 可选值:true显示 false不显示
|
|
60
|
-
* position:组合组件显示位置 默认值:default 可选值:right右边 left左边 ends两端
|
|
61
|
-
* 插槽right、left
|
|
62
|
-
* 相关事件:focus、blur、confirm
|
|
63
|
-
*/
|
|
64
|
-
const props = defineProps({
|
|
65
|
-
value: {
|
|
66
|
-
type: String,
|
|
67
|
-
default: '',
|
|
68
|
-
required: true
|
|
69
|
-
},
|
|
70
|
-
size: {
|
|
71
|
-
type: String,
|
|
72
|
-
default: 'medium',
|
|
73
|
-
validator: (value: string) => ['small', 'medium', 'large'].includes(value)
|
|
74
|
-
},
|
|
75
|
-
placeholder: {
|
|
76
|
-
type: [String, Array],
|
|
77
|
-
default: ''
|
|
78
|
-
},
|
|
79
|
-
placeholderTimeNumber: {
|
|
80
|
-
type: Number,
|
|
81
|
-
default: 8000
|
|
82
|
-
},
|
|
83
|
-
maxlength: {
|
|
84
|
-
type: Number,
|
|
85
|
-
default: null
|
|
86
|
-
},
|
|
87
|
-
disabled: {
|
|
88
|
-
type: Boolean,
|
|
89
|
-
default: false
|
|
90
|
-
},
|
|
91
|
-
type: {
|
|
92
|
-
type: String,
|
|
93
|
-
default: 'text',
|
|
94
|
-
validator: (value: string) => ['text', 'password', 'number'].includes(value)
|
|
95
|
-
},
|
|
96
|
-
showCounter: {
|
|
97
|
-
type: Boolean,
|
|
98
|
-
default: false
|
|
99
|
-
},
|
|
100
|
-
borderModel: {
|
|
101
|
-
type: String,
|
|
102
|
-
default: 'all'
|
|
103
|
-
},
|
|
104
|
-
combinationConfig: {
|
|
105
|
-
type: Object as () => CombinationConfig,
|
|
106
|
-
default: {
|
|
107
|
-
isShow: false,
|
|
108
|
-
position: 'default',
|
|
109
|
-
buttonText: ''
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
inputTextPosition: {
|
|
113
|
-
type: String,
|
|
114
|
-
default: 'left'
|
|
115
|
-
},
|
|
116
|
-
autoFocus: {
|
|
117
|
-
type: Boolean,
|
|
118
|
-
default: false
|
|
103
|
+
const managePlaceholderRotation = () => {
|
|
104
|
+
// 清理现有定时器
|
|
105
|
+
if (placeholderTimer) {
|
|
106
|
+
clearInterval(placeholderTimer);
|
|
107
|
+
placeholderTimer = null;
|
|
119
108
|
}
|
|
120
|
-
});
|
|
121
109
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let placeholderInterval: NodeJS.Timeout | null = null;
|
|
110
|
+
// 启动条件:未被禁用 且 配置有效
|
|
111
|
+
if (!props.disabled && props.placeholder && props.placeholder.length > 1 && props.placeholderTimeNumber > 0) {
|
|
112
|
+
placeholderTimer = setInterval(() => {
|
|
113
|
+
const placeholderArray = Array.isArray(props.placeholder) ? props.placeholder : [props.placeholder!];
|
|
114
|
+
currentPlaceholderIndex.value = (currentPlaceholderIndex.value + 1) % placeholderArray.length;
|
|
115
|
+
}, props.placeholderTimeNumber);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
() => props.value,
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
currentLength.value = newVal.length;
|
|
119
|
+
const inputValue = computed({
|
|
120
|
+
get: () => props.value,
|
|
121
|
+
set: (newValue) => {
|
|
122
|
+
emit('update:value', newValue);
|
|
137
123
|
}
|
|
138
|
-
);
|
|
124
|
+
});
|
|
139
125
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
currentPlaceholder.value = newVal;
|
|
148
|
-
}
|
|
126
|
+
usePropertyMonitor({ source: inputValue, propertyName: 'value' }, emit);
|
|
127
|
+
usePropertyMonitor(
|
|
128
|
+
{
|
|
129
|
+
source: () => props.disabled,
|
|
130
|
+
propertyName: 'disabled',
|
|
131
|
+
// 状态变化时重新管理定时器
|
|
132
|
+
onChange: managePlaceholderRotation
|
|
149
133
|
},
|
|
150
|
-
|
|
134
|
+
emit
|
|
135
|
+
);
|
|
136
|
+
usePropertyMonitor(
|
|
137
|
+
{
|
|
138
|
+
source: () => ({
|
|
139
|
+
placeholder: props.placeholder,
|
|
140
|
+
time: props.placeholderTimeNumber
|
|
141
|
+
}),
|
|
142
|
+
propertyName: 'placeholderConfig',
|
|
143
|
+
onChange: managePlaceholderRotation,
|
|
144
|
+
immediate: true
|
|
145
|
+
},
|
|
146
|
+
emit
|
|
151
147
|
);
|
|
152
|
-
|
|
153
|
-
onMounted(() => {
|
|
154
|
-
if (Array.isArray(props.placeholder) && props.placeholder.length > 1) {
|
|
155
|
-
startPlaceholderRotation();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
148
|
|
|
159
149
|
onUnmounted(() => {
|
|
160
|
-
if (
|
|
161
|
-
clearInterval(
|
|
162
|
-
|
|
150
|
+
if (placeholderTimer) {
|
|
151
|
+
clearInterval(placeholderTimer);
|
|
152
|
+
placeholderTimer = null;
|
|
163
153
|
}
|
|
164
154
|
});
|
|
165
|
-
|
|
166
|
-
const startPlaceholderRotation = () => {
|
|
167
|
-
placeholderInterval = setInterval(() => {
|
|
168
|
-
placeholderIndex.value = (placeholderIndex.value + 1) % props.placeholder.length;
|
|
169
|
-
currentPlaceholder.value = props.placeholder[placeholderIndex.value];
|
|
170
|
-
}, props.placeholderTimeNumber);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const handleInput = (e: any) => {
|
|
174
|
-
const value = e.detail.value;
|
|
175
|
-
inputValue.value = value;
|
|
176
|
-
currentLength.value = value.length;
|
|
177
|
-
emit('update:value', value);
|
|
178
|
-
emit('input', value);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const handleFocus = (e: any) => {
|
|
182
|
-
focused.value = true;
|
|
183
|
-
emit('focus', e);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const handleBlur = (e: any) => {
|
|
187
|
-
focused.value = false;
|
|
188
|
-
emit('blur', e);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
//回车搜索
|
|
192
|
-
const handleConfirm = (e: any) => {
|
|
193
|
-
if (e.detail.value.trim() === '' && Array.isArray(props.placeholder) && currentPlaceholder.value) {
|
|
194
|
-
emit('confirm', currentPlaceholder.value, e);
|
|
195
|
-
} else {
|
|
196
|
-
emit('confirm', e.detail.value, e);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
155
|
</script>
|
|
200
156
|
|
|
201
157
|
<style lang="scss" scoped>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
height: 50rpx;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
&--medium {
|
|
210
|
-
height: 60rpx;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
&--large {
|
|
214
|
-
height: 70rpx;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.input-container {
|
|
218
|
-
width: 100%;
|
|
219
|
-
height: 100%;
|
|
220
|
-
position: relative;
|
|
221
|
-
|
|
222
|
-
&--default {
|
|
223
|
-
width: 100%;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
&--right,
|
|
227
|
-
&--left {
|
|
228
|
-
width: 75%;
|
|
229
|
-
}
|
|
230
|
-
&--ends {
|
|
231
|
-
width: 50%;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.v-input {
|
|
235
|
-
width: 100%;
|
|
236
|
-
height: 100%;
|
|
237
|
-
min-height: 45rpx;
|
|
238
|
-
box-sizing: border-box;
|
|
239
|
-
background-color: #fff;
|
|
240
|
-
font-size: v-bind("config.fontSize.mediumText");
|
|
241
|
-
line-height: 1.5;
|
|
242
|
-
transition: border-color 0.3s;
|
|
243
|
-
text-align: v-bind('props.inputTextPosition');
|
|
244
|
-
|
|
245
|
-
::v-deep .uni-input-wrapper {
|
|
246
|
-
width: 92% !important;
|
|
247
|
-
margin-left: 3%;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
&--border--all {
|
|
251
|
-
border: 1px solid v-bind('config.border.color');
|
|
252
|
-
border-radius: 6rpx;
|
|
253
|
-
|
|
254
|
-
&.input-focused {
|
|
255
|
-
border-color: v-bind('config.border.default');
|
|
256
|
-
box-shadow: v-bind("config.VInput.boxShadow");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
&--border--none {
|
|
261
|
-
border: none;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
&--border--bottom {
|
|
265
|
-
border-bottom: 1rpx solid v-bind('config.border.color');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
&--border--top {
|
|
269
|
-
border-top: 1rpx solid v-bind('config.border.color');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
&--border--left {
|
|
273
|
-
border-left: 1rpx solid v-bind('config.border.color');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
&--border--right {
|
|
277
|
-
border-right: 1rpx solid v-bind('config.border.color');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
&--border--ends {
|
|
281
|
-
border-left: 1rpx solid v-bind('config.border.color');
|
|
282
|
-
border-right: 1rpx solid v-bind('config.border.color');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
&--border--up-down {
|
|
286
|
-
border-top: 1rpx solid v-bind('config.border.color');
|
|
287
|
-
border-bottom: 1rpx solid v-bind('config.border.color');
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
&--right--combination--right {
|
|
291
|
-
border-right: none;
|
|
292
|
-
border-top-right-radius: 0;
|
|
293
|
-
border-bottom-right-radius: 0;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
&--right--combination--left {
|
|
297
|
-
border-left: none;
|
|
298
|
-
border-top-left-radius: 0;
|
|
299
|
-
border-bottom-left-radius: 0;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
&--right--combination--ends {
|
|
303
|
-
border-right: none;
|
|
304
|
-
border-top-right-radius: 0;
|
|
305
|
-
border-bottom-right-radius: 0;
|
|
306
|
-
|
|
307
|
-
border-left: none;
|
|
308
|
-
border-top-left-radius: 0;
|
|
309
|
-
border-bottom-left-radius: 0;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
&--disabled {
|
|
313
|
-
opacity: v-bind('config.opacity.disabled');
|
|
314
|
-
background-color: v-bind('config.backgroundColor.disabled');
|
|
315
|
-
color: v-bind('config.fontColor.text');
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
.input-counter {
|
|
319
|
-
position: absolute;
|
|
320
|
-
bottom: 8rpx;
|
|
321
|
-
right: 8rpx;
|
|
322
|
-
font-size: v-bind("config.fontSize.smallText");
|
|
323
|
-
color: v-bind('config.fontColor.text');
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
.combination {
|
|
328
|
-
width: 25%;
|
|
329
|
-
height: 100%;
|
|
330
|
-
button {
|
|
331
|
-
width: 100%;
|
|
332
|
-
height: 100%;
|
|
333
|
-
display: flex;
|
|
334
|
-
align-items: center;
|
|
335
|
-
justify-content: center;
|
|
336
|
-
background-color: v-bind('config.backgroundColor.default');
|
|
337
|
-
color: v-bind('config.fontColor.reversal');
|
|
338
|
-
font-size: v-bind("config.fontSize.mediumText");
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
.left-combination button {
|
|
343
|
-
border-bottom-right-radius: 0;
|
|
344
|
-
border-top-right-radius: 0;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.right-combination button {
|
|
348
|
-
border-bottom-left-radius: 0;
|
|
349
|
-
border-top-left-radius: 0;
|
|
350
|
-
}
|
|
158
|
+
uni-input {
|
|
159
|
+
height: 100%;
|
|
160
|
+
min-height: auto;
|
|
161
|
+
line-height: normal;
|
|
351
162
|
}
|
|
352
163
|
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ref, onUnmounted } from 'vue';
|
|
2
|
+
|
|
3
|
+
// ========== 类型定义 ==========
|
|
4
|
+
|
|
5
|
+
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
6
|
+
type IntervalHandle = ReturnType<typeof setInterval>;
|
|
7
|
+
|
|
8
|
+
export interface DebounceOptions {
|
|
9
|
+
/** 防抖等待时间(毫秒) */
|
|
10
|
+
wait: number;
|
|
11
|
+
/** 是否立即执行第一次 */
|
|
12
|
+
immediate?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CooldownOptions {
|
|
16
|
+
/** 总冷却时间(毫秒) */
|
|
17
|
+
stabilizationTime: number;
|
|
18
|
+
/** 每次倒计时减少的值 */
|
|
19
|
+
degressionTime: number;
|
|
20
|
+
/** 倒计时更新间隔 */
|
|
21
|
+
intervalUpdateTime: number;
|
|
22
|
+
/** 是否在冷却期间点击时重置计时器 */
|
|
23
|
+
resetOnClick?: boolean;
|
|
24
|
+
/** 倒计时回调 */
|
|
25
|
+
onCountdown?: (payload: { remaining: number; total: number }) => void;
|
|
26
|
+
/** 冷却完成回调 */
|
|
27
|
+
onComplete?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ========== 防抖 Hook ==========
|
|
31
|
+
|
|
32
|
+
export function useDebounce<T extends (...args: any[]) => any>(callback: T, options: DebounceOptions) {
|
|
33
|
+
const timer = ref<TimerHandle | null>(null);
|
|
34
|
+
const isPending = ref(false);
|
|
35
|
+
|
|
36
|
+
function debounce(...args: Parameters<T>) {
|
|
37
|
+
if (timer.value) clearTimeout(timer.value);
|
|
38
|
+
|
|
39
|
+
const shouldCallNow = options.immediate && !isPending.value;
|
|
40
|
+
|
|
41
|
+
isPending.value = true;
|
|
42
|
+
timer.value = setTimeout(() => {
|
|
43
|
+
if (!options.immediate) {
|
|
44
|
+
callback(...args);
|
|
45
|
+
}
|
|
46
|
+
isPending.value = false;
|
|
47
|
+
}, options.wait);
|
|
48
|
+
|
|
49
|
+
if (shouldCallNow) {
|
|
50
|
+
callback(...args);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cancel() {
|
|
55
|
+
if (timer.value) {
|
|
56
|
+
clearTimeout(timer.value);
|
|
57
|
+
timer.value = null;
|
|
58
|
+
}
|
|
59
|
+
isPending.value = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onUnmounted(() => {
|
|
63
|
+
cancel();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
debounce,
|
|
68
|
+
cancel,
|
|
69
|
+
isPending
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ========== 冷却 Hook ==========
|
|
74
|
+
|
|
75
|
+
export function useCooldown(options: CooldownOptions) {
|
|
76
|
+
const countdownTimer = ref<IntervalHandle | null>(null);
|
|
77
|
+
const cooldownTimer = ref<TimerHandle | null>(null);
|
|
78
|
+
const countdown = ref(options.stabilizationTime);
|
|
79
|
+
const isCountingDown = ref(false);
|
|
80
|
+
|
|
81
|
+
function stop() {
|
|
82
|
+
if (countdownTimer.value) {
|
|
83
|
+
clearInterval(countdownTimer.value);
|
|
84
|
+
countdownTimer.value = null;
|
|
85
|
+
}
|
|
86
|
+
if (cooldownTimer.value) {
|
|
87
|
+
clearTimeout(cooldownTimer.value);
|
|
88
|
+
cooldownTimer.value = null;
|
|
89
|
+
}
|
|
90
|
+
isCountingDown.value = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function start() {
|
|
94
|
+
// 如果已在冷却中且允许重置,则重新开始
|
|
95
|
+
if (isCountingDown.value && options.resetOnClick) {
|
|
96
|
+
stop();
|
|
97
|
+
} else if (isCountingDown.value) {
|
|
98
|
+
return; // 冷却中且不允许重置,直接返回
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isCountingDown.value = true;
|
|
102
|
+
countdown.value = options.stabilizationTime;
|
|
103
|
+
|
|
104
|
+
// 立即触发一次
|
|
105
|
+
options.onCountdown?.({
|
|
106
|
+
remaining: countdown.value,
|
|
107
|
+
total: options.stabilizationTime
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 倒计时 interval
|
|
111
|
+
countdownTimer.value = setInterval(() => {
|
|
112
|
+
countdown.value -= options.degressionTime;
|
|
113
|
+
|
|
114
|
+
const remaining = Math.max(0, countdown.value);
|
|
115
|
+
options.onCountdown?.({ remaining, total: options.stabilizationTime });
|
|
116
|
+
|
|
117
|
+
if (countdown.value <= 0) {
|
|
118
|
+
stop();
|
|
119
|
+
options.onComplete?.();
|
|
120
|
+
}
|
|
121
|
+
}, options.intervalUpdateTime);
|
|
122
|
+
|
|
123
|
+
// 冷却结束定时器
|
|
124
|
+
cooldownTimer.value = setTimeout(() => {
|
|
125
|
+
isCountingDown.value = false;
|
|
126
|
+
}, options.stabilizationTime);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onUnmounted(() => {
|
|
130
|
+
stop();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
countdown,
|
|
135
|
+
isCountingDown,
|
|
136
|
+
start,
|
|
137
|
+
stop
|
|
138
|
+
};
|
|
139
|
+
}
|