mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 09:28:37 +00:00
🎨 feat: Implement responsive design for SelectableButtonGroup component
This commit introduces a comprehensive responsive design system for the SelectableButtonGroup component that adapts to container width changes, particularly optimized for dynamic sidebar layouts. ## Key Features ### 1. Container Width Detection - Added `useContainerWidth` hook using ResizeObserver API - Real-time container width monitoring for responsive calculations - Automatic layout adjustments based on available space ### 2. Intelligent Column Layout Implements a 4-tier responsive system: - **≤280px**: 1 column + tags (mobile portrait) - **281-380px**: 2 columns + tags (narrow screens) - **381-460px**: 3 columns - tags (general case, prioritizes readability) - **>460px**: 3 columns + tags (wide screens, full feature display) ### 3. Dynamic Tag Visibility - Tags automatically hide in medium-width containers (381-460px) to improve text readability - Tags show in narrow and wide containers where space allows for optimal UX - Responsive threshold ensures content clarity across all viewport sizes ### 4. Adaptive Grid Spacing - Compact spacing `[4,4]` for containers ≤400px - Standard spacing `[6,6]` for larger containers - Additional `.sbg-compact` CSS class for fine-tuned styling in narrow layouts ### 5. Sidebar Integration - Perfectly compatible with dynamic sidebar width: `clamp(280px, 24vw, 520px)` - Automatically adjusts as sidebar scales with viewport changes - Maintains optimal button density and information display at all sizes ## Technical Implementation - **Hook**: `useContainerWidth.js` - ResizeObserver-based width detection - **Component**: Enhanced `SelectableButtonGroup.jsx` with responsive logic - **Styling**: Added `.sbg-compact` mode in `index.css` - **Performance**: Efficient span calculation using `Math.floor(24 / perRow)` ## Benefits - Improved UX across all screen sizes and sidebar configurations - Better text readability through intelligent tag hiding - Seamless integration with existing responsive sidebar system - Maintains component functionality while optimizing space utilization Closes: Responsive design implementation for model marketplace sidebar components
This commit is contained in:
@@ -20,6 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
|
||||||
|
import { useContainerWidth } from '../../../hooks/common/useContainerWidth';
|
||||||
import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui';
|
import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
@@ -52,11 +53,29 @@ const SelectableButtonGroup = ({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [skeletonCount] = useState(6);
|
const [skeletonCount] = useState(6);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const perRow = 3;
|
const [containerRef, containerWidth] = useContainerWidth();
|
||||||
|
|
||||||
|
// 基于容器宽度计算响应式列数和标签显示策略
|
||||||
|
const getResponsiveConfig = () => {
|
||||||
|
if (containerWidth <= 280) return { columns: 1, showTags: true }; // 极窄:1列+标签
|
||||||
|
if (containerWidth <= 380) return { columns: 2, showTags: true }; // 窄屏:2列+标签
|
||||||
|
if (containerWidth <= 460) return { columns: 3, showTags: false }; // 中等:3列不加标签
|
||||||
|
return { columns: 3, showTags: true }; // 最宽:3列+标签
|
||||||
|
};
|
||||||
|
|
||||||
|
const { columns: perRow, showTags: shouldShowTags } = getResponsiveConfig();
|
||||||
const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32
|
const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32
|
||||||
const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
|
const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
|
||||||
const showSkeleton = useMinimumLoadingTime(loading);
|
const showSkeleton = useMinimumLoadingTime(loading);
|
||||||
|
|
||||||
|
// 统一使用紧凑的网格间距
|
||||||
|
const gutterSize = [4, 4];
|
||||||
|
|
||||||
|
// 计算 Semi UI Col 的 span 值
|
||||||
|
const getColSpan = () => {
|
||||||
|
return Math.floor(24 / perRow);
|
||||||
|
};
|
||||||
|
|
||||||
const maskStyle = isOpen
|
const maskStyle = isOpen
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
@@ -87,13 +106,10 @@ const SelectableButtonGroup = ({
|
|||||||
const renderSkeletonButtons = () => {
|
const renderSkeletonButtons = () => {
|
||||||
|
|
||||||
const placeholder = (
|
const placeholder = (
|
||||||
<Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }}>
|
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
||||||
{Array.from({ length: skeletonCount }).map((_, index) => (
|
{Array.from({ length: skeletonCount }).map((_, index) => (
|
||||||
<Col
|
<Col
|
||||||
{...(isMobile
|
span={getColSpan()}
|
||||||
? { span: 12 }
|
|
||||||
: { span: 8 }
|
|
||||||
)}
|
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -105,7 +121,7 @@ const SelectableButtonGroup = ({
|
|||||||
border: '1px solid var(--semi-color-border)',
|
border: '1px solid var(--semi-color-border)',
|
||||||
borderRadius: 'var(--semi-border-radius-medium)',
|
borderRadius: 'var(--semi-border-radius-medium)',
|
||||||
padding: '0 12px',
|
padding: '0 12px',
|
||||||
gap: '8px'
|
gap: '6px'
|
||||||
}}>
|
}}>
|
||||||
{withCheckbox && (
|
{withCheckbox && (
|
||||||
<Skeleton.Title active style={{ width: 14, height: 14 }} />
|
<Skeleton.Title active style={{ width: 14, height: 14 }} />
|
||||||
@@ -129,7 +145,7 @@ const SelectableButtonGroup = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const contentElement = showSkeleton ? renderSkeletonButtons() : (
|
const contentElement = showSkeleton ? renderSkeletonButtons() : (
|
||||||
<Row gutter={[8, 8]} style={{ lineHeight: '32px', ...style }}>
|
<Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0);
|
const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0);
|
||||||
const isActive = Array.isArray(activeValue)
|
const isActive = Array.isArray(activeValue)
|
||||||
@@ -139,10 +155,7 @@ const SelectableButtonGroup = ({
|
|||||||
if (withCheckbox) {
|
if (withCheckbox) {
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
{...(isMobile
|
span={getColSpan()}
|
||||||
? { span: 12 }
|
|
||||||
: { span: 8 }
|
|
||||||
)}
|
|
||||||
key={item.value}
|
key={item.value}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -166,7 +179,7 @@ const SelectableButtonGroup = ({
|
|||||||
<Tooltip content={item.label}>
|
<Tooltip content={item.label}>
|
||||||
<span className="sbg-ellipsis">{item.label}</span>
|
<span className="sbg-ellipsis">{item.label}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{item.tagCount !== undefined && (
|
{item.tagCount !== undefined && shouldShowTags && (
|
||||||
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -177,10 +190,7 @@ const SelectableButtonGroup = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
{...(isMobile
|
span={getColSpan()}
|
||||||
? { span: 12 }
|
|
||||||
: { span: 8 }
|
|
||||||
)}
|
|
||||||
key={item.value}
|
key={item.value}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -196,7 +206,7 @@ const SelectableButtonGroup = ({
|
|||||||
<Tooltip content={item.label}>
|
<Tooltip content={item.label}>
|
||||||
<span className="sbg-ellipsis">{item.label}</span>
|
<span className="sbg-ellipsis">{item.label}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{item.tagCount !== undefined && (
|
{item.tagCount !== undefined && shouldShowTags && (
|
||||||
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +218,7 @@ const SelectableButtonGroup = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8">
|
<div className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`} ref={containerRef}>
|
||||||
{title && (
|
{title && (
|
||||||
<Divider margin="12px" align="left">
|
<Divider margin="12px" align="left">
|
||||||
{showSkeleton ? (
|
{showSkeleton ? (
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const PricingPage = () => {
|
|||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Sider
|
<Sider
|
||||||
className="pricing-scroll-hide pricing-sidebar"
|
className="pricing-scroll-hide pricing-sidebar"
|
||||||
width={460}
|
|
||||||
>
|
>
|
||||||
<PricingSidebar {...allProps} />
|
<PricingSidebar {...allProps} />
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|||||||
52
web/src/hooks/common/useContainerWidth.js
Normal file
52
web/src/hooks/common/useContainerWidth.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测容器宽度的 Hook
|
||||||
|
* @returns {[ref, width]} 容器引用和当前宽度
|
||||||
|
*/
|
||||||
|
export const useContainerWidth = () => {
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
const { width: newWidth } = entry.contentRect;
|
||||||
|
setWidth(newWidth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
|
||||||
|
// 初始化宽度
|
||||||
|
setWidth(element.getBoundingClientRect().width);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [ref, width];
|
||||||
|
};
|
||||||
@@ -728,8 +728,9 @@ html.dark .with-pastel-balls::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pricing-sidebar {
|
.pricing-sidebar {
|
||||||
min-width: 400px;
|
width: clamp(280px, 24vw, 520px) !important;
|
||||||
max-width: 400px;
|
min-width: clamp(280px, 24vw, 520px) !important;
|
||||||
|
max-width: clamp(280px, 24vw, 520px) !important;
|
||||||
height: calc(100vh - 60px);
|
height: calc(100vh - 60px);
|
||||||
background-color: var(--semi-color-bg-0);
|
background-color: var(--semi-color-bg-0);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
Reference in New Issue
Block a user