hy-app 0.6.8 → 0.7.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 (109) hide show
  1. package/attributes.json +1 -1
  2. package/components/hy-action-sheet/index.scss +2 -2
  3. package/components/hy-avatar/hy-avatar.vue +12 -5
  4. package/components/hy-avatar/index.scss +2 -2
  5. package/components/hy-avatar/props.ts +4 -2
  6. package/components/hy-avatar/typing.d.ts +8 -1
  7. package/components/hy-calendar/hy-calendar.vue +1 -0
  8. package/components/hy-calendar/month.vue +25 -5
  9. package/components/hy-calendar/props.ts +1 -1
  10. package/components/hy-cascader/hy-cascader.vue +503 -0
  11. package/components/hy-cascader/index.scss +84 -0
  12. package/components/hy-cascader/props.ts +88 -0
  13. package/components/hy-cascader/typing.d.ts +34 -0
  14. package/components/hy-cell-item/index.scss +8 -9
  15. package/components/hy-check-button/typing.d.ts +12 -7
  16. package/components/hy-code-input/hy-code-input.vue +33 -11
  17. package/components/hy-code-input/index.scss +2 -2
  18. package/components/hy-code-input/typing.d.ts +4 -0
  19. package/components/hy-config-provider/hy-config-provider.vue +58 -53
  20. package/components/hy-config-provider/props.ts +1 -1
  21. package/components/hy-datetime-picker/props.ts +1 -1
  22. package/components/hy-datetime-picker/typing.d.ts +1 -1
  23. package/components/hy-dropdown/typing.d.ts +5 -0
  24. package/components/hy-float-button/index.scss +1 -1
  25. package/components/hy-form/typing.d.ts +6 -0
  26. package/components/hy-form-item/typing.d.ts +1 -1
  27. package/components/hy-grid/hy-grid.vue +12 -12
  28. package/components/hy-grid/index.scss +1 -2
  29. package/components/hy-grid/props.ts +7 -2
  30. package/components/hy-grid/typing.d.ts +1 -1
  31. package/components/hy-icon/typing.d.ts +52 -9
  32. package/components/hy-index-bar/index.scss +1 -1
  33. package/components/hy-keyboard/constants.ts +83 -0
  34. package/components/hy-keyboard/hy-keyboard.vue +375 -0
  35. package/components/hy-keyboard/index.scss +73 -0
  36. package/components/hy-keyboard/key/index.scss +79 -0
  37. package/components/hy-keyboard/key/index.vue +91 -0
  38. package/components/hy-keyboard/key/types.ts +1 -0
  39. package/components/hy-keyboard/props.ts +108 -0
  40. package/components/hy-keyboard/typing.d.ts +18 -0
  41. package/components/hy-line-progress/index.scss +3 -3
  42. package/components/hy-menu/index.scss +1 -1
  43. package/components/hy-modal/index.scss +2 -2
  44. package/components/hy-modal/typing.d.ts +2 -2
  45. package/components/hy-notice-bar/hy-column-notice.vue +54 -94
  46. package/components/hy-notice-bar/hy-notice-bar.vue +128 -96
  47. package/components/hy-notice-bar/hy-row-notice.vue +89 -121
  48. package/components/hy-notice-bar/index.scss +1 -1
  49. package/components/hy-notice-bar/props.ts +86 -85
  50. package/components/hy-notify/hy-notify.vue +9 -9
  51. package/components/hy-notify/typing.d.ts +22 -0
  52. package/components/hy-picker/hy-picker.vue +126 -149
  53. package/components/hy-picker/props.ts +4 -1
  54. package/components/hy-picker/typing.d.ts +22 -0
  55. package/components/hy-popover/index.scss +6 -6
  56. package/components/hy-popup/index.scss +6 -5
  57. package/components/hy-price/hy-price.vue +52 -23
  58. package/components/hy-price/props.ts +54 -54
  59. package/components/hy-radio/props.ts +1 -1
  60. package/components/hy-radio/typing.d.ts +5 -1
  61. package/components/hy-rolling-num/typing.d.ts +1 -0
  62. package/components/hy-scroll-list/index.scss +2 -2
  63. package/components/hy-search/index.scss +1 -1
  64. package/components/hy-signature/index.scss +1 -1
  65. package/components/hy-skeleton/index.scss +21 -4
  66. package/components/hy-slider/index.scss +3 -3
  67. package/components/hy-steps/hy-steps.vue +1 -1
  68. package/components/hy-steps/index.scss +3 -3
  69. package/components/hy-submit-bar/typing.d.ts +3 -3
  70. package/components/hy-subsection/index.scss +8 -8
  71. package/components/hy-swipe-action/hy-swipe-action.vue +34 -15
  72. package/components/hy-swipe-action/index.scss +0 -1
  73. package/components/hy-swiper/index.scss +2 -2
  74. package/components/hy-switch/index.scss +3 -3
  75. package/components/hy-switch/typing.d.ts +4 -0
  76. package/components/hy-tabbar/props.ts +2 -1
  77. package/components/hy-tabbar/typing.d.ts +5 -4
  78. package/components/hy-tabbar-group/index.scss +2 -2
  79. package/components/hy-tabs/hy-tabs.vue +338 -336
  80. package/components/hy-tabs/index.scss +7 -7
  81. package/components/hy-tabs/typing.d.ts +41 -33
  82. package/components/hy-tag/index.scss +1 -1
  83. package/components/hy-textarea/hy-textarea.vue +2 -2
  84. package/components/hy-textarea/index.scss +4 -4
  85. package/components/hy-textarea/typing.d.ts +4 -0
  86. package/components/hy-toast/hy-toast.vue +14 -6
  87. package/components/hy-toast/index.scss +1 -1
  88. package/components/hy-toast/typing.d.ts +42 -36
  89. package/components/hy-tooltip/hy-tooltip.vue +1 -1
  90. package/components/hy-tooltip/index.scss +7 -6
  91. package/components/hy-tooltip/props.ts +1 -1
  92. package/components/hy-upload/hy-upload.vue +16 -1
  93. package/components/hy-upload/index.scss +149 -144
  94. package/components/hy-warn/index.scss +1 -1
  95. package/components/index.ts +6 -0
  96. package/global.d.ts +2 -0
  97. package/libs/config/icon.ts +442 -430
  98. package/libs/css/_function.scss +7 -13
  99. package/libs/css/_mixin.scss +3 -3
  100. package/libs/css/common.scss +1 -1
  101. package/libs/css/iconfont.css +445 -441
  102. package/libs/css/theme.scss +54 -25
  103. package/libs/locale/lang/en-US.ts +4 -0
  104. package/libs/locale/lang/zh-CN.ts +4 -0
  105. package/libs/typing/index.ts +80 -0
  106. package/package.json +5 -2
  107. package/tags.json +1 -1
  108. package/web-types.json +1 -1
  109. package/components/hy-qrcode/qrcode.js.bak +0 -1434
