✨ 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:
@@ -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'}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user