vue2server7 7.0.11 → 7.0.13

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,8 +1,9 @@
1
1
  <template>
2
- <div class="number-range" :class="{ 'is-disabled': disabled }">
2
+ <div class="number-range" :class="{ 'is-disabled': disabled, 'is-error': showInternalError }">
3
3
  <el-input
4
4
  v-model="minDisplay"
5
5
  class="number-range__input"
6
+ :class="{ 'is-error': showInternalError }"
6
7
  :placeholder="minPlaceholder"
7
8
  :disabled="disabled"
8
9
  clearable
@@ -15,6 +16,7 @@
15
16
  <el-input
16
17
  v-model="maxDisplay"
17
18
  class="number-range__input"
19
+ :class="{ 'is-error': showInternalError }"
18
20
  :placeholder="maxPlaceholder"
19
21
  :disabled="disabled"
20
22
  clearable
@@ -23,15 +25,44 @@
23
25
  @blur="onMaxBlur"
24
26
  @clear="onMaxClear"
25
27
  />
28
+ <span v-if="showInternalError" class="number-range__error">结束值不能小于起始值</span>
26
29
  </div>
27
30
  </template>
28
31
 
29
32
  <script lang="ts">
30
33
  import type { FormItemRule } from 'element-plus'
31
34
 
32
- type RangeValue = [number | null, number | null]
35
+ export type RangeValue = [number | string | null, number | string | null]
33
36
  type ValidatorCb = (error?: Error) => void
