@hbdlzy/ui-core 0.1.5 → 0.1.6

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 (40) hide show
  1. package/components.manifest.json +21 -0
  2. package/dist/components/OutlinedCascader/OutlinedCascader.types.d.ts +3 -0
  3. package/dist/components/OutlinedCascader/OutlinedCascader.vue.d.ts +9 -0
  4. package/dist/components/OutlinedDatePicker/OutlinedDatePicker.types.d.ts +3 -0
  5. package/dist/components/OutlinedDatePicker/OutlinedDatePicker.vue.d.ts +9 -0
  6. package/dist/components/OutlinedDateTimePicker/OutlinedDateTimePicker.types.d.ts +3 -0
  7. package/dist/components/OutlinedDateTimePicker/OutlinedDateTimePicker.vue.d.ts +9 -0
  8. package/dist/components/OutlinedInput/OutlinedInput.types.d.ts +3 -0
  9. package/dist/components/OutlinedInput/OutlinedInput.vue.d.ts +9 -0
  10. package/dist/components/OutlinedSelect/OutlinedSelect.types.d.ts +3 -0
  11. package/dist/components/OutlinedSelect/OutlinedSelect.vue.d.ts +9 -0
  12. package/dist/components/OutlinedTimePicker/OutlinedTimePicker.types.d.ts +3 -0
  13. package/dist/components/OutlinedTimePicker/OutlinedTimePicker.vue.d.ts +9 -0
  14. package/dist/components/OutlinedTreeSelect/OutlinedTreeSelect.types.d.ts +3 -0
  15. package/dist/components/OutlinedTreeSelect/OutlinedTreeSelect.vue.d.ts +9 -0
  16. package/dist/index.cjs +5 -5
  17. package/dist/index.js +339 -121
  18. package/dist/style.css +1 -1
  19. package/package.json +1 -1
  20. package/src/components/OutlinedCascader/OutlinedCascader.types.ts +3 -0
  21. package/src/components/OutlinedCascader/OutlinedCascader.vue +71 -6
  22. package/src/components/OutlinedCascader/README.md +3 -0
  23. package/src/components/OutlinedDatePicker/OutlinedDatePicker.types.ts +3 -0
  24. package/src/components/OutlinedDatePicker/OutlinedDatePicker.vue +71 -6
  25. package/src/components/OutlinedDatePicker/README.md +3 -0
  26. package/src/components/OutlinedDateTimePicker/OutlinedDateTimePicker.types.ts +3 -0
  27. package/src/components/OutlinedDateTimePicker/OutlinedDateTimePicker.vue +144 -64
  28. package/src/components/OutlinedDateTimePicker/README.md +3 -0
  29. package/src/components/OutlinedInput/OutlinedInput.types.ts +3 -0
  30. package/src/components/OutlinedInput/OutlinedInput.vue +87 -8
  31. package/src/components/OutlinedInput/README.md +3 -0
  32. package/src/components/OutlinedSelect/OutlinedSelect.types.ts +3 -0
  33. package/src/components/OutlinedSelect/OutlinedSelect.vue +72 -6
  34. package/src/components/OutlinedSelect/README.md +3 -0
  35. package/src/components/OutlinedTimePicker/OutlinedTimePicker.types.ts +3 -0
  36. package/src/components/OutlinedTimePicker/OutlinedTimePicker.vue +75 -8
  37. package/src/components/OutlinedTimePicker/README.md +3 -0
  38. package/src/components/OutlinedTreeSelect/OutlinedTreeSelect.types.ts +3 -0
  39. package/src/components/OutlinedTreeSelect/OutlinedTreeSelect.vue +72 -6
  40. package/src/components/OutlinedTreeSelect/README.md +3 -0
@@ -1,74 +1,91 @@
1
1
  <template>
2
2
  <div
3
3
  class="outlined-date-time-picker"
4
- :class="[rootClass]"
5
- :style="rootStyle"
4
+ :class="[containerClasses, rootClass]"
5
+ :style="[containerStyle, rootStyle]"
6
6
  >
