mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 22:37:26 +00:00
🎨 feat(ui): enhance pricing components with improved icons and responsive design
- Replace copy button icon from semi-ui IconCopy to lucide-react Copy in PricingCardView - Add conditional tooltip functionality to SelectableButtonGroup that only shows when text overflows - Implement responsive table column behavior in PricingTableColumns with mobile-aware fixed positioning - Use DOM-based overflow detection (scrollWidth vs clientWidth) for better performance - Apply useIsMobile hook to conditionally set fixed: 'right' only on desktop devices These changes improve user experience across different screen sizes and provide more consistent iconography throughout the pricing interface.
This commit is contained in:
@@ -38,7 +38,7 @@ const ScrollableContainer = forwardRef(({
|
||||
children,
|
||||
maxHeight = '24rem',
|
||||
className = '',
|
||||
contentClassName = 'p-2',
|
||||
contentClassName = '',
|
||||
fadeIndicatorClassName = '',
|
||||
checkInterval = 100,
|
||||
scrollThreshold = 5,
|
||||
|
||||
@@ -17,8 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
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';
|
||||
@@ -51,10 +50,34 @@ const SelectableButtonGroup = ({
|
||||
loading = false
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [skeletonCount] = useState(6);
|
||||
const isMobile = useIsMobile();
|
||||
const [skeletonCount] = useState(12);
|
||||
const [containerRef, containerWidth] = useContainerWidth();
|
||||
|
||||
const ConditionalTooltipText = ({ text }) => {
|
||||
const textRef = useRef(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = textRef.current;
|
||||
if (!el) return;
|
||||
setIsOverflowing(el.scrollWidth > el.clientWidth);
|
||||
}, [text, containerWidth]);
|
||||
|
||||
const textElement = (
|
||||
<span ref={textRef} className="sbg-ellipsis">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
|
||||
return isOverflowing ? (
|
||||
<Tooltip content={text}>
|
||||
{textElement}
|
||||
</Tooltip>
|
||||
) : (
|
||||
textElement
|
||||
);
|
||||
};
|
||||
|
||||
// 基于容器宽度计算响应式列数和标签显示策略
|
||||
const getResponsiveConfig = () => {
|
||||
if (containerWidth <= 280) return { columns: 1, showTags: true }; // 极窄:1列+标签
|
||||
@@ -176,9 +199,7 @@ const SelectableButtonGroup = ({
|
||||
>
|
||||
<div className="sbg-content">
|
||||
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
|
||||
<Tooltip content={item.label}>
|
||||
<span className="sbg-ellipsis">{item.label}</span>
|
||||
</Tooltip>
|
||||
<ConditionalTooltipText text={item.label} />
|
||||
{item.tagCount !== undefined && shouldShowTags && (
|
||||
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
||||
)}
|
||||
@@ -203,9 +224,7 @@ const SelectableButtonGroup = ({
|
||||
>
|
||||
<div className="sbg-content">
|
||||
{item.icon && (<span className="sbg-icon">{item.icon}</span>)}
|
||||
<Tooltip content={item.label}>
|
||||
<span className="sbg-ellipsis">{item.label}</span>
|
||||
</Tooltip>
|
||||
<ConditionalTooltipText text={item.label} />
|
||||
{item.tagCount !== undefined && shouldShowTags && (
|
||||
<Tag className="sbg-tag" color='white' shape="circle" size="small">{item.tagCount}</Tag>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ import PricingQuotaTypes from '../filter/PricingQuotaTypes';
|
||||
import PricingEndpointTypes from '../filter/PricingEndpointTypes';
|
||||
import PricingVendors from '../filter/PricingVendors';
|
||||
import PricingTags from '../filter/PricingTags';
|
||||
import PricingDisplaySettings from '../filter/PricingDisplaySettings';
|
||||
|
||||
import { resetPricingFilters } from '../../../../helpers/utils';
|
||||
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||
|
||||
@@ -107,21 +107,6 @@ const PricingSidebar = ({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PricingDisplaySettings
|
||||
showWithRecharge={showWithRecharge}
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
tokenUnit={tokenUnit}
|
||||
setTokenUnit={setTokenUnit}
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<PricingVendors
|
||||
filterVendor={filterVendor}
|
||||
setFilterVendor={setFilterVendor}
|
||||
|
||||
@@ -26,7 +26,21 @@ const PricingContent = ({ isMobile, sidebarProps, ...props }) => {
|
||||
<div className={isMobile ? "pricing-content-mobile" : "pricing-scroll-hide"}>
|
||||
{/* 固定的顶部区域(分类介绍 + 搜索和操作) */}
|
||||
<div className="pricing-search-header">
|
||||
<PricingTopSection {...props} isMobile={isMobile} sidebarProps={sidebarProps} />
|
||||
<PricingTopSection
|
||||
{...props}
|
||||
isMobile={isMobile}
|
||||
sidebarProps={sidebarProps}
|
||||
showWithRecharge={sidebarProps.showWithRecharge}
|
||||
setShowWithRecharge={sidebarProps.setShowWithRecharge}
|
||||
currency={sidebarProps.currency}
|
||||
setCurrency={sidebarProps.setCurrency}
|
||||
showRatio={sidebarProps.showRatio}
|
||||
setShowRatio={sidebarProps.setShowRatio}
|
||||
viewMode={sidebarProps.viewMode}
|
||||
setViewMode={sidebarProps.setViewMode}
|
||||
tokenUnit={sidebarProps.tokenUnit}
|
||||
setTokenUnit={sidebarProps.setTokenUnit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 可滚动的内容区域 */}
|
||||
|
||||
@@ -35,6 +35,16 @@ const PricingTopSection = memo(({
|
||||
filteredModels,
|
||||
loading,
|
||||
searchValue,
|
||||
showWithRecharge,
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
t
|
||||
}) => {
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
@@ -53,6 +63,16 @@ const PricingTopSection = memo(({
|
||||
isMobile={isMobile}
|
||||
searchValue={searchValue}
|
||||
setShowFilterModal={setShowFilterModal}
|
||||
showWithRecharge={showWithRecharge}
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
tokenUnit={tokenUnit}
|
||||
setTokenUnit={setTokenUnit}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
@@ -78,6 +98,16 @@ const PricingTopSection = memo(({
|
||||
isMobile={isMobile}
|
||||
searchValue={searchValue}
|
||||
setShowFilterModal={setShowFilterModal}
|
||||
showWithRecharge={showWithRecharge}
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
tokenUnit={tokenUnit}
|
||||
setTokenUnit={setTokenUnit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -126,7 +126,17 @@ const PricingVendorIntro = memo(({
|
||||
handleCompositionEnd,
|
||||
isMobile = false,
|
||||
searchValue = '',
|
||||
setShowFilterModal
|
||||
setShowFilterModal,
|
||||
showWithRecharge,
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
tokenUnit,
|
||||
setTokenUnit
|
||||
}) => {
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const [descModalVisible, setDescModalVisible] = useState(false);
|
||||
@@ -239,9 +249,19 @@ const PricingVendorIntro = memo(({
|
||||
isMobile={isMobile}
|
||||
searchValue={searchValue}
|
||||
setShowFilterModal={setShowFilterModal}
|
||||
showWithRecharge={showWithRecharge}
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
currency={currency}
|
||||
setCurrency={setCurrency}
|
||||
showRatio={showRatio}
|
||||
setShowRatio={setShowRatio}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
tokenUnit={tokenUnit}
|
||||
setTokenUnit={setTokenUnit}
|
||||
t={t}
|
||||
/>
|
||||
), [selectedRowKeys, copyText, handleChange, handleCompositionStart, handleCompositionEnd, isMobile, searchValue, setShowFilterModal, t]);
|
||||
), [selectedRowKeys, copyText, handleChange, handleCompositionStart, handleCompositionEnd, isMobile, searchValue, setShowFilterModal, showWithRecharge, setShowWithRecharge, currency, setCurrency, showRatio, setShowRatio, viewMode, setViewMode, tokenUnit, setTokenUnit, t]);
|
||||
|
||||
const renderHeaderCard = useCallback(({ title, count, description, rightContent, primaryDarkerChannel }) => (
|
||||
<Card className="!rounded-2xl shadow-sm border-0"
|
||||
|
||||
@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Input, Button } from '@douyinfe/semi-ui';
|
||||
import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
|
||||
import { Input, Button, Switch, Select, Divider, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { IconSearch, IconCopy, IconFilter, IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
|
||||
const SearchActions = memo(({
|
||||
selectedRowKeys = [],
|
||||
@@ -30,6 +30,16 @@ const SearchActions = memo(({
|
||||
isMobile = false,
|
||||
searchValue = '',
|
||||
setShowFilterModal,
|
||||
showWithRecharge,
|
||||
setShowWithRecharge,
|
||||
currency,
|
||||
setCurrency,
|
||||
showRatio,
|
||||
setShowRatio,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
t
|
||||
}) => {
|
||||
const handleCopyClick = useCallback(() => {
|
||||
@@ -42,6 +52,14 @@ const SearchActions = memo(({
|
||||
setShowFilterModal?.(true);
|
||||
}, [setShowFilterModal]);
|
||||
|
||||
const handleViewModeToggle = useCallback(() => {
|
||||
setViewMode?.(viewMode === 'table' ? 'card' : 'table');
|
||||
}, [viewMode, setViewMode]);
|
||||
|
||||
const handleTokenUnitToggle = useCallback(() => {
|
||||
setTokenUnit?.(tokenUnit === 'K' ? 'M' : 'K');
|
||||
}, [tokenUnit, setTokenUnit]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex-1">
|
||||
@@ -67,6 +85,63 @@ const SearchActions = memo(({
|
||||
{t('复制')}
|
||||
</Button>
|
||||
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Divider layout="vertical" margin="8px" />
|
||||
|
||||
{/* 充值价格显示开关 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">{t('充值价格显示')}</span>
|
||||
<Switch
|
||||
checked={showWithRecharge}
|
||||
onChange={setShowWithRecharge}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 货币单位选择 */}
|
||||
{showWithRecharge && (
|
||||
<Select
|
||||
value={currency}
|
||||
onChange={setCurrency}
|
||||
optionList={[
|
||||
{ value: 'USD', label: 'USD' },
|
||||
{ value: 'CNY', label: 'CNY' }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 显示倍率开关 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">{t('倍率')}</span>
|
||||
<Tooltip content={t('倍率是用于系统计算不同模型的最终价格用的,如果您不理解倍率,请忽略')}>
|
||||
<IconHelpCircle size="small" style={{ color: 'var(--semi-color-text-2)', cursor: 'help' }} />
|
||||
</Tooltip>
|
||||
<Switch
|
||||
checked={showRatio}
|
||||
onChange={setShowRatio}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 视图模式切换按钮 */}
|
||||
<Button
|
||||
theme={viewMode === 'table' ? 'solid' : 'outline'}
|
||||
type={viewMode === 'table' ? 'primary' : 'tertiary'}
|
||||
onClick={handleViewModeToggle}
|
||||
>
|
||||
{t('表格视图')}
|
||||
</Button>
|
||||
|
||||
{/* Token单位切换按钮 */}
|
||||
<Button
|
||||
theme={tokenUnit === 'K' ? 'solid' : 'outline'}
|
||||
type={tokenUnit === 'K' ? 'primary' : 'tertiary'}
|
||||
onClick={handleTokenUnitToggle}
|
||||
>
|
||||
{tokenUnit}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<Button
|
||||
theme="outline"
|
||||
|
||||
@@ -71,7 +71,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<>
|
||||
<PricingDisplaySettings
|
||||
showWithRecharge={showWithRecharge}
|
||||
setShowWithRecharge={setShowWithRecharge}
|
||||
@@ -131,7 +131,7 @@ const FilterModalContent = ({ sidebarProps, t }) => {
|
||||
loading={loading}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ const PricingCardSkeleton = ({
|
||||
size="small"
|
||||
style={{
|
||||
width: 64,
|
||||
height: 20,
|
||||
height: 18,
|
||||
borderRadius: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Tag, Tooltip, Checkbox, Empty, Pagination, Button, Avatar } from '@douyinfe/semi-ui';
|
||||
import { IconHelpCircle, IconCopy } from '@douyinfe/semi-icons';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
|
||||
import { stringToColor, calculateModelPrice, formatPriceInfo, getLobeHubIcon } from '../../../../../helpers';
|
||||
import PricingCardSkeleton from './PricingCardSkeleton';
|
||||
@@ -245,7 +246,7 @@ const PricingCardView = ({
|
||||
size="small"
|
||||
theme="outline"
|
||||
type="tertiary"
|
||||
icon={<IconCopy />}
|
||||
icon={<Copy size={12} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyText(model.model_name);
|
||||
|
||||
@@ -38,7 +38,6 @@ const PricingTable = ({
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
tokenUnit,
|
||||
setTokenUnit,
|
||||
displayPrice,
|
||||
searchValue,
|
||||
showRatio,
|
||||
@@ -99,7 +98,6 @@ const PricingTable = ({
|
||||
dataSource={filteredModels}
|
||||
loading={loading}
|
||||
rowSelection={rowSelection}
|
||||
className="custom-table"
|
||||
scroll={compactMode ? undefined : { x: 'max-content' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => openModelDetail && openModelDetail(record),
|
||||
@@ -114,7 +112,7 @@ const PricingTable = ({
|
||||
/>
|
||||
}
|
||||
pagination={{
|
||||
defaultPageSize: 100,
|
||||
defaultPageSize: 20,
|
||||
pageSize: pageSize,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Tag, Space, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { renderModelTag, stringToColor, calculateModelPrice, getLobeHubIcon } from '../../../../../helpers';
|
||||
import { renderLimitedItems, renderDescription } from '../../../../common/ui/RenderUtils';
|
||||
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
|
||||
|
||||
function renderQuotaType(type, t) {
|
||||
switch (type) {
|
||||
@@ -98,7 +99,7 @@ export const getPricingTableColumns = ({
|
||||
displayPrice,
|
||||
showRatio,
|
||||
}) => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const priceDataCache = new WeakMap();
|
||||
|
||||
const getPriceData = (record) => {
|
||||
@@ -207,7 +208,7 @@ export const getPricingTableColumns = ({
|
||||
const priceColumn = {
|
||||
title: t('模型价格'),
|
||||
dataIndex: 'model_price',
|
||||
fixed: 'right',
|
||||
...(isMobile ? {} : { fixed: 'right' }),
|
||||
render: (text, record, index) => {
|
||||
const priceData = getPriceData(record);
|
||||
|
||||
@@ -215,10 +216,10 @@ export const getPricingTableColumns = ({
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="text-gray-700">
|
||||
{t('提示')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
|
||||
{t('输入')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
|
||||
</div>
|
||||
<div className="text-gray-700">
|
||||
{t('补全')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
|
||||
{t('输出')} {priceData.completionPrice} / 1{priceData.unitLabel} tokens
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useModelPricingData = () => {
|
||||
const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
|
||||
const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
|
||||
const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string
|
||||
const [pageSize, setPageSize] = useState(100);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [currency, setCurrency] = useState('USD');
|
||||
const [showWithRecharge, setShowWithRecharge] = useState(false);
|
||||
|
||||
@@ -293,7 +293,8 @@
|
||||
"账号绑定": "Account Binding",
|
||||
"绑定微信账号": "Bind WeChat Account",
|
||||
"微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account",
|
||||
"输入": "Enter",
|
||||
"输入": "Input",
|
||||
"输出": "Output",
|
||||
"验证码": "Verification Code",
|
||||
"获取验证码": "Get Verification Code",
|
||||
"三分钟内有效": "Valid for three minutes",
|
||||
|
||||
@@ -52,22 +52,6 @@ code {
|
||||
}
|
||||
|
||||
/* ==================== 导航和侧边栏样式 ==================== */
|
||||
.semi-radio,
|
||||
.semi-tagInput,
|
||||
.semi-input-textarea-wrapper,
|
||||
.semi-navigation-sub-title,
|
||||
.semi-chat-inputBox-sendButton,
|
||||
.semi-page-item,
|
||||
.semi-navigation-item,
|
||||
.semi-tag-closable,
|
||||
.semi-input-wrapper,
|
||||
.semi-tabs-tab-button,
|
||||
.semi-select,
|
||||
.semi-button,
|
||||
.semi-datepicker-range-input {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 4px !important;
|
||||
padding: 4px 12px !important;
|
||||
@@ -778,4 +762,21 @@ html.dark .with-pastel-balls::before {
|
||||
.semi-card-header,
|
||||
.semi-card-body {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* ==================== 自定义圆角样式 ==================== */
|
||||
.semi-radio,
|
||||
.semi-tagInput,
|
||||
.semi-input-textarea-wrapper,
|
||||
.semi-navigation-sub-title,
|
||||
.semi-chat-inputBox-sendButton,
|
||||
.semi-page-item,
|
||||
.semi-navigation-item,
|
||||
.semi-tag-closable,
|
||||
.semi-input-wrapper,
|
||||
.semi-tabs-tab-button,
|
||||
.semi-select,
|
||||
.semi-button,
|
||||
.semi-datepicker-range-input {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
Reference in New Issue
Block a user