Merge branch 'feature/1.7-cxx' into 'main'

feature/1.7-MCP

See merge request apipark/APIPark!305
This commit is contained in:
lichunxian
2025-04-11 16:49:06 +08:00
7 changed files with 387 additions and 160 deletions
@@ -86,9 +86,9 @@ export default function ApiRequestSetting() {
</Form.Item>
<Form.Item<ApiRequestSettingFieldType>
label={$t('集成地址')}
label={$t('OpenAPI & MCP 调用地址')}
name="sitePrefix"
rules={[{ whitespace: true }]}
rules={[{ required: true, whitespace: true }]}
extra={$t('与外部平台集成时,获取 API 市场中文档信息的域名')}
>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
@@ -10,6 +10,7 @@ import { useConnection } from './hook/useConnection'
import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { useNavigate } from 'react-router-dom'
import { ServiceDetailType } from '@market/const/serviceHub/type'
import useCopyToClipboard from '@common/hooks/copy'
type ConfigList = {
@@ -34,12 +35,20 @@ type ApiKeyItem = {
const IntegrationAIContainer = ({
type,
handleToolsChange
handleToolsChange,
customClassName,
service,
serviceId,
currentTab
}: {
type: 'global' | 'service'
handleToolsChange: (value: Tool[]) => void
customClassName?: string
service?: ServiceDetailType
serviceId?: string
currentTab?: string
}) => {
const [activeTab, setActiveTab] = useState('mcp')
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
const { message } = App.useApp()
const [configContent, setConfigContent] = useState<string>('')
const [apiKey, setApiKey] = useState<string>('')
@@ -66,14 +75,14 @@ const IntegrationAIContainer = ({
const params: ConfigList = {
mcp: {
title: $t('MCP 配置'),
configContent: '',
configContent: service?.mcpAccessConfig || '',
apiKeys: []
}
}
if (type === 'global') {
if (type === 'service') {
params.openApi = {
title: $t('Open API 文档'),
configContent: '',
configContent: service?.openapiAddress || '',
apiKeys: []
}
}
@@ -131,7 +140,7 @@ const IntegrationAIContainer = ({
* 获取 API Key 列表
*/
const getKeysList = () => {
fetchData<BasicResponse<null>>(type === 'global' ? 'simple/system/apikeys' : '', {
fetchData<BasicResponse<null>>(type === 'global' ? 'simple/system/apikeys' : 'my/apikeys', {
method: 'GET'
})
.then((response) => {
@@ -212,28 +221,34 @@ const IntegrationAIContainer = ({
// 使用 useRef 保存最新的连接状态和断开函数
const connectionStatusRef = useRef(connectionStatus)
const disconnectFnRef = useRef(disconnectMcpServer)
// 当连接状态或断开函数变化时更新 ref
useEffect(() => {
connectionStatusRef.current = connectionStatus
disconnectFnRef.current = disconnectMcpServer
}, [connectionStatus, disconnectMcpServer])
const setupComponent = () => {
initTabsData()
if (type === 'global') {
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
} else {
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
}
getKeysList()
}
useEffect(() => {
setupComponent()
}, [service])
useEffect(() => {
if (type === 'service') {
currentTab === 'MCP' ? setActiveTab('mcp') : setActiveTab('openApi')
}
}, [currentTab])
// 仅在组件加载时执行初始化逻辑
useEffect(() => {
// 局部函数,仅在此 effect 执行期间存在
const setupComponent = () => {
if (type === 'global') {
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
}
initTabsData()
getKeysList()
}
// 执行初始化
setupComponent()
// 返回清理函数,只会在组件卸载时执行
return () => {
try {
@@ -242,7 +257,6 @@ const IntegrationAIContainer = ({
if (disconnectFn) {
disconnectFn()
}
} catch (err) {
console.error('断开连接时出错:', err)
}
@@ -250,11 +264,11 @@ const IntegrationAIContainer = ({
}, [type])
useEffect(() => {
if (activeTab === 'openApi' && tabContent?.openApi?.configContent) {
setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
setConfigContent(tabContent?.openApi?.configContent)
} else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) {
setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
}
}, [apiKey, activeTab, tabContent])
}, [service, apiKey, activeTab, tabContent])
useEffect(() => {
if (mcpServerUrl) {
@@ -274,7 +288,7 @@ const IntegrationAIContainer = ({
<>
<Card
style={{ borderRadius: '10px' }}
className="w-[400px] h-fit"
className={`w-[400px] h-fit ${customClassName}`}
classNames={{
body: 'p-[10px]'
}}
@@ -289,7 +303,7 @@ const IntegrationAIContainer = ({
{$t('AI 代理集成')}
</p>
<div className="tab-container mt-3">
{type === 'service' && (
{type === 'service' && service?.basic.enableMcp && (
<div className="tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
@@ -310,21 +324,39 @@ const IntegrationAIContainer = ({
</div>
{/* 标签页内容区域 */}
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
<ReactJson
src={configContent ? JSON.parse(configContent) : {}}
theme="monokai"
indentWidth={2}
displayDataTypes={false}
displayObjectSize={false}
name={false}
collapsed={false}
enableClipboard={false}
style={{
backgroundColor: 'transparent',
wordBreak: 'break-word',
whiteSpace: 'normal'
}}
/>
{activeTab === 'mcp' ? (
<ReactJson
src={
configContent
? typeof configContent === 'string'
? (() => {
try {
return JSON.parse(configContent);
} catch (e) {
return {};
}
})()
: configContent
: {}
}
theme="monokai"
indentWidth={2}
displayDataTypes={false}
displayObjectSize={false}
name={false}
collapsed={false}
enableClipboard={false}
style={{
backgroundColor: 'transparent',
wordBreak: 'break-word',
whiteSpace: 'normal'
}}
/>
) : (
<>
<pre className="whitespace-pre-wrap break-words">{configContent || ''}</pre>
</>
)}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
@@ -2,12 +2,12 @@ import { Icon } from '@iconify/react/dist/iconify.js'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { Card } from 'antd'
const McpToolsContainer = ({ tools = [] }: { tools: Tool[] }) => {
const McpToolsContainer = ({ tools = [], customClassName }: { tools: Tool[]; customClassName?: string }) => {
return (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-full flex-1 mr-[10px]`}
className={`w-full flex-1 mr-[10px] ${customClassName}`}
classNames={{
body: 'p-[10px]'
}}
@@ -284,7 +284,6 @@ export function useConnection({
// if (token) {
// headers["Authorization"] = `Bearer ${token}`;
// }
// 创建SSE客户端传输层
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
eventSourceInit: {
@@ -17,6 +17,8 @@ export type ServiceBasicInfoType = {
approvalType: 'auto' | 'manual'
serviceKind: 'ai' | 'rest'
sitePrefix?: string
enableMcp: boolean
invokeCount: number
}
export type ServiceDetailType = {
@@ -25,6 +27,9 @@ export type ServiceDetailType = {
basic: ServiceBasicInfoType
apiDoc: string
applied: boolean
mcpServerAddress?: string
mcpAccessConfig?: string
openapiAddress?: string
}
export type ServiceHubCategoryConfigFieldType = {
@@ -1,4 +1,4 @@
import { ApiFilled, ArrowLeftOutlined } from '@ant-design/icons'
import { ApiFilled, ApiOutlined, ArrowLeftOutlined } from '@ant-design/icons'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { EntityItem, RouterParams } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
@@ -6,15 +6,26 @@ import { useFetch } from '@common/hooks/http.ts'
import { $t } from '@common/locales/index.ts'
import { Icon } from '@iconify/react/dist/iconify.js'
import { approvalTypeTranslate } from '@market/const/serviceHub/const.tsx'
import { App, Avatar, Button, Descriptions, Divider, Tabs } from 'antd'
import { App, Avatar, Button, Card, Descriptions, Divider, Tabs, Tag, Tooltip } from 'antd'
import { DefaultOptionType } from 'antd/es/cascader'
import DOMPurify from 'dompurify'
import { useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { ApplyServiceHandle, ServiceBasicInfoType, ServiceDetailType } from '../../const/serviceHub/type.ts'
import { ApplyServiceModal } from './ApplyServiceModal.tsx'
import ServiceHubApiDocument from './ServiceHubApiDocument.tsx'
import Integrate from './integrate.tsx'
import { SERVICE_KIND_OPTIONS } from '@core/const/system/const.tsx'
import IntegrationAIContainer from '@core/pages/mcpService/IntegrationAIContainer.tsx'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import McpToolsContainer from '@core/pages/mcpService/McpToolsContainer.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
type TabItemType = {
key: string
label: string
children: React.ReactNode
icon?: React.ReactNode
}
const ServiceHubDetail = () => {
const { serviceId } = useParams<RouterParams>()
@@ -28,6 +39,14 @@ const ServiceHubDetail = () => {
const { modal, message } = App.useApp()
const [mySystemOptionList, setMySystemOptionList] = useState<DefaultOptionType[]>()
const [service, setService] = useState<ServiceDetailType>()
const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([])
const [serviceTags, setServiceTags] = useState<
{ color: string; textColor: string; title: string; content: React.ReactNode }[]
>([])
const [tools, setTools] = useState<Tool[]>([])
const [tabItem, setTabItem] = useState<TabItemType[]>([])
const [currentTab, setCurrentTab] = useState('')
const { state } = useGlobalContext()
const navigate = useNavigate()
const modifyApiDoc = (apiDoc: string, apiPrefix: string) => {
@@ -60,7 +79,11 @@ const ServiceHubDetail = () => {
'invoke_address',
'approval_type',
'service_kind',
'site_prefix'
'site_prefix',
'enable_mcp',
'mcp_server_address',
'mcp_access_config',
'openapi_address'
]
}).then((response) => {
const { code, data, msg } = response
@@ -73,12 +96,66 @@ const ServiceHubDetail = () => {
setServiceName(data.service.name)
setServiceDesc(data.service.description)
setServiceDoc(DOMPurify.sanitize(data.service.document))
setServiceMetricsList(data.service.basic)
setTabItemList(data.service.basic)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const handleTabChange = (value: any) => {
setCurrentTab(value)
}
const setServiceMetricsList = (serviceBasicInfo: ServiceBasicInfoType) => {
// 设置服务指标数据
setServiceMetrics([
{
title: 'API 数量',
icon: <ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]" />,
value: serviceBasicInfo.apiNum.toString()
},
{
title: '接入消费者数量',
icon: <Icon icon="tabler:api-app" width="14" height="14" />,
value: serviceBasicInfo.appNum.toString()
},
{
title: '30天内调用次数',
icon: <Icon icon="iconoir:graph-up" width="14" height="14" />,
value: formatInvokeCount(serviceBasicInfo.invokeCount ?? 0)
}
])
// 设置服务标签数据
const tags = [
{
color: '#7371fc1b',
textColor: 'text-theme',
title: serviceBasicInfo?.catalogue?.name || '-',
content: serviceBasicInfo?.catalogue?.name || '-'
},
{
color: '#fbe5e5',
textColor: 'text-[#000]',
title: serviceBasicInfo?.serviceKind || '-',
content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceBasicInfo?.serviceKind)?.label || '-'
}
]
// 如果启用了MCP,添加MCP标签
if (serviceBasicInfo?.enableMcp) {
tags.push({
color: '#ffc107',
textColor: 'text-[#000]',
title: 'MCP',
content: 'MCP'
})
}
setServiceTags(tags)
}
useEffect(() => {
if (!serviceId) {
console.warn('缺少serviceId')
@@ -134,120 +211,234 @@ const ServiceHubDetail = () => {
})
}
const items = [
{
key: 'introduction',
label: $t('介绍'),
children: (
<>
<div
className="p-btnbase preview-document mb-PAGE_INSIDE_B"
dangerouslySetInnerHTML={{ __html: serviceDoc || '' }}
></div>
</>
),
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14" />
},
{
key: 'api-document',
label: $t('API 文档'),
children: (
<div
className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}
>
<ServiceHubApiDocument service={service!} />
</div>
),
icon: <ApiFilled />
},
{
key: 'api-integrate',
label: $t('集成'),
children: (
<div
className={`p-btnbase ${serviceBasicInfo?.serviceKind?.toLocaleLowerCase() === 'ai' ? 'ai-service-api-preview' : ''}`}
>
<Integrate service={service!} />
</div>
),
icon: <Icon icon="icon-park-solid:whole-site-accelerator" width="15" height="15" />
const handleToolsChange = (value: Tool[]) => {
setTools(value)
}
// 格式化调用次数,添加K和M单位
const formatInvokeCount = (count: number | null | undefined): string => {
if (count === null || count === undefined) return '-'
if (count >= 1000000) {
const value = Math.floor(count / 100000) / 10
return `${value}M`
}
]
if (count >= 1000) {
const value = Math.floor(count / 100) / 10
return `${value}K`
}
return count.toString()
}
/**
* 定义一个更新标签项的函数,在serviceBasicInfo或tools变化时调用
*/
const updateTabItems = useCallback(() => {
if (!serviceBasicInfo) return
const descriptionItem = [
{
label: $t('供应方'),
value: serviceBasicInfo?.team?.name || '-',
className: 'pb-[10px]'
},
{
label: $t('版本'),
value: serviceBasicInfo?.version || '-',
className: 'pb-[10px]'
},
{
label: $t('更新时间'),
value: serviceBasicInfo?.updateTime || '-',
className: 'pb-[10px]',
isTimeString: true
},
{
label: $t('审核'),
value: serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-',
className: 'pb-[0px]'
}
]
const items: TabItemType[] = [
{
key: 'introduction',
label: $t('介绍'),
children: (
<>
<Card
style={{
borderRadius: '10px'
}}
className="w-full h-[calc(100vh-420px)] overflow-auto"
classNames={{
body: 'p-[10px]'
}}
>
<Card
style={{
borderRadius: '10px'
}}
className={`w-full`}
classNames={{
body: 'p-[15px] h-auto bg-[#f8f8f8]'
}}
>
<Descriptions column={1}>
{descriptionItem.map((item, index) => (
<Descriptions.Item key={index} label={item.label} className={item.className}>
{item.isTimeString ? (
<span className="truncate" title={item.value}>
{item.value}
</span>
) : (
item.value
)}
</Descriptions.Item>
))}
</Descriptions>
</Card>
<div
className="p-btnbase preview-document mb-PAGE_INSIDE_B"
dangerouslySetInnerHTML={{ __html: serviceDoc || '' }}
></div>
</Card>
</>
),
icon: <Icon icon="ic:baseline-space-dashboard" width="14" height="14" />
},
{
key: 'api-document',
label: $t('API'),
children: (
<Card
style={{
borderRadius: '10px'
}}
className="w-full h-[calc(100vh-420px)] overflow-auto"
classNames={{
body: 'p-[10px] pt-[0px]'
}}
>
<ServiceHubApiDocument service={service!} />
</Card>
),
icon: <ApiFilled />
}
]
if (serviceBasicInfo.enableMcp) {
items.push({
key: 'MCP',
label: 'MCP',
children: <McpToolsContainer tools={tools} customClassName="h-[calc(100vh-420px)] overflow-auto" />,
icon: <Icon icon="ph:network-x-fill" width="15" height="15" />
})
}
setTabItem(items)
}, [serviceBasicInfo, serviceDoc, service, tools, state.language])
/**
* 当初始化serviceBasicInfo时调用的函数
* @param _serviceBasicInfo
*/
const setTabItemList = (_serviceBasicInfo: ServiceBasicInfoType) => {
// 只调用更新函数,更新将由useEffect处理
updateTabItems()
}
useEffect(() => {
if (serviceBasicInfo) {
updateTabItems()
}
}, [tools, updateTabItems, serviceBasicInfo])
return (
<section className="grid grid-cols-5 h-full mr-PAGE_INSIDE_X">
<section className="col-span-4 border-0 border-r-[1px] border-solid border-BORDER flex flex-col overflow-hidden">
<section className="flex flex-col gap-btnbase p-btnbase">
<div className="text-[18px] leading-[25px] pb-[12px]">
<Button type="text" onClick={() => navigate(`/serviceHub/list`)}>
<ArrowLeftOutlined className="max-h-[14px]" />
{$t('返回')}
</Button>
</div>
<div className="flex">
{/* <Avatar shape="square" size={50} className=" bg-[linear-gradient(135deg,white,#f0f0f0)] text-[#333] rounded-[12px]" > {service?.name?.substring(0,1)}</Avatar> */}
<Avatar
shape="square"
size={50}
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
src={
serviceBasicInfo?.logo ? (
<img
src={serviceBasicInfo?.logo}
alt="Logo"
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
/>
) : undefined
}
icon={serviceBasicInfo?.logo ? '' : <iconpark-icon name="auto-generate-api"></iconpark-icon>}
>
{' '}
</Avatar>
<div className="pl-[20px] w-[calc(100%-50px)]">
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold flex items-center gap-[4px]">
<div className="pr-[40px]">
<header>
<Button type="text" onClick={() => navigate(`/serviceHub/list`)}>
<ArrowLeftOutlined className="max-h-[14px]" />
{$t('返回')}
</Button>
</header>
<Card
style={{
borderRadius: '10px',
background: 'linear-gradient(35deg, rgb(246, 246, 260) 0%, rgb(255, 255, 255) 40%)'
}}
className={`w-full mt-[20px]`}
classNames={{
body: 'p-[15px] h-[180px]'
}}
>
<div className="service-info">
<div className="flex items-center">
<div>
<Avatar
shape="square"
size={50}
className={`rounded-[12px] border-none rounded-[12px] ${serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`}
src={
serviceBasicInfo?.logo ? (
<img
src={serviceBasicInfo?.logo}
alt="Logo"
style={{ maxWidth: '200px', width: '45px', height: '45px', objectFit: 'unset' }}
/>
) : undefined
}
icon={serviceBasicInfo?.logo ? '' : <Icon icon="tabler:api-app" />}
>
{' '}
</Avatar>
</div>
<div className="pl-[20px] w-[calc(100%-50px)] overflow-hidden">
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold w-full flex items-center gap-[4px]">
{serviceName}
</p>
<div className="mt-[10px] flex flex-col gap-btnrbase font-normal">
<p>{serviceDesc || '-'}</p>
<p className="flex items-center gap-[4px]">
<Icon icon="ic:baseline-link" width="18" height="18" />
<span className="font-bold">{$t('Base URL')}</span>: {serviceBasicInfo?.invokeAddress || '-'}
</p>
<div>
<Button type="primary" onClick={() => openModal('apply')}>
{$t('申请')}
</Button>
</div>
<div className="mt-[5px] h-[20px] flex items-center font-normal">
{serviceTags.map((tag, index) => (
<Tag
key={index}
color={tag.color}
className={`${tag.textColor} font-normal border-0 mr-[12px] max-w-[150px] truncate`}
bordered={false}
title={tag.title}
>
{tag.content}
</Tag>
))}
{serviceMetrics.map((item, index) => (
<Tooltip key={index} title={$t(item.title)}>
<span className="mr-[12px] flex items-center">
<span className="h-[14px] mr-[4px] flex items-center">{item.icon}</span>
<span className="font-normal text-[14px]">{item.value}</span>
</span>
</Tooltip>
))}
</div>
</div>
</div>
</section>
<Tabs className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto" items={items} />
</section>
<section className="col-span-1 p-btnbase px-btnrbase">
<Descriptions title={$t('服务信息')} column={1} size={'small'}>
<Descriptions.Item label={$t('接入消费者')}>{serviceBasicInfo?.appNum ?? '-'}</Descriptions.Item>
<Descriptions.Item label={$t('供应方')}>{serviceBasicInfo?.team?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t('审核')}>
{serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-'}
</Descriptions.Item>
<Descriptions.Item label={$t('分类')}>{serviceBasicInfo?.catalogue?.name || '-'}</Descriptions.Item>
<Descriptions.Item label={$t('标签')}>
{serviceBasicInfo?.tags?.map((x) => x.name)?.join(',') || '-'}
</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions column={1}>
<Descriptions.Item label={$t('版本')}>{serviceBasicInfo?.version || '-'}</Descriptions.Item>
<Descriptions.Item label={$t('更新时间')}>
<span className="truncate" title={serviceBasicInfo?.updateTime}>
{serviceBasicInfo?.updateTime || '-'}
</span>
</Descriptions.Item>
</Descriptions>
</section>
</section>
<span className="line-clamp-2 mt-[15px] text-[12px] text-[#666]" title={serviceDesc}>
{serviceDesc || $t('暂无服务描述')}
</span>
</div>
<div className="absolute bottom-[15px]">
<Button type="primary" onClick={() => openModal('apply')}>
{$t('申请')}
</Button>
</div>
</Card>
<div className="flex">
<Tabs
className="p-btnbase pr-0 overflow-hidden [&>.ant-tabs-content-holder]:overflow-auto w-full flex-1 mr-[10px]"
onChange={handleTabChange}
items={tabItem}
/>
<IntegrationAIContainer
service={service}
currentTab={currentTab}
serviceId={serviceId}
customClassName="mt-[70px] max-h-[calc(100vh-420px)] overflow-auto"
type={'service'}
handleToolsChange={handleToolsChange}
></IntegrationAIContainer>
</div>
</div>
)
}
@@ -160,7 +160,7 @@ const ServiceHubList: FC = () => {
classNames={{ header: 'border-b-[0px] p-[20px] ', body: 'pt-0 h-[110px]' }}
onClick={() => showDocumentDetail(item)}
>
<span className="line-clamp-3 text-[12px] text-[#666] " style={{ 'word-break': 'auto-phrase' }}>
<span className="line-clamp-3 text-[12px] text-[#666]" title={item.description}>
{item.description || $t('暂无服务描述')}
</span>
<CardAction service={item} />
@@ -283,7 +283,7 @@ const CardAction = (props: { service: ServiceHubTableListItem }) => {
<Tooltip title={$t('接入消费者数量')}>
<span className="mr-[12px] flex items-center">
<span className="h-[14px] mr-[4px] flex items-center ">
<iconpark-icon size="14px" name="auto-generate-api"></iconpark-icon>
<Icon icon="tabler:api-app" width="14" height="14" />
</span>
<span className="font-normal text-[14px]">{service.subscriberNum ?? '-'}</span>
</span>
@@ -293,7 +293,7 @@ const CardAction = (props: { service: ServiceHubTableListItem }) => {
<span className="h-[14px] mr-[4px] flex items-center ">
<Icon icon="iconoir:graph-up" width="14" height="14" />
</span>
<span className="font-normal text-[14px]">{formatInvokeCount(service.invokeCount)}</span>
<span className="font-normal text-[14px]">{formatInvokeCount(service.invokeCount ?? 0)}</span>
</span>
</Tooltip>
</div>