34
37
 
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
+ }
44
+
45
+ export interface NumberRangeProps {
46
+ /** 数组模式绑定值 [min, max],与 start/end 二选一;值可以是 string 或 number */
47
+ modelValue?: RangeValue
48
+ /** 双字段模式 - 起始值(支持 string 类型数字) */
49
+ start?: number | string | null
50
+ /** 双字段模式 - 结束值(支持 string 类型数字) */
51
+ end?: number | string | null
52
+ minPlaceholder?: string
53
+ maxPlaceholder?: string
54
+ /** 两个输入框之间的分隔文字 */
55
+ separator?: string
56
+ /** 允许输入的最小边界 */
57
+ min?: number
58
+ /** 允许输入的最大边界 */
59
+ max?: number
60
+ step?: number
61
+ /** 小数位数限制,如 2 表示最多两位小数 */
62
+ precision?: number
63
+ disabled?: boolean
64
+ }
65
+
35
66
  /**
36
67
  * NumberRange 数字区间输入组件
37
68
  *
@@ -61,10 +92,10 @@ export function rangeRequired(
61
92
  trigger,
62
93
  validator: startField
63
94
  ? (_r: unknown, _v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
64
- cb(source[startField] == null || source[startField] === '' ? new Error(message) : undefined)
95
+ cb(toNum(source[startField]) == null ? new Error(message) : undefined)
65
96
  : (_r: unknown, v: unknown, cb: ValidatorCb) => {
66
97
  const arr = v as RangeValue | undefined
67
- cb(!Array.isArray(arr) || arr[0] == null || arr[1] == null ? new Error(message) : undefined)
98
+ cb(!Array.isArray(arr) || toNum(arr[0]) == null || toNum(arr[1]) == null ? new Error(message) : undefined)
68
99
  }
69
100
  }
70
101
  }
@@ -78,43 +109,26 @@ export function rangeRule(
78
109
  return {
79
110
  trigger,
80
111
  validator: startField
81
- ? (_r: unknown, v: unknown, cb: ValidatorCb, source: Record<string, unknown>) =>
82
- cb(source[startField] != null && v != null && (v as number) < (source[startField] as number) ? new Error(message) : undefined)
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
+ }
83
117
  : (_r: unknown, v: unknown, cb: ValidatorCb) => {
84
118
  const arr = v as RangeValue | undefined
85
- cb(arr?.[0] != null && arr?.[1] != null && arr[1] < arr[0] ? new Error(message) : 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)
86
122
  }
87
123
  }
88
124
  }
89
125
  </script>
90
126
 
91
127
  <script setup lang="ts">
92
- import { ref, watch, computed } from 'vue'
128
+ import { ref, watch, computed, inject } from 'vue'
129
+ import { formItemContextKey } from 'element-plus'
93
130
 
94
- type Nullable<T> = T | null | undefined
95
-
96
- interface Props {
97
- /** 数组模式绑定值 [min, max],与 start/end 二选一 */
98
- modelValue?: RangeValue
99
- /** 双字段模式 - 起始值 */
100
- start?: number | null
101
- /** 双字段模式 - 结束值 */
102
- end?: number | null
103
- minPlaceholder?: string
104
- maxPlaceholder?: string
105
- /** 两个输入框之间的分隔文字 */
106
- separator?: string
107
- /** 允许输入的最小边界 */
108
- min?: number
109
- /** 允许输入的最大边界 */
110
- max?: number
111
- step?: number
112
- /** 小数位数限制,如 2 表示最多两位小数 */
113
- precision?: number
114
- disabled?: boolean
115
- }
116
-
117
- const props = withDefaults(defineProps<Props>(), {
131
+ const props = withDefaults(defineProps<NumberRangeProps>(), {
118
132
  modelValue: undefined,
119
133
  start: undefined,
120
134
  end: undefined,
@@ -135,18 +149,22 @@ const emit = defineEmits<{
135
149
  change: [value: RangeValue]
136
150
  }>()
137
151
 
152
+ /** 检测是否处于 el-form-item 内,有则交给 form 校验,无则组件内部校验 */
153
+ const formItemContext = inject(formItemContextKey, undefined)
154
+ const useInternalValidation = computed(() => !formItemContext)
155
+
138
156
  /** 根据是否传入 modelValue 自动判断绑定模式 */
139
157
  const isArrayMode = computed(() => props.modelValue !== undefined)
140
158
 
141
- function getInitMin(): Nullable<number> {
142
- return isArrayMode.value ? props.modelValue?.[0] : props.start
159
+ function getInitMin(): number | null {
160
+ return toNum(isArrayMode.value ? props.modelValue?.[0] : props.start)
143
161
  }
144
- function getInitMax(): Nullable<number> {
145
- return isArrayMode.value ? props.modelValue?.[1] : props.end
162
+ function getInitMax(): number | null {
163
+ return toNum(isArrayMode.value ? props.modelValue?.[1] : props.end)
146
164
  }
147
165
 
148
- function toDisplay(val: Nullable<number>): string {
149
- if (val === null || val === undefined) return ''
166
+ function toDisplay(val: number | null): string {
167
+ if (val === null) return ''
150
168
  return String(val)
151
169
  }
152
170
 
@@ -202,6 +220,12 @@ const rangeInvalid = computed(() => {
202
220
 
203
221
  const isValid = computed(() => !rangeInvalid.value)
204
222
 
223
+ /** 内部校验:仅在无 el-form-item 且两个输入框都失焦后展示 */
224
+ const blurred = ref(false)
225
+ const showInternalError = computed(() =>
226
+ useInternalValidation.value && blurred.value && rangeInvalid.value
227
+ )
228
+
205
229
  /** 根据绑定模式 emit 对应事件 */
206
230
  function emitValue(): void {
207
231
  const minVal = formatNum(parseNum(minDisplay.value))
@@ -254,6 +278,7 @@ function onMinBlur(): void {
254
278
  minDisplay.value = ''
255
279
  }
256
280
  emitValue()
281
+ if (!minFocused.value && !maxFocused.value) blurred.value = true
257
282
  }
258
283
 
259
284
  function onMaxBlur(): void {
@@ -268,9 +293,11 @@ function onMaxBlur(): void {
268
293
  maxDisplay.value = ''
269
294
  }
270
295
  emitValue()
296
+ if (!minFocused.value && !maxFocused.value) blurred.value = true
271
297
  }
272
298
 
273
299
  function validate(): boolean {
300
+ blurred.value = true
274
301
  return isValid.value
275
302
  }
276
303
 
@@ -280,13 +307,14 @@ defineExpose({ validate, isValid })
280
307
  watch(
281
308
  () => isArrayMode.value ? props.modelValue : [props.start, props.end],
282
309
  (val) => {
283
- const newMin = toDisplay(val?.[0] ?? null)
284
- const newMax = toDisplay(val?.[1] ?? null)
310
+ const newMin = toDisplay(toNum(val?.[0]))
311
+ const newMax = toDisplay(toNum(val?.[1]))
285
312
  if (newMin !== minDisplay.value) minDisplay.value = newMin
286
313
  if (newMax !== maxDisplay.value) maxDisplay.value = newMax
287
- if ((val?.[0] == null) && (val?.[1] == null)) {
314
+ if (toNum(val?.[0]) == null && toNum(val?.[1]) == null) {
288
315
  minFocused.value = false
289
316
  maxFocused.value = false
317
+ blurred.value = false
290
318
  }
291
319
  },
292
320
  { deep: true }
@@ -298,12 +326,12 @@ watch(
298
326
  display: inline-flex;
299
327
  align-items: center;
300
328
  gap: 8px;
301
- flex-wrap: wrap;
302
- position: relative;
329
+ width: 100%;
303
330
  }
304
331
 
305
332
  .number-range__input {
306
- width: 140px;
333
+ flex: 1;
334
+ min-width: 0;
307
335
  }
308
336
 
309
337
  .number-range__separator {
@@ -315,4 +343,16 @@ watch(
315
343
  .number-range.is-disabled .number-range__separator {
316
344
  color: var(--el-text-color-placeholder);
317
345
  }
346
+
347
+ .number-range__input.is-error :deep(.el-input__wrapper) {
348
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
349
+ }
350
+
351
+ .number-range__error {
352
+ width: 100%;
353
+ color: var(--el-color-danger);
354
+ font-size: 12px;
355
+ line-height: 1;
356
+ padding-top: 2px;
357
+ }
318
358
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.11",
3
+ "version": "7.0.13",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",