@zfqh/uview-x 0.0.0-stage → 0.0.1
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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +127 -3
- package/components/uv-badge/props.ts +83 -0
- package/components/uv-badge/uv-badge.vue +186 -0
- package/components/uv-button/props.ts +148 -0
- package/components/uv-button/uv-button.vue +565 -0
- package/components/uv-checkbox/props.ts +83 -0
- package/components/uv-checkbox/uv-checkbox.vue +403 -0
- package/components/uv-checkbox-group/props.ts +97 -0
- package/components/uv-checkbox-group/uv-checkbox-group.vue +163 -0
- package/components/uv-code/props.ts +48 -0
- package/components/uv-code/uv-code.vue +156 -0
- package/components/uv-dialog/props.ts +56 -0
- package/components/uv-dialog/uv-dialog.vue +492 -0
- package/components/uv-form/props.ts +59 -0
- package/components/uv-form/uv-form.vue +256 -0
- package/components/uv-form/valid.ts +1344 -0
- package/components/uv-form-item/props.ts +62 -0
- package/components/uv-form-item/uv-form-item.vue +335 -0
- package/components/uv-icon/icons.ts +161 -0
- package/components/uv-icon/props.ts +91 -0
- package/components/uv-icon/uv-icon.vue +204 -0
- package/components/uv-icon/uvicons.ttf +0 -0
- package/components/uv-input/props.ts +185 -0
- package/components/uv-input/uv-input.vue +431 -0
- package/components/uv-line/props.ts +42 -0
- package/components/uv-line/uv-line.vue +75 -0
- package/components/uv-line-progress/props.ts +43 -0
- package/components/uv-line-progress/uv-line-progress.vue +166 -0
- package/components/uv-loading-icon/props.ts +78 -0
- package/components/uv-loading-icon/uv-loading-icon.vue +226 -0
- package/components/uv-overlay/props.ts +38 -0
- package/components/uv-overlay/uv-overlay.vue +120 -0
- package/components/uv-popup/props.ts +101 -0
- package/components/uv-popup/uv-popup.vue +371 -0
- package/components/uv-safe-bottom/props.ts +18 -0
- package/components/uv-safe-bottom/uv-safe-bottom.vue +120 -0
- package/components/uv-status-bar/props.ts +22 -0
- package/components/uv-status-bar/uv-status-bar.vue +121 -0
- package/components/uv-tabs/props.ts +84 -0
- package/components/uv-tabs/uv-tabs.vue +432 -0
- package/components/uv-transition/props.ts +38 -0
- package/components/uv-transition/uv-transition.vue +170 -0
- package/hooks/index.ts +5 -0
- package/hooks/useDebounce.ts +35 -0
- package/hooks/useI18n.ts +95 -0
- package/hooks/useThrottle.ts +38 -0
- package/index.ts +23 -0
- package/libs/dayjs/index.ts +217 -0
- package/libs/index.ts +4 -0
- package/libs/luch-request/adapters/index.ts +133 -0
- package/libs/luch-request/core/InterceptorManager.ts +52 -0
- package/libs/luch-request/core/Request.ts +202 -0
- package/libs/luch-request/core/buildFullPath.ts +21 -0
- package/libs/luch-request/core/defaults.ts +34 -0
- package/libs/luch-request/core/dispatchRequest.ts +7 -0
- package/libs/luch-request/core/mergeConfig.ts +127 -0
- package/libs/luch-request/core/settle.ts +17 -0
- package/libs/luch-request/helpers/buildURL.ts +65 -0
- package/libs/luch-request/helpers/combineURLs.ts +15 -0
- package/libs/luch-request/helpers/isAbsoluteURL.ts +15 -0
- package/libs/luch-request/index.d.ts +197 -0
- package/libs/luch-request/index.ts +3 -0
- package/libs/luch-request/utils/clone.ts +265 -0
- package/libs/luch-request/utils.ts +136 -0
- package/locales/en.ts +78 -0
- package/locales/index.ts +169 -0
- package/locales/zh-Hans.ts +78 -0
- package/package.json +42 -4
- package/styles/color.scss +33 -0
- package/styles/common.scss +134 -0
- package/styles/components.scss +19 -0
- package/styles/index.scss +7 -0
- package/styles/theme.scss +44 -0
- package/styles/var.module.scss +32 -0
- package/styles/vue.scss +39 -0
- package/theme.scss +2 -0
- package/types/index.d.ts +66 -0
- package/utils/colorGradient.ts +139 -0
- package/utils/compat.ts +165 -0
- package/utils/compress.ts +34 -0
- package/utils/config.ts +95 -0
- package/utils/debounce.ts +7 -0
- package/utils/digit.ts +157 -0
- package/utils/function.ts +771 -0
- package/utils/http.ts +5 -0
- package/utils/index.ts +57 -0
- package/utils/locale.ts +296 -0
- package/utils/platform.ts +71 -0
- package/utils/route.ts +142 -0
- package/utils/test.ts +256 -0
- package/utils/throttle.ts +7 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view
|
|
3
|
+
class="uv-status-bar"
|
|
4
|
+
:class="[p.customClass]"
|
|
5
|
+
:style="[statusBarStyle]"
|
|
6
|
+
>
|
|
7
|
+
<slot />
|
|
8
|
+
</view>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { computed, onMounted, ref } from 'vue'
|
|
13
|
+
import { $uv } from '../../utils'
|
|
14
|
+
import props from './props'
|
|
15
|
+
|
|
16
|
+
defineOptions({
|
|
17
|
+
name: 'uv-status-bar',
|
|
18
|
+
// #ifdef UNI-APP-X
|
|
19
|
+
styleIsolation: 'app-and-page',
|
|
20
|
+
// #endif
|
|
21
|
+
virtualHost: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* StatusBar 状态栏占位
|
|
26
|
+
* @description 本组件主要用于状态栏填充,比如在自定义导航栏的时候,它会自动适配一个恰当的状态栏高度。
|
|
27
|
+
* App 蒸汽模式使用 uni.getWindowInfo() 的 statusBarHeight 计算高度。
|
|
28
|
+
* @tutorial https://www.uvui.cn/components/statusBar.html
|
|
29
|
+
* @property {String} bgColor 背景色 (默认 'transparent' )
|
|
30
|
+
* @property {Object} customStyle 自定义样式
|
|
31
|
+
* @property {String} customClass 自定义类名
|
|
32
|
+
* @example <uv-status-bar></uv-status-bar>
|
|
33
|
+
*/
|
|
34
|
+
const p = defineProps(props)
|
|
35
|
+
|
|
36
|
+
// uni-app x:窗口信息用 getWindowInfo()、设备信息用 getDeviceInfo();
|
|
37
|
+
// 旧基座缺 statusBarHeight 时回退 getSystemInfoSync() / safeArea 字段。
|
|
38
|
+
const systemInfo = ref<any>({})
|
|
39
|
+
const readSystemInfo = () => {
|
|
40
|
+
const info: any = {}
|
|
41
|
+
try {
|
|
42
|
+
const windowInfo: any = uni.getWindowInfo()
|
|
43
|
+
info.statusBarHeight = windowInfo.statusBarHeight
|
|
44
|
+
info.safeAreaInsets = windowInfo.safeAreaInsets
|
|
45
|
+
info.safeArea = windowInfo.safeArea
|
|
46
|
+
} catch (e) {}
|
|
47
|
+
try {
|
|
48
|
+
const deviceInfo: any = uni.getDeviceInfo()
|
|
49
|
+
info.osName = deviceInfo.osName
|
|
50
|
+
info.deviceType = deviceInfo.deviceType
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
if (info.statusBarHeight === undefined) {
|
|
53
|
+
try {
|
|
54
|
+
const legacyInfo: any = uni.getSystemInfoSync()
|
|
55
|
+
if (info.statusBarHeight === undefined) info.statusBarHeight = legacyInfo.statusBarHeight
|
|
56
|
+
if (info.safeAreaInsets === undefined) info.safeAreaInsets = legacyInfo.safeAreaInsets
|
|
57
|
+
if (info.safeArea === undefined) info.safeArea = legacyInfo.safeArea
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
}
|
|
60
|
+
systemInfo.value = info
|
|
61
|
+
}
|
|
62
|
+
readSystemInfo()
|
|
63
|
+
|
|
64
|
+
onMounted(() => {
|
|
65
|
+
readSystemInfo()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/** 状态栏高度:getWindowInfo().statusBarHeight,缺失时依次用 safeAreaInsets.top / safeArea.top 兜底 */
|
|
69
|
+
const statusBarHeight = computed(() => {
|
|
70
|
+
const info = systemInfo.value || {}
|
|
71
|
+
const height = Number(info.statusBarHeight)
|
|
72
|
+
if (height > 0) return height
|
|
73
|
+
const insets = info.safeAreaInsets
|
|
74
|
+
if (insets && typeof insets.top === 'number' && insets.top > 0) return insets.top
|
|
75
|
+
// #ifdef APP || MP
|
|
76
|
+
const safeArea = info.safeArea
|
|
77
|
+
if (safeArea && safeArea.top > 0) return safeArea.top
|
|
78
|
+
// #endif
|
|
79
|
+
return 0
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const statusBarStyle = computed(() => {
|
|
83
|
+
const style: any = {}
|
|
84
|
+
const height = statusBarHeight.value
|
|
85
|
+
if (height > 0) {
|
|
86
|
+
style.height = $uv.addUnit(height, 'px')
|
|
87
|
+
}
|
|
88
|
+
if (p.bgColor) {
|
|
89
|
+
if (p.bgColor.indexOf('gradient') > -1) {
|
|
90
|
+
// #ifdef APP-NVUE
|
|
91
|
+
style.backgroundColor = '#ffffff'
|
|
92
|
+
// #endif
|
|
93
|
+
// #ifndef APP-NVUE
|
|
94
|
+
style.backgroundImage = p.bgColor
|
|
95
|
+
// #endif
|
|
96
|
+
} else {
|
|
97
|
+
style.background = p.bgColor
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return $uv.deepMerge(style, $uv.addStyle(p.customStyle))
|
|
101
|
+
})
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<style lang="scss">
|
|
105
|
+
@import '../../styles/components.scss';
|
|
106
|
+
|
|
107
|
+
.uv-status-bar {
|
|
108
|
+
/* #ifndef APP-NVUE */
|
|
109
|
+
width: 100%;
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
/* #endif */
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* #ifndef APP-NVUE */
|
|
115
|
+
/* #ifdef APP || MP */
|
|
116
|
+
.uv-status-bar {
|
|
117
|
+
height: var(--status-bar-height);
|
|
118
|
+
}
|
|
119
|
+
/* #endif */
|
|
120
|
+
/* #endif */
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
import config from '../../utils/config';
|
|
4
|
+
|
|
5
|
+
const props = {
|
|
6
|
+
// 滑块的移动过渡时间,单位ms
|
|
7
|
+
duration: {
|
|
8
|
+
type: Number,
|
|
9
|
+
default: 300,
|
|
10
|
+
},
|
|
11
|
+
// tabs标签数组
|
|
12
|
+
list: {
|
|
13
|
+
type: Array,
|
|
14
|
+
default: () => [],
|
|
15
|
+
},
|
|
16
|
+
// 滑块颜色
|
|
17
|
+
lineColor: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: '#3c9cff',
|
|
20
|
+
},
|
|
21
|
+
// 菜单选择中时的样式
|
|
22
|
+
activeStyle: {
|
|
23
|
+
type: [String, Object],
|
|
24
|
+
default: () => ({
|
|
25
|
+
color: '#303133',
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
// 菜单非选中时的样式
|
|
29
|
+
inactiveStyle: {
|
|
30
|
+
type: [String, Object],
|
|
31
|
+
default: () => ({
|
|
32
|
+
color: '#606266',
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
// 滑块长度
|
|
36
|
+
lineWidth: {
|
|
37
|
+
type: [String, Number],
|
|
38
|
+
default: 20,
|
|
39
|
+
},
|
|
40
|
+
// 滑块高度
|
|
41
|
+
lineHeight: {
|
|
42
|
+
type: [String, Number],
|
|
43
|
+
default: 3,
|
|
44
|
+
},
|
|
45
|
+
// 滑块背景显示大小,当滑块背景设置为图片时使用
|
|
46
|
+
lineBgSize: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: 'cover',
|
|
49
|
+
},
|
|
50
|
+
// 菜单item的样式
|
|
51
|
+
itemStyle: {
|
|
52
|
+
type: [String, Object],
|
|
53
|
+
default: () => ({
|
|
54
|
+
height: '44px',
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
// 菜单是否可滚动
|
|
58
|
+
scrollable: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: true,
|
|
61
|
+
},
|
|
62
|
+
// 当前选中标签的索引
|
|
63
|
+
current: {
|
|
64
|
+
type: [Number, String],
|
|
65
|
+
default: 0,
|
|
66
|
+
},
|
|
67
|
+
// 默认读取的键名
|
|
68
|
+
keyName: {
|
|
69
|
+
type: String,
|
|
70
|
+
default: 'name',
|
|
71
|
+
},
|
|
72
|
+
customStyle: {
|
|
73
|
+
type: [Object, String],
|
|
74
|
+
default: () => ({}),
|
|
75
|
+
},
|
|
76
|
+
customClass: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: '',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
...config.props.tabs,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default props;
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view
|
|
3
|
+
class="uv-tabs"
|
|
4
|
+
:class="[p.customClass]"
|
|
5
|
+
:style="[$uv.addStyle(p.customStyle)]"
|
|
6
|
+
>
|
|
7
|
+
<view class="uv-tabs__wrapper">
|
|
8
|
+
<slot name="left" />
|
|
9
|
+
<view class="uv-tabs__wrapper__scroll-view-wrapper">
|
|
10
|
+
<scroll-view
|
|
11
|
+
<!-- #ifdef UNI-APP-X -->
|
|
12
|
+
:direction="p.scrollable ? 'horizontal' : 'none'"
|
|
13
|
+
<!-- #endif -->
|
|
14
|
+
<!-- #ifndef UNI-APP-X -->
|
|
15
|
+
:scroll-x="p.scrollable"
|
|
16
|
+
<!-- #endif -->
|
|
17
|
+
<!-- #ifdef APP-NVUE -->
|
|
18
|
+
ref="scrollRef"
|
|
19
|
+
<!-- #endif -->
|
|
20
|
+
<!-- #ifdef WEB || H5 -->
|
|
21
|
+
ref="scrollFrameRef"
|
|
22
|
+
<!-- #endif -->
|
|
23
|
+
class="uv-tabs__wrapper__scroll-view"
|
|
24
|
+
:scroll-left="scrollLeft"
|
|
25
|
+
:scroll-with-animation="true"
|
|
26
|
+
:show-scrollbar="false"
|
|
27
|
+
>
|
|
28
|
+
<view
|
|
29
|
+
class="uv-tabs__wrapper__nav"
|
|
30
|
+
:style="[navStyle]"
|
|
31
|
+
>
|
|
32
|
+
<view
|
|
33
|
+
v-for="(item, index) in p.list"
|
|
34
|
+
:key="index"
|
|
35
|
+
<!-- #ifdef APP-NVUE -->
|
|
36
|
+
ref="itemRefs"
|
|
37
|
+
<!-- #endif -->
|
|
38
|
+
class="uv-tabs__wrapper__nav__item"
|
|
39
|
+
:class="[
|
|
40
|
+
`uv-tabs__wrapper__nav__item-${index}`,
|
|
41
|
+
item.disabled ? 'uv-tabs__wrapper__nav__item--disabled' : ''
|
|
42
|
+
]"
|
|
43
|
+
:style="[itemFlexStyle, $uv.addStyle(p.itemStyle)]"
|
|
44
|
+
@tap="clickHandler(item, index)"
|
|
45
|
+
>
|
|
46
|
+
<text
|
|
47
|
+
class="uv-tabs__wrapper__nav__item__text"
|
|
48
|
+
:class="[item.disabled ? 'uv-tabs__wrapper__nav__item__text--disabled' : '']"
|
|
49
|
+
:style="[textStyle(index)]"
|
|
50
|
+
>{{ item[p.keyName] }}</text>
|
|
51
|
+
<uv-badge
|
|
52
|
+
:show="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
|
|
53
|
+
:is-dot="item.badge && item.badge.isDot"
|
|
54
|
+
:value="item.badge && item.badge.value"
|
|
55
|
+
:max="item.badge && item.badge.max"
|
|
56
|
+
:type="item.badge && item.badge.type"
|
|
57
|
+
:show-zero="item.badge && item.badge.showZero"
|
|
58
|
+
:bg-color="item.badge && item.badge.bgColor"
|
|
59
|
+
:color="item.badge && item.badge.color"
|
|
60
|
+
:shape="item.badge && item.badge.shape"
|
|
61
|
+
:number-type="item.badge && item.badge.numberType"
|
|
62
|
+
:inverted="item.badge && item.badge.inverted"
|
|
63
|
+
custom-style="margin-left: 4px;"
|
|
64
|
+
></uv-badge>
|
|
65
|
+
</view>
|
|
66
|
+
<view
|
|
67
|
+
v-if="Number(p.lineHeight) > 0 && Number(p.lineWidth) > 0"
|
|
68
|
+
class="uv-tabs__wrapper__nav__line"
|
|
69
|
+
:style="[lineStyle]"
|
|
70
|
+
></view>
|
|
71
|
+
</view>
|
|
72
|
+
</scroll-view>
|
|
73
|
+
</view>
|
|
74
|
+
<slot name="right" />
|
|
75
|
+
</view>
|
|
76
|
+
</view>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
|
|
81
|
+
import { $uv } from '../../utils'
|
|
82
|
+
import props from './props'
|
|
83
|
+
import { getRect, getRectNvue } from '../../utils/compat'
|
|
84
|
+
|
|
85
|
+
defineOptions({
|
|
86
|
+
name: 'uv-tabs',
|
|
87
|
+
// #ifdef UNI-APP-X
|
|
88
|
+
styleIsolation: 'app-and-page',
|
|
89
|
+
// #endif
|
|
90
|
+
virtualHost: true,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Tabs 标签
|
|
95
|
+
* @description tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
|
|
96
|
+
* @tutorial https://www.uvui.cn/components/tabs.html
|
|
97
|
+
* @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
|
|
98
|
+
* @property {String | Number} duration 滑块移动一次所需的时间,单位ms(默认 300 )
|
|
99
|
+
* @property {String | Object} activeStyle 菜单选中时的样式(默认{ color: '#303133' })
|
|
100
|
+
* @property {String | Object} inactiveStyle 菜单非选中时的样式(默认{ color: '#606266' })
|
|
101
|
+
* @property {String | Number} lineWidth 滑块长度(默认 20)
|
|
102
|
+
* @property {String | Number} lineHeight 滑块高度(默认 3)
|
|
103
|
+
* @property {String} lineColor 滑块颜色(默认:'#3c9cff')
|
|
104
|
+
* @property {String} lineBgSize 滑块背景显示大小,当滑块背景设置为图片时使用(默认 cover)
|
|
105
|
+
* @property {String | Object} itemStyle 菜单item的样式(默认 { height: '44px' })
|
|
106
|
+
* @property {Boolean} scrollable 菜单是否可滚动,选项很少的时候设置为false整个tabs自动居中显示(默认:true)
|
|
107
|
+
* @property {String | Number} current 当前选中标签的索引(默认 0 )
|
|
108
|
+
* @property {String} keyName 从list元素对象中读取的键名(默认 'name' )
|
|
109
|
+
* @property {Object} customStyle 自定义外部样式
|
|
110
|
+
* @event {Function} change 标签改变时触发 index: 点击了第几个tab,索引从0开始
|
|
111
|
+
* @event {Function} click 点击标签时触发 index: 点击了第几个tab,索引从0开始
|
|
112
|
+
* @example <uv-tabs :list="list" :current="current" @change="change"></uv-tabs>
|
|
113
|
+
*/
|
|
114
|
+
const p = defineProps(props)
|
|
115
|
+
|
|
116
|
+
const emit = defineEmits(['click', 'change'])
|
|
117
|
+
|
|
118
|
+
const instance = getCurrentInstance()
|
|
119
|
+
|
|
120
|
+
// 第一次移动滑块时不需要过渡动画
|
|
121
|
+
const firstTime = ref(true)
|
|
122
|
+
const scrollLeft = ref(0)
|
|
123
|
+
const lineOffsetLeft = ref(0)
|
|
124
|
+
const innerCurrent = ref(0)
|
|
125
|
+
// 导航内容总宽度(可滚动模式由测量值决定,保证内容宽度大于容器宽度才可滚动)
|
|
126
|
+
const navWidth = ref(0)
|
|
127
|
+
|
|
128
|
+
// #ifdef APP-NVUE
|
|
129
|
+
// nvue 不支持 uni.createSelectorQuery,使用 dom 模块对 ref 节点测量
|
|
130
|
+
const scrollRef = ref<any>(null)
|
|
131
|
+
const itemRefs = ref<any[]>([])
|
|
132
|
+
// #endif
|
|
133
|
+
|
|
134
|
+
// #ifdef WEB || H5
|
|
135
|
+
// uni-app x 的 H5 运行时对 scroll-left 响应式更新不稳定(x App 用 direction),
|
|
136
|
+
// H5 下直接同步内部滚动容器的 scrollLeft,保证各端滚动定位一致。
|
|
137
|
+
const scrollFrameRef = ref<any>(null)
|
|
138
|
+
const applyH5ScrollLeft = (value: number) => {
|
|
139
|
+
nextTick(() => {
|
|
140
|
+
const frame: any = scrollFrameRef.value
|
|
141
|
+
const root: any = frame && frame.$el ? frame.$el : frame
|
|
142
|
+
if (!root || typeof root.querySelectorAll !== 'function') return
|
|
143
|
+
const nodes: any[] = root.querySelectorAll('.uni-scroll-view')
|
|
144
|
+
const scroller: any = nodes && nodes.length > 1 ? nodes[1] : nodes && nodes[0]
|
|
145
|
+
if (scroller) scroller.scrollLeft = value
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
// #endif
|
|
149
|
+
|
|
150
|
+
// 非响应式的测量结果,仅用于计算滑块位置与滚动位置
|
|
151
|
+
let itemRects: any[] = []
|
|
152
|
+
let tabsRect: any = {
|
|
153
|
+
left: 0,
|
|
154
|
+
right: 0,
|
|
155
|
+
width: 0
|
|
156
|
+
}
|
|
157
|
+
let scrollViewWidth = 0
|
|
158
|
+
let isMounted = false
|
|
159
|
+
|
|
160
|
+
const navStyle = computed(() => {
|
|
161
|
+
if (!p.scrollable) {
|
|
162
|
+
return { flex: 1 }
|
|
163
|
+
}
|
|
164
|
+
// 可滚动模式下显式设置内容总宽度,避免各端 scroll-view 对横向内容布局差异导致无法滚动
|
|
165
|
+
return navWidth.value > 0 ? { width: `${navWidth.value}px` } : {}
|
|
166
|
+
})
|
|
167
|
+
const itemFlexStyle = computed(() => (p.scrollable ? {} : { flex: 1 }))
|
|
168
|
+
|
|
169
|
+
const lineStyle = computed(() => {
|
|
170
|
+
const style: any = {
|
|
171
|
+
width: $uv.addUnit(p.lineWidth),
|
|
172
|
+
transform: `translate(${lineOffsetLeft.value}px)`,
|
|
173
|
+
transitionDuration: `${firstTime.value ? 0 : Number(p.duration) || 0}ms`,
|
|
174
|
+
height: firstTime.value ? 0 : $uv.addUnit(p.lineHeight),
|
|
175
|
+
// 使用 background 简写而不是 backgroundColor:lineColor 支持传入背景图(url(...))
|
|
176
|
+
background: p.lineColor,
|
|
177
|
+
backgroundSize: p.lineBgSize
|
|
178
|
+
}
|
|
179
|
+
return style
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const textStyle = (index: number) => {
|
|
183
|
+
const item = p.list[index] as any
|
|
184
|
+
// 取当前是否激活的样式
|
|
185
|
+
const style: any =
|
|
186
|
+
(index === innerCurrent.value ? $uv.addStyle(p.activeStyle) : $uv.addStyle(p.inactiveStyle)) || {}
|
|
187
|
+
const extra: any = {}
|
|
188
|
+
// 如果当前菜单被禁用,则加上对应颜色
|
|
189
|
+
if (item && item.disabled) {
|
|
190
|
+
extra.color = '#c8c9cc'
|
|
191
|
+
}
|
|
192
|
+
return $uv.deepMerge(style, extra)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 获取单个节点的尺寸(返回 unbounded 的 rect 对象)
|
|
196
|
+
const queryRect = (selector: string) => {
|
|
197
|
+
return getRect(instance ? instance.proxy : null, selector)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 获取导航菜单 scroll-view 的尺寸
|
|
201
|
+
const getTabsRect = () => {
|
|
202
|
+
// #ifdef APP-NVUE
|
|
203
|
+
return getRectNvue(scrollRef.value)
|
|
204
|
+
// #endif
|
|
205
|
+
// #ifndef APP-NVUE
|
|
206
|
+
return queryRect('.uv-tabs__wrapper__scroll-view')
|
|
207
|
+
// #endif
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 获取所有标签的尺寸(selectAll,返回顺序与 list 一致)
|
|
211
|
+
const getAllItemRect = () => {
|
|
212
|
+
// #ifdef APP-NVUE
|
|
213
|
+
const refs = Array.isArray(itemRefs.value) ? itemRefs.value : []
|
|
214
|
+
if (!refs.length) return Promise.resolve([])
|
|
215
|
+
return Promise.all(refs.map((item: any) => getRectNvue(item)))
|
|
216
|
+
// #endif
|
|
217
|
+
// #ifndef APP-NVUE
|
|
218
|
+
return getRect(instance ? instance.proxy : null, '.uv-tabs__wrapper__nav__item', true)
|
|
219
|
+
// #endif
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 设置滑块的位置
|
|
223
|
+
const setLineLeft = () => {
|
|
224
|
+
const tabItem = itemRects[innerCurrent.value]
|
|
225
|
+
if (!tabItem || !tabItem.width) return
|
|
226
|
+
// 获取滑块该移动的位置
|
|
227
|
+
let offsetLeft = 0
|
|
228
|
+
for (let i = 0; i < innerCurrent.value; i++) {
|
|
229
|
+
const rect = itemRects[i]
|
|
230
|
+
if (rect) offsetLeft += rect.width
|
|
231
|
+
}
|
|
232
|
+
// 获取下划线的数值px表示法
|
|
233
|
+
let lineWidth = $uv.getPx(p.lineWidth)
|
|
234
|
+
if ($uv.test.number(p.lineWidth) && $uv.config.unit) {
|
|
235
|
+
lineWidth = $uv.getPx(`${p.lineWidth}${$uv.config.unit}`)
|
|
236
|
+
}
|
|
237
|
+
lineOffsetLeft.value = offsetLeft + (tabItem.width - lineWidth) / 2
|
|
238
|
+
// 第一次移动滑块,需要等过渡结束后再开启动画
|
|
239
|
+
if (firstTime.value) {
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
firstTime.value = false
|
|
242
|
+
}, 20)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 将激活的 tab 移动到 scroll-view 的中间
|
|
247
|
+
const setScrollLeft = () => {
|
|
248
|
+
const tabRect = itemRects[innerCurrent.value]
|
|
249
|
+
if (!tabRect || !tabsRect.width) return
|
|
250
|
+
// 累加得到当前 item 到左边的距离
|
|
251
|
+
let offsetLeft = 0
|
|
252
|
+
for (let i = 0; i < innerCurrent.value; i++) {
|
|
253
|
+
const rect = itemRects[i]
|
|
254
|
+
if (rect) offsetLeft += rect.width
|
|
255
|
+
}
|
|
256
|
+
const windowWidth = $uv.sys().windowWidth || 0
|
|
257
|
+
// nvue 的 dom.getComponentRect 在部分基座下只返回 width/height,缺失 left/right 时兜底:
|
|
258
|
+
// right 用 left + width 补齐,left 缺失按 0 处理,避免 NaN 导致 scrollLeft 失效。
|
|
259
|
+
const tabsWidth = Number(tabsRect.width) || 0
|
|
260
|
+
const tabsLeft = typeof tabsRect.left === 'number' && isFinite(tabsRect.left) ? tabsRect.left : 0
|
|
261
|
+
const tabsRight =
|
|
262
|
+
typeof tabsRect.right === 'number' && isFinite(tabsRect.right) ? tabsRect.right : tabsLeft + tabsWidth
|
|
263
|
+
let left =
|
|
264
|
+
offsetLeft -
|
|
265
|
+
(tabsWidth - tabRect.width) / 2 -
|
|
266
|
+
(windowWidth - tabsRight) / 2 +
|
|
267
|
+
tabsLeft / 2
|
|
268
|
+
if (!isFinite(left)) left = 0
|
|
269
|
+
// 限制 scrollLeft 的最大值为整个内容宽度减去 scroll-view 的宽度
|
|
270
|
+
left = Math.min(left, scrollViewWidth - tabsWidth)
|
|
271
|
+
scrollLeft.value = Math.max(0, left)
|
|
272
|
+
// #ifdef WEB || H5
|
|
273
|
+
applyH5ScrollLeft(scrollLeft.value)
|
|
274
|
+
// #endif
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 获取所有标签的尺寸并重新定位滑块
|
|
278
|
+
const resize = () => {
|
|
279
|
+
// 组件未挂载时 DOM 还不存在,跳过(由 onMounted 中的 init 负责首次测量)
|
|
280
|
+
if (!isMounted) return
|
|
281
|
+
if (!p.list || p.list.length === 0) return
|
|
282
|
+
return Promise.all([getTabsRect(), getAllItemRect()]).then((results: any[]) => {
|
|
283
|
+
const tabs = results[0]
|
|
284
|
+
const rects = Array.isArray(results[1]) ? results[1] : []
|
|
285
|
+
if (tabs) tabsRect = tabs
|
|
286
|
+
itemRects = rects
|
|
287
|
+
scrollViewWidth = 0
|
|
288
|
+
rects.forEach((rect: any) => {
|
|
289
|
+
if (rect) scrollViewWidth += rect.width
|
|
290
|
+
})
|
|
291
|
+
navWidth.value = scrollViewWidth
|
|
292
|
+
setLineLeft()
|
|
293
|
+
setScrollLeft()
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const init = () => {
|
|
298
|
+
$uv.sleep().then(() => {
|
|
299
|
+
resize()
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 点击某一个标签
|
|
304
|
+
const clickHandler = (item: any, index: number) => {
|
|
305
|
+
const payload = {
|
|
306
|
+
...item,
|
|
307
|
+
index
|
|
308
|
+
}
|
|
309
|
+
// 因为标签可能为 disabled 状态,所以 click 是一定会发出的,但是 change 事件是需要可用的状态才发出
|
|
310
|
+
emit('click', payload)
|
|
311
|
+
// 如果 disabled 状态,返回
|
|
312
|
+
if (item && item.disabled) return
|
|
313
|
+
if (innerCurrent.value !== index) {
|
|
314
|
+
emit('change', payload)
|
|
315
|
+
}
|
|
316
|
+
innerCurrent.value = index
|
|
317
|
+
nextTick(() => {
|
|
318
|
+
resize()
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
watch(
|
|
323
|
+
() => p.current,
|
|
324
|
+
(newValue: any) => {
|
|
325
|
+
const value = Number(newValue) || 0
|
|
326
|
+
// 内外部值不相等时,才尝试移动滑块
|
|
327
|
+
if (value !== innerCurrent.value) {
|
|
328
|
+
innerCurrent.value = value
|
|
329
|
+
nextTick(() => {
|
|
330
|
+
resize()
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{ immediate: true }
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// list 变化时,重新渲染 list 各项信息
|
|
338
|
+
watch(
|
|
339
|
+
() => p.list,
|
|
340
|
+
() => {
|
|
341
|
+
nextTick(() => {
|
|
342
|
+
resize()
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
onMounted(() => {
|
|
348
|
+
isMounted = true
|
|
349
|
+
init()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
defineExpose({
|
|
353
|
+
init,
|
|
354
|
+
resize,
|
|
355
|
+
setLineLeft,
|
|
356
|
+
setScrollLeft
|
|
357
|
+
})
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style lang="scss">
|
|
361
|
+
@import '../../styles/components.scss';
|
|
362
|
+
@import '../../styles/color.scss';
|
|
363
|
+
|
|
364
|
+
.uv-tabs {
|
|
365
|
+
display: flex;
|
|
366
|
+
flex-direction: column;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.uv-tabs__wrapper {
|
|
370
|
+
display: flex;
|
|
371
|
+
flex-direction: row;
|
|
372
|
+
align-items: center;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.uv-tabs__wrapper__scroll-view-wrapper {
|
|
376
|
+
flex: 1;
|
|
377
|
+
/* #ifdef WEB || H5 || MP */
|
|
378
|
+
overflow: auto hidden;
|
|
379
|
+
/* #endif */
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.uv-tabs__wrapper__scroll-view {
|
|
383
|
+
flex: 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.uv-tabs__wrapper__nav {
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: row;
|
|
389
|
+
position: relative;
|
|
390
|
+
/* #ifndef APP-NVUE */
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
/* #endif */
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.uv-tabs__wrapper__nav__item {
|
|
396
|
+
padding: 0 11px;
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: row;
|
|
399
|
+
align-items: center;
|
|
400
|
+
justify-content: center;
|
|
401
|
+
/* #ifndef APP-NVUE */
|
|
402
|
+
flex-shrink: 0;
|
|
403
|
+
/* #endif */
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.uv-tabs__wrapper__nav__item--disabled {
|
|
407
|
+
/* #ifdef WEB || H5 || MP */
|
|
408
|
+
cursor: not-allowed;
|
|
409
|
+
/* #endif */
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.uv-tabs__wrapper__nav__item__text {
|
|
413
|
+
font-size: 15px;
|
|
414
|
+
color: $uv-content-color;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.uv-tabs__wrapper__nav__item__text--disabled {
|
|
418
|
+
color: $uv-disabled-color !important;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.uv-tabs__wrapper__nav__line {
|
|
422
|
+
height: 3px;
|
|
423
|
+
background-color: $uv-primary;
|
|
424
|
+
width: 30px;
|
|
425
|
+
position: absolute;
|
|
426
|
+
left: 0;
|
|
427
|
+
bottom: 2px;
|
|
428
|
+
border-radius: 100px;
|
|
429
|
+
transition-property: transform;
|
|
430
|
+
transition-duration: 300ms;
|
|
431
|
+
}
|
|
432
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
|
|
3
|
+
import config from '../../utils/config';
|
|
4
|
+
|
|
5
|
+
const props = {
|
|
6
|
+
show: {
|
|
7
|
+
type: Boolean,
|
|
8
|
+
default: false,
|
|
9
|
+
},
|
|
10
|
+
mode: {
|
|
11
|
+
type: [Array, String, null],
|
|
12
|
+
default: () => 'fade',
|
|
13
|
+
},
|
|
14
|
+
duration: {
|
|
15
|
+
type: [String, Number],
|
|
16
|
+
default: 300,
|
|
17
|
+
},
|
|
18
|
+
timingFunction: {
|
|
19
|
+
type: String,
|
|
20
|
+
default: 'ease-out',
|
|
21
|
+
},
|
|
22
|
+
customStyle: {
|
|
23
|
+
type: [Object, String],
|
|
24
|
+
default: () => ({}),
|
|
25
|
+
},
|
|
26
|
+
customClass: {
|
|
27
|
+
type: [String, Array],
|
|
28
|
+
default: '',
|
|
29
|
+
},
|
|
30
|
+
cellChild: {
|
|
31
|
+
type: Boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
...config.props.transition,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default props;
|