@didaoktv/didaoui-uniapp 1.0.0 → 1.2.0

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 (114) hide show
  1. package/README.md +16 -7
  2. package/components/dd-action-sheet/dd-action-sheet.vue +3 -3
  3. package/components/dd-album/album.js +19 -0
  4. package/components/dd-album/dd-album.vue +345 -0
  5. package/components/dd-album/props.js +89 -0
  6. package/components/dd-avatar/avatar.js +19 -0
  7. package/components/dd-avatar/dd-avatar.vue +172 -160
  8. package/components/dd-avatar/props.js +84 -0
  9. package/components/dd-avatar-group/avatarGroup.js +14 -0
  10. package/components/dd-avatar-group/dd-avatar-group.vue +110 -0
  11. package/components/dd-avatar-group/props.js +57 -0
  12. package/components/dd-badge/dd-badge.vue +2 -2
  13. package/components/dd-barcode/dd-barcode.vue +1030 -0
  14. package/components/dd-bill-detail/dd-bill-detail.vue +265 -0
  15. package/components/dd-button/dd-button.vue +17 -17
  16. package/components/dd-calendar/dd-calendar.vue +357 -0
  17. package/components/dd-canvas/dd-canvas.vue +1088 -0
  18. package/components/dd-capsule-button/dd-capsule-button.vue +6 -6
  19. package/components/dd-car-keyboard/carKeyboard.js +6 -0
  20. package/components/dd-car-keyboard/dd-car-keyboard.vue +315 -0
  21. package/components/dd-car-keyboard/props.js +19 -0
  22. package/components/dd-cascader/dd-cascader.vue +405 -0
  23. package/components/dd-cell/dd-cell.vue +50 -23
  24. package/components/dd-cell-group/dd-cell-group.vue +32 -25
  25. package/components/dd-champion-card/dd-champion-card.vue +4 -4
  26. package/components/dd-checkbox/dd-checkbox.vue +30 -39
  27. package/components/dd-checkbox-group/dd-checkbox-group.vue +67 -0
  28. package/components/dd-copy/dd-copy.vue +74 -0
  29. package/components/dd-coupon/dd-coupon.vue +368 -0
  30. package/components/dd-cropper/dd-cropper.vue +1297 -0
  31. package/components/dd-dialog/dd-dialog.vue +1 -1
  32. package/components/dd-divider/dd-divider.vue +3 -3
  33. package/components/dd-dropdown-item/dd-dropdown-item.vue +5 -5
  34. package/components/dd-empty-state/dd-empty-state.vue +1 -1
  35. package/components/dd-field/dd-field.vue +531 -106
  36. package/components/dd-form/dd-form.vue +116 -0
  37. package/components/dd-form/rules.ts +84 -0
  38. package/components/dd-goods-sku/dd-goods-sku.vue +434 -0
  39. package/components/dd-icon/dd-icon.vue +2 -2
  40. package/components/dd-image/dd-image.vue +4 -4
  41. package/components/dd-input/dd-input.vue +5 -5
  42. package/components/dd-keyboard/dd-keyboard.vue +178 -0
  43. package/components/dd-keyboard/keyboard.js +22 -0
  44. package/components/dd-keyboard/props.js +89 -0
  45. package/components/dd-link/dd-link.vue +87 -0
  46. package/components/dd-link/link.js +18 -0
  47. package/components/dd-link/props.js +44 -0
  48. package/components/dd-list-cell/dd-list-cell.vue +4 -4
  49. package/components/dd-loading/dd-loading.vue +1 -1
  50. package/components/dd-loadmore/dd-loadmore.vue +2 -2
  51. package/components/dd-markdown/dd-markdown.vue +344 -0
  52. package/components/dd-markdown/marked.esm.mjs +7 -0
  53. package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +9 -9
  54. package/components/dd-navigation/dd-navigation.vue +5 -5
  55. package/components/dd-number-keyboard/dd-number-keyboard.vue +200 -0
  56. package/components/dd-number-keyboard/numberKeyboard.js +8 -0
  57. package/components/dd-number-keyboard/props.js +24 -0
  58. package/components/dd-parse/dd-parse.vue +534 -0
  59. package/components/dd-parse/node/handler.wxs +26 -0
  60. package/components/dd-parse/node/node.vue +603 -0
  61. package/components/dd-parse/parse.js +13 -0
  62. package/components/dd-parse/parser.js +1400 -0
  63. package/components/dd-parse/props.js +57 -0
  64. package/components/dd-popover/dd-popover.vue +10 -10
  65. package/components/dd-popover-item/dd-popover-item.vue +3 -3
  66. package/components/dd-popup/dd-popup.vue +5 -5
  67. package/components/dd-poster/dd-poster.vue +695 -0
  68. package/components/dd-progress/dd-progress.vue +3 -3
  69. package/components/dd-pull-refresh/dd-pull-refresh.vue +3 -3
  70. package/components/dd-qrcode/dd-qrcode.vue +509 -0
  71. package/components/dd-qrcode/qrcode.js +1283 -0
  72. package/components/dd-radio/dd-radio.vue +19 -20
  73. package/components/dd-radio-group/dd-radio-group.vue +53 -0
  74. package/components/dd-rate/dd-rate.vue +1 -1
  75. package/components/dd-room-card/dd-room-card.vue +3 -3
  76. package/components/dd-search-bar/dd-search-bar.vue +8 -8
  77. package/components/dd-segmented-tab/dd-segmented-tab.vue +6 -6
  78. package/components/dd-signature/dd-signature.vue +513 -0
  79. package/components/dd-skeleton/dd-skeleton.vue +1 -1
  80. package/components/dd-slider/dd-slider.vue +4 -4
  81. package/components/dd-stat-card/dd-stat-card.vue +5 -5
  82. package/components/dd-step/dd-step.vue +11 -11
  83. package/components/dd-stepper/dd-stepper.vue +6 -6
  84. package/components/dd-swipe-action/dd-swipe-action.vue +1 -1
  85. package/components/dd-swipeable-tab/dd-swipeable-tab.vue +1 -1
  86. package/components/dd-tabbar-item/dd-tabbar-item.vue +8 -8
  87. package/components/dd-tag/dd-tag.vue +17 -1
  88. package/components/dd-text/dd-text.vue +250 -0
  89. package/components/dd-text/props.js +120 -0
  90. package/components/dd-text/text.js +30 -0
  91. package/components/dd-text/value.js +87 -0
  92. package/components/dd-toast/dd-toast.vue +2 -2
  93. package/components/dd-upload/dd-upload.vue +4 -4
  94. package/components/dd-workorder-card/dd-workorder-card.vue +237 -0
  95. package/index.ts +50 -9
  96. package/libs/config/color.js +16 -0
  97. package/libs/config/config.js +25 -0
  98. package/libs/config/props.js +185 -0
  99. package/libs/config/zIndex.js +20 -0
  100. package/libs/function/digit.js +167 -0
  101. package/libs/function/index.js +932 -0
  102. package/libs/function/test.js +327 -0
  103. package/libs/i18n/index.d.ts +2 -0
  104. package/libs/i18n/index.js +89 -0
  105. package/libs/i18n/locales/zh-Hans.js +86 -0
  106. package/libs/mixin/button.js +18 -0
  107. package/libs/mixin/mixin.js +44 -0
  108. package/libs/mixin/mpMixin.js +13 -0
  109. package/libs/mixin/openType.js +27 -0
  110. package/libs/utils.ts +462 -0
  111. package/libs/vue.js +3 -0
  112. package/package.json +1 -1
  113. package/scss/_dd-shared.scss +40 -0
  114. package/scss/_variables.scss +4 -1
