vue2server7 7.0.18 → 7.0.20

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.
@@ -1,14 +1,14 @@
1
1
  <template>
2
- <div class="number-range" :class="{ 'is-disabled': disabled, 'is-error': showInternalError }">
2
+ <div class="number-range" :class="{ 'is-disabled': disabled }">
3
3
  <el-input
4
4
  v-model="minDisplay"
5
5
  class="number-range__input"
6
- :class="{ 'is-error': showInternalError }"
6
+ :class="{ 'is-error': showError }"
7
7
  :placeholder="minPlaceholder"
8
8
  :disabled="disabled"
9
9
  clearable
10
10
  @input="onMinInput"
11
- @focus="onMinFocus"
11
+ @focus="onFocus"
12
12
  @blur="onMinBlur"
13
13
  @clear="onMinClear"
14
14
  />
@@ -16,175 +16,95 @@
16
16
  <el-input
17
17
  v-model="maxDisplay"
18
18
  class="number-range__input"
19
- :class="{ 'is-error': showInternalError }"
19
+ :class="{ 'is-error': showError }"
20
20
  :placeholder="maxPlaceholder"
21
21
  :disabled="disabled"
22
22
  clearable
23
23
  @input="onMaxInput"
24
- @focus="onMaxFocus"
24
+ @focus="onFocus"
25
25
  @blur="onMaxBlur"
26
26
  @clear="onMaxClear"
27
27
  />
28
- <span v-if="showInternalError" class="number-range__error">结束值不能小于起始值</span>
28
+ <span v-if="showError" class="number-range__error">{{ errorMessage }}</span>
29
29
  </div>
30
30
  </template>
31
31
 
32
- <script lang="ts">
33
- import type { FormItemRule } from 'element-plus'
34
-
35
- export type RangeValue = [number | string | null, number | string | null]
36
- type ValidatorCb = (error?: Error) => void
32
+ <script setup lang="ts">
33
+ import { ref, watch, computed } from 'vue'
37
34
 
38
- /** 将字符串/数字统一转为 number,空值返回 null */
39
- function toNum(val: unknown): number | null {
40
- if (val === null || val === undefined || val === '') return null
41
- const n = Number(val)
42
- return Number.isNaN(n) ? null : n
43
- }
35
+ export type RangeValue = [number | null, number | null]
44
36
 
