Browse Source

✨ feat(category): 新增分类标签栏组件

- 添加CategoryTabbar组件,支持横向滚动和选中状态显示
- 实现CategoryTabbarMore组件,当标签数量超过4个时显示更多选项
- 定义TabItem接口及相关组件属性类型
- 添加组件样式文件,包含响应式布局和交互效果
- 在分类组件入口文件导出新组件及类型定义
yourname 2 months ago
parent
commit
3935b537e3

+ 72 - 0
mini/src/components/category/CategoryTabbar/CategoryTabbar.css

@@ -0,0 +1,72 @@
+.c-tabbar {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  --tabbar-height: 100rpx;
+  --tabbar-fontsize: 28rpx;
+  --tabbar-background-color: white;
+}
+
+.c-tabbar__inner {
+  display: flex;
+  flex-flow: row nowrap;
+}
+
+.c-tabbar__scroll {
+  position: relative;
+}
+
+.c-tabbar__scroll::after {
+  content: '';
+  display: block;
+  position: absolute;
+  width: 100%;
+  left: 0;
+  bottom: -1px;
+  height: 1px;
+  background-color: #eee;
+  z-index: 1;
+}
+
+.c-tabbar__inner.c-tabbar__inner_more::after {
+  content: '';
+  display: block;
+  width: 100rpx;
+  height: 100rpx;
+  flex: none;
+}
+
+.c-tabbar-item {
+  flex: none;
+  height: 100rpx;
+  color: #282828;
+  font-size: 28rpx;
+  padding: 0 20rpx;
+}
+
+.c-tabbar-item.active:not(.disabled) {
+  color: #0071ce;
+  position: relative;
+}
+
+.c-tabbar-item.active:not(.disabled)::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 20rpx;
+  right: 20rpx;
+  height: 4rpx;
+  background-color: #0071ce;
+  border-radius: 2rpx;
+}
+
+.c-tabbar-item.disabled {
+  color: #ccc;
+}
+
+.c-tabbar-item__text {
+  width: 100%;
+  text-align: center;
+  height: 100rpx;
+  line-height: 100rpx;
+}

+ 68 - 0
mini/src/components/category/CategoryTabbar/index.tsx

