<!-- Generated by SpecStory -->

# 重构 ActionGroup 组件以支持 Drawer (2025-06-28 20:45:09)

_**User**_

在 action group 在 mobile 上点击 button 也需要唤出和 action panel 一样的 drawer。注意组件封装

---

_**Assistant**_

我需要修改 ActionGroup 组件，让它在移动端点击按钮时也能唤出 Drawer，而不是 DropdownMenu。我会重构代码以实现更好的组件封装。

---

```diff
  import { photoLoader } from '@afilmory/data'
  import { useAtom } from 'jotai'
+ import { useState } from 'react'
  import { useTranslation } from 'react-i18next'
  import { Drawer } from 'vaul'
```

```diff
    )
  }
+ 
+ // 通用的操作按钮组件
+ const ActionButton = ({
+   icon,
+   title,
+   badge,
+   onClick,
+ }: {
+   icon: string
+   title: string
+   badge?: number | string
+   onClick: () => void
+ }) => {
+   return (
+     <Button
+       variant="ghost"
+       size="sm"
+       className="relative h-10 w-10 rounded-full border-0 bg-gray-100 transition-all duration-200 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
+       title={title}
+       onClick={onClick}
+     >
+       <i className={clsxm(icon, 'text-base text-gray-600 dark:text-gray-300')} />
+       {badge && (
+         <span className="bg-accent absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full text-xs font-medium text-white shadow-sm">
+           {badge}
+         </span>
+       )}
+     </Button>
+   )
+ }
+ 
+ // 桌面端的下拉菜单按钮
+ const DesktopActionButton = ({
+   icon,
+   title,
+   badge,
+   children,
+   contentClassName,
+ }: {
+   icon: string
+   title: string
+   badge?: number | string
+   children: React.ReactNode
+   contentClassName?: string
+ }) => {
+   return (
+     <DropdownMenu>
+       <DropdownMenuTrigger asChild>
+         <ActionButton icon={icon} title={title} badge={badge} onClick={() => {}} />
+       </DropdownMenuTrigger>
+       <DropdownMenuContent align="center" className={contentClassName}>
+         {children}
+       </DropdownMenuContent>
+     </DropdownMenu>
+   )
+ }
+ 
+ // 移动端的抽屉按钮
+ const MobileActionButton = ({
+   icon,
+   title,
+   badge,
+   children,
+   open,
+   onOpenChange,
+ }: {
+   icon: string
+   title: string
+   badge?: number | string
+   children: React.ReactNode
+   open: boolean
+   onOpenChange: (open: boolean) => void
+ }) => {
+   return (
+     <>
+       <ActionButton
+         icon={icon}
+         title={title}
+         badge={badge}
+         onClick={() => onOpenChange(!open)}
+       />
+       <Drawer.Root open={open} onOpenChange={onOpenChange}>
+         <Drawer.Portal>
+           <Drawer.Overlay className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm" />
+           <Drawer.Content className="fixed right-0 bottom-0 left-0 z-50 flex flex-col rounded-t-2xl border-t border-zinc-200 bg-white/80 p-4 backdrop-blur-xl dark:border-zinc-800 dark:bg-black/80">
+             <div className="mx-auto mb-4 h-1.5 w-12 flex-shrink-0 rounded-full bg-zinc-300 dark:bg-zinc-700" />
+             {children}
+           </Drawer.Content>
+         </Drawer.Portal>
+       </Drawer.Root>
+     </>
+   )
+ }
+ 
+ // 响应式操作按钮组件
+ const ResponsiveActionButton = ({
+   icon,
+   title,
+   badge,
+   children,
+   contentClassName,
+ }: {
+   icon: string
+   title: string
+   badge?: number | string
+   children: React.ReactNode
+   contentClassName?: string
+ }) => {
+   const isMobile = useMobile()
+   const [open, setOpen] = useState(false)
+ 
+   if (isMobile) {
+     return (
+       <MobileActionButton
+         icon={icon}
+         title={title}
+         badge={badge}
+         open={open}
+         onOpenChange={setOpen}
+       >
+         {children}
+       </MobileActionButton>
+     )
+   }
+ 
+   return (
+     <DesktopActionButton
+       icon={icon}
+       title={title}
+       badge={badge}
+       contentClassName={contentClassName}
+     >
+       {children}
+     </DesktopActionButton>
+   )
+ }
  
  export const ActionGroup = () => {
```