7
- <div
8
- class="outlined-date-time-picker__field"
9
- :class="fieldClasses"
10
- :style="fieldStyle"
11
- >
12
- <el-date-picker
13
- ref="datePickerRef"
14
- class="outlined-date-time-picker__control"
15
- :model-value="dateValue"
16
- type="date"
17
- format="YYYY-MM-DD"
18
- value-format="YYYY-MM-DD"
19
- :placeholder="placeholder"
20
- :clearable="clearable"
21
- :disabled-date="resolvedDisabledDate"
22
- :style="controlStyle"
23
- @update:model-value="handleDateUpdate"
24
- @focus="handleFocus('date')"
25
- @blur="handleBlur('date', $event)"
26
- @change="handleDateChange"
27
- @visible-change="handleVisibleChange('date', $event)"
28
- />
29
-
7
+ <div class="outlined-date-time-picker__inner">
30
8
  <div
31
- v-if="floatingLabel"
32
- class="outlined-date-time-picker__label"
33
- :class="dateLabelClasses"
9
+ class="outlined-date-time-picker__field"
10
+ :class="fieldClasses"
34
11
  >
35
- {{ floatingLabel }}
12
+ <el-date-picker
13
+ ref="datePickerRef"
14
+ class="outlined-date-time-picker__control"
15
+ :model-value="dateValue"
16
+ type="date"
17
+ format="YYYY-MM-DD"
18
+ value-format="YYYY-MM-DD"
19
+ :placeholder="datePlaceholder"
20
+ :clearable="clearable"
21
+ :disabled-date="resolvedDisabledDate"
22
+ :style="controlStyle"
23
+ :aria-invalid="hasError ? 'true' : undefined"
24
+ @update:model-value="handleDateUpdate"
25
+ @focus="handleFocus('date')"
26
+ @blur="handleBlur('date', $event)"
27
+ @change="handleDateChange"
28
+ @visible-change="handleVisibleChange('date', $event)"
29
+ />
30
+
31
+ <div
32
+ v-if="floatingLabel"
33
+ class="outlined-date-time-picker__label"
34
+ :class="dateLabelClasses"
35
+ >
36
+ <span
37
+ v-if="required"
38
+ class="outlined-date-time-picker__required"
39
+ >*</span>
40
+ <span>{{ floatingLabel }}</span>
41
+ </div>
36
42
  </div>
37
- </div>
38
43
 
39
- <span class="outlined-date-time-picker__separator">{{ separatorText }}</span>
40
-
41
- <div
42
- class="outlined-date-time-picker__field"
43
- :class="fieldClasses"
44
- :style="fieldStyle"
45
- >
46
- <el-time-picker
47
- ref="timePickerRef"
48
- class="outlined-date-time-picker__control"
49
- :model-value="timeValue"
50
- format="HH:mm"
51
- value-format="HH:mm"
52
- :placeholder="timePlaceholder"
53
- :clearable="clearable"
54
- :disabled="!dateValue"
55
- :disabled-hours="resolvedDisabledHours"
56
- :disabled-minutes="resolvedDisabledMinutes"
57
- :style="controlStyle"
58
- @update:model-value="handleTimeUpdate"
59
- @focus="handleFocus('time')"
60
- @blur="handleBlur('time', $event)"
61
- @change="handleTimeChange"
62
- @visible-change="handleVisibleChange('time', $event)"
63
- />
44
+ <span class="outlined-date-time-picker__separator">{{ separatorText }}</span>
64
45
 
65
46
  <div
66
- class="outlined-date-time-picker__label"
67
- :class="timeLabelClasses"
47
+ class="outlined-date-time-picker__field"
48
+ :class="fieldClasses"
68
49
  >
