|
|
@@ -0,0 +1,111 @@
|
|
|
+import React, { useRef } from 'react';
|
|
|
+import { useScrollIndicator, useTouchScroll } from '../hooks/useScrollIndicator';
|
|
|
+import { INK_COLORS } from '../styles/colors';
|
|
|
+import { FONT_STYLES } from '../styles/typography';
|
|
|
+
|
|
|
+interface Category {
|
|
|
+ name: string;
|
|
|
+ path?: string;
|
|
|
+ image?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface CategoryScrollProps {
|
|
|
+ categories: Category[];
|
|
|
+ onNavigate: (path: string) => void;
|
|
|
+}
|
|
|
+
|
|
|
+export const CategoryScroll: React.FC<CategoryScrollProps> = ({ categories, onNavigate }) => {
|
|
|
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
|
+ const { showIndicator } = useScrollIndicator({
|
|
|
+ containerRef: scrollContainerRef,
|
|
|
+ threshold: 0.1
|
|
|
+ });
|
|
|
+ const { handleTouchStart, handleTouchEnd, handleTouchMove } = useTouchScroll();
|
|
|
+
|
|
|
+ if (categories.length === 0) return null;
|
|
|
+
|
|
|
+ const itemsPerRow = 4;
|
|
|
+ const totalRows = 2;
|
|
|
+ const visibleItems = Math.min(itemsPerRow * totalRows, categories.length);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="relative">
|
|
|
+ <div
|
|
|
+ ref={scrollContainerRef}
|
|
|
+ className="overflow-x-auto pb-2 scroll-container"
|
|
|
+ style={{
|
|
|
+ WebkitOverflowScrolling: 'touch',
|
|
|
+ height: '200px',
|
|
|
+ cursor: 'grab'
|
|
|
+ }}
|
|
|
+ onTouchStart={handleTouchStart}
|
|
|
+ onTouchEnd={handleTouchEnd}
|
|
|
+ onTouchMove={handleTouchMove}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="grid gap-3"
|
|
|
+ style={{
|
|
|
+ gridTemplateColumns: `repeat(${Math.max(8, categories.length)}, 1fr)`,
|
|
|
+ gridTemplateRows: 'repeat(2, 1fr)',
|
|
|
+ minWidth: `${Math.max(categories.length * 82, 340)}px`,
|
|
|
+ width: 'max-content',
|
|
|
+ height: '100%',
|
|
|
+ paddingRight: '30px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {categories.map((category, index) => (
|
|
|
+ <button
|
|
|
+ key={index}
|
|
|
+ onClick={() => onNavigate(category.path || '/')}
|
|
|
+ className="category-item flex flex-col items-center justify-center p-2 rounded-xl transition-all duration-300 hover:shadow-lg backdrop-blur-sm cursor-pointer touch-feedback"
|
|
|
+ style={{
|
|
|
+ backgroundColor: 'rgba(255,255,255,0.7)',
|
|
|
+ border: `1px solid ${INK_COLORS.ink.medium}`,
|
|
|
+ color: INK_COLORS.text.primary,
|
|
|
+ width: '75px',
|
|
|
+ height: '85px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="w-12 h-12 rounded-full flex items-center justify-center shadow-sm mb-1"
|
|
|
+ style={{ backgroundColor: INK_COLORS.accent.blue, color: 'white' }}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src={category.image || '/images/placeholder.jpg'}
|
|
|
+ alt={category.name}
|
|
|
+ className="w-7 h-7 object-cover rounded-full"
|
|
|
+ onError={(e) => {
|
|
|
+ (e.target as HTMLImageElement).style.display = 'none';
|
|
|
+ (e.target as HTMLImageElement).parentElement!.innerHTML = '📱';
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <span
|
|
|
+ className={`${FONT_STYLES.caption} text-xs text-center line-clamp-2`}
|
|
|
+ style={{ fontSize: '11px', fontWeight: 500 }}
|
|
|
+ >
|
|
|
+ {category.name}
|
|
|
+ </span>
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 智能滚动指示器 */}
|
|
|
+ {showIndicator && categories.length > visibleItems && (
|
|
|
+ <div className="absolute right-0 top-0 bottom-0 flex items-center pointer-events-none">
|
|
|
+ <div className="w-8 h-full bg-gradient-to-l from-ink-light via-ink-light/80 to-transparent"></div>
|
|
|
+ <div className="absolute right-1 top-1/2 -translate-y-1/2">
|
|
|
+ <div className="w-6 h-6 rounded-full bg-white shadow-lg flex items-center justify-center scroll-indicator">
|
|
|
+ <svg className="w-3 h-3 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default CategoryScroll;
|