@@ -75,7 +75,7 @@
75
75
  top: 20px;
76
76
  right: 15px;
77
77
  background-color: rgba(128, 128, 128, 0.25);
78
- border-radius: $hy-border-radius-circle;
78
+ border-radius: $hy-radius-circle;
79
79
  padding: $hy-border-margin-padding-sm;
80
80
  }
81
81
 
@@ -112,7 +112,7 @@
112
112
  width: $hy-img-size-base;
113
113
  height: $hy-img-size-base;
114
114
  margin: 0 auto 7px;
115
- border-radius: $hy-border-radius-base;
115
+ border-radius: $hy-radius-base;
116
116
  }
117
117
 
118
118
  @include e(panel-title) {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <view class="hy-avatar" :class="avatarClass" :style="avatarStyle" @tap="clickHandler">
2
+ <view :class="avatarClass" :style="avatarStyle" @tap="clickHandler">
3
3
  <slot v-if="$slots.default"></slot>
4
4
  <template v-else>
5
5
  <!-- #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU -->
@@ -24,7 +24,7 @@
24
24
  justifyContent: 'center',
25
25
  textAlign: 'center',
26
26
  color: color,
27
- fontSize: fontSize
27
+ fontSize: addUnit(fontSize)
28
28
  }"
29
29
  >
