feat(user-models): clickable model card opens API access info

This commit is contained in:
jialin
2026-07-09 20:36:52 +08:00
committed by jialin
parent 794ac724ca
commit dc54c21d70
3 changed files with 136 additions and 85 deletions
+13 -4
View File
@@ -44,7 +44,7 @@ const ModelItemContent = styled.div`
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 100%; width: 100%;
cursor: default; cursor: pointer;
.content { .content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -127,15 +127,23 @@ const renderTag = (item: any, index = 0) => {
const ModelItem: React.FC<{ const ModelItem: React.FC<{
model: Record<string, any>; model: Record<string, any>;
onClick?: (model: Record<string, any>) => void;
}> = (props) => { }> = (props) => {
const { model } = props; const { model, onClick } = props;
const intl = useIntl(); const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const handleCardClick = () => {
onClick?.(model);
};
// ``model.name`` from ``/v2/my-models`` is the OpenAI-style id // ``model.name`` from ``/v2/my-models`` is the OpenAI-style id
// (org-prefixed for non-platform routes, bare for platform). Use it // (org-prefixed for non-platform routes, bare for platform). Use it
// verbatim — the playground / dispatcher both key off that exact id. // verbatim — the playground / dispatcher both key off that exact id.
const handleOpenPlayGroundClick = () => { const handleOpenPlayGroundClick = (e: React.MouseEvent) => {
// Card is clickable (opens API access info); keep the playground
// action isolated so it doesn't also trigger the card click.
e.stopPropagation();
const modelName = encodeURIComponent(model.name); const modelName = encodeURIComponent(model.name);
for (const [category, path] of Object.entries(categoryToPathMap)) { for (const [category, path] of Object.entries(categoryToPathMap)) {
if ( if (
@@ -178,9 +186,10 @@ const ModelItem: React.FC<{
<CardWrapper> <CardWrapper>
<TemplateCard <TemplateCard
height={140} height={140}
clickable={false} clickable={true}
hoverable={true} hoverable={true}
ghost ghost
onClick={handleCardClick}
header={ header={
<Header> <Header>
<span className="text gap-8"> <span className="text gap-8">
@@ -0,0 +1,30 @@
import { useState } from 'react';
const useViewApIInfo = () => {
const [apiAccessInfo, setAPIAccessInfo] = useState<any>({
show: false,
data: {}
});
const openViewAPIInfo = (row: any) => {
setAPIAccessInfo({
show: true,
data: row
});
};
const closeViewAPIInfo = () => {
setAPIAccessInfo({
show: false,
data: {}
});
};
return {
apiAccessInfo,
openViewAPIInfo,
closeViewAPIInfo
};
};
export default useViewApIInfo;
+93 -81
View File
@@ -14,8 +14,10 @@ import { Button, Input, Space } from 'antd';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import PageBox from '../_components/page-box'; import PageBox from '../_components/page-box';
import { MY_MODELS_API, queryMyModels } from './apis'; import { MY_MODELS_API, queryMyModels } from './apis';
import APIAccessInfoModal from './components/api-access-info';
import ModelItem from './components/model-item'; import ModelItem from './components/model-item';
import { categoryOptions, MyModelsStatusValueMap } from './config'; import { categoryOptions, MyModelsStatusValueMap } from './config';
import useViewApIInfo from './hooks/use-view-api-info';
const Dot = ({ color }: { color: string }) => { const Dot = ({ color }: { color: string }) => {
return ( return (
<span <span
@@ -57,6 +59,7 @@ const UserModels: React.FC = () => {
} }
}); });
const intl = useIntl(); const intl = useIntl();
const { apiAccessInfo, openViewAPIInfo, closeViewAPIInfo } = useViewApIInfo();
const statusOptions = useMemo(() => { const statusOptions = useMemo(() => {
return [ return [
@@ -91,7 +94,7 @@ const UserModels: React.FC = () => {
}; };
const renderCard = (data: any) => { const renderCard = (data: any) => {
return <ModelItem model={data} />; return <ModelItem model={data} onClick={openViewAPIInfo} />;
}; };
const loadMore = useMemoizedFn((nextPage: number) => { const loadMore = useMemoizedFn((nextPage: number) => {
@@ -152,86 +155,95 @@ const UserModels: React.FC = () => {
}; };
return ( return (
<PageBox> <>
<PageTools <PageBox>
marginBottom={22} <PageTools
marginTop={0} marginBottom={22}
left={ marginTop={0}
<Space> left={
<Input <Space>
placeholder={intl.formatMessage({ id: 'common.filter.name' })} <Input
style={{ width: 230 }} placeholder={intl.formatMessage({ id: 'common.filter.name' })}
size="large" style={{ width: 230 }}
allowClear size="large"
onClear={() => allowClear
handleNameChange({ onClear={() =>
target: { handleNameChange({
value: '' target: {
} value: ''
}) }
} })
onChange={handleNameChange} }
></Input> onChange={handleNameChange}
<BaseSelect ></Input>
allowClear <BaseSelect
showSearch={false} allowClear
placeholder={intl.formatMessage({ id: 'models.filter.category' })} showSearch={false}
style={{ width: 180 }} placeholder={intl.formatMessage({
size="large" id: 'models.filter.category'
maxTagCount={1} })}
options={categoryOptions} style={{ width: 180 }}
onChange={handleCategoryChange} size="large"
></BaseSelect> maxTagCount={1}
<BaseSelect options={categoryOptions}
allowClear onChange={handleCategoryChange}
showSearch={false} ></BaseSelect>
placeholder={intl.formatMessage({ id: 'common.filter.status' })} <BaseSelect
style={{ width: 180 }} allowClear
size="large" showSearch={false}
maxTagCount={1} placeholder={intl.formatMessage({ id: 'common.filter.status' })}
optionRender={optionRender} style={{ width: 180 }}
labelRender={labelRender} size="large"
options={statusOptions} maxTagCount={1}
onChange={handleStatusChange} optionRender={optionRender}
></BaseSelect> labelRender={labelRender}
<Button options={statusOptions}
type="text" onChange={handleStatusChange}
style={{ color: 'var(--ant-color-text-tertiary)' }} ></BaseSelect>
icon={<SyncOutlined></SyncOutlined>} <Button
onClick={handleRefresh} type="text"
></Button> style={{ color: 'var(--ant-color-text-tertiary)' }}
</Space> icon={<SyncOutlined></SyncOutlined>}
} onClick={handleRefresh}
></PageTools> ></Button>
<InfiniteScrollerProvider </Space>
value={{ }
total: dataSource.totalPage, ></PageTools>
current: queryParams.page, <InfiniteScrollerProvider
loading: dataSource.loading, value={{
refresh: loadMore total: dataSource.totalPage,
}} current: queryParams.page,
> loading: dataSource.loading,
<TemplateCardList refresh: loadMore
dataList={dataList} }}
loading={dataSource.loading} >
activeId={false} <TemplateCardList
isFirst={!dataSource.loadend} dataList={dataList}
renderItem={renderCard} loading={dataSource.loading}
></TemplateCardList> activeId={false}
<NoResult isFirst={!dataSource.loadend}
loading={dataSource.loading} renderItem={renderCard}
loadend={dataSource.loadend} ></TemplateCardList>
dataSource={dataList} <NoResult
image={<IconFont type="icon-models" />} loading={dataSource.loading}
filters={{ ...queryParams }} loadend={dataSource.loadend}
noFoundText={intl.formatMessage({ dataSource={dataList}
id: 'noresult.mymodels.nofound' image={<IconFont type="icon-models" />}
})} filters={{ ...queryParams }}
title={intl.formatMessage({ id: 'noresult.mymodels.title' })} noFoundText={intl.formatMessage({
subTitle={intl.formatMessage({ id: 'noresult.mymodels.subTitle' })} id: 'noresult.mymodels.nofound'
></NoResult> })}
</InfiniteScrollerProvider> title={intl.formatMessage({ id: 'noresult.mymodels.title' })}
</PageBox> subTitle={intl.formatMessage({ id: 'noresult.mymodels.subTitle' })}
></NoResult>
</InfiniteScrollerProvider>
</PageBox>
<APIAccessInfoModal
open={apiAccessInfo.show}
data={apiAccessInfo.data}
onClose={closeViewAPIInfo}
></APIAccessInfoModal>
</>
); );
}; };