hookehuyr

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

将资料列表、知识库、收藏和计划书页面的横向筛选标签抽取为通用组件,避免重复的样式和滚动条隐藏逻辑。新增组件示例文件并更新全局组件类型声明。
...@@ -77,6 +77,11 @@ src/ ...@@ -77,6 +77,11 @@ src/
77 ## 📄 页面说明 77 ## 📄 页面说明
78 78
79 - 计划书页面支持顶部搜索与标签固定,列表区域独立滚动,便于快速筛选和浏览 79 - 计划书页面支持顶部搜索与标签固定,列表区域独立滚动,便于快速筛选和浏览
80 +- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs 组件进行横向筛选
81 +
82 +## ✅ 优化建议
83 +
84 +- 新增筛选类页面优先复用 FilterTabs,避免重复样式与交互逻辑
80 85
81 ## ⚙️ 配置说明 86 ## ⚙️ 配置说明
82 87
......
...@@ -8,6 +8,8 @@ export {} ...@@ -8,6 +8,8 @@ export {}
8 declare module 'vue' { 8 declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default'] 10 DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default']
11 + FilterTabs: typeof import('./src/components/FilterTabs.vue')['default']
12 + 'FilterTabs.example': typeof import('./src/components/FilterTabs.example.vue')['default']
11 IconFont: typeof import('./src/components/IconFont.vue')['default'] 13 IconFont: typeof import('./src/components/IconFont.vue')['default']
12 IndexNav: typeof import('./src/components/indexNav.vue')['default'] 14 IndexNav: typeof import('./src/components/indexNav.vue')['default']
13 ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] 15 ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default']
......
...@@ -6,6 +6,26 @@ ...@@ -6,6 +6,26 @@
6 6
7 --- 7 ---
8 8
9 +## [2026-01-31] - 抽取筛选 Tabs 组件并统一使用
10 +
11 +### 新增
12 +- 新增 FilterTabs 组件与小程序示例文件
13 +
14 +### 重构
15 +- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs
16 +- 滚动条隐藏逻辑集中在 FilterTabs 内
17 +
18 +---
19 +
20 +**详细信息**
21 +- **影响文件**: 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
22 +- **技术栈**: Vue 3, Taro, TailwindCSS
23 +- **测试状态**: pnpm lint(存在既有警告)
24 +- **备注**:
25 + - 统一横向筛选标签样式与交互
26 +
27 +---
28 +
9 **详细信息** 29 **详细信息**
10 - **影响文件**: src/pages/plan/index.vue, README.md 30 - **影响文件**: src/pages/plan/index.vue, README.md
11 - **技术栈**: Vue 3, Taro, TailwindCSS 31 - **技术栈**: Vue 3, Taro, TailwindCSS
......
1 +<template>
2 + <view class="p-[24rpx] bg-white">
3 + <FilterTabs
4 + v-model="activeTab"
5 + :tabs="tabs"
6 + label-key="title"
7 + value-key="key"
8 + wrapper-class="mb-[24rpx]"
9 + @change="handleChange"
10 + >
11 + <template #label="{ item }">
12 + <text>{{ item.title }}</text>
13 + </template>
14 + </FilterTabs>
15 + <view class="text-[24rpx] text-gray-500">当前选中:{{ activeTab }}</view>
16 + </view>
17 +</template>
18 +
19 +<script setup>
20 +import { ref } from 'vue'
21 +import FilterTabs from '@/components/FilterTabs.vue'
22 +
23 +const activeTab = ref('all')
24 +const tabs = [
25 + { title: '全部', key: 'all' },
26 + { title: '入职培训', key: 'onboarding' },
27 + { title: '签单相关', key: 'signing' }
28 +]
29 +
30 +const handleChange = (value) => {
31 + console.log('选中项:', value)
32 +}
33 +</script>
1 +<template>
2 + <view :class="['flex overflow-x-auto no-scrollbar space-x-[24rpx]', wrapperClass]">
3 + <view v-for="(item, index) in tabs" :key="getItemKey(item, index)"
4 + :class="[
5 + 'px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors',
6 + tabClass,
7 + isActive(item, index) ? activeClassComputed : inactiveClassComputed
8 + ]"
9 + @tap="handleSelect(item, index)"
10 + >
11 + <slot name="label" :item="item" :index="index">
12 + {{ getItemLabel(item) }}
13 + </slot>
14 + </view>
15 + </view>
16 +</template>
17 +
18 +<script setup>
19 +import { computed } from 'vue'
20 +
21 +const props = defineProps({
22 + tabs: {
23 + type: Array,
24 + default: () => []
25 + },
26 + modelValue: {
27 + type: [String, Number],
28 + default: 0
29 + },
30 + labelKey: {
31 + type: String,
32 + default: 'title'
33 + },
34 + valueKey: {
35 + type: String,
36 + default: ''
37 + },
38 + wrapperClass: {
39 + type: String,
40 + default: ''
41 + },
42 + tabClass: {
43 + type: String,
44 + default: ''
45 + },
46 + activeClass: {
47 + type: String,
48 + default: ''
49 + },
50 + inactiveClass: {
51 + type: String,
52 + default: ''
53 + }
54 +})
55 +
56 +const emit = defineEmits(['update:modelValue', 'change'])
57 +
58 +const activeClassComputed = computed(() => {
59 + return props.activeClass || 'bg-[#2563EB] text-white'
60 +})
61 +
62 +const inactiveClassComputed = computed(() => {
63 + return props.inactiveClass || 'bg-[#F3F4F6] text-[#6B7280]'
64 +})
65 +
66 +const getItemLabel = (item) => {
67 + if (typeof item === 'string') return item
68 + return item?.[props.labelKey] ?? ''
69 +}
70 +
71 +const getItemValue = (item, index) => {
72 + if (typeof item !== 'object' || item === null) return index
73 + if (props.valueKey && item[props.valueKey] !== undefined) return item[props.valueKey]
74 + return index
75 +}
76 +
77 +const getItemKey = (item, index) => {
78 + const value = getItemValue(item, index)
79 + return value ?? index
80 +}
81 +
82 +const isActive = (item, index) => {
83 + return getItemValue(item, index) === props.modelValue
84 +}
85 +
86 +const handleSelect = (item, index) => {
87 + const value = getItemValue(item, index)
88 + if (value === props.modelValue) return
89 + emit('update:modelValue', value)
90 + emit('change', value)
91 +}
92 +</script>
93 +
94 +<style lang="less" scoped>
95 +:deep(.no-scrollbar::-webkit-scrollbar) {
96 + display: none;
97 + width: 0;
98 + height: 0;
99 +}
100 +
101 +:deep(.no-scrollbar) {
102 + -ms-overflow-style: none;
103 + scrollbar-width: none;
104 +}
105 +</style>
...@@ -5,14 +5,12 @@ ...@@ -5,14 +5,12 @@
5 5
6 <!-- Tabs Section --> 6 <!-- Tabs Section -->
7 <view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]"> 7 <view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]">
8 - <div class="flex overflow-x-auto no-scrollbar space-x-[24rpx]"> 8 + <FilterTabs
9 - <div v-for="(tab, index) in tabs" :key="index" 9 + v-model="activeTab"
10 - class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors" 10 + :tabs="tabs"
11 - :class="activeTab === tab.key ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'" 11 + label-key="title"
12 - @click="activeTab = tab.key"> 12 + value-key="key"
13 - {{ tab.title }} 13 + />
14 - </div>
15 - </div>
16 </view> 14 </view>
17 15
18 <!-- List Section --> 16 <!-- List Section -->
...@@ -71,7 +69,7 @@ import { useGo } from '@/hooks/useGo' ...@@ -71,7 +69,7 @@ import { useGo } from '@/hooks/useGo'
71 import { useFileOperation } from '@/composables/useFileOperation' 69 import { useFileOperation } from '@/composables/useFileOperation'
72 import { getDocumentIcon } from '@/utils/documentIcons' 70 import { getDocumentIcon } from '@/utils/documentIcons'
73 import IconFont from '@/components/IconFont.vue' 71 import IconFont from '@/components/IconFont.vue'
74 -import TabBar from '@/components/TabBar.vue' 72 +import FilterTabs from '@/components/FilterTabs.vue'
75 import NavHeader from '@/components/NavHeader.vue' 73 import NavHeader from '@/components/NavHeader.vue'
76 import ListItemActions from '@/components/ListItemActions/index.vue' 74 import ListItemActions from '@/components/ListItemActions/index.vue'
77 75
...@@ -147,14 +145,3 @@ const onDelete = (item) => { ...@@ -147,14 +145,3 @@ const onDelete = (item) => {
147 }) 145 })
148 } 146 }
149 </script> 147 </script>
150 -
151 -<style lang="less">
152 -.no-scrollbar::-webkit-scrollbar {
153 - display: none;
154 -}
155 -
156 -.no-scrollbar {
157 - -ms-overflow-style: none;
158 - scrollbar-width: none;
159 -}
160 -</style>
......
...@@ -7,14 +7,7 @@ ...@@ -7,14 +7,7 @@
7 <div class="px-[40rpx] mt-[40rpx]"> 7 <div class="px-[40rpx] mt-[40rpx]">
8 8
9 <!-- Filter Tabs --> 9 <!-- Filter Tabs -->
10 - <div class="flex overflow-x-auto no-scrollbar mb-[40rpx] space-x-[24rpx]"> 10 + <FilterTabs v-model="activeTab" :tabs="tabs" wrapper-class="mb-[40rpx]" />
11 - <div v-for="(tab, index) in tabs" :key="index"
12 - class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
13 - :class="activeTab === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
14 - @click="activeTab = index">
15 - {{ tab }}
16 - </div>
17 - </div>
18 11
19 <!-- Section Title --> 12 <!-- Section Title -->
20 <div class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]"> 13 <div class="text-[#1F2937] text-[32rpx] font-bold mb-[24rpx]">
...@@ -62,7 +55,7 @@ ...@@ -62,7 +55,7 @@
62 <script setup> 55 <script setup>
63 import { ref, computed } from 'vue' 56 import { ref, computed } from 'vue'
64 import NavHeader from '@/components/NavHeader.vue' 57 import NavHeader from '@/components/NavHeader.vue'
65 -import TabBar from '@/components/TabBar.vue' 58 +import FilterTabs from '@/components/FilterTabs.vue'
66 import { useListItemClick, ListType } from '@/composables/useListItemClick' 59 import { useListItemClick, ListType } from '@/composables/useListItemClick'
67 60
68 const activeTab = ref(0) 61 const activeTab = ref(0)
...@@ -140,14 +133,3 @@ const { handleClick: handleProductClick } = useListItemClick({ ...@@ -140,14 +133,3 @@ const { handleClick: handleProductClick } = useListItemClick({
140 } 133 }
141 }) 134 })
142 </script> 135 </script>
143 -
144 -<style>
145 -.no-scrollbar::-webkit-scrollbar {
146 - display: none;
147 -}
148 -
149 -.no-scrollbar {
150 - -ms-overflow-style: none;
151 - scrollbar-width: none;
152 -}
153 -</style>
......
...@@ -20,14 +20,12 @@ ...@@ -20,14 +20,12 @@
20 <!-- Category Tabs --> 20 <!-- Category Tabs -->
21 <!-- 根据是否有分类数据决定是否显示 tab --> 21 <!-- 根据是否有分类数据决定是否显示 tab -->
22 <div v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]"> 22 <div v-if="categories && categories.length > 0" class="px-[32rpx] mt-[32rpx]">
23 - <div class="flex overflow-x-auto no-scrollbar mb-[40rpx] space-x-[24rpx]"> 23 + <FilterTabs
24 - <div v-for="(category, index) in categories" :key="index" 24 + v-model="activeCategoryIndex"
25 - class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors" 25 + :tabs="categories"
26 - :class="activeCategoryIndex === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'" 26 + label-key="name"
27 - @tap="activeCategoryIndex = index"> 27 + wrapper-class="mb-[40rpx]"
28 - {{ category.name }} 28 + />
29 - </div>
30 - </div>
31 </div> 29 </div>
32 30
33 <!-- Material List --> 31 <!-- Material List -->
...@@ -370,14 +368,6 @@ const onDelete = (item) => { ...@@ -370,14 +368,6 @@ const onDelete = (item) => {
370 </script> 368 </script>
371 369
372 <style lang="less" scoped> 370 <style lang="less" scoped>
373 -.no-scrollbar::-webkit-scrollbar {
374 - display: none;
375 -}
376 -
377 -.no-scrollbar {
378 - -ms-overflow-style: none;
379 - scrollbar-width: none;
380 -}
381 371
382 @keyframes slideIn { 372 @keyframes slideIn {
383 from { 373 from {
......
...@@ -15,14 +15,7 @@ ...@@ -15,14 +15,7 @@
15 15
16 <!-- Tabs --> 16 <!-- Tabs -->
17 <view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]"> 17 <view class="bg-white mt-[2rpx] px-[24rpx] py-[20rpx]">
18 - <div class="flex overflow-x-auto no-scrollbar space-x-[24rpx]"> 18 + <FilterTabs v-model="activeTab" :tabs="tabs" label-key="title" />
19 - <div v-for="(tab, index) in tabs" :key="index"
20 - class="px-[32rpx] py-[16rpx] rounded-full text-[28rpx] whitespace-nowrap transition-colors"
21 - :class="activeTab === index ? 'bg-[#2563EB] text-white' : 'bg-[#F3F4F6] text-[#6B7280]'"
22 - @click="activeTab = index">
23 - {{ tab.title }}
24 - </div>
25 - </div>
26 </view> 19 </view>
27 </view> 20 </view>
28 21
...@@ -81,7 +74,7 @@ import { ref, computed } from 'vue' ...@@ -81,7 +74,7 @@ import { ref, computed } from 'vue'
81 import { useGo } from '@/hooks/useGo' 74 import { useGo } from '@/hooks/useGo'
82 import { useFileOperation } from '@/composables/useFileOperation' 75 import { useFileOperation } from '@/composables/useFileOperation'
83 import IconFont from '@/components/IconFont.vue' 76 import IconFont from '@/components/IconFont.vue'
84 -import TabBar from '@/components/TabBar.vue' 77 +import FilterTabs from '@/components/FilterTabs.vue'
85 import NavHeader from '@/components/NavHeader.vue' 78 import NavHeader from '@/components/NavHeader.vue'
86 import ListItemActions from '@/components/ListItemActions/index.vue' 79 import ListItemActions from '@/components/ListItemActions/index.vue'
87 import Taro from '@tarojs/taro' 80 import Taro from '@tarojs/taro'
......