feat(ui): enhance pricing table & filters with responsive button-group, fixed column, scroll tweaks (#1365)

• SelectableButtonGroup
  • Added optional collapsible support with gradient mask & toggle
  • Dynamic tagCount badge support for groups / quota types
  • Switched to responsive Row/Col (`xs 24`, `sm 24`, `lg 12`, `xl 8`) for fluid layout
  • Shows expand button only when item count exceeds visible rows

• Sidebar filters
  • PricingGroups & PricingQuotaTypes now pass tag counts to button-group
  • Counts derived from current models & quota_type

• PricingTableColumns
  • Moved “Availability” column to far right; fixed via `fixed: 'right'`
  • Re-ordered columns and preserved ratio / price logic

• PricingTable
  • Added `compactMode` prop; strips fixed columns and sets `scroll={compactMode ? undefined : { x: 'max-content' }}`
  • Processes columns to remove `fixed` in compact mode

• PricingPage & index.css
  • Added `.pricing-scroll-hide` utility to hide Y-axis scrollbar for `Sider` & `Content`

• Responsive / style refinements
  • Sidebar width adjusted to 460px
  • Scrollbars hidden uniformly across pricing modules

These changes complete the model-pricing UI refactor, ensuring clean scrolling, responsive filters, and fixed availability column for better usability.
This commit is contained in:
t0ng7u
2025-07-23 02:23:25 +08:00
parent a044070e1d
commit b964f755ec
6 changed files with 102 additions and 88 deletions

View File

@@ -82,7 +82,7 @@ const SelectableButtonGroup = ({
{items.map((item) => { {items.map((item) => {
const isActive = activeValue === item.value; const isActive = activeValue === item.value;
return ( return (
<Col span={8} key={item.value}> <Col xs={24} sm={24} md={24} lg={12} xl={8} key={item.value}>
<Button <Button
onClick={() => onChange(item.value)} onClick={() => onChange(item.value)}
theme={isActive ? 'solid' : 'outline'} theme={isActive ? 'solid' : 'outline'}

View File

@@ -23,7 +23,7 @@ import PricingTable from './PricingTable.jsx';
const PricingContent = (props) => { const PricingContent = (props) => {
return ( return (
<> <div className="pricing-scroll-hide">
{/* 固定的搜索和操作区域 */} {/* 固定的搜索和操作区域 */}
<div <div
style={{ style={{
@@ -45,7 +45,7 @@ const PricingContent = (props) => {
> >
<PricingTable {...props} /> <PricingTable {...props} />
</div> </div>
</> </div>
); );
}; };

View File

@@ -35,6 +35,7 @@ const PricingPage = () => {
<Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}> <Layout style={{ height: 'calc(100vh - 60px)', overflow: 'hidden', marginTop: '60px' }}>
{/* 左侧边栏 */} {/* 左侧边栏 */}
<Sider <Sider
className="pricing-scroll-hide"
style={{ style={{
width: 460, width: 460,
height: 'calc(100vh - 60px)', height: 'calc(100vh - 60px)',
@@ -48,6 +49,7 @@ const PricingPage = () => {
{/* 右侧内容区 */} {/* 右侧内容区 */}
<Content <Content
className="pricing-scroll-hide"
style={{ style={{
height: 'calc(100vh - 60px)', height: 'calc(100vh - 60px)',
backgroundColor: 'var(--semi-color-bg-0)', backgroundColor: 'var(--semi-color-bg-0)',

View File

@@ -45,6 +45,7 @@ const PricingTable = ({
filteredValue, filteredValue,
handleGroupClick, handleGroupClick,
showRatio, showRatio,
compactMode = false,
t t
}) => { }) => {
@@ -83,8 +84,8 @@ const PricingTable = ({
]); ]);
// 更新列定义中的 filteredValue // 更新列定义中的 filteredValue
const tableColumns = useMemo(() => { const processedColumns = useMemo(() => {
return columns.map(column => { const cols = columns.map(column => {
if (column.dataIndex === 'model_name') { if (column.dataIndex === 'model_name') {
return { return {
...column, ...column,
@@ -93,16 +94,23 @@ const PricingTable = ({
} }
return column; return column;
}); });
}, [columns, filteredValue]);
// Remove fixed property when in compact mode (mobile view)
if (compactMode) {
return cols.map(({ fixed, ...rest }) => rest);
}
return cols;
}, [columns, filteredValue, compactMode]);
const ModelTable = useMemo(() => ( const ModelTable = useMemo(() => (
<Card className="!rounded-xl overflow-hidden" bordered={false}> <Card className="!rounded-xl overflow-hidden" bordered={false}>
<Table <Table
columns={tableColumns} columns={processedColumns}
dataSource={filteredModels} dataSource={filteredModels}
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
className="custom-table" className="custom-table"
scroll={compactMode ? undefined : { x: 'max-content' }}
empty={ empty={
<Empty <Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />} image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
@@ -120,7 +128,7 @@ const PricingTable = ({
}} }}
/> />
</Card> </Card>
), [filteredModels, loading, tableColumns, rowSelection, pageSize, setPageSize, t]); ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, t, compactMode]);
return ModelTable; return ModelTable;
}; };

View File

@@ -92,84 +92,88 @@ export const getPricingTableColumns = ({
handleGroupClick, handleGroupClick,
showRatio, showRatio,
}) => { }) => {
const baseColumns = [ const endpointColumn = {
{ title: t('可用端点类型'),
title: t('可用性'), dataIndex: 'supported_endpoint_types',
dataIndex: 'available', render: (text, record, index) => {
render: (text, record, index) => { return renderSupportedEndpoints(text);
return renderAvailable(record.enable_groups.includes(selectedGroup), t);
},
sorter: (a, b) => {
const aAvailable = a.enable_groups.includes(selectedGroup);
const bAvailable = b.enable_groups.includes(selectedGroup);
return Number(aAvailable) - Number(bAvailable);
},
defaultSortOrder: 'descend',
}, },
{ };
title: t('可用端点类型'),
dataIndex: 'supported_endpoint_types', const modelNameColumn = {
render: (text, record, index) => { title: t('模型名称'),
return renderSupportedEndpoints(text); dataIndex: 'model_name',
}, render: (text, record, index) => {
}, return renderModelTag(text, {
{ onClick: () => {
title: t('模型名称'), copyText(text);
dataIndex: 'model_name', }
render: (text, record, index) => { });
return renderModelTag(text, { },
onClick: () => { onFilter: (value, record) =>
copyText(text); record.model_name.toLowerCase().includes(value.toLowerCase()),
} };
});
}, const quotaColumn = {
onFilter: (value, record) => title: t('计费类型'),
record.model_name.toLowerCase().includes(value.toLowerCase()), dataIndex: 'quota_type',
}, render: (text, record, index) => {
{ return renderQuotaType(parseInt(text), t);
title: t('计费类型'), },
dataIndex: 'quota_type', sorter: (a, b) => a.quota_type - b.quota_type,
render: (text, record, index) => { };
return renderQuotaType(parseInt(text), t);
}, const enableGroupColumn = {
sorter: (a, b) => a.quota_type - b.quota_type, title: t('可用分组'),
}, dataIndex: 'enable_groups',
{ render: (text, record, index) => {
title: t('可用分组'), return (
dataIndex: 'enable_groups', <Space wrap>
render: (text, record, index) => { {text.map((group) => {
return ( if (usableGroup[group]) {
<Space wrap> if (group === selectedGroup) {
{text.map((group) => { return (
if (usableGroup[group]) { <Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}>
if (group === selectedGroup) { {group}
return ( </Tag>
<Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}> );
{group} } else {
</Tag> return (
); <Tag
} else { key={group}
return ( color='blue'
<Tag shape='circle'
key={group} onClick={() => handleGroupClick(group)}
color='blue' className="cursor-pointer hover:opacity-80 transition-opacity"
shape='circle' >
onClick={() => handleGroupClick(group)} {group}
className="cursor-pointer hover:opacity-80 transition-opacity" </Tag>
> );
{group} }
</Tag> }
); })}
} </Space>
} );
})} },
</Space> };
);
}, const baseColumns = [endpointColumn, modelNameColumn, quotaColumn, enableGroupColumn];
},
]; const availabilityColumn = {
title: t('可用性'),
dataIndex: 'available',
fixed: 'right',
render: (text, record, index) => {
return renderAvailable(record.enable_groups.includes(selectedGroup), t);
},
sorter: (a, b) => {
const aAvailable = a.enable_groups.includes(selectedGroup);
const bAvailable = b.enable_groups.includes(selectedGroup);
return Number(aAvailable) - Number(bAvailable);
},
defaultSortOrder: 'descend',
};
// 倍率列 - 只有在showRatio为true时才包含
const ratioColumn = { const ratioColumn = {
title: () => ( title: () => (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
@@ -207,7 +211,6 @@ export const getPricingTableColumns = ({
}, },
}; };
// 价格列
const priceColumn = { const priceColumn = {
title: ( title: (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -264,12 +267,11 @@ export const getPricingTableColumns = ({
}, },
}; };
// 根据showRatio决定是否包含倍率列
const columns = [...baseColumns]; const columns = [...baseColumns];
if (showRatio) { if (showRatio) {
columns.push(ratioColumn); columns.push(ratioColumn);
} }
columns.push(priceColumn); columns.push(priceColumn);
columns.push(availabilityColumn);
return columns; return columns;
}; };

View File

@@ -391,7 +391,8 @@ code {
background: transparent; background: transparent;
} }
/* 隐藏卡片内容区域滚动条 */ /* 隐藏内容区域滚动条 */
.pricing-scroll-hide,
.model-test-scroll, .model-test-scroll,
.card-content-scroll, .card-content-scroll,
.model-settings-scroll, .model-settings-scroll,
@@ -403,6 +404,7 @@ code {
scrollbar-width: none; scrollbar-width: none;
} }
.pricing-scroll-hide::-webkit-scrollbar,
.model-test-scroll::-webkit-scrollbar, .model-test-scroll::-webkit-scrollbar,
.card-content-scroll::-webkit-scrollbar, .card-content-scroll::-webkit-scrollbar,
.model-settings-scroll::-webkit-scrollbar, .model-settings-scroll::-webkit-scrollbar,