@@ -1,78 +1,183 @@
1
1
  <template>
2
2
  <view
3
3
  class="dd-field"
4
- :class="{
5
- 'dd-field--error': errorState,
6
- 'dd-field--disabled': disabled,
7
- 'dd-field--borderless': borderless,
8
- }"
4
+ :class="[
5
+ rootClass,
6
+ {
7
+ 'dd-field--error': errorState,
8
+ 'dd-field--disabled': disabledState,
9
+ 'dd-field--readonly': readonly,
10
+ 'dd-field--borderless': !border,
11
+ 'dd-field--center': center,
12
+ 'dd-field--label-top': effectiveLabelPosition === 'top',
13
+ 'dd-field--clickable': clickableState,
14
+ 'dd-field--large': size === 'large',
15
+ },
16
+ ]"
17
+ :hover-class="clickableState ? 'dd-field--active' : ''"
18
+ :hover-stay-time="100"
19
+ @click="onClickRow"
9
20
  >
10
- <view class="dd-field__row">
11
- <view v-if="label || $slots.label" class="dd-field__label">
12
- <text v-if="required" class="dd-field__required">*</text>
13
- <text class="dd-field__label-text"><slot name="label">{{ label }}</slot></text>
21
+ <view
22
+ v-if="label || $slots.label"
23
+ class="dd-field__label"
24
+ :class="`dd-field__label--${effectiveLabelAlign}`"
25
+ :style="labelStyle"
26
+ >
27
+ <text v-if="requiredState" class="dd-field__required-mark">*</text>
28
+ <view class="dd-field__label-text" :style="titleStyle">
29
+ <slot name="label">{{ label }}</slot>
14
30
  </view>
