hookehuyr

feat: 新增 FilterTabs 组件并统一筛选标签样式

将资料列表、知识库、收藏和计划书页面的横向筛选标签抽取为通用组件,避免重复的样式和滚动条隐藏逻辑。新增组件示例文件并更新全局组件类型声明。
......@@ -77,6 +77,11 @@ src/
## 📄 页面说明
- 计划书页面支持顶部搜索与标签固定,列表区域独立滚动,便于快速筛选和浏览
- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs 组件进行横向筛选
## ✅ 优化建议
- 新增筛选类页面优先复用 FilterTabs,避免重复样式与交互逻辑
## ⚙️ 配置说明
......
......@@ -8,6 +8,8 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default']
FilterTabs: typeof import('./src/components/FilterTabs.vue')['default']
'FilterTabs.example': typeof import('./src/components/FilterTabs.example.vue')['default']
IconFont: typeof import('./src/components/IconFont.vue')['default']
IndexNav: typeof import('./src/components/indexNav.vue')['default']
ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default']
......
......@@ -6,6 +6,26 @@
---
## [2026-01-31] - 抽取筛选 Tabs 组件并统一使用
### 新增
- 新增 FilterTabs 组件与小程序示例文件
### 重构
- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs
- 滚动条隐藏逻辑集中在 FilterTabs 内
---
**详细信息**
- **影响文件**: src/components/FilterTabs.vue, src/components/FilterTabs.example.vue, src/pages/material-list/index.vue, src/pages/knowledge-base/index.vue, src/pages/favorites/index.vue, src/pages/plan/index.vue, README.md
- **技术栈**: Vue 3, Taro, TailwindCSS
- **测试状态**: pnpm lint(存在既有警告)
- **备注**:
- 统一横向筛选标签样式与交互
---
**详细信息**
- **影响文件**: src/pages/plan/index.vue, README.md
- **技术栈**: Vue 3, Taro, TailwindCSS
......
<template>
<view class="p-[24rpx] bg-white">
<FilterTabs
v-model="activeTab"
:tabs="tabs"
label-key="title"
value-key="key"
wrapper-class="mb-[24rpx]"
@change="handleChange"
>
<template #label="{ item }">
<text>{{ item.title }}</text>
</template>
</FilterTabs>
<view class="text-[24rpx] text-gray-500">当前选中:{{ activeTab }}</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import FilterTabs from '@/components/FilterTabs.vue'
const activeTab = ref('all')
const tabs = [
{ title: '全部', key: 'all' },
{ title: '入职培训', key: 'onboarding' },
{ title: '签单相关', key: 'signing' }
]
const handleChange = (value) => {
console.log('选中项:', value)
}
</script>
<template>
<view :class="['flex overflow-x-auto no-scrollbar space-x-[24rpx]', wrapperClass]">
<view v-for="(item, index) in tabs" :key="getItemKey(item, index)"
:class="[
'px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors',
tabClass,
isActive(item, index) ? activeClassComputed : inactiveClassComputed
]"
@tap="handleSelect(item, index)"
>
<slot name="label" :item="item" :index="index">
{{ getItemLabel(item) }}
</slot>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
tabs: {
type: Array,
default: () => []
},
modelValue: {
type: [String, Number],
default: 0
},
labelKey: {
type: String,
default: 'title'
},
valueKey: {
type: String,
default: ''
},
wrapperClass: {
type: String,
default: ''
},
tabClass: {
type: String,
default: ''
},
activeClass: {
type: String,
default: ''
},
inactiveClass: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const activeClassComputed = computed(() => {
return props.activeClass || 'bg-[#2563EB] text-white'
})
const inactiveClassComputed = computed(() => {
return props.inactiveClass || 'bg-[#F3F4F6] text-[#6B7280]'
})
const getItemLabel = (item) => {
if (typeof item === 'string') return item
return item?.[props.labelKey] ?? ''
}
const getItemValue = (item, index) => {
if (typeof item !== 'object' || item === null) return index
if (props.valueKey && item[props.valueKey] !== undefined) return item[props.valueKey]
return index
}
const getItemKey = (item, index) => {
const value = getItemValue(item, index)
return value ?? index
}
const isActive = (item, index) => {
return getItemValue(item, index) === props.modelValue
}
const handleSelect = (item, index) => {
const value = getItemValue(item, index)
if (value === props.modelValue) return
emit('update:modelValue', value)
emit('change', value)
}
</script>
<style lang="less" scoped>
:deep(.no-scrollbar::-webkit-scrollbar) {
display: none;
width: 0;
height: 0;
}
:deep(.no-scrollbar) {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
......@@ -5,14 +5,12 @@
<!-- Tabs Section -->
<view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]">
<div class="flex overflow-x-auto no-scrollbar space-x-[24rpx]">
<div v-for="(tab, index) in tabs" :key="index"
class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
:class="activeTab === tab.key ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
@click="activeTab = tab.key">
{{ tab.title }}
</div>
</div>
<FilterTabs
v-model="activeTab"
:tabs="tabs"
label-key="title"
value-key="key"
/>
</view>
<!-- List Section -->
......@@ -71,7 +69,7 @@ import { useGo } from '@/hooks/useGo'
import { useFileOperation } from '@/composables/useFileOperation'
import { getDocumentIcon } from '@/utils/documentIcons'
import IconFont from '@/components/IconFont.vue'
import TabBar from '@/components/TabBar.vue'
import FilterTabs from '@/components/FilterTabs.vue'
import NavHeader from '@/components/NavHeader.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
......@@ -147,14 +145,3 @@ const onDelete = (item) => {
})
}
</script>
<style lang="less">
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
......
......@@ -7,14 +7,7 @@
<div class="px-[40rpx] mt-[40rpx]">
<!-- Filter Tabs -->
<div class="flex overflow-x-auto no-scrollbar mb-[40rpx] space-x-[24rpx]">
<div v-for="(tab, index) in tabs" :key="index"
class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
:class="activeTab === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
@click="activeTab = index">
{{ tab }}
</div>
</div>
<FilterTabs v-model="activeTab" :tabs="tabs" wrapper-class="mb-[40rpx]" />
<!-- Section Title -->
<div class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]">
......@@ -62,7 +55,7 @@
<script setup>
import { ref, computed } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import FilterTabs from '@/components/FilterTabs.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
const activeTab = ref(0)
......@@ -140,14 +133,3 @@ const { handleClick: handleProductClick } = useListItemClick({
}
})
</script>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
......
......@@ -20,14 +20,12 @@
<!-- Category Tabs -->
<!-- 根据是否有分类数据决定是否显示 tab -->
<div v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]">
<div class="flex overflow-x-auto no-scrollbar mb-[40rpx] space-x-[24rpx]">
<div v-for="(category, index) in categories" :key="index"
class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
:class="activeCategoryIndex === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
@tap="activeCategoryIndex = index">
{{ category.name }}
</div>
</div>
<FilterTabs
v-model="activeCategoryIndex"
:tabs="categories"
label-key="name"
wrapper-class="mb-[40rpx]"
/>
</div>
<!-- Material List -->
......@@ -370,14 +368,6 @@ const onDelete = (item) => {
</script>
<style lang="less" scoped>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
@keyframes slideIn {
from {
......
......@@ -15,14 +15,7 @@
<!-- Tabs -->
<view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]">
<div class="flex overflow-x-auto no-scrollbar space-x-[24rpx]">
<div v-for="(tab, index) in tabs" :key="index"
class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
:class="activeTab === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
@click="activeTab = index">
{{ tab.title }}
</div>
</div>
<FilterTabs v-model="activeTab" :tabs="tabs" label-key="title" />
</view>
</view>
......@@ -81,7 +74,7 @@ import { ref, computed } from 'vue'
import { useGo } from '@/hooks/useGo'
import { useFileOperation } from '@/composables/useFileOperation'
import IconFont from '@/components/IconFont.vue'
import TabBar from '@/components/TabBar.vue'
import FilterTabs from '@/components/FilterTabs.vue'
import NavHeader from '@/components/NavHeader.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
import Taro from '@tarojs/taro'
......