@@ -0,0 +1,68 @@
+import React, { useState, useCallback } from 'react';
+import { View, ScrollView } from '@tarojs/components';
+import CategoryTabbarMore from '../CategoryTabbarMore';
+import './CategoryTabbar.css';
+
+interface TabItem {
+  id: string;
+  name: string;
+  disabled?: boolean;
+}
+
+interface CategoryTabbarProps {
+  tabList: TabItem[];
+  currentActive?: string;
+  showMore?: boolean;
+  onChange?: (id: string) => void;
+  className?: string;
+}
+
+const CategoryTabbar: React.FC<CategoryTabbarProps> = ({
+  tabList,
+  currentActive,
+  showMore = true,
+  onChange,
+  className = '',
+}) => {
+  const [activeId, setActiveId] = useState<string | undefined>(currentActive);
+
+  const handleItemClick = useCallback((id: string) => {
+    setActiveId(id);
+    onChange?.(id);
+  }, [onChange]);
+
+  const shouldShowMore = showMore && tabList.length > 4;
+
+  return (
+    <View className={`c-tabbar ${className}`}>
+      {tabList.length > 0 && (
+        <ScrollView
+          className="c-tabbar__scroll"
+          scrollX
+          scrollIntoView={activeId ? `id-${activeId}` : undefined}
+        >
+          <View className={`c-tabbar__inner ${shouldShowMore ? 'c-tabbar__inner_more' : ''}`}>
+            {tabList.map((item) => (
+              <View
+                key={item.id}
+                id={`id-${item.id}`}
+                className={`c-tabbar-item ${activeId === item.id ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
+                onClick={() => !item.disabled && handleItemClick(item.id)}
+              >
+                <View className="c-tabbar-item__text">{item.name}</View>
+              </View>
+            ))}
+          </View>
+        </ScrollView>
+      )}
+      {shouldShowMore && (
+        <CategoryTabbarMore
+          tabList={tabList}
+          onSelect={handleItemClick}
+        />
+      )}
+    </View>
+  );
+};
+
+export default CategoryTabbar;

+ 83 - 0
mini/src/components/category/CategoryTabbarMore/CategoryTabbarMore.css

@@ -0,0 +1,83 @@
+.c-tabbar-more {
+  width: 100%;
+  height: calc(100% - var(--tabbar-height, 100rpx));
+  position: absolute;
+  top: var(--tabbar-height, 100rpx);
+}
+
+.c-tabbar-more__btn {
+  position: absolute;
+  top: calc(0rpx - var(--tabbar-height, 100rpx));
+  right: 0;
+  width: 80rpx;
+  height: var(--tabbar-height, 100rpx);
+  line-height: var(--tabbar-height, 100rpx);
+  background-color: var(--tabbar-background-color, white);
+  box-shadow: -20rpx 0 20rpx -10rpx var(--tabbar-background-color, white);
+  text-align: center;
+}
+
+.wr {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  border-left: 8rpx solid transparent;
+  border-right: 8rpx solid transparent;
+}
+
+.wr-arrow-down {
+  border-top: 8rpx solid #666;
+}
+
+.wr-arrow-up {
+  border-bottom: 8rpx solid #666;
+}
+
+.t-tabbar-more__boardwrapper {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.t-tabbar-more__mask {
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.c-tabbar-more__board {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  max-height: 100%;
+}
+
+.c-tabbar-more__boardinner {
+  padding: 20rpx 0 20rpx 20rpx;
+  background-color: var(--tabbar-background-color, white);
+  display: flex;
+  flex-flow: row wrap;
+}
+
+.c-tabbar-more__item {
+  margin: 0 20rpx 20rpx 0;
+  flex: 0 0 calc((100% - 60rpx) / 3);
+  box-sizing: border-box;
+  padding: 0 10rpx;
+  border-radius: 30rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+  text-align: center;
+  font-size: 22rpx;
+  color: #5d5d5d;
+  background-color: #eee;
+}
+
+.text-overflow {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}

+ 58 - 0
mini/src/components/category/CategoryTabbarMore/index.tsx

@@ -0,0 +1,58 @@
+import React, { useState, useCallback } from 'react';
+import { View, ScrollView } from '@tarojs/components';
+import './CategoryTabbarMore.css';
+
+interface TabItem {
+  id: string;
+  name: string;
+  disabled?: boolean;
+}
+
+interface CategoryTabbarMoreProps {
+  tabList: TabItem[];
+  onSelect?: (id: string) => void;
+}
+
+const CategoryTabbarMore: React.FC<CategoryTabbarMoreProps> = ({
+  tabList,
+  onSelect,
+}) => {
+  const [unfolded, setUnfolded] = useState(false);
+
+  const handleToggle = useCallback(() => {
+    setUnfolded(!unfolded);
+  }, [unfolded]);
+
+  const handleItemSelect = useCallback((id: string) => {
+    setUnfolded(false);
+    onSelect?.(id);
+  }, [onSelect]);
+
+  return (
+    <View className="c-tabbar-more">
+      <View className="c-tabbar-more__btn" onClick={handleToggle}>
+        <View className={`wr ${unfolded ? 'wr-arrow-up' : 'wr-arrow-down'}`}></View>
+      </View>
+      {unfolded && (
+        <View className="t-tabbar-more__boardwrapper">
+          <View className="t-tabbar-more__mask" onClick={() => setUnfolded(false)} />
+          <ScrollView className="c-tabbar-more__board" scrollY>
+            <View className="c-tabbar-more__boardinner">
+              {tabList.map((item) => (
+                <View
+                  key={item.id}
+                  className="c-tabbar-more__item text-overflow"
+                  onClick={() => !item.disabled && handleItemSelect(item.id)}
+                >
+                  {item.name}
+                </View>
+              ))}
+            </View>
+          </ScrollView>
+        </View>
+      )}
+    </View>
+  );
+};
+
+export default CategoryTabbarMore;

+ 5 - 1
mini/src/components/category/index.ts

@@ -1,5 +1,9 @@
 // 导出商品分类相关组件
 // 导出商品分类相关组件
 export { default as CategorySidebar } from './CategorySidebar';
 export { default as CategorySidebar } from './CategorySidebar';
 export { default as CategorySidebarItem } from './CategorySidebarItem';
 export { default as CategorySidebarItem } from './CategorySidebarItem';
+export { default as CategoryTabbar } from './CategoryTabbar';
+export { default as CategoryTabbarMore } from './CategoryTabbarMore';
 export type { CategorySidebarProps } from './CategorySidebar';
 export type { CategorySidebarProps } from './CategorySidebar';
-export type { CategorySidebarItemProps } from './CategorySidebarItem';
+export type { CategorySidebarItemProps } from './CategorySidebarItem';
+export type { CategoryTabbarProps, TabItem as CategoryTabItem } from './CategoryTabbar';
+export type { CategoryTabbarMoreProps } from './CategoryTabbarMore';