30
30
  {{ text }}
@@ -127,7 +127,7 @@ const avatarStyle = computed<CSSProperties>(() => {
127
127
  return Object.assign(style, props.customStyle)
128
128
  })
129
129
  const avatarClass = computed<string[]>(() => {
130
- const classes: string[] = [`hy-avatar--${props.shape}`]
130
+ const classes: string[] = ['hy-avatar', `hy-avatar--${props.shape}`, props.customClass || '']
131
131
  if (typeof props.size === 'string') {
132
132
  classes.push(`hy-avatar--${props.size}`)
133
133
  }
@@ -151,10 +151,17 @@ function errorHandler() {
151
151
  }
152
152
 
153
153
  /**
154
- * @description 点击头像
154
+ * 点击头像
155
155
  * */
156
156
  const clickHandler = (e: Event) => {
157
- emit('click', props.name, e)
157
+ emit(
158
+ 'click',
159
+ {
160
+ name: props.name,
161
+ url: avatarUrl.value
162
+ },
163
+ e
164
+ )
158
165
  }
159
166
  </script>
160
167
 
@@ -7,7 +7,7 @@
7
7
  justify-content: center;
8
8
 
9
9
  @include m(circle) {
10
- border-radius: $hy-border-radius-circle;
10
+ border-radius: $hy-radius-circle;
11
11
  }
12
12
 
13
13
  @include m(square) {
@@ -34,7 +34,7 @@
34
34
  height: 100%;
35
35
 
36
36
  @include m(circle) {
37
- border-radius: $hy-border-radius-circle;
37
+ border-radius: $hy-radius-circle;
38
38
  overflow: hidden;
39
39
  }
40
40
 
@@ -41,7 +41,7 @@ const avatarProps = {
41
41
  },
42
42
  /** 文字大小 */
43
43
  fontSize: {
44
- type: Number,
44
+ type: [String, Number],
45
45
  default: 18
46
46
  },
47
47
  /** 显示的图标 */
@@ -72,7 +72,9 @@ const avatarProps = {
72
72
  customStyle: {
73
73
  type: Object as PropType<CSSProperties>,
74
74
  default: () => {}
75
- }
75
+ },
76
+ /** 自定义外部类名 */
77
+ customClass: String
76
78
  }
77
79
 
78
80
  export default avatarProps
@@ -1,4 +1,11 @@
1
+ export interface IAvatarClickConfig {
2
+ /** 组件标识符 */
3
+ name: string
4
+ /** 头像图片url */
5
+ url?: string
6
+ }
7
+
1
8
  export interface IAvatarEmit {
2
9
  /** 点击头像触发 */
3
- (e: 'click', name: string, e: Event): void
10
+ (e: 'click', config: IAvatarClickConfig, e: Event): void
4
11
  }
@@ -5,6 +5,7 @@
5
5
  closeable
6
6
  @close="close"
7
7
  :round="round"
8
+ :z-index="998"
8
9
  :closeOnClickOverlay="closeOnClickOverlay"
9
10
  >
10
11
  <view class="hy-calendar">
@@ -312,9 +312,9 @@ const clickHandler = (item: DateItem) => {
312
312
  if (dayjs(dateStr).isBefore(dayjs(startDate))) {
313
313
  newSelected = [dateStr]
314
314
  } else if (dayjs(dateStr).isAfter(dayjs(startDate))) {
315
- // 范围限制校验
316
- const daysDiff = dayjs(dateStr).diff(dayjs(startDate), 'day')
317
- if (daysDiff >= (props.maxRange as number) && props.showRangePrompt) {
315
+ // 范围限制校验(计算包含两端的总天数)
316
+ const daysDiff = dayjs(dateStr).diff(dayjs(startDate), 'day') + 1
317
+ if (daysDiff > (props.maxRange as number) && props.showRangePrompt) {
318
318
  uni.showToast({
319
319
  title: props.rangePrompt || `选择天数不能超过 ${props.maxRange} 天`,
320
320
  icon: 'none'
@@ -359,9 +359,29 @@ const setDefaultDate = () => {
359
359
  dates = Array.isArray(props.defaultDate)
360
360
  ? [props.defaultDate[0]]
361
361
  : [dayjs(props.defaultDate).format('YYYY-MM-DD')]
362
+ } else if (props.mode === 'range') {
363
+ if (Array.isArray(props.defaultDate) && props.defaultDate.length >= 2) {
364
+ // range 模式下,如果默认日期是数组且包含两个日期,填充中间所有日期
365
+ const startDate = dayjs(props.defaultDate[0]).format('YYYY-MM-DD')
366
+ const endDate = dayjs(props.defaultDate[1]).format('YYYY-MM-DD')
367
+ const arr = []
368
+ let curr = dayjs(startDate)
369
+ const end = dayjs(endDate)
370
+ while (curr.isBefore(end) || curr.isSame(end, 'day')) {
371
+ arr.push(curr.format('YYYY-MM-DD'))
372
+ curr = curr.add(1, 'day')
373
+ }
374
+ dates = arr
375
+ } else if (Array.isArray(props.defaultDate)) {
376
+ dates = props.defaultDate
377
+ }
362
378
  } else {
363
- if (!Array.isArray(props.defaultDate)) return
364
- dates = props.defaultDate
379
+ // multiple 模式
380
+ if (Array.isArray(props.defaultDate)) {
381
+ dates = props.defaultDate
382
+ } else {
383
+ dates = [dayjs(props.defaultDate).format('YYYY-MM-DD')]
384
+ }
365
385
  }
366
386
 
367
387
  const min = props.minDate || dayjs().format('YYYY-MM-DD')
@@ -156,7 +156,7 @@ const calendarProps = {
156
156
  },
157
157
  /** 单选与多选禁止选中的日期列表,mode!=range时有效。 */
158
158
  forbidDays: {
159
- type: Array as PropType<Date[]>,
159
+ type: Array as PropType<string[]>,
160
160
  default: () => []
161
161
  },
162
162
  /** 单选与多选禁止选中的日期选择时提示 */
@@ -0,0 +1,503 @@
1
+ <template>
2
+ <view class="hy-cascader">
3
+ <view v-if="hasInput" class="hy-cascader-input cursor-pointer" @click="onShowByClickInput">
4
+ <slot v-if="$slots.default"></slot>
5
+ <template v-else>
6
+ <hy-input
7
+ v-model="inputLabelValue"
8
+ :disabled="input?.disabled"
9
+ :disabledColor="input?.disabledColor"
10
+ :shape="input?.shape"
11
+ :border="input?.border"
12
+ :prefixIcon="input?.prefixIcon"
13
+ :suffixIcon="input?.suffixIcon"
14
+ :color="input?.color"
15
+ :fontSize="input?.fontSize"
16
+ :inputAlign="input?.inputAlign"
17
+ :placeholder="input?.placeholder || placeholder"
18
+ :placeholderStyle="input?.placeholderStyle"
19
+ :placeholderClass="input?.placeholderClass"
20
+ :customStyle="Object.assign({ 'pointer-events': 'none' }, input?.customStyle)"
21
+ ></hy-input>
22
+ <view class="input-cover"></view>
23
+ </template>
24
+ </view>
25
+ <hy-popup
26
+ :show="show || (hasInput && showByClickInput)"
27
+ :zIndex="zIndex"
28
+ :round="10"
29
+ :closeOnClickOverlay="closeOnClickOverlay"
30
+ @close="cancel"
31
+ @open="onOpen"
32
+ >
33
+ <view class="hy-cascader">
34
+ <view class="hy-cascader__header" v-if="showToolbar">
35
+ <view class="hy-cascader__header-title">{{ title || t('placeholder') }}</view>
36
+ <view class="hy-cascader__header-close" @tap="cancel">
37
+ <hy-icon name="close" size="20" />
38
+ </view>
39
+ </view>
40
+
41
+ <hy-tabs
42
+ ref="tabsRef"
43
+ v-model:current="activeTab"
44
+ :list="tabsList"
45
+ :is-swiper="true"
46
+ swiperHeight="700rpx"
47
+ :line-width="40"
48
+ >
49
+ <template #default="{ index: tabIndex }">
50
+ <view class="hy-cascader__panel">
51
+ <scroll-view
52
+ v-if="!loadingTabs.includes(tabIndex)"
53
+ scroll-y
54
+ class="hy-cascader__content"
55
+ >
56
+ <view
57
+ v-for="(item, index) in getOptionsByLevel(tabIndex)"
58
+ :key="item[valueKey]"
59
+ :class="[
60
+ 'hy-cascader__item',
61
+ isItemSelected(tabIndex, index) &&
62
+ 'hy-cascader__item--active'
63
+ ]"
64
+ @tap="handleItemClick(tabIndex, index)"
65
+ >
66
+ {{ item[labelKey] }}
67
+ <hy-icon
68
+ v-if="
69
+ isItemSelected(tabIndex, index) &&
70
+ !item[childrenKey]?.length &&
71
+ !item[isLeafKey]
72
+ "
73
+ :name="IconConfig.CHECK_MASK"
74
+ :color="themeConfig?.themeColor.value"
75
+ />
76
+ </view>
77
+ </scroll-view>
78
+ <hy-loading
79
+ :show="loadingTabs.includes(tabIndex)"
80
+ mode="circle"
81
+ custom-class="hy-cascader__loading"
82
+ ></hy-loading>
83
+ </view>
84
+ </template>
85
+ </hy-tabs>
86
+ </view>
87
+ </hy-popup>
88
+ </view>
89
+ </template>
90
+
91
+ <script lang="ts">
92
+ export default {
93
+ name: 'hy-cascader',
94
+ options: {
95
+ addGlobalClass: true,
96
+ virtualHost: true,
97
+ styleIsolation: 'shared'
98
+ }
99
+ }
100
+ </script>
101
+
102
+ <script setup lang="ts">
103
+ import { computed, ref, watch, inject } from 'vue'
104
+ import type { CascaderOption, CascaderValue, CascaderEmitValue } from './typing'
105
+ import type { ICascaderEmits } from './typing'
106
+ import type { ITabsExpose, TabsItemVo } from '../hy-tabs/typing'
107
+ import cascaderProps from './props'
108
+ import { useTranslate, IconConfig } from '../../libs'
109
+ // 组件
110
+ import HyTabs from '../hy-tabs/hy-tabs.vue'
111
+ import HyPopup from '../hy-popup/hy-popup.vue'
112
+ import HyIcon from '../hy-icon/hy-icon.vue'
113
+ import HyLoading from '../hy-loading/hy-loading.vue'
114
+ import HyInput from '../hy-input/hy-input.vue'
115
+
116
+ const { t } = useTranslate('cascader')
117
+
118
+ defineOptions({})
119
+
120
+ const props = defineProps(cascaderProps)
121
+ const emit = defineEmits<ICascaderEmits>()
122
+
123
+ const showByClickInput = ref(false)
124
+ const activePath = ref<number[]>([])
125
+ const activeTab = ref(0)
126
+ const themeConfig = inject('hy-config-provider', null)
127
+ const innerOptions = ref<CascaderOption[]>([...props.options])
128
+ const loadingTabs = ref<number[]>([])
129
+ const isRestoring = ref(false)
130
+ const tabsRef = ref<ITabsExpose>()
131
+ console.log(themeConfig)
132
+
133
+ /**
134
+ * 生成标签页列表,用于显示已选择的路径层级
135
+ * @returns {TabsItemVo[]} 标签页列表
136
+ */
137
+ const tabsList = computed((): TabsItemVo[] => {
138
+ const tabs: TabsItemVo[] = [{ name: '请选择' }]
139
+ let currentOptions = innerOptions.value
140
+
141
+ for (let i = 0; i < activePath.value.length; i++) {
142
+ const index = activePath.value[i]
143
+ const selected = currentOptions[index]
144
+ if (selected) {
145
+ tabs[i] = { name: selected[props.labelKey] }
146
+ if (selected[props.childrenKey]?.length) {
147
+ tabs[i + 1] = { name: '请选择' }
148
+ }
149
+ currentOptions = selected[props.childrenKey] || []
150
+ }
151
+ }
152
+
153
+ return tabs.slice(0, activePath.value.length + 1)
154
+ })
155
+
156
+ /**
157
+ * 根据层级获取对应级别的选项列表
158
+ * @param {number} level - 层级索引
159
+ * @returns {CascaderOption[]} 对应层级的选项列表
160
+ */
161
+ const getOptionsByLevel = (level: number): CascaderOption[] => {
162
+ if (level === 0) {
163
+ return innerOptions.value
164
+ }
165
+
166
+ let options = innerOptions.value
167
+ for (let i = 0; i < level; i++) {
168
+ const index = activePath.value[i]
169
+ if (options[index]?.[props.childrenKey]) {
170
+ options = options[index][props.childrenKey]
171
+ } else {
172
+ return []
173
+ }
174
+ }
175
+ return options
176
+ }
177
+
178
+ /**
179
+ * DFS 查找某父节点的已缓存子节点
180
+ * @param {string | number} parentValue - 父节点的值
181
+ * @returns {CascaderOption[] | null} 缓存的子节点列表
182
+ */
183
+ const getCachedChildren = (parentValue: string | number): CascaderOption[] | null => {
184
+ const { valueKey, childrenKey } = props
185
+
186
+ function dfs(options: CascaderOption[]): CascaderOption[] | null {
187
+ for (const option of options) {
188
+ if (option[valueKey] === parentValue) {
189
+ const children = option[childrenKey]
190
+ return Array.isArray(children) ? children : null
191
+ }
192
+ if (option[childrenKey] && option[childrenKey].length > 0) {
193
+ const result = dfs(option[childrenKey])
194
+ if (result) return result
195
+ }
196
+ }
197
+ return null
198
+ }
199
+
200
+ return innerOptions.value.length > 0 ? dfs(innerOptions.value) : null
201
+ }
202
+
203
+ /**
204
+ * 将异步加载的子节点挂载到缓存树中对应父节点下
205
+ * @param {string | number} parentValue - 父节点的值
206
+ * @param {CascaderOption[]} children - 子节点列表
207
+ */
208
+ const attachChildrenToTree = (parentValue: string | number, children: CascaderOption[]) => {
209
+ const { valueKey, childrenKey } = props
210
+
211
+ function dfs(options: CascaderOption[]): boolean {
212
+ for (const option of options) {
213
+ if (option[valueKey] === parentValue) {
214
+ option[childrenKey] = children
215
+ return true
216
+ }
217
+ if (option[childrenKey] && option[childrenKey].length > 0) {
218
+ if (dfs(option[childrenKey])) return true
219
+ }
220
+ }
221
+ return false
222
+ }
223
+
224
+ dfs(innerOptions.value)
225
+ }
226
+
227
+ /**
228
+ * 判断指定层级的选项是否被选中
229
+ * @param {number} level - 层级索引
230
+ * @param {number} index - 选项索引
231
+ * @returns {boolean} 是否被选中
232
+ */
233
+ const isItemSelected = (level: number, index: number): boolean => {
234
+ if (level >= activePath.value.length) return false
235
+ return activePath.value[level] === index
236
+ }
237
+
238
+ /**
239
+ * 根据 modelValue 初始化选中路径(静态模式)
240
+ */
241
+ const initActivePath = () => {
242
+ if (!props.modelValue || !props.modelValue.value.length) {
243
+ activePath.value = []
244
+ return
245
+ }
246
+
247
+ const path: number[] = []
248
+ let options = innerOptions.value
249
+
250
+ for (const targetValue of props.modelValue.value) {
251
+ const index = options.findIndex((opt) => opt[props.valueKey] === targetValue)
252
+ if (index === -1) break
253
+ path.push(index)
254
+ options = options[index]?.[props.childrenKey] || []
255
+ }
256
+
257
+ activePath.value = path
258
+ activeTab.value = path.length > 0 ? path.length - 1 : 0
259
+ }
260
+
261
+ /**
262
+ * 异步模式下根据路径数组逐级调用 lazyLoad 恢复选中状态
263
+ * @param {(string | number)[]} pathValues - 路径值数组
264
+ */
265
+ const autoRestoreFromPath = (pathValues: (string | number)[]) => {
266
+ const { lazyLoad, valueKey } = props
267
+ if (!lazyLoad || pathValues.length === 0) return
268
+
269
+ isRestoring.value = true
270
+
271
+ function restoreLevel(level: number, parentOption: CascaderOption | null) {
272
+ const targetValue = pathValues[level]
273
+ const isLastLevel = level === pathValues.length - 1
274
+
275
+ let cachedOptions: CascaderOption[] | null = null
276
+ if (level === 0) {
277
+ cachedOptions = innerOptions.value.length > 0 ? innerOptions.value : null
278
+ } else if (parentOption) {
279
+ cachedOptions = getCachedChildren(parentOption[valueKey])
280
+ }
281
+
282
+ if (cachedOptions) {
283
+ const matchedIndex = cachedOptions.findIndex((opt) => opt[valueKey] === targetValue)
284
+ activePath.value = activePath.value.slice(0, level)
285
+ if (matchedIndex !== -1) {
286
+ activePath.value.push(matchedIndex)
287
+ }
288
+ activeTab.value = Math.max(activePath.value.length - 1, 0)
289
+
290
+ if (matchedIndex === -1 || isLastLevel) {
291
+ isRestoring.value = false
292
+ return
293
+ }
294
+ restoreLevel(level + 1, cachedOptions[matchedIndex])
295
+ } else {
296
+ const tabIndex = level
297
+ loadingTabs.value = [...loadingTabs.value, tabIndex]
298
+
299
+ lazyLoad!(parentOption, tabIndex, (children) => {
300
+ loadingTabs.value = loadingTabs.value.filter((i) => i !== tabIndex)
301
+
302
+ if (level === 0) {
303
+ innerOptions.value = children
304
+ } else if (parentOption) {
305
+ attachChildrenToTree(parentOption[valueKey], children)
306
+ }
307
+
308
+ if (children.length === 0) {
309
+ isRestoring.value = false
310
+ return
311
+ }
312
+
313
+ const matchedIndex = children.findIndex((opt) => opt[valueKey] === targetValue)
314
+ activePath.value = activePath.value.slice(0, level)
315
+ if (matchedIndex !== -1) {
316
+ activePath.value.push(matchedIndex)
317
+ }
318
+ activeTab.value = Math.max(activePath.value.length - 1, 0)
319
+
320
+ if (matchedIndex === -1 || isLastLevel) {
321
+ isRestoring.value = false
322
+ return
323
+ }
324
+ restoreLevel(level + 1, children[matchedIndex])
325
+ })
326
+ }
327
+ }
328
+
329
+ restoreLevel(0, null)
330
+ }
331
+
332
+ watch(
333
+ () => props.options,
334
+ (newOptions) => {
335
+ innerOptions.value = [...newOptions]
336
+ if (!props.lazyLoad) {
337
+ initActivePath()
338
+ }
339
+ },
340
+ { deep: true, immediate: true }
341
+ )
342
+
343
+ watch(
344
+ () => props.modelValue,
345
+ () => {
346
+ if (!props.lazyLoad) {
347
+ initActivePath()
348
+ } else if (props.modelValue && props.modelValue.value.length > 0) {
349
+ autoRestoreFromPath(props.modelValue.value)
350
+ }
351
+ },
352
+ { deep: true }
353
+ )
354
+
355
+ watch(
356
+ () => showByClickInput.value || props.show,
357
+ (val) => {
358
+ if (val && props.lazyLoad && innerOptions.value.length === 0) {
359
+ loadingTabs.value = [0]
360
+ props.lazyLoad(null, 0, (children) => {
361
+ innerOptions.value = children
362
+ loadingTabs.value = loadingTabs.value.filter((i) => i !== 0)
363
+ })
364
+ }
365
+ }
366
+ )
367
+
368
+ /**
369
+ * 处理选项点击事件
370
+ * @param {number} level - 当前层级
371
+ * @param {number} index - 点击的选项索引
372
+ */
373
+ const handleItemClick = (level: number, index: number) => {
374
+ const options = getOptionsByLevel(level)
375
+ const item = options[index]
376
+ if (item.disabled) return
377
+
378
+ activePath.value = activePath.value.slice(0, level)
379
+ activePath.value.push(index)
380
+ activeTab.value = level
381
+
382
+ const { childrenKey, lazyLoad, isLeafKey, valueKey } = props
383
+
384
+ if (item[isLeafKey] === true) {
385
+ onConfirm()
386
+ return
387
+ }
388
+
389
+ if (lazyLoad) {
390
+ const cachedChildren = getCachedChildren(item[valueKey])
391
+
392
+ loadingTabs.value = [...loadingTabs.value, level]
393
+
394
+ if (cachedChildren !== null) {
395
+ setTimeout(() => {
396
+ loadingTabs.value = loadingTabs.value.filter((i) => i !== level)
397
+ activeTab.value = level + 1
398
+ }, 300)
399
+ } else {
400
+ const nextTabIndex = level + 1
401
+ lazyLoad(item, nextTabIndex, (children) => {
402
+ loadingTabs.value = loadingTabs.value.filter((i) => i !== level)
403
+ attachChildrenToTree(item[valueKey], children)
404
+
405
+ if (children.length === 0) {
406
+ onConfirm()
407
+ } else {
408
+ activeTab.value = nextTabIndex
409
+ }
410
+ })
411
+ }
412
+ return
413
+ }
414
+
415
+ if (!item[childrenKey]?.length) {
416
+ onConfirm()
417
+ return
418
+ }
419
+
420
+ activeTab.value = level + 1
421
+ }
422
+
423
+ /**
424
+ * 获取要发射的选中值对象
425
+ * @returns {CascaderEmitValue} 包含 value、label、selectedOptions 的对象
426
+ */
427
+ const getEmitValue = (): CascaderEmitValue => {
428
+ const value: (string | number)[] = []
429
+ const label: string[] = []
430
+ const selectedOptions: CascaderOption[] = []
431
+
432
+ let options = innerOptions.value
433
+
434
+ for (const index of activePath.value) {
435
+ const selected = options[index]
436
+ if (selected) {
437
+ value.push(selected[props.valueKey])
438
+ label.push(selected[props.labelKey])
439
+ selectedOptions.push(selected)
440
+ options = selected[props.childrenKey] || []
441
+ }
442
+ }
443
+
444
+ return { value, label, selectedOptions }
445
+ }
446
+
447
+ /**
448
+ * 计算输入框显示的标签值,将选中的标签用分隔符连接
449
+ * @returns {string} 输入框显示的文本
450
+ */
451
+ const inputLabelValue = computed((): string => {
452
+ if (!props.modelValue || !props.modelValue.label.length) return ''
453
+ return props.modelValue.label.join(props.separator)
454
+ })
455
+
456
+ /**
457
+ * 处理输入框点击事件,切换弹窗显示状态
458
+ */
459
+ const onShowByClickInput = () => {
460
+ if (!props.input?.disabled) {
461
+ showByClickInput.value = !showByClickInput.value
462
+ }
463
+ }
464
+
465
+ /**
466
+ * 取消选择,关闭弹窗
467
+ */
468
+ const cancel = () => {
469
+ if (props.hasInput) {
470
+ showByClickInput.value = false
471
+ }
472
+ emit('update:show', false)
473
+ emit('cancel')
474
+ }
475
+
476
+ /**
477
+ * 确认选择,关闭弹窗并发射选中值
478
+ */
479
+ const onConfirm = () => {
480
+ const emitValue = getEmitValue()
481
+ const modelValue: CascaderValue = {
482
+ value: emitValue.value,
483
+ label: emitValue.label
484
+ }
485
+ emit('update:modelValue', modelValue)
486
+ if (props.hasInput) {
487
+ showByClickInput.value = false
488
+ }
489
+ emit('update:show', false)
490
+ emit('confirm', emitValue)
491
+ }
492
+
493
+ /**
494
+ * 打开弹窗执行函数
495
+ */
496
+ const onOpen = () => {
497
+ tabsRef.value?.resize()
498
+ }
499
+ </script>
500
+
501
+ <style lang="scss" scoped>
502
+ @import './index.scss';
503
+ </style>