45
- export interface NumberRangeProps {
46
- /** 数组模式绑定值 [min, max],与 start/end 二选一;值可以是 string 或 number */
37
+ const props = withDefaults(defineProps<{
38
+ /** 数组模式绑定值 [min, max] */
47
39
  modelValue?: RangeValue
48
- /** 双字段模式 - 起始值(支持 string 类型数字) */
49
- start?: number | string | null
50
- /** 双字段模式 - 结束值(支持 string 类型数字) */
51
- end?: number | string | null
40
+ /** 双字段模式 - 起始值 */
41
+ start?: number | null
42
+ /** 双字段模式 - 结束值 */
43
+ end?: number | null
52
44
  minPlaceholder?: string
53
45
  maxPlaceholder?: string
54
- /** 两个输入框之间的分隔文字 */
55
46
  separator?: string
56
- /** 允许输入的最小边界 */
57
- min?: number
58
- /** 允许输入的最大边界 */
59
- max?: number
60
- step?: number
61
- /** 小数位数限制,如 2 表示最多两位小数 */
47
+ /** 小数位数限制 */
62
48
  precision?: number
63
49
  disabled?: boolean
64
- }
65
-
66
- /**
67
- * NumberRange 数字区间输入组件
68
- *
69
- * 两种绑定模式:
70
- * 数组模式 v-model="form.amountRange"
71
- * 双字段模式 v-model:start="form.ageMin" v-model:end="form.ageMax"
72
- *
73
- * 配套校验规则(配合 el-form 使用):
74
- * import { rangeRule, rangeRequired } from '@/components/NumberRange.vue'
75
- *
76
- * 数组模式:
77
- * rangeRequired('请输入金额范围')
78
- * rangeRule('结束金额不能小于起始金额')
79
- *
80
- * 双字段模式:
81
- * rangeRequired('请输入年龄范围', 'ageMin')
82
- * rangeRule('结束年龄不能小于起始年龄', 'ageMin')
83
- */
84
-
85
- /** 区间必填校验规则 */
86
- export function rangeRequired(
87
- message = '请输入完整区间',
88
- startField?: string,
89
- trigger = 'change'
90
- ): FormItemRule {
91
- return {
92
- trigger,
93
- validator: startField
94
- ? (_r: unknown, _v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
95
- cb(toNum(source[startField]) == null ? new Error(message) : undefined)
96
- : (_r: unknown, v: unknown, cb: ValidatorCb) => {
97
- const arr = v as RangeValue | undefined
98
- cb(!Array.isArray(arr) || toNum(arr[0]) == null || toNum(arr[1]) == null ? new Error(message) : undefined)
99
- }
100
- }
101
- }
102
-
103
- /** 区间大小校验规则(结束值 >= 起始值) */
104
- export function rangeRule(
105
- message = '结束值不能小于起始值',
106
- startField?: string,
107
- trigger = 'change'
108
- ): FormItemRule {
109
- return {
110
- trigger,
111
- validator: startField
112
- ? (_r: unknown, v: unknown, cb: ValidatorCb, source: Record<string, unknown>) => {
113
- const startNum = toNum(source[startField])
114
- const endNum = toNum(v)
115
- cb(startNum != null && endNum != null && endNum < startNum ? new Error(message) : undefined)
116
- }
117
- : (_r: unknown, v: unknown, cb: ValidatorCb) => {
118
- const arr = v as RangeValue | undefined
119
- const minNum = toNum(arr?.[0])
120
- const maxNum = toNum(arr?.[1])
121
- cb(minNum != null && maxNum != null && maxNum < minNum ? new Error(message) : undefined)
122
- }
123
- }
124
- }
125
- </script>
126
-
127
- <script setup lang="ts">
128
- import { ref, watch, computed, inject } from 'vue'
129
- import { formItemContextKey } from 'element-plus'
130
-
131
- const props = withDefaults(defineProps<NumberRangeProps>(), {
50
+ /** 自定义错误提示 */
51
+ errorMessage?: string
52
+ }>(), {
132
53
  modelValue: undefined,
133
54
  start: undefined,
134
55
  end: undefined,
135
56
  minPlaceholder: '最小值',
136
57
  maxPlaceholder: '最大值',
137
58
  separator: '到',
138
- min: -Infinity,
139
- max: Infinity,
140
- step: 1,
141
59
  precision: undefined,
142
- disabled: false
60
+ disabled: false,
61
+ errorMessage: '结束值不能小于起始值'
143
62
  })
144
63
 
145
64
  const emit = defineEmits<{
146
65
  'update:modelValue': [value: RangeValue]
147
66
  'update:start': [value: number | null]
148
67
  'update:end': [value: number | null]
149
- change: [value: RangeValue]
68
+ /** 校验状态变化:true 表示通过,false 表示不通过 */
69
+ 'validate': [valid: boolean]
150
70
  }>()
151
71
 
152
- /** 检测是否处于 el-form-item 内,有则交给 form 校验,无则组件内部校验 */
153
- const formItemContext = inject(formItemContextKey, undefined)
154
- const useInternalValidation = computed(() => !formItemContext)
155
-
156
- /** 根据是否传入 modelValue 自动判断绑定模式 */
72
+ // 判断绑定模式
157
73
  const isArrayMode = computed(() => props.modelValue !== undefined)
158
74
 
159
- function getInitMin(): number | null {
160
- return toNum(isArrayMode.value ? props.modelValue?.[0] : props.start)
161
- }
162
- function getInitMax(): number | null {
163
- return toNum(isArrayMode.value ? props.modelValue?.[1] : props.end)
164
- }
165
-
166
- function toDisplay(val: number | null): string {
167
- if (val === null) return ''
168
- return String(val)
169
- }
75
+ // 显示值
76
+ const minDisplay = ref('')
77
+ const maxDisplay = ref('')
170
78
 
171
- const minDisplay = ref(toDisplay(getInitMin()))
172
- const maxDisplay = ref(toDisplay(getInitMax()))
79
+ // 焦点状态
173
80
  const minFocused = ref(false)
174
81
  const maxFocused = ref(false)
82
+ const hasBlurred = ref(false) // 是否已失焦过
175
83
 
176
- /**
177
- * 输入过滤:只允许数字、小数点、负号;
178
- * 小数点仅保留第一个,小数位数受 precision 约束
179
- */
180
- function sanitize(raw: string | null | undefined): string {
181
- if (raw === '' || raw === null || raw === undefined) return ''
182
- let s = raw.replace(/[^0-9.\-]/g, '')
84
+ // 校验状态
85
+ const rangeInvalid = computed(() => {
86
+ const min = toNum(minDisplay.value)
87
+ const max = toNum(maxDisplay.value)
88
+ return min !== null && max !== null && max < min
89
+ })
90
+
91
+ // 显示错误:两个输入框都失焦后且校验不通过
92
+ const showError = computed(() => hasBlurred.value && !minFocused.value && !maxFocused.value && rangeInvalid.value)
93
+
94
+ // 转换为数字
95
+ function toNum(val: unknown): number | null {
96
+ if (val === null || val === undefined || val === '') return null
97
+ const n = Number(val)
98
+ return Number.isNaN(n) ? null : n
99
+ }
183
100
 
101
+ // 输入过滤
102
+ function sanitize(raw: string): string {
103
+ if (!raw) return ''
104
+ let s = raw.replace(/[^0-9.\-]/g, '')
184
105
  const negative = s.startsWith('-')
185
106
  s = s.replace(/-/g, '')
186
107
  if (negative) s = '-' + s
187
-
188
108
  const dotIdx = s.indexOf('.')
189
109
  if (dotIdx !== -1) {
190
110
  const intPart = s.slice(0, dotIdx)
@@ -197,127 +117,101 @@ function sanitize(raw: string | null | undefined): string {
197
117
  return s
198
118
  }
199
119
 
200
- function parseNum(val: string | null | undefined): number | null {
201
- if (val === '' || val === null || val === undefined) return null
202
- const n = Number(val)
203
- return Number.isNaN(n) ? null : n
204
- }
205
-
206
- /** 按 precision 格式化数值 */
207
- function formatNum(val: number | null | undefined): number | null {
208
- if (val === null || val === undefined) return null
120
+ function formatNum(val: number | null): number | null {
121
+ if (val === null) return null
209
122
  if (props.precision !== undefined) {
210
- return Number(Number(val).toFixed(props.precision))
123
+ return Number(val.toFixed(props.precision))
211
124
  }
212
- return Number(val)
125
+ return val
213
126
  }
214
127
 
215
- const rangeInvalid = computed(() => {
216
- const minNum = parseNum(minDisplay.value)
217
- const maxNum = parseNum(maxDisplay.value)
218
- return minNum !== null && maxNum !== null && maxNum < minNum
219
- })
220
-
221
- const isValid = computed(() => !rangeInvalid.value)
222
-
223
- /** 内部校验:仅在无 el-form-item 且两个输入框都失焦后展示 */
224
- const blurred = ref(false)
225
- const showInternalError = computed(() =>
226
- useInternalValidation.value && blurred.value && rangeInvalid.value
227
- )
228
-
229
- /** 根据绑定模式 emit 对应事件 */
230
- function emitValue(): void {
231
- const minVal = formatNum(parseNum(minDisplay.value))
232
- const maxVal = formatNum(parseNum(maxDisplay.value))
128
+ function emitValue() {
129
+ const minVal = formatNum(toNum(minDisplay.value))
130
+ const maxVal = formatNum(toNum(maxDisplay.value))
233
131
  if (isArrayMode.value) {
234
132
  emit('update:modelValue', [minVal, maxVal])
235
133
  } else {
236
134
  emit('update:start', minVal)
237
135
  emit('update:end', maxVal)
238
136
  }
239
- emit('change', [minVal, maxVal])
137
+ // 发送校验状态
138
+ emit('validate', !rangeInvalid.value)
240
139
  }
241
140
 
242
- function onMinInput(val: string | number): void {
141
+ function onMinInput(val: string | number) {
243
142
  minDisplay.value = sanitize(String(val))
244
143
  }
245
144
 
246
- function onMaxInput(val: string | number): void {
145
+ function onMaxInput(val: string | number) {
247
146
  maxDisplay.value = sanitize(String(val))
248
147
  }
249
148
 
250
- function onMinClear(): void {
149
+ function onMinClear() {
251
150
  minDisplay.value = ''
252
151
  emitValue()
253
152
  }
254
153
 
255
- function onMaxClear(): void {
154
+ function onMaxClear() {
256
155
  maxDisplay.value = ''
257
156
  emitValue()
258
157
  }
259
158
 
260
- function onMinFocus(): void {
261
- minFocused.value = true
262
- }
263
-
264
- function onMaxFocus(): void {
265
- maxFocused.value = true
159
+ function onFocus() {
160
+ // 获取焦点时清除错误提示
161
+ hasBlurred.value = false
266
162
  }
267
163
 
268
- /** 失焦时格式化数值、钳位到 [min, max] 边界,并触发 emit */
269
- function onMinBlur(): void {
164
+ function onMinBlur() {
270
165
  minFocused.value = false
271
- let v = parseNum(minDisplay.value)
166
+ // 格式化
167
+ let v = toNum(minDisplay.value)
272
168
  if (v !== null) {
273
- if (v < props.min) v = props.min
274
- if (v > props.max) v = props.max
275
- v = formatNum(v)!
276
- minDisplay.value = String(v)
277
- } else {
278
- minDisplay.value = ''
169
+ minDisplay.value = String(formatNum(v))
279
170
  }
280
171
  emitValue()
281
- if (!minFocused.value && !maxFocused.value) blurred.value = true
172
+ // 两个都失焦后标记
173
+ if (!minFocused.value && !maxFocused.value) {
174
+ hasBlurred.value = true
175
+ }
282
176
  }
283
177
 
284
- function onMaxBlur(): void {
178
+ function onMaxBlur() {
285
179
  maxFocused.value = false
286
- let v = parseNum(maxDisplay.value)
180
+ let v = toNum(maxDisplay.value)
287
181
  if (v !== null) {
288
- if (v > props.max) v = props.max
289
- if (v < props.min) v = props.min
290
- v = formatNum(v)!
291
- maxDisplay.value = String(v)
292
- } else {
293
- maxDisplay.value = ''
182
+ maxDisplay.value = String(formatNum(v))
294
183
  }
295
184
  emitValue()
296
- if (!minFocused.value && !maxFocused.value) blurred.value = true
185
+ if (!minFocused.value && !maxFocused.value) {
186
+ hasBlurred.value = true
187
+ }
297
188
  }
298
189
 
190
+ // 手动校验方法
299
191
  function validate(): boolean {
300
- blurred.value = true
301
- return isValid.value
192
+ hasBlurred.value = true
193
+ return !rangeInvalid.value
302
194
  }
303
195
 
304
- defineExpose({ validate, isValid })
196
+ // 重置方法
197
+ function reset() {
198
+ minDisplay.value = ''
199
+ maxDisplay.value = ''
200
+ hasBlurred.value = false
201
+ }
305
202
 
306
- /** 外部 v-model 变化时同步内部显示值;重置为空时清除焦点状态 */
203
+ defineExpose({ validate, reset })
204
+
205
+ // 同步外部值
307
206
  watch(
308
207
  () => isArrayMode.value ? props.modelValue : [props.start, props.end],
309
208
  (val) => {
310
- const newMin = toDisplay(toNum(val?.[0]))
311
- const newMax = toDisplay(toNum(val?.[1]))
209
+ const newMin = val?.[0] != null ? String(val[0]) : ''
210
+ const newMax = val?.[1] != null ? String(val[1]) : ''
312
211
  if (newMin !== minDisplay.value) minDisplay.value = newMin
313
212
  if (newMax !== maxDisplay.value) maxDisplay.value = newMax
314
- if (toNum(val?.[0]) == null && toNum(val?.[1]) == null) {
315
- minFocused.value = false
316
- maxFocused.value = false
317
- blurred.value = false
318
- }
319
213
  },
320
- { deep: true }
214
+ { immediate: true, deep: true }
321
215
  )
322
216
  </script>
323
217
 
@@ -326,7 +220,6 @@ watch(
326
220
  display: flex;
327
221
  align-items: center;
328
222
  gap: 8px;
329
- width: 100%;
330
223
  flex-wrap: wrap;
331
224
  }
332
225
 
@@ -354,6 +247,6 @@ watch(
354
247
  color: var(--el-color-danger);
355
248
  font-size: 12px;
356
249
  line-height: 1;
357
- padding-top: 2px;
250
+ padding-top: 4px;
358
251
  }
359
252
  </style>