31
+ </view>
32
+ <view class="dd-field__value">
15
33
  <view class="dd-field__body">
16
- <textarea
17
- v-if="type === 'textarea'"
18
- class="dd-field__control dd-field__textarea"
19
- :value="modelValue"
20
- :placeholder="placeholder"
21
- placeholder-style="color:#9E9E9E"
22
- :disabled="disabled"
23
- :maxlength="maxlength"
24
- :auto-height="autosize"
25
- :style="{ textAlign: inputAlign }"
26
- @input="onInput"
27
- @focus="onFocus"
28
- @blur="onBlur"
29
- @confirm="onConfirm"
30
- />
31
- <input
32
- v-else
33
- class="dd-field__control"
34
- :type="inputType"
35
- :password="isPassword"
36
- :value="modelValue"
37
- :placeholder="placeholder"
38
- placeholder-style="color:#9E9E9E"
39
- :disabled="disabled"
40
- :maxlength="maxlength"
41
- :style="{ textAlign: inputAlign }"
42
- @input="onInput"
43
- @focus="onFocus"
44
- @blur="onBlur"
45
- @confirm="onConfirm"
46
- />
47
- <view v-if="showClear" class="dd-field__clear" @click="onClear"><dd-icon name="cross" /></view>
34
+ <view
35
+ v-if="$slots['left-icon'] || leftIcon || icon"
36
+ class="dd-field__left-icon"
37
+ @click="emit('click-left-icon', $event)"
38
+ >
39
+ <slot name="left-icon">
40
+ <dd-icon :name="icon || leftIcon" />
41
+ </slot>
42
+ </view>
43
+ <view class="dd-field__control-wrap" @click="emit('click-input', $event)">
44
+ <slot>
45
+ <textarea
46
+ v-if="type === 'textarea'"
47
+ class="dd-field__control dd-field__textarea"
48
+ :value="modelValue"
49
+ :placeholder="placeholder"
50
+ :placeholder-style="placeholderStyle"
51
+ :disabled="inputDisabled"
52
+ :maxlength="maxlength"
53
+ :auto-height="isAutoHeight"
54
+ :focus="props.focus || focusState"
55
+ :style="controlStyle"
56
+ :confirm-type="confirmType"
57
+ @input="onInput"
58
+ @focus="onFocus"
59
+ @blur="onBlur"
60
+ @confirm="onConfirm"
61
+ />
62
+ <input
63
+ v-else
64
+ class="dd-field__control"
65
+ :type="inputType"
66
+ :password="isPassword"
67
+ :value="modelValue"
68
+ :placeholder="placeholder"
69
+ :placeholder-style="placeholderStyle"
70
+ :disabled="inputDisabled"
71
+ :maxlength="maxlength"
72
+ :focus="props.focus || focusState"
73
+ :style="controlStyle"
74
+ :confirm-type="confirmType"
75
+ @input="onInput"
76
+ @focus="onFocus"
77
+ @blur="onBlur"
78
+ @confirm="onConfirm"
79
+ />
80
+ </slot>
81
+ </view>
82
+ <view v-if="showClear" class="dd-field__clear" @click.stop="onClear">
83
+ <dd-icon :name="clearIcon" />
84
+ </view>
85
+ <view
86
+ v-if="$slots['right-icon'] || rightIcon"
87
+ class="dd-field__right-icon"
88
+ @click="emit('click-right-icon', $event)"
89
+ >
90
+ <slot name="right-icon">
91
+ <dd-icon :name="rightIcon" />
92
+ </slot>
93
+ </view>
94
+ <view
95
+ v-if="isLink"
96
+ class="dd-field__arrow"
97
+ :class="`dd-field__arrow--${arrowDirection}`"
98
+ ><dd-icon name="arrow" /></view>
99
+ <view v-if="$slots.button" class="dd-field__button" @click.stop>
100
+ <slot name="button"></slot>
101
+ </view>
102
+ </view>
103
+ <view
104
+ v-if="displayError || $slots['error-message']"
105
+ class="dd-field__error-message"
106
+ :class="`dd-field__error-message--${errorMessageAlign}`"
107
+ >
108
+ <slot name="error-message">{{ displayError }}</slot>
48
109
  </view>
110
+ <text v-if="showWordLimit" class="dd-field__word-limit">{{ len }}/{{ maxlength }}</text>
49
111
  </view>
