#
tokens: 46574/50000 9/349 files (page 7/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 7 of 9. Use http://codebase.md/higress-group/himarket?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       ├── api-style.mdc
│       └── project-architecture.mdc
├── .gitignore
├── build.sh
├── deploy
│   ├── docker
│   │   ├── docker-compose.yml
│   │   └── Docker部署说明.md
│   └── helm
│       ├── Chart.yaml
│       ├── Helm部署说明.md
│       ├── templates
│       │   ├── _helpers.tpl
│       │   ├── himarket-admin-cm.yaml
│       │   ├── himarket-admin-deployment.yaml
│       │   ├── himarket-admin-service.yaml
│       │   ├── himarket-frontend-cm.yaml
│       │   ├── himarket-frontend-deployment.yaml
│       │   ├── himarket-frontend-service.yaml
│       │   ├── himarket-server-cm.yaml
│       │   ├── himarket-server-deployment.yaml
│       │   ├── himarket-server-service.yaml
│       │   ├── mysql.yaml
│       │   └── serviceaccount.yaml
│       └── values.yaml
├── LICENSE
├── NOTICE
├── pom.xml
├── portal-bootstrap
│   ├── Dockerfile
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── alibaba
│       │   │           └── apiopenplatform
│       │   │               ├── config
│       │   │               │   ├── AsyncConfig.java
│       │   │               │   ├── FilterConfig.java
│       │   │               │   ├── PageConfig.java
│       │   │               │   ├── RestTemplateConfig.java
│       │   │               │   ├── SecurityConfig.java
│       │   │               │   └── SwaggerConfig.java
│       │   │               ├── filter
│       │   │               │   └── PortalResolvingFilter.java
│       │   │               └── PortalApplication.java
│       │   └── resources
│       │       └── application.yaml
│       └── test
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           └── integration
│                               └── AdministratorAuthIntegrationTest.java
├── portal-dal
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── converter
│                           │   ├── AdpAIGatewayConfigConverter.java
│                           │   ├── APIGConfigConverter.java
│                           │   ├── APIGRefConfigConverter.java
│                           │   ├── ApiKeyConfigConverter.java
│                           │   ├── ConsumerAuthConfigConverter.java
│                           │   ├── GatewayConfigConverter.java
│                           │   ├── HigressConfigConverter.java
│                           │   ├── HigressRefConfigConverter.java
│                           │   ├── HmacConfigConverter.java
│                           │   ├── JsonConverter.java
│                           │   ├── JwtConfigConverter.java
│                           │   ├── NacosRefConfigConverter.java
│                           │   ├── PortalSettingConfigConverter.java
│                           │   ├── PortalUiConfigConverter.java
│                           │   └── ProductIconConverter.java
│                           ├── entity
│                           │   ├── Administrator.java
│                           │   ├── BaseEntity.java
│                           │   ├── Consumer.java
│                           │   ├── ConsumerCredential.java
│                           │   ├── ConsumerRef.java
│                           │   ├── Developer.java
│                           │   ├── DeveloperExternalIdentity.java
│                           │   ├── Gateway.java
│                           │   ├── NacosInstance.java
│                           │   ├── Portal.java
│                           │   ├── PortalDomain.java
│                           │   ├── Product.java
│                           │   ├── ProductPublication.java
│                           │   ├── ProductRef.java
│                           │   └── ProductSubscription.java
│                           ├── repository
│                           │   ├── AdministratorRepository.java
│                           │   ├── BaseRepository.java
│                           │   ├── ConsumerCredentialRepository.java
│                           │   ├── ConsumerRefRepository.java
│                           │   ├── ConsumerRepository.java
│                           │   ├── DeveloperExternalIdentityRepository.java
│                           │   ├── DeveloperRepository.java
│                           │   ├── GatewayRepository.java
│                           │   ├── NacosInstanceRepository.java
│                           │   ├── PortalDomainRepository.java
│                           │   ├── PortalRepository.java
│                           │   ├── ProductPublicationRepository.java
│                           │   ├── ProductRefRepository.java
│                           │   ├── ProductRepository.java
│                           │   └── SubscriptionRepository.java
│                           └── support
│                               ├── common
│                               │   ├── Encrypted.java
│                               │   ├── Encryptor.java
│                               │   └── User.java
│                               ├── consumer
│                               │   ├── AdpAIAuthConfig.java
│                               │   ├── APIGAuthConfig.java
│                               │   ├── ApiKeyConfig.java
│                               │   ├── ConsumerAuthConfig.java
│                               │   ├── HigressAuthConfig.java
│                               │   ├── HmacConfig.java
│                               │   └── JwtConfig.java
│                               ├── enums
│                               │   ├── APIGAPIType.java
│                               │   ├── ConsumerAuthType.java
│                               │   ├── ConsumerStatus.java
│                               │   ├── CredentialMode.java
│                               │   ├── DeveloperAuthType.java
│                               │   ├── DeveloperStatus.java
│                               │   ├── DomainType.java
│                               │   ├── GatewayType.java
│                               │   ├── GrantType.java
│                               │   ├── HigressAPIType.java
│                               │   ├── JwtAlgorithm.java
│                               │   ├── ProductIconType.java
│                               │   ├── ProductStatus.java
│                               │   ├── ProductType.java
│                               │   ├── ProtocolType.java
│                               │   ├── PublicKeyFormat.java
│                               │   ├── SourceType.java
│                               │   ├── SubscriptionStatus.java
│                               │   └── UserType.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayConfig.java
│                               │   ├── APIGConfig.java
│                               │   ├── GatewayConfig.java
│                               │   └── HigressConfig.java
│                               ├── portal
│                               │   ├── AuthCodeConfig.java
│                               │   ├── IdentityMapping.java
│                               │   ├── JwtBearerConfig.java
│                               │   ├── OAuth2Config.java
│                               │   ├── OidcConfig.java
│                               │   ├── PortalSettingConfig.java
│                               │   ├── PortalUiConfig.java
│                               │   └── PublicKeyConfig.java
│                               └── product
│                                   ├── APIGRefConfig.java
│                                   ├── HigressRefConfig.java
│                                   ├── NacosRefConfig.java
│                                   └── ProductIcon.java
├── portal-server
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── controller
│                           │   ├── AdministratorController.java
│                           │   ├── ConsumerController.java
│                           │   ├── DeveloperController.java
│                           │   ├── GatewayController.java
│                           │   ├── NacosController.java
│                           │   ├── OAuth2Controller.java
│                           │   ├── OidcController.java
│                           │   ├── PortalController.java
│                           │   └── ProductController.java
│                           ├── core
│                           │   ├── advice
│                           │   │   ├── ExceptionAdvice.java
│                           │   │   └── ResponseAdvice.java
│                           │   ├── annotation
│                           │   │   ├── AdminAuth.java
│                           │   │   ├── AdminOrDeveloperAuth.java
│                           │   │   └── DeveloperAuth.java
│                           │   ├── constant
│                           │   │   ├── CommonConstants.java
│                           │   │   ├── IdpConstants.java
│                           │   │   ├── JwtConstants.java
│                           │   │   └── Resources.java
│                           │   ├── event
│                           │   │   ├── DeveloperDeletingEvent.java
│                           │   │   ├── PortalDeletingEvent.java
│                           │   │   └── ProductDeletingEvent.java
│                           │   ├── exception
│                           │   │   ├── BusinessException.java
│                           │   │   └── ErrorCode.java
│                           │   ├── response
│                           │   │   └── Response.java
│                           │   ├── security
│                           │   │   ├── ContextHolder.java
│                           │   │   ├── DeveloperAuthenticationProvider.java
│                           │   │   └── JwtAuthenticationFilter.java
│                           │   └── utils
│                           │       ├── IdGenerator.java
│                           │       ├── PasswordHasher.java
│                           │       └── TokenUtil.java
│                           ├── dto
│                           │   ├── converter
│                           │   │   ├── InputConverter.java
│                           │   │   ├── NacosToGatewayToolsConverter.java
│                           │   │   └── OutputConverter.java
│                           │   ├── params
│                           │   │   ├── admin
│                           │   │   │   ├── AdminCreateParam.java
│                           │   │   │   ├── AdminLoginParam.java
│                           │   │   │   └── ResetPasswordParam.java
│                           │   │   ├── consumer
│                           │   │   │   ├── CreateConsumerParam.java
│                           │   │   │   ├── CreateCredentialParam.java
│                           │   │   │   ├── CreateSubscriptionParam.java
│                           │   │   │   ├── QueryConsumerParam.java
│                           │   │   │   ├── QuerySubscriptionParam.java
│                           │   │   │   └── UpdateCredentialParam.java
│                           │   │   ├── developer
│                           │   │   │   ├── CreateDeveloperParam.java
│                           │   │   │   ├── CreateExternalDeveloperParam.java
│                           │   │   │   ├── DeveloperLoginParam.java
│                           │   │   │   ├── QueryDeveloperParam.java
│                           │   │   │   ├── UnbindExternalIdentityParam.java
│                           │   │   │   ├── UpdateDeveloperParam.java
│                           │   │   │   └── UpdateDeveloperStatusParam.java
│                           │   │   ├── gateway
│                           │   │   │   ├── ImportGatewayParam.java
│                           │   │   │   ├── QueryAdpAIGatewayParam.java
│                           │   │   │   ├── QueryAPIGParam.java
│                           │   │   │   └── QueryGatewayParam.java
│                           │   │   ├── nacos
│                           │   │   │   ├── CreateNacosParam.java
│                           │   │   │   ├── QueryNacosNamespaceParam.java
│                           │   │   │   ├── QueryNacosParam.java
│                           │   │   │   └── UpdateNacosParam.java
│                           │   │   ├── portal
│                           │   │   │   ├── BindDomainParam.java
│                           │   │   │   ├── CreatePortalParam.java
│                           │   │   │   └── UpdatePortalParam.java
│                           │   │   └── product
│                           │   │       ├── CreateProductParam.java
│                           │   │       ├── CreateProductRefParam.java
│                           │   │       ├── PublishProductParam.java
│                           │   │       ├── QueryProductParam.java
│                           │   │       ├── QueryProductSubscriptionParam.java
│                           │   │       ├── UnPublishProductParam.java
│                           │   │       └── UpdateProductParam.java
│                           │   └── result
│                           │       ├── AdminResult.java
│                           │       ├── AdpGatewayInstanceResult.java
│                           │       ├── AdpMcpServerListResult.java
│                           │       ├── AdpMCPServerResult.java
│                           │       ├── APIConfigResult.java
│                           │       ├── APIGMCPServerResult.java
│                           │       ├── APIResult.java
│                           │       ├── AuthResult.java
│                           │       ├── ConsumerCredentialResult.java
│                           │       ├── ConsumerResult.java
│                           │       ├── DeveloperResult.java
│                           │       ├── GatewayMCPServerResult.java
│                           │       ├── GatewayResult.java
│                           │       ├── HigressMCPServerResult.java
│                           │       ├── IdpResult.java
│                           │       ├── IdpState.java
│                           │       ├── IdpTokenResult.java
│                           │       ├── MCPConfigResult.java
│                           │       ├── MCPServerResult.java
│                           │       ├── MseNacosResult.java
│                           │       ├── NacosMCPServerResult.java
│                           │       ├── NacosNamespaceResult.java
│                           │       ├── NacosResult.java
│                           │       ├── PageResult.java
│                           │       ├── PortalResult.java
│                           │       ├── ProductPublicationResult.java
│                           │       ├── ProductRefResult.java
│                           │       ├── ProductResult.java
│                           │       └── SubscriptionResult.java
│                           └── service
│                               ├── AdministratorService.java
│                               ├── AdpAIGatewayService.java
│                               ├── ConsumerService.java
│                               ├── DeveloperService.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayOperator.java
│                               │   ├── AIGatewayOperator.java
│                               │   ├── APIGOperator.java
│                               │   ├── client
│                               │   │   ├── AdpAIGatewayClient.java
│                               │   │   ├── APIGClient.java
│                               │   │   ├── GatewayClient.java
│                               │   │   ├── HigressClient.java
│                               │   │   ├── PopGatewayClient.java
│                               │   │   └── SLSClient.java
│                               │   ├── factory
│                               │   │   └── HTTPClientFactory.java
│                               │   ├── GatewayOperator.java
│                               │   └── HigressOperator.java
│                               ├── GatewayService.java
│                               ├── IdpService.java
│                               ├── impl
│                               │   ├── AdministratorServiceImpl.java
│                               │   ├── ConsumerServiceImpl.java
│                               │   ├── DeveloperServiceImpl.java
│                               │   ├── GatewayServiceImpl.java
│                               │   ├── IdpServiceImpl.java
│                               │   ├── NacosServiceImpl.java
│                               │   ├── OAuth2ServiceImpl.java
│                               │   ├── OidcServiceImpl.java
│                               │   ├── PortalServiceImpl.java
│                               │   └── ProductServiceImpl.java
│                               ├── NacosService.java
│                               ├── OAuth2Service.java
│                               ├── OidcService.java
│                               ├── PortalService.java
│                               └── ProductService.java
├── portal-web
│   ├── api-portal-admin
│   │   ├── .env
│   │   ├── .gitignore
│   │   ├── bin
│   │   │   ├── replace_var.py
│   │   │   └── start.sh
│   │   ├── Dockerfile
│   │   ├── eslint.config.js
│   │   ├── index.html
│   │   ├── nginx.conf
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   ├── proxy.conf
│   │   ├── public
│   │   │   ├── logo.png
│   │   │   └── vite.svg
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── aliyunThemeToken.ts
│   │   │   ├── App.css
│   │   │   ├── App.tsx
│   │   │   ├── assets
│   │   │   │   └── react.svg
│   │   │   ├── components
│   │   │   │   ├── api-product
│   │   │   │   │   ├── ApiProductApiDocs.tsx
│   │   │   │   │   ├── ApiProductDashboard.tsx
│   │   │   │   │   ├── ApiProductFormModal.tsx
│   │   │   │   │   ├── ApiProductLinkApi.tsx
│   │   │   │   │   ├── ApiProductOverview.tsx
│   │   │   │   │   ├── ApiProductPolicy.tsx
│   │   │   │   │   ├── ApiProductPortal.tsx
│   │   │   │   │   ├── ApiProductUsageGuide.tsx
│   │   │   │   │   ├── SwaggerUIWrapper.css
│   │   │   │   │   └── SwaggerUIWrapper.tsx
│   │   │   │   ├── common
│   │   │   │   │   ├── AdvancedSearch.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── console
│   │   │   │   │   ├── GatewayTypeSelector.tsx
│   │   │   │   │   ├── ImportGatewayModal.tsx
│   │   │   │   │   ├── ImportHigressModal.tsx
│   │   │   │   │   ├── ImportMseNacosModal.tsx
│   │   │   │   │   └── NacosTypeSelector.tsx
│   │   │   │   ├── icons
│   │   │   │   │   └── McpServerIcon.tsx
│   │   │   │   ├── Layout.tsx
│   │   │   │   ├── LayoutWrapper.tsx
│   │   │   │   ├── portal
│   │   │   │   │   ├── PortalConsumers.tsx
│   │   │   │   │   ├── PortalDashboard.tsx
│   │   │   │   │   ├── PortalDevelopers.tsx
│   │   │   │   │   ├── PortalDomain.tsx
│   │   │   │   │   ├── PortalFormModal.tsx
│   │   │   │   │   ├── PortalOverview.tsx
│   │   │   │   │   ├── PortalPublishedApis.tsx
│   │   │   │   │   ├── PortalSecurity.tsx
│   │   │   │   │   ├── PortalSettings.tsx
│   │   │   │   │   ├── PublicKeyManager.tsx
│   │   │   │   │   └── ThirdPartyAuthManager.tsx
│   │   │   │   └── subscription
│   │   │   │       └── SubscriptionListModal.tsx
│   │   │   ├── contexts
│   │   │   │   └── LoadingContext.tsx
│   │   │   ├── index.css
│   │   │   ├── lib
│   │   │   │   ├── api.ts
│   │   │   │   ├── constant.ts
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── pages
│   │   │   │   ├── ApiProductDetail.tsx
│   │   │   │   ├── ApiProducts.tsx
│   │   │   │   ├── Dashboard.tsx
│   │   │   │   ├── GatewayConsoles.tsx
│   │   │   │   ├── Login.tsx
│   │   │   │   ├── NacosConsoles.tsx
│   │   │   │   ├── PortalDetail.tsx
│   │   │   │   ├── Portals.tsx
│   │   │   │   └── Register.tsx
│   │   │   ├── routes
│   │   │   │   └── index.tsx
│   │   │   ├── types
│   │   │   │   ├── api-product.ts
│   │   │   │   ├── consumer.ts
│   │   │   │   ├── gateway.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── portal.ts
│   │   │   │   ├── shims-js-yaml.d.ts
│   │   │   │   └── subscription.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tailwind.config.js
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── api-portal-frontend
│       ├── .env
│       ├── .gitignore
│       ├── .husky
│       │   └── pre-commit
│       ├── bin
│       │   ├── replace_var.py
│       │   └── start.sh
│       ├── Dockerfile
│       ├── eslint.config.js
│       ├── index.html
│       ├── nginx.conf
│       ├── package.json
│       ├── postcss.config.js
│       ├── proxy.conf
│       ├── public
│       │   ├── favicon.ico
│       │   ├── logo.png
│       │   ├── logo.svg
│       │   ├── MCP.png
│       │   ├── MCP.svg
│       │   └── vite.svg
│       ├── README.md
│       ├── src
│       │   ├── aliyunThemeToken.ts
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── assets
│       │   │   ├── aliyun.png
│       │   │   ├── github.png
│       │   │   ├── google.png
│       │   │   └── react.svg
│       │   ├── components
│       │   │   ├── consumer
│       │   │   │   ├── ConsumerBasicInfo.tsx
│       │   │   │   ├── CredentialManager.tsx
│       │   │   │   ├── index.ts
│       │   │   │   └── SubscriptionManager.tsx
│       │   │   ├── Layout.tsx
│       │   │   ├── Navigation.tsx
│       │   │   ├── ProductHeader.tsx
│       │   │   ├── SwaggerUIWrapper.css
│       │   │   ├── SwaggerUIWrapper.tsx
│       │   │   └── UserInfo.tsx
│       │   ├── index.css
│       │   ├── lib
│       │   │   ├── api.ts
│       │   │   ├── statusUtils.ts
│       │   │   └── utils.ts
│       │   ├── main.tsx
│       │   ├── pages
│       │   │   ├── ApiDetail.tsx
│       │   │   ├── Apis.tsx
│       │   │   ├── Callback.tsx
│       │   │   ├── ConsumerDetail.tsx
│       │   │   ├── Consumers.tsx
│       │   │   ├── GettingStarted.tsx
│       │   │   ├── Home.tsx
│       │   │   ├── Login.tsx
│       │   │   ├── Mcp.tsx
│       │   │   ├── McpDetail.tsx
│       │   │   ├── OidcCallback.tsx
│       │   │   ├── Profile.tsx
│       │   │   ├── Register.tsx
│       │   │   └── Test.css
│       │   ├── router.tsx
│       │   ├── types
│       │   │   ├── consumer.ts
│       │   │   └── index.ts
│       │   └── vite-env.d.ts
│       ├── tailwind.config.js
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
└── README.md
```

# Files

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/portal/PortalSettings.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {Card, Form, Input, Select, Switch, Button, Divider, Space, Tag, Table, Modal, message, Tabs} from 'antd'
  2 | import {SaveOutlined, PlusOutlined, DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
  3 | import {useState, useMemo} from 'react'
  4 | import {Portal, ThirdPartyAuthConfig, AuthenticationType, OidcConfig, OAuth2Config} from '@/types'
  5 | import {portalApi} from '@/lib/api'
  6 | import {ThirdPartyAuthManager} from './ThirdPartyAuthManager'
  7 | 
  8 | interface PortalSettingsProps {
  9 |     portal: Portal
 10 |     onRefresh?: () => void
 11 | }
 12 | 
 13 | export function PortalSettings({portal, onRefresh}: PortalSettingsProps) {
 14 |     const [form] = Form.useForm()
 15 |     const [loading, setLoading] = useState(false)
 16 |     const [domainModalVisible, setDomainModalVisible] = useState(false)
 17 |     const [domainForm] = Form.useForm()
 18 |     const [domainLoading, setDomainLoading] = useState(false)
 19 | 
 20 | 
 21 |     // 本地OIDC配置状态,避免频繁刷新
 22 |     // local的有点问题,一切tab就坏了
 23 | 
 24 | 
 25 |     const handleSave = async () => {
 26 |         try {
 27 |             setLoading(true)
 28 |             const values = await form.validateFields()
 29 |             
 30 |             await portalApi.updatePortal(portal.portalId, {
 31 |                 name: portal.name, // 保持现有名称不变
 32 |                 description: portal.description, // 保持现有描述不变
 33 |                 portalSettingConfig: {
 34 |                     ...portal.portalSettingConfig,
 35 |                     builtinAuthEnabled: values.builtinAuthEnabled,
 36 |                     oidcAuthEnabled: values.oidcAuthEnabled,
 37 |                     autoApproveDevelopers: values.autoApproveDevelopers,
 38 |                     autoApproveSubscriptions: values.autoApproveSubscriptions,
 39 |                     frontendRedirectUrl: values.frontendRedirectUrl,
 40 |                 },
 41 |                 portalDomainConfig: portal.portalDomainConfig,
 42 |                 portalUiConfig: portal.portalUiConfig,
 43 |             })
 44 | 
 45 |             message.success('Portal设置保存成功')
 46 |             onRefresh?.()
 47 |         } catch (error) {
 48 |             message.error('保存Portal设置失败')
 49 |         } finally {
 50 |             setLoading(false)
 51 |         }
 52 |     }
 53 | 
 54 |     const handleSettingUpdate = async (key: string, value: any) => {
 55 |         try {
 56 |             await portalApi.updatePortal(portal.portalId, {
 57 |                 ...portal,
 58 |                 portalSettingConfig: {
 59 |                     ...portal.portalSettingConfig,
 60 |                     [key]: value
 61 |                 }
 62 |             })
 63 |             message.success('设置已更新')
 64 |             onRefresh?.()
 65 |         } catch (error) {
 66 |             message.error('设置更新失败')
 67 |         }
 68 |     }
 69 | 
 70 |     const handleAddDomain = () => {
 71 |         setDomainModalVisible(true)
 72 |         domainForm.resetFields()
 73 |     }
 74 | 
 75 |     const handleDomainModalOk = async () => {
 76 |         try {
 77 |             setDomainLoading(true)
 78 |             const values = await domainForm.validateFields()
 79 | 
 80 |             const newDomain = {
 81 |                 domain: values.domain,
 82 |                 protocol: values.protocol,
 83 |                 type: 'CUSTOM'
 84 |             }
 85 | 
 86 |             await portalApi.bindDomain(portal.portalId, newDomain)
 87 |             message.success('域名绑定成功')
 88 |             onRefresh?.()
 89 |             setDomainModalVisible(false)
 90 | 
 91 |         } catch (error) {
 92 |             message.error('绑定域名失败')
 93 |         } finally {
 94 |             setDomainLoading(false)
 95 |         }
 96 |     }
 97 | 
 98 |     const handleDomainModalCancel = () => {
 99 |         setDomainModalVisible(false)
100 |         domainForm.resetFields()
101 |     }
102 | 
103 |     const handleDeleteDomain = async (domain: string) => {
104 |         Modal.confirm({
105 |             title: '确认解绑',
106 |             icon: <ExclamationCircleOutlined/>,
107 |             content: `确定要解绑域名 "${domain}" 吗?此操作不可恢复。`,
108 |             okText: '确认解绑',
109 |             okType: 'danger',
110 |             cancelText: '取消',
111 |             async onOk() {
112 |                 try {
113 |                     await portalApi.unbindDomain(portal.portalId, domain)
114 |                     message.success('域名解绑成功')
115 |                     onRefresh?.()
116 |                 } catch (error) {
117 |                     message.error('解绑域名失败')
118 |                 }
119 |             },
120 |         })
121 |     }
122 | 
123 |     // 合并OIDC和OAuth2配置用于统一显示
124 |     const thirdPartyAuthConfigs = useMemo((): ThirdPartyAuthConfig[] => {
125 |         const configs: ThirdPartyAuthConfig[] = []
126 |         
127 |         // 添加OIDC配置
128 |         if (portal.portalSettingConfig?.oidcConfigs) {
129 |             portal.portalSettingConfig.oidcConfigs.forEach(oidcConfig => {
130 |                 configs.push({
131 |                     ...oidcConfig,
132 |                     type: AuthenticationType.OIDC
133 |                 })
134 |             })
135 |         }
136 |         
137 |         // 添加OAuth2配置
138 |         if (portal.portalSettingConfig?.oauth2Configs) {
139 |             portal.portalSettingConfig.oauth2Configs.forEach(oauth2Config => {
140 |                 configs.push({
141 |                     ...oauth2Config,
142 |                     type: AuthenticationType.OAUTH2
143 |                 })
144 |             })
145 |         }
146 |         
147 |         return configs
148 |     }, [portal.portalSettingConfig?.oidcConfigs, portal.portalSettingConfig?.oauth2Configs])
149 | 
150 |     // 第三方认证配置保存函数
151 |     const handleSaveThirdPartyAuth = async (configs: ThirdPartyAuthConfig[]) => {
152 |         try {
153 |             // 分离OIDC和OAuth2配置,去掉type字段
154 |             const oidcConfigs = configs
155 |                 .filter(config => config.type === AuthenticationType.OIDC)
156 |                 .map(config => {
157 |                     const { type, ...oidcConfig } = config as (OidcConfig & { type: AuthenticationType.OIDC })
158 |                     return oidcConfig
159 |                 })
160 | 
161 |             const oauth2Configs = configs
162 |                 .filter(config => config.type === AuthenticationType.OAUTH2)
163 |                 .map(config => {
164 |                     const { type, ...oauth2Config } = config as (OAuth2Config & { type: AuthenticationType.OAUTH2 })
165 |                     return oauth2Config
166 |                 })
167 |             
168 |             const updateData = {
169 |                 ...portal,
170 |                 portalSettingConfig: {
171 |                     ...portal.portalSettingConfig,
172 |                     // 直接保存分离的配置数组
173 |                     oidcConfigs: oidcConfigs,
174 |                     oauth2Configs: oauth2Configs
175 |                 }
176 |             }
177 |             
178 |             await portalApi.updatePortal(portal.portalId, updateData)
179 |             
180 |             onRefresh?.()
181 |         } catch (error) {
182 |             throw error
183 |         }
184 |     }
185 | 
186 |     // 域名表格列定义
187 |     const domainColumns = [
188 |         {
189 |             title: '域名',
190 |             dataIndex: 'domain',
191 |             key: 'domain',
192 |         },
193 |         {
194 |             title: '协议',
195 |             dataIndex: 'protocol',
196 |             key: 'protocol',
197 |         },
198 |         {
199 |             title: '类型',
200 |             dataIndex: 'type',
201 |             key: 'type',
202 |             render: (type: string) => (
203 |                 <Tag color={type === 'DEFAULT' ? 'blue' : 'green'}>
204 |                     {type === 'DEFAULT' ? '默认域名' : '自定义域名'}
205 |                 </Tag>
206 |             )
207 |         },
208 |         {
209 |             title: '操作',
210 |             key: 'action',
211 |             render: (_: any, record: any) => (
212 |                 <Space>
213 |                     {record.type === 'CUSTOM' && (
214 |                         <Button
215 |                             type="link"
216 |                             danger
217 |                             icon={<DeleteOutlined/>}
218 |                             onClick={() => handleDeleteDomain(record.domain)}
219 |                         >
220 |                             解绑
221 |                         </Button>
222 |                     )}
223 |                 </Space>
224 |             )
225 |         }
226 |     ]
227 | 
228 |     const tabItems = [
229 |         {
230 |             key: 'auth',
231 |             label: '安全设置',
232 |             children: (
233 |                 <div className="space-y-6">
234 |                     {/* 基本安全设置 */}
235 |                     <div className="grid grid-cols-2 gap-6">
236 |                         <Form.Item
237 |                             name="builtinAuthEnabled"
238 |                             label="账号密码登录"
239 |                             valuePropName="checked"
240 |                         >
241 |                             <Switch
242 |                                 onChange={(checked) => handleSettingUpdate('builtinAuthEnabled', checked)}
243 |                             />
244 |                         </Form.Item>
245 |                         {/* <Form.Item
246 |               name="oidcAuthEnabled"
247 |               label="OIDC认证"
248 |               valuePropName="checked"
249 |             >
250 |               <Switch 
251 |                 onChange={(checked) => handleSettingUpdate('oidcAuthEnabled', checked)}
252 |               />
253 |             </Form.Item> */}
254 |                         <Form.Item
255 |                             name="autoApproveDevelopers"
256 |                             label="开发者自动审批"
257 |                             valuePropName="checked"
258 |                         >
259 |                             <Switch
260 |                                 onChange={(checked) => handleSettingUpdate('autoApproveDevelopers', checked)}
261 |                             />
262 |                         </Form.Item>
263 |                         <Form.Item
264 |                             name="autoApproveSubscriptions"
265 |                             label="订阅自动审批"
266 |                             valuePropName="checked"
267 |                         >
268 |                             <Switch
269 |                                 onChange={(checked) => handleSettingUpdate('autoApproveSubscriptions', checked)}
270 |                             />
271 |                         </Form.Item>
272 |                     </div>
273 | 
274 |                     {/* 第三方认证管理 */}
275 |                     <Divider/>
276 |                     <ThirdPartyAuthManager
277 |                         configs={thirdPartyAuthConfigs}
278 |                         onSave={handleSaveThirdPartyAuth}
279 |                     />
280 |                 </div>
281 |             )
282 |         },
283 |         {
284 |             key: 'domain',
285 |             label: '域名管理',
286 |             children: (
287 |                 <div>
288 |                     <div className="flex justify-between items-center mb-4">
289 |                         <div>
290 |                             <h3 className="text-lg font-medium">域名列表</h3>
291 |                             <p className="text-sm text-gray-500">管理Portal的域名配置</p>
292 |                         </div>
293 |                         <Button
294 |                             type="primary"
295 |                             icon={<PlusOutlined/>}
296 |                             onClick={handleAddDomain}
297 |                         >
298 |                             绑定域名
299 |                         </Button>
300 |                     </div>
301 |                     <Table
302 |                         columns={domainColumns}
303 |                         dataSource={portal.portalDomainConfig || []}
304 |                         rowKey="domain"
305 |                         pagination={false}
306 |                         size="small"
307 |                     />
308 |                 </div>
309 |             )
310 |         }
311 |     ]
312 | 
313 |     return (
314 |         <div className="p-6 space-y-6">
315 |             <div className="flex justify-between items-center">
316 |                 <div>
317 |                     <h1 className="text-2xl font-bold mb-2">Portal设置</h1>
318 |                     <p className="text-gray-600">配置Portal的基本设置和高级选项</p>
319 |                 </div>
320 |                 <Space>
321 |                     <Button type="primary" icon={<SaveOutlined/>} loading={loading} onClick={handleSave}>
322 |                         保存设置
323 |                     </Button>
324 |                 </Space>
325 |             </div>
326 | 
327 |             <Form
328 |                 form={form}
329 |                 layout="vertical"
330 |                 initialValues={{
331 |                     portalSettingConfig: portal.portalSettingConfig,
332 |                     builtinAuthEnabled: portal.portalSettingConfig?.builtinAuthEnabled,
333 |                     oidcAuthEnabled: portal.portalSettingConfig?.oidcAuthEnabled,
334 |                     autoApproveDevelopers: portal.portalSettingConfig?.autoApproveDevelopers,
335 |                     autoApproveSubscriptions: portal.portalSettingConfig?.autoApproveSubscriptions,
336 |                     frontendRedirectUrl: portal.portalSettingConfig?.frontendRedirectUrl,
337 |                     portalDomainConfig: portal.portalDomainConfig,
338 |                 }}
339 |             >
340 |                 <Card>
341 |                     <Tabs
342 |                         items={tabItems}
343 |                         defaultActiveKey="auth"
344 |                         type="card"
345 |                     />
346 |                 </Card>
347 |             </Form>
348 | 
349 |             {/* 域名绑定模态框 */}
350 |             <Modal
351 |                 title="绑定域名"
352 |                 open={domainModalVisible}
353 |                 onOk={handleDomainModalOk}
354 |                 onCancel={handleDomainModalCancel}
355 |                 confirmLoading={domainLoading}
356 |                 okText="绑定"
357 |                 cancelText="取消"
358 |             >
359 |                 <Form
360 |                     form={domainForm}
361 |                     layout="vertical"
362 |                 >
363 |                     <Form.Item
364 |                         name="domain"
365 |                         label="域名"
366 |                         rules={[
367 |                             {required: true, message: '请输入域名'},
368 |                             {
369 |                                 pattern: /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
370 |                                 message: '请输入有效的域名格式'
371 |                             }
372 |                         ]}
373 |                     >
374 |                         <Input placeholder="example.com"/>
375 |                     </Form.Item>
376 |                     <Form.Item
377 |                         name="protocol"
378 |                         label="协议"
379 |                         rules={[{required: true, message: '请选择协议'}]}
380 |                     >
381 |                         <Select placeholder="请选择协议">
382 |                             <Select.Option value="HTTP">HTTP</Select.Option>
383 |                             <Select.Option value="HTTPS">HTTPS</Select.Option>
384 |                         </Select>
385 |                     </Form.Item>
386 |                 </Form>
387 |             </Modal>
388 |         </div>
389 |     )
390 | }
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/pages/Portals.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useCallback, memo, useEffect } from "react";
  2 | import { useNavigate } from "react-router-dom";
  3 | import {
  4 |   Button,
  5 |   Card,
  6 |   Avatar,
  7 |   Dropdown,
  8 |   Modal,
  9 |   Form,
 10 |   Input,
 11 |   message,
 12 |   Tooltip,
 13 |   Pagination,
 14 |   Skeleton,
 15 | } from "antd";
 16 | import { PlusOutlined, MoreOutlined, LinkOutlined } from "@ant-design/icons";
 17 | import type { MenuProps } from "antd";
 18 | import { portalApi } from "../lib/api";
 19 | 
 20 | import { Portal } from '@/types'
 21 | 
 22 | // 优化的Portal卡片组件
 23 | const PortalCard = memo(
 24 |   ({
 25 |     portal,
 26 |     onNavigate,
 27 |     fetchPortals,
 28 |   }: {
 29 |     portal: Portal;
 30 |     onNavigate: (id: string) => void;
 31 |     fetchPortals: () => void;
 32 |   }) => {
 33 |     const handleCardClick = useCallback(() => {
 34 |       onNavigate(portal.portalId);
 35 |     }, [portal.portalId, onNavigate]);
 36 | 
 37 |     const handleLinkClick = useCallback((e: React.MouseEvent) => {
 38 |       e.stopPropagation();
 39 |     }, []);
 40 | 
 41 |     const dropdownItems: MenuProps["items"] = [
 42 |      
 43 |       {
 44 |         key: "delete",
 45 |         label: "删除",
 46 |         danger: true,
 47 |         onClick: (e) => {
 48 |           e?.domEvent?.stopPropagation(); // 阻止事件冒泡
 49 |           Modal.confirm({
 50 |             title: "删除Portal",
 51 |             content: "确定要删除该Portal吗?",
 52 |             onOk: () => {
 53 |               return handleDeletePortal(portal.portalId);
 54 |             },
 55 |           });
 56 |         },
 57 |       },
 58 |     ];
 59 | 
 60 |     const handleDeletePortal = useCallback((portalId: string) => {
 61 |       return portalApi.deletePortal(portalId).then(() => {
 62 |         message.success("Portal删除成功");
 63 |         fetchPortals();
 64 |       }).catch((error) => {
 65 |         message.error(error?.response?.data?.message || "删除失败,请稍后重试");
 66 |         throw error;
 67 |       });
 68 |     }, [fetchPortals]);
 69 | 
 70 |     return (
 71 |       <Card
 72 |         className="cursor-pointer hover:shadow-xl transition-all duration-300 hover:scale-[1.02] border border-gray-100 hover:border-blue-300 bg-gradient-to-br from-white to-gray-50/30"
 73 |         onClick={handleCardClick}
 74 |         bodyStyle={{ padding: "20px" }}
 75 |       >
 76 |         <div className="flex items-center justify-between mb-6">
 77 |           <div className="flex items-center space-x-4">
 78 |             <div className="relative">
 79 |               <Avatar
 80 |                 size={48}
 81 |                 className="bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg"
 82 |                 style={{ fontSize: "18px", fontWeight: "600" }}
 83 |               >
 84 |                 {portal.title.charAt(0).toUpperCase()}
 85 |               </Avatar>
 86 |               <div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-400 rounded-full border-2 border-white"></div>
 87 |             </div>
 88 |             <div>
 89 |               <h3 className="text-xl font-bold text-gray-800 mb-1">
 90 |                 {portal.title}
 91 |               </h3>
 92 |               <p className="text-sm text-gray-500">{portal.description}</p>
 93 |             </div>
 94 |           </div>
 95 |           <Dropdown menu={{ items: dropdownItems }} trigger={["click"]}>
 96 |             <Button
 97 |               type="text"
 98 |               icon={<MoreOutlined />}
 99 |               onClick={(e) => e.stopPropagation()}
100 |               className="hover:bg-gray-100 rounded-full"
101 |             />
102 |           </Dropdown>
103 |         </div>
104 | 
105 |         <div className="space-y-6">
106 |           <div className="flex items-center space-x-3 p-3 bg-blue-50 rounded-lg border border-blue-100">
107 |             <LinkOutlined className="h-4 w-4 text-blue-500" />
108 |             <Tooltip
109 |               title={portal.portalDomainConfig?.[0].domain}
110 |               placement="top"
111 |               color="#000"
112 |             >
113 |               <a
114 |                 href={`http://${portal.portalDomainConfig?.[0].domain}`}
115 |                 target="_blank"
116 |                 rel="noopener noreferrer"
117 |                 className="text-blue-600 hover:text-blue-700 font-medium text-sm"
118 |                 onClick={handleLinkClick}
119 |                 style={{
120 |                   display: "inline-block",
121 |                   maxWidth: 200,
122 |                   overflow: "hidden",
123 |                   textOverflow: "ellipsis",
124 |                   whiteSpace: "nowrap",
125 |                   verticalAlign: "bottom",
126 |                   cursor: "pointer",
127 |                 }}
128 |               >
129 |                 {portal.portalDomainConfig?.[0].domain}
130 |               </a>
131 |             </Tooltip>
132 |           </div>
133 | 
134 |           <div className="space-y-3">
135 |             {/* 第一行:账号密码登录 + 开发者自动审批 */}
136 |             <div className="grid grid-cols-2 gap-4">
137 |               <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
138 |                 <span className="text-xs font-medium text-gray-600">
139 |                   账号密码登录
140 |                 </span>
141 |                 <span
142 |                   className={`px-2 py-1 rounded-full text-xs font-medium ${
143 |                     portal.portalSettingConfig?.builtinAuthEnabled
144 |                       ? "bg-green-100 text-green-700"
145 |                       : "bg-red-100 text-red-700"
146 |                   }`}
147 |                 >
148 |                   {portal.portalSettingConfig?.builtinAuthEnabled
149 |                     ? "支持"
150 |                     : "不支持"}
151 |                 </span>
152 |               </div>
153 | 
154 |               <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
155 |                 <span className="text-xs font-medium text-gray-600">
156 |                   开发者自动审批
157 |                 </span>
158 |                 <span
159 |                   className={`px-2 py-1 rounded-full text-xs font-medium ${
160 |                     portal.portalSettingConfig?.autoApproveDevelopers
161 |                       ? "bg-green-100 text-green-700"
162 |                       : "bg-yellow-100 text-yellow-700"
163 |                   }`}
164 |                 >
165 |                   {portal.portalSettingConfig?.autoApproveDevelopers
166 |                     ? "是"
167 |                     : "否"}
168 |                 </span>
169 |               </div>
170 |             </div>
171 | 
172 |             {/* 第二行:订阅自动审批 + 域名配置 */}
173 |             <div className="grid grid-cols-2 gap-4">
174 |               <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
175 |                 <span className="text-xs font-medium text-gray-600">
176 |                   订阅自动审批
177 |                 </span>
178 |                 <span
179 |                   className={`px-2 py-1 rounded-full text-xs font-medium ${
180 |                     portal.portalSettingConfig?.autoApproveSubscriptions
181 |                       ? "bg-green-100 text-green-700"
182 |                       : "bg-yellow-100 text-yellow-700"
183 |                   }`}
184 |                 >
185 |                   {portal.portalSettingConfig?.autoApproveSubscriptions
186 |                     ? "是"
187 |                     : "否"}
188 |                 </span>
189 |               </div>
190 | 
191 |               <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
192 |                 <span className="text-xs font-medium text-gray-600">
193 |                   域名配置
194 |                 </span>
195 |                 <span className="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-medium">
196 |                   {portal.portalDomainConfig?.length || 0}个
197 |                 </span>
198 |               </div>
199 |             </div>
200 |           </div>
201 | 
202 |           <div className="text-center pt-4 border-t border-gray-100">
203 |             <div className="inline-flex items-center space-x-2 text-sm text-gray-500 hover:text-blue-600 transition-colors">
204 |               <span onClick={handleCardClick}>点击查看详情</span>
205 |               <svg
206 |                 className="w-4 h-4"
207 |                 fill="none"
208 |                 stroke="currentColor"
209 |                 viewBox="0 0 24 24"
210 |               >
211 |                 <path
212 |                   strokeLinecap="round"
213 |                   strokeLinejoin="round"
214 |                   strokeWidth={2}
215 |                   d="M9 5l7 7-7 7"
216 |                 />
217 |               </svg>
218 |             </div>
219 |           </div>
220 |         </div>
221 |       </Card>
222 |     );
223 |   }
224 | );
225 | 
226 | PortalCard.displayName = "PortalCard";
227 | 
228 | export default function Portals() {
229 |   const navigate = useNavigate();
230 |   const [portals, setPortals] = useState<Portal[]>([]);
231 |   const [loading, setLoading] = useState<boolean>(true); // 初始状态为 loading
232 |   const [error, setError] = useState<string | null>(null);
233 |   const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
234 |   const [form] = Form.useForm();
235 |   const [pagination, setPagination] = useState({
236 |     current: 1,
237 |     pageSize: 12,
238 |     total: 0,
239 |   });
240 | 
241 |   const fetchPortals = useCallback((page = 1, size = 12) => {
242 |     setLoading(true);
243 |     portalApi.getPortals({ page, size }).then((res: any) => {
244 |       const list = res?.data?.content || [];
245 |       const portals: Portal[] = list.map((item: any) => ({
246 |         portalId: item.portalId,
247 |         name: item.name,
248 |         title: item.name,
249 |         description: item.description,
250 |         adminId: item.adminId,
251 |         portalSettingConfig: item.portalSettingConfig,
252 |         portalUiConfig: item.portalUiConfig,
253 |         portalDomainConfig: item.portalDomainConfig || [],
254 |       }));
255 |       setPortals(portals);
256 |       setPagination({
257 |         current: page,
258 |         pageSize: size,
259 |         total: res?.data?.totalElements || 0,
260 |       });
261 |     }).catch((err: any) => {
262 |       setError(err?.message || "加载失败");
263 |     }).finally(() => {
264 |       setLoading(false);
265 |     });
266 |   }, []);
267 | 
268 |   useEffect(() => {
269 |     setError(null);
270 |     fetchPortals(1, 12);
271 |   }, [fetchPortals]);
272 | 
273 |   // 处理分页变化
274 |   const handlePaginationChange = (page: number, pageSize: number) => {
275 |     fetchPortals(page, pageSize);
276 |   };
277 | 
278 |   const handleCreatePortal = useCallback(() => {
279 |     setIsModalVisible(true);
280 |   }, []);
281 | 
282 |   const handleModalOk = useCallback(async () => {
283 |     try {
284 |       const values = await form.validateFields();
285 |       setLoading(true);
286 | 
287 |       const newPortal = {
288 |         name: values.name,
289 |         title: values.title,
290 |         description: values.description,
291 |       };
292 | 
293 |       await portalApi.createPortal(newPortal);
294 |       message.success("Portal创建成功");
295 |       setIsModalVisible(false);
296 |       form.resetFields();
297 | 
298 |       fetchPortals()
299 |     } catch (error: any) {
300 |       // message.error(error?.message || "创建失败");
301 |     } finally {
302 |       setLoading(false);
303 |     }
304 |   }, [form]);
305 | 
306 |   const handleModalCancel = useCallback(() => {
307 |     setIsModalVisible(false);
308 |     form.resetFields();
309 |   }, [form]);
310 | 
311 |   const handlePortalClick = useCallback(
312 |     (portalId: string) => {
313 |       navigate(`/portals/detail?id=${portalId}`);
314 |     },
315 |     [navigate]
316 |   );
317 | 
318 |   return (
319 |     <div className="space-y-6">
320 |       <div className="flex items-center justify-between">
321 |         <div>
322 |           <h1 className="text-3xl font-bold tracking-tight">Portal</h1>
323 |           <p className="text-gray-500 mt-2">管理和配置您的开发者门户</p>
324 |         </div>
325 |         <Button
326 |           type="primary"
327 |           icon={<PlusOutlined />}
328 |           onClick={handleCreatePortal}
329 |         >
330 |           创建 Portal
331 |         </Button>
332 |       </div>
333 |       {error && <div className="text-red-500">{error}</div>}
334 |       
335 |       {loading ? (
336 |         <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
337 |           {Array.from({ length: pagination.pageSize || 12 }).map((_, index) => (
338 |             <div key={index} className="h-full rounded-lg shadow-lg bg-white p-4">
339 |               <div className="flex items-start space-x-4">
340 |                 <Skeleton.Avatar size={48} active />
341 |                 <div className="flex-1 min-w-0">
342 |                   <div className="flex items-center justify-between mb-2">
343 |                     <Skeleton.Input active size="small" style={{ width: 120 }} />
344 |                     <Skeleton.Input active size="small" style={{ width: 60 }} />
345 |                   </div>
346 |                   <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
347 |                   <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
348 |                   <div className="flex items-center justify-between">
349 |                     <Skeleton.Input active size="small" style={{ width: 60 }} />
350 |                     <Skeleton.Input active size="small" style={{ width: 80 }} />
351 |                   </div>
352 |                 </div>
353 |               </div>
354 |             </div>
355 |           ))}
356 |         </div>
357 |       ) : (
358 |         <>
359 |           <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
360 |             {portals.map((portal) => (
361 |               <PortalCard
362 |                 key={portal.portalId}
363 |                 portal={portal}
364 |                 onNavigate={handlePortalClick}
365 |                 fetchPortals={() => fetchPortals(pagination.current, pagination.pageSize)}
366 |               />
367 |             ))}
368 |           </div>
369 | 
370 |           {pagination.total > 0 && (
371 |             <div className="flex justify-center mt-6">
372 |               <Pagination
373 |                 current={pagination.current}
374 |                 pageSize={pagination.pageSize}
375 |                 total={pagination.total}
376 |                 onChange={handlePaginationChange}
377 |                 showSizeChanger
378 |                 showQuickJumper
379 |                 showTotal={(total) => `共 ${total} 条`}
380 |                 pageSizeOptions={['6', '12', '24', '48']}
381 |               />
382 |             </div>
383 |           )}
384 |         </>
385 |       )}
386 | 
387 |       <Modal
388 |         title="创建Portal"
389 |         open={isModalVisible}
390 |         onOk={handleModalOk}
391 |         onCancel={handleModalCancel}
392 |         confirmLoading={loading}
393 |         width={600}
394 |       >
395 |         <Form form={form} layout="vertical">
396 |           <Form.Item
397 |             name="name"
398 |             label="名称"
399 |             rules={[{ required: true, message: "请输入Portal名称" }]}
400 |           >
401 |             <Input placeholder="请输入Portal名称" />
402 |           </Form.Item>
403 | 
404 |           {/* <Form.Item
405 |             name="title"
406 |             label="标题"
407 |             rules={[{ required: true, message: "请输入Portal标题" }]}
408 |           >
409 |             <Input placeholder="请输入Portal标题" />
410 |           </Form.Item> */}
411 | 
412 |           <Form.Item
413 |             name="description"
414 |             label="描述"
415 |             rules={[{ message: "请输入描述" }]}
416 |           >
417 |             <Input.TextArea rows={3} placeholder="请输入Portal描述" />
418 |           </Form.Item>
419 |         </Form>
420 |       </Modal>
421 |     </div>
422 |   );
423 | }
424 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/DeveloperServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.util.BooleanUtil;
 23 | import cn.hutool.core.util.StrUtil;
 24 | import com.alibaba.apiopenplatform.core.constant.Resources;
 25 | import com.alibaba.apiopenplatform.core.event.DeveloperDeletingEvent;
 26 | import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
 27 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 28 | import com.alibaba.apiopenplatform.dto.params.developer.CreateDeveloperParam;
 29 | import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
 30 | import com.alibaba.apiopenplatform.dto.params.developer.QueryDeveloperParam;
 31 | import com.alibaba.apiopenplatform.dto.params.developer.UpdateDeveloperParam;
 32 | import com.alibaba.apiopenplatform.dto.result.AuthResult;
 33 | import com.alibaba.apiopenplatform.dto.result.DeveloperResult;
 34 | import com.alibaba.apiopenplatform.dto.result.PageResult;
 35 | import com.alibaba.apiopenplatform.entity.Developer;
 36 | import com.alibaba.apiopenplatform.entity.Portal;
 37 | import com.alibaba.apiopenplatform.repository.DeveloperRepository;
 38 | import com.alibaba.apiopenplatform.repository.PortalRepository;
 39 | import com.alibaba.apiopenplatform.service.DeveloperService;
 40 | import com.alibaba.apiopenplatform.core.utils.PasswordHasher;
 41 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 42 | import com.alibaba.apiopenplatform.repository.DeveloperExternalIdentityRepository;
 43 | import com.alibaba.apiopenplatform.entity.DeveloperExternalIdentity;
 44 | import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType;
 45 | import com.alibaba.apiopenplatform.support.enums.DeveloperStatus;
 46 | import lombok.RequiredArgsConstructor;
 47 | import lombok.extern.slf4j.Slf4j;
 48 | import org.springframework.context.ApplicationEventPublisher;
 49 | import org.springframework.context.event.EventListener;
 50 | import org.springframework.data.domain.Page;
 51 | import org.springframework.data.domain.Pageable;
 52 | import org.springframework.data.jpa.domain.Specification;
 53 | import org.springframework.scheduling.annotation.Async;
 54 | import org.springframework.stereotype.Service;
 55 | import org.springframework.transaction.annotation.Transactional;
 56 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 57 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 58 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 59 | 
 60 | import javax.persistence.criteria.Predicate;
 61 | import java.util.*;
 62 | import javax.servlet.http.HttpServletRequest;
 63 | 
 64 | @Service
 65 | @RequiredArgsConstructor
 66 | @Slf4j
 67 | @Transactional
 68 | public class DeveloperServiceImpl implements DeveloperService {
 69 | 
 70 |     private final DeveloperRepository developerRepository;
 71 | 
 72 |     private final DeveloperExternalIdentityRepository externalRepository;
 73 | 
 74 |     private final PortalRepository portalRepository;
 75 | 
 76 |     private final ContextHolder contextHolder;
 77 | 
 78 |     private final ApplicationEventPublisher eventPublisher;
 79 | 
 80 |     @Override
 81 |     public AuthResult registerDeveloper(CreateDeveloperParam param) {
 82 |         DeveloperResult developer = createDeveloper(param);
 83 | 
 84 |         // 检查是否自动审批
 85 |         String portalId = contextHolder.getPortal();
 86 |         Portal portal = findPortal(portalId);
 87 |         boolean autoApprove = portal.getPortalSettingConfig() != null
 88 |                 && BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveDevelopers());
 89 | 
 90 |         if (autoApprove) {
 91 |             String token = generateToken(developer.getDeveloperId());
 92 |             return AuthResult.of(token, TokenUtil.getTokenExpiresIn());
 93 |         }
 94 |         return null;
 95 |     }
 96 | 
 97 |     @Override
 98 |     public DeveloperResult createDeveloper(CreateDeveloperParam param) {
 99 |         String portalId = contextHolder.getPortal();
100 |         developerRepository.findByPortalIdAndUsername(portalId, param.getUsername()).ifPresent(developer -> {
101 |             throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.DEVELOPER, param.getUsername()));
102 |         });
103 | 
104 |         Developer developer = param.convertTo();
105 |         developer.setDeveloperId(generateDeveloperId());
106 |         developer.setPortalId(portalId);
107 |         developer.setPasswordHash(PasswordHasher.hash(param.getPassword()));
108 | 
109 |         Portal portal = findPortal(portalId);
110 |         boolean autoApprove = portal.getPortalSettingConfig() != null
111 |                 && BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveDevelopers());
112 |         developer.setStatus(autoApprove ? DeveloperStatus.APPROVED : DeveloperStatus.PENDING);
113 |         developer.setAuthType(DeveloperAuthType.BUILTIN);
114 | 
115 |         developerRepository.save(developer);
116 |         return new DeveloperResult().convertFrom(developer);
117 |     }
118 | 
119 |     @Override
120 |     public AuthResult login(String username, String password) {
121 |         String portalId = contextHolder.getPortal();
122 |         Developer developer = developerRepository.findByPortalIdAndUsername(portalId, username)
123 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, username));
124 | 
125 |         if (!DeveloperStatus.APPROVED.equals(developer.getStatus())) {
126 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "账号审批中");
127 |         }
128 | 
129 |         if (!PasswordHasher.verify(password, developer.getPasswordHash())) {
130 |             throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
131 |         }
132 | 
133 |         String token = generateToken(developer.getDeveloperId());
134 |         return AuthResult.builder()
135 |                 .accessToken(token)
136 |                 .expiresIn(TokenUtil.getTokenExpiresIn())
137 |                 .build();
138 |     }
139 | 
140 |     @Override
141 |     public void existsDeveloper(String developerId) {
142 |         developerRepository.findByDeveloperId(developerId)
143 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, developerId));
144 |     }
145 | 
146 |     @Override
147 |     public DeveloperResult createExternalDeveloper(CreateExternalDeveloperParam param) {
148 |         Developer developer = Developer.builder()
149 |                 .developerId(IdGenerator.genDeveloperId())
150 |                 .portalId(contextHolder.getPortal())
151 |                 .username(buildExternalName(param.getProvider(), param.getDisplayName()))
152 |                 .email(param.getEmail())
153 |                 // 默认APPROVED
154 |                 .status(DeveloperStatus.APPROVED)
155 |                 .build();
156 | 
157 |         DeveloperExternalIdentity externalIdentity = DeveloperExternalIdentity.builder()
158 |                 .provider(param.getProvider())
159 |                 .subject(param.getSubject())
160 |                 .displayName(param.getDisplayName())
161 |                 .authType(param.getAuthType())
162 |                 .developer(developer)
163 |                 .build();
164 | 
165 |         developerRepository.save(developer);
166 |         externalRepository.save(externalIdentity);
167 |         return new DeveloperResult().convertFrom(developer);
168 |     }
169 | 
170 |     @Override
171 |     public DeveloperResult getExternalDeveloper(String provider, String subject) {
172 |         return externalRepository.findByProviderAndSubject(provider, subject)
173 |                 .map(o -> new DeveloperResult().convertFrom(o.getDeveloper()))
174 |                 .orElse(null);
175 |     }
176 | 
177 |     private String buildExternalName(String provider, String subject) {
178 |         return StrUtil.format("{}_{}", provider, subject);
179 |     }
180 | 
181 |     @Override
182 |     public void deleteDeveloper(String developerId) {
183 |         eventPublisher.publishEvent(new DeveloperDeletingEvent(developerId));
184 |         externalRepository.deleteByDeveloper_DeveloperId(developerId);
185 |         developerRepository.findByDeveloperId(developerId).ifPresent(developerRepository::delete);
186 |     }
187 | 
188 |     @Override
189 |     public DeveloperResult getDeveloper(String developerId) {
190 |         Developer developer = findDeveloper(developerId);
191 |         return new DeveloperResult().convertFrom(developer);
192 |     }
193 | 
194 |     @Override
195 |     public PageResult<DeveloperResult> listDevelopers(QueryDeveloperParam param, Pageable pageable) {
196 |         if (contextHolder.isDeveloper()) {
197 |             param.setPortalId(contextHolder.getPortal());
198 |         }
199 |         Page<Developer> developers = developerRepository.findAll(buildSpecification(param), pageable);
200 |         return new PageResult<DeveloperResult>().convertFrom(developers, developer -> new DeveloperResult().convertFrom(developer));
201 |     }
202 | 
203 |     @Override
204 |     public void setDeveloperStatus(String developerId, DeveloperStatus status) {
205 |         Developer developer = findDeveloper(developerId);
206 |         developer.setStatus(status);
207 |         developerRepository.save(developer);
208 |     }
209 | 
210 |     @Override
211 |     @Transactional
212 |     public boolean resetPassword(String developerId, String oldPassword, String newPassword) {
213 |         Developer developer = findDeveloper(developerId);
214 | 
215 |         if (!PasswordHasher.verify(oldPassword, developer.getPasswordHash())) {
216 |             throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
217 |         }
218 | 
219 |         developer.setPasswordHash(PasswordHasher.hash(newPassword));
220 |         developerRepository.save(developer);
221 |         return true;
222 |     }
223 | 
224 |     @Override
225 |     public boolean updateProfile(UpdateDeveloperParam param) {
226 |         Developer developer = findDeveloper(contextHolder.getUser());
227 | 
228 |         String username = param.getUsername();
229 |         if (username != null && !username.equals(developer.getUsername())) {
230 |             if (developerRepository.findByPortalIdAndUsername(developer.getPortalId(), username).isPresent()) {
231 |                 throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.DEVELOPER, username));
232 |             }
233 |         }
234 |         param.update(developer);
235 | 
236 |         developerRepository.save(developer);
237 |         return true;
238 |     }
239 | 
240 |     @EventListener
241 |     @Async("taskExecutor")
242 |     public void handlePortalDeletion(PortalDeletingEvent event) {
243 |         String portalId = event.getPortalId();
244 |         List<Developer> developers = developerRepository.findByPortalId(portalId);
245 |         developers.forEach(developer -> deleteDeveloper(developer.getDeveloperId()));
246 |     }
247 | 
248 |     private String generateToken(String developerId) {
249 |         return TokenUtil.generateDeveloperToken(developerId);
250 |     }
251 | 
252 |     private Developer createExternalDeveloper(String providerName, String providerSubject, String email, String displayName, String rawInfoJson) {
253 |         String portalId = contextHolder.getPortal();
254 |         String username = generateUniqueUsername(portalId, displayName, providerName, providerSubject);
255 | 
256 |         Developer developer = Developer.builder()
257 |                 .developerId(generateDeveloperId())
258 |                 .portalId(portalId)
259 |                 .username(username)
260 |                 .email(email)
261 |                 .status(DeveloperStatus.APPROVED)
262 |                 .authType(DeveloperAuthType.OIDC)
263 |                 .build();
264 |         developer = developerRepository.save(developer);
265 | 
266 |         DeveloperExternalIdentity ext = DeveloperExternalIdentity.builder()
267 |                 .provider(providerName)
268 |                 .subject(providerSubject)
269 |                 .displayName(displayName)
270 |                 .rawInfoJson(rawInfoJson)
271 |                 .developer(developer)
272 |                 .build();
273 |         externalRepository.save(ext);
274 |         return developer;
275 |     }
276 | 
277 |     private String generateUniqueUsername(String portalId, String displayName, String providerName, String providerSubject) {
278 |         String username = displayName != null ? displayName : providerName + "_" + providerSubject;
279 |         String originalUsername = username;
280 |         int suffix = 1;
281 |         while (developerRepository.findByPortalIdAndUsername(portalId, username).isPresent()) {
282 |             username = originalUsername + "_" + suffix;
283 |             suffix++;
284 |         }
285 |         return username;
286 |     }
287 | 
288 |     private String generateDeveloperId() {
289 |         return IdGenerator.genDeveloperId();
290 |     }
291 | 
292 |     private Developer findDeveloper(String developerId) {
293 |         return developerRepository.findByDeveloperId(developerId)
294 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, developerId));
295 |     }
296 | 
297 |     private Portal findPortal(String portalId) {
298 |         return portalRepository.findByPortalId(portalId)
299 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
300 |     }
301 | 
302 |     private Specification<Developer> buildSpecification(QueryDeveloperParam param) {
303 |         return (root, query, cb) -> {
304 |             List<Predicate> predicates = new ArrayList<>();
305 | 
306 |             if (StrUtil.isNotBlank(param.getPortalId())) {
307 |                 predicates.add(cb.equal(root.get("portalId"), param.getPortalId()));
308 |             }
309 |             if (StrUtil.isNotBlank(param.getUsername())) {
310 |                 String likePattern = "%" + param.getUsername() + "%";
311 |                 predicates.add(cb.like(root.get("username"), likePattern));
312 |             }
313 |             if (param.getStatus() != null) {
314 |                 predicates.add(cb.equal(root.get("status"), param.getStatus()));
315 |             }
316 | 
317 |             return cb.and(predicates.toArray(new Predicate[0]));
318 |         };
319 |     }
320 | 
321 |     @Override
322 |     public void logout(HttpServletRequest request) {
323 |         // 使用TokenUtil处理登出逻辑
324 |         com.alibaba.apiopenplatform.core.utils.TokenUtil.revokeToken(request);
325 |     }
326 | 
327 |     @Override
328 |     public DeveloperResult getCurrentDeveloperInfo() {
329 |         String currentUserId = contextHolder.getUser();
330 |         Developer developer = findDeveloper(currentUserId);
331 |         return new DeveloperResult().convertFrom(developer);
332 |     }
333 | 
334 |     @Override
335 |     public boolean changeCurrentDeveloperPassword(String oldPassword, String newPassword) {
336 |         String currentUserId = contextHolder.getUser();
337 |         return resetPassword(currentUserId, oldPassword, newPassword);
338 |     }
339 | 
340 | }
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/pages/ApiDetail.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useEffect, useState } from "react";
  2 | import { useParams } from "react-router-dom";
  3 | import { Card, Alert, Row, Col, Tabs } from "antd";
  4 | import { Layout } from "../components/Layout";
  5 | import { ProductHeader } from "../components/ProductHeader";
  6 | import { SwaggerUIWrapper } from "../components/SwaggerUIWrapper";
  7 | import api from "../lib/api";
  8 | import type { Product, ApiResponse } from "../types";
  9 | import ReactMarkdown from "react-markdown";
 10 | import remarkGfm from 'remark-gfm';
 11 | import 'react-markdown-editor-lite/lib/index.css';
 12 | import * as yaml from 'js-yaml';
 13 | import { Button, Typography, Space, Divider, message } from "antd";
 14 | import { CopyOutlined, RocketOutlined, DownloadOutlined } from "@ant-design/icons";
 15 | 
 16 | const { Title, Paragraph } = Typography;
 17 | 
 18 | interface UpdatedProduct extends Omit<Product, 'apiSpec'> {
 19 |   apiConfig?: {
 20 |     spec: string;
 21 |     meta: {
 22 |       source: string;
 23 |       type: string;
 24 |     };
 25 |   };
 26 |   createAt: string;
 27 |   enabled: boolean;
 28 | }
 29 | 
 30 | function ApiDetailPage() {
 31 |   const { id } = useParams();
 32 |   const [loading, setLoading] = useState(true);
 33 |   const [error, setError] = useState('');
 34 |   const [apiData, setApiData] = useState<UpdatedProduct | null>(null);
 35 |   const [baseUrl, setBaseUrl] = useState<string>('');
 36 |   const [examplePath, setExamplePath] = useState<string>('/{path}');
 37 |   const [exampleMethod, setExampleMethod] = useState<string>('GET');
 38 | 
 39 |   useEffect(() => {
 40 |     if (!id) return;
 41 |     fetchApiDetail();
 42 |   }, [id]);
 43 | 
 44 |   const fetchApiDetail = async () => {
 45 |     setLoading(true);
 46 |     setError('');
 47 |     try {
 48 |       const response: ApiResponse<UpdatedProduct> = await api.get(`/products/${id}`);
 49 |       if (response.code === "SUCCESS" && response.data) {
 50 |         setApiData(response.data);
 51 |         
 52 |         // 提取基础URL和示例路径用于curl示例
 53 |         if (response.data.apiConfig?.spec) {
 54 |           try {
 55 |             let openApiDoc: any;
 56 |             try {
 57 |               openApiDoc = yaml.load(response.data.apiConfig.spec);
 58 |             } catch {
 59 |               openApiDoc = JSON.parse(response.data.apiConfig.spec);
 60 |             }
 61 |             
 62 |             // 提取服务器URL并处理尾部斜杠
 63 |             let serverUrl = openApiDoc?.servers?.[0]?.url || '';
 64 |             if (serverUrl && serverUrl.endsWith('/')) {
 65 |               serverUrl = serverUrl.slice(0, -1); // 移除末尾的斜杠
 66 |             }
 67 |             setBaseUrl(serverUrl);
 68 |             
 69 |             // 提取第一个可用的路径和方法作为示例
 70 |             const paths = openApiDoc?.paths;
 71 |             if (paths && typeof paths === 'object') {
 72 |               const pathEntries = Object.entries(paths);
 73 |               if (pathEntries.length > 0) {
 74 |                 const [firstPath, pathMethods] = pathEntries[0] as [string, any];
 75 |                 if (pathMethods && typeof pathMethods === 'object') {
 76 |                   const methods = Object.keys(pathMethods);
 77 |                   if (methods.length > 0) {
 78 |                     const firstMethod = methods[0].toUpperCase();
 79 |                     setExamplePath(firstPath);
 80 |                     setExampleMethod(firstMethod);
 81 |                   }
 82 |                 }
 83 |               }
 84 |             }
 85 |           } catch (error) {
 86 |             console.error('解析OpenAPI规范失败:', error);
 87 |           }
 88 |         }
 89 |       }
 90 |     } catch (error) {
 91 |       console.error('获取API详情失败:', error);
 92 |       setError('加载失败,请稍后重试');
 93 |     } finally {
 94 |       setLoading(false);
 95 |     }
 96 |   };
 97 | 
 98 | 
 99 | 
100 | 
101 |   if (error) {
102 |     return (
103 |       <Layout loading={loading}>
104 |         <Alert message={error} type="error" showIcon className="my-8" />
105 |       </Layout>
106 |     );
107 |   }
108 | 
109 |   if (!apiData) {
110 |     return (
111 |       <Layout loading={loading}>
112 |         <Alert message="未找到API信息" type="warning" showIcon className="my-8" />
113 |       </Layout>
114 |     );
115 |   }
116 | 
117 |   return (
118 |     <Layout loading={loading}>
119 |       <div className="mb-6">
120 |         <ProductHeader
121 |           name={apiData.name}
122 |           description={apiData.description}
123 |           icon={apiData.icon}
124 |           defaultIcon="/logo.svg"
125 |           updatedAt={apiData.updatedAt}
126 |           productType="REST_API"
127 |         />
128 |         <hr className="border-gray-200 mt-4" />
129 |       </div>
130 | 
131 |       {/* 主要内容区域 - 左右布局 */}
132 |       <Row gutter={24}>
133 |         {/* 左侧内容 */}
134 |         <Col span={15}>
135 |           <Card className="mb-6 rounded-lg border-gray-200">
136 |             <Tabs
137 |               defaultActiveKey="overview"
138 |               items={[
139 |                 {
140 |                   key: "overview",
141 |                   label: "Overview",
142 |                   children: apiData.document ? (
143 |                     <div className="min-h-[400px]">
144 |                       <div 
145 |                         className="prose prose-lg max-w-none"
146 |                         style={{
147 |                           lineHeight: '1.7',
148 |                           color: '#374151',
149 |                           fontSize: '16px',
150 |                           fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
151 |                         }}
152 |                       >
153 |                         <style>{`
154 |                           .prose h1 {
155 |                             color: #111827;
156 |                             font-weight: 700;
157 |                             font-size: 2.25rem;
158 |                             line-height: 1.2;
159 |                             margin-top: 0;
160 |                             margin-bottom: 1.5rem;
161 |                             border-bottom: 2px solid #e5e7eb;
162 |                             padding-bottom: 0.5rem;
163 |                           }
164 |                           .prose h2 {
165 |                             color: #1f2937;
166 |                             font-weight: 600;
167 |                             font-size: 1.875rem;
168 |                             line-height: 1.3;
169 |                             margin-top: 2rem;
170 |                             margin-bottom: 1rem;
171 |                             border-bottom: 1px solid #e5e7eb;
172 |                             padding-bottom: 0.25rem;
173 |                           }
174 |                           .prose h3 {
175 |                             color: #374151;
176 |                             font-weight: 600;
177 |                             font-size: 1.5rem;
178 |                             margin-top: 1.5rem;
179 |                             margin-bottom: 0.75rem;
180 |                           }
181 |                           .prose p {
182 |                             margin-bottom: 1.25rem;
183 |                             color: #4b5563;
184 |                             line-height: 1.7;
185 |                             font-size: 16px;
186 |                           }
187 |                           .prose code {
188 |                             background-color: #f3f4f6;
189 |                             border: 1px solid #e5e7eb;
190 |                             border-radius: 0.375rem;
191 |                             padding: 0.125rem 0.375rem;
192 |                             font-size: 0.875rem;
193 |                             color: #374151;
194 |                             font-weight: 500;
195 |                           }
196 |                           .prose pre {
197 |                             background-color: #1f2937;
198 |                             border-radius: 0.5rem;
199 |                             padding: 1.25rem;
200 |                             overflow-x: auto;
201 |                             margin: 1.5rem 0;
202 |                             border: 1px solid #374151;
203 |                           }
204 |                           .prose pre code {
205 |                             background-color: transparent;
206 |                             border: none;
207 |                             color: #f9fafb;
208 |                             padding: 0;
209 |                             font-size: 0.875rem;
210 |                             font-weight: normal;
211 |                           }
212 |                           .prose blockquote {
213 |                             border-left: 4px solid #3b82f6;
214 |                             padding-left: 1rem;
215 |                             margin: 1.5rem 0;
216 |                             color: #6b7280;
217 |                             font-style: italic;
218 |                             background-color: #f8fafc;
219 |                             padding: 1rem;
220 |                             border-radius: 0.375rem;
221 |                             font-size: 16px;
222 |                           }
223 |                           .prose ul, .prose ol {
224 |                             margin: 1.25rem 0;
225 |                             padding-left: 1.5rem;
226 |                           }
227 |                           .prose ol {
228 |                             list-style-type: decimal;
229 |                             list-style-position: outside;
230 |                           }
231 |                           .prose ul {
232 |                             list-style-type: disc;
233 |                             list-style-position: outside;
234 |                           }
235 |                           .prose li {
236 |                             margin: 0.5rem 0;
237 |                             color: #4b5563;
238 |                             display: list-item;
239 |                             font-size: 16px;
240 |                           }
241 |                           .prose ol li {
242 |                             padding-left: 0.25rem;
243 |                           }
244 |                           .prose ul li {
245 |                             padding-left: 0.25rem;
246 |                           }
247 |                           .prose table {
248 |                             width: 100%;
249 |                             border-collapse: collapse;
250 |                             margin: 1.5rem 0;
251 |                             font-size: 16px;
252 |                           }
253 |                           .prose th, .prose td {
254 |                             border: 1px solid #d1d5db;
255 |                             padding: 0.75rem;
256 |                             text-align: left;
257 |                           }
258 |                           .prose th {
259 |                             background-color: #f9fafb;
260 |                             font-weight: 600;
261 |                             color: #374151;
262 |                             font-size: 16px;
263 |                           }
264 |                           .prose td {
265 |                             color: #4b5563;
266 |                             font-size: 16px;
267 |                           }
268 |                           .prose a {
269 |                             color: #3b82f6;
270 |                             text-decoration: underline;
271 |                             font-weight: 500;
272 |                             transition: color 0.2s;
273 |                             font-size: inherit;
274 |                           }
275 |                           .prose a:hover {
276 |                             color: #1d4ed8;
277 |                           }
278 |                           .prose strong {
279 |                             color: #111827;
280 |                             font-weight: 600;
281 |                             font-size: inherit;
282 |                           }
283 |                           .prose em {
284 |                             color: #6b7280;
285 |                             font-style: italic;
286 |                             font-size: inherit;
287 |                           }
288 |                           .prose hr {
289 |                             border: none;
290 |                             height: 1px;
291 |                             background-color: #e5e7eb;
292 |                             margin: 2rem 0;
293 |                           }
294 |                         `}</style>
295 |                         <ReactMarkdown remarkPlugins={[remarkGfm]}>{apiData.document}</ReactMarkdown>
296 |                       </div>
297 |                     </div>
298 |                   ) : (
299 |                     <div className="text-gray-500 text-center py-8">
300 |                       暂无文档内容
301 |                     </div>
302 |                   ),
303 |                 },
304 |                 {
305 |                   key: "openapi-spec",
306 |                   label: "OpenAPI Specification",
307 |                   children: (
308 |                     <div>
309 |                       {apiData.apiConfig && apiData.apiConfig.spec ? (
310 |                         <SwaggerUIWrapper apiSpec={apiData.apiConfig.spec} />
311 |                       ) : (
312 |                         <div className="text-gray-500 text-center py-8">
313 |                           暂无OpenAPI规范
314 |                         </div>
315 |                       )}
316 |                     </div>
317 |                   ),
318 |                 },
319 |               ]}
320 |             />
321 |           </Card>
322 |         </Col>
323 | 
324 |         {/* 右侧内容 */}
325 |         <Col span={9}>
326 |           <Card 
327 |             className="rounded-lg border-gray-200"
328 |             title={
329 |               <Space>
330 |                 <RocketOutlined />
331 |                 <span>快速开始</span>
332 |               </Space>
333 |             }>
334 |             <Space direction="vertical" className="w-full" size="middle">
335 |               {/* cURL示例 */}
336 |               <div>
337 |                 <Title level={5}>cURL调用示例</Title>
338 |                 <div className="bg-gray-50 p-3 rounded border relative">
339 |                   <pre className="text-sm mb-0">
340 | {`curl -X ${exampleMethod} \\
341 |   '${baseUrl || 'https://api.example.com'}${examplePath}' \\
342 |   -H 'Accept: application/json' \\
343 |   -H 'Content-Type: application/json'`}
344 |                   </pre>
345 |                   <Button 
346 |                     type="text" 
347 |                     size="small"
348 |                     icon={<CopyOutlined />}
349 |                     className="absolute top-2 right-2"
350 |                     onClick={() => {
351 |                       const curlCommand = `curl -X ${exampleMethod} \\\n  '${baseUrl || 'https://api.example.com'}${examplePath}' \\\n  -H 'Accept: application/json' \\\n  -H 'Content-Type: application/json'`;
352 |                       navigator.clipboard.writeText(curlCommand);
353 |                       message.success('cURL命令已复制到剪贴板', 1);
354 |                     }}
355 |                   />
356 |                 </div>
357 |               </div>
358 | 
359 |               <Divider />
360 | 
361 |               {/* 下载OAS文件 */}
362 |               <div>
363 |                 <Title level={5}>OpenAPI规范文件</Title>
364 |                 <Paragraph type="secondary">
365 |                   下载完整的OpenAPI规范文件,用于代码生成、API测试等场景
366 |                 </Paragraph>
367 |                 <Space>
368 |                   <Button 
369 |                     type="primary"
370 |                     icon={<DownloadOutlined />}
371 |                     onClick={() => {
372 |                       if (apiData?.apiConfig?.spec) {
373 |                         const blob = new Blob([apiData.apiConfig.spec], { type: 'text/yaml' });
374 |                         const url = URL.createObjectURL(blob);
375 |                         const link = document.createElement('a');
376 |                         link.href = url;
377 |                         link.download = `${apiData.name || 'api'}-openapi.yaml`;
378 |                         document.body.appendChild(link);
379 |                         link.click();
380 |                         document.body.removeChild(link);
381 |                         URL.revokeObjectURL(url);
382 |                         message.success('OpenAPI规范文件下载成功', 1);
383 |                       }
384 |                     }}
385 |                   >
386 |                     下载YAML
387 |                   </Button>
388 |                   <Button 
389 |                     icon={<DownloadOutlined />}
390 |                     onClick={() => {
391 |                       if (apiData?.apiConfig?.spec) {
392 |                         try {
393 |                           const yamlDoc = yaml.load(apiData.apiConfig.spec);
394 |                           const jsonSpec = JSON.stringify(yamlDoc, null, 2);
395 |                           const blob = new Blob([jsonSpec], { type: 'application/json' });
396 |                           const url = URL.createObjectURL(blob);
397 |                           const link = document.createElement('a');
398 |                           link.href = url;
399 |                           link.download = `${apiData.name || 'api'}-openapi.json`;
400 |                           document.body.appendChild(link);
401 |                           link.click();
402 |                           document.body.removeChild(link);
403 |                           URL.revokeObjectURL(url);
404 |                           message.success('OpenAPI规范文件下载成功', 1);
405 |                         } catch (error) {
406 |                           message.error('转换JSON格式失败');
407 |                         }
408 |                       }
409 |                     }}
410 |                   >
411 |                     下载JSON
412 |                   </Button>
413 |                 </Space>
414 |               </div>
415 |             </Space>
416 |           </Card>
417 |         </Col>
418 |       </Row>
419 | 
420 |     </Layout>
421 |   );
422 | }
423 | 
424 | export default ApiDetailPage; 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/pages/ApiProducts.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { memo, useCallback, useEffect, useState } from 'react';
  2 | import { useNavigate } from 'react-router-dom';
  3 | import type { MenuProps } from 'antd';
  4 | import { Badge, Button, Card, Dropdown, Modal, message, Pagination, Skeleton, Input, Select, Tag, Space } from 'antd';
  5 | import type { ApiProduct, ProductIcon } from '@/types/api-product';
  6 | import { ApiOutlined, MoreOutlined, PlusOutlined, ExclamationCircleOutlined, ExclamationCircleFilled, ClockCircleFilled, CheckCircleFilled, SearchOutlined } from '@ant-design/icons';
  7 | import McpServerIcon from '@/components/icons/McpServerIcon';
  8 | import { apiProductApi } from '@/lib/api';
  9 | import ApiProductFormModal from '@/components/api-product/ApiProductFormModal';
 10 | 
 11 | // 优化的产品卡片组件
 12 | const ProductCard = memo(({ product, onNavigate, handleRefresh, onEdit }: {
 13 |   product: ApiProduct;
 14 |   onNavigate: (productId: string) => void;
 15 |   handleRefresh: () => void;
 16 |   onEdit: (product: ApiProduct) => void;
 17 | }) => {
 18 |   // 处理产品图标的函数
 19 |   const getTypeIcon = (icon: ProductIcon | null | undefined, type: string) => {
 20 |     if (icon) {
 21 |       switch (icon.type) {
 22 |         case "URL":
 23 |           return <img src={icon.value} alt="icon" style={{ borderRadius: '8px', minHeight: '40px', width: '40px', height: '40px', objectFit: 'cover' }} />
 24 |         case "BASE64":
 25 |           // 如果value已经包含data URL前缀,直接使用;否则添加前缀
 26 |           const src = icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`;
 27 |           return <img src={src} alt="icon" style={{ borderRadius: '8px', minHeight: '40px', width: '40px', height: '40px', objectFit: 'cover' }} />
 28 |         default:
 29 |           return type === "REST_API" ? <ApiOutlined style={{ fontSize: '16px', width: '16px', height: '16px' }} /> : <McpServerIcon style={{ fontSize: '16px', width: '16px', height: '16px' }} />
 30 |       }
 31 |     } else {
 32 |       return type === "REST_API" ? <ApiOutlined style={{ fontSize: '16px', width: '16px', height: '16px' }} /> : <McpServerIcon style={{ fontSize: '16px', width: '16px', height: '16px' }} />
 33 |     }
 34 |   }
 35 | 
 36 |   const handleClick = useCallback(() => {
 37 |     onNavigate(product.productId)
 38 |   }, [product.productId, onNavigate]);
 39 | 
 40 |   const handleDelete = useCallback((productId: string, productName: string, e?: React.MouseEvent | any) => {
 41 |     if (e && e.stopPropagation) e.stopPropagation();
 42 |     Modal.confirm({
 43 |       title: '确认删除',
 44 |       icon: <ExclamationCircleOutlined />,
 45 |       content: `确定要删除API产品 "${productName}" 吗?此操作不可恢复。`,
 46 |       okText: '确认删除',
 47 |       okType: 'danger',
 48 |       cancelText: '取消',
 49 |       onOk() {
 50 |         apiProductApi.deleteApiProduct(productId).then(() => {
 51 |           message.success('API Product 删除成功');
 52 |           handleRefresh();
 53 |         });
 54 |       },
 55 |     });
 56 |   }, [handleRefresh]);
 57 | 
 58 |   const handleEdit = useCallback((e?: React.MouseEvent | any) => {
 59 |     if (e && e?.domEvent?.stopPropagation) e.domEvent.stopPropagation();
 60 |     onEdit(product);
 61 |   }, [product, onEdit]);
 62 | 
 63 |   const dropdownItems: MenuProps['items'] = [
 64 |     {
 65 |       key: 'edit',
 66 |       label: '编辑',
 67 |       onClick: handleEdit,
 68 |     },
 69 |     {
 70 |       type: 'divider',
 71 |     },
 72 |     {
 73 |       key: 'delete',
 74 |       label: '删除',
 75 |       danger: true,
 76 |       onClick: (info: any) => handleDelete(product.productId, product.name, info?.domEvent),
 77 |     },
 78 |   ]
 79 | 
 80 |   return (
 81 |     <Card
 82 |       className="hover:shadow-lg transition-shadow cursor-pointer rounded-xl border border-gray-200 shadow-sm hover:border-blue-300"
 83 |       onClick={handleClick}
 84 |       bodyStyle={{ padding: '16px' }}
 85 |     >
 86 |       <div className="flex items-center justify-between mb-4">
 87 |         <div className="flex items-center space-x-3">
 88 |           <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-100">
 89 |             {getTypeIcon(product.icon, product.type)}
 90 |           </div>
 91 |           <div>
 92 |             <h3 className="text-lg font-semibold">{product.name}</h3>
 93 |             <div className="flex items-center gap-3 mt-1 flex-wrap">
 94 |               {product.category && <Badge color="green" text={product.category} />}
 95 |               <div className="flex items-center">
 96 |                 {product.type === "REST_API" ? (
 97 |                   <ApiOutlined className="text-blue-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
 98 |                 ) : (
 99 |                   <McpServerIcon className="text-black mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
100 |                 )}
101 |                 <span className="text-xs text-gray-700">
102 |                   {product.type === "REST_API" ? "REST API" : "MCP Server"}
103 |                 </span>
104 |               </div>
105 |               <div className="flex items-center">
106 |                 {product.status === "PENDING" ? (
107 |                   <ExclamationCircleFilled className="text-yellow-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
108 |                 ) : product.status === "READY" ? (
109 |                   <ClockCircleFilled className="text-blue-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
110 |                 ) : (
111 |                   <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
112 |                 )}
113 |                 <span className="text-xs text-gray-700">
114 |                   {product.status === "PENDING" ? "待配置" : product.status === "READY" ? "待发布" : "已发布"}
115 |                 </span>
116 |               </div>
117 |             </div>
118 |           </div>
119 |         </div>
120 |         <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
121 |           <Button
122 |             type="text"
123 |             icon={<MoreOutlined />}
124 |             onClick={(e) => e.stopPropagation()}
125 |           />
126 |         </Dropdown>
127 |       </div>
128 | 
129 |       <div className="space-y-4">
130 |         {product.description && (
131 |           <p className="text-sm text-gray-600">{product.description}</p>
132 |         )}
133 | 
134 |       </div>
135 |     </Card>
136 |   )
137 | })
138 | 
139 | ProductCard.displayName = 'ProductCard'
140 | 
141 | export default function ApiProducts() {
142 |   const navigate = useNavigate();
143 |   const [apiProducts, setApiProducts] = useState<ApiProduct[]>([]);
144 |   const [filters, setFilters] = useState<{ type?: string, name?: string }>({});
145 |   const [loading, setLoading] = useState(true); // 初始状态为 loading
146 |   const [pagination, setPagination] = useState({
147 |     current: 1,
148 |     pageSize: 12,
149 |     total: 0,
150 |   });
151 | 
152 |   const [modalVisible, setModalVisible] = useState(false);
153 |   const [editingProduct, setEditingProduct] = useState<ApiProduct | null>(null);
154 | 
155 |   // 搜索状态
156 |   const [searchValue, setSearchValue] = useState('');
157 |   const [searchType, setSearchType] = useState<'name' | 'type'>('name');
158 |   const [activeFilters, setActiveFilters] = useState<Array<{ type: string; value: string; label: string }>>([]);
159 | 
160 |   const fetchApiProducts = useCallback((page = 1, size = 12, queryFilters?: { type?: string, name?: string }) => {
161 |     setLoading(true);
162 |     const params = { page, size, ...(queryFilters || {}) };
163 |     apiProductApi.getApiProducts(params).then((res: any) => {
164 |       const products = res.data.content;
165 |       setApiProducts(products);
166 |       setPagination({
167 |         current: page,
168 |         pageSize: size,
169 |         total: res.data.totalElements || 0,
170 |       });
171 |     }).finally(() => {
172 |       setLoading(false);
173 |     });
174 |   }, []); // 不依赖任何状态,避免无限循环
175 | 
176 |   useEffect(() => {
177 |     fetchApiProducts(1, 12);
178 |   }, []); // 只在组件初始化时执行一次
179 | 
180 |   // 产品类型选项
181 |   const typeOptions = [
182 |     { label: 'REST API', value: 'REST_API' },
183 |     { label: 'MCP Server', value: 'MCP_SERVER' },
184 |   ];
185 | 
186 |   // 搜索类型选项
187 |   const searchTypeOptions = [
188 |     { label: '产品名称', value: 'name' as const },
189 |     { label: '产品类型', value: 'type' as const },
190 |   ];
191 | 
192 |   // 搜索处理函数
193 |   const handleSearch = () => {
194 |     if (searchValue.trim()) {
195 |       let labelText = '';
196 |       let filterValue = searchValue.trim();
197 |       
198 |       if (searchType === 'name') {
199 |         labelText = `产品名称:${searchValue.trim()}`;
200 |       } else {
201 |         const typeLabel = typeOptions.find(opt => opt.value === searchValue.trim())?.label || searchValue.trim();
202 |         labelText = `产品类型:${typeLabel}`;
203 |       }
204 |       
205 |       const newFilter = { type: searchType, value: filterValue, label: labelText };
206 |       const updatedFilters = activeFilters.filter(f => f.type !== searchType);
207 |       updatedFilters.push(newFilter);
208 |       setActiveFilters(updatedFilters);
209 |       
210 |       const filters: { type?: string, name?: string } = {};
211 |       updatedFilters.forEach(filter => {
212 |         if (filter.type === 'type' || filter.type === 'name') {
213 |           filters[filter.type] = filter.value;
214 |         }
215 |       });
216 |       
217 |       setFilters(filters);
218 |       fetchApiProducts(1, pagination.pageSize, filters);
219 |       setSearchValue('');
220 |     }
221 |   };
222 | 
223 |   // 移除单个筛选条件
224 |   const removeFilter = (filterType: string) => {
225 |     const updatedFilters = activeFilters.filter(f => f.type !== filterType);
226 |     setActiveFilters(updatedFilters);
227 |     
228 |     const newFilters: { type?: string, name?: string } = {};
229 |     updatedFilters.forEach(filter => {
230 |       if (filter.type === 'type' || filter.type === 'name') {
231 |         newFilters[filter.type] = filter.value;
232 |       }
233 |     });
234 |     
235 |     setFilters(newFilters);
236 |     fetchApiProducts(1, pagination.pageSize, newFilters);
237 |   };
238 | 
239 |   // 清空所有筛选条件
240 |   const clearAllFilters = () => {
241 |     setActiveFilters([]);
242 |     setFilters({});
243 |     fetchApiProducts(1, pagination.pageSize, {});
244 |   };
245 | 
246 |   // 处理分页变化
247 |   const handlePaginationChange = (page: number, pageSize: number) => {
248 |     fetchApiProducts(page, pageSize, filters); // 传递当前filters
249 |   };
250 | 
251 |   // 直接使用服务端返回的列表
252 | 
253 |   // 优化的导航处理函数
254 |   const handleNavigateToProduct = useCallback((productId: string) => {
255 |     navigate(`/api-products/detail?productId=${productId}`);
256 |   }, [navigate]);
257 | 
258 |   // 处理创建
259 |   const handleCreate = () => {
260 |     setEditingProduct(null);
261 |     setModalVisible(true);
262 |   };
263 | 
264 |   // 处理编辑
265 |   const handleEdit = (product: ApiProduct) => {
266 |     setEditingProduct(product);
267 |     setModalVisible(true);
268 |   };
269 | 
270 |   // 处理模态框成功
271 |   const handleModalSuccess = () => {
272 |     setModalVisible(false);
273 |     setEditingProduct(null);
274 |     fetchApiProducts(pagination.current, pagination.pageSize, filters);
275 |   };
276 | 
277 |   // 处理模态框取消
278 |   const handleModalCancel = () => {
279 |     setModalVisible(false);
280 |     setEditingProduct(null);
281 |   };
282 | 
283 |   return (
284 |     <div className="space-y-6">
285 |       <div className="flex items-center justify-between">
286 |         <div>
287 |           <h1 className="text-3xl font-bold tracking-tight">API Products</h1>
288 |           <p className="text-gray-500 mt-2">
289 |             管理和配置您的API产品
290 |           </p>
291 |         </div>
292 |         <Button onClick={handleCreate} type="primary" icon={<PlusOutlined/>}>
293 |           创建 API Product
294 |         </Button>
295 |       </div>
296 | 
297 |       {/* 搜索和筛选 */}
298 |       <div className="space-y-4">
299 |         {/* 搜索框 */}
300 |         <div className="flex items-center max-w-xl">
301 |           {/* 左侧:搜索类型选择器 */}
302 |           <Select
303 |             value={searchType}
304 |             onChange={setSearchType}
305 |             style={{ 
306 |               width: 120,
307 |               borderTopRightRadius: 0,
308 |               borderBottomRightRadius: 0,
309 |               backgroundColor: '#f5f5f5',
310 |             }}
311 |             className="h-10"
312 |             size="large"
313 |           >
314 |             {searchTypeOptions.map(option => (
315 |               <Select.Option key={option.value} value={option.value}>
316 |                 {option.label}
317 |               </Select.Option>
318 |             ))}
319 |           </Select>
320 | 
321 |           {/* 中间:搜索值输入框或选择框 */}
322 |           {searchType === 'type' ? (
323 |             <Select
324 |               placeholder="请选择产品类型"
325 |               value={searchValue}
326 |               onChange={(value) => {
327 |                 setSearchValue(value);
328 |                 // 对于类型选择,立即执行搜索
329 |                 if (value) {
330 |                   const typeLabel = typeOptions.find(opt => opt.value === value)?.label || value;
331 |                   const labelText = `产品类型:${typeLabel}`;
332 |                   const newFilter = { type: 'type', value, label: labelText };
333 |                   const updatedFilters = activeFilters.filter(f => f.type !== 'type');
334 |                   updatedFilters.push(newFilter);
335 |                   setActiveFilters(updatedFilters);
336 |                   
337 |                   const filters: { type?: string, name?: string } = {};
338 |                   updatedFilters.forEach(filter => {
339 |                     if (filter.type === 'type' || filter.type === 'name') {
340 |                       filters[filter.type] = filter.value;
341 |                     }
342 |                   });
343 |                   
344 |                   setFilters(filters);
345 |                   fetchApiProducts(1, pagination.pageSize, filters);
346 |                   setSearchValue('');
347 |                 }
348 |               }}
349 |               style={{ 
350 |                 flex: 1,
351 |                 borderTopLeftRadius: 0,
352 |                 borderBottomLeftRadius: 0,
353 |                 borderTopRightRadius: 0,
354 |                 borderBottomRightRadius: 0,
355 |               }}
356 |               allowClear
357 |               onClear={clearAllFilters}
358 |               className="h-10"
359 |               size="large"
360 |             >
361 |               {typeOptions.map(option => (
362 |                 <Select.Option key={option.value} value={option.value}>
363 |                   {option.label}
364 |                 </Select.Option>
365 |               ))}
366 |             </Select>
367 |           ) : (
368 |             <Input
369 |               placeholder="请输入要检索的产品名称"
370 |               value={searchValue}
371 |               onChange={(e) => setSearchValue(e.target.value)}
372 |               style={{ 
373 |                 flex: 1,
374 |                 borderTopLeftRadius: 0,
375 |                 borderBottomLeftRadius: 0,
376 |                 borderTopRightRadius: 0,
377 |                 borderBottomRightRadius: 0,
378 |               }}
379 |               onPressEnter={handleSearch}
380 |               allowClear
381 |               onClear={() => setSearchValue('')}
382 |               size="large"
383 |               className="h-10"
384 |             />
385 |           )}
386 | 
387 |           {/* 右侧:搜索按钮 */}
388 |           <Button
389 |             icon={<SearchOutlined />}
390 |             onClick={handleSearch}
391 |             style={{
392 |               borderTopLeftRadius: 0,
393 |               borderBottomLeftRadius: 0,
394 |               width: 48,
395 |             }}
396 |             className="h-10"
397 |             size="large"
398 |           />
399 |         </div>
400 | 
401 |         {/* 筛选条件标签 */}
402 |         {activeFilters.length > 0 && (
403 |           <div className="flex items-center gap-2">
404 |             <span className="text-sm text-gray-500">筛选条件:</span>
405 |             <Space wrap>
406 |               {activeFilters.map(filter => (
407 |                 <Tag
408 |                   key={filter.type}
409 |                   closable
410 |                   onClose={() => removeFilter(filter.type)}
411 |                   style={{
412 |                     backgroundColor: '#f5f5f5',
413 |                     border: '1px solid #d9d9d9',
414 |                     borderRadius: '16px',
415 |                     color: '#666',
416 |                     fontSize: '12px',
417 |                     padding: '4px 12px',
418 |                   }}
419 |                 >
420 |                   {filter.label}
421 |                 </Tag>
422 |               ))}
423 |             </Space>
424 |             <Button
425 |               type="link"
426 |               size="small"
427 |               onClick={clearAllFilters}
428 |               className="text-blue-500 hover:text-blue-600 text-sm"
429 |             >
430 |               清除筛选条件
431 |             </Button>
432 |           </div>
433 |         )}
434 |       </div>
435 | 
436 |       {loading ? (
437 |         <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
438 |           {Array.from({ length: pagination.pageSize || 12 }).map((_, index) => (
439 |             <div key={index} className="h-full rounded-lg shadow-lg bg-white p-4">
440 |               <div className="flex items-start space-x-4">
441 |                 <Skeleton.Avatar size={48} active />
442 |                 <div className="flex-1 min-w-0">
443 |                   <div className="flex items-center justify-between mb-2">
444 |                     <Skeleton.Input active size="small" style={{ width: 120 }} />
445 |                     <Skeleton.Input active size="small" style={{ width: 60 }} />
446 |                   </div>
447 |                   <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
448 |                   <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
449 |                   <div className="flex items-center justify-between">
450 |                     <Skeleton.Input active size="small" style={{ width: 60 }} />
451 |                     <Skeleton.Input active size="small" style={{ width: 80 }} />
452 |                   </div>
453 |                 </div>
454 |               </div>
455 |             </div>
456 |           ))}
457 |         </div>
458 |       ) : (
459 |         <>
460 |           <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
461 |             {apiProducts.map((product) => (
462 |               <ProductCard
463 |                 key={product.productId}
464 |                 product={product}
465 |                 onNavigate={handleNavigateToProduct}
466 |                 handleRefresh={() => fetchApiProducts(pagination.current, pagination.pageSize, filters)}
467 |                 onEdit={handleEdit}
468 |               />
469 |             ))}
470 |           </div>
471 | 
472 |           {pagination.total > 0 && (
473 |             <div className="flex justify-center mt-6">
474 |               <Pagination
475 |                 current={pagination.current}
476 |                 pageSize={pagination.pageSize}
477 |                 total={pagination.total}
478 |                 onChange={handlePaginationChange}
479 |                 showSizeChanger
480 |                 showQuickJumper
481 |                 showTotal={(total) => `共 ${total} 条`}
482 |                 pageSizeOptions={['6', '12', '24', '48']}
483 |               />
484 |             </div>
485 |           )}
486 |         </>
487 |       )}
488 | 
489 |       <ApiProductFormModal
490 |         visible={modalVisible}
491 |         onCancel={handleModalCancel}
492 |         onSuccess={handleModalSuccess}
493 |         productId={editingProduct?.productId}
494 |         initialData={editingProduct || undefined}
495 |       />
496 |     </div>
497 |   )
498 | }
499 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/AIGatewayOperator.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.gateway;
 21 | 
 22 | import cn.hutool.core.codec.Base64;
 23 | import cn.hutool.core.map.MapUtil;
 24 | import cn.hutool.core.util.StrUtil;
 25 | import cn.hutool.json.JSONArray;
 26 | import cn.hutool.json.JSONObject;
 27 | import cn.hutool.json.JSONUtil;
 28 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 29 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 30 | import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult;
 31 | import com.alibaba.apiopenplatform.dto.result.*;
 32 | import com.alibaba.apiopenplatform.entity.Gateway;
 33 | import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
 34 | import com.alibaba.apiopenplatform.service.gateway.client.PopGatewayClient;
 35 | import com.alibaba.apiopenplatform.service.gateway.client.SLSClient;
 36 | import com.alibaba.apiopenplatform.support.consumer.APIGAuthConfig;
 37 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 38 | import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
 39 | import com.alibaba.apiopenplatform.support.enums.GatewayType;
 40 | import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
 41 | import com.aliyuncs.http.MethodType;
 42 | import com.aliyun.sdk.gateway.pop.exception.PopClientException;
 43 | import com.aliyun.sdk.service.apig20240327.models.*;
 44 | import com.aliyun.sdk.service.sls20201230.models.*;
 45 | import lombok.extern.slf4j.Slf4j;
 46 | import org.springframework.stereotype.Service;
 47 | 
 48 | import java.util.*;
 49 | import java.util.concurrent.CompletableFuture;
 50 | import java.util.concurrent.ExecutionException;
 51 | import java.util.stream.Collectors;
 52 | 
 53 | @Service
 54 | @Slf4j
 55 | public class AIGatewayOperator extends APIGOperator {
 56 | 
 57 |     @Override
 58 |     public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
 59 |         PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());
 60 | 
 61 |         Map<String , String> queryParams = MapUtil.<String, String>builder()
 62 |                 .put("gatewayId", gateway.getGatewayId())
 63 |                 .put("pageNumber", String.valueOf(page))
 64 |                 .put("pageSize", String.valueOf(size))
 65 |                 .build();
 66 | 
 67 |         return client.execute("/v1/mcp-servers", MethodType.GET, queryParams, data -> {
 68 |             List<APIGMCPServerResult> mcpServers = Optional.ofNullable(data.getJSONArray("items"))
 69 |                     .map(items -> items.stream()
 70 |                             .map(JSONObject.class::cast)
 71 |                             .map(json -> {
 72 |                                 APIGMCPServerResult result = new APIGMCPServerResult();
 73 |                                 result.setMcpServerName(json.getStr("name"));
 74 |                                 result.setMcpServerId(json.getStr("mcpServerId"));
 75 |                                 result.setMcpRouteId(json.getStr("routeId"));
 76 |                                 result.setApiId(json.getStr("apiId"));
 77 |                                 return result;
 78 |                             })
 79 |                             .collect(Collectors.toList()))
 80 |                     .orElse(new ArrayList<>());
 81 | 
 82 |             return PageResult.of(mcpServers, page, size, data.getInt("totalSize"));
 83 |         });
 84 |     }
 85 | 
 86 |     public PageResult<? extends GatewayMCPServerResult> fetchMcpServers_V1(Gateway gateway, int page, int size) {
 87 |         PageResult<APIResult> apiPage = fetchAPIs(gateway, APIGAPIType.MCP, 0, 1);
 88 |         if (apiPage.getTotalElements() == 0) {
 89 |             return PageResult.empty(page, size);
 90 |         }
 91 | 
 92 |         // MCP Server定义在一个API下
 93 |         String apiId = apiPage.getContent().get(0).getApiId();
 94 |         try {
 95 |             PageResult<HttpRoute> routesPage = fetchHttpRoutes(gateway, apiId, page, size);
 96 |             if (routesPage.getTotalElements() == 0) {
 97 |                 return PageResult.empty(page, size);
 98 |             }
 99 | 
100 |             return PageResult.<APIGMCPServerResult>builder().build()
101 |                     .mapFrom(routesPage, route -> {
102 |                         APIGMCPServerResult r = new APIGMCPServerResult().convertFrom(route);
103 |                         r.setApiId(apiId);
104 |                         return r;
105 |                     });
106 |         } catch (Exception e) {
107 |             log.error("Error fetching MCP servers", e);
108 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching MCP servers,Cause:" + e.getMessage());
109 |         }
110 |     }
111 | 
112 |     @Override
113 |     public String fetchMcpConfig(Gateway gateway, Object conf) {
114 |         APIGRefConfig config = (APIGRefConfig) conf;
115 |         PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());
116 |         String mcpServerId = config.getMcpServerId();
117 |         MCPConfigResult mcpConfig = new MCPConfigResult();
118 | 
119 |         return client.execute("/v1/mcp-servers/" + mcpServerId, MethodType.GET, null, data -> {
120 |             mcpConfig.setMcpServerName(data.getStr("name"));
121 | 
122 |             // mcpServer config
123 |             MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
124 |             String path = data.getStr("mcpServerPath");
125 |             String exposedUriPath = data.getStr("exposedUriPath");
126 |             if (StrUtil.isNotBlank(exposedUriPath)) {
127 |                 path += exposedUriPath;
128 |             }
129 |             serverConfig.setPath(path);
130 | 
131 |             JSONArray domains = data.getJSONArray("domainInfos");
132 |             if (domains != null && !domains.isEmpty()) {
133 |                 serverConfig.setDomains(domains.stream()
134 |                         .map(JSONObject.class::cast)
135 |                         .map(json -> MCPConfigResult.Domain.builder()
136 |                                 .domain(json.getStr("name"))
137 |                                 .protocol(Optional.ofNullable(json.getStr("protocol"))
138 |                                         .map(String::toLowerCase)
139 |                                         .orElse(null))
140 |                                 .build())
141 |                         .collect(Collectors.toList()));
142 |             }
143 |             mcpConfig.setMcpServerConfig(serverConfig);
144 | 
145 |             // meta
146 |             MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
147 |             meta.setSource(GatewayType.APIG_AI.name());
148 |             meta.setProtocol(data.getStr("protocol"));
149 |             meta.setCreateFromType(data.getStr("createFromType"));
150 |             mcpConfig.setMeta(meta);
151 | 
152 |             // tools
153 |             String tools = data.getStr("mcpServerConfig");
154 |             if (StrUtil.isNotBlank(tools)) {
155 |                 mcpConfig.setTools(Base64.decodeStr(tools));
156 |             }
157 | 
158 |             return JSONUtil.toJsonStr(mcpConfig);
159 |         });
160 |     }
161 | 
162 |     public String fetchMcpConfig_V1(Gateway gateway, Object conf) {
163 |         APIGRefConfig config = (APIGRefConfig) conf;
164 |         HttpRoute httpRoute = fetchHTTPRoute(gateway, config.getApiId(), config.getMcpRouteId());
165 | 
166 |         MCPConfigResult m = new MCPConfigResult();
167 |         m.setMcpServerName(httpRoute.getName());
168 | 
169 |         // mcpServer config
170 |         MCPConfigResult.MCPServerConfig c = new MCPConfigResult.MCPServerConfig();
171 |         if (httpRoute.getMatch() != null) {
172 |             c.setPath(httpRoute.getMatch().getPath().getValue());
173 |         }
174 |         if (httpRoute.getDomainInfos() != null) {
175 |             c.setDomains(httpRoute.getDomainInfos().stream()
176 |                     .map(domainInfo -> MCPConfigResult.Domain.builder()
177 |                             .domain(domainInfo.getName())
178 |                             .protocol(Optional.ofNullable(domainInfo.getProtocol())
179 |                                     .map(String::toLowerCase)
180 |                                     .orElse(null))
181 |                             .build())
182 |                     .collect(Collectors.toList()));
183 |         }
184 |         m.setMcpServerConfig(c);
185 | 
186 |         // meta
187 |         MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
188 |         meta.setSource(GatewayType.APIG_AI.name());
189 | 
190 |         // tools
191 |         HttpRoute.McpServerInfo mcpServerInfo = httpRoute.getMcpServerInfo();
192 |         boolean fetchTool = true;
193 |         if (mcpServerInfo.getMcpRouteConfig() != null) {
194 |             String protocol = mcpServerInfo.getMcpRouteConfig().getProtocol();
195 |             meta.setCreateFromType(protocol);
196 | 
197 |             // HTTP转MCP需从插件获取tools配置
198 |             fetchTool = StrUtil.equalsIgnoreCase(protocol, "HTTP");
199 |         }
200 | 
201 |         if (fetchTool) {
202 |             String toolSpec = fetchMcpTools(gateway, config.getMcpRouteId());
203 |             if (StrUtil.isNotBlank(toolSpec)) {
204 |                 m.setTools(toolSpec);
205 |                 // 默认为HTTP转MCP
206 |                 if (StrUtil.isBlank(meta.getCreateFromType())) {
207 |                     meta.setCreateFromType("HTTP");
208 |                 }
209 |             }
210 |         }
211 | 
212 |         m.setMeta(meta);
213 |         return JSONUtil.toJsonStr(m);
214 |     }
215 | 
216 |     @Override
217 |     public GatewayType getGatewayType() {
218 |         return GatewayType.APIG_AI;
219 |     }
220 | 
221 |     @Override
222 |     public String getDashboard(Gateway gateway, String type) {
223 |         SLSClient ticketClient = new SLSClient(gateway.getApigConfig(), true);
224 |         String ticket = null;
225 |         try {
226 |             CreateTicketResponse response = ticketClient.execute(c -> {
227 |                 CreateTicketRequest request = CreateTicketRequest.builder().build();
228 |                 try {
229 |                     return c.createTicket(request).get();
230 |                 } catch (InterruptedException | ExecutionException e) {
231 |                     throw new RuntimeException(e);
232 |                 }
233 |             });
234 |             ticket = response.getBody().getTicket();
235 |         } catch (Exception e) {
236 |             log.error("Error fetching API", e);
237 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching createTicket API,Cause:" + e.getMessage());
238 |         }
239 |         SLSClient client = new SLSClient(gateway.getApigConfig(), false);
240 |         String projectName = null;
241 |         try {
242 |             ListProjectResponse response = client.execute(c -> {
243 |                 ListProjectRequest request = ListProjectRequest.builder().projectName("product").build();
244 |                 try {
245 |                     return c.listProject(request).get();
246 |                 } catch (InterruptedException | ExecutionException e) {
247 |                     throw new RuntimeException(e);
248 |                 }
249 |             });
250 |             projectName = response.getBody().getProjects().get(0).getProjectName();
251 |         } catch (Exception e) {
252 |             log.error("Error fetching Project", e);
253 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Project,Cause:" + e.getMessage());
254 |         }
255 |         String region = gateway.getApigConfig().getRegion();
256 |         String gatewayId = gateway.getGatewayId();
257 |         String dashboardId = "";
258 |         String gatewayFilter = "";
259 |         if (type.equals("Portal")) {
260 |             dashboardId = "dashboard-1758009692051-393998";
261 |             gatewayFilter = "";
262 |         } else if (type.equals("MCP")) {
263 |             dashboardId = "dashboard-1757483808537-433375";
264 |             gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
265 |         } else if (type.equals("API")) {
266 |             dashboardId = "dashboard-1756276497392-966932";
267 |             gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
268 |             ;
269 |         }
270 |         String dashboardUrl = String.format("https://sls.console.aliyun.com/lognext/project/%s/dashboard/%s?%s&slsRegion=%s&sls_ticket=%s&isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true", projectName, dashboardId, gatewayFilter, region, ticket);
271 |         log.info("Dashboard URL: {}", dashboardUrl);
272 |         return dashboardUrl;
273 |     }
274 | 
275 |     public String fetchMcpTools(Gateway gateway, String routeId) {
276 |         APIGClient client = getClient(gateway);
277 | 
278 |         try {
279 |             CompletableFuture<ListPluginAttachmentsResponse> f = client.execute(c -> {
280 |                 ListPluginAttachmentsRequest request = ListPluginAttachmentsRequest.builder()
281 |                         .gatewayId(gateway.getGatewayId())
282 |                         .attachResourceId(routeId)
283 |                         .attachResourceType("GatewayRoute")
284 |                         .pageNumber(1)
285 |                         .pageSize(100)
286 |                         .build();
287 | 
288 |                 return c.listPluginAttachments(request);
289 |             });
290 | 
291 |             ListPluginAttachmentsResponse response = f.join();
292 |             if (response.getStatusCode() != 200) {
293 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
294 |             }
295 | 
296 |             for (ListPluginAttachmentsResponseBody.Items item : response.getBody().getData().getItems()) {
297 |                 PluginClassInfo classInfo = item.getPluginClassInfo();
298 | 
299 |                 if (!StrUtil.equalsIgnoreCase(classInfo.getName(), "mcp-server")) {
300 |                     continue;
301 |                 }
302 | 
303 |                 String pluginConfig = item.getPluginConfig();
304 |                 if (StrUtil.isNotBlank(pluginConfig)) {
305 |                     return Base64.decodeStr(pluginConfig);
306 |                 }
307 |             }
308 |         } catch (Exception e) {
309 |             log.error("Error fetching Plugin Attachment", e);
310 |             throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Plugin Attachment,Cause:" + e.getMessage());
311 |         }
312 |         return null;
313 |     }
314 | 
315 |     @Override
316 |     public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
317 |         APIGClient client = getClient(gateway);
318 | 
319 |         APIGRefConfig config = (APIGRefConfig) refConfig;
320 |         // MCP Server 授权
321 |         String mcpRouteId = config.getMcpRouteId();
322 | 
323 |         try {
324 |             // 确认Gateway的EnvId
325 |             String envId = fetchGatewayEnv(gateway);
326 | 
327 |             CreateConsumerAuthorizationRulesRequest.AuthorizationRules rule = CreateConsumerAuthorizationRulesRequest.AuthorizationRules.builder()
328 |                     .consumerId(consumerId)
329 |                     .expireMode("LongTerm")
330 |                     .resourceType("MCP")
331 |                     .resourceIdentifier(CreateConsumerAuthorizationRulesRequest.ResourceIdentifier.builder()
332 |                             .resourceId(mcpRouteId)
333 |                             .environmentId(envId).build())
334 |                     .build();
335 | 
336 |             CompletableFuture<CreateConsumerAuthorizationRulesResponse> f = client.execute(c -> {
337 |                         CreateConsumerAuthorizationRulesRequest request = CreateConsumerAuthorizationRulesRequest.builder()
338 |                                 .authorizationRules(Collections.singletonList(rule))
339 |                                 .build();
340 | 
341 |                         return c.createConsumerAuthorizationRules(request);
342 |                     }
343 |             );
344 |             CreateConsumerAuthorizationRulesResponse response = f.join();
345 |             if (200 != response.getStatusCode()) {
346 |                 throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
347 |             }
348 | 
349 |             APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
350 |                     .authorizationRuleIds(response.getBody().getData().getConsumerAuthorizationRuleIds())
351 |                     .build();
352 |             return ConsumerAuthConfig.builder()
353 |                     .apigAuthConfig(apigAuthConfig)
354 |                     .build();
355 |         } catch (Exception e) {
356 |             Throwable cause = e.getCause();
357 |             if (cause instanceof PopClientException
358 |                     && "Conflict.ConsumerAuthorizationForbidden".equals(((PopClientException) cause).getErrCode())) {
359 |                 return getConsumerAuthorization(gateway, consumerId, mcpRouteId);
360 |             }
361 |             log.error("Error authorizing consumer {} to mcp server {} in AI gateway {}", consumerId, mcpRouteId, gateway.getGatewayId(), e);
362 |             throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to mcp server in AI gateway: " + e.getMessage());
363 |         }
364 |     }
365 | 
366 |     public ConsumerAuthConfig getConsumerAuthorization(Gateway gateway, String consumerId, String resourceId) {
367 |         APIGClient client = getClient(gateway);
368 | 
369 |         CompletableFuture<QueryConsumerAuthorizationRulesResponse> f = client.execute(c -> {
370 |             QueryConsumerAuthorizationRulesRequest request = QueryConsumerAuthorizationRulesRequest.builder()
371 |                     .consumerId(consumerId)
372 |                     .resourceId(resourceId)
373 |                     .resourceType("MCP")
374 |                     .build();
375 | 
376 |             return c.queryConsumerAuthorizationRules(request);
377 |         });
378 |         QueryConsumerAuthorizationRulesResponse response = f.join();
379 | 
380 |         if (200 != response.getStatusCode()) {
381 |             throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
382 |         }
383 | 
384 |         QueryConsumerAuthorizationRulesResponseBody.Items items = response.getBody().getData().getItems().get(0);
385 |         APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
386 |                 .authorizationRuleIds(Collections.singletonList(items.getConsumerAuthorizationRuleId()))
387 |                 .build();
388 | 
389 |         return ConsumerAuthConfig.builder()
390 |                 .apigAuthConfig(apigAuthConfig)
391 |                 .build();
392 |     }
393 | }
394 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/ProductServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.collection.CollUtil;
 23 | import cn.hutool.core.util.StrUtil;
 24 | import cn.hutool.json.JSONUtil;
 25 | import com.alibaba.apiopenplatform.core.constant.Resources;
 26 | import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
 27 | import com.alibaba.apiopenplatform.core.event.ProductDeletingEvent;
 28 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 29 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 30 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 31 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 32 | import com.alibaba.apiopenplatform.dto.params.product.*;
 33 | import com.alibaba.apiopenplatform.dto.result.*;
 34 | import com.alibaba.apiopenplatform.entity.*;
 35 | import com.alibaba.apiopenplatform.repository.*;
 36 | import com.alibaba.apiopenplatform.service.GatewayService;
 37 | import com.alibaba.apiopenplatform.service.PortalService;
 38 | import com.alibaba.apiopenplatform.service.ProductService;
 39 | import com.alibaba.apiopenplatform.service.NacosService;
 40 | import com.alibaba.apiopenplatform.support.enums.ProductStatus;
 41 | import com.alibaba.apiopenplatform.support.enums.ProductType;
 42 | import com.alibaba.apiopenplatform.support.enums.SourceType;
 43 | import com.alibaba.apiopenplatform.support.product.NacosRefConfig;
 44 | import lombok.RequiredArgsConstructor;
 45 | import lombok.extern.slf4j.Slf4j;
 46 | import org.springframework.context.ApplicationEventPublisher;
 47 | import org.springframework.context.event.EventListener;
 48 | import org.springframework.data.domain.Page;
 49 | 
 50 | import javax.persistence.criteria.*;
 51 | import javax.transaction.Transactional;
 52 | 
 53 | import org.springframework.data.domain.Pageable;
 54 | import org.springframework.data.jpa.domain.Specification;
 55 | import org.springframework.scheduling.annotation.Async;
 56 | import org.springframework.stereotype.Service;
 57 | 
 58 | import java.util.*;
 59 | import java.util.stream.Collectors;
 60 | 
 61 | @Service
 62 | @Slf4j
 63 | @RequiredArgsConstructor
 64 | @Transactional
 65 | public class ProductServiceImpl implements ProductService {
 66 | 
 67 |     private final ContextHolder contextHolder;
 68 | 
 69 |     private final PortalService portalService;
 70 | 
 71 |     private final GatewayService gatewayService;
 72 | 
 73 |     private final ProductRepository productRepository;
 74 | 
 75 |     private final ProductRefRepository productRefRepository;
 76 | 
 77 |     private final ProductPublicationRepository publicationRepository;
 78 | 
 79 |     private final SubscriptionRepository subscriptionRepository;
 80 | 
 81 |     private final ConsumerRepository consumerRepository;
 82 | 
 83 |     private final NacosService nacosService;
 84 | 
 85 |     private final ApplicationEventPublisher eventPublisher;
 86 | 
 87 |     @Override
 88 |     public ProductResult createProduct(CreateProductParam param) {
 89 |         productRepository.findByNameAndAdminId(param.getName(), contextHolder.getUser())
 90 |                 .ifPresent(product -> {
 91 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PRODUCT, product.getName()));
 92 |                 });
 93 | 
 94 |         String productId = IdGenerator.genApiProductId();
 95 | 
 96 |         Product product = param.convertTo();
 97 |         product.setProductId(productId);
 98 |         product.setAdminId(contextHolder.getUser());
 99 | 
100 |         // 设置默认的自动审批配置,如果未指定则默认为null(使用平台级别配置)
101 |         if (param.getAutoApprove() != null) {
102 |             product.setAutoApprove(param.getAutoApprove());
103 |         }
104 | 
105 |         productRepository.save(product);
106 | 
107 |         return getProduct(productId);
108 |     }
109 | 
110 |     @Override
111 |     public ProductResult getProduct(String productId) {
112 |         Product product = contextHolder.isAdministrator() ?
113 |                 findProduct(productId) :
114 |                 findPublishedProduct(contextHolder.getPortal(), productId);
115 | 
116 |         ProductResult result = new ProductResult().convertFrom(product);
117 | 
118 |         // 补充Product信息
119 |         fullFillProduct(result);
120 |         return result;
121 |     }
122 | 
123 |     @Override
124 |     public PageResult<ProductResult> listProducts(QueryProductParam param, Pageable pageable) {
125 |         log.info("zhaoh-test-listProducts-start");
126 |         if (contextHolder.isDeveloper()) {
127 |             param.setPortalId(contextHolder.getPortal());
128 |         }
129 | 
130 |         Page<Product> products = productRepository.findAll(buildSpecification(param), pageable);
131 |         return new PageResult<ProductResult>().convertFrom(
132 |                 products, product -> {
133 |                     ProductResult result = new ProductResult().convertFrom(product);
134 |                     fullFillProduct(result);
135 |                     return result;
136 |                 });
137 |     }
138 | 
139 |     @Override
140 |     public ProductResult updateProduct(String productId, UpdateProductParam param) {
141 |         Product product = findProduct(productId);
142 | 
143 |         // 更换API产品类型
144 |         if (param.getType() != null && product.getType() != param.getType()) {
145 |             productRefRepository.findFirstByProductId(productId)
146 |                     .ifPresent(productRef -> {
147 |                         throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已关联API");
148 |                     });
149 |         }
150 |         param.update(product);
151 | 
152 |         // Consumer鉴权配置同步至网关
153 |         Optional.ofNullable(param.getEnableConsumerAuth()).ifPresent(product::setEnableConsumerAuth);
154 | 
155 |         // 更新自动审批配置
156 |         Optional.ofNullable(param.getAutoApprove()).ifPresent(product::setAutoApprove);
157 | 
158 |         productRepository.saveAndFlush(product);
159 |         return getProduct(product.getProductId());
160 |     }
161 | 
162 |     @Override
163 |     public void publishProduct(String productId, String portalId) {
164 |         portalService.existsPortal(portalId);
165 |         if (publicationRepository.findByPortalIdAndProductId(portalId, productId).isPresent()) {
166 |             return;
167 |         }
168 | 
169 |         Product product = findProduct(productId);
170 |         product.setStatus(ProductStatus.PUBLISHED);
171 | 
172 |         // 未关联不允许发布
173 |         if (getProductRef(productId) == null) {
174 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API");
175 |         }
176 | 
177 |         ProductPublication productPublication = new ProductPublication();
178 |         productPublication.setPortalId(portalId);
179 |         productPublication.setProductId(productId);
180 | 
181 |         publicationRepository.save(productPublication);
182 |         productRepository.save(product);
183 |     }
184 | 
185 |     @Override
186 |     public PageResult<ProductPublicationResult> getPublications(String productId, Pageable pageable) {
187 |         Page<ProductPublication> publications = publicationRepository.findByProductId(productId, pageable);
188 | 
189 |         return new PageResult<ProductPublicationResult>().convertFrom(
190 |                 publications, publication -> {
191 |                     ProductPublicationResult publicationResult = new ProductPublicationResult().convertFrom(publication);
192 |                     PortalResult portal;
193 |                     try {
194 |                         portal = portalService.getPortal(publication.getPortalId());
195 |                     } catch (Exception e) {
196 |                         log.error("Failed to get portal: {}", publication.getPortalId(), e);
197 |                         return null;
198 |                     }
199 | 
200 |                     publicationResult.setPortalName(portal.getName());
201 |                     publicationResult.setAutoApproveSubscriptions(portal.getPortalSettingConfig().getAutoApproveSubscriptions());
202 | 
203 |                     return publicationResult;
204 |                 });
205 |     }
206 | 
207 |     @Override
208 |     public void unpublishProduct(String productId, String portalId) {
209 |         portalService.existsPortal(portalId);
210 | 
211 |         publicationRepository.findByPortalIdAndProductId(portalId, productId)
212 |                 .ifPresent(publicationRepository::delete);
213 |     }
214 | 
215 |     @Override
216 |     public void deleteProduct(String productId) {
217 |         Product Product = findProduct(productId);
218 | 
219 |         // 下线后删除
220 |         publicationRepository.deleteByProductId(productId);
221 |         productRepository.delete(Product);
222 | 
223 |         // 异步清理Product资源
224 |         eventPublisher.publishEvent(new ProductDeletingEvent(productId));
225 |     }
226 | 
227 |     /**
228 |      * 查找产品,如果不存在则抛出异常
229 |      */
230 |     private Product findProduct(String productId) {
231 |         return productRepository.findByProductId(productId)
232 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
233 |     }
234 | 
235 |     @Override
236 |     public void addProductRef(String productId, CreateProductRefParam param) {
237 |         Product product = findProduct(productId);
238 | 
239 |         // 是否已存在API引用
240 |         productRefRepository.findByProductId(product.getProductId())
241 |                 .ifPresent(productRef -> {
242 |                     throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已关联API", Resources.PRODUCT, productId));
243 |                 });
244 |         ProductRef productRef = param.convertTo();
245 |         productRef.setProductId(productId);
246 |         syncConfig(product, productRef);
247 | 
248 |         productRepository.save(product);
249 |         productRefRepository.save(productRef);
250 |     }
251 | 
252 |     @Override
253 |     public ProductRefResult getProductRef(String productId) {
254 |         return productRefRepository.findFirstByProductId(productId)
255 |                 .map(productRef -> new ProductRefResult().convertFrom(productRef))
256 |                 .orElse(null);
257 |     }
258 | 
259 |     @Override
260 |     public void deleteProductRef(String productId) {
261 |         Product product = findProduct(productId);
262 |         product.setStatus(ProductStatus.PENDING);
263 | 
264 |         ProductRef productRef = productRefRepository.findFirstByProductId(productId)
265 |                 .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API"));
266 | 
267 |         // 已发布的产品不允许解绑
268 |         if (publicationRepository.existsByProductId(productId)) {
269 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已发布");
270 |         }
271 | 
272 |         productRefRepository.delete(productRef);
273 |         productRepository.save(product);
274 |     }
275 | 
276 |     private void syncConfig(Product product, ProductRef productRef) {
277 |         SourceType sourceType = productRef.getSourceType();
278 | 
279 |         if (sourceType.isGateway()) {
280 |             GatewayResult gateway = gatewayService.getGateway(productRef.getGatewayId());
281 |             Object config = gateway.getGatewayType().isHigress() ? productRef.getHigressRefConfig() : gateway.getGatewayType().isAdpAIGateway() ? productRef.getAdpAIGatewayRefConfig() : productRef.getApigRefConfig();
282 |             if (product.getType() == ProductType.REST_API) {
283 |                 String apiConfig = gatewayService.fetchAPIConfig(gateway.getGatewayId(), config);
284 |                 productRef.setApiConfig(apiConfig);
285 |             } else {
286 |                 String mcpConfig = gatewayService.fetchMcpConfig(gateway.getGatewayId(), config);
287 |                 productRef.setMcpConfig(mcpConfig);
288 |             }
289 |         } else if (sourceType.isNacos()) {
290 |             // 从Nacos获取MCP Server配置
291 |             NacosRefConfig nacosRefConfig = productRef.getNacosRefConfig();
292 |             if (nacosRefConfig != null) {
293 |                 String mcpConfig = nacosService.fetchMcpConfig(productRef.getNacosId(), nacosRefConfig);
294 |                 productRef.setMcpConfig(mcpConfig);
295 |             }
296 |         }
297 |         product.setStatus(ProductStatus.READY);
298 |         productRef.setEnabled(true);
299 |     }
300 | 
301 |     private void fullFillProduct(ProductResult product) {
302 |         productRefRepository.findFirstByProductId(product.getProductId())
303 |                 .ifPresent(productRef -> {
304 |                     product.setEnabled(productRef.getEnabled());
305 |                     if (StrUtil.isNotBlank(productRef.getApiConfig())) {
306 |                         product.setApiConfig(JSONUtil.toBean(productRef.getApiConfig(), APIConfigResult.class));
307 |                     }
308 | 
309 |                     // API Config
310 |                     if (StrUtil.isNotBlank(productRef.getMcpConfig())) {
311 |                         product.setMcpConfig(JSONUtil.toBean(productRef.getMcpConfig(), MCPConfigResult.class));
312 |                     }
313 |                     product.setStatus(ProductStatus.READY);
314 |                 });
315 | 
316 |         if (publicationRepository.existsByProductId(product.getProductId())) {
317 |             product.setStatus(ProductStatus.PUBLISHED);
318 |         }
319 |     }
320 | 
321 |     private Product findPublishedProduct(String portalId, String productId) {
322 |         ProductPublication publication = publicationRepository.findByPortalIdAndProductId(portalId, productId)
323 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
324 | 
325 |         return findProduct(publication.getProductId());
326 |     }
327 | 
328 |     private Specification<Product> buildSpecification(QueryProductParam param) {
329 |         return (root, query, cb) -> {
330 |             List<Predicate> predicates = new ArrayList<>();
331 | 
332 |             if (StrUtil.isNotBlank(param.getPortalId())) {
333 |                 Subquery<String> subquery = query.subquery(String.class);
334 |                 Root<ProductPublication> publicationRoot = subquery.from(ProductPublication.class);
335 |                 subquery.select(publicationRoot.get("productId"))
336 |                         .where(cb.equal(publicationRoot.get("portalId"), param.getPortalId()));
337 |                 predicates.add(root.get("productId").in(subquery));
338 |             }
339 | 
340 |             if (param.getType() != null) {
341 |                 predicates.add(cb.equal(root.get("type"), param.getType()));
342 |             }
343 | 
344 |             if (StrUtil.isNotBlank(param.getCategory())) {
345 |                 predicates.add(cb.equal(root.get("category"), param.getCategory()));
346 |             }
347 | 
348 |             if (param.getStatus() != null) {
349 |                 predicates.add(cb.equal(root.get("status"), param.getStatus()));
350 |             }
351 | 
352 |             if (StrUtil.isNotBlank(param.getName())) {
353 |                 String likePattern = "%" + param.getName() + "%";
354 |                 predicates.add(cb.like(root.get("name"), likePattern));
355 |             }
356 | 
357 |             return cb.and(predicates.toArray(new Predicate[0]));
358 |         };
359 |     }
360 | 
361 |     @EventListener
362 |     @Async("taskExecutor")
363 |     @Override
364 |     public void handlePortalDeletion(PortalDeletingEvent event) {
365 |         String portalId = event.getPortalId();
366 |         try {
367 |             log.info("Starting to cleanup publications for portal {}", portalId);
368 |             publicationRepository.deleteAllByPortalId(portalId);
369 | 
370 |             log.info("Completed cleanup publications for portal {}", portalId);
371 |         } catch (Exception e) {
372 |             log.error("Failed to cleanup developers for portal {}: {}", portalId, e.getMessage());
373 |         }
374 |     }
375 | 
376 |     @Override
377 |     public Map<String, ProductResult> getProducts(List<String> productIds) {
378 |         List<Product> products = productRepository.findByProductIdIn(productIds);
379 |         return products.stream()
380 |                 .collect(Collectors.toMap(Product::getProductId, product -> new ProductResult().convertFrom(product)));
381 |     }
382 | 
383 |     @Override
384 |     public String getProductDashboard(String productId) {
385 |         // 获取产品关联的网关信息
386 |         ProductRef productRef = productRefRepository.findFirstByProductId(productId)
387 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
388 | 
389 |         if (productRef.getGatewayId() == null) {
390 |             throw new BusinessException(ErrorCode.INVALID_REQUEST, "该产品尚未关联网关服务");
391 |         }
392 |         // 基于产品类型选择Dashboard类型
393 |         Product product = findProduct(productId);
394 |         String dashboardType;
395 |         if (product.getType() == ProductType.MCP_SERVER) {
396 |             dashboardType = "MCP";
397 |         } else {
398 |             // REST_API、HTTP_API 统一走 API 面板
399 |             dashboardType = "API";
400 |         }
401 |         // 通过网关服务获取Dashboard URL
402 |         return gatewayService.getDashboard(productRef.getGatewayId(), dashboardType);
403 |     }
404 | 
405 |     @Override
406 |     public PageResult<SubscriptionResult> listProductSubscriptions(String productId, QueryProductSubscriptionParam param, Pageable pageable) {
407 |         existsProduct(productId);
408 |         Page<ProductSubscription> subscriptions = subscriptionRepository.findAll(buildProductSubscriptionSpec(productId, param), pageable);
409 | 
410 |         List<String> consumerIds = subscriptions.getContent().stream()
411 |                 .map(ProductSubscription::getConsumerId)
412 |                 .collect(Collectors.toList());
413 |         if (CollUtil.isEmpty(consumerIds)) {
414 |             return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
415 |         }
416 | 
417 |         Map<String, Consumer> consumers = consumerRepository.findByConsumerIdIn(consumerIds)
418 |                 .stream()
419 |                 .collect(Collectors.toMap(Consumer::getConsumerId, consumer -> consumer));
420 | 
421 |         return new PageResult<SubscriptionResult>().convertFrom(subscriptions, s -> {
422 |             SubscriptionResult r = new SubscriptionResult().convertFrom(s);
423 |             Consumer consumer = consumers.get(r.getConsumerId());
424 |             if (consumer != null) {
425 |                 r.setConsumerName(consumer.getName());
426 |             }
427 |             return r;
428 |         });
429 |     }
430 | 
431 |     @Override
432 |     public void existsProduct(String productId) {
433 |         productRepository.findByProductId(productId)
434 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
435 |     }
436 | 
437 |     private Specification<ProductSubscription> buildProductSubscriptionSpec(String productId, QueryProductSubscriptionParam param) {
438 |         return (root, query, cb) -> {
439 |             List<Predicate> predicates = new ArrayList<>();
440 |             predicates.add(cb.equal(root.get("productId"), productId));
441 | 
442 |             // 如果是开发者,只能查看自己的Consumer订阅
443 |             if (contextHolder.isDeveloper()) {
444 |                 Subquery<String> consumerSubquery = query.subquery(String.class);
445 |                 Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);
446 |                 consumerSubquery.select(consumerRoot.get("consumerId"))
447 |                         .where(cb.equal(consumerRoot.get("developerId"), contextHolder.getUser()));
448 | 
449 |                 predicates.add(root.get("consumerId").in(consumerSubquery));
450 |             }
451 | 
452 |             if (param.getStatus() != null) {
453 |                 predicates.add(cb.equal(root.get("status"), param.getStatus()));
454 |             }
455 | 
456 |             if (StrUtil.isNotBlank(param.getConsumerName())) {
457 |                 Subquery<String> consumerSubquery = query.subquery(String.class);
458 |                 Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);
459 | 
460 |                 consumerSubquery.select(consumerRoot.get("consumerId"))
461 |                         .where(cb.like(
462 |                                 cb.lower(consumerRoot.get("name")),
463 |                                 "%" + param.getConsumerName().toLowerCase() + "%"
464 |                         ));
465 | 
466 |                 predicates.add(root.get("consumerId").in(consumerSubquery));
467 |             }
468 | 
469 |             return cb.and(predicates.toArray(new Predicate[0]));
470 |         };
471 |     }
472 | }
473 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/ProductHeader.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect } from "react";
  2 | import { Typography, Button, Modal, Select, message, Popconfirm, Input, Pagination, Spin } from "antd";
  3 | import { ApiOutlined, CheckCircleFilled, ClockCircleFilled, ExclamationCircleFilled, PlusOutlined } from "@ant-design/icons";
  4 | import { useParams } from "react-router-dom";
  5 | import { getConsumers, subscribeProduct, getProductSubscriptionStatus, unsubscribeProduct, getProductSubscriptions } from "../lib/api";
  6 | import type { Consumer } from "../types/consumer";
  7 | import type { McpConfig, ProductIcon } from "../types";
  8 | 
  9 | const { Title, Paragraph } = Typography;
 10 | const { Search } = Input;
 11 | 
 12 | interface ProductHeaderProps {
 13 |   name: string;
 14 |   description: string;
 15 |   icon?: ProductIcon | null;
 16 |   defaultIcon?: string;
 17 |   mcpConfig?: McpConfig | null;
 18 |   updatedAt?: string;
 19 |   productType?: 'REST_API' | 'MCP_SERVER';
 20 | }
 21 | 
 22 | // 处理产品图标的函数
 23 | const getIconUrl = (icon?: ProductIcon | null, defaultIcon?: string): string => {
 24 |   const fallback = defaultIcon || "/logo.svg";
 25 |   
 26 |   if (!icon) {
 27 |     return fallback;
 28 |   }
 29 |   
 30 |   switch (icon.type) {
 31 |     case "URL":
 32 |       return icon.value || fallback;
 33 |     case "BASE64":
 34 |       // 如果value已经包含data URL前缀,直接使用;否则添加前缀
 35 |       return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
 36 |     default:
 37 |       return fallback;
 38 |   }
 39 | };
 40 | 
 41 | export const ProductHeader: React.FC<ProductHeaderProps> = ({
 42 |   name,
 43 |   description,
 44 |   icon,
 45 |   defaultIcon = "/default-icon.png",
 46 |   mcpConfig,
 47 |   updatedAt,
 48 |   productType,
 49 | }) => {
 50 |   const { id, mcpName } = useParams();
 51 |   const [isManageModalVisible, setIsManageModalVisible] = useState(false);
 52 |   const [isApplyingSubscription, setIsApplyingSubscription] = useState(false);
 53 |   const [selectedConsumerId, setSelectedConsumerId] = useState<string>('');
 54 |   const [consumers, setConsumers] = useState<Consumer[]>([]);
 55 |   
 56 |   // 分页相关state
 57 |   const [currentPage, setCurrentPage] = useState(1);
 58 |   const [pageSize, setPageSize] = useState(5); // 每页显示5个订阅
 59 |   
 60 |   // 分开管理不同的loading状态
 61 |   const [consumersLoading, setConsumersLoading] = useState(false);
 62 |   const [submitLoading, setSubmitLoading] = useState(false);
 63 |   const [imageLoadFailed, setImageLoadFailed] = useState(false);
 64 | 
 65 |   // 订阅状态相关的state
 66 |   const [subscriptionStatus, setSubscriptionStatus] = useState<{
 67 |     hasSubscription: boolean;
 68 |     subscribedConsumers: any[];
 69 |     allConsumers: any[];
 70 |     fullSubscriptionData?: {
 71 |       content: any[];
 72 |       totalElements: number;
 73 |       totalPages: number;
 74 |     };
 75 |   } | null>(null);
 76 |   const [subscriptionLoading, setSubscriptionLoading] = useState(false);
 77 |   
 78 |   // 订阅详情分页数据(用于管理弹窗)
 79 |   const [subscriptionDetails, setSubscriptionDetails] = useState<{
 80 |     content: any[];
 81 |     totalElements: number;
 82 |     totalPages: number;
 83 |   }>({ content: [], totalElements: 0, totalPages: 0 });
 84 |   const [detailsLoading, setDetailsLoading] = useState(false);
 85 |   
 86 |   // 搜索相关state
 87 |   const [searchKeyword, setSearchKeyword] = useState("");
 88 | 
 89 |   // 判断是否应该显示申请订阅按钮
 90 |   const shouldShowSubscribeButton = !mcpConfig || mcpConfig.meta.source !== 'NACOS';
 91 | 
 92 |   // 获取产品ID
 93 |   const productId = id || mcpName || '';
 94 | 
 95 |   // 查询订阅状态
 96 |   const fetchSubscriptionStatus = async () => {
 97 |     if (!productId || !shouldShowSubscribeButton) return;
 98 |     
 99 |     setSubscriptionLoading(true);
100 |     try {
101 |       const status = await getProductSubscriptionStatus(productId);
102 |       setSubscriptionStatus(status);
103 |     } catch (error) {
104 |       console.error('获取订阅状态失败:', error);
105 |     } finally {
106 |       setSubscriptionLoading(false);
107 |     }
108 |   };
109 | 
110 |   // 获取订阅详情(用于管理弹窗)
111 |   const fetchSubscriptionDetails = async (page: number = 1, search: string = ''): Promise<void> => {
112 |     if (!productId) return Promise.resolve();
113 |     
114 |     setDetailsLoading(true);
115 |     try {
116 |       const response = await getProductSubscriptions(productId, {
117 |         consumerName: search.trim() || undefined,
118 |         page: page - 1, // 后端使用0基索引
119 |         size: pageSize
120 |       });
121 |       
122 |       setSubscriptionDetails({
123 |         content: response.data.content || [],
124 |         totalElements: response.data.totalElements || 0,
125 |         totalPages: response.data.totalPages || 0
126 |       });
127 |     } catch (error) {
128 |       console.error('获取订阅详情失败:', error);
129 |       message.error('获取订阅详情失败,请重试');
130 |     } finally {
131 |       setDetailsLoading(false);
132 |     }
133 |   };
134 | 
135 |   useEffect(() => {
136 |     fetchSubscriptionStatus();
137 |   }, [productId, shouldShowSubscribeButton]);
138 | 
139 |   // 获取消费者列表
140 |   const fetchConsumers = async () => {
141 |     try {
142 |       setConsumersLoading(true);
143 |       const response = await getConsumers({}, { page: 1, size: 100 });
144 |       if (response.data) {
145 |         setConsumers(response.data.content || response.data);
146 |       }
147 |     } catch (error) {
148 |       // message.error('获取消费者列表失败');
149 |     } finally {
150 |       setConsumersLoading(false);
151 |     }
152 |   };
153 | 
154 |   // 开始申请订阅流程
155 |   const startApplyingSubscription = () => {
156 |     setIsApplyingSubscription(true);
157 |     setSelectedConsumerId('');
158 |     fetchConsumers();
159 |   };
160 | 
161 |   // 取消申请订阅
162 |   const cancelApplyingSubscription = () => {
163 |     setIsApplyingSubscription(false);
164 |     setSelectedConsumerId('');
165 |   };
166 | 
167 |   // 提交申请订阅
168 |   const handleApplySubscription = async () => {
169 |     if (!selectedConsumerId) {
170 |       message.warning('请选择消费者');
171 |       return;
172 |     }
173 | 
174 |     try {
175 |       setSubmitLoading(true);
176 |       await subscribeProduct(selectedConsumerId, productId);
177 |       message.success('申请提交成功');
178 |       
179 |       // 重置状态
180 |       setIsApplyingSubscription(false);
181 |       setSelectedConsumerId('');
182 |       
183 |       // 重新获取订阅状态和详情数据
184 |       await fetchSubscriptionStatus();
185 |       await fetchSubscriptionDetails(currentPage, '');
186 |     } catch (error) {
187 |       console.error('申请订阅失败:', error);
188 |       message.error('申请提交失败,请重试');
189 |     } finally {
190 |       setSubmitLoading(false);
191 |     }
192 |   };
193 | 
194 |   // 显示管理弹窗
195 |   const showManageModal = () => {
196 |     setIsManageModalVisible(true);
197 |     
198 |     // 优先使用已缓存的数据,避免重复查询
199 |     if (subscriptionStatus?.fullSubscriptionData) {
200 |       setSubscriptionDetails({
201 |         content: subscriptionStatus.fullSubscriptionData.content,
202 |         totalElements: subscriptionStatus.fullSubscriptionData.totalElements,
203 |         totalPages: subscriptionStatus.fullSubscriptionData.totalPages
204 |       });
205 |       // 重置分页到第一页
206 |       setCurrentPage(1);
207 |       setSearchKeyword('');
208 |     } else {
209 |       // 如果没有缓存数据,则重新获取
210 |       fetchSubscriptionDetails(1, '');
211 |     }
212 |   };
213 | 
214 |   // 处理搜索输入变化
215 |   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
216 |     const value = e.target.value;
217 |     setSearchKeyword(value);
218 |     // 只更新状态,不触发搜索
219 |   };
220 | 
221 |   // 执行搜索
222 |   const handleSearch = (value?: string) => {
223 |     // 如果传入了value参数,使用该参数;否则使用当前的searchKeyword
224 |     const keyword = value !== undefined ? value : searchKeyword;
225 |     const trimmedKeyword = keyword.trim();
226 |     setCurrentPage(1);
227 |     
228 |     // 总是调用API进行搜索,不使用缓存
229 |     fetchSubscriptionDetails(1, trimmedKeyword);
230 |   };
231 | 
232 |   // 处理回车键搜索
233 |   const handleSearchKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
234 |     if (e.key === 'Enter') {
235 |       handleSearch();
236 |     }
237 |   };
238 | 
239 | 
240 |   // 隐藏管理弹窗
241 |   const handleManageCancel = () => {
242 |     setIsManageModalVisible(false);
243 |     // 重置申请订阅状态
244 |     setIsApplyingSubscription(false);
245 |     setSelectedConsumerId('');
246 |     // 重置分页和搜索
247 |     setCurrentPage(1);
248 |     setSearchKeyword('');
249 |     // 清空订阅详情数据
250 |     setSubscriptionDetails({ content: [], totalElements: 0, totalPages: 0 });
251 |   };
252 | 
253 |   // 取消订阅
254 |   const handleUnsubscribe = async (consumerId: string) => {
255 |     try {
256 |       await unsubscribeProduct(consumerId, productId);
257 |       message.success('取消订阅成功');
258 |       
259 |       // 重新获取订阅状态和详情数据
260 |       await fetchSubscriptionStatus();
261 |       await fetchSubscriptionDetails(currentPage, '');
262 |     } catch (error) {
263 |       console.error('取消订阅失败:', error);
264 |       message.error('取消订阅失败,请重试');
265 |     }
266 |   };
267 | 
268 |   return (
269 |     <>
270 |       <div className="mb-2">
271 |         {/* 第一行:图标和标题信息 */}
272 |         <div className="flex items-center gap-4 mb-3">
273 |           {(!icon || imageLoadFailed) && productType === 'REST_API' ? (
274 |             <div className="w-16 h-16 rounded-xl flex-shrink-0 flex items-center justify-center bg-gray-50 border border-gray-200">
275 |               <ApiOutlined className="text-3xl text-black" />
276 |             </div>
277 |           ) : (
278 |             <img
279 |               src={getIconUrl(icon, defaultIcon)}
280 |               alt="icon"
281 |               className="w-16 h-16 rounded-xl object-cover border border-gray-200 flex-shrink-0"
282 |               onError={(e) => {
283 |                 const target = e.target as HTMLImageElement;
284 |                 if (productType === 'REST_API') {
285 |                   setImageLoadFailed(true);
286 |                 } else {
287 |                   // 确保有一个最终的fallback图片,避免无限循环请求
288 |                   const fallbackIcon = defaultIcon || "/logo.svg";
289 |                   const currentUrl = new URL(target.src, window.location.href).href;
290 |                   const fallbackUrl = new URL(fallbackIcon, window.location.href).href;
291 |                   if (currentUrl !== fallbackUrl) {
292 |                     target.src = fallbackIcon;
293 |                   }
294 |                 }
295 |               }}
296 |             />
297 |           )}
298 |           <div className="flex-1 min-w-0 flex flex-col justify-center">
299 |             <Title level={3} className="mb-1 text-xl font-semibold">
300 |               {name}
301 |             </Title>
302 |             {updatedAt && (
303 |               <div className="text-sm text-gray-400">
304 |                 {new Date(updatedAt).toLocaleDateString('zh-CN', {
305 |                   year: 'numeric',
306 |                   month: '2-digit',
307 |                   day: '2-digit'
308 |                 }).replace(/\//g, '.')} updated
309 |               </div>
310 |             )}
311 |           </div>
312 |         </div>
313 |         
314 |         {/* 第二行:描述信息,与左边框对齐 */}
315 |         <Paragraph className="text-gray-600 mb-3 text-sm leading-relaxed">
316 |           {description}
317 |         </Paragraph>
318 |         
319 |         {/* 第三行:徽章式订阅状态 + 管理按钮,与左边框对齐 */}
320 |         {shouldShowSubscribeButton && (
321 |           <div className="flex items-center gap-4">
322 |             {subscriptionLoading ? (
323 |               <Button loading>加载中...</Button>
324 |             ) : (
325 |               <>
326 |                 {/* 订阅状态徽章 */}
327 |                 <div className="flex items-center">
328 |                   {subscriptionStatus?.hasSubscription ? (
329 |                     <>
330 |                       <div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
331 |                       <span className="text-sm text-gray-600 font-medium">已订阅</span>
332 |                     </>
333 |                   ) : (
334 |                     <>
335 |                       <div className="w-2 h-2 bg-gray-400 rounded-full mr-2"></div>
336 |                       <span className="text-sm text-gray-600">未订阅</span>
337 |                     </>
338 |                   )}
339 |                 </div>
340 |                 
341 |                 {/* 管理按钮 */}
342 |                 <Button 
343 |                   type="primary" 
344 |                   onClick={showManageModal}
345 |                 >
346 |                   管理订阅
347 |                 </Button>
348 |               </>
349 |             )}
350 |           </div>
351 |         )}
352 |       </div>
353 | 
354 | 
355 |       {/* 订阅管理弹窗 */}
356 |       <Modal
357 |         title="订阅管理"
358 |         open={isManageModalVisible}
359 |         onCancel={handleManageCancel}
360 |         footer={null}
361 |         width={600}
362 |         styles={{
363 |           content: {
364 |             borderRadius: '8px',
365 |             padding: 0
366 |           },
367 |           header: {
368 |             borderRadius: '8px 8px 0 0',
369 |             marginBottom: 0,
370 |             paddingBottom: '8px'
371 |           },
372 |           body: {
373 |             padding: '0px'
374 |           }
375 |         }}
376 |       >
377 |         <div className="px-6 py-4">
378 |           {/* 产品名称标识 - 容器框样式 */}
379 |           <div className="mb-4">
380 |             <div className="bg-blue-50 border border-blue-200 rounded px-3 py-2">
381 |               <span className="text-sm text-gray-600 mr-2">产品名称:</span>
382 |               <span className="text-sm text-gray-600">{name}</span>
383 |             </div>
384 |           </div>
385 |           
386 |           {/* 搜索框 */}
387 |           <div className="mb-4">
388 |             <Search
389 |               placeholder="搜索消费者名称"
390 |               value={searchKeyword}
391 |               onChange={handleSearchChange}
392 |               onSearch={handleSearch}
393 |               onPressEnter={handleSearchKeyPress}
394 |               allowClear
395 |               style={{ width: 250 }}
396 |             />
397 |           </div>
398 |           
399 |           {/* 优化的表格式 - 无表头,内嵌分页 */}
400 |           <div className="border border-gray-200 rounded overflow-hidden">
401 |             {detailsLoading ? (
402 |               <div className="p-8 text-center">
403 |                 <Spin size="large" />
404 |               </div>
405 |             ) : subscriptionDetails.content && subscriptionDetails.content.length > 0 ? (
406 |               <>
407 |                 {/* 表格内容 */}
408 |                 <div className="divide-y divide-gray-100">
409 |                   {(searchKeyword.trim() 
410 |                     ? subscriptionDetails.content 
411 |                     : subscriptionDetails.content.slice((currentPage - 1) * pageSize, currentPage * pageSize)
412 |                   ).map((item) => (
413 |                       <div key={item.consumerId} className="flex items-center px-4 py-3 hover:bg-gray-50">
414 |                         {/* 消费者名称 - 40% */}
415 |                         <div className="flex-1 min-w-0 pr-4">
416 |                           <span className="text-sm text-gray-700 truncate block">
417 |                             {item.consumerName}
418 |                           </span>
419 |                         </div>
420 |                         {/* 状态 - 30% */}
421 |                         <div className="w-24 flex items-center pr-4">
422 |                           {item.status === 'APPROVED' ? (
423 |                             <>
424 |                               <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
425 |                               <span className="text-xs text-gray-700">已通过</span>
426 |                             </>
427 |                           ) : item.status === 'PENDING' ? (
428 |                             <>
429 |                               <ClockCircleFilled className="text-blue-500 mr-1" style={{fontSize: '10px'}} />
430 |                               <span className="text-xs text-gray-700">审核中</span>
431 |                             </>
432 |                           ) : (
433 |                             <>
434 |                               <ExclamationCircleFilled className="text-red-500 mr-1" style={{fontSize: '10px'}} />
435 |                               <span className="text-xs text-gray-700">已拒绝</span>
436 |                             </>
437 |                           )}
438 |                         </div>
439 |                         
440 |                         {/* 操作 - 30% */}
441 |                         <div className="w-20">
442 |                           <Popconfirm
443 |                             title="确定要取消订阅吗?"
444 |                             onConfirm={() => handleUnsubscribe(item.consumerId)}
445 |                             okText="确认"
446 |                             cancelText="取消"
447 |                           >
448 |                             <Button type="link" danger size="small" className="p-0">
449 |                               取消订阅
450 |                             </Button>
451 |                           </Popconfirm>
452 |                         </div>
453 |                       </div>
454 |                     ))}
455 |                 </div>
456 |               </>
457 |             ) : (
458 |               <div className="p-8 text-center text-gray-500">
459 |                 {searchKeyword ? '未找到匹配的订阅记录' : '暂无订阅记录'}
460 |               </div>
461 |             )}
462 |           </div>
463 |           
464 |           {/* 分页 - 使用Ant Design分页组件,右对齐 */}
465 |           {subscriptionDetails.totalElements > 0 && (
466 |             <div className="mt-3 flex justify-end">
467 |               <Pagination
468 |                 current={currentPage}
469 |                 total={subscriptionDetails.totalElements}
470 |                 pageSize={pageSize}
471 |                 size="small"
472 |                 showSizeChanger={true}
473 |                 showQuickJumper={true}
474 |                 onChange={(page, size) => {
475 |                   setCurrentPage(page);
476 |                   if (size !== pageSize) {
477 |                     setPageSize(size);
478 |                   }
479 |                   
480 |                   // 如果有搜索关键词,需要重新查询;否则使用缓存数据
481 |                   if (searchKeyword.trim()) {
482 |                     fetchSubscriptionDetails(page, searchKeyword);
483 |                   }
484 |                   // 无搜索时不需要重新查询,Ant Design会自动处理前端分页
485 |                 }}
486 |                 onShowSizeChange={(_current, size) => {
487 |                   setPageSize(size);
488 |                   setCurrentPage(1);
489 |                   
490 |                   // 如果有搜索关键词,需要重新查询;否则使用缓存数据
491 |                   if (searchKeyword.trim()) {
492 |                     fetchSubscriptionDetails(1, searchKeyword);
493 |                   }
494 |                   // 无搜索时不需要重新查询,页面大小变化会自动重新渲染
495 |                 }}
496 |                 showTotal={(total) => `共 ${total} 条`}
497 |                 pageSizeOptions={['5', '10', '20']}
498 |                 hideOnSinglePage={false}
499 |               />
500 |             </div>
501 |           )}
502 |           
503 |           {/* 申请订阅区域 - 移回底部 */}
504 |           <div className={`border-t pt-3 ${subscriptionDetails.totalElements > 0 ? 'mt-4' : 'mt-2'}`}>
505 |             <div className="flex justify-end">
506 |               {!isApplyingSubscription ? (
507 |                 <Button 
508 |                   type="primary" 
509 |                   icon={<PlusOutlined />}
510 |                   onClick={startApplyingSubscription}
511 |                 >
512 |                   订阅
513 |                 </Button>
514 |               ) : (
515 |                 <div className="w-full">
516 |                   <div className="bg-gray-50 p-4 rounded">
517 |                     <div className="mb-4">
518 |                       <label className="block text-sm font-medium text-gray-700 mb-2">
519 |                         选择消费者
520 |                       </label>
521 |                       <Select
522 |                         placeholder="搜索或选择消费者"
523 |                         style={{ width: '100%' }}
524 |                         value={selectedConsumerId}
525 |                         onChange={setSelectedConsumerId}
526 |                         showSearch
527 |                         loading={consumersLoading}
528 |                         filterOption={(input, option) =>
529 |                           (option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
530 |                         }
531 |                         notFoundContent={consumersLoading ? '加载中...' : '暂无消费者数据'}
532 |                       >
533 |                         {consumers
534 |                           .filter(consumer => {
535 |                             // 过滤掉已经订阅的consumer
536 |                             const isAlreadySubscribed = subscriptionStatus?.subscribedConsumers?.some(
537 |                               item => item.consumer.consumerId === consumer.consumerId
538 |                             );
539 |                             return !isAlreadySubscribed;
540 |                           })
541 |                           .map(consumer => (
542 |                             <Select.Option key={consumer.consumerId} value={consumer.consumerId}>
543 |                               {consumer.name}
544 |                             </Select.Option>
545 |                           ))
546 |                         }
547 |                       </Select>
548 |                     </div>
549 |                     <div className="flex justify-end gap-2">
550 |                       <Button onClick={cancelApplyingSubscription}>
551 |                         取消
552 |                       </Button>
553 |                       <Button 
554 |                         type="primary"
555 |                         loading={submitLoading}
556 |                         disabled={!selectedConsumerId}
557 |                         onClick={handleApplySubscription}
558 |                       >
559 |                         确认申请
560 |                       </Button>
561 |                     </div>
562 |                   </div>
563 |                 </div>
564 |               )}
565 |             </div>
566 |           </div>
567 |         </div>
568 |       </Modal>
569 |     </>
570 |   );
571 | }; 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductApiDocs.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Card, Tag, Tabs, Table, Collapse, Descriptions } from "antd";
  2 | import { useEffect, useMemo, useState } from "react";
  3 | import type { ApiProduct } from "@/types/api-product";
  4 | import MonacoEditor from "react-monaco-editor";
  5 | import * as yaml from "js-yaml";
  6 | import { ProductTypeMap } from "@/lib/utils";
  7 | 
  8 | // 来源类型映射
  9 | const FromTypeMap: Record<string, string> = {
 10 |   HTTP: "HTTP转MCP",
 11 |   MCP: "MCP直接代理",
 12 |   OPEN_API: "OpenAPI转MCP",
 13 |   DIRECT_ROUTE: "直接路由",
 14 |   DATABASE: "数据库",
 15 | };
 16 | 
 17 | // 来源映射
 18 | const SourceMap: Record<string, string> = {
 19 |   APIG_AI: "AI网关",
 20 |   HIGRESS: "Higress",
 21 |   NACOS: "Nacos",
 22 |   APIG_API: "API网关"
 23 | };
 24 | 
 25 | interface ApiProductApiDocsProps {
 26 |   apiProduct: ApiProduct;
 27 |   handleRefresh: () => void;
 28 | }
 29 | 
 30 | export function ApiProductApiDocs({ apiProduct }: ApiProductApiDocsProps) {
 31 |   const [content, setContent] = useState("");
 32 | 
 33 |   // OpenAPI 端点
 34 |   const [endpoints, setEndpoints] = useState<
 35 |     Array<{
 36 |       key: string;
 37 |       method: string;
 38 |       path: string;
 39 |       description: string;
 40 |       operationId?: string;
 41 |     }>
 42 |   >([]);
 43 | 
 44 |   // MCP 配置解析结果
 45 |   const [mcpParsed, setMcpParsed] = useState<{
 46 |     server?: { name?: string; config?: Record<string, unknown> };
 47 |     tools?: Array<{
 48 |       name: string;
 49 |       description?: string;
 50 |       args?: Array<{
 51 |         name: string;
 52 |         description?: string;
 53 |         type?: string;
 54 |         required?: boolean;
 55 |         position?: string;
 56 |         defaultValue?: string | number | boolean | null;
 57 |         enumValues?: Array<string> | null;
 58 |       }>;
 59 |     }>;
 60 |     allowTools?: Array<string>;
 61 |   }>({});
 62 | 
 63 |   // MCP 连接配置JSON
 64 |   const [httpJson, setHttpJson] = useState("");
 65 |   const [sseJson, setSseJson] = useState("");
 66 |   const [localJson, setLocalJson] = useState("");
 67 | 
 68 |   // 生成连接配置JSON
 69 |   const generateConnectionConfig = (
 70 |     domains: Array<{ domain: string; protocol: string }> | null | undefined,
 71 |     path: string | null | undefined,
 72 |     serverName: string,
 73 |     localConfig?: unknown,
 74 |     protocolType?: string
 75 |   ) => {
 76 |     // 互斥:优先判断本地模式
 77 |     if (localConfig) {
 78 |       const localConfigJson = JSON.stringify(localConfig, null, 2);
 79 |       setLocalJson(localConfigJson);
 80 |       setHttpJson("");
 81 |       setSseJson("");
 82 |       return;
 83 |     }
 84 | 
 85 |     // HTTP/SSE 模式
 86 |     if (domains && domains.length > 0 && path) {
 87 |       const domain = domains[0];
 88 |       const baseUrl = `${domain.protocol}://${domain.domain}`;
 89 |       const endpoint = `${baseUrl}${path}`;
 90 | 
 91 |       if (protocolType === 'SSE') {
 92 |         // 仅生成SSE配置,不追加/sse
 93 |         const sseConfig = `{
 94 |   "mcpServers": {
 95 |     "${serverName}": {
 96 |       "type": "sse",
 97 |       "url": "${endpoint}"
 98 |     }
 99 |   }
100 | }`;
101 |         setSseJson(sseConfig);
102 |         setHttpJson("");
103 |         setLocalJson("");
104 |         return;
105 |       } else if (protocolType === 'StreamableHTTP') {
106 |         // 仅生成HTTP配置
107 |         const httpConfig = `{
108 |   "mcpServers": {
109 |     "${serverName}": {
110 |       "url": "${endpoint}"
111 |     }
112 |   }
113 | }`;
114 |         setHttpJson(httpConfig);
115 |         setSseJson("");
116 |         setLocalJson("");
117 |         return;
118 |       } else {
119 |         // protocol为null或其他值:生成两种配置
120 |         const httpConfig = `{
121 |   "mcpServers": {
122 |     "${serverName}": {
123 |       "url": "${endpoint}"
124 |     }
125 |   }
126 | }`;
127 | 
128 |         const sseConfig = `{
129 |   "mcpServers": {
130 |     "${serverName}": {
131 |       "type": "sse",
132 |       "url": "${endpoint}/sse"
133 |     }
134 |   }
135 | }`;
136 | 
137 |         setHttpJson(httpConfig);
138 |         setSseJson(sseConfig);
139 |         setLocalJson("");
140 |         return;
141 |       }
142 |     }
143 | 
144 |     // 无有效配置
145 |     setHttpJson("");
146 |     setSseJson("");
147 |     setLocalJson("");
148 |   };
149 | 
150 | 
151 |   useEffect(() => {
152 |     // 设置源码内容
153 |     if (apiProduct.apiConfig?.spec) {
154 |       setContent(apiProduct.apiConfig.spec);
155 |     } else if (apiProduct.mcpConfig?.tools) {
156 |       setContent(apiProduct.mcpConfig.tools);
157 |     } else {
158 |       setContent("");
159 |     }
160 | 
161 |     // 解析 OpenAPI(如有)
162 |     if (apiProduct.apiConfig?.spec) {
163 |       const spec = apiProduct.apiConfig.spec;
164 |       try {
165 |         const list: Array<{
166 |           key: string;
167 |           method: string;
168 |           path: string;
169 |           description: string;
170 |           operationId?: string;
171 |         }> = [];
172 | 
173 |         const lines = spec.split("\n");
174 |         let currentPath = "";
175 |         let inPaths = false;
176 | 
177 |         for (let i = 0; i < lines.length; i++) {
178 |           const line = lines[i];
179 |           const trimmedLine = line.trim();
180 |           const indentLevel = line.length - line.trimStart().length;
181 | 
182 |           if (trimmedLine === "paths:" || trimmedLine.startsWith("paths:")) {
183 |             inPaths = true;
184 |             continue;
185 |           }
186 |           if (!inPaths) continue;
187 | 
188 |           if (
189 |             inPaths &&
190 |             indentLevel === 2 &&
191 |             trimmedLine.startsWith("/") &&
192 |             trimmedLine.endsWith(":")
193 |           ) {
194 |             currentPath = trimmedLine.slice(0, -1);
195 |             continue;
196 |           }
197 | 
198 |           if (inPaths && indentLevel === 4) {
199 |             const httpMethods = [
200 |               "get:",
201 |               "post:",
202 |               "put:",
203 |               "delete:",
204 |               "patch:",
205 |               "head:",
206 |               "options:",
207 |             ];
208 |             for (const method of httpMethods) {
209 |               if (trimmedLine.startsWith(method)) {
210 |                 const methodName = method.replace(":", "").toUpperCase();
211 |                 const operationId = extractOperationId(lines, i);
212 |                 list.push({
213 |                   key: `${methodName}-${currentPath}`,
214 |                   method: methodName,
215 |                   path: currentPath,
216 |                   description: operationId || `${methodName} ${currentPath}`,
217 |                   operationId,
218 |                 });
219 |                 break;
220 |               }
221 |             }
222 |           }
223 |         }
224 | 
225 |         setEndpoints(list.length > 0 ? list : []);
226 |       } catch {
227 |         setEndpoints([]);
228 |       }
229 |     } else {
230 |       setEndpoints([]);
231 |     }
232 | 
233 |     // 解析 MCP YAML(如有)
234 |     if (apiProduct.mcpConfig?.tools) {
235 |       try {
236 |         const doc = yaml.load(apiProduct.mcpConfig.tools) as any;
237 |         const toolsRaw = Array.isArray(doc?.tools) ? doc.tools : [];
238 |         const tools = toolsRaw.map((t: any) => ({
239 |           name: String(t?.name ?? ""),
240 |           description: t?.description ? String(t.description) : undefined,
241 |           args: Array.isArray(t?.args)
242 |             ? t.args.map((a: any) => ({
243 |                 name: String(a?.name ?? ""),
244 |                 description: a?.description ? String(a.description) : undefined,
245 |                 type: a?.type ? String(a.type) : undefined,
246 |                 required: Boolean(a?.required),
247 |                 position: a?.position ? String(a.position) : undefined,
248 |                 defaultValue: a?.defaultValue ?? a?.default ?? null,
249 |                 enumValues: a?.enumValues ?? a?.enum ?? null,
250 |               }))
251 |             : undefined,
252 |         }));
253 | 
254 |         setMcpParsed({
255 |           server: doc?.server,
256 |           tools,
257 |           allowTools: Array.isArray(doc?.allowTools)
258 |             ? doc.allowTools
259 |             : undefined,
260 |         });
261 | 
262 |         // 生成连接配置JSON test
263 |         generateConnectionConfig(
264 |           apiProduct.mcpConfig.mcpServerConfig?.domains,
265 |           apiProduct.mcpConfig.mcpServerConfig?.path,
266 |           apiProduct.mcpConfig.mcpServerName,
267 |           apiProduct.mcpConfig.mcpServerConfig?.rawConfig,
268 |           apiProduct.mcpConfig.meta?.protocol
269 |         );
270 |       } catch {
271 |         setMcpParsed({});
272 |       }
273 |     } else {
274 |       setMcpParsed({});
275 |     }
276 |   }, [apiProduct]);
277 | 
278 |   const isOpenApi = useMemo(
279 |     () => Boolean(apiProduct.apiConfig?.spec),
280 |     [apiProduct]
281 |   );
282 |   const isMcp = useMemo(
283 |     () => Boolean(apiProduct.mcpConfig?.tools),
284 |     [apiProduct]
285 |   );
286 | 
287 |   const openApiColumns = useMemo(
288 |     () => [
289 |       {
290 |         title: "方法",
291 |         dataIndex: "method",
292 |         key: "method",
293 |         width: 100,
294 |         render: (method: string) => (
295 |           <span>
296 |             <Tag
297 |               color={
298 |                 method === "GET"
299 |                   ? "green"
300 |                   : method === "POST"
301 |                   ? "blue"
302 |                   : method === "PUT"
303 |                   ? "orange"
304 |                   : method === "DELETE"
305 |                   ? "red"
306 |                   : "default"
307 |               }
308 |             >
309 |               {method}
310 |             </Tag>
311 |           </span>
312 |         ),
313 |       },
314 |       {
315 |         title: "路径",
316 |         dataIndex: "path",
317 |         key: "path",
318 |         width: 260,
319 |         render: (path: string) => (
320 |           <code className="text-sm bg-gray-100 px-2 py-1 rounded">{path}</code>
321 |         ),
322 |       },
323 |     ],
324 |     []
325 |   );
326 | 
327 |   function extractOperationId(lines: string[], startIndex: number): string {
328 |     const currentIndent =
329 |       lines[startIndex].length - lines[startIndex].trimStart().length;
330 |     for (
331 |       let i = startIndex + 1;
332 |       i < Math.min(startIndex + 20, lines.length);
333 |       i++
334 |     ) {
335 |       const line = lines[i];
336 |       const trimmedLine = line.trim();
337 |       const lineIndent = line.length - line.trimStart().length;
338 |       if (lineIndent <= currentIndent && trimmedLine !== "") break;
339 |       if (trimmedLine.startsWith("operationId:")) {
340 |         return trimmedLine.replace("operationId:", "").trim();
341 |       }
342 |     }
343 |     return "";
344 |   }
345 | 
346 |   return (
347 |     <div className="p-6 space-y-6">
348 |       <div className="flex justify-between items-center">
349 |         <div>
350 |           <h1 className="text-2xl font-bold mb-2">API配置</h1>
351 |           <p className="text-gray-600">查看API定义和规范</p>
352 |         </div>
353 |       </div>
354 | 
355 |       <Tabs
356 |         defaultActiveKey="overview"
357 |         items={[
358 |           {
359 |             key: "overview",
360 |             label: "API配置",
361 |             children: (
362 |               <div className="space-y-4">
363 |                 {isOpenApi && (
364 |                   <>
365 |                     <Descriptions
366 |                       column={2}
367 |                       bordered
368 |                       size="small"
369 |                       className="mb-4"
370 |                     >
371 |                       {/* 'APIG_API' | 'HIGRESS' | 'APIG_AI' */}
372 |                       <Descriptions.Item label="API来源">
373 |                         {SourceMap[apiProduct.apiConfig?.meta.source || '']}
374 |                       </Descriptions.Item>
375 |                       <Descriptions.Item label="API类型">
376 |                         {apiProduct.apiConfig?.meta.type}
377 |                       </Descriptions.Item>
378 |                     </Descriptions>
379 |                     <Table
380 |                       columns={openApiColumns as any}
381 |                       dataSource={endpoints}
382 |                       rowKey="key"
383 |                       pagination={false}
384 |                       size="small"
385 |                     />
386 |                   </>
387 |                 )}
388 | 
389 |                 {isMcp && (
390 |                   <>
391 |                     <Descriptions
392 |                       column={2}
393 |                       bordered
394 |                       size="small"
395 |                       className="mb-4"
396 |                     >
397 |                       <Descriptions.Item label="名称">
398 |                         {mcpParsed.server?.name ||
399 |                           apiProduct.mcpConfig?.meta.mcpServerName ||
400 |                           "—"}
401 |                       </Descriptions.Item>
402 |                       <Descriptions.Item label="来源">
403 |                         {apiProduct.mcpConfig?.meta.source
404 |                           ? SourceMap[apiProduct.mcpConfig.meta.source] || apiProduct.mcpConfig.meta.source
405 |                           : "—"}
406 |                       </Descriptions.Item>
407 |                       <Descriptions.Item label="来源类型">
408 |                         {apiProduct.mcpConfig?.meta.fromType
409 |                           ? FromTypeMap[apiProduct.mcpConfig.meta.fromType] || apiProduct.mcpConfig.meta.fromType
410 |                           : "—"}
411 |                       </Descriptions.Item>
412 |                       <Descriptions.Item label="API类型">
413 |                         {apiProduct.mcpConfig?.meta.source
414 |                           ? ProductTypeMap[apiProduct.type] || apiProduct.type
415 |                           : "—"}
416 |                       </Descriptions.Item>
417 |                     </Descriptions>
418 |                     <div className="mb-2">
419 |                       <span className="font-bold mr-2">工具列表:</span>
420 |                       {/* {Array.isArray(mcpParsed.tools) && mcpParsed.tools.length > 0 ? (
421 |                         mcpParsed.tools.map((tool, idx) => (
422 |                           <Tag key={tool.name || idx} color="blue" className="mr-1">
423 |                             {tool.name}
424 |                           </Tag>
425 |                         ))
426 |                       ) : (
427 |                         <span className="text-gray-400">—</span>
428 |                       )} */}
429 |                     </div>
430 | 
431 |                     <Collapse accordion>
432 |                       {(mcpParsed.tools || []).map((tool, idx) => (
433 |                         <Collapse.Panel header={tool.name} key={idx}>
434 |                           {tool.description && (
435 |                             <div className="mb-2 text-gray-600">
436 |                               {tool.description}
437 |                             </div>
438 |                           )}
439 |                           <div className="mb-2 font-bold">输入参数:</div>
440 |                           <div className="space-y-2">
441 |                             {tool.args && tool.args.length > 0 ? (
442 |                               tool.args.map((arg, aidx) => (
443 |                                 <div key={aidx} className="flex flex-col mb-2">
444 |                                   <div className="flex items-center mb-1">
445 |                                     <span className="font-medium mr-2">
446 |                                       {arg.name}
447 |                                     </span>
448 |                                     {arg.type && (
449 |                                       <span className="text-xs text-gray-500 mr-2">
450 |                                         ({arg.type})
451 |                                       </span>
452 |                                     )}
453 |                                     {arg.required && (
454 |                                       <span className="text-red-500 text-xs">
455 |                                         *
456 |                                       </span>
457 |                                     )}
458 |                                   </div>
459 |                                   {arg.description && (
460 |                                     <div className="text-xs text-gray-500 mb-1">
461 |                                       {arg.description}
462 |                                     </div>
463 |                                   )}
464 |                                   <input
465 |                                     disabled
466 |                                     className="border rounded px-2 py-1 text-sm bg-gray-100 w-full max-w-md"
467 |                                     placeholder={
468 |                                       arg.defaultValue !== undefined &&
469 |                                       arg.defaultValue !== null
470 |                                         ? String(arg.defaultValue)
471 |                                         : ""
472 |                                     }
473 |                                   />
474 |                                   {Array.isArray(arg.enumValues) &&
475 |                                     arg.enumValues.length > 0 && (
476 |                                       <div className="text-xs text-gray-500 mt-1">
477 |                                         可选值:{arg.enumValues.join(", ")}
478 |                                       </div>
479 |                                     )}
480 |                                 </div>
481 |                               ))
482 |                             ) : (
483 |                               <span className="text-gray-400">无参数</span>
484 |                             )}
485 |                           </div>
486 |                         </Collapse.Panel>
487 |                       ))}
488 |                     </Collapse>
489 |                   </>
490 |                 )}
491 |                 {!isOpenApi && !isMcp && (
492 |                   <Card>
493 |                     <div className="text-center py-8 text-gray-500">
494 |                       <p>暂无配置</p>
495 |                     </div>
496 |                   </Card>
497 |                   )}
498 |               </div>
499 |             ),
500 |           },
501 |           ...(!isMcp ? [{
502 |             key: "source",
503 |             label: "OpenAPI 规范",
504 |             children: (
505 |               <div style={{ height: 460 }}>
506 |                 <MonacoEditor
507 |                   language="yaml"
508 |                   theme="vs-light"
509 |                   value={content}
510 |                   options={{
511 |                     readOnly: true,
512 |                     minimap: { enabled: true },
513 |                     scrollBeyondLastLine: false,
514 |                     scrollbar: { vertical: "visible", horizontal: "visible" },
515 |                     wordWrap: "off",
516 |                     lineNumbers: "on",
517 |                     automaticLayout: true,
518 |                     fontSize: 14,
519 |                     copyWithSyntaxHighlighting: true,
520 |                     contextmenu: true,
521 |                   }}
522 |                   height="100%"
523 |                 />
524 |               </div>
525 |             ),
526 |           }] : []),
527 |           ...(isMcp ? [{
528 |             key: "mcpServerConfig",
529 |             label: "MCP连接配置",
530 |             children: (
531 |               <div className="space-y-4">
532 |                 <div className="">
533 |                   {apiProduct.mcpConfig?.mcpServerConfig?.rawConfig ? (
534 |                     // Local Mode - 显示本地配置
535 |                     <div>
536 |                       <h3 className="text-lg font-bold mb-2">Local Config</h3>
537 |                       <MonacoEditor
538 |                         language="json"
539 |                         theme="vs-light"
540 |                         value={localJson}
541 |                         options={{
542 |                           readOnly: true,
543 |                           minimap: { enabled: true },
544 |                           scrollBeyondLastLine: false,
545 |                           scrollbar: { vertical: "visible", horizontal: "visible" },
546 |                           wordWrap: "off",
547 |                           lineNumbers: "on",
548 |                           automaticLayout: true,
549 |                           fontSize: 14,
550 |                           copyWithSyntaxHighlighting: true,
551 |                           contextmenu: true,
552 |                         }}
553 |                         height="150px"
554 |                       />
555 |                     </div>
556 |                   ) : (
557 |                     // HTTP/SSE Mode - 根据配置状态动态显示
558 |                     <>
559 |                       {httpJson && (
560 |                         <div className="mt-4">
561 |                           <h3 className="text-lg font-bold mb-2">HTTP Config</h3>
562 |                           <MonacoEditor
563 |                             language="json"
564 |                             theme="vs-light"
565 |                             value={httpJson}
566 |                             options={{
567 |                               readOnly: true,
568 |                               minimap: { enabled: true },
569 |                               scrollBeyondLastLine: false,
570 |                               scrollbar: { vertical: "visible", horizontal: "visible" },
571 |                               wordWrap: "off",
572 |                               lineNumbers: "on",
573 |                               automaticLayout: true,
574 |                               fontSize: 14,
575 |                               copyWithSyntaxHighlighting: true,
576 |                               contextmenu: true,
577 |                             }}
578 |                             height="150px"
579 |                           />
580 |                         </div>
581 |                       )}
582 |                       {sseJson && (
583 |                         <div className="mt-4">
584 |                           <h3 className="text-lg font-bold mb-2">SSE Config</h3>
585 |                           <MonacoEditor
586 |                             language="json"
587 |                             theme="vs-light"
588 |                             value={sseJson}
589 |                             options={{
590 |                               readOnly: true,
591 |                               minimap: { enabled: true },
592 |                               scrollBeyondLastLine: false,
593 |                               scrollbar: { vertical: "visible", horizontal: "visible" },
594 |                               wordWrap: "off",
595 |                               lineNumbers: "on",
596 |                               automaticLayout: true,
597 |                               fontSize: 14,
598 |                               copyWithSyntaxHighlighting: true,
599 |                               contextmenu: true,
600 |                             }}
601 |                             height="150px"
602 |                           />
603 |                         </div>
604 |                       )}
605 |                     </>
606 |                   )}
607 |                 </div>
608 |               </div>
609 |             ),
610 |           }] : [])
611 |         ]}
612 |       />
613 |     </div>
614 |   );
615 | }
616 | 
```
Page 7/9FirstPrevNextLast