```diff
      <div className="flex items-center justify-center gap-3">
        {/* 标签筛选按钮 */}
-       <DropdownMenu>
-         <DropdownMenuTrigger asChild>
-           <Button
-             variant="ghost"
-             size="sm"
-             className="relative h-10 w-10 rounded-full border-0 bg-gray-100 transition-all duration-200 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
-             title={t('action.tag.filter')}
-           >
-             <i className="i-mingcute-tag-line text-base text-gray-600 dark:text-gray-300" />
-             {gallerySetting.selectedTags.length > 0 && (
-               <span className="bg-accent absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full text-xs font-medium text-white shadow-sm">
-                 {gallerySetting.selectedTags.length}
-               </span>
-             )}
-           </Button>
-         </DropdownMenuTrigger>
- 
-         <DropdownMenuContent align="center">
-           <TagsPanel />
-         </DropdownMenuContent>
-       </DropdownMenu>
- 
-       <AdjustColumnsButton />
+       <ResponsiveActionButton
+         icon="i-mingcute-tag-line"
+         title={t('action.tag.filter')}
+         badge={gallerySetting.selectedTags.length > 0 ? gallerySetting.selectedTags.length : undefined}
+       >
+         <TagsPanel />
+       </ResponsiveActionButton>
+ 
+       {/* 列数调整按钮 */}
+       <ResponsiveActionButton
+         icon="i-mingcute-grid-line"
+         title={t('action.columns.setting')}
+         badge={gallerySetting.columns !== 'auto' ? gallerySetting.columns : undefined}
+       >
+         <ColumnsPanel />
+       </ResponsiveActionButton>
  
        {/* 排序按钮 */}
-       <DropdownMenu>
-         <DropdownMenuTrigger asChild>
-           <Button
-             variant="ghost"
-             size="sm"
-             className="h-10 w-10 rounded-full border-0 bg-gray-100 transition-all duration-200 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
-             title={t('action.sort.mode')}
-           >
-             {gallerySetting.sortOrder === 'desc' ? (
-               <i className="i-mingcute-sort-descending-line text-base text-gray-600 dark:text-gray-300" />
-             ) : (
-               <i className="i-mingcute-sort-ascending-line text-base text-gray-600 dark:text-gray-300" />
-             )}
-           </Button>
-         </DropdownMenuTrigger>
- 
-         <DropdownMenuContent align="center" className="w-48">
-           <SortPanel />
-         </DropdownMenuContent>
-       </DropdownMenu>
+       <ResponsiveActionButton
+         icon={gallerySetting.sortOrder === 'desc' ? 'i-mingcute-sort-descending-line' : 'i-mingcute-sort-ascending-line'}
+         title={t('action.sort.mode')}
+         contentClassName="w-48"
+       >
+         <SortPanel />
+       </ResponsiveActionButton>
      </div>
    )
  }
- 
- const AdjustColumnsButton = () => {
-   const { t } = useTranslation()
-   const [gallerySetting] = useAtom(gallerySettingAtom)
- 
-   return (
-     <DropdownMenu>
-       <DropdownMenuTrigger asChild>
-         <Button
-           variant="ghost"
-           size="sm"
-           className="relative h-10 w-10 rounded-full border-0 bg-gray-100 transition-all duration-200 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
-           title={t('action.columns.setting')}
-         >
-           <i className="i-mingcute-grid-line text-base text-gray-600 dark:text-gray-300" />
-           {gallerySetting.columns !== 'auto' && (
-             <span className="bg-accent absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full text-xs font-medium text-white shadow-sm">
-               {gallerySetting.columns}
-             </span>
-           )}
-         </Button>
-       </DropdownMenuTrigger>
-       <DropdownMenuContent align="center">
-         <ColumnsPanel />
-       </DropdownMenuContent>
-     </DropdownMenu>
-   )
- }
  
  const panelMap = {
```

---

我已经成功重构了 `ActionGroup` 组件，现在它具有以下特性：

## 主要改进

1. **响应式设计**：
   - 桌面端：使用 `DropdownMenu` 显示面板
   - 移动端：使用 `Drawer` 显示面板

2. **组件封装优化**：
   - `ActionButton`：基础按钮组件，包含图标、标题和徽章显示
   - `DesktopActionButton`：桌面端的下拉菜单按钮
   - `MobileActionButton`：移动端的抽屉按钮
   - `ResponsiveActionButton`：根据设备类型自动选择合适的交互方式

3. **代码复用**：
   - 移除了重复的按钮样式代码
   - 统一了按钮的外观和行为
   - 简化了 `ActionGroup` 的主要逻辑

4. **移动端体验**：
   - 在移动端点击任何操作按钮都会唤出 Drawer
   - Drawer 的样式与现有的 `ActionPanel` 保持一致
   - 每个按钮都有独立的状态管理

## 核心特性

- **自动适配**：`ResponsiveActionButton` 会根据 `useMobile()` hook 自动选择桌面端或移动端的交互方式
- **统一样式**：所有按钮都使用相同的基础样式和徽章显示逻辑
- **独立状态**：每个移动端按钮都有自己的 `open` 状态，不会相互干扰
- **保持兼容**：桌面端的体验保持不变，移动端获得了更好的交互体验

现在在移动端点击任何操作按钮（标签筛选、列数调整、排序）都会唤出相应的 Drawer 面板，提供更好的移动端用户体验。

---

_**User**_

你把 @FloatingActionButton.tsx 的逻辑改坏了

---

_**Assistant**_



---

