368 lines
6.5 KiB
Vue
368 lines
6.5 KiB
Vue
<template>
|
|
<view
|
|
class="cl-tabs"
|
|
:class="[
|
|
{
|
|
'is-content': $slots.default,
|
|
'is-fill': fill,
|
|
'is-border': border,
|
|
'is-dropdown': showDropdown,
|
|
'is-checkable': checkable,
|
|
},
|
|
]"
|
|
:style="{
|
|
backgroundColor: backgroundColor,
|
|
}"
|
|
>
|
|
<view class="cl-tabs__header">
|
|
<scroll-view
|
|
class="cl-tabs__bar"
|
|
scroll-with-animation
|
|
scroll-x
|
|
:scroll-left="scrollLeft"
|
|
:style="{
|
|
height: parseRpx(height),
|
|
}"
|
|
>
|
|
<view
|
|
class="cl-tabs__bar-box"
|
|
:style="{
|
|
'justify-content': justify,
|
|
}"
|
|
>
|
|
<view
|
|
class="cl-tabs__bar-item"
|
|
v-for="(item, index) in tabs"
|
|
:key="index"
|
|
:style="{
|
|
color: current === item.value ? color : unColor,
|
|
padding: `0 ${gutter}rpx`,
|
|
}"
|
|
:class="{
|
|
'is-active': current === item.value,
|
|
}"
|
|
@tap="change(index)"
|
|
>
|
|
<!-- 前缀图标 -->
|
|
<text
|
|
v-if="item.prefixIcon"
|
|
class="cl-tabs__icon is-prefix"
|
|
:class="[item.prefixIcon]"
|
|
></text>
|
|
|
|
<!-- 文本内容 -->
|
|
<text class="cl-tabs__label">{{ item.label }}</text>
|
|
|
|
<!-- 后缀图标 -->
|
|
<text
|
|
v-if="item.suffixIcon"
|
|
class="cl-tabs__icon is-suffix"
|
|
:class="[item.suffixIcon]"
|
|
></text>
|
|
</view>
|
|
|
|
<!-- 选中样式 -->
|
|
<view
|
|
class="cl-tabs__line"
|
|
v-if="lineLeft > 0 && showLine"
|
|
:style="{
|
|
'background-color': color,
|
|
left: lineLeft + 'px',
|
|
}"
|
|
></view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- 下拉图标 -->
|
|
<view class="cl-tabs__dropdown" @tap="openDropdown" v-if="showDropdown">
|
|
<cl-icon :name="`${dropdown.visible ? 'arrow-top' : 'arrow-bottom'}`"></cl-icon>
|
|
</view>
|
|
|
|
<!-- 下拉列表 -->
|
|
<view
|
|
class="cl-tabs__dropdown-box"
|
|
:style="{
|
|
maxHeight: dropdown.visible ? dropdown.height : '0',
|
|
}"
|
|
>
|
|
<slot name="dropdown"></slot>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
type PropType,
|
|
computed,
|
|
defineComponent,
|
|
getCurrentInstance,
|
|
nextTick,
|
|
onMounted,
|
|
reactive,
|
|
ref,
|
|
watch,
|
|
} from "vue";
|
|
import { parseRpx } from "/@/cool/utils";
|
|
|
|
export default defineComponent({
|
|
name: "cl-tabs",
|
|
|
|
props: {
|
|
// 绑定值
|
|
modelValue: [String, Number],
|
|
// 高度
|
|
height: {
|
|
type: [String, Number],
|
|
default: 80,
|
|
},
|
|
// 标签列表
|
|
list: {
|
|
type: Array as PropType<{ label: string; value: any; [key: string]: any }[]>,
|
|
default: [],
|
|
},
|
|
// 是否循环显示
|
|
loop: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// 标签是否填充
|
|
fill: Boolean,
|
|
// 水平布局
|
|
justify: {
|
|
type: String as PropType<"start" | "center" | "end">,
|
|
default: "start",
|
|
},
|
|
// 是否带有下边框
|
|
border: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// 标签间隔
|
|
gutter: {
|
|
type: Number,
|
|
default: 30,
|
|
},
|
|
// 选中颜色
|
|
color: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
// 未选中颜色
|
|
unColor: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
// 背景色
|
|
backgroundColor: {
|
|
type: String,
|
|
default: "#fff",
|
|
},
|
|
// 是否显示下拉按钮
|
|
showDropdown: Boolean,
|
|
// 显示下划线
|
|
showLine: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// 是否可选
|
|
checkable: Boolean,
|
|
// 是否禁用
|
|
disabled: Boolean,
|
|
},
|
|
|
|
emits: ["update:modelValue", "change"],
|
|
|
|
setup(props, { emit }) {
|
|
const { proxy }: any = getCurrentInstance();
|
|
|
|
// 当前选中
|
|
const current = ref<number | string>();
|
|
|
|
// 下划线左位移
|
|
const lineLeft = ref(0);
|
|
|
|
// 左滚动距离
|
|
const scrollLeft = ref(0);
|
|
|
|
// 左位移
|
|
const offsetLeft = ref(0);
|
|
|
|
// 宽度
|
|
const width = ref(375);
|
|
|
|
// 列表
|
|
const tabs = computed(() => props.list);
|
|
|
|
// 刷新
|
|
function refresh() {
|
|
nextTick(() => {
|
|
// 获取选项卡宽度
|
|
uni.createSelectorQuery()
|
|
// #ifndef MP-ALIPAY
|
|
.in(proxy)
|
|
// #endif
|
|
.select(".cl-tabs")
|
|
.boundingClientRect((d: any) => {
|
|
offsetLeft.value = d.left;
|
|
width.value = d.width;
|
|
getRect();
|
|
})
|
|
.exec();
|
|
});
|
|
}
|
|
|
|
// 下拉
|
|
const dropdown = reactive({
|
|
visible: false,
|
|
height: "200rpx",
|
|
timer: null as any,
|
|
});
|
|
|
|
// 打开下拉框
|
|
function openDropdown() {
|
|
dropdown.visible = !dropdown.visible;
|
|
|
|
// 清除计时器
|
|
clearTimeout(dropdown.timer);
|
|
|
|
if (dropdown.visible) {
|
|
const fn = () => {
|
|
uni.createSelectorQuery()
|
|
// #ifndef MP-ALIPAY
|
|
.in(proxy)
|
|
// #endif
|
|
.select(".cl-tabs__dropdown-box")
|
|
.boundingClientRect((res) => {
|
|
dropdown.height = res.height + "px";
|
|
})
|
|
.exec();
|
|
};
|
|
|
|
// 获取下拉区域高度
|
|
dropdown.timer = setTimeout(fn, 300);
|
|
}
|
|
}
|
|
|
|
// 关闭下拉框
|
|
function closeDropdown() {
|
|
dropdown.visible = false;
|
|
}
|
|
|
|
// 改变
|
|
async function change(index: number) {
|
|
if (props.disabled) {
|
|
return false;
|
|
}
|
|
|
|
let { value } = tabs.value[index];
|
|
|
|
if (props.checkable) {
|
|
if (value == current.value) {
|
|
value = undefined;
|
|
}
|
|
}
|
|
|
|
emit("update:modelValue", value);
|
|
emit("change", value);
|
|
current.value = value;
|
|
}
|
|
|
|
// 获取下标
|
|
function getIndex() {
|
|
return tabs.value.findIndex((e) => e.value == current.value);
|
|
}
|
|
|
|
// 上一个
|
|
function prev() {
|
|
let index = getIndex();
|
|
|
|
change(index <= 0 ? (props.loop ? tabs.value.length - 1 : 0) : index - 1);
|
|
}
|
|
|
|
// 下一个
|
|
function next() {
|
|
let index = getIndex();
|
|
|
|
change(
|
|
index >= tabs.value.length - 1
|
|
? props.loop
|
|
? 0
|
|
: tabs.value.length - 1
|
|
: index + 1,
|
|
);
|
|
}
|
|
|
|
const rect = ref<any>();
|
|
|
|
// 大小
|
|
function getRect() {
|
|
nextTick(() => {
|
|
uni.createSelectorQuery()
|
|
.in(proxy)
|
|
.selectAll(".cl-tabs__bar-item")
|
|
.fields({ rect: true, size: true }, () => {})
|
|
.exec((d) => {
|
|
rect.value = d[0];
|
|
onOffset();
|
|
});
|
|
});
|
|
}
|
|
|
|
// 间距
|
|
function onOffset() {
|
|
if (rect.value) {
|
|
nextTick(() => {
|
|
let item = rect.value[getIndex()];
|
|
|
|
if (item) {
|
|
let x = item.left - (width.value - item.width) / 2 - offsetLeft.value;
|
|
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
|
|
scrollLeft.value = x;
|
|
lineLeft.value =
|
|
item.left + item.width / 2 - uni.upx2px(16) - offsetLeft.value;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 监听绑定值
|
|
watch(
|
|
() => props.modelValue,
|
|
(val: any) => {
|
|
current.value = val;
|
|
onOffset();
|
|
},
|
|
{
|
|
immediate: true,
|
|
},
|
|
);
|
|
|
|
// 监听列表改变
|
|
watch(() => props.list, refresh);
|
|
|
|
onMounted(() => {
|
|
refresh();
|
|
});
|
|
|
|
return {
|
|
current,
|
|
scrollLeft,
|
|
lineLeft,
|
|
tabs,
|
|
dropdown,
|
|
change,
|
|
prev,
|
|
next,
|
|
openDropdown,
|
|
closeDropdown,
|
|
parseRpx,
|
|
};
|
|
},
|
|
});
|
|
</script>
|