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.
- package/attributes.json +1 -1
- package/components/hy-action-sheet/index.scss +2 -2
- package/components/hy-avatar/hy-avatar.vue +12 -5
- package/components/hy-avatar/index.scss +2 -2
- package/components/hy-avatar/props.ts +4 -2
- package/components/hy-avatar/typing.d.ts +8 -1
- package/components/hy-calendar/hy-calendar.vue +1 -0
- package/components/hy-calendar/month.vue +25 -5
- package/components/hy-calendar/props.ts +1 -1
- package/components/hy-cascader/hy-cascader.vue +503 -0
- package/components/hy-cascader/index.scss +84 -0
- package/components/hy-cascader/props.ts +88 -0
- package/components/hy-cascader/typing.d.ts +34 -0
- package/components/hy-cell-item/index.scss +8 -9
- package/components/hy-check-button/typing.d.ts +12 -7
- package/components/hy-code-input/hy-code-input.vue +33 -11
- package/components/hy-code-input/index.scss +2 -2
- package/components/hy-code-input/typing.d.ts +4 -0
- package/components/hy-config-provider/hy-config-provider.vue +58 -53
- package/components/hy-config-provider/props.ts +1 -1
- package/components/hy-datetime-picker/props.ts +1 -1
- package/components/hy-datetime-picker/typing.d.ts +1 -1
- package/components/hy-dropdown/typing.d.ts +5 -0
- package/components/hy-float-button/index.scss +1 -1
- package/components/hy-form/typing.d.ts +6 -0
- package/components/hy-form-item/typing.d.ts +1 -1
- package/components/hy-grid/hy-grid.vue +12 -12
- package/components/hy-grid/index.scss +1 -2
- package/components/hy-grid/props.ts +7 -2
- package/components/hy-grid/typing.d.ts +1 -1
- package/components/hy-icon/typing.d.ts +52 -9
- package/components/hy-index-bar/index.scss +1 -1
- package/components/hy-keyboard/constants.ts +83 -0
- package/components/hy-keyboard/hy-keyboard.vue +375 -0
- package/components/hy-keyboard/index.scss +73 -0
- package/components/hy-keyboard/key/index.scss +79 -0
- package/components/hy-keyboard/key/index.vue +91 -0
- package/components/hy-keyboard/key/types.ts +1 -0
- package/components/hy-keyboard/props.ts +108 -0
- package/components/hy-keyboard/typing.d.ts +18 -0
- package/components/hy-line-progress/index.scss +3 -3
- package/components/hy-menu/index.scss +1 -1
- package/components/hy-modal/index.scss +2 -2
- package/components/hy-modal/typing.d.ts +2 -2
- package/components/hy-notice-bar/hy-column-notice.vue +54 -94
- package/components/hy-notice-bar/hy-notice-bar.vue +128 -96
- package/components/hy-notice-bar/hy-row-notice.vue +89 -121
- package/components/hy-notice-bar/index.scss +1 -1
- package/components/hy-notice-bar/props.ts +86 -85
- package/components/hy-notify/hy-notify.vue +9 -9
- package/components/hy-notify/typing.d.ts +22 -0
- package/components/hy-picker/hy-picker.vue +126 -149
- package/components/hy-picker/props.ts +4 -1
- package/components/hy-picker/typing.d.ts +22 -0
- package/components/hy-popover/index.scss +6 -6
- package/components/hy-popup/index.scss +6 -5
- package/components/hy-price/hy-price.vue +52 -23
- package/components/hy-price/props.ts +54 -54
- package/components/hy-radio/props.ts +1 -1
- package/components/hy-radio/typing.d.ts +5 -1
- package/components/hy-rolling-num/typing.d.ts +1 -0
- package/components/hy-scroll-list/index.scss +2 -2
- package/components/hy-search/index.scss +1 -1
- package/components/hy-signature/index.scss +1 -1
- package/components/hy-skeleton/index.scss +21 -4
- package/components/hy-slider/index.scss +3 -3
- package/components/hy-steps/hy-steps.vue +1 -1
- package/components/hy-steps/index.scss +3 -3
- package/components/hy-submit-bar/typing.d.ts +3 -3
- package/components/hy-subsection/index.scss +8 -8
- package/components/hy-swipe-action/hy-swipe-action.vue +34 -15
- package/components/hy-swipe-action/index.scss +0 -1
- package/components/hy-swiper/index.scss +2 -2
- package/components/hy-switch/index.scss +3 -3
- package/components/hy-switch/typing.d.ts +4 -0
- package/components/hy-tabbar/props.ts +2 -1
- package/components/hy-tabbar/typing.d.ts +5 -4
- package/components/hy-tabbar-group/index.scss +2 -2
- package/components/hy-tabs/hy-tabs.vue +338 -336
- package/components/hy-tabs/index.scss +7 -7
- package/components/hy-tabs/typing.d.ts +41 -33
- package/components/hy-tag/index.scss +1 -1
- package/components/hy-textarea/hy-textarea.vue +2 -2
- package/components/hy-textarea/index.scss +4 -4
- package/components/hy-textarea/typing.d.ts +4 -0
- package/components/hy-toast/hy-toast.vue +14 -6
- package/components/hy-toast/index.scss +1 -1
- package/components/hy-toast/typing.d.ts +42 -36
- package/components/hy-tooltip/hy-tooltip.vue +1 -1
- package/components/hy-tooltip/index.scss +7 -6
- package/components/hy-tooltip/props.ts +1 -1
- package/components/hy-upload/hy-upload.vue +16 -1
- package/components/hy-upload/index.scss +149 -144
- package/components/hy-warn/index.scss +1 -1
- package/components/index.ts +6 -0
- package/global.d.ts +2 -0
- package/libs/config/icon.ts +442 -430
- package/libs/css/_function.scss +7 -13
- package/libs/css/_mixin.scss +3 -3
- package/libs/css/common.scss +1 -1
- package/libs/css/iconfont.css +445 -441
- package/libs/css/theme.scss +54 -25
- package/libs/locale/lang/en-US.ts +4 -0
- package/libs/locale/lang/zh-CN.ts +4 -0
- package/libs/typing/index.ts +80 -0
- package/package.json +5 -2
- package/tags.json +1 -1
- package/web-types.json +1 -1
- 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-
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
154
|
+
* 点击头像
|
|
155
155
|
* */
|
|
156
156
|
const clickHandler = (e: Event) => {
|
|
157
|
-
emit(
|
|
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-
|
|
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-
|
|
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',
|
|
10
|
+
(e: 'click', config: IAvatarClickConfig, e: Event): void
|
|
4
11
|
}
|
|
@@ -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
|
|
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
|
-
|
|
364
|
-
|
|
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')
|
|
@@ -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>
|