SelectableButtonGroup.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useState, useRef } from 'react';
  16. import { Divider, Button, Tag, Row, Col, Collapsible } from '@douyinfe/semi-ui';
  17. import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
  18. /**
  19. * 通用可选择按钮组组件
  20. *
  21. * @param {string} title 标题
  22. * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 按钮项
  23. * @param {*} activeValue 当前激活的值
  24. * @param {(value:any)=>void} onChange 选择改变回调
  25. * @param {function} t i18n
  26. * @param {object} style 额外样式
  27. * @param {boolean} collapsible 是否支持折叠,默认true
  28. * @param {number} collapseHeight 折叠时的高度,默认200
  29. */
  30. const SelectableButtonGroup = ({
  31. title,
  32. items = [],
  33. activeValue,
  34. onChange,
  35. t = (v) => v,
  36. style = {},
  37. collapsible = true,
  38. collapseHeight = 200
  39. }) => {
  40. const [isOpen, setIsOpen] = useState(false);
  41. const perRow = 3;
  42. const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32
  43. const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
  44. const contentRef = useRef(null);
  45. const maskStyle = isOpen
  46. ? {}
  47. : {
  48. WebkitMaskImage:
  49. 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)',
  50. };
  51. const toggle = () => {
  52. setIsOpen(!isOpen);
  53. };
  54. const linkStyle = {
  55. position: 'absolute',
  56. left: 0,
  57. right: 0,
  58. textAlign: 'center',
  59. bottom: -10,
  60. fontWeight: 400,
  61. cursor: 'pointer',
  62. fontSize: '12px',
  63. color: 'var(--semi-color-text-2)',
  64. display: 'flex',
  65. alignItems: 'center',
  66. justifyContent: 'center',
  67. gap: 4,
  68. };
  69. const contentElement = (
  70. <Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }} ref={contentRef}>
  71. {items.map((item) => {
  72. const isActive = activeValue === item.value;
  73. return (
  74. <Col xs={24} sm={24} md={24} lg={12} xl={8} key={item.value}>
  75. <Button
  76. onClick={() => onChange(item.value)}
  77. theme={isActive ? 'solid' : 'outline'}
  78. type={isActive ? 'primary' : 'tertiary'}
  79. icon={item.icon}
  80. style={{ width: '100%' }}
  81. >
  82. <span style={{ marginRight: item.tagCount !== undefined ? 4 : 0 }}>{item.label}</span>
  83. {item.tagCount !== undefined && (
  84. <Tag
  85. color='white'
  86. shape="circle"
  87. size="small"
  88. >
  89. {item.tagCount}
  90. </Tag>
  91. )}
  92. </Button>
  93. </Col>
  94. );
  95. })}
  96. </Row>
  97. );
  98. return (
  99. <div className="mb-8">
  100. {title && (
  101. <Divider margin="12px" align="left">
  102. {title}
  103. </Divider>
  104. )}
  105. {needCollapse ? (
  106. <div style={{ position: 'relative' }}>
  107. <Collapsible isOpen={isOpen} collapseHeight={collapseHeight} style={{ ...maskStyle }}>
  108. {contentElement}
  109. </Collapsible>
  110. {isOpen ? null : (
  111. <div onClick={toggle} style={{ ...linkStyle }}>
  112. <IconChevronDown size="small" />
  113. <span>{t('展开更多')}</span>
  114. </div>
  115. )}
  116. {isOpen && (
  117. <div onClick={toggle} style={{
  118. ...linkStyle,
  119. position: 'static',
  120. marginTop: 8,
  121. bottom: 'auto'
  122. }}>
  123. <IconChevronUp size="small" />
  124. <span>{t('收起')}</span>
  125. </div>
  126. )}
  127. </div>
  128. ) : (
  129. contentElement
  130. )}
  131. </div>
  132. );
  133. };
  134. export default SelectableButtonGroup;