/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useRef, useEffect } from 'react'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton } from '@douyinfe/semi-ui'; import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; /** * 通用可选择按钮组组件 * * @param {string} title 标题 * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 按钮项 * @param {*|Array} activeValue 当前激活的值,可以是单个值或数组(多选) * @param {(value:any)=>void} onChange 选择改变回调 * @param {function} t i18n * @param {object} style 额外样式 * @param {boolean} collapsible 是否支持折叠,默认true * @param {number} collapseHeight 折叠时的高度,默认200 * @param {boolean} withCheckbox 是否启用前缀 Checkbox 来控制激活状态 * @param {boolean} loading 是否处于加载状态 */ const SelectableButtonGroup = ({ title, items = [], activeValue, onChange, t = (v) => v, style = {}, collapsible = true, collapseHeight = 200, withCheckbox = false, loading = false }) => { const [isOpen, setIsOpen] = useState(false); const [showSkeleton, setShowSkeleton] = useState(loading); const [skeletonCount] = useState(6); const isMobile = useIsMobile(); const perRow = 3; const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32 const needCollapse = collapsible && items.length > perRow * maxVisibleRows; const loadingStartRef = useRef(Date.now()); const contentRef = useRef(null); useEffect(() => { if (loading) { loadingStartRef.current = Date.now(); setShowSkeleton(true); } else { const elapsed = Date.now() - loadingStartRef.current; const remaining = Math.max(0, 500 - elapsed); if (remaining === 0) { setShowSkeleton(false); } else { const timer = setTimeout(() => setShowSkeleton(false), remaining); return () => clearTimeout(timer); } } }, [loading]); const maskStyle = isOpen ? {} : { WebkitMaskImage: 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)', }; const toggle = () => { setIsOpen(!isOpen); }; const linkStyle = { position: 'absolute', left: 0, right: 0, textAlign: 'center', bottom: -10, fontWeight: 400, cursor: 'pointer', fontSize: '12px', color: 'var(--semi-color-text-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, }; const renderSkeletonButtons = () => { const placeholder = ( {Array.from({ length: skeletonCount }).map((_, index) => (
{withCheckbox && ( )}
))}
); return ( ); }; const contentElement = showSkeleton ? renderSkeletonButtons() : ( {items.map((item) => { const isActive = Array.isArray(activeValue) ? activeValue.includes(item.value) : activeValue === item.value; if (withCheckbox) { return ( ); } return ( ); })} ); return (
{title && ( {showSkeleton ? ( ) : ( title )} )} {needCollapse && !showSkeleton ? (
{contentElement} {isOpen ? null : (
{t('展开更多')}
)} {isOpen && (
{t('收起')}
)}
) : ( contentElement )}
); }; export default SelectableButtonGroup;