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.
Files changed (31) hide show
  1. package/README.md +147 -0
  2. package/components/config/css/basic.scss +19 -0
  3. package/components/config/interface/basic-type.js +16 -0
  4. package/components/config/interface/components-interface.ts +0 -0
  5. package/components/config/interface/monitor/components/input-monitor.js +0 -0
  6. package/components/config/interface/monitor/property-monitor.ts +136 -0
  7. package/components/config/interface/props/basic-props.ts +88 -0
  8. package/components/config/interface/props/components/button-props.ts +85 -0
  9. package/components/config/interface/props/components/input-props.ts +69 -0
  10. package/components/config/interface/props/props-tools.ts +64 -0
  11. package/components/config/style/basic.js +346 -0
  12. package/components/config/style/component-registry.js +142 -0
  13. package/components/config/style/components/button-style.js +160 -0
  14. package/components/config/style/components/input-style.js +98 -0
  15. package/components/config/style/components-style.js +622 -0
  16. package/components/config/style/property-mapper.js +377 -0
  17. package/components/config/style/pseudo-processor.js +213 -0
  18. package/components/config.js +3 -3
  19. package/components/icon/iconfont.css +87 -0
  20. package/components/icon/iconfont.js +1 -0
  21. package/components/icon/iconfont.json +135 -0
  22. package/components/icon/iconfont.ttf +0 -0
  23. package/components/icon/iconfont.woff +0 -0
  24. package/components/icon/iconfont.woff2 +0 -0
  25. package/components/model/native/v-button/v-button.vue +81 -273
  26. package/components/model/native/v-input/v-input.vue +132 -321
  27. package/components/utils/event-modifiers.ts +139 -0
  28. package/components/utils/validator.ts +451 -0
  29. package/index.js +372 -0
  30. package/package.json +12 -4
  31. package/components/model/native/v-text-button/v-text-button.vue +0 -139
@@ -1,352 +1,163 @@
1
1
  <template>
2
- <view :class="['input-combination', `input-combination--${size}`]">
3
- <view :class="['combination', 'left-combination']" v-if="combinationConfig.isShow && combinationConfig.position === 'left' || combinationConfig.position === 'ends'">
2
+ <view :id="props.id" :class="computedClasses" :style="computedStyle">
3
+ <view :style="leftSlotStyle" class="left-slot">
4
4
  <slot name="left">
5
- <button>{{ combinationConfig.buttonText }}</button>
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="['input-container', `input-container--${combinationConfig.position}`]">
11
+ <view :style="inputStyle" class="center-input">
9
12
  <input
10
- :value="inputValue"
11
- :class="[
12
- 'v-input',
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
- :maxlength="maxlength || undefined"
20
- :disabled="disabled"
21
- :type="type"
22
- :focus="autoFocus"
23
- @input="handleInput"
24
- @focus="handleFocus"
25
- @blur="handleBlur"
26
- @confirm="handleConfirm"
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 :class="['combination', 'right-combination']" v-if="combinationConfig.isShow && combinationConfig.position === 'right' || combinationConfig.position === 'ends'">
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, watch, onMounted, onUnmounted, inject } from 'vue';
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
- interface CombinationConfig {
42
- isShow: Boolean;
43
- position: String;
44
- buttonText: String;
45
- }
100
+ // 定时器管理
101
+ let placeholderTimer: ReturnType<typeof setInterval> | null = null;
46
102
 
47
- /**
48
- * v-input 输入框
49
- * value 双向绑定值
50
- * size 文本框大小 默认值:medium 可选值small小、medium中、large大
51
- * placeholder 提示语 字符串、数组
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
- const emit = defineEmits(['update:value', 'input', 'focus', 'blur', 'confirm']);
123
-
124
- const config = inject<any>('config');
125
- const inputValue = ref(props.value);
126
- const focused = ref(false);
127
- const currentLength = ref(props.value.length);
128
- const currentPlaceholder = ref('');
129
- const placeholderIndex = ref(0);
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
- watch(
133
- () => props.value,
134
- (newVal) => {
135
- inputValue.value = newVal;
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
- watch(
141
- () => props.placeholder,
142
- (newVal) => {
143
- if (Array.isArray(newVal) && newVal.length > 0) {
144
- currentPlaceholder.value = newVal[0];
145
- placeholderIndex.value = 0;
146
- } else if (typeof newVal === 'string') {
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
- { immediate: true }
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 (placeholderInterval) {
161
- clearInterval(placeholderInterval);
162
- placeholderInterval = null;
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
- .input-combination {
203
- display: flex;
204
-
205
- &--small {
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
+ }