50
- <view v-if="errorMessage || showWordLimit" class="dd-field__footer">
51
- <text v-if="errorMessage" class="dd-field__error">{{ errorMessage }}</text>
52
- <text v-if="showWordLimit" class="dd-field__counter">{{ len }}/{{ maxlength }}</text>
112
+ <view v-if="$slots.extra" class="dd-field__extra">
113
+ <slot name="extra"></slot>
53
114
  </view>
54
115
  </view>
55
116
  </template>
56
117
 
57
118
  <script setup lang="ts">
58
- import { computed } from 'vue'
119
+ import { computed, getCurrentInstance, inject, nextTick, onMounted, onUnmounted, ref } from 'vue'
59
120
  import DdIcon from '../dd-icon/dd-icon.vue'
121
+ import {
122
+ runRules,
123
+ type DdFieldRule,
124
+ type DdFieldValidateTrigger,
125
+ type DdFormContext,
126
+ } from '../dd-form/rules'
60
127
 
61
128
  interface Props {
62
- modelValue?: string | number
129
+ /** any:default 插槽自定义控件(radio-group/checkbox-group)可绑数组/布尔值参与校验 */
130
+ modelValue?: any
63
131
  type?: 'text' | 'textarea' | 'password' | 'number' | 'digit'
64
132
  label?: string
65
133
  placeholder?: string
66
134
  disabled?: boolean
67
- required?: boolean
68
- borderless?: boolean
135
+ readonly?: boolean
136
+ /** true 恒显星号;'auto' = rules 含 required 规则时显示 */
137
+ required?: boolean | 'auto'
138
+ border?: boolean
139
+ center?: boolean
140
+ clickable?: boolean
141
+ isLink?: boolean
142
+ arrowDirection?: 'left' | 'right' | 'up' | 'down'
143
+ /** 单元格尺寸,large 增大行高(与 dd-cell 对齐) */
144
+ size?: 'normal' | 'large'
145
+ icon?: string
146
+ leftIcon?: string
147
+ rightIcon?: string
148
+ url?: string
149
+ linkType?: 'navigateTo' | 'redirectTo' | 'reLaunch' | 'switchTab'
150
+ titleStyle?: string
69
151
  inputAlign?: 'left' | 'center' | 'right'
70
152
  clearable?: boolean
153
+ /** always 恒显清空按钮;focus 聚焦时显示(默认) */
154
+ clearTrigger?: 'always' | 'focus'
155
+ /** 清空按钮图标名 */
156
+ clearIcon?: string
71
157
  maxlength?: number
72
158
  showWordLimit?: boolean
73
- autosize?: boolean
159
+ /** textarea 自适应:true = auto-height;对象 = 以 rpx 设 min/maxHeight(内部 upx2px 转换) */
160
+ autosize?: boolean | { minHeight?: number; maxHeight?: number }
161
+ /** textarea 最小行数(以行高换算 min-height) */
162
+ rows?: number
163
+ focus?: boolean
74
164
  error?: boolean
75
165
  errorMessage?: string
166
+ /** 错误提示文案对齐 */
167
+ errorMessageAlign?: 'left' | 'center' | 'right'
168
+ /** 输入格式化(如手机号 3-4-4 分隔) */
169
+ formatter?: (value: string) => string
170
+ formatTrigger?: 'onChange' | 'onBlur'
171
+ labelWidth?: string
172
+ labelAlign?: 'left' | 'center' | 'right'
173
+ labelPosition?: 'left' | 'top'
174
+ /** 键盘右下角按钮文案(uniapp 原生 confirm-type) */
175
+ confirmType?: 'done' | 'send' | 'search' | 'next' | 'go'
176
+ /** 占位符样式(uniapp 原生 placeholder-style) */
177
+ placeholderStyle?: string
178
+ /** 表单字段名(注册进 dd-form 参与校验/取值) */
179
+ name?: string
180
+ rules?: DdFieldRule[]
76
181
  }
77
182
 
