|
|
@@ -0,0 +1,318 @@
|
|
|
+<template>
|
|
|
+ <view class="scrollable-tabs-container" :style="{ top: top + 'upx', height: height + 'upx' }">
|
|
|
+ <scroll-view
|
|
|
+ class="tabs-scroll"
|
|
|
+ :scroll-x="true"
|
|
|
+ :scroll-left="scrollLeft"
|
|
|
+ :show-scrollbar="false"
|
|
|
+ :enable-flex="true"
|
|
|
+ scroll-with-animation
|
|
|
+ >
|
|
|
+ <view class="tabs-content">
|
|
|
+ <view
|
|
|
+ v-for="(tab, index) in tabs"
|
|
|
+ :key="index"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: currentTab === index }"
|
|
|
+ :id="`tab-${index}`"
|
|
|
+ @click="handleTabClick(index)"
|
|
|
+ >
|
|
|
+ <view class="tab-content">
|
|
|
+ <text class="tab-text">{{ tab.name }}</text>
|
|
|
+ <text class="tab-count">({{ tab.value || 0 }})</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="currentTab === index" class="tab-indicator"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'ScrollableTabs',
|
|
|
+ props: {
|
|
|
+ tabs: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ currentTab: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ top: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ type: Number,
|
|
|
+ default: 104
|
|
|
+ },
|
|
|
+ isFixed: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ scrollLeft: 0,
|
|
|
+ tabItemWidth: 120, // 每个tab项的最小宽度(upx)
|
|
|
+ platform: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ // 获取当前平台信息
|
|
|
+ // #ifdef H5
|
|
|
+ this.platform = 'h5'
|
|
|
+ // #endif
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ this.platform = 'mp-weixin'
|
|
|
+ // #endif
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ this.platform = 'app-plus'
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ currentTab: {
|
|
|
+ handler(newVal) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.scrollToActiveTab(newVal)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ immediate: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleTabClick(index) {
|
|
|
+ if (index !== this.currentTab) {
|
|
|
+ this.$emit('change', { index })
|
|
|
+
|
|
|
+ // 小程序端延迟执行滚动,避免卡顿
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ setTimeout(() => {
|
|
|
+ this.scrollToActiveTab(index)
|
|
|
+ }, 50)
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef MP-WEIXIN
|
|
|
+ this.scrollToActiveTab(index)
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ scrollToActiveTab(index) {
|
|
|
+ // 使用简化的滚动计算,提升跨平台兼容性
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const query = uni.createSelectorQuery().in(this)
|
|
|
+
|
|
|
+ // 获取容器和tab的信息
|
|
|
+ query.select('.tabs-scroll').boundingClientRect()
|
|
|
+ query.select(`#tab-${index}`).boundingClientRect()
|
|
|
+ query.exec((res) => {
|
|
|
+ if (!res || res.length < 2 || !res[0] || !res[1]) {
|
|
|
+ // 降级方案:简单计算
|
|
|
+ this.scrollLeft = index * 160
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const containerRect = res[0]
|
|
|
+ const tabRect = res[1]
|
|
|
+ const containerWidth = containerRect.width
|
|
|
+
|
|
|
+ // 计算tab相对于容器的位置
|
|
|
+ const tabLeft = tabRect.left - containerRect.left + this.scrollLeft
|
|
|
+ const tabCenter = tabLeft + tabRect.width / 2
|
|
|
+
|
|
|
+ // 计算理想的滚动位置(让当前tab居中)
|
|
|
+ const idealScrollLeft = tabCenter - containerWidth / 2
|
|
|
+
|
|
|
+ // 获取所有tab的总宽度
|
|
|
+ query.select('.tabs-content').boundingClientRect().exec((contentRes) => {
|
|
|
+ if (contentRes && contentRes[0]) {
|
|
|
+ const maxScrollLeft = Math.max(0, contentRes[0].width - containerWidth)
|
|
|
+ this.scrollLeft = Math.max(0, Math.min(idealScrollLeft, maxScrollLeft))
|
|
|
+ } else {
|
|
|
+ // 降级方案
|
|
|
+ this.scrollLeft = Math.max(0, idealScrollLeft)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.scrollable-tabs-container {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background-color: #fff;
|
|
|
+ border-bottom: 1upx solid #f0f0f0;
|
|
|
+ z-index: 999;
|
|
|
+
|
|
|
+ width: 100%;
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ /* 手机mobile屏幕小于1000px */
|
|
|
+ @media screen and (max-width: 1100px) {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 收银台cashier屏幕大于1000px */
|
|
|
+ @media screen and (min-width: 1100px) {
|
|
|
+ width: 40%;
|
|
|
+ }
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tabs-scroll {
|
|
|
+ height: 100%;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ ::-webkit-scrollbar {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tabs-content {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 20upx;
|
|
|
+
|
|
|
+ /* 小程序端优化 */
|
|
|
+ /* #ifdef MP-WEIXIN */
|
|
|
+ flex-shrink: 0;
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: 140upx;
|
|
|
+ padding: 16upx 28upx;
|
|
|
+ height: 100%;
|
|
|
+ /* #ifdef H5 */
|
|
|
+ cursor: pointer;
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ /* #ifdef APP-PLUS || MP-WEIXIN */
|
|
|
+ transition: color 0.2s;
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ // 增加点击区域,提升用户体验
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: -10upx;
|
|
|
+ left: -10upx;
|
|
|
+ right: -10upx;
|
|
|
+ bottom: -10upx;
|
|
|
+ z-index: -1;
|
|
|
+ }
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ &:not(:last-child) {
|
|
|
+ margin-right: 20upx;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ .tab-text {
|
|
|
+ color: #3385FF;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-count {
|
|
|
+ color: #3385FF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.tab-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ /* App端优化 */
|
|
|
+ /* #ifdef APP-PLUS */
|
|
|
+ flex-shrink: 0;
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tab-text {
|
|
|
+ font-size: 30upx;
|
|
|
+ color: #666;
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ transition: color 0.3s ease;
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ /* #ifdef APP-PLUS || MP-WEIXIN */
|
|
|
+ transition: color 0.2s;
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tab-count {
|
|
|
+ font-size: 24upx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 4upx;
|
|
|
+
|
|
|
+ /* #ifdef H5 */
|
|
|
+ transition: color 0.3s ease;
|
|
|
+ /* #endif */
|
|
|
+
|
|
|
+ /* #ifdef APP-PLUS || MP-WEIXIN */
|
|
|
+ transition: color 0.2s;
|
|
|
+ /* #endif */
|
|
|
+}
|
|
|
+
|
|
|
+.tab-indicator {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 40upx;
|
|
|
+ height: 4upx;
|
|
|
+ background-color: #3385FF;
|
|
|
+ border-radius: 2upx;
|
|
|
+}
|
|
|
+
|
|
|
+/* #ifdef H5 */
|
|
|
+// 深色模式支持
|
|
|
+@media (prefers-color-scheme: dark) {
|
|
|
+ .scrollable-tabs-container {
|
|
|
+ background-color: #1f1f1f;
|
|
|
+ border-bottom-color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-text {
|
|
|
+ color: #ccc;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-count {
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-item.active {
|
|
|
+ .tab-text,
|
|
|
+ .tab-count {
|
|
|
+ color: #3385FF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+/* #endif */
|
|
|
+</style>
|