69
- {{ timePlaceholder }}
50
+ <el-time-picker
51
+ ref="timePickerRef"
52
+ class="outlined-date-time-picker__control"
53
+ :model-value="timeValue"
54
+ format="HH:mm"
55
+ value-format="HH:mm"
56
+ :placeholder="timeControlPlaceholder"
57
+ :clearable="clearable"
58
+ :disabled="!dateValue"
59
+ :disabled-hours="resolvedDisabledHours"
60
+ :disabled-minutes="resolvedDisabledMinutes"
61
+ :style="controlStyle"
62
+ :aria-invalid="hasError ? 'true' : undefined"
63
+ @update:model-value="handleTimeUpdate"
64
+ @focus="handleFocus('time')"
65
+ @blur="handleBlur('time', $event)"
66
+ @change="handleTimeChange"
67
+ @visible-change="handleVisibleChange('time', $event)"
68
+ />
69
+
70
+ <div
71
+ class="outlined-date-time-picker__label"
72
+ :class="timeLabelClasses"
73
+ >
74
+ <span
75
+ v-if="required"
76
+ class="outlined-date-time-picker__required"
77
+ >*</span>
78
+ <span>{{ timePlaceholder }}</span>
79
+ </div>
70
80
  </div>
71
81
  </div>
82
+
83
+ <p
84
+ v-if="hasError"
85
+ class="outlined-date-time-picker__error"
86
+ >
87
+ {{ resolvedErrorMessage }}
88
+ </p>
72
89
  </div>
73
90
  </template>
74
91
 
@@ -99,7 +116,10 @@ const props = withDefaults(defineProps<OutlinedDateTimePickerProps>(), {
99
116
  timePlaceholder: '时间',
100
117
  clearable: false,
101
118
  marginBottom: 20,
102
- paddingTop: 20
119
+ paddingTop: 20,
120
+ required: false,
121
+ error: false,
122
+ errorMessage: ''
103
123
  })
104
124
 