78
183
  const props = withDefaults(defineProps<Props>(), {
@@ -81,154 +186,465 @@ const props = withDefaults(defineProps<Props>(), {
81
186
  label: '',
82
187
  placeholder: '',
83
188
  disabled: false,
189
+ readonly: false,
84
190
  required: false,
85
- borderless: false,
191
+ border: true,
192
+ center: false,
193
+ clickable: false,
194
+ isLink: false,
195
+ arrowDirection: 'right',
196
+ size: 'normal',
197
+ icon: '',
198
+ leftIcon: '',
199
+ rightIcon: '',
200
+ url: '',
201
+ linkType: 'navigateTo',
202
+ titleStyle: '',
86
203
  inputAlign: 'left',
87
204
  clearable: false,
205
+ clearTrigger: 'focus',
206
+ clearIcon: 'clear',
88
207
  maxlength: -1,
89
208
  showWordLimit: false,
90
209
  autosize: false,
210
+ rows: 0,
211
+ focus: false,
91
212
  error: false,
92
213
  errorMessage: '',
214
+ errorMessageAlign: 'left',
215
+ formatter: undefined,
216
+ formatTrigger: 'onChange',
217
+ labelWidth: '',
218
+ labelAlign: undefined,
219
+ labelPosition: undefined,
220
+ confirmType: 'done',
221
+ placeholderStyle: 'color:#9E9E9E',
222
+ name: '',
223
+ rules: undefined,
93
224
  })
94
225
 
95
226
  const emit = defineEmits<{
96
- (e: 'update:modelValue', val: string): void
227
+ (e: 'update:modelValue', val: any): void
97
228
  (e: 'focus', val: Event): void
98
229
  (e: 'blur', val: Event): void
99
230
  (e: 'clear'): void
100
231
  (e: 'confirm', val: string): void
232
+ (e: 'click', val: Event): void
233
+ (e: 'click-input', val: Event): void
234
+ (e: 'click-left-icon', val: Event): void
235
+ (e: 'click-right-icon', val: Event): void
101
236
  }>()
102
237
 
238
+ const formCtx = inject<DdFormContext | null>('dd-form', null)
239
+ const stateErrorMessage = ref('')
240
+ const focused = ref(false)
241
+
242
+ // ponytail: scrollIntoView 用 pageScrollTo(selector),仅页面级滚动,嵌套 scroll-view 不滚动;uid 保证选择器唯一
243
+ const uid = getCurrentInstance()?.uid ?? 0
244
+ const rootClass = `dd-field-${uid}`
245
+
246
+ const disabledState = computed(() => props.disabled || !!formCtx?.props.disabled)
247
+ // ponytail: uniapp input 无 readonly 属性,原生层用 disabled 拦输入,readonly 仅免去禁用样式
248
+ const inputDisabled = computed(() => disabledState.value || props.readonly)
249
+ const requiredState = computed(
250
+ () =>
251
+ props.required === true ||
252
+ (props.required === 'auto' && !!props.rules?.some((r) => r.required))
253
+ )
254
+ const clickableState = computed(() => props.clickable || props.isLink || !!props.url)
255
+ const errorState = computed(() => props.error || !!displayError.value)
256
+ const displayError = computed(() => props.errorMessage || stateErrorMessage.value)
257
+ const showClear = computed(
258
+ () =>
259
+ props.clearable &&
260
+ !disabledState.value &&
261
+ !!String(props.modelValue ?? '') &&
262
+ (props.clearTrigger === 'always' || focused.value)
263
+ )
264
+ const showWordLimit = computed(() => props.showWordLimit && props.maxlength > 0)
265
+ const len = computed(() => String(props.modelValue ?? '').length)
266
+
267
+ const effectiveLabelPosition = computed(
268
+ () => props.labelPosition || formCtx?.props.labelPosition || 'left'
269
+ )
270
+ const effectiveLabelWidth = computed(() => props.labelWidth || formCtx?.props.labelWidth || '')
271
+ const effectiveLabelAlign = computed(() => props.labelAlign || 'left')
272
+
273
+ const labelStyle = computed(() =>
274
+ effectiveLabelPosition.value === 'top' || !effectiveLabelWidth.value
275
+ ? undefined
276
+ : { width: effectiveLabelWidth.value }
277
+ )
278
+
103
279
  // ponytail: password 用 uni 原生 password 布尔属性跨端
104
280
  const inputType = computed<'text' | 'number' | 'digit'>(() => {
105
- if (props.type === 'password') return 'text'
106
- if (props.type === 'text') return 'text'
281
+ if (props.type === 'password' || props.type === 'text') return 'text'
107
282
  return props.type as 'number' | 'digit'
108
283
  })
109
284
  const isPassword = computed(() => props.type === 'password')
110
- const errorState = computed(() => props.error || !!props.errorMessage)
111
- const showClear = computed(() => props.clearable && !!props.modelValue && !props.disabled)
112
- const showWordLimit = computed(() => props.showWordLimit && props.maxlength > 0)
113
- const len = computed(() => String(props.modelValue ?? '').length)
285
+ const isAutoHeight = computed(() => props.autosize === true)
286
+ /** autosize 对象形式:不启用原生 auto-height,用高度区间模拟;props 为 rpx,运行时 upx2px 转 px(H5 动态 style 不识别 rpx) */
287
+ const autosizeStyle = computed(() => {
288
+ if (typeof props.autosize === 'object' && props.autosize) {
289
+ const s: Record<string, string> = {}
290
+ if (props.autosize.minHeight) s.minHeight = `${uni.upx2px(props.autosize.minHeight)}px`
291
+ if (props.autosize.maxHeight) s.maxHeight = `${uni.upx2px(props.autosize.maxHeight)}px`
292
+ return s
293
+ }
294
+ return undefined
295
+ })
296
+
297
+ const controlStyle = computed(() => ({
298
+ textAlign: props.inputAlign,
299
+ ...(props.type === 'textarea' && props.rows > 0 && !props.autosize
300
+ ? { minHeight: `${props.rows * 1.5}em` }
301
+ : {}),
302
+ ...(autosizeStyle.value || {}),
303
+ }))
304
+
305
+ function applyFormatter(val: string): string {
306
+ return props.formatter ? props.formatter(val) : val
307
+ }
114
308
 
115
309
  function onInput(e: any) {
116
- emit('update:modelValue', e?.detail?.value ?? e?.target?.value ?? '')
310
+ const raw = e?.detail?.value ?? e?.target?.value ?? ''
311
+ const val = props.formatTrigger === 'onChange' ? applyFormatter(raw) : raw
312
+ emit('update:modelValue', val)
313
+ validateWithTrigger('onChange')
117
314
  }
315
+
118
316
  function onFocus(e: Event) {
317
+ focused.value = true
119
318
  emit('focus', e)
120
319
  }
320
+
121
321
  function onBlur(e: Event) {
322
+ focused.value = false
323
+ if (props.formatTrigger === 'onBlur') {
324
+ const val = applyFormatter(String(props.modelValue ?? ''))
325
+ if (val !== props.modelValue) emit('update:modelValue', val)
326
+ }
122
327
  emit('blur', e)
328
+ validateWithTrigger('onBlur')
123
329
  }
330
+
124
331
  function onConfirm(e: any) {
125
332
  const v = e?.detail?.value ?? e?.target?.value ?? ''
126
333
  emit('confirm', v)
127
334
  }
335
+
128
336
  function onClear() {
129
337
  emit('update:modelValue', '')
130
338
  emit('clear')
339
+ validateWithTrigger('onChange')
340
+ }
341
+
342
+ function navigate() {
343
+ const opts = { url: props.url }
344
+ switch (props.linkType) {
345
+ case 'redirectTo':
346
+ uni.redirectTo(opts)
347
+ break
348
+ case 'reLaunch':
349
+ uni.reLaunch(opts)
350
+ break
351
+ case 'switchTab':
352
+ uni.switchTab(opts)
353
+ break
354
+ default:
355
+ uni.navigateTo(opts)
356
+ }
357
+ }
358
+
359
+ function onClickRow(e: Event) {
360
+ if (props.url) navigate()
361
+ emit('click', e)
362
+ }
363
+
364
+ /** 校验并显示错误;返回错误消息(undefined = 通过) */
365
+ async function validate(trigger?: DdFieldValidateTrigger): Promise<string | undefined> {
366
+ if (!props.rules?.length) {
367
+ stateErrorMessage.value = ''
368
+ return undefined
369
+ }
370
+ const message = await runRules(props.modelValue, props.rules, trigger)
371
+ stateErrorMessage.value = message ?? ''
372
+ return message
373
+ }
374
+
375
+ /** 事件驱动校验:form 默认触发时机匹配、或某条规则显式声明该 trigger 时执行 */
376
+ async function validateWithTrigger(trigger: DdFieldValidateTrigger) {
377
+ if (!formCtx || !props.rules?.length) return
378
+ const hasExplicit = props.rules.some((r) => r.trigger === trigger)
379
+ if (formCtx.props.validateTrigger !== trigger && !hasExplicit) return
380
+ await validate(trigger)
381
+ }
382
+
383
+ function resetValidation() {
384
+ stateErrorMessage.value = ''
131
385
  }
386
+
387
+ function scrollIntoView() {
388
+ uni.pageScrollTo({ selector: `.${rootClass}`, duration: 300 })
389
+ }
390
+
391
+ /** 手动聚焦/失焦(vant focus()/blur() 对齐;blur 经 uni.hideKeyboard 收起键盘) */
392
+ const focusState = ref(false)
393
+
394
+ function focusField() {
395
+ focusState.value = false
396
+ nextTick(() => {
397
+ focusState.value = true
398
+ })
399
+ }
400
+
401
+ function blurField() {
402
+ focusState.value = false
403
+ uni.hideKeyboard()
404
+ }
405
+
406
+ onMounted(() => {
407
+ if (formCtx && props.name) {
408
+ formCtx.register({
409
+ name: props.name,
410
+ getValue: () => props.modelValue,
411
+ validate,
412
+ resetValidation,
413
+ scrollIntoView,
414
+ })
415
+ }
416
+ })
417
+
418
+ onUnmounted(() => {
419
+ if (formCtx && props.name) formCtx.unregister(props.name)
420
+ })
421
+
422
+ defineExpose({ validate, validateWithTrigger, resetValidation, scrollIntoView, focus: focusField, blur: blurField })
132
423
  </script>
133
424
 
134
425
  <style lang="scss" scoped>
135
426
  @import '../../scss/variables';
136
427
  @import '../../scss/mixins';
137
428
 
429
+ // 样式对齐 vant 4 Field(cell:padding 10px 16px / line-height 24px / 16px 字号 / label 6.2em 主文本色)
430
+ // 颜色全部走 $dd-* 主题 token;1px 发丝线为刻意的例外
138
431
  .dd-field {
139
- background: transparent;
140
- padding: 16rpx 24rpx 0 32rpx;
432
+ position: relative;
433
+ display: flex;
434
+ align-items: flex-start;
435
+ flex-wrap: wrap; // extra 插槽独占第二行
141
436
  box-sizing: border-box;
142
- @include dd-hairline-bottom($dd-border-subtle);
437
+ width: 100%;
438
+ padding: 20rpx $dd-space-4;
439
+ overflow: hidden;
440
+ font-size: $dd-font-size-body;
441
+ line-height: $dd-line-height-lead;
442
+ color: $dd-text-primary;
443
+ background: $dd-bg-elevated;
143
444
 
144
- &__row {
145
- display: flex;
146
- align-items: flex-start;
147
- min-height: 112rpx;
445
+ // vant 式发丝线:两侧缩进 32rpx
446
+ &::after {
447
+ position: absolute;
448
+ box-sizing: border-box;
449
+ content: ' ';
450
+ pointer-events: none;
451
+ right: $dd-space-4;
452
+ bottom: 0;
453
+ left: $dd-space-4;
454
+ border-bottom: 1px solid $dd-border-subtle;
455
+ transform: scaleY(0.5);
456
+ }
457
+
458
+ &--large {
459
+ padding-top: $dd-space-3;
460
+ padding-bottom: $dd-space-3;
461
+ }
462
+
463
+ &--center {
464
+ align-items: center;
465
+ }
466
+
467
+ &--label-top {
468
+ flex-direction: column;
469
+
470
+ .dd-field__label {
471
+ width: 100%;
472
+ margin-right: 0;
473
+ }
148
474
  }
149
475
 
150
476
  &__label {
151
477
  display: flex;
152
478
  align-items: center;
153
- flex-shrink: 0;
479
+ flex: none;
480
+ box-sizing: border-box;
154
481
  width: 6.2em;
155
- min-height: 56rpx;
156
- font-size: 28rpx;
157
- color: $dd-text-secondary;
482
+ margin-right: $dd-space-3;
483
+ color: $dd-text-primary;
484
+ text-align: left;
485
+ word-wrap: break-word;
486
+
487
+ &--center {
488
+ justify-content: center;
489
+ }
490
+ &--right {
491
+ justify-content: flex-end;
492
+ }
158
493
  }
159
- &__required {
160
- color: $dd-error-500;
494
+
495
+ &__required-mark {
161
496
  margin-right: 4rpx;
497
+ color: $dd-error-500;
162
498
  }
499
+
163
500
  &__label-text {
164
- @include dd-ellipsis(1);
501
+ min-width: 0;
502
+ word-wrap: break-word;
165
503
  }
166
504
 
167
- &__body {
505
+ &__value {
168
506
  flex: 1;
169
507
  min-width: 0;
508
+ }
509
+
510
+ &__body {
511
+ display: flex;
512
+ align-items: center;
513
+ }
514
+
515
+ &__control-wrap {
170
516
  display: flex;
171
517
  align-items: center;
518
+ flex: 1;
519
+ min-width: 0;
520
+ min-height: 48rpx;
172
521
  }
173
522
 
174
523
  &__control {
175
524
  flex: 1;
176
525
  min-width: 0;
177
- font-size: 28rpx;
178
- color: $dd-text-primary;
526
+ width: 100%;
527
+ margin: 0;
528
+ padding: 0;
529
+ border: 0;
530
+ resize: none;
179
531
  background: transparent;
180
- border: none;
532
+ color: $dd-text-primary;
533
+ font-size: inherit;
534
+ line-height: inherit;
535
+
536
+ &::placeholder {
537
+ color: $dd-text-tertiary;
538
+ }
539
+
540
+ // readonly(原生 disabled 拦输入但无禁用样式):保持正常文本色
541
+ &:disabled {
542
+ color: $dd-text-primary;
543
+ opacity: 1;
544
+ }
181
545
  }
546
+
182
547
  &__textarea {
183
- padding: 16rpx 0;
184
- min-height: 60rpx;
185
- line-height: 1.5;
548
+ min-height: 48rpx;
186
549
  }
187
550
 
188
- &__clear {
189
- @include dd-flex-center;
551
+ &__left-icon {
552
+ display: flex;
553
+ align-items: center;
190
554
  flex-shrink: 0;
191
- width: 36rpx;
192
- height: 36rpx;
193
- margin-left: 16rpx;
194
- border-radius: $dd-radius-full;
195
- background: $dd-neutral-800;
196
- color: $dd-text-secondary;
197
- font-size: 24rpx;
198
- line-height: 1;
555
+ margin-right: $dd-space-1;
556
+ font-size: $dd-font-size-h4;
557
+ color: $dd-text-primary;
199
558
  }
200
559
 
201
- &__footer {
560
+ &__clear {
202
561
  display: flex;
203
562
  align-items: center;
204
- justify-content: flex-end;
205
- min-height: 40rpx;
206
- padding-bottom: 8rpx;
563
+ justify-content: center;
564
+ flex-shrink: 0;
565
+ margin-left: $dd-space-1;
566
+ padding: $dd-space-1 0 $dd-space-1 $dd-space-2;
567
+ font-size: $dd-font-size-h4;
568
+ color: $dd-text-tertiary;
207
569
  }
208
- &__error {
209
- flex: 1;
210
- font-size: 24rpx;
211
- color: $dd-error-500;
212
- @include dd-ellipsis(1);
570
+
571
+ &__right-icon {
572
+ display: flex;
573
+ align-items: center;
574
+ flex-shrink: 0;
575
+ margin-left: $dd-space-1;
576
+ font-size: $dd-font-size-h4;
577
+ color: $dd-text-tertiary;
213
578
  }
214
- &__counter {
579
+
580
+ &__arrow {
581
+ display: flex;
582
+ align-items: center;
215
583
  flex-shrink: 0;
216
- font-size: 24rpx;
584
+ margin-left: $dd-space-1;
585
+ font-size: $dd-font-size-h4;
217
586
  color: $dd-text-tertiary;
587
+
588
+ &--left {
589
+ transform: rotate(180deg);
590
+ }
591
+ &--up {
592
+ transform: rotate(-90deg);
593
+ }
594
+ &--down {
595
+ transform: rotate(90deg);
596
+ }
218
597
  }
219
598
 
220
- &--error {
221
- .dd-field__control {
222
- color: $dd-error-500;
599
+ &__button {
600
+ flex-shrink: 0;
601
+ margin-left: $dd-space-2;
602
+ display: flex;
603
+ align-items: center;
604
+ }
605
+
606
+ &__error-message {
607
+ font-size: $dd-font-size-caption;
608
+ line-height: 32rpx;
609
+ color: $dd-error-500;
610
+ text-align: left;
611
+
612
+ &--center {
613
+ text-align: center;
223
614
  }
224
- .dd-field__label-text {
615
+ &--right {
616
+ text-align: right;
617
+ }
618
+ }
619
+
620
+ &__word-limit {
621
+ display: block;
622
+ margin-top: $dd-space-1;
623
+ font-size: $dd-font-size-caption;
624
+ line-height: 32rpx;
625
+ color: $dd-text-secondary;
626
+ text-align: right;
627
+ }
628
+
629
+ // error:输入文字与 placeholder 变红,label 不变(vant 行为)
630
+ &--error {
631
+ .dd-field__control,
632
+ .dd-field__control::placeholder {
225
633
  color: $dd-error-500;
226
634
  }
227
635
  }
228
636
 
637
+ // disabled:仅文字变灰,不整体降透明度(vant 行为)
229
638
  &--disabled {
230
- opacity: 0.5;
231
- @include dd-no-touch;
639
+ .dd-field__label,
640
+ .dd-field__left-icon {
641
+ color: $dd-text-tertiary;
642
+ }
643
+
644
+ .dd-field__control,
645
+ .dd-field__control::placeholder {
646
+ color: $dd-text-tertiary;
647
+ }
232
648
  }
233
649
 
234
650
  &--borderless {
@@ -236,5 +652,14 @@ function onClear() {
236
652
  display: none;
237
653
  }
238
654
  }
655
+
656
+ &--active {
657
+ background: $dd-surface-container-high;
658
+ }
659
+
660
+ &__extra {
661
+ flex-basis: 100%;
662
+ margin-top: $dd-space-1;
663
+ }
239
664
  }
240
665
  </style>