返回 AiToEarn
1 /**
2 * PlanTabBar - 草稿箱 Tab 栏
3 * 可滚动 Tab + 更多按钮 + 新建按钮
4 * 两页面(线下推广 / 草稿箱)复用
5 */
6
7 'use client'
8
9 import { Ellipsis, Plus } from 'lucide-react'
10 import { useEffect, useRef } from 'react'
11 import { useShallow } from 'zustand/react/shallow'
12 import { useTransClient } from '@/app/i18n/client'
13 import { Button } from '@/components/ui/button'
14 import { Skeleton } from '@/components/ui/skeleton'
15 import { useIsMobile } from '@/hooks/useIsMobile'
16 import { useBrandPromotionStore } from '@/store/draft-box/brandPromotionStore'
17 import { usePlanTabStore } from '@/store/draft-box/planTabStore'
18 import { cn } from '@/utils/className'
19 import MorePanel from './MorePanel'
20 import styles from './PlanTabBar.module.scss'
21
22 interface PlanTabBarProps {
23 onPlanChange?: (planId: string) => void
24 }
25
26 function PlanTabBar({ onPlanChange }: PlanTabBarProps) {
27 const { t } = useTransClient('brandPromotion')
28 const isMobile = useIsMobile()
29 const scrollRef = useRef<HTMLDivElement>(null)
30 const activeTabRef = useRef<HTMLButtonElement>(null)
31
32 const {
33 tabPlans,
34 tabPlansLoading,
35 selectedPlanId,
36 } = usePlanTabStore(
37 useShallow(state => ({
38 tabPlans: state.tabPlans,
39 tabPlansLoading: state.tabPlansLoading,
40 selectedPlanId: state.selectedPlanId,
41 })),
42 )
43
44 const selectPlan = usePlanTabStore(state => state.selectPlan)
45 const openCreatePlanModal = useBrandPromotionStore(state => state.openCreatePlanModal)
46
47 // PC端鼠标滚轮水平滚动
48 useEffect(() => {
49 const el = scrollRef.current
50 if (!el || isMobile)
51 return
52
53 const onWheel = (e: WheelEvent) => {
54 if (e.deltaY === 0)
55 return
56 e.preventDefault()
57 el.scrollLeft += e.deltaY
58 }
59
60 el.addEventListener('wheel', onWheel, { passive: false })
61 return () => el.removeEventListener('wheel', onWheel)
62 }, [isMobile])
63
64 // 选中 Tab 后自动滚动到容器水平居中位置
65 useEffect(() => {
66 if (activeTabRef.current && scrollRef.current) {
67 const container = scrollRef.current
68 const tab = activeTabRef.current
69 const scrollLeft = tab.offsetLeft - container.offsetWidth / 2 + tab.offsetWidth / 2
70 container.scrollTo({ left: scrollLeft, behavior: 'smooth' })
71 }
72 }, [selectedPlanId])
73
74 const handleTabClick = (planId: string) => {
75 if (planId === selectedPlanId) {
76 return
77 }
78 selectPlan(planId)
79 onPlanChange?.(planId)
80 }
81
82 if (tabPlansLoading) {
83 return (
84 <div className="flex items-center gap-2 px-4 py-2 border-b border-border">
85 <Skeleton className="h-8 w-20" />
86 <Skeleton className="h-8 w-20" />
87 <Skeleton className="h-8 w-20" />
88 </div>
89 )
90 }
91
92 if (tabPlans.length === 0)
93 return null
94
95 return (
96 <div className="flex items-center border-b border-border bg-background">
97 {/* 可滚动 Tab 区域 */}
98 <div
99 ref={scrollRef}
100 className={cn(
101 'flex-1 flex items-center overflow-x-auto min-w-0',
102 styles.scrollContainer,
103 )}
104 >
105 {tabPlans.map(plan => (
106 <button
107 key={plan.id}
108 ref={plan.id === selectedPlanId ? activeTabRef : undefined}
109 className={cn(
110 'shrink-0 px-4 py-2.5 text-sm cursor-pointer transition-colors whitespace-nowrap',
111 'hover:text-foreground',
112 'border-b-2 border-transparent',
113 plan.id === selectedPlanId
114 ? 'border-b-primary text-foreground font-medium'
115 : 'text-muted-foreground',
116 )}
117 onClick={() => handleTabClick(plan.id)}
118 >
119 {plan.name || plan.title}
120 </button>
121 ))}
122 </div>
123
124 {/* 右侧固定按钮区域 */}
125 <div className="flex items-center shrink-0 border-l border-border px-1 gap-0.5">
126 {/* 更多按钮 */}
127 <MorePanel
128 trigger={(
129 <Button
130 variant="ghost"
131 size="sm"
132 className="cursor-pointer h-8 px-2"
133 >
134 <Ellipsis className="h-4 w-4" />
135 {!isMobile && <span className="ml-1">{t('planTab.more')}</span>}
136 </Button>
137 )}
138 onPlanChange={onPlanChange}
139 />
140
141 {/* 新建按钮 */}
142 <Button
143 variant="ghost"
144 size="sm"
145 className="cursor-pointer h-8 px-2"
146 onClick={openCreatePlanModal}
147 >
148 <Plus className="h-4 w-4" />
149 {!isMobile && <span className="ml-1">{t('planTab.newPlan')}</span>}
150 </Button>
151 </div>
152 </div>
153 )
154 }
155
156 export default PlanTabBar
157
157 lines Plain Text