105
125
  const emit = defineEmits<{
@@ -135,19 +155,29 @@ const rootClass = computed(() => attrs.class)
135
155
  const rootStyle = computed(() => attrs.style)
136
156
  const floatingLabel = computed(() => props.label || props.placeholder || '')
137
157
 
138
- const fieldClasses = computed(() => ({
139
- 'outlined-date-time-picker__field--bordered': props.isBorder
158
+ const containerClasses = computed(() => ({
159
+ 'outlined-date-time-picker--error': hasError.value
140
160
  }))
141
161
 
142
- const fieldStyle = computed(() => ({
162
+ const containerStyle = computed(() => ({
143
163
  marginBottom: toCssValue(props.marginBottom),
144
164
  paddingTop: toCssValue(props.paddingTop)
145
165
  }))
146
166
 
167
+ const fieldClasses = computed(() => ({
168
+ 'outlined-date-time-picker__field--bordered': props.isBorder,
169
+ 'outlined-date-time-picker__field--error': hasError.value
170
+ }))
171
+
147
172
  const controlStyle = computed(() => ({
148
173
  height: `${props.inputHeight}px`
149
174
  }))
150
175
 
176
+ const datePlaceholder = computed(() => addRequiredMark(props.placeholder))
177
+ const timeControlPlaceholder = computed(() => addRequiredMark(props.timePlaceholder))
178
+ const hasError = computed(() => Boolean(props.error))
179
+ const resolvedErrorMessage = computed(() => props.errorMessage || createRequiredMessage(floatingLabel.value))
180
+
151
181
  const resolvedDisabledDate = computed(() => createDisabledDateHandler(props.disabledDate))
152
182
 
153
183
  const resolvedDisabledHours = computed(() => {
@@ -316,6 +346,18 @@ function toCssValue(value: OutlinedDateTimePickerCssValue | undefined) {
316
346
  return value
317
347
  }
318
348
 
349
+ function addRequiredMark(value: string) {
350
+ if (!props.required || !value || value.startsWith('*')) {
351
+ return value
352
+ }
353
+
354
+ return `*${value}`
355
+ }
356
+
357
+ function createRequiredMessage(label: string) {
358
+ return label ? `${label}为必填项` : '该项为必填项'
359
+ }
360
+
319
361
  defineExpose<OutlinedDateTimePickerExpose>({
320
362
  focusDate: () => {
321
363
  datePickerRef.value?.focus?.()
@@ -342,6 +384,10 @@ defineExpose<OutlinedDateTimePickerExpose>({
342
384
 
343
385
  <style scoped>
344
386
  .outlined-date-time-picker {
387
+ position: relative;
388
+ }
389
+
390
+ .outlined-date-time-picker__inner {
345
391
  display: flex;
346
392
  align-items: center;
347
393
  gap: 8px;
@@ -360,6 +406,9 @@ defineExpose<OutlinedDateTimePickerExpose>({
360
406
  .outlined-date-time-picker__label {
361
407
  position: absolute;
362
408
  left: -2px;
409
+ display: inline-flex;
410
+ align-items: center;
411
+ gap: 2px;
363
412
  padding: 2px 5px;
364
413
  border-radius: 2px;
365
414
  background-color: #ffffff;
@@ -389,6 +438,23 @@ defineExpose<OutlinedDateTimePickerExpose>({
389
438
  color: var(--el-text-color-regular);
390
439
  }
391
440
 
441
+ .outlined-date-time-picker--error {
442
+ --el-input-focus-border-color: var(--el-color-danger);
443
+ --el-input-hover-border-color: var(--el-color-danger);
444
+ }
445
+
446
+ .outlined-date-time-picker__required,
447
+ .outlined-date-time-picker--error .outlined-date-time-picker__label,
448
+ .outlined-date-time-picker__error {
449
+ color: var(--el-color-danger);
450
+ }
451
+
452
+ .outlined-date-time-picker__error {
453
+ margin: 6px 0 0;
454
+ font-size: 12px;
455
+ line-height: 1.2;
456
+ }
457
+
392
458
  :deep(.outlined-date-time-picker__control.el-date-editor) {
393
459
  width: 100%;
394
460
  }
@@ -409,13 +475,27 @@ defineExpose<OutlinedDateTimePickerExpose>({
409
475
  right: 30px;
410
476
  }
411
477
 
412
- :deep(.outlined-date-time-picker__field--bordered .el-input__wrapper) {
478
+ .outlined-date-time-picker__field--bordered :deep(.el-input__wrapper) {
413
479
  box-shadow: none;
414
480
  border-radius: 0;
415
481
  border-bottom: 1px solid #cccccc;
416
482
  }
417
483
 
418
- :deep(.outlined-date-time-picker__field--bordered .el-input__wrapper.is-focus) {
484
+ .outlined-date-time-picker__field--bordered :deep(.el-input__wrapper.is-focus) {
419
485
  border-bottom-color: var(--el-color-primary);
420
486
  }
487
+
488
+ .outlined-date-time-picker__field--error :deep(.el-input__wrapper) {
489
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
490
+ }
491
+
492
+ .outlined-date-time-picker__field--error :deep(.el-input__wrapper.is-focus),
493
+ .outlined-date-time-picker__field--error :deep(.el-input__wrapper:hover) {
494
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
495
+ }
496
+
497
+ .outlined-date-time-picker__field--error.outlined-date-time-picker__field--bordered :deep(.el-input__wrapper) {
498
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
499
+ border-bottom-color: transparent;
500
+ }
421
501
  </style>
@@ -48,6 +48,9 @@ const openDatePanel = () => {
48
48
  | `value` | `string` | `''` | `v-model:value` 绑定值,格式为 `YYYY-MM-DD HH:mm` |
49
49
  | `placeholder` | `string` | `''` | 日期部分占位文案,也会作为浮动标签主文案 |
50
50
  | `label` | `string` | `''` | 自定义浮动标签文案,优先级高于 `placeholder` |
51
+ | `required` | `boolean` | `false` | 是否显示必填星号 |
52
+ | `error` | `boolean` | `false` | 是否显示错误态,开启后日期和时间输入框变红并展示错误文案 |
53
+ | `errorMessage` | `string` | `''` | 错误文案,未传时默认使用“字段名为必填项” |
51
54
  | `inputHeight` | `number` | `40` | 输入框高度 |
52
55
  | `isBorder` | `boolean` | `false` | 是否启用下边框风格 |
53
56
  | `disabledDate` | `string \| number \| Date \| ((date: Date) => boolean)` | `undefined` | 日期禁用规则,支持 `nowDate` |
@@ -21,6 +21,9 @@ export interface OutlinedInputProps {
21
21
  showWordLimit?: boolean
22
22
  marginBottom?: OutlinedInputCssValue
23
23
  paddingTop?: OutlinedInputCssValue
24
+ required?: boolean
25
+ error?: boolean
26
+ errorMessage?: string
24
27
  }
25
28
 
26
29
  export interface OutlinedInputExpose {
@@ -9,7 +9,7 @@
9
9
  v-bind="inputAttrs"
10
10
  :model-value="inputValue"
11
11
  :type="resolvedType"
12
- :placeholder="placeholder"
12
+ :placeholder="controlPlaceholder"
13
13
  :disabled="disabled"
14
14
  :maxlength="maxlength"
15
15
  :max="max"
@@ -18,6 +18,7 @@
18
18
  :show-word-limit="showWordLimit"
19
19
  :clearable="clearable"
20
20
  :style="inputStyle"
21
+ :aria-invalid="hasError ? 'true' : undefined"
21
22
  @update:model-value="handleModelValueUpdate"
22
23
  @focus="handleFocus"
23
24
  @blur="handleBlur"
@@ -48,8 +49,19 @@
48
49
  class="outlined-input__label"
49
50
  :class="labelClasses"
50
51
  >
51
- {{ floatingLabel }}
52
+ <span
53
+ v-if="required"
54
+ class="outlined-input__required"
55
+ >*</span>
56
+ <span>{{ floatingLabel }}</span>
52
57
  </div>
58
+
59
+ <p
60
+ v-if="hasError"
61
+ class="outlined-input__error"
62
+ >
63
+ {{ resolvedErrorMessage }}
64
+ </p>
53
65
  </div>
54
66
  </template>
55
67
 
@@ -84,7 +96,10 @@ const props = withDefaults(defineProps<OutlinedInputProps>(), {
84
96
  clearable: false,
85
97
  showWordLimit: true,
86
98
  marginBottom: 20,
87
- paddingTop: 20
99
+ paddingTop: 20,
100
+ required: false,
101
+ error: false,
102
+ errorMessage: ''
88
103
  })
89
104
 
90
105
  const emit = defineEmits<{
@@ -127,6 +142,7 @@ const hasTextareaAutosize = computed(() => {
127
142
 
128
143
  const containerClasses = computed(() => ({
129
144
  'outlined-input--bordered': props.isBorder,
145
+ 'outlined-input--error': hasError.value,
130
146
  'outlined-input--textarea-default': resolvedType.value === 'textarea' && !hasTextareaAutosize.value
131
147
  }))
132
148
 
@@ -146,6 +162,9 @@ const inputStyle = computed(() => {
146
162
  })
147
163
 
148
164
  const floatingLabel = computed(() => props.label || props.placeholder || '')
165
+ const controlPlaceholder = computed(() => addRequiredMark(props.placeholder))
166
+ const hasError = computed(() => Boolean(props.error))
167
+ const resolvedErrorMessage = computed(() => props.errorMessage || createRequiredMessage(floatingLabel.value))
149
168
 
150
169
  const hasContent = computed(() => hasFilledValue(inputValue.value))
151
170
 
@@ -222,6 +241,18 @@ function toCssValue(value: OutlinedInputCssValue | undefined) {
222
241
  return value
223
242
  }
224
243
 
244
+ function addRequiredMark(value: string) {
245
+ if (!props.required || !value || value.startsWith('*')) {
246
+ return value
247
+ }
248
+
249
+ return `*${value}`
250
+ }
251
+
252
+ function createRequiredMessage(label: string) {
253
+ return label ? `${label}为必填项` : '该项为必填项'
254
+ }
255
+
225
256
  defineExpose<OutlinedInputExpose>({
226
257
  focus: () => {
227
258
  inputRef.value?.focus()
@@ -247,6 +278,9 @@ defineExpose<OutlinedInputExpose>({
247
278
  .outlined-input__label {
248
279
  position: absolute;
249
280
  left: -2px;
281
+ display: inline-flex;
282
+ align-items: center;
283
+ gap: 2px;
250
284
  padding: 2px 5px;
251
285
  border-radius: 2px;
252
286
  background-color: #ffffff;
@@ -276,32 +310,77 @@ defineExpose<OutlinedInputExpose>({
276
310
  color: var(--el-text-color-regular);
277
311
  }
278
312
 
313
+ .outlined-input--error {
314
+ --el-input-focus-border-color: var(--el-color-danger);
315
+ --el-input-hover-border-color: var(--el-color-danger);
316
+ }
317
+
318
+ .outlined-input__required,
319
+ .outlined-input--error .outlined-input__label,
320
+ .outlined-input__error {
321
+ color: var(--el-color-danger);
322
+ }
323
+
324
+ .outlined-input__error {
325
+ margin: 6px 0 0;
326
+ font-size: 12px;
327
+ line-height: 1.2;
328
+ }
329
+
279
330
  .outlined-input__suffix-text {
280
331
  white-space: nowrap;
281
332
  }
282
333
 
283
- :deep(.outlined-input--bordered .el-input__wrapper) {
334
+ .outlined-input--bordered :deep(.el-input__wrapper) {
284
335
  box-shadow: none;
285
336
  border-radius: 0;
286
337
  border-bottom: 1px solid #cccccc;
287
338
  }
288
339
 
289
- :deep(.outlined-input--bordered .el-input__wrapper.is-focus) {
340
+ .outlined-input--bordered :deep(.el-input__wrapper.is-focus) {
290
341
  border-bottom-color: var(--el-color-primary);
291
342
  }
292
343
 
293
- :deep(.outlined-input--bordered .el-textarea__inner) {
344
+ .outlined-input--bordered :deep(.el-textarea__inner) {
294
345
  border-width: 0 0 1px;
295
346
  border-radius: 0;
296
347
  box-shadow: none;
297
348
  }
298
349
 
299
- :deep(.outlined-input--bordered .el-textarea__inner:focus) {
350
+ .outlined-input--bordered :deep(.el-textarea__inner:focus) {
300
351
  border-bottom-color: var(--el-color-primary);
301
352
  }
302
353
 
303
- :deep(.outlined-input--textarea-default .el-textarea__inner) {
354
+ .outlined-input--textarea-default :deep(.el-textarea__inner) {
304
355
  min-height: 100px !important;
305
356
  max-height: 180px;
306
357
  }
358
+
359
+ .outlined-input--error :deep(.el-input__wrapper) {
360
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
361
+ }
362
+
363
+ .outlined-input--error :deep(.el-input__wrapper.is-focus),
364
+ .outlined-input--error :deep(.el-input__wrapper:hover) {
365
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
366
+ }
367
+
368
+ .outlined-input--error :deep(.el-textarea__inner) {
369
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
370
+ }
371
+
372
+ .outlined-input--error :deep(.el-textarea__inner:focus),
373
+ .outlined-input--error :deep(.el-textarea__inner:hover) {
374
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
375
+ }
376
+
377
+ .outlined-input--error.outlined-input--bordered :deep(.el-input__wrapper) {
378
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
379
+ border-bottom-color: transparent;
380
+ }
381
+
382
+ .outlined-input--error.outlined-input--bordered :deep(.el-textarea__inner) {
383
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
384
+ border-bottom-color: transparent;
385
+ }
307
386
  </style>
@@ -101,6 +101,9 @@ const remark = ref('')
101
101
  - `value`: 当前输入值,推荐通过 `v-model:value` 使用
102
102
  - `placeholder`: 输入框占位文案,同时默认作为浮动标签文案
103
103
  - `label`: 自定义浮动标签文案,未传时回退到 `placeholder`
104
+ - `required`: 是否显示必填星号,默认 `false`
105
+ - `error`: 是否显示错误态,开启后输入框变红并展示错误文案
106
+ - `errorMessage`: 错误文案,未传时默认使用“字段名为必填项”
104
107
  - `disabled`: 是否禁用
105
108
  - `regex`: 数值校验正则,仅在 `isNumber` 为 `true` 时生效
106
109
  - `maxlength`: 最大输入长度
@@ -37,6 +37,9 @@ export interface OutlinedSelectProps {
37
37
  maxCollapseTags?: number
38
38
  marginBottom?: OutlinedSelectCssValue
39
39
  paddingTop?: OutlinedSelectCssValue
40
+ required?: boolean
41
+ error?: boolean
42
+ errorMessage?: string
40
43
  }
41
44
 
42
45
  export interface OutlinedSelectExpose {
@@ -9,7 +9,7 @@
9
9
  v-bind="selectAttrs"
10
10
  class="outlined-select__control"
11
11
  :model-value="selectValue"
12
- :placeholder="placeholder"
12
+ :placeholder="controlPlaceholder"
13
13
  :disabled="disabled"
14
14
  :multiple="multiple"
15
15
  :filterable="filterable"
@@ -18,6 +18,7 @@
18
18
  :collapse-tags-tooltip="collapseTagsTooltip"
19
19
  :max-collapse-tags="maxCollapseTags"
20
20
  :style="selectStyle"
21
+ :aria-invalid="hasError ? 'true' : undefined"
21
22
  @update:model-value="handleModelValueUpdate"
22
23
  @focus="handleFocus"
23
24
  @blur="handleBlur"
@@ -62,8 +63,19 @@
62
63
  class="outlined-select__label"
63
64
  :class="labelClasses"
64
65
  >
65
- {{ floatingLabel }}
66
+ <span
67
+ v-if="required"
68
+ class="outlined-select__required"
69
+ >*</span>
70
+ <span>{{ floatingLabel }}</span>
66
71
  </div>
72
+
73
+ <p
74
+ v-if="hasError"
75
+ class="outlined-select__error"
76
+ >
77
+ {{ resolvedErrorMessage }}
78
+ </p>
67
79
  </div>
68
80
  </template>
69
81
 
@@ -103,7 +115,10 @@ const props = withDefaults(defineProps<OutlinedSelectProps>(), {
103
115
  noCheck: false,
104
116
  maxCollapseTags: 5,
105
117
  marginBottom: 20,
106
- paddingTop: 20
118
+ paddingTop: 20,
119
+ required: false,
120
+ error: false,
121
+ errorMessage: ''
107
122
  })
108
123
 
109
124
  const emit = defineEmits<{
@@ -141,7 +156,8 @@ const selectAttrs = computed(() => {
141
156
  })
142
157
 
143
158
  const containerClasses = computed(() => ({
144
- 'outlined-select--bordered': props.isBorder
159
+ 'outlined-select--bordered': props.isBorder,
160
+ 'outlined-select--error': hasError.value
145
161
  }))
146
162
 
147
163
  const containerStyle = computed(() => ({
@@ -155,6 +171,9 @@ const selectStyle = computed(() => ({
155
171
  }))
156
172
 
157
173
  const floatingLabel = computed(() => props.label || props.placeholder || '')
174
+ const controlPlaceholder = computed(() => addRequiredMark(props.placeholder))
175
+ const hasError = computed(() => Boolean(props.error))
176
+ const resolvedErrorMessage = computed(() => props.errorMessage || createRequiredMessage(floatingLabel.value))
158
177
  const hasCustomOptionsSlot = computed(() => Boolean(slots.default))
159
178
 
160
179
  const hasContent = computed(() => {
@@ -263,6 +282,18 @@ function toCssValue(value: OutlinedSelectCssValue | undefined) {
263
282
  return value
264
283
  }
265
284
 
285
+ function addRequiredMark(value: string) {
286
+ if (!props.required || !value || value.startsWith('*')) {
287
+ return value
288
+ }
289
+
290
+ return `*${value}`
291
+ }
292
+
293
+ function createRequiredMessage(label: string) {
294
+ return label ? `${label}为必填项` : '该项为必填项'
295
+ }
296
+
266
297
  defineExpose<OutlinedSelectExpose>({
267
298
  focus: () => {
268
299
  selectRef.value?.focus?.()
@@ -301,6 +332,9 @@ defineExpose<OutlinedSelectExpose>({
301
332
  .outlined-select__label {
302
333
  position: absolute;
303
334
  left: -2px;
335
+ display: inline-flex;
336
+ align-items: center;
337
+ gap: 2px;
304
338
  padding: 2px 5px;
305
339
  border-radius: 2px;
306
340
  background-color: #ffffff;
@@ -330,6 +364,23 @@ defineExpose<OutlinedSelectExpose>({
330
364
  color: var(--el-text-color-regular);
331
365
  }
332
366
 
367
+ .outlined-select--error {
368
+ --el-input-focus-border-color: var(--el-color-danger);
369
+ --el-input-hover-border-color: var(--el-color-danger);
370
+ }
371
+
372
+ .outlined-select__required,
373
+ .outlined-select--error .outlined-select__label,
374
+ .outlined-select__error {
375
+ color: var(--el-color-danger);
376
+ }
377
+
378
+ .outlined-select__error {
379
+ margin: 6px 0 0;
380
+ font-size: 12px;
381
+ line-height: 1.2;
382
+ }
383
+
333
384
  :deep(.outlined-select__control.el-select) {
334
385
  width: 100%;
335
386
  }
@@ -347,13 +398,28 @@ defineExpose<OutlinedSelectExpose>({
347
398
  min-height: inherit;
348
399
  }
349
400
 
350
- :deep(.outlined-select--bordered .el-select__wrapper) {
401
+ .outlined-select--bordered :deep(.el-select__wrapper) {
351
402
  box-shadow: none;
352
403
  border-radius: 0;
353
404
  border-bottom: 1px solid #cccccc;
354
405
  }
355
406
 
356
- :deep(.outlined-select--bordered .el-select__wrapper.is-focused) {
407
+ .outlined-select--bordered :deep(.el-select__wrapper.is-focused) {
357
408
  border-bottom-color: var(--el-color-primary);
358
409
  }
410
+
411
+ .outlined-select--error :deep(.el-select__wrapper) {
412
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
413
+ }
414
+
415
+ .outlined-select--error :deep(.el-select__wrapper.is-focused),
416
+ .outlined-select--error :deep(.el-select__wrapper.is-focus),
417
+ .outlined-select--error :deep(.el-select__wrapper:hover) {
418
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
419
+ }
420
+
421
+ .outlined-select--error.outlined-select--bordered :deep(.el-select__wrapper) {
422
+ box-shadow: 0 0 0 1px var(--el-color-danger) inset;
423
+ border-bottom-color: transparent;
424
+ }
359
425
  </style>
@@ -113,6 +113,9 @@ const tagOptions = [
113
113
  - `options`: 配置式选项列表,默认 `[]`
114
114
  - `placeholder`: 占位文案,同时默认作为浮动标签文案
115
115
  - `label`: 自定义浮动标签文案,未传时回退到 `placeholder`
116
+ - `required`: 是否显示必填星号,默认 `false`
117
+ - `error`: 是否显示错误态,开启后选择框变红并展示错误文案
118
+ - `errorMessage`: 错误文案,未传时默认使用“字段名为必填项”
116
119
  - `disabled`: 是否禁用
117
120
  - `collapseTags`: 多选时是否折叠标签
118
121
  - `collapseTagsTooltip`: 折叠标签时是否显示 tooltip
@@ -24,6 +24,9 @@ export interface OutlinedTimePickerProps {
24
24
  isBorder?: boolean
25
25
  marginBottom?: OutlinedTimePickerCssValue
26
26
  paddingTop?: OutlinedTimePickerCssValue
27
+ required?: boolean
28
+ error?: boolean
29
+ errorMessage?: string
27
30
  }
28
31
 
29
32
  export interface OutlinedTimePickerExpose {