#
tokens: 47640/50000 17/349 files (page 5/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 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/PortalSecurity.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {Card, Form, Switch, Divider, message} from 'antd'
  2 | import {useMemo} from 'react'
  3 | import {Portal, ThirdPartyAuthConfig, AuthenticationType, OidcConfig, OAuth2Config} from '@/types'
  4 | import {portalApi} from '@/lib/api'
  5 | import {ThirdPartyAuthManager} from './ThirdPartyAuthManager'
  6 | 
  7 | interface PortalSecurityProps {
  8 |     portal: Portal
  9 |     onRefresh?: () => void
 10 | }
 11 | 
 12 | export function PortalSecurity({portal, onRefresh}: PortalSecurityProps) {
 13 |     const [form] = Form.useForm()
 14 | 
 15 |     const handleSave = async () => {
 16 |         try {
 17 |             const values = await form.validateFields()
 18 |             
 19 |             await portalApi.updatePortal(portal.portalId, {
 20 |                 name: portal.name,
 21 |                 description: portal.description,
 22 |                 portalSettingConfig: {
 23 |                     ...portal.portalSettingConfig,
 24 |                     builtinAuthEnabled: values.builtinAuthEnabled,
 25 |                     oidcAuthEnabled: values.oidcAuthEnabled,
 26 |                     autoApproveDevelopers: values.autoApproveDevelopers,
 27 |                     autoApproveSubscriptions: values.autoApproveSubscriptions,
 28 |                     frontendRedirectUrl: values.frontendRedirectUrl,
 29 |                 },
 30 |                 portalDomainConfig: portal.portalDomainConfig,
 31 |                 portalUiConfig: portal.portalUiConfig,
 32 |             })
 33 | 
 34 |             message.success('安全设置保存成功')
 35 |             onRefresh?.()
 36 |         } catch (error) {
 37 |             message.error('保存安全设置失败')
 38 |         }
 39 |     }
 40 | 
 41 |     const handleSettingUpdate = () => {
 42 |         // 立即更新配置
 43 |         handleSave()
 44 |     }
 45 | 
 46 |     // 第三方认证配置保存函数
 47 |     const handleSaveThirdPartyAuth = async (configs: ThirdPartyAuthConfig[]) => {
 48 |         try {
 49 |             // 分离OIDC和OAuth2配置,去掉type字段
 50 |             const oidcConfigs = configs
 51 |                 .filter(config => config.type === AuthenticationType.OIDC)
 52 |                 .map(config => {
 53 |                     const { type, ...oidcConfig } = config as (OidcConfig & { type: AuthenticationType.OIDC })
 54 |                     return oidcConfig
 55 |                 })
 56 | 
 57 |             const oauth2Configs = configs
 58 |                 .filter(config => config.type === AuthenticationType.OAUTH2)
 59 |                 .map(config => {
 60 |                     const { type, ...oauth2Config } = config as (OAuth2Config & { type: AuthenticationType.OAUTH2 })
 61 |                     return oauth2Config
 62 |                 })
 63 |             
 64 |             const updateData = {
 65 |                 ...portal,
 66 |                 portalSettingConfig: {
 67 |                     ...portal.portalSettingConfig,
 68 |                     // 直接保存分离的配置数组
 69 |                     oidcConfigs: oidcConfigs,
 70 |                     oauth2Configs: oauth2Configs
 71 |                 }
 72 |             }
 73 |             
 74 |             await portalApi.updatePortal(portal.portalId, updateData)
 75 |             
 76 |             onRefresh?.()
 77 |         } catch (error) {
 78 |             throw error
 79 |         }
 80 |     }
 81 | 
 82 |     // 合并OIDC和OAuth2配置用于统一显示
 83 |     const thirdPartyAuthConfigs = useMemo((): ThirdPartyAuthConfig[] => {
 84 |         const configs: ThirdPartyAuthConfig[] = []
 85 |         
 86 |         // 添加OIDC配置
 87 |         if (portal.portalSettingConfig?.oidcConfigs) {
 88 |             portal.portalSettingConfig.oidcConfigs.forEach(oidcConfig => {
 89 |                 configs.push({
 90 |                     ...oidcConfig,
 91 |                     type: AuthenticationType.OIDC
 92 |                 })
 93 |             })
 94 |         }
 95 |         
 96 |         // 添加OAuth2配置
 97 |         if (portal.portalSettingConfig?.oauth2Configs) {
 98 |             portal.portalSettingConfig.oauth2Configs.forEach(oauth2Config => {
 99 |                 configs.push({
100 |                     ...oauth2Config,
101 |                     type: AuthenticationType.OAUTH2
102 |                 })
103 |             })
104 |         }
105 |         
106 |         return configs
107 |     }, [portal.portalSettingConfig?.oidcConfigs, portal.portalSettingConfig?.oauth2Configs])
108 | 
109 | 
110 |     return (
111 |         <div className="p-6 space-y-6">
112 |             <div>
113 |                 <h1 className="text-2xl font-bold mb-2">Portal安全配置</h1>
114 |                 <p className="text-gray-600">配置Portal的认证与审批方式</p>
115 |             </div>
116 | 
117 |             <Form
118 |                 form={form}
119 |                 layout="vertical"
120 |                 initialValues={{
121 |                     builtinAuthEnabled: portal.portalSettingConfig?.builtinAuthEnabled,
122 |                     oidcAuthEnabled: portal.portalSettingConfig?.oidcAuthEnabled,
123 |                     autoApproveDevelopers: portal.portalSettingConfig?.autoApproveDevelopers,
124 |                     autoApproveSubscriptions: portal.portalSettingConfig?.autoApproveSubscriptions,
125 |                     frontendRedirectUrl: portal.portalSettingConfig?.frontendRedirectUrl,
126 |                 }}
127 |             >
128 |                 <Card>
129 |                     <div className="space-y-6">
130 |                         {/* 基本安全配置标题 */}
131 |                         <h3 className="text-lg font-medium">基本安全配置</h3>
132 |                         
133 |                         {/* 基本安全设置内容 */}
134 |                         <div className="grid grid-cols-2 gap-6">
135 |                             <Form.Item
136 |                                 name="builtinAuthEnabled"
137 |                                 label="账号密码登录"
138 |                                 valuePropName="checked"
139 |                             >
140 |                                 <Switch
141 |                                     onChange={() => handleSettingUpdate()}
142 |                                 />
143 |                             </Form.Item>
144 |                             <Form.Item
145 |                                 name="autoApproveDevelopers"
146 |                                 label="开发者自动审批"
147 |                                 valuePropName="checked"
148 |                             >
149 |                                 <Switch
150 |                                     onChange={() => handleSettingUpdate()}
151 |                                 />
152 |                             </Form.Item>
153 |                             <Form.Item
154 |                                 name="autoApproveSubscriptions"
155 |                                 label="订阅自动审批"
156 |                                 valuePropName="checked"
157 |                             >
158 |                                 <Switch
159 |                                     onChange={() => handleSettingUpdate()}
160 |                                 />
161 |                             </Form.Item>
162 |                         </div>
163 | 
164 |                         <Divider />
165 |                         
166 |                         {/* 第三方认证管理器 - 内部已有标题,不需要重复添加 */}
167 |                         <ThirdPartyAuthManager
168 |                             configs={thirdPartyAuthConfigs}
169 |                             onSave={handleSaveThirdPartyAuth}
170 |                         />
171 |                     </div>
172 |                 </Card>
173 |             </Form>
174 |         </div>
175 |     )
176 | }
177 | 
```

--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------

```
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <!--
  3 |   ~ Licensed to the Apache Software Foundation (ASF) under one
  4 |   ~ or more contributor license agreements.  See the NOTICE file
  5 |   ~ distributed with this work for additional information
  6 |   ~ regarding copyright ownership.  The ASF licenses this file
  7 |   ~ to you under the Apache License, Version 2.0 (the
  8 |   ~ "License"); you may not use this file except in compliance
  9 |   ~ with the License.  You may obtain a copy of the License at
 10 |   ~
 11 |   ~   http://www.apache.org/licenses/LICENSE-2.0
 12 |   ~
 13 |   ~ Unless required by applicable law or agreed to in writing,
 14 |   ~ software distributed under the License is distributed on an
 15 |   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 16 |   ~ KIND, either express or implied.  See the License for the
 17 |   ~ specific language governing permissions and limitations
 18 |   ~ under the License.
 19 |   -->
 20 | <project xmlns="http://maven.apache.org/POM/4.0.0"
 21 |          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 22 |          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 23 |     <modelVersion>4.0.0</modelVersion>
 24 | 
 25 |     <groupId>com.alibaba.himarket</groupId>
 26 |     <artifactId>himarket</artifactId>
 27 |     <version>1.0-SNAPSHOT</version>
 28 |     <packaging>pom</packaging>
 29 |     <name>himarket</name>
 30 |     <description>HiMarket AI OPEN Platform</description>
 31 |     <url>https://github.com/higress-group/himarket</url>
 32 | 
 33 |     <licenses>
 34 |         <license>
 35 |             <name>Apache License, Version 2.0</name>
 36 |             <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
 37 |             <distribution>repo</distribution>
 38 |         </license>
 39 |     </licenses>
 40 | 
 41 |     <modules>
 42 |         <module>portal-dal</module>
 43 |         <module>portal-server</module>
 44 |         <module>portal-bootstrap</module>
 45 |     </modules>
 46 | 
 47 |     <properties>
 48 |         <java.version>1.8</java.version>
 49 |         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 50 |         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 51 |         <spring-boot.version>2.7.18</spring-boot.version>
 52 |         <mybatis.version>2.3.1</mybatis.version>
 53 |         <mariadb.version>3.4.1</mariadb.version>
 54 |         <hutool.version>5.8.32</hutool.version>
 55 |         <bouncycastle.version>1.78</bouncycastle.version>
 56 |         <springdoc.version>1.7.0</springdoc.version>
 57 |         <apigsdk.version>4.0.10</apigsdk.version>
 58 |         <msesdk.version>7.21.0</msesdk.version>
 59 |         <aliyunsdk.version>4.4.6</aliyunsdk.version>
 60 |         <okhttp.version>4.12.0</okhttp.version>
 61 |         <maven.compiler.source>${java.version}</maven.compiler.source>
 62 |         <maven.compiler.target>${java.version}</maven.compiler.target>
 63 |     </properties>
 64 | 
 65 |     <!-- Dependency Management -->
 66 |     <dependencyManagement>
 67 |         <dependencies>
 68 |             <!-- Spring Boot Dependencies -->
 69 |             <dependency>
 70 |                 <groupId>org.springframework.boot</groupId>
 71 |                 <artifactId>spring-boot-dependencies</artifactId>
 72 |                 <version>${spring-boot.version}</version>
 73 |                 <type>pom</type>
 74 |                 <scope>import</scope>
 75 |             </dependency>
 76 | 
 77 |             <!-- MariaDB Driver -->
 78 |             <dependency>
 79 |                 <groupId>org.mariadb.jdbc</groupId>
 80 |                 <artifactId>mariadb-java-client</artifactId>
 81 |                 <version>${mariadb.version}</version>
 82 |             </dependency>
 83 | 
 84 |             <!-- Hutool -->
 85 |             <dependency>
 86 |                 <groupId>cn.hutool</groupId>
 87 |                 <artifactId>hutool-all</artifactId>
 88 |                 <version>${hutool.version}</version>
 89 |             </dependency>
 90 | 
 91 |             <!-- Spring Boot Starter Security -->
 92 |             <dependency>
 93 |                 <groupId>org.springframework.boot</groupId>
 94 |                 <artifactId>spring-boot-starter-security</artifactId>
 95 |                 <version>${spring-boot.version}</version>
 96 |             </dependency>
 97 | 
 98 |             <!-- Spring Boot Starter OAuth2 Client -->
 99 |             <dependency>
100 |                 <groupId>org.springframework.boot</groupId>
101 |                 <artifactId>spring-boot-starter-oauth2-client</artifactId>
102 |             </dependency>
103 | 
104 |             <dependency>
105 |                 <groupId>org.springdoc</groupId>
106 |                 <artifactId>springdoc-openapi-ui</artifactId>
107 |                 <version>${springdoc.version}</version>
108 |             </dependency>
109 | 
110 |             <dependency>
111 |                 <groupId>com.aliyun</groupId>
112 |                 <artifactId>alibabacloud-apig20240327</artifactId>
113 |                 <version>${apigsdk.version}</version>
114 |             </dependency>
115 | 
116 |             <!-- 阿里云 MSE SDK -->
117 |             <dependency>
118 |                 <groupId>com.aliyun</groupId>
119 |                 <artifactId>mse20190531</artifactId>
120 |                 <version>${msesdk.version}</version>
121 |             </dependency>
122 | 
123 |             <dependency>
124 |                 <groupId>com.aliyun</groupId>
125 |                 <artifactId>aliyun-java-sdk-core</artifactId>
126 |                 <version>${aliyunsdk.version}</version>
127 |             </dependency>
128 | 
129 |             <dependency>
130 |                 <groupId>com.squareup.okhttp3</groupId>
131 |                 <artifactId>okhttp</artifactId>
132 |                 <version>${okhttp.version}</version>
133 |             </dependency>
134 | 
135 |             <dependency>
136 |                 <groupId>com.alibaba.nacos</groupId>
137 |                 <artifactId>nacos-maintainer-client</artifactId>
138 |                 <version>3.0.2</version>
139 |             </dependency>
140 | 
141 |             <!-- Bouncy Castle Provider -->
142 |             <dependency>
143 |                 <groupId>org.bouncycastle</groupId>
144 |                 <artifactId>bcprov-jdk15to18</artifactId>
145 |                 <version>${bouncycastle.version}</version>
146 |             </dependency>
147 |         </dependencies>
148 |     </dependencyManagement>
149 | 
150 |     <!-- Build Configuration -->
151 |     <build>
152 |         <pluginManagement>
153 |             <plugins>
154 |                 <plugin>
155 |                     <groupId>org.springframework.boot</groupId>
156 |                     <artifactId>spring-boot-maven-plugin</artifactId>
157 |                     <version>${spring-boot.version}</version>
158 |                 </plugin>
159 |                 <plugin>
160 |                     <groupId>org.apache.maven.plugins</groupId>
161 |                     <artifactId>maven-compiler-plugin</artifactId>
162 |                     <version>3.8.1</version>
163 |                     <configuration>
164 |                         <source>${java.version}</source>
165 |                         <target>${java.version}</target>
166 |                         <encoding>${project.build.sourceEncoding}</encoding>
167 |                     </configuration>
168 |                 </plugin>
169 |             </plugins>
170 |         </pluginManagement>
171 |     </build>
172 | </project>
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/core/utils/TokenUtil.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.core.utils;
 21 | 
 22 | import cn.hutool.core.map.MapUtil;
 23 | import cn.hutool.core.util.ObjectUtil;
 24 | import cn.hutool.core.util.StrUtil;
 25 | import cn.hutool.extra.spring.SpringUtil;
 26 | import cn.hutool.jwt.JWT;
 27 | import cn.hutool.jwt.JWTUtil;
 28 | import cn.hutool.jwt.signers.JWTSignerUtil;
 29 | import com.alibaba.apiopenplatform.core.constant.CommonConstants;
 30 | import com.alibaba.apiopenplatform.support.common.User;
 31 | import com.alibaba.apiopenplatform.support.enums.UserType;
 32 | 
 33 | import javax.servlet.http.Cookie;
 34 | import javax.servlet.http.HttpServletRequest;
 35 | import java.nio.charset.StandardCharsets;
 36 | import java.time.Duration;
 37 | import java.util.*;
 38 | import java.util.concurrent.ConcurrentHashMap;
 39 | 
 40 | public class TokenUtil {
 41 | 
 42 |     private static String JWT_SECRET;
 43 | 
 44 |     private static long JWT_EXPIRE_MILLIS;
 45 | 
 46 |     private static final Map<String, Long> INVALID_TOKENS = new ConcurrentHashMap<>();
 47 | 
 48 |     private static String getJwtSecret() {
 49 |         if (JWT_SECRET == null) {
 50 |             JWT_SECRET = SpringUtil.getProperty("jwt.secret");
 51 |         }
 52 | 
 53 |         if (StrUtil.isBlank(JWT_SECRET)) {
 54 |             throw new RuntimeException("JWT secret cannot be empty");
 55 |         }
 56 |         return JWT_SECRET;
 57 |     }
 58 | 
 59 |     private static long getJwtExpireMillis() {
 60 |         if (JWT_EXPIRE_MILLIS == 0) {
 61 |             String expiration = SpringUtil.getProperty("jwt.expiration");
 62 |             if (StrUtil.isBlank(expiration)) {
 63 |                 throw new RuntimeException("JWT expiration is empty");
 64 |             }
 65 | 
 66 |             if (expiration.matches("\\d+[smhd]")) {
 67 |                 JWT_EXPIRE_MILLIS = Duration.parse("PT" + expiration.toUpperCase()).toMillis();
 68 |             } else {
 69 |                 JWT_EXPIRE_MILLIS = Long.parseLong(expiration);
 70 |             }
 71 |         }
 72 |         return JWT_EXPIRE_MILLIS;
 73 |     }
 74 | 
 75 |     public static String generateAdminToken(String userId) {
 76 |         return generateToken(UserType.ADMIN, userId);
 77 |     }
 78 | 
 79 |     public static String generateDeveloperToken(String userId) {
 80 |         return generateToken(UserType.DEVELOPER, userId);
 81 |     }
 82 | 
 83 |     /**
 84 |      * 生成令牌
 85 |      *
 86 |      * @param userType
 87 |      * @param userId
 88 |      * @return
 89 |      */
 90 |     private static String generateToken(UserType userType, String userId) {
 91 |         long now = System.currentTimeMillis();
 92 | 
 93 |         Map<String, String> claims = MapUtil.<String, String>builder()
 94 |                 .put(CommonConstants.USER_TYPE, userType.name())
 95 |                 .put(CommonConstants.USER_ID, userId)
 96 |                 .build();
 97 | 
 98 |         return JWT.create()
 99 |                 .addPayloads(claims)
100 |                 .setIssuedAt(new Date(now))
101 |                 .setExpiresAt(new Date(now + getJwtExpireMillis()))
102 |                 .setSigner(JWTSignerUtil.hs256(getJwtSecret().getBytes(StandardCharsets.UTF_8)))
103 |                 .sign();
104 |     }
105 | 
106 |     /**
107 |      * 解析Token
108 |      *
109 |      * @param token
110 |      * @return
111 |      */
112 |     public static User parseUser(String token) {
113 |         JWT jwt = JWTUtil.parseToken(token);
114 | 
115 |         // 验证签名
116 |         boolean isValid = jwt.setSigner(JWTSignerUtil.hs256(getJwtSecret().getBytes(StandardCharsets.UTF_8))).verify();
117 |         if (!isValid) {
118 |             throw new IllegalArgumentException("Invalid token signature");
119 |         }
120 | 
121 |         // 验证过期时间
122 |         Object expObj = jwt.getPayloads().get(JWT.EXPIRES_AT);
123 |         if (ObjectUtil.isNotNull(expObj)) {
124 |             long expireAt = Long.parseLong(expObj.toString());
125 |             if (expireAt * 1000 <= System.currentTimeMillis()) {
126 |                 throw new IllegalArgumentException("Token has expired");
127 |             }
128 |         }
129 | 
130 |         return jwt.getPayloads().toBean(User.class);
131 |     }
132 | 
133 |     public static String getTokenFromRequest(HttpServletRequest request) {
134 |         // 从Header中获取token
135 |         String authHeader = request.getHeader(CommonConstants.AUTHORIZATION_HEADER);
136 | 
137 |         String token = null;
138 |         if (authHeader != null && authHeader.startsWith(CommonConstants.BEARER_PREFIX)) {
139 |             token = authHeader.substring(CommonConstants.BEARER_PREFIX.length());
140 |         }
141 | 
142 |         // 从Cookie中获取token
143 |         if (StrUtil.isBlank(token)) {
144 |             token = Optional.ofNullable(request.getCookies())
145 |                     .flatMap(cookies -> Arrays.stream(cookies)
146 |                             .filter(cookie -> CommonConstants.AUTH_TOKEN_COOKIE.equals(cookie.getName()))
147 |                             .map(Cookie::getValue)
148 |                             .findFirst())
149 |                     .orElse(null);
150 |         }
151 |         if (StrUtil.isBlank(token) || isTokenRevoked(token)) {
152 |             return null;
153 |         }
154 | 
155 |         return token;
156 |     }
157 | 
158 |     public static void revokeToken(String token) {
159 |         if (StrUtil.isBlank(token)) {
160 |             return;
161 |         }
162 |         long expireAt = getTokenExpireTime(token);
163 |         INVALID_TOKENS.put(token, expireAt);
164 |         cleanExpiredTokens();
165 |     }
166 | 
167 |     private static long getTokenExpireTime(String token) {
168 |         JWT jwt = JWTUtil.parseToken(token);
169 |         Object expObj = jwt.getPayloads().get(JWT.EXPIRES_AT);
170 |         if (ObjectUtil.isNotNull(expObj)) {
171 |             return Long.parseLong(expObj.toString()) * 1000; // JWT过期时间是秒,转换为毫秒
172 |         }
173 |         return System.currentTimeMillis() + getJwtExpireMillis(); // 默认过期时间
174 |     }
175 | 
176 |     public static void revokeToken(HttpServletRequest request) {
177 |         String token = getTokenFromRequest(request);
178 |         if (StrUtil.isNotBlank(token)) {
179 |             revokeToken(token);
180 |         }
181 |     }
182 | 
183 |     public static boolean isTokenRevoked(String token) {
184 |         if (StrUtil.isBlank(token)) {
185 |             return false;
186 |         }
187 |         Long expireAt = INVALID_TOKENS.get(token);
188 |         if (expireAt == null) {
189 |             return false;
190 |         }
191 |         if (expireAt <= System.currentTimeMillis()) {
192 |             INVALID_TOKENS.remove(token);
193 |             return false;
194 |         }
195 |         return true;
196 |     }
197 | 
198 |     private static void cleanExpiredTokens() {
199 |         long now = System.currentTimeMillis();
200 |         INVALID_TOKENS.entrySet().removeIf(entry -> entry.getValue() <= now);
201 |     }
202 | 
203 |     public static long getTokenExpiresIn() {
204 |         return getJwtExpireMillis() / 1000;
205 |     }
206 | }
207 | 
```

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

```typescript
  1 | import { Card, Table, Button, Space, Typography, Input, Avatar } from "antd";
  2 | import { SearchOutlined, DeleteOutlined, EyeOutlined } from "@ant-design/icons";
  3 | import { Layout } from "../components/Layout";
  4 | import { useEffect, useState, useCallback } from "react";
  5 | import { getConsumers, deleteConsumer, createConsumer } from "../lib/api";
  6 | import { message, Modal } from "antd";
  7 | import { Link, useSearchParams } from "react-router-dom";
  8 | import { formatDateTime } from "../lib/utils";
  9 | import type { Consumer } from "../types/consumer";
 10 | 
 11 | const { Title, Paragraph } = Typography;
 12 | const { Search } = Input;
 13 | 
 14 | function ConsumersPage() {
 15 |   const [searchParams] = useSearchParams();
 16 |   const productId = searchParams.get('productId');
 17 |   
 18 |   const [consumers, setConsumers] = useState<Consumer[]>([]);
 19 |   const [loading, setLoading] = useState(false);
 20 |   const [page, setPage] = useState(1);
 21 |   const [pageSize, setPageSize] = useState(10);
 22 |   const [total, setTotal] = useState(0);
 23 |   const [searchInput, setSearchInput] = useState(""); // 输入框的值
 24 |   const [searchName, setSearchName] = useState(""); // 实际搜索的值
 25 |   const [addModalOpen, setAddModalOpen] = useState(false);
 26 |   const [addLoading, setAddLoading] = useState(false);
 27 |   const [addForm, setAddForm] = useState({ name: '', description: '' });
 28 | 
 29 |   const fetchConsumers = useCallback(async (searchKeyword?: string, targetPage?: number) => {
 30 |     setLoading(true);
 31 |     try {
 32 |       const res = await getConsumers(
 33 |         { name: searchKeyword || '' },
 34 |         { page: targetPage || page, size: pageSize }
 35 |       );
 36 |       setConsumers(res.data?.content || []);
 37 |       setTotal(res.data?.totalElements || 0);
 38 |     } catch {
 39 |       // message.error("获取消费者列表失败");
 40 |     } finally {
 41 |       setLoading(false);
 42 |     }
 43 |   }, [page, pageSize]); // 不依赖 searchName
 44 | 
 45 |   // 初始加载和分页变化时调用
 46 |   useEffect(() => {
 47 |     fetchConsumers(searchName);
 48 |   }, [page, pageSize, fetchConsumers]); // 包含fetchConsumers以确保初始加载
 49 | 
 50 |   // 处理搜索
 51 |   const handleSearch = useCallback(async (searchValue?: string) => {
 52 |     const actualSearchValue = searchValue !== undefined ? searchValue : searchInput;
 53 |     setSearchName(actualSearchValue);
 54 |     setPage(1);
 55 |     // 直接调用API,不依赖状态变化
 56 |     await fetchConsumers(actualSearchValue, 1);
 57 |   }, [searchInput, fetchConsumers]);
 58 | 
 59 |   const handleDelete = (record: Consumer) => {
 60 |     Modal.confirm({
 61 |       title: `确定要删除消费者「${record.name}」吗?`,
 62 |       onOk: async () => {
 63 |         try {
 64 |           await deleteConsumer(record.consumerId);
 65 |           message.success("删除成功");
 66 |           await fetchConsumers(searchName); // 使用当前搜索条件重新加载
 67 |         } catch {
 68 |           // message.error("删除失败");
 69 |         }
 70 |       },
 71 |     });
 72 |   };
 73 | 
 74 |   const handleAdd = async () => {
 75 |     if (!addForm.name.trim()) {
 76 |       message.warning('请输入消费者名称');
 77 |       return;
 78 |     }
 79 |     setAddLoading(true);
 80 |     try {
 81 |       await createConsumer({ name: addForm.name, description: addForm.description });
 82 |       message.success('新增成功');
 83 |       setAddModalOpen(false);
 84 |       setAddForm({ name: '', description: '' });
 85 |       await fetchConsumers(searchName); // 使用当前搜索条件重新加载
 86 |     } catch {
 87 |       // message.error('新增失败');
 88 |     } finally {
 89 |       setAddLoading(false);
 90 |     }
 91 |   };
 92 | 
 93 |   const columns = [
 94 |     {
 95 |       title: '消费者',
 96 |       dataIndex: 'name',
 97 |       key: 'name',
 98 |       render: (name: string, record: Consumer) => (
 99 |         <div className="flex items-center space-x-3">
100 |           <Avatar className="bg-blue-500">
101 |             {name?.charAt(0).toUpperCase()}
102 |           </Avatar>
103 |           <div>
104 |             <div className="font-medium">{name}</div>
105 |             <div className="text-xs text-gray-400">{record.description}</div>
106 |           </div>
107 |         </div>
108 |       ),
109 |     },
110 |     {
111 |       title: '创建时间',
112 |       dataIndex: 'createAt',
113 |       key: 'createAt',
114 |       render: (date: string) => date ? formatDateTime(date) : '-',
115 |     },
116 |     {
117 |       title: '操作',
118 |       key: 'action',
119 |       render: (_: unknown, record: Consumer) => (
120 |         <Space>
121 |           <Link to={`/consumers/${record.consumerId}`}>
122 |             <Button 
123 |               type="link" 
124 |               icon={<EyeOutlined />}
125 |             >
126 |               查看详情
127 |             </Button>
128 |           </Link>
129 |           <Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
130 |             删除
131 |           </Button>
132 |         </Space>
133 |       ),
134 |     },
135 |   ];
136 | 
137 |   return (
138 |     <Layout>
139 |       <div className="mb-8">
140 |         <Title level={1} className="mb-2">
141 |           {productId ? '产品订阅管理' : '消费者管理'}
142 |         </Title>
143 |         <Paragraph className="text-gray-600">
144 |           {productId ? '管理此产品的消费者订阅情况' : '管理API的消费者用户和订阅信息'}
145 |         </Paragraph>
146 |       </div>
147 | 
148 |       <Card>
149 |         <div className="mb-4 flex gap-4">
150 |           {!productId && (
151 |             <Button type="primary" onClick={() => setAddModalOpen(true)}>
152 |               新增消费者
153 |             </Button>
154 |           )}
155 |           <Search
156 |             placeholder={"搜索消费者..."}
157 |             prefix={<SearchOutlined />}
158 |             style={{ width: 300 }}
159 |             value={searchInput}
160 |             onChange={e => setSearchInput(e.target.value)}
161 |             onSearch={handleSearch}
162 |             allowClear
163 |           />
164 |         </div>
165 |         <Table
166 |           columns={columns}
167 |           dataSource={consumers}
168 |           rowKey="consumerId"
169 |           loading={loading}
170 |           pagination={{
171 |             total,
172 |             current: page,
173 |             pageSize,
174 |             showSizeChanger: true,
175 |             showQuickJumper: true,
176 |             showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
177 |             onChange: (p, ps) => {
178 |               setPage(p);
179 |               setPageSize(ps);
180 |             },
181 |           }}
182 |         />
183 |         <Modal
184 |           title="新增消费者"
185 |           open={addModalOpen}
186 |           onCancel={() => { setAddModalOpen(false); setAddForm({ name: '', description: '' }); }}
187 |           onOk={handleAdd}
188 |           confirmLoading={addLoading}
189 |           okText="提交"
190 |           cancelText="取消"
191 |         >
192 |           <div style={{ marginBottom: 16 }}>
193 |             <Input
194 |               placeholder="消费者名称"
195 |               value={addForm.name}
196 |               maxLength={50}
197 |               onChange={e => setAddForm(f => ({ ...f, name: e.target.value }))}
198 |               disabled={addLoading}
199 |             />
200 |           </div>
201 |           <div>
202 |             <Input.TextArea
203 |               placeholder="描述(可选),长度限制64"
204 |               value={addForm.description}
205 |               maxLength={64}
206 |               onChange={e => setAddForm(f => ({ ...f, description: e.target.value }))}
207 |               disabled={addLoading}
208 |               rows={3}
209 |             />
210 |           </div>
211 |         </Modal>
212 |       </Card>
213 | 
214 |       <Card title="消费者统计" className="mt-8">
215 |         <div className="flex justify-center">
216 |           <div className="text-center">
217 |             <div className="text-2xl font-bold text-blue-600">{total}</div>
218 |             <div className="text-sm text-gray-500">总消费者</div>
219 |           </div>
220 |           {/* 其他统计项可根据接口返回字段补充 */}
221 |         </div>
222 |       </Card>
223 |     </Layout>
224 |   );
225 | }
226 | 
227 | export default ConsumersPage; 
```

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

```typescript
  1 | import { useEffect, useState } from "react";
  2 | import { Card, Tag, Typography, Input, Avatar, Skeleton } from "antd";
  3 | import { Link } from "react-router-dom";
  4 | import { Layout } from "../components/Layout";
  5 | import api from "../lib/api";
  6 | import { ProductStatus } from "../types";
  7 | import type { Product, ApiResponse, PaginatedResponse, ProductIcon } from "../types";
  8 | // import { getCategoryText, getCategoryColor } from "../lib/statusUtils";
  9 | 
 10 | const { Title, Paragraph } = Typography;
 11 | const { Search } = Input;
 12 | 
 13 | interface McpServer {
 14 |   key: string;
 15 |   name: string;
 16 |   description: string;
 17 |   status: string;
 18 |   version: string;
 19 |   endpoints: number;
 20 |   category: string;
 21 |   creator: string;
 22 |   icon?: ProductIcon;
 23 |   mcpConfig?: any;
 24 |   updatedAt: string;
 25 | }
 26 | 
 27 | function McpPage() {
 28 |   const [loading, setLoading] = useState(false);
 29 |   const [mcpServers, setMcpServers] = useState<McpServer[]>([]);
 30 |   const [searchText, setSearchText] = useState('');
 31 | 
 32 |   useEffect(() => {
 33 |     fetchMcpServers();
 34 |   }, []);
 35 |   // 处理产品图标的函数
 36 |   const getIconUrl = (icon?: ProductIcon | null): string => {
 37 |     const fallback = "/MCP.svg";
 38 |     
 39 |     if (!icon) {
 40 |       return fallback;
 41 |     }
 42 |     
 43 |     switch (icon.type) {
 44 |       case "URL":
 45 |         return icon.value || fallback;
 46 |       case "BASE64":
 47 |         // 如果value已经包含data URL前缀,直接使用;否则添加前缀
 48 |         return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
 49 |       default:
 50 |         return fallback;
 51 |     }
 52 |   };
 53 |   const fetchMcpServers = async () => {
 54 |     setLoading(true);
 55 |     try {
 56 |       const response: ApiResponse<PaginatedResponse<Product>> = await api.get("/products?type=MCP_SERVER&page=0&size=100");
 57 |       if (response.code === "SUCCESS" && response.data) {
 58 |         // 移除重复过滤,简化数据映射
 59 |         const mapped = response.data.content.map((item: Product) => ({
 60 |           key: item.productId,
 61 |           name: item.name,
 62 |           description: item.description,
 63 |           status: item.status === ProductStatus.ENABLE ? 'active' : 'inactive',
 64 |           version: 'v1.0.0',
 65 |           endpoints: 0,
 66 |           category: item.category,
 67 |           creator: 'Unknown',
 68 |           icon: item.icon || undefined,
 69 |           mcpConfig: item.mcpConfig,
 70 |           updatedAt: item.updatedAt?.slice(0, 10) || ''
 71 |         }));
 72 |         setMcpServers(mapped);
 73 |       }
 74 |     } catch (error) {
 75 |       console.error('获取MCP服务器列表失败:', error);
 76 |     } finally {
 77 |       setLoading(false);
 78 |     }
 79 |   };
 80 | 
 81 | 
 82 | 
 83 |   const filteredMcpServers = mcpServers.filter(server => {
 84 |     return server.name.toLowerCase().includes(searchText.toLowerCase()) ||
 85 |            server.description.toLowerCase().includes(searchText.toLowerCase()) ||
 86 |            server.creator.toLowerCase().includes(searchText.toLowerCase());
 87 |   });
 88 | 
 89 |   return (
 90 |     <Layout>
 91 |       {/* Header Section */}
 92 |       <div className="text-center mb-8">
 93 |         <Title level={1} className="mb-4">
 94 |           MCP 市场
 95 |         </Title>
 96 |         <Paragraph className="text-gray-600 text-lg max-w-4xl mx-auto text-flow text-flow-grey slow">
 97 |           支持私有化部署,共建和兼容MCP市场官方协议,具备更多管理能力,支持自动注册、智能路由的MCP市场
 98 |         </Paragraph>
 99 |       </div>
100 | 
101 |       {/* Search Section */}
102 |       <div className="flex justify-center mb-8">
103 |         <div className="relative w-full max-w-2xl">
104 |           <Search
105 |             placeholder="请输入内容"
106 |             size="large"
107 |             value={searchText}
108 |             onChange={(e) => setSearchText(e.target.value)}
109 |             className="rounded-lg shadow-lg"
110 |           />
111 |         </div>
112 |       </div>
113 | 
114 |       {/* Servers Section */}
115 |       <div className="mb-6">
116 |         <Title level={3} className="mb-4">
117 |           热门/推荐 MCP Servers: {filteredMcpServers.length}
118 |         </Title>
119 |       </div>
120 | 
121 |       {/* Servers Grid */}
122 |       {loading ? (
123 |         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
124 |           {Array.from({ length: 6 }).map((_, index) => (
125 |             <Card key={index} className="h-full rounded-lg shadow-lg">
126 |               <Skeleton loading active>
127 |                 <div className="flex items-start space-x-4 mb-2">
128 |                   <Skeleton.Avatar size={48} active />
129 |                   <div className="flex-1 min-w-0">
130 |                     <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
131 |                     <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
132 |                     <Skeleton.Input active size="small" style={{ width: '60%' }} />
133 |                   </div>
134 |                 </div>
135 |               </Skeleton>
136 |             </Card>
137 |           ))}
138 |         </div>
139 |       ) : (
140 |         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
141 |           {filteredMcpServers.map((server) => (
142 |           <Link key={server.key} to={`/mcp/${server.key}`} className="block">
143 |             <Card
144 |               hoverable
145 |               className="h-full transition-all duration-200 hover:shadow-lg cursor-pointer rounded-lg shadow-lg"
146 |             >
147 |               <div className="flex items-start space-x-4 mb-2">
148 |                 {/* Server Icon */}
149 |                 {server.icon ? (
150 |                   <Avatar
151 |                     size={48}
152 |                     src={getIconUrl(server.icon)}
153 |                   />
154 |                 ) : (
155 |                   <Avatar
156 |                     size={48}
157 |                     className="bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg"
158 |                     style={{ fontSize: "18px", fontWeight: "600" }}
159 |                   >
160 |                     {server.name[0]}
161 |                   </Avatar>
162 |                 )}
163 | 
164 |                 {/* Server Info */}
165 |                 <div className="flex-1 min-w-0">
166 |                   <div className="flex items-center justify-between mb-2">
167 |                     <Title level={5} className="mb-0 truncate">
168 |                       {server.name}
169 |                     </Title>
170 |                     <Tag className="text-xs text-green-700 border-0 bg-transparent px-0">
171 |                       {server.mcpConfig?.mcpServerConfig?.transportMode || 'remote'}
172 |                     </Tag>
173 |                   </div>
174 |                   </div>
175 |                 </div>
176 |                   <Paragraph className="text-sm text-gray-600 mb-3 line-clamp-2">
177 |                     {server.description}
178 |                   </Paragraph>
179 | 
180 |                   <div className="flex items-center justify-between">
181 |                     {/* <Tag color={getCategoryColor(server.category || 'OFFICIAL')} className="">
182 |                       {getCategoryText(server.category || 'OFFICIAL')}
183 |                     </Tag> */}
184 |                     <div className="text-xs text-gray-400">
185 |                       更新 {server.updatedAt}
186 |                     </div>
187 |                   </div>
188 |             </Card>
189 |           </Link>
190 |         ))}
191 |       </div>
192 |       )}
193 | 
194 |       {/* Empty State */}
195 |       {filteredMcpServers.length === 0 && (
196 |         <div className="text-center py-8">
197 |           <div className="text-gray-500">暂无MCP服务器</div>
198 |         </div>
199 |       )}
200 |     </Layout>
201 |   );
202 | }
203 | 
204 | export default McpPage; 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/lib/api.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
  2 | import { getToken, removeToken } from './utils'
  3 | import {  message } from 'antd'
  4 | 
  5 | 
  6 | 
  7 | const api: AxiosInstance = axios.create({
  8 |   baseURL: import.meta.env.VITE_API_BASE_URL,
  9 |   timeout: 10000,
 10 |   headers: {
 11 |     'Content-Type': 'application/json',
 12 |   },
 13 |   withCredentials: true, // 确保跨域请求时携带 cookie
 14 | })
 15 | 
 16 | // 请求拦截器
 17 | api.interceptors.request.use(
 18 |   (config: InternalAxiosRequestConfig) => {
 19 |     const token = getToken()
 20 |     if (token && config.headers) {
 21 |       config.headers.Authorization = `Bearer ${token}`
 22 |     }
 23 |     return config
 24 |   },
 25 |   (error) => {
 26 |     return Promise.reject(error)
 27 |   }
 28 | )
 29 | 
 30 | // 响应拦截器
 31 | api.interceptors.response.use(
 32 |   (response: AxiosResponse) => {
 33 |     return response.data
 34 |   },
 35 |   (error) => {
 36 |     message.error(error.response?.data?.message || '请求发生错误');
 37 |     if (error.response?.status === 403 || error.response?.status === 401) {
 38 |       removeToken()
 39 |       window.location.href = '/login'
 40 |     }
 41 |     return Promise.reject(error)
 42 |   }
 43 | )
 44 | 
 45 | 
 46 | export default api
 47 | 
 48 | // 用户相关API
 49 | export const authApi = {
 50 |   getNeedInit: () => {
 51 |     return api.get('/admins/need-init')
 52 |   }
 53 | }
 54 | // Portal相关API
 55 | export const portalApi = {
 56 |   // 获取portal列表
 57 |   getPortals: (params?: { page?: number; size?: number }) => {
 58 |     return api.get(`/portals`, { params })
 59 |   },
 60 |   // 获取Portal Dashboard URL
 61 |   getPortalDashboard: (portalId: string, type: string = 'Portal') => {
 62 |     return api.get(`/portals/${portalId}/dashboard`, { params: { type } })
 63 |   },
 64 |   deletePortal: (portalId: string) => {
 65 |     return api.delete(`/portals/${portalId}`)
 66 |   },
 67 |   createPortal: (data: any) => {
 68 |     return api.post(`/portals`, data)
 69 |   },
 70 |   // 获取portal详情
 71 |   getPortalDetail: (portalId: string) => {
 72 |     return api.get(`/portals/${portalId}`)
 73 |   },
 74 |   // 绑定域名
 75 |   bindDomain: (portalId: string, domainData: { domain: string; protocol: string; type: string }) => {
 76 |     return api.post(`/portals/${portalId}/domains`, domainData)
 77 |   },
 78 |   // 解绑域名
 79 |   unbindDomain: (portalId: string, domain: string) => {
 80 |     const encodedDomain = encodeURIComponent(domain)
 81 |     return api.delete(`/portals/${portalId}/domains/${encodedDomain}`)
 82 |   },
 83 |   // 更新Portal
 84 |   updatePortal: (portalId: string, data: any) => {
 85 |     return api.put(`/portals/${portalId}`, data)
 86 |   },
 87 |   // 更新Portal设置
 88 |   updatePortalSettings: (portalId: string, settings: any) => {
 89 |     return api.put(`/portals/${portalId}/setting`, settings)
 90 |   },
 91 |   // 获取Portal的开发者列表
 92 |   getDeveloperList: (portalId: string, pagination?: { page: number; size: number }) => {
 93 |     return api.get(`/developers`, {
 94 |       params: {
 95 |         portalId,
 96 |         ...pagination
 97 |       }
 98 |     })
 99 |   },
100 |   // 更新开发者状态
101 |   updateDeveloperStatus: (portalId: string, developerId: string, status: string) => {
102 |     return api.patch(`/developers/${developerId}/status`, {
103 |       portalId,
104 |       status
105 |     })
106 |   },
107 |   deleteDeveloper: (developerId: string) => {
108 |     return api.delete(`/developers/${developerId}`)
109 |   },
110 |   getConsumerList: (portalId: string, developerId: string, pagination?: { page: number; size: number }) => {
111 |     return api.get(`/consumers`, {
112 |       params: {
113 |         portalId,
114 |         developerId,
115 |         ...pagination
116 |       }
117 |     })
118 |   },
119 |   // 审批consumer
120 |   approveConsumer: (consumerId: string) => {
121 |     return api.patch(`/consumers/${consumerId}/status`)
122 |   },
123 |   // 获取Consumer的订阅列表
124 |   getConsumerSubscriptions: (consumerId: string, params?: { page?: number; size?: number; status?: string }) => {
125 |     return api.get(`/consumers/${consumerId}/subscriptions`, { params })
126 |   },
127 |   // 审批订阅申请
128 |   approveSubscription: (consumerId: string, productId: string) => {
129 |     return api.patch(`/consumers/${consumerId}/subscriptions/${productId}`)
130 |   },
131 |   // 删除订阅
132 |   deleteSubscription: (consumerId: string, productId: string) => {
133 |     return api.delete(`/consumers/${consumerId}/subscriptions/${productId}`)
134 |   }
135 | }
136 | 
137 | // API Product相关API
138 | export const apiProductApi = {
139 |   // 获取API产品列表
140 |   getApiProducts: (params?: any) => {
141 |     return api.get(`/products`, { params })
142 |   },
143 |   // 获取API产品详情
144 |   getApiProductDetail: (productId: string) => {
145 |     return api.get(`/products/${productId}`)
146 |   },
147 |   // 创建API产品
148 |   createApiProduct: (data: any) => {
149 |     return api.post(`/products`, data)
150 |   },
151 |   // 删除API产品
152 |   deleteApiProduct: (productId: string) => {
153 |     return api.delete(`/products/${productId}`)
154 |   },
155 |   // 更新API产品
156 |   updateApiProduct: (productId: string, data: any) => {
157 |     return api.put(`/products/${productId}`, data)
158 |   },
159 |   // 获取API产品关联的服务
160 |   getApiProductRef: (productId: string) => {
161 |     return api.get(`/products/${productId}/ref`)
162 |   },
163 |   // 创建API产品关联
164 |   createApiProductRef: (productId: string, data: any) => {
165 |     return api.post(`/products/${productId}/ref`, data)
166 |   },
167 |   // 删除API产品关联
168 |   deleteApiProductRef: (productId: string) => {
169 |     return api.delete(`/products/${productId}/ref`)
170 |   },
171 |   // 获取API产品已发布的门户列表
172 |   getApiProductPublications: (productId: string, params?: any) => {
173 |     return api.get(`/products/${productId}/publications`, { params })
174 |   },
175 |   // 发布API产品到门户
176 |   publishToPortal: (productId: string, portalId: string) => {
177 |     return api.post(`/products/${productId}/publications/${portalId}`)
178 |   },
179 |   // 取消发布API产品到门户
180 |   cancelPublishToPortal: (productId: string, portalId: string) => {
181 |     return api.delete(`/products/${productId}/publications/${portalId}`)
182 |   },
183 |   // 获取API产品的Dashboard监控面板URL
184 |   getProductDashboard: (productId: string) => {
185 |     return api.get(`/products/${productId}/dashboard`)
186 |   }
187 | }
188 | 
189 | // Gateway相关API
190 | export const gatewayApi = {
191 |   // 获取网关列表
192 |   getGateways: (params?: any) => {
193 |     return api.get(`/gateways`, { params })
194 |   },
195 |   // 获取APIG网关
196 |   getApigGateway: (data: any) => {
197 |     return api.get(`/gateways/apig`, { params: {
198 |       ...data,
199 |     } })
200 |   },
201 |   // 获取ADP网关
202 |   getAdpGateways: (data: any) => {
203 |     return api.post(`/gateways/adp`, data)
204 |   },
205 |   // 删除网关
206 |   deleteGateway: (gatewayId: string) => {
207 |     return api.delete(`/gateways/${gatewayId}`)
208 |   },
209 |   // 导入网关
210 |   importGateway: (data: any) => {
211 |     return api.post(`/gateways`, { ...data })
212 |   },
213 |   // 获取网关的REST API列表
214 |   getGatewayRestApis: (gatewayId: string, data: any) => {
215 |     return api.get(`/gateways/${gatewayId}/rest-apis`, {
216 |       params: data
217 |     })
218 |   },
219 |   // 获取网关的MCP Server列表
220 |   getGatewayMcpServers: (gatewayId: string, data: any) => {
221 |     return api.get(`/gateways/${gatewayId}/mcp-servers`, {
222 |       params: data
223 |     })
224 |   },
225 |   // 获取网关的Dashboard URL
226 |   getDashboard: (gatewayId: string) => {
227 |     return api.get(`/gateways/${gatewayId}/dashboard`)
228 |   }
229 | } 
230 | 
231 | export const nacosApi = {
232 |   getNacos: (params?: any) => {
233 |     return api.get(`/nacos`, { params })
234 |   },
235 |   // 从阿里云 MSE 获取 Nacos 集群列表
236 |   getMseNacos: (params: { regionId: string; accessKey: string; secretKey: string; page?: number; size?: number }) => {
237 |     return api.get(`/nacos/mse`, { params })
238 |   },
239 |   createNacos: (data: any) => {
240 |     return api.post(`/nacos`, data)
241 |   },
242 |   deleteNacos: (nacosId: string) => {
243 |     return api.delete(`/nacos/${nacosId}`)
244 |   },
245 |   updateNacos: (nacosId: string, data: any) => {
246 |     return api.put(`/nacos/${nacosId}`, data)
247 |   },
248 |   getNacosMcpServers: (nacosId: string, data: any) => {
249 |     return api.get(`/nacos/${nacosId}/mcp-servers`, {
250 |       params: data
251 |     })
252 |   },
253 |   // 获取指定 Nacos 实例的命名空间列表
254 |   getNamespaces: (nacosId: string, params?: { page?: number; size?: number }) => {
255 |     return api.get(`/nacos/${nacosId}/namespaces`, { params })
256 |   }
257 | }
```

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

```typescript
  1 | import { useState, useEffect } from 'react'
  2 | import { Card, Table, Modal, Form, Button, Space, Select, message, Checkbox } from 'antd'
  3 | import { EyeOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
  4 | import { Portal, ApiProduct } from '@/types'
  5 | import { apiProductApi } from '@/lib/api'
  6 | import { useNavigate } from 'react-router-dom'
  7 | import { ProductTypeMap } from '@/lib/utils'
  8 | 
  9 | interface PortalApiProductsProps {
 10 |   portal: Portal
 11 | }
 12 | 
 13 | export function PortalPublishedApis({ portal }: PortalApiProductsProps) {
 14 |   const navigate = useNavigate()
 15 |   const [apiProducts, setApiProducts] = useState<ApiProduct[]>([])
 16 |   const [apiProductsOptions, setApiProductsOptions] = useState<ApiProduct[]>([])
 17 |   const [isModalVisible, setIsModalVisible] = useState(false)
 18 |   const [selectedApiIds, setSelectedApiIds] = useState<string[]>([])
 19 |   const [loading, setLoading] = useState(false)
 20 |   const [modalLoading, setModalLoading] = useState(false)
 21 |   
 22 |   // 分页状态
 23 |   const [currentPage, setCurrentPage] = useState(1)
 24 |   const [pageSize, setPageSize] = useState(10)
 25 |   const [total, setTotal] = useState(0)
 26 | 
 27 |   const [form] = Form.useForm()
 28 |   
 29 |   useEffect(() => {
 30 |     if (portal.portalId) {
 31 |       fetchApiProducts()
 32 |     }
 33 |   }, [portal.portalId, currentPage, pageSize])
 34 | 
 35 |   const fetchApiProducts = () => {
 36 |     setLoading(true)
 37 |     apiProductApi.getApiProducts({
 38 |       portalId: portal.portalId,
 39 |       page: currentPage,
 40 |       size: pageSize
 41 |     }).then((res) => {
 42 |       setApiProducts(res.data.content)
 43 |       setTotal(res.data.totalElements || 0)
 44 |     }).finally(() => {
 45 |       setLoading(false)
 46 |     })
 47 |   }
 48 | 
 49 |   useEffect(() => {
 50 |     if (isModalVisible) {
 51 |       setModalLoading(true)
 52 |       apiProductApi.getApiProducts({
 53 |         page: 1,
 54 |         size: 500, // 获取所有可用的API
 55 |         status: 'READY'
 56 |       }).then((res) => {
 57 |         // 过滤掉已发布在该门户里的api
 58 |         setApiProductsOptions(res.data.content.filter((api: ApiProduct) => 
 59 |           !apiProducts.some((a: ApiProduct) => a.productId === api.productId)
 60 |         ))
 61 |       }).finally(() => {
 62 |         setModalLoading(false)
 63 |       })
 64 |     }
 65 |   }, [isModalVisible]) // 移除apiProducts依赖,避免重复请求
 66 | 
 67 |   const handlePageChange = (page: number, size?: number) => {
 68 |     setCurrentPage(page)
 69 |     if (size) {
 70 |       setPageSize(size)
 71 |     }
 72 |   }
 73 | 
 74 |   const columns = [
 75 |     {
 76 |       title: '名称/ID',
 77 |       key: 'nameAndId',
 78 |       width: 280,
 79 |       render: (_: any, record: ApiProduct) => (
 80 |         <div>
 81 |           <div className="text-sm font-medium text-gray-900 truncate">{record.name}</div>
 82 |           <div className="text-xs text-gray-500 truncate">{record.productId}</div>
 83 |         </div>
 84 |       ),
 85 |     },
 86 |     {
 87 |       title: '类型',
 88 |       dataIndex: 'type',
 89 |       key: 'type',
 90 |       width: 120,
 91 |       render: (text: string) => ProductTypeMap[text] || text
 92 |     },
 93 |     {
 94 |       title: '描述',
 95 |       dataIndex: 'description',
 96 |       key: 'description',
 97 |       width: 400,
 98 |     },
 99 |     // {
100 |     //   title: '分类',
101 |     //   dataIndex: 'category',
102 |     //   key: 'category',
103 |     // },
104 |     {
105 |       title: '操作',
106 |       key: 'action',
107 |       width: 180,
108 |       render: (_: any, record: ApiProduct) => (
109 |         <Space size="middle">
110 |           <Button
111 |             onClick={() => {
112 |               navigate(`/api-products/detail?productId=${record.productId}`)
113 |             }}
114 |             type="link" icon={<EyeOutlined />}>
115 |             查看
116 |           </Button>
117 |           
118 |           <Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.productId, record.name)}>
119 |             移除
120 |           </Button>
121 |         </Space>
122 |       ),
123 |     },
124 |   ]
125 | 
126 |   const modalColumns = [
127 |     {
128 |       title: '选择',
129 |       dataIndex: 'select',
130 |       key: 'select',
131 |       width: 60,
132 |       render: (_: any, record: ApiProduct) => (
133 |         <Checkbox
134 |           checked={selectedApiIds.includes(record.productId)}
135 |           onChange={(e) => {
136 |             if (e.target.checked) {
137 |               setSelectedApiIds([...selectedApiIds, record.productId])
138 |             } else {
139 |               setSelectedApiIds(selectedApiIds.filter(id => id !== record.productId))
140 |             }
141 |           }}
142 |         />
143 |       ),
144 |     },
145 |     {
146 |       title: '名称',
147 |       dataIndex: 'name',
148 |       key: 'name',
149 |       width: 320,
150 |       render: (_: any, record: ApiProduct) => (
151 |         <div>
152 |           <div className="text-sm font-medium text-gray-900 truncate">
153 |             {record.name}
154 |           </div>
155 |           <div className="text-xs text-gray-500 truncate">
156 |             {record.productId}
157 |           </div>
158 |         </div>
159 |       ),
160 |     },
161 |     {
162 |       title: '类型',
163 |       dataIndex: 'type',
164 |       key: 'type',
165 |       width: 100,
166 |       render: (type: string) => ProductTypeMap[type] || type,
167 |     },
168 |     {
169 |       title: '描述',
170 |       dataIndex: 'description',
171 |       key: 'description',
172 |       width: 300,
173 |     },
174 |   ]
175 | 
176 |   const handleDelete = (productId: string, productName: string) => {
177 |     Modal.confirm({
178 |       title: '确认移除',
179 |       icon: <ExclamationCircleOutlined />,
180 |       content: `确定要从门户中移除API产品 "${productName}" 吗?此操作不可恢复。`,
181 |       okText: '确认移除',
182 |       okType: 'danger',
183 |       cancelText: '取消',
184 |       onOk() {
185 |         apiProductApi.cancelPublishToPortal(productId, portal.portalId).then((res) => {
186 |           message.success('移除成功')
187 |           fetchApiProducts()
188 |           setIsModalVisible(false)
189 |         }).catch((error) => {
190 |           // message.error('移除失败')
191 |         })
192 |       },
193 |     })
194 |   }
195 | 
196 |   const handlePublish = async () => {
197 |     if (selectedApiIds.length === 0) {
198 |       message.warning('请至少选择一个API')
199 |       return
200 |     }
201 | 
202 |     setModalLoading(true)
203 |     try {
204 |       // 批量发布选中的API
205 |       for (const productId of selectedApiIds) {
206 |         await apiProductApi.publishToPortal(productId, portal.portalId)
207 |       }
208 |       message.success(`成功发布 ${selectedApiIds.length} 个API`)
209 |       setSelectedApiIds([])
210 |       fetchApiProducts()
211 |       setIsModalVisible(false)
212 |     } catch (error) {
213 |       // message.error('发布失败')
214 |     } finally {
215 |       setModalLoading(false)
216 |     }
217 |   }
218 | 
219 |   const handleModalCancel = () => {
220 |     setIsModalVisible(false)
221 |     setSelectedApiIds([])
222 |   }
223 | 
224 |   return (
225 |     <div className="p-6 space-y-6">
226 |       <div className="flex justify-between items-center">
227 |         <div>
228 |           <h1 className="text-2xl font-bold mb-2">API Product</h1>
229 |           <p className="text-gray-600">管理在此Portal中发布的API产品</p>
230 |         </div>
231 |         <Button type="primary" onClick={() => setIsModalVisible(true)}>
232 |           发布新API
233 |         </Button>
234 |       </div>
235 | 
236 |       <Card>
237 |         <Table 
238 |           columns={columns} 
239 |           dataSource={apiProducts}
240 |           rowKey="productId"
241 |           loading={loading}
242 |           pagination={{
243 |             current: currentPage,
244 |             pageSize: pageSize,
245 |             total: total,
246 |             showSizeChanger: true,
247 |             showQuickJumper: true,
248 |             showTotal: (total) => `共 ${total} 条`,
249 |             onChange: handlePageChange,
250 |             onShowSizeChange: handlePageChange,
251 |           }}
252 |         />
253 |       </Card>
254 | 
255 |       <Modal
256 |         title="发布API产品"
257 |         open={isModalVisible}
258 |         onOk={handlePublish}
259 |         onCancel={handleModalCancel}
260 |         okText="发布"
261 |         cancelText="取消"
262 |         width={800}
263 |         confirmLoading={modalLoading}
264 |       >
265 |         <Table
266 |           columns={modalColumns}
267 |           dataSource={apiProductsOptions}
268 |           rowKey="productId"
269 |           loading={modalLoading}
270 |           pagination={false}
271 |           scroll={{ y: 400 }}
272 |         />
273 |       </Modal>
274 |     </div>
275 |   )
276 | } 
```

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

```typescript
  1 | import { useEffect, useState } from "react";
  2 | import { Card, Tag, Typography, Input, Avatar, Skeleton } from "antd";
  3 | import { Link } from "react-router-dom";
  4 | import { Layout } from "../components/Layout";
  5 | import api from "../lib/api";
  6 | import { ProductStatus } from "../types";
  7 | import type { Product, ApiResponse, PaginatedResponse, ProductIcon } from "../types";
  8 | // import { getCategoryText, getCategoryColor } from "../lib/statusUtils";
  9 | import './Test.css';
 10 | 
 11 | const { Title, Paragraph } = Typography;
 12 | const { Search } = Input;
 13 | 
 14 | 
 15 | 
 16 | interface ApiProductListItem {
 17 |   key: string;
 18 |   name: string;
 19 |   description: string;
 20 |   status: string;
 21 |   version: string;
 22 |   endpoints: number;
 23 |   category: string;
 24 |   creator: string;
 25 |   icon?: ProductIcon;
 26 |   updatedAt: string;
 27 | }
 28 | 
 29 | function APIsPage() {
 30 |   const [loading, setLoading] = useState(false);
 31 |   const [apiProducts, setApiProducts] = useState<ApiProductListItem[]>([]);
 32 |   const [searchText, setSearchText] = useState('');
 33 | 
 34 |   useEffect(() => {
 35 |     fetchApiProducts();
 36 |   }, []);
 37 | 
 38 |   // 处理产品图标的函数
 39 |   const getIconUrl = (icon?: ProductIcon | null): string => {
 40 |     const fallback = "/logo.svg";
 41 |     
 42 |     if (!icon) {
 43 |       return fallback;
 44 |     }
 45 |     
 46 |     switch (icon.type) {
 47 |       case "URL":
 48 |         return icon.value || fallback;
 49 |       case "BASE64":
 50 |         // 如果value已经包含data URL前缀,直接使用;否则添加前缀
 51 |         return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
 52 |       default:
 53 |         return fallback;
 54 |     }
 55 |   };
 56 |   const fetchApiProducts = async () => {
 57 |     setLoading(true);
 58 |     try {
 59 |       const response: ApiResponse<PaginatedResponse<Product>> = await api.get("/products?type=REST_API&page=0&size=100");
 60 |       if (response.code === "SUCCESS" && response.data) {
 61 |         // 移除重复过滤,简化数据映射
 62 |         const mapped = response.data.content.map((item: Product) => ({
 63 |           key: item.productId,
 64 |           name: item.name,
 65 |           description: item.description,
 66 |           status: item.status === ProductStatus.ENABLE ? 'active' : 'inactive',
 67 |           version: 'v1.0.0',
 68 |           endpoints: 0,
 69 |           category: item.category,
 70 |           creator: 'Unknown',
 71 |           icon: item.icon || undefined,
 72 |           updatedAt: item.updatedAt?.slice(0, 10) || ''
 73 |         }));
 74 |         setApiProducts(mapped);
 75 |       }
 76 |     } catch (error) {
 77 |       console.error('获取API产品列表失败:', error);
 78 |     } finally {
 79 |       setLoading(false);
 80 |     }
 81 |   };
 82 | 
 83 | 
 84 | 
 85 |   const filteredApiProducts = apiProducts.filter(product => {
 86 |     return product.name.toLowerCase().includes(searchText.toLowerCase()) ||
 87 |            product.description.toLowerCase().includes(searchText.toLowerCase()) ||
 88 |            product.creator.toLowerCase().includes(searchText.toLowerCase());
 89 |   });
 90 | 
 91 |   const getApiIcon = (name: string) => {
 92 |     // Generate initials for API icon
 93 |     const words = name.split(' ');
 94 |     if (words.length >= 2) {
 95 |       return words[0][0] + words[1][0];
 96 |     }
 97 |     return name.substring(0, 2).toUpperCase();
 98 |   };
 99 | 
100 |   const getApiIconColor = (name: string) => {
101 |     const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
102 |     const index = name.charCodeAt(0) % colors.length;
103 |     return colors[index];
104 |   };
105 | 
106 |   return (
107 |     <Layout>
108 |       {/* Header Section */}
109 |       <div className="text-center mb-8">
110 |         <Title level={1} className="mb-4">
111 |           API 市场
112 |         </Title>
113 |         <Paragraph className="text-gray-600 text-lg max-w-4xl mx-auto text-flow text-flow-grey slow">
114 |           支持私有化部署,具备更多管理能力,支持自动注册、智能路由的API市场
115 |         </Paragraph>
116 |       </div>
117 | 
118 |       {/* Search Section */}
119 |       <div className="flex justify-center mb-8">
120 |         <div className="relative w-full max-w-2xl">
121 |           <Search
122 |             placeholder="请输入内容"
123 |             size="large"
124 |             value={searchText}
125 |             onChange={(e) => setSearchText(e.target.value)}
126 |             className="rounded-lg shadow-lg"
127 |           />
128 |         </div>
129 |       </div>
130 | 
131 |       {/* APIs Section */}
132 |       <div className="mb-6">
133 |         <Title level={3} className="mb-4">
134 |           热门/推荐 APIs: {filteredApiProducts.length}
135 |         </Title>
136 |       </div>
137 | 
138 |       {/* APIs Grid */}
139 |       {loading ? (
140 |         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
141 |           {Array.from({ length: 6 }).map((_, index) => (
142 |             <Card key={index} className="h-full rounded-lg shadow-lg">
143 |               <Skeleton loading active>
144 |                 <div className="flex items-start space-x-4">
145 |                   <Skeleton.Avatar size={48} active />
146 |                   <div className="flex-1 min-w-0">
147 |                     <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
148 |                     <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
149 |                     <Skeleton.Input active size="small" style={{ width: '60%' }} />
150 |                   </div>
151 |                 </div>
152 |               </Skeleton>
153 |             </Card>
154 |           ))}
155 |         </div>
156 |       ) : (
157 |         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
158 |           {filteredApiProducts.map((product) => (
159 |           <Link key={product.key} to={`/apis/${product.key}`} className="block">
160 |             <Card
161 |               hoverable
162 |               className="h-full transition-all duration-200 hover:shadow-lg cursor-pointer rounded-lg shadow-lg"
163 |             >
164 |               <div className="flex items-start space-x-4">
165 |                 {/* API Icon */}
166 |                 <Avatar
167 |                   size={48}
168 |                   src={product.icon ? getIconUrl(product.icon) : undefined}
169 |                   style={{ 
170 |                     backgroundColor: getApiIconColor(product.name),
171 |                     fontSize: '18px',
172 |                     fontWeight: 'bold'
173 |                   }}
174 |                 >
175 |                   {!product.icon && getApiIcon(product.name)}
176 |                 </Avatar>
177 | 
178 |                 {/* API Info */}
179 |                 <div className="flex-1 min-w-0">
180 |                   <div className="flex items-center justify-between mb-2">
181 |                     <Title level={5} className="mb-0 truncate">
182 |                       {product.name}
183 |                     </Title>
184 |                     <Tag className="text-xs text-green-700 border-0 bg-transparent px-0">
185 |                       REST
186 |                     </Tag>
187 |                   </div>
188 | 
189 |                   {/* <div className="text-sm text-gray-500 mb-2">
190 |                     创建者: {product.creator}
191 |                   </div> */}
192 | 
193 |                   <Paragraph className="text-sm text-gray-600 mb-3 line-clamp-2">
194 |                     {product.description}
195 |                   </Paragraph>
196 | 
197 |                   <div className="flex items-center justify-between">
198 |                     {/* <Tag color={getCategoryColor(product.category)} className="">
199 |                       {getCategoryText(product.category)}
200 |                     </Tag> */}
201 |                     <div className="text-xs text-gray-400">
202 |                       更新 {product.updatedAt}
203 |                     </div>
204 |                   </div>
205 |                 </div>
206 |               </div>
207 |             </Card>
208 |           </Link>
209 |         ))}
210 |       </div>
211 |       )}
212 | 
213 |       {/* Empty State */}
214 |       {filteredApiProducts.length === 0 && (
215 |         <div className="text-center py-8">
216 |           <div className="text-gray-500">暂无API产品</div>
217 |         </div>
218 |       )}
219 |     </Layout>
220 |   );
221 | }
222 | 
223 | export default APIsPage; 
```

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

```typescript
  1 | import { Card, Button, Table, Tag, Space, Modal, Form, Input, Select, Switch, message } from 'antd'
  2 | import { PlusOutlined, EditOutlined, DeleteOutlined, SettingOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
  3 | import { useState } from 'react'
  4 | import type { ApiProduct } from '@/types/api-product';
  5 | import { formatDateTime } from '@/lib/utils'
  6 | 
  7 | interface ApiProductPolicyProps {
  8 |   apiProduct: ApiProduct
  9 | }
 10 | 
 11 | interface Policy {
 12 |   id: string
 13 |   name: string
 14 |   type: string
 15 |   status: string
 16 |   description: string
 17 |   createdAt: string
 18 |   config: any
 19 | }
 20 | 
 21 | const mockPolicies: Policy[] = [
 22 |   {
 23 |     id: "1",
 24 |     name: "Rate Limiting",
 25 |     type: "rate-limiting",
 26 |     status: "enabled",
 27 |     description: "限制API调用频率",
 28 |     createdAt: "2025-01-01T10:00:00Z",
 29 |     config: {
 30 |       minute: 100,
 31 |       hour: 1000
 32 |     }
 33 |   },
 34 |   {
 35 |     id: "2",
 36 |     name: "Authentication",
 37 |     type: "key-auth",
 38 |     status: "enabled",
 39 |     description: "API密钥认证",
 40 |     createdAt: "2025-01-02T11:00:00Z",
 41 |     config: {
 42 |       key_names: ["apikey"],
 43 |       hide_credentials: true
 44 |     }
 45 |   },
 46 |   {
 47 |     id: "3",
 48 |     name: "CORS",
 49 |     type: "cors",
 50 |     status: "enabled",
 51 |     description: "跨域资源共享",
 52 |     createdAt: "2025-01-03T12:00:00Z",
 53 |     config: {
 54 |       origins: ["*"],
 55 |       methods: ["GET", "POST", "PUT", "DELETE"]
 56 |     }
 57 |   }
 58 | ]
 59 | 
 60 | export function ApiProductPolicy({ apiProduct }: ApiProductPolicyProps) {
 61 |   const [policies, setPolicies] = useState<Policy[]>(mockPolicies)
 62 |   const [isModalVisible, setIsModalVisible] = useState(false)
 63 |   const [editingPolicy, setEditingPolicy] = useState<Policy | null>(null)
 64 |   const [form] = Form.useForm()
 65 | 
 66 |   const columns = [
 67 |     {
 68 |       title: '策略名称',
 69 |       dataIndex: 'name',
 70 |       key: 'name',
 71 |     },
 72 |     {
 73 |       title: '类型',
 74 |       dataIndex: 'type',
 75 |       key: 'type',
 76 |       render: (type: string) => {
 77 |         const typeMap: { [key: string]: string } = {
 78 |           'rate-limiting': '限流',
 79 |           'key-auth': '认证',
 80 |           'cors': 'CORS',
 81 |           'acl': '访问控制'
 82 |         }
 83 |         return <Tag color="blue">{typeMap[type] || type}</Tag>
 84 |       }
 85 |     },
 86 |     {
 87 |       title: '状态',
 88 |       dataIndex: 'status',
 89 |       key: 'status',
 90 |       render: (status: string) => (
 91 |         <Tag color={status === 'enabled' ? 'green' : 'red'}>
 92 |           {status === 'enabled' ? '启用' : '禁用'}
 93 |         </Tag>
 94 |       )
 95 |     },
 96 |     {
 97 |       title: '描述',
 98 |       dataIndex: 'description',
 99 |       key: 'description',
100 |       ellipsis: true,
101 |     },
102 |     {
103 |       title: '创建时间',
104 |       dataIndex: 'createdAt',
105 |       key: 'createdAt',
106 |       render: (date: string) => formatDateTime(date)
107 |     },
108 |     {
109 |       title: '操作',
110 |       key: 'action',
111 |       render: (_: any, record: Policy) => (
112 |         <Space size="middle">
113 |           <Button type="link" icon={<SettingOutlined />}>
114 |             配置
115 |           </Button>
116 |           <Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
117 |             编辑
118 |           </Button>
119 |           <Button 
120 |             type="link" 
121 |             danger 
122 |             icon={<DeleteOutlined />}
123 |             onClick={() => handleDelete(record.id, record.name)}
124 |           >
125 |             删除
126 |           </Button>
127 |         </Space>
128 |       ),
129 |     },
130 |   ]
131 | 
132 |   const handleAdd = () => {
133 |     setEditingPolicy(null)
134 |     setIsModalVisible(true)
135 |   }
136 | 
137 |   const handleEdit = (policy: Policy) => {
138 |     setEditingPolicy(policy)
139 |     form.setFieldsValue({
140 |       name: policy.name,
141 |       type: policy.type,
142 |       description: policy.description,
143 |       status: policy.status
144 |     })
145 |     setIsModalVisible(true)
146 |   }
147 | 
148 |   const handleDelete = (id: string, policyName: string) => {
149 |     Modal.confirm({
150 |       title: '确认删除',
151 |       icon: <ExclamationCircleOutlined />,
152 |       content: `确定要删除策略 "${policyName}" 吗?此操作不可恢复。`,
153 |       okText: '确认删除',
154 |       okType: 'danger',
155 |       cancelText: '取消',
156 |       onOk() {
157 |         setPolicies(policies.filter(policy => policy.id !== id))
158 |         message.success('策略删除成功')
159 |       },
160 |     })
161 |   }
162 | 
163 |   const handleModalOk = () => {
164 |     form.validateFields().then((values) => {
165 |       if (editingPolicy) {
166 |         // 编辑现有策略
167 |         setPolicies(policies.map(policy => 
168 |           policy.id === editingPolicy.id 
169 |             ? { ...policy, ...values }
170 |             : policy
171 |         ))
172 |       } else {
173 |         // 添加新策略
174 |         const newPolicy: Policy = {
175 |           id: Date.now().toString(),
176 |           name: values.name,
177 |           type: values.type,
178 |           status: values.status,
179 |           description: values.description,
180 |           createdAt: new Date().toISOString(),
181 |           config: {}
182 |         }
183 |         setPolicies([...policies, newPolicy])
184 |       }
185 |       setIsModalVisible(false)
186 |       form.resetFields()
187 |       setEditingPolicy(null)
188 |     })
189 |   }
190 | 
191 |   const handleModalCancel = () => {
192 |     setIsModalVisible(false)
193 |     form.resetFields()
194 |     setEditingPolicy(null)
195 |   }
196 | 
197 |   return (
198 |     <div className="p-6 space-y-6">
199 |       <div className="flex justify-between items-center">
200 |         <div>
201 |           <h1 className="text-2xl font-bold mb-2">策略管理</h1>
202 |           <p className="text-gray-600">管理API产品的策略配置</p>
203 |         </div>
204 |         <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
205 |           添加策略
206 |         </Button>
207 |       </div>
208 | 
209 |       <Card>
210 |         <Table 
211 |           columns={columns} 
212 |           dataSource={policies}
213 |           rowKey="id"
214 |           pagination={false}
215 |         />
216 |       </Card>
217 | 
218 |       <Card title="策略设置">
219 |         <div className="space-y-4">
220 |           <div className="flex justify-between items-center">
221 |             <span>策略继承</span>
222 |             <Switch defaultChecked />
223 |           </div>
224 |           <div className="flex justify-between items-center">
225 |             <span>策略优先级</span>
226 |             <Switch defaultChecked />
227 |           </div>
228 |           <div className="flex justify-between items-center">
229 |             <span>策略日志</span>
230 |             <Switch defaultChecked />
231 |           </div>
232 |         </div>
233 |       </Card>
234 | 
235 |       <Modal
236 |         title={editingPolicy ? "编辑策略" : "添加策略"}
237 |         open={isModalVisible}
238 |         onOk={handleModalOk}
239 |         onCancel={handleModalCancel}
240 |         okText={editingPolicy ? "更新" : "添加"}
241 |         cancelText="取消"
242 |       >
243 |         <Form form={form} layout="vertical">
244 |           <Form.Item
245 |             name="name"
246 |             label="策略名称"
247 |             rules={[{ required: true, message: '请输入策略名称' }]}
248 |           >
249 |             <Input placeholder="请输入策略名称" />
250 |           </Form.Item>
251 |           <Form.Item
252 |             name="type"
253 |             label="策略类型"
254 |             rules={[{ required: true, message: '请选择策略类型' }]}
255 |           >
256 |             <Select placeholder="请选择策略类型">
257 |               <Select.Option value="rate-limiting">限流</Select.Option>
258 |               <Select.Option value="key-auth">认证</Select.Option>
259 |               <Select.Option value="cors">CORS</Select.Option>
260 |               <Select.Option value="acl">访问控制</Select.Option>
261 |             </Select>
262 |           </Form.Item>
263 |           <Form.Item
264 |             name="description"
265 |             label="描述"
266 |             rules={[{ required: true, message: '请输入策略描述' }]}
267 |           >
268 |             <Input.TextArea placeholder="请输入策略描述" rows={3} />
269 |           </Form.Item>
270 |           <Form.Item
271 |             name="status"
272 |             label="状态"
273 |             rules={[{ required: true, message: '请选择状态' }]}
274 |           >
275 |             <Select placeholder="请选择状态">
276 |               <Select.Option value="enabled">启用</Select.Option>
277 |               <Select.Option value="disabled">禁用</Select.Option>
278 |             </Select>
279 |           </Form.Item>
280 |         </Form>
281 |       </Modal>
282 |     </div>
283 |   )
284 | } 
```

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

```typescript
  1 | import { useState, useEffect } from 'react'
  2 | import { useNavigate, useSearchParams } from 'react-router-dom'
  3 | import { Button, Dropdown, MenuProps, Modal, message } from 'antd'
  4 | import {
  5 |   MoreOutlined,
  6 |   LeftOutlined,
  7 |   EyeOutlined,
  8 |   LinkOutlined,
  9 |   BookOutlined,
 10 |   GlobalOutlined,
 11 |   DashboardOutlined
 12 | } from '@ant-design/icons'
 13 | import { ApiProductOverview } from '@/components/api-product/ApiProductOverview'
 14 | import { ApiProductLinkApi } from '@/components/api-product/ApiProductLinkApi'
 15 | import { ApiProductUsageGuide } from '@/components/api-product/ApiProductUsageGuide'
 16 | import { ApiProductPortal } from '@/components/api-product/ApiProductPortal'
 17 | import { ApiProductDashboard } from '@/components/api-product/ApiProductDashboard'
 18 | import { apiProductApi } from '@/lib/api';
 19 | import type { ApiProduct, LinkedService } from '@/types/api-product';
 20 | 
 21 | import ApiProductFormModal from '@/components/api-product/ApiProductFormModal';
 22 | 
 23 | const menuItems = [
 24 |   {
 25 |     key: "overview",
 26 |     label: "Overview",
 27 |     description: "产品概览",
 28 |     icon: EyeOutlined
 29 |   },
 30 |   {
 31 |     key: "link-api",
 32 |     label: "Link API",
 33 |     description: "API关联",
 34 |     icon: LinkOutlined
 35 |   },
 36 |   {
 37 |     key: "usage-guide",
 38 |     label: "Usage Guide",
 39 |     description: "使用指南",
 40 |     icon: BookOutlined
 41 |   },
 42 |   {
 43 |     key: "portal",
 44 |     label: "Portal",
 45 |     description: "发布的门户",
 46 |     icon: GlobalOutlined
 47 |   },
 48 |   {
 49 |     key: "dashboard",
 50 |     label: "Dashboard",
 51 |     description: "实时监控和统计",
 52 |     icon: DashboardOutlined
 53 |   }
 54 | ]
 55 | 
 56 | export default function ApiProductDetail() {
 57 |   const navigate = useNavigate()
 58 |   const [searchParams, setSearchParams] = useSearchParams()
 59 |   const [apiProduct, setApiProduct] = useState<ApiProduct | null>(null)
 60 |   const [linkedService, setLinkedService] = useState<LinkedService | null>(null)
 61 |   const [loading, setLoading] = useState(true) // 添加 loading 状态
 62 |   
 63 |   // 从URL query参数获取当前tab,默认为overview
 64 |   const currentTab = searchParams.get('tab') || 'overview'
 65 |   // 验证tab值是否有效,如果无效则使用默认值
 66 |   const validTab = menuItems.some(item => item.key === currentTab) ? currentTab : 'overview'
 67 |   const [activeTab, setActiveTab] = useState(validTab)
 68 | 
 69 |   const [editModalVisible, setEditModalVisible] = useState(false)
 70 | 
 71 |   
 72 |   const fetchApiProduct = async () => {
 73 |     const productId = searchParams.get('productId')
 74 |     if (productId) {
 75 |       setLoading(true)
 76 |       try {
 77 |         // 并行获取Product详情和关联信息
 78 |         const [productRes, refRes] = await Promise.all([
 79 |           apiProductApi.getApiProductDetail(productId),
 80 |           apiProductApi.getApiProductRef(productId).catch(() => ({ data: null })) // 关联信息获取失败不影响页面显示
 81 |         ])
 82 |         
 83 |         setApiProduct(productRes.data)
 84 |         setLinkedService(refRes.data || null)
 85 |       } catch (error) {
 86 |         console.error('获取Product详情失败:', error)
 87 |       } finally {
 88 |         setLoading(false)
 89 |       }
 90 |     }
 91 |   }
 92 | 
 93 |   // 更新关联信息的回调函数
 94 |   const handleLinkedServiceUpdate = (newLinkedService: LinkedService | null) => {
 95 |     setLinkedService(newLinkedService)
 96 |   }
 97 | 
 98 |   useEffect(() => {
 99 |     fetchApiProduct()
100 |   }, [searchParams.get('productId')])
101 | 
102 |   // 同步URL参数和activeTab状态
103 |   useEffect(() => {
104 |     setActiveTab(validTab)
105 |   }, [validTab, searchParams])
106 | 
107 |   const handleBackToApiProducts = () => {
108 |     navigate('/api-products')
109 |   }
110 | 
111 |   const handleTabChange = (tabKey: string) => {
112 |     setActiveTab(tabKey)
113 |     // 更新URL query参数
114 |     const newSearchParams = new URLSearchParams(searchParams)
115 |     newSearchParams.set('tab', tabKey)
116 |     setSearchParams(newSearchParams)
117 |   }
118 | 
119 |   const renderContent = () => {
120 |     if (!apiProduct) {
121 |       return <div className="p-6">Loading...</div>
122 |     }
123 |     
124 |     switch (activeTab) {
125 |       case "overview":
126 |         return <ApiProductOverview apiProduct={apiProduct} linkedService={linkedService} onEdit={handleEdit} />
127 |       case "link-api":
128 |         return <ApiProductLinkApi 
129 |           apiProduct={apiProduct} 
130 |           linkedService={linkedService}
131 |           onLinkedServiceUpdate={handleLinkedServiceUpdate}
132 |           handleRefresh={fetchApiProduct}
133 |         />
134 |       case "usage-guide":
135 |         return <ApiProductUsageGuide apiProduct={apiProduct} handleRefresh={fetchApiProduct} />
136 |       case "portal":
137 |         return <ApiProductPortal apiProduct={apiProduct} />
138 |       case "dashboard":
139 |         return <ApiProductDashboard apiProduct={apiProduct} />
140 |       default:
141 |         return <ApiProductOverview apiProduct={apiProduct} linkedService={linkedService} onEdit={handleEdit} />
142 |     }
143 |   }
144 | 
145 |   const dropdownItems: MenuProps['items'] = [
146 |     {
147 |       key: 'delete',
148 |       label: '删除',
149 |       onClick: () => {
150 |         Modal.confirm({
151 |           title: '确认删除',
152 |           content: '确定要删除该产品吗?',
153 |           onOk: () => {
154 |             handleDeleteApiProduct()
155 |           },
156 |         })
157 |       },
158 |       danger: true,
159 |     },
160 |   ]
161 | 
162 |   const handleDeleteApiProduct = () => {
163 |     if (!apiProduct) return;
164 |     
165 |     apiProductApi.deleteApiProduct(apiProduct.productId).then(() => {
166 |       message.success('删除成功')
167 |       navigate('/api-products')
168 |     }).catch((error) => {
169 |       // message.error(error.response?.data?.message || '删除失败')
170 |     })
171 |   }
172 | 
173 |   const handleEdit = () => {
174 |     setEditModalVisible(true)
175 |   }
176 | 
177 |   const handleEditSuccess = () => {
178 |     setEditModalVisible(false)
179 |     fetchApiProduct()
180 |   }
181 | 
182 |   const handleEditCancel = () => {
183 |     setEditModalVisible(false)
184 |   }
185 | 
186 |   return (
187 |     <div className="flex h-full w-full overflow-hidden">
188 |       {/* API Product 详情侧边栏 */}
189 |       <div className="w-64 border-r bg-white flex flex-col flex-shrink-0">
190 |         {/* 返回按钮 */}
191 |         <div className="pb-4 border-b">
192 |           <Button
193 |             type="text"
194 |             // className="w-full justify-start"
195 |             onClick={handleBackToApiProducts}
196 |             icon={<LeftOutlined />}
197 |           >
198 |             返回
199 |           </Button>
200 |         </div>
201 | 
202 |         {/* API Product 信息 */}
203 |         <div className="p-4 border-b">
204 |           <div className="flex items-center justify-between mb-2">
205 |             <h2 className="text-lg font-semibold">{apiProduct?.name || 'Loading...'}</h2>
206 |             <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
207 |               <Button type="text" icon={<MoreOutlined />} />
208 |             </Dropdown>
209 |           </div>
210 |         </div>
211 | 
212 |         {/* 导航菜单 */}
213 |         <nav className="flex-1 p-4 space-y-1">
214 |           {menuItems.map((item) => {
215 |             const Icon = item.icon;
216 |             return (
217 |               <button
218 |                 key={item.key}
219 |                 onClick={() => handleTabChange(item.key)}
220 |                 className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors ${
221 |                   activeTab === item.key
222 |                     ? "bg-blue-500 text-white"
223 |                     : "hover:bg-gray-100"
224 |                 }`}
225 |               >
226 |                 <Icon className="h-4 w-4 flex-shrink-0" />
227 |                 <div>
228 |                   <div className="font-medium">{item.label}</div>
229 |                   <div className="text-xs opacity-70">{item.description}</div>
230 |                 </div>
231 |               </button>
232 |             );
233 |           })}
234 |         </nav>
235 |       </div>
236 | 
237 |       {/* 主内容区域 */}
238 |       <div className="flex-1 overflow-auto min-w-0">
239 |         <div className="w-full max-w-full">
240 |           {renderContent()}
241 |         </div>
242 |       </div>
243 | 
244 |       {apiProduct && (
245 |         <ApiProductFormModal
246 |           visible={editModalVisible}
247 |           onCancel={handleEditCancel}
248 |           onSuccess={handleEditSuccess}
249 |           productId={apiProduct.productId}
250 |           initialData={apiProduct}
251 |         />
252 |       )}
253 |     </div>
254 |   )
255 | }
256 | 
```

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

```typescript
  1 | import { useState, useEffect } from 'react'
  2 | import { useNavigate, useSearchParams } from 'react-router-dom'
  3 | import { Button, Dropdown, MenuProps, Typography, Spin, Modal, message } from 'antd'
  4 | import {
  5 |   MoreOutlined,
  6 |   LeftOutlined,
  7 |   EyeOutlined,
  8 |   ApiOutlined,
  9 |   TeamOutlined,
 10 |   SafetyOutlined,
 11 |   CloudOutlined,
 12 |   DashboardOutlined
 13 | } from '@ant-design/icons'
 14 | import { PortalOverview } from '@/components/portal/PortalOverview'
 15 | import { PortalPublishedApis } from '@/components/portal/PortalPublishedApis'
 16 | import { PortalDevelopers } from '@/components/portal/PortalDevelopers'
 17 | import { PortalConsumers } from '@/components/portal/PortalConsumers'
 18 | import { PortalDashboard } from '@/components/portal/PortalDashboard'
 19 | import { PortalSecurity } from '@/components/portal/PortalSecurity'
 20 | import { PortalDomain } from '@/components/portal/PortalDomain'
 21 | import PortalFormModal from '@/components/portal/PortalFormModal'
 22 | import { portalApi } from '@/lib/api'
 23 | import { Portal } from '@/types'
 24 | 
 25 | const { Title } = Typography
 26 | 
 27 | // 移除mockPortal,使用真实API数据
 28 | 
 29 | const menuItems = [
 30 |   {
 31 |     key: "overview",
 32 |     label: "Overview",
 33 |     icon: EyeOutlined,
 34 |     description: "Portal概览"
 35 |   },
 36 |   {
 37 |     key: "published-apis",
 38 |     label: "Products",
 39 |     icon: ApiOutlined,
 40 |     description: "已发布的API产品"
 41 |   },
 42 |   {
 43 |     key: "developers",
 44 |     label: "Developers",
 45 |     icon: TeamOutlined,
 46 |     description: "开发者管理"
 47 |   },
 48 |   {
 49 |     key: "security",
 50 |     label: "Security",
 51 |     icon: SafetyOutlined,
 52 |     description: "安全设置"
 53 |   },
 54 |   {
 55 |     key: "domain",
 56 |     label: "Domain",
 57 |     icon: CloudOutlined,
 58 |     description: "域名管理"
 59 |   },
 60 |   // {
 61 |   //   key: "consumers",
 62 |   //   label: "Consumers",
 63 |   //   icon: UserOutlined,
 64 |   //   description: "消费者管理"
 65 |   // },
 66 |   {
 67 |     key: "dashboard",
 68 |     label: "Dashboard",
 69 |     icon: DashboardOutlined,
 70 |     description: "监控面板"
 71 |   }
 72 | ]
 73 | 
 74 | export default function PortalDetail() {
 75 |   const navigate = useNavigate()
 76 |   const [searchParams, setSearchParams] = useSearchParams()
 77 |   const [portal, setPortal] = useState<Portal | null>(null)
 78 |   const [loading, setLoading] = useState(true) // 初始状态为 loading
 79 |   const [error, setError] = useState<string | null>(null)
 80 |   const [editModalVisible, setEditModalVisible] = useState(false)
 81 | 
 82 |   // 从URL查询参数获取当前tab,默认为overview
 83 |   const currentTab = searchParams.get('tab') || 'overview'
 84 |   const [activeTab, setActiveTab] = useState(currentTab)
 85 | 
 86 |   const fetchPortalData = async () => {
 87 |     try {
 88 |       setLoading(true)
 89 |       const portalId = searchParams.get('id') || 'portal-6882e06f4fd0c963020e3485'
 90 |       const response = await portalApi.getPortalDetail(portalId) as any
 91 |       if (response && response.code === 'SUCCESS') {
 92 |         setPortal(response.data)
 93 |       } else {
 94 |         setError(response?.message || '获取Portal信息失败')
 95 |       }
 96 |     } catch (err) {
 97 |       console.error('获取Portal信息失败:', err)
 98 |       setError('获取Portal信息失败')
 99 |     } finally {
100 |       setLoading(false)
101 |     }
102 |   }
103 | 
104 |   useEffect(() => {
105 |     fetchPortalData()
106 |   }, [])
107 | 
108 |   // 当URL中的tab参数变化时,更新activeTab
109 |   useEffect(() => {
110 |     setActiveTab(currentTab)
111 |   }, [currentTab])
112 | 
113 |   const handleBackToPortals = () => {
114 |     navigate('/portals')
115 |   }
116 | 
117 |   // 处理tab切换,同时更新URL查询参数
118 |   const handleTabChange = (tabKey: string) => {
119 |     setActiveTab(tabKey)
120 |     const newSearchParams = new URLSearchParams(searchParams)
121 |     newSearchParams.set('tab', tabKey)
122 |     setSearchParams(newSearchParams)
123 |   }
124 | 
125 |   const handleEdit = () => {
126 |     setEditModalVisible(true)
127 |   }
128 | 
129 |   const handleEditSuccess = () => {
130 |     setEditModalVisible(false)
131 |     fetchPortalData()
132 |   }
133 | 
134 |   const handleEditCancel = () => {
135 |     setEditModalVisible(false)
136 |   }
137 | 
138 |   const renderContent = () => {
139 |     if (!portal) return null
140 |     
141 |     switch (activeTab) {
142 |       case "overview":
143 |         return <PortalOverview portal={portal} onEdit={handleEdit} />
144 |       case "published-apis":
145 |         return <PortalPublishedApis portal={portal} />
146 |       case "developers":
147 |         return <PortalDevelopers portal={portal} />
148 |       case "security":
149 |         return <PortalSecurity portal={portal} onRefresh={fetchPortalData} />
150 |       case "domain":
151 |         return <PortalDomain portal={portal} onRefresh={fetchPortalData} />
152 |       case "consumers":
153 |         return <PortalConsumers portal={portal} />
154 |       case "dashboard":
155 |         return <PortalDashboard portal={portal} />
156 |       default:
157 |         return <PortalOverview portal={portal} onEdit={handleEdit} />
158 |     }
159 |   }
160 | 
161 |   const dropdownItems: MenuProps['items'] = [
162 |     {
163 |       key: "delete",
164 |       label: "删除",
165 |       danger: true,
166 |       onClick: () => {
167 |         Modal.confirm({
168 |           title: "删除Portal",
169 |           content: "确定要删除该Portal吗?",
170 |           onOk: () => {
171 |             return handleDeletePortal();
172 |           },
173 |         });
174 |       },
175 |     },
176 |   ]
177 |   const handleDeletePortal = () => {
178 |     return portalApi.deletePortal(searchParams.get('id') || '').then(() => {
179 |       message.success('删除成功')
180 |       navigate('/portals')
181 |     }).catch((error) => {
182 |       message.error(error?.response?.data?.message || '删除失败,请稍后重试')
183 |       throw error; // 重新抛出错误,让Modal保持loading状态
184 |     })
185 |   }
186 | 
187 |   if (error || !portal) {
188 |     return (
189 |       <div className="flex h-full items-center justify-center">
190 |         <div className="text-center">
191 |           {error && <><p className=" mb-4">{error || 'Portal信息不存在'}</p>
192 |           <Button onClick={() => navigate('/portals')}>返回Portal列表</Button></>}
193 |           {!error && <Spin fullscreen spinning={loading} />}
194 |         </div>
195 |       </div>
196 |     )
197 |   }
198 | 
199 |   return (
200 |     <div className="flex h-full">
201 |       <Spin fullscreen spinning={loading} />
202 |       {/* Portal详情侧边栏 */}
203 |       <div className="w-64 border-r bg-white flex flex-col">
204 |         {/* 返回按钮 */}
205 |         <div className="pb-4 border-b">
206 |           <Button 
207 |             type="text"
208 |             // className="w-full justify-start text-gray-600 hover:text-gray-900"
209 |             onClick={handleBackToPortals}
210 |             icon={<LeftOutlined />}
211 |           >
212 |             返回
213 |           </Button>
214 |         </div>
215 | 
216 |         {/* Portal 信息 */}
217 |         <div className="p-4 border-b">
218 |           <div className="flex items-center justify-between mb-2">
219 |             <Title level={5} className="mb-0">{portal.name}</Title>
220 |             <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
221 |               <Button type="text" icon={<MoreOutlined />} size="small" />
222 |             </Dropdown>
223 |           </div>
224 |         </div>
225 | 
226 |         {/* 导航菜单 */}
227 |         <nav className="flex-1 p-4 space-y-2">
228 |           {menuItems.map((item) => {
229 |             const Icon = item.icon
230 |             return (
231 |               <button
232 |                 key={item.key}
233 |                 onClick={() => handleTabChange(item.key)}
234 |                 className={`w-full flex items-center gap-3 px-3 py-3 rounded-lg text-left transition-colors ${
235 |                   activeTab === item.key
236 |                     ? "bg-blue-50 text-blue-600 border border-blue-200"
237 |                     : "hover:bg-gray-50 text-gray-700"
238 |                 }`}
239 |               >
240 |                 <Icon className="h-4 w-4 flex-shrink-0" />
241 |                 <div className="flex-1">
242 |                   <div className="font-medium">{item.label}</div>
243 |                   <div className="text-xs text-gray-500 mt-1">{item.description}</div>
244 |                 </div>
245 |               </button>
246 |             )
247 |           })}
248 |         </nav>
249 |       </div>
250 | 
251 |       {/* 主内容区域 */}
252 |       <div className="flex-1 overflow-auto">
253 |       {renderContent()}
254 |       </div>
255 | 
256 |       {portal && (
257 |         <PortalFormModal
258 |           visible={editModalVisible}
259 |           onCancel={handleEditCancel}
260 |           onSuccess={handleEditSuccess}
261 |           portal={portal}
262 |         />
263 |       )}
264 |     </div>
265 |   )
266 | } 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/dto/converter/NacosToGatewayToolsConverter.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.dto.converter;
 21 | 
 22 | import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
 23 | import com.fasterxml.jackson.databind.ObjectMapper;
 24 | import com.fasterxml.jackson.databind.JsonNode;
 25 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 26 | import lombok.Data;
 27 | 
 28 | import java.util.ArrayList;
 29 | import java.util.HashMap;
 30 | import java.util.List;
 31 | import java.util.Map;
 32 | import java.util.Objects;
 33 | import java.util.stream.Collectors;
 34 | 
 35 | @Data
 36 | public class NacosToGatewayToolsConverter {
 37 | 
 38 |     private Server server = new Server();
 39 |     private List<Tool> tools = new ArrayList<>();
 40 |     private List<String> allowTools = new ArrayList<>();
 41 | 
 42 |     public void convertFromNacos(McpServerDetailInfo nacosDetail) {
 43 |         server.setName(nacosDetail.getName());
 44 |         server.getConfig().put("apiKey", "your-api-key-here");
 45 |         allowTools.add(nacosDetail.getName());
 46 |         
 47 |         if (nacosDetail.getToolSpec() != null) {
 48 |             convertTools(nacosDetail.getToolSpec());
 49 |         }
 50 |     }
 51 | 
 52 |     public String toYaml() {
 53 |         try {
 54 |             ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
 55 |             return yamlMapper.writeValueAsString(this);
 56 |         } catch (Exception e) {
 57 |             throw new RuntimeException("Failed to convert to YAML", e);
 58 |         }
 59 |     }
 60 | 
 61 |     private void convertTools(Object toolSpec) {
 62 |         try {
 63 |             ObjectMapper jsonMapper = new ObjectMapper();
 64 |             String toolSpecJson = jsonMapper.writeValueAsString(toolSpec);
 65 |             JsonNode toolSpecNode = jsonMapper.readTree(toolSpecJson);
 66 |             
 67 |             if (toolSpecNode.isArray()) {
 68 |                 for (JsonNode toolNode : toolSpecNode) {
 69 |                     Tool tool = convertToolNode(toolNode);
 70 |                     if (tool != null) {
 71 |                         tools.add(tool);
 72 |                     }
 73 |                 }
 74 |             } else if (toolSpecNode.has("tools") && toolSpecNode.get("tools").isArray()) {
 75 |                 JsonNode toolsNode = toolSpecNode.get("tools");
 76 |                 for (JsonNode toolNode : toolsNode) {
 77 |                     Tool tool = convertToolNode(toolNode);
 78 |                     if (tool != null) {
 79 |                         tools.add(tool);
 80 |                     }
 81 |                 }
 82 |             }
 83 |         } catch (Exception e) {
 84 |             // 转换失败时,tools保持空列表
 85 |         }
 86 |     }
 87 |     
 88 |     private Tool convertToolNode(JsonNode toolNode) {
 89 |         Tool result = new Tool();
 90 |         result.setName(getStringValue(toolNode, "name"));
 91 |         result.setDescription(getStringValue(toolNode, "description"));
 92 |         
 93 |         if (result.getName() == null) {
 94 |             return null;
 95 |         }
 96 |         
 97 |         List<Arg> args = convertArgs(toolNode);
 98 |         result.setArgs(args);
 99 |         result.setRequestTemplate(buildDefaultRequestTemplate(result.getName()));
100 |         result.setResponseTemplate(buildDefaultResponseTemplate());
101 |         
102 |         return result;
103 |     }
104 |     
105 |     private List<Arg> convertArgs(JsonNode toolNode) {
106 |         List<Arg> args = new ArrayList<>();
107 |         
108 |         try {
109 |             if (toolNode.has("inputSchema") && toolNode.get("inputSchema").has("properties")) {
110 |                 JsonNode properties = toolNode.get("inputSchema").get("properties");
111 |                 properties.fields().forEachRemaining(entry -> {
112 |                     String argName = entry.getKey();
113 |                     JsonNode argNode = entry.getValue();
114 |                     
115 |                     Arg arg = new Arg();
116 |                     arg.setName(argName);
117 |                     arg.setDescription(getStringValue(argNode, "description"));
118 |                     arg.setType(getStringValue(argNode, "type"));
119 |                     arg.setRequired(getBooleanValue(argNode, "required", false));
120 |                     arg.setPosition("query");
121 |                     
122 |                     args.add(arg);
123 |                 });
124 |             } else if (toolNode.has("args") && toolNode.get("args").isArray()) {
125 |                 JsonNode argsNode = toolNode.get("args");
126 |                 for (JsonNode argNode : argsNode) {
127 |                     Arg arg = new Arg();
128 |                     arg.setName(getStringValue(argNode, "name"));
129 |                     arg.setDescription(getStringValue(argNode, "description"));
130 |                     arg.setType(getStringValue(argNode, "type"));
131 |                     arg.setRequired(getBooleanValue(argNode, "required", false));
132 |                     arg.setPosition(getStringValue(argNode, "position"));
133 |                     arg.setDefaultValue(getStringValue(argNode, "default"));
134 |                     
135 |                     args.add(arg);
136 |                 }
137 |             }
138 |         } catch (Exception e) {
139 |             // 转换失败时,args保持空列表
140 |         }
141 |         
142 |         return args;
143 |     }
144 |     
145 |     private RequestTemplate buildDefaultRequestTemplate(String toolName) {
146 |         RequestTemplate template = new RequestTemplate();
147 |         template.setUrl("https://api.example.com/v1/" + toolName);
148 |         template.setMethod("GET");
149 |         
150 |         Header header = new Header();
151 |         header.setKey("Content-Type");
152 |         header.setValue("application/json");
153 |         template.getHeaders().add(header);
154 |         
155 |         return template;
156 |     }
157 |     
158 |     private ResponseTemplate buildDefaultResponseTemplate() {
159 |         ResponseTemplate template = new ResponseTemplate();
160 |         template.setBody("");
161 |         return template;
162 |     }
163 |     
164 |     private String getStringValue(JsonNode node, String fieldName) {
165 |         return node.has(fieldName) && !node.get(fieldName).isNull() ? 
166 |             node.get(fieldName).asText() : null;
167 |     }
168 |     
169 |     private boolean getBooleanValue(JsonNode node, String fieldName, boolean defaultValue) {
170 |         return node.has(fieldName) && !node.get(fieldName).isNull() ? 
171 |             node.get(fieldName).asBoolean() : defaultValue;
172 |     }
173 | 
174 |     @Data
175 |     public static class Server {
176 |         private String name;
177 |         private Map<String, Object> config = new HashMap<>();
178 |     }
179 |     
180 |     @Data
181 |     public static class Tool {
182 |         private String name;
183 |         private String description;
184 |         private List<Arg> args = new ArrayList<>();
185 |         private RequestTemplate requestTemplate;
186 |         private ResponseTemplate responseTemplate;
187 |     }
188 |     
189 |     @Data
190 |     public static class Arg {
191 |         private String name;
192 |         private String description;
193 |         private String type;
194 |         private boolean required;
195 |         private String position;
196 |         private String defaultValue;
197 |         private List<String> enumValues;
198 |     }
199 |     
200 |     @Data
201 |     public static class RequestTemplate {
202 |         private String url;
203 |         private String method;
204 |         private List<Header> headers = new ArrayList<>();
205 |     }
206 |     
207 |     @Data
208 |     public static class ResponseTemplate {
209 |         private String body;
210 |     }
211 |     
212 |     @Data
213 |     public static class Header {
214 |         private String key;
215 |         private String value;
216 |     }
217 | }
218 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/SwaggerUIWrapper.css:
--------------------------------------------------------------------------------

```css
  1 | /* Swagger UI 自定义样式 */
  2 | .swagger-ui-wrapper {
  3 |   /* 隐藏顶部的信息栏,因为我们已经在上层显示了 */
  4 |   .swagger-ui .info {
  5 |     display: none;
  6 |   }
  7 | 
  8 |   /* 完全隐藏服务器选择器的容器样式 */
  9 |   .swagger-ui .scheme-container {
 10 |     padding: 0;
 11 |     background: transparent;
 12 |     border: none;
 13 |     margin-bottom: 16px;
 14 |     position: relative;
 15 |     box-shadow: none;
 16 |   }
 17 | 
 18 |   /* 隐藏服务器区域的所有边框和背景 */
 19 |   .swagger-ui .scheme-container > div {
 20 |     background: transparent !important;
 21 |     border: none !important;
 22 |     box-shadow: none !important;
 23 |     padding: 0 !important;
 24 |   }
 25 | 
 26 |   /* 服务器URL样式优化 */
 27 |   .swagger-ui .servers-title {
 28 |     font-weight: 600;
 29 |     margin-bottom: 8px;
 30 |     color: #262626;
 31 |   }
 32 | 
 33 |   .swagger-ui .servers select {
 34 |     font-family: Monaco, Consolas, monospace;
 35 |     background: white;
 36 |     border: 1px solid #d9d9d9;
 37 |     border-radius: 4px;
 38 |     padding: 8px 40px 8px 12px;
 39 |     font-size: 14px;
 40 |     color: #1890ff;
 41 |     cursor: pointer;
 42 |     min-width: 300px;
 43 |     position: relative;
 44 |   }
 45 | 
 46 |   .swagger-ui .servers select:focus {
 47 |     border-color: #40a9ff;
 48 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
 49 |     outline: none;
 50 |   }
 51 | 
 52 |   /* 服务器选择器容器 */
 53 |   .swagger-ui .servers {
 54 |     position: relative;
 55 |   }
 56 | 
 57 |   /* 调整操作项的样式 */
 58 |   .swagger-ui .opblock {
 59 |     border-radius: 6px;
 60 |     border: 1px solid #e5e7eb;
 61 |     margin-bottom: 8px;
 62 |     box-shadow: none;
 63 |     width: 100%;
 64 |     margin-left: 0;
 65 |     margin-right: 0;
 66 |   }
 67 | 
 68 |   .swagger-ui .opblock.opblock-get {
 69 |     border-color: #61affe;
 70 |     background: rgba(97, 175, 254, 0.03);
 71 |   }
 72 | 
 73 |   .swagger-ui .opblock.opblock-post {
 74 |     border-color: #49cc90;
 75 |     background: rgba(73, 204, 144, 0.03);
 76 |   }
 77 | 
 78 |   .swagger-ui .opblock.opblock-put {
 79 |     border-color: #fca130;
 80 |     background: rgba(252, 161, 48, 0.03);
 81 |   }
 82 | 
 83 |   .swagger-ui .opblock.opblock-delete {
 84 |     border-color: #f93e3e;
 85 |     background: rgba(249, 62, 62, 0.03);
 86 |   }
 87 | 
 88 |   .swagger-ui .opblock.opblock-patch {
 89 |     border-color: #50e3c2;
 90 |     background: rgba(80, 227, 194, 0.03);
 91 |   }
 92 | 
 93 |   /* 调整展开的操作项样式 */
 94 |   .swagger-ui .opblock.is-open {
 95 |     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 96 |   }
 97 | 
 98 |   /* 调整参数表格样式 */
 99 |   .swagger-ui .parameters-container {
100 |     background: transparent;
101 |   }
102 | 
103 |   .swagger-ui .parameter__name {
104 |     font-family: Monaco, Consolas, monospace;
105 |     font-weight: 600;
106 |   }
107 | 
108 |   /* 调整响应区域样式 */
109 |   .swagger-ui .responses-wrapper {
110 |     background: transparent;
111 |   }
112 | 
113 |   /* 调整Try it out按钮 */
114 |   .swagger-ui .btn.try-out__btn {
115 |     background: #1890ff;
116 |     color: white;
117 |     border: none;
118 |     border-radius: 4px;
119 |     padding: 6px 16px;
120 |     font-size: 14px;
121 |   }
122 | 
123 |   .swagger-ui .btn.try-out__btn:hover {
124 |     background: #40a9ff;
125 |   }
126 | 
127 |   /* 调整Execute按钮 */
128 |   .swagger-ui .btn.execute {
129 |     background: #52c41a;
130 |     color: white;
131 |     border: none;
132 |     border-radius: 4px;
133 |     padding: 8px 20px;
134 |     font-size: 14px;
135 |     font-weight: 500;
136 |   }
137 | 
138 |   .swagger-ui .btn.execute:hover {
139 |     background: #73d13d;
140 |   }
141 | 
142 |   /* 调整Clear按钮 */
143 |   .swagger-ui .btn.btn-clear {
144 |     background: #ff4d4f;
145 |     color: white;
146 |     border: none;
147 |     border-radius: 4px;
148 |     padding: 6px 16px;
149 |     font-size: 14px;
150 |   }
151 | 
152 |   .swagger-ui .btn.btn-clear:hover {
153 |     background: #ff7875;
154 |   }
155 | 
156 |   /* 调整模型区域 */
157 |   .swagger-ui .model-container {
158 |     background: #f8f9fa;
159 |     border: 1px solid #e9ecef;
160 |     border-radius: 4px;
161 |   }
162 | 
163 |   /* 调整代码高亮 */
164 |   .swagger-ui .highlight-code {
165 |     background: #2d3748;
166 |     border-radius: 4px;
167 |   }
168 | 
169 |   /* 调整输入框样式 */
170 |   .swagger-ui input[type=text],
171 |   .swagger-ui input[type=password],
172 |   .swagger-ui input[type=search],
173 |   .swagger-ui input[type=email],
174 |   .swagger-ui input[type=url],
175 |   .swagger-ui input[type=number] {
176 |     border: 1px solid #d9d9d9;
177 |     border-radius: 4px;
178 |     padding: 6px 11px;
179 |     font-size: 14px;
180 |     line-height: 1.5;
181 |   }
182 | 
183 |   .swagger-ui input[type=text]:focus,
184 |   .swagger-ui input[type=password]:focus,
185 |   .swagger-ui input[type=search]:focus,
186 |   .swagger-ui input[type=email]:focus,
187 |   .swagger-ui input[type=url]:focus,
188 |   .swagger-ui input[type=number]:focus {
189 |     border-color: #40a9ff;
190 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
191 |     outline: none;
192 |   }
193 | 
194 |   /* 调整文本域样式 */
195 |   .swagger-ui textarea {
196 |     border: 1px solid #d9d9d9;
197 |     border-radius: 4px;
198 |     padding: 6px 11px;
199 |     font-size: 14px;
200 |     line-height: 1.5;
201 |   }
202 | 
203 |   .swagger-ui textarea:focus {
204 |     border-color: #40a9ff;
205 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
206 |     outline: none;
207 |   }
208 | 
209 |   /* 调整下拉选择样式 */
210 |   .swagger-ui select {
211 |     border: 1px solid #d9d9d9;
212 |     border-radius: 4px;
213 |     padding: 6px 11px;
214 |     font-size: 14px;
215 |     line-height: 1.5;
216 |   }
217 | 
218 |   .swagger-ui select:focus {
219 |     border-color: #40a9ff;
220 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
221 |     outline: none;
222 |   }
223 | 
224 |   /* 隐藏授权部分(如果不需要) */
225 |   .swagger-ui .auth-wrapper {
226 |     display: none;
227 |   }
228 | 
229 |   /* 调整整体字体 */
230 |   .swagger-ui {
231 |     font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
232 |   }
233 | 
234 | 
235 |   /* 调整标题样式 */
236 |   .swagger-ui .opblock-summary-description {
237 |     font-size: 14px;
238 |     color: #666;
239 |   }
240 | 
241 |   /* 调整HTTP方法标签 */
242 |   .swagger-ui .opblock-summary-method {
243 |     font-weight: bold;
244 |     text-transform: uppercase;
245 |     border-radius: 3px;
246 |     padding: 6px 12px;
247 |     font-size: 12px;
248 |     min-width: 60px;
249 |     text-align: center;
250 |   }
251 | 
252 |   /* 调整路径显示 */
253 |   .swagger-ui .opblock-summary-path {
254 |     font-family: Monaco, Consolas, monospace;
255 |     font-size: 16px;
256 |     font-weight: 500;
257 |   }
258 | 
259 |   /* 移除不必要的边距 */
260 |   .swagger-ui .swagger-container {
261 |     max-width: none !important;
262 |     width: 100% !important;
263 |     padding: 0;
264 |     margin: 0;
265 |   }
266 | 
267 |   /* 调整顶层wrapper */
268 |   .swagger-ui .wrapper {
269 |     padding: 0;
270 |     margin: 0;
271 |     width: 100% !important;
272 |     max-width: none !important;
273 |   }
274 | 
275 |   /* 移除左侧空白 */
276 |   .swagger-ui .information-container {
277 |     margin: 0;
278 |     padding: 0;
279 |   }
280 | 
281 |   /* 移除整体左边距 */
282 |   .swagger-ui {
283 |     margin-left: 0 !important;
284 |     padding-left: 0 !important;
285 |   }
286 | 
287 |   /* 移除操作块的左边距 */
288 |   .swagger-ui .opblock-tag-section {
289 |     margin-left: 0 !important;
290 |     padding-left: 0 !important;
291 |     margin-right: 0 !important;
292 |     padding-right: 0 !important;
293 |     width: 100% !important;
294 |   }
295 | 
296 |   /* 确保接口标签区域占满宽度 */
297 |   .swagger-ui .opblock-tag {
298 |     width: 100%;
299 |     margin: 0;
300 |   }
301 | 
302 |   /* 强制所有Swagger UI容器占满宽度 */
303 |   .swagger-ui-wrapper {
304 |     width: 100% !important;
305 |   }
306 | 
307 |   .swagger-ui {
308 |     width: 100% !important;
309 |     max-width: none !important;
310 |   }
311 | 
312 |   .swagger-ui .info {
313 |     width: 100% !important;
314 |   }
315 | 
316 |   .swagger-ui .scheme-container {
317 |     width: 100% !important;
318 |     max-width: none !important;
319 |   }
320 | 
321 |   /* 强制内容区域占满宽度 */
322 |   .swagger-ui .swagger-container .wrapper {
323 |     width: 100% !important;
324 |     max-width: none !important;
325 |   }
326 | 
327 |   /* 强制操作列表容器占满宽度 */
328 |   .swagger-ui .swagger-container .wrapper .col-12 {
329 |     width: 100% !important;
330 |     max-width: none !important;
331 |     flex: 0 0 100% !important;
332 |   }
333 | 
334 |   /* Servers标题样式 */
335 |   .swagger-ui .servers-title {
336 |     font-size: 14px !important;
337 |     font-weight: 500 !important;
338 |     margin-bottom: 8px !important;
339 |     color: #262626 !important;
340 |     text-align: left !important;
341 |   }
342 | 
343 |   /* 接口列表标题样式 */
344 |   .swagger-ui .opblock-tag {
345 |     font-size: 16px !important;
346 |     font-weight: 500 !important;
347 |     text-align: left !important;
348 |     margin-left: 0 !important;
349 |   }
350 | 
351 |   /* 去掉复制按钮的边框 */
352 |   .copy-server-btn {
353 |     border: none !important;
354 |     background: transparent !important;
355 |     padding: 6px 8px !important;
356 |     color: #666 !important;
357 |     transition: all 0.2s !important;
358 |   }
359 | 
360 |   .copy-server-btn:hover {
361 |     background: #f5f5f5 !important;
362 |     color: #1890ff !important;
363 |   }
364 | 
365 |   /* 调整接口列表与上方分割线的距离 */
366 |   .swagger-ui .opblock-tag-section {
367 |     margin-top: 20px !important;
368 |   }
369 | 
370 |   /* 调整分割线样式,确保与接口边框分开 */
371 |   .swagger-ui .opblock-tag h3 {
372 |     margin-bottom: 20px !important;
373 |     padding-bottom: 12px !important;
374 |     border-bottom: 1px solid #e8e8e8 !important;
375 |   }
376 | 
377 |   /* 确保第一个接口容器与分割线有足够间距 */
378 |   .swagger-ui .opblock-tag-section .opblock:first-child {
379 |     margin-top: 16px !important;
380 |   }
381 | }
382 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/SwaggerUIWrapper.css:
--------------------------------------------------------------------------------

```css
  1 | /* Swagger UI 自定义样式 */
  2 | .swagger-ui-wrapper {
  3 |   /* 隐藏顶部的信息栏,因为我们已经在上层显示了 */
  4 |   .swagger-ui .info {
  5 |     display: none;
  6 |   }
  7 | 
  8 |   /* 完全隐藏服务器选择器的容器样式 */
  9 |   .swagger-ui .scheme-container {
 10 |     padding: 0;
 11 |     background: transparent;
 12 |     border: none;
 13 |     margin-bottom: 16px;
 14 |     position: relative;
 15 |     box-shadow: none;
 16 |   }
 17 | 
 18 |   /* 隐藏服务器区域的所有边框和背景 */
 19 |   .swagger-ui .scheme-container > div {
 20 |     background: transparent !important;
 21 |     border: none !important;
 22 |     box-shadow: none !important;
 23 |     padding: 0 !important;
 24 |   }
 25 | 
 26 |   /* 服务器URL样式优化 */
 27 |   .swagger-ui .servers-title {
 28 |     font-weight: 600;
 29 |     margin-bottom: 8px;
 30 |     color: #262626;
 31 |   }
 32 | 
 33 |   .swagger-ui .servers select {
 34 |     font-family: Monaco, Consolas, monospace;
 35 |     background: white;
 36 |     border: 1px solid #d9d9d9;
 37 |     border-radius: 4px;
 38 |     padding: 8px 40px 8px 12px;
 39 |     font-size: 14px;
 40 |     color: #1890ff;
 41 |     cursor: pointer;
 42 |     min-width: 300px;
 43 |     position: relative;
 44 |   }
 45 | 
 46 |   .swagger-ui .servers select:focus {
 47 |     border-color: #40a9ff;
 48 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
 49 |     outline: none;
 50 |   }
 51 | 
 52 |   /* 服务器选择器容器 */
 53 |   .swagger-ui .servers {
 54 |     position: relative;
 55 |   }
 56 | 
 57 |   /* 调整操作项的样式 */
 58 |   .swagger-ui .opblock {
 59 |     border-radius: 6px;
 60 |     border: 1px solid #e5e7eb;
 61 |     margin-bottom: 8px;
 62 |     box-shadow: none;
 63 |     width: 100%;
 64 |     margin-left: 0;
 65 |     margin-right: 0;
 66 |   }
 67 | 
 68 |   .swagger-ui .opblock.opblock-get {
 69 |     border-color: #61affe;
 70 |     background: rgba(97, 175, 254, 0.03);
 71 |   }
 72 | 
 73 |   .swagger-ui .opblock.opblock-post {
 74 |     border-color: #49cc90;
 75 |     background: rgba(73, 204, 144, 0.03);
 76 |   }
 77 | 
 78 |   .swagger-ui .opblock.opblock-put {
 79 |     border-color: #fca130;
 80 |     background: rgba(252, 161, 48, 0.03);
 81 |   }
 82 | 
 83 |   .swagger-ui .opblock.opblock-delete {
 84 |     border-color: #f93e3e;
 85 |     background: rgba(249, 62, 62, 0.03);
 86 |   }
 87 | 
 88 |   .swagger-ui .opblock.opblock-patch {
 89 |     border-color: #50e3c2;
 90 |     background: rgba(80, 227, 194, 0.03);
 91 |   }
 92 | 
 93 |   /* 调整展开的操作项样式 */
 94 |   .swagger-ui .opblock.is-open {
 95 |     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 96 |   }
 97 | 
 98 |   /* 调整参数表格样式 */
 99 |   .swagger-ui .parameters-container {
100 |     background: transparent;
101 |   }
102 | 
103 |   .swagger-ui .parameter__name {
104 |     font-family: Monaco, Consolas, monospace;
105 |     font-weight: 600;
106 |   }
107 | 
108 |   /* 调整响应区域样式 */
109 |   .swagger-ui .responses-wrapper {
110 |     background: transparent;
111 |   }
112 | 
113 |   /* 调整Try it out按钮 */
114 |   .swagger-ui .btn.try-out__btn {
115 |     background: #1890ff;
116 |     color: white;
117 |     border: none;
118 |     border-radius: 4px;
119 |     padding: 6px 16px;
120 |     font-size: 14px;
121 |   }
122 | 
123 |   .swagger-ui .btn.try-out__btn:hover {
124 |     background: #40a9ff;
125 |   }
126 | 
127 |   /* 调整Execute按钮 */
128 |   .swagger-ui .btn.execute {
129 |     background: #52c41a;
130 |     color: white;
131 |     border: none;
132 |     border-radius: 4px;
133 |     padding: 8px 20px;
134 |     font-size: 14px;
135 |     font-weight: 500;
136 |   }
137 | 
138 |   .swagger-ui .btn.execute:hover {
139 |     background: #73d13d;
140 |   }
141 | 
142 |   /* 调整Clear按钮 */
143 |   .swagger-ui .btn.btn-clear {
144 |     background: #ff4d4f;
145 |     color: white;
146 |     border: none;
147 |     border-radius: 4px;
148 |     padding: 6px 16px;
149 |     font-size: 14px;
150 |   }
151 | 
152 |   .swagger-ui .btn.btn-clear:hover {
153 |     background: #ff7875;
154 |   }
155 | 
156 |   /* 调整模型区域 */
157 |   .swagger-ui .model-container {
158 |     background: #f8f9fa;
159 |     border: 1px solid #e9ecef;
160 |     border-radius: 4px;
161 |   }
162 | 
163 |   /* 调整代码高亮 */
164 |   .swagger-ui .highlight-code {
165 |     background: #2d3748;
166 |     border-radius: 4px;
167 |   }
168 | 
169 |   /* 调整输入框样式 */
170 |   .swagger-ui input[type=text],
171 |   .swagger-ui input[type=password],
172 |   .swagger-ui input[type=search],
173 |   .swagger-ui input[type=email],
174 |   .swagger-ui input[type=url],
175 |   .swagger-ui input[type=number] {
176 |     border: 1px solid #d9d9d9;
177 |     border-radius: 4px;
178 |     padding: 6px 11px;
179 |     font-size: 14px;
180 |     line-height: 1.5;
181 |   }
182 | 
183 |   .swagger-ui input[type=text]:focus,
184 |   .swagger-ui input[type=password]:focus,
185 |   .swagger-ui input[type=search]:focus,
186 |   .swagger-ui input[type=email]:focus,
187 |   .swagger-ui input[type=url]:focus,
188 |   .swagger-ui input[type=number]:focus {
189 |     border-color: #40a9ff;
190 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
191 |     outline: none;
192 |   }
193 | 
194 |   /* 调整文本域样式 */
195 |   .swagger-ui textarea {
196 |     border: 1px solid #d9d9d9;
197 |     border-radius: 4px;
198 |     padding: 6px 11px;
199 |     font-size: 14px;
200 |     line-height: 1.5;
201 |   }
202 | 
203 |   .swagger-ui textarea:focus {
204 |     border-color: #40a9ff;
205 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
206 |     outline: none;
207 |   }
208 | 
209 |   /* 调整下拉选择样式 */
210 |   .swagger-ui select {
211 |     border: 1px solid #d9d9d9;
212 |     border-radius: 4px;
213 |     padding: 6px 11px;
214 |     font-size: 14px;
215 |     line-height: 1.5;
216 |   }
217 | 
218 |   .swagger-ui select:focus {
219 |     border-color: #40a9ff;
220 |     box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
221 |     outline: none;
222 |   }
223 | 
224 |   /* 隐藏授权部分(如果不需要) */
225 |   .swagger-ui .auth-wrapper {
226 |     display: none;
227 |   }
228 | 
229 |   /* 调整整体字体 */
230 |   .swagger-ui {
231 |     font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
232 |   }
233 | 
234 | 
235 |   /* 调整标题样式 */
236 |   .swagger-ui .opblock-summary-description {
237 |     font-size: 14px;
238 |     color: #666;
239 |   }
240 | 
241 |   /* 调整HTTP方法标签 */
242 |   .swagger-ui .opblock-summary-method {
243 |     font-weight: bold;
244 |     text-transform: uppercase;
245 |     border-radius: 3px;
246 |     padding: 6px 12px;
247 |     font-size: 12px;
248 |     min-width: 60px;
249 |     text-align: center;
250 |   }
251 | 
252 |   /* 调整路径显示 */
253 |   .swagger-ui .opblock-summary-path {
254 |     font-family: Monaco, Consolas, monospace;
255 |     font-size: 16px;
256 |     font-weight: 500;
257 |   }
258 | 
259 |   /* 移除不必要的边距 */
260 |   .swagger-ui .swagger-container {
261 |     max-width: none !important;
262 |     width: 100% !important;
263 |     padding: 0;
264 |     margin: 0;
265 |   }
266 | 
267 |   /* 调整顶层wrapper */
268 |   .swagger-ui .wrapper {
269 |     padding: 0;
270 |     margin: 0;
271 |     width: 100% !important;
272 |     max-width: none !important;
273 |   }
274 | 
275 |   /* 移除左侧空白 */
276 |   .swagger-ui .information-container {
277 |     margin: 0;
278 |     padding: 0;
279 |   }
280 | 
281 |   /* 移除整体左边距 */
282 |   .swagger-ui {
283 |     margin-left: 0 !important;
284 |     padding-left: 0 !important;
285 |   }
286 | 
287 |   /* 移除操作块的左边距 */
288 |   .swagger-ui .opblock-tag-section {
289 |     margin-left: 0 !important;
290 |     padding-left: 0 !important;
291 |     margin-right: 0 !important;
292 |     padding-right: 0 !important;
293 |     width: 100% !important;
294 |   }
295 | 
296 |   /* 确保接口标签区域占满宽度 */
297 |   .swagger-ui .opblock-tag {
298 |     width: 100%;
299 |     margin: 0;
300 |   }
301 | 
302 |   /* 强制所有Swagger UI容器占满宽度 */
303 |   .swagger-ui-wrapper {
304 |     width: 100% !important;
305 |   }
306 | 
307 |   .swagger-ui {
308 |     width: 100% !important;
309 |     max-width: none !important;
310 |   }
311 | 
312 |   .swagger-ui .info {
313 |     width: 100% !important;
314 |   }
315 | 
316 |   .swagger-ui .scheme-container {
317 |     width: 100% !important;
318 |     max-width: none !important;
319 |   }
320 | 
321 |   /* 强制内容区域占满宽度 */
322 |   .swagger-ui .swagger-container .wrapper {
323 |     width: 100% !important;
324 |     max-width: none !important;
325 |   }
326 | 
327 |   /* 强制操作列表容器占满宽度 */
328 |   .swagger-ui .swagger-container .wrapper .col-12 {
329 |     width: 100% !important;
330 |     max-width: none !important;
331 |     flex: 0 0 100% !important;
332 |   }
333 | 
334 |   /* Servers标题样式 */
335 |   .swagger-ui .servers-title {
336 |     font-size: 14px !important;
337 |     font-weight: 500 !important;
338 |     margin-bottom: 8px !important;
339 |     color: #262626 !important;
340 |     text-align: left !important;
341 |   }
342 | 
343 |   /* 接口列表标题样式 */
344 |   .swagger-ui .opblock-tag {
345 |     font-size: 16px !important;
346 |     font-weight: 500 !important;
347 |     text-align: left !important;
348 |     margin-left: 0 !important;
349 |   }
350 | 
351 |   /* 去掉复制按钮的边框 */
352 |   .copy-server-btn {
353 |     border: none !important;
354 |     background: transparent !important;
355 |     padding: 6px 8px !important;
356 |     color: #666 !important;
357 |     transition: all 0.2s !important;
358 |   }
359 | 
360 |   .copy-server-btn:hover {
361 |     background: #f5f5f5 !important;
362 |     color: #1890ff !important;
363 |   }
364 | 
365 |   /* 调整接口列表与上方分割线的距离 */
366 |   .swagger-ui .opblock-tag-section {
367 |     margin-top: 20px !important;
368 |   }
369 | 
370 |   /* 调整分割线样式,确保与接口边框分开 */
371 |   .swagger-ui .opblock-tag h3 {
372 |     margin-bottom: 20px !important;
373 |     padding-bottom: 12px !important;
374 |     border-bottom: 1px solid #e8e8e8 !important;
375 |   }
376 | 
377 |   /* 确保第一个接口容器与分割线有足够间距 */
378 |   .swagger-ui .opblock-tag-section .opblock:first-child {
379 |     margin-top: 16px !important;
380 |   }
381 | }
382 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/client/HigressClient.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.client;
 21 | 
 22 | import cn.hutool.core.map.MapBuilder;
 23 | import cn.hutool.json.JSONUtil;
 24 | import com.alibaba.apiopenplatform.service.gateway.HigressOperator;
 25 | import com.alibaba.apiopenplatform.service.gateway.factory.HTTPClientFactory;
 26 | import com.alibaba.apiopenplatform.support.gateway.HigressConfig;
 27 | import lombok.Data;
 28 | import lombok.extern.slf4j.Slf4j;
 29 | import org.springframework.core.ParameterizedTypeReference;
 30 | import org.springframework.http.*;
 31 | import org.springframework.web.client.HttpClientErrorException;
 32 | import org.springframework.web.client.RestTemplate;
 33 | 
 34 | import java.util.List;
 35 | import java.util.Map;
 36 | 
 37 | @Slf4j
 38 | public class HigressClient extends GatewayClient {
 39 | 
 40 |     private static final String HIGRESS_COOKIE_NAME = "_hi_sess";
 41 | 
 42 |     private final RestTemplate restTemplate;
 43 |     private final HigressConfig config;
 44 |     private String higressToken;
 45 |     private final ThreadLocal<Boolean> isRetrying = new ThreadLocal<>();
 46 | 
 47 |     public HigressClient(HigressConfig higressConfig) {
 48 |         this.config = higressConfig;
 49 |         this.restTemplate = HTTPClientFactory.createRestTemplate();
 50 |     }
 51 | 
 52 |     public <T, R> T execute(String path,
 53 |                             HttpMethod method,
 54 |                             Map<String, String> queryParams,
 55 |                             R body,
 56 |                             ParameterizedTypeReference<T> responseType) {
 57 |         return execute(path, method, null, queryParams, body, responseType);
 58 |     }
 59 | 
 60 |     public <T, R> T execute(String path,
 61 |                             HttpMethod method,
 62 |                             Map<String, String> queryParams,
 63 |                             R body,
 64 |                             Class<T> responseType) {
 65 |         return execute(path, method, queryParams, body,
 66 |                 ParameterizedTypeReference.forType(responseType));
 67 |     }
 68 | 
 69 |     public <T, R> T execute(String path,
 70 |                             HttpMethod method,
 71 |                             HttpHeaders headers,
 72 |                             Map<String, String> queryParams,
 73 |                             R body,
 74 |                             ParameterizedTypeReference<T> responseType) {
 75 |         try {
 76 |             return doExecute(path, method, headers, queryParams, body, responseType);
 77 |         } finally {
 78 |             isRetrying.remove();
 79 |         }
 80 |     }
 81 | 
 82 |     private <T, R> T doExecute(String path,
 83 |                                HttpMethod method,
 84 |                                HttpHeaders headers,
 85 |                                Map<String, String> queryParams,
 86 |                                R body,
 87 |                                ParameterizedTypeReference<T> responseType) {
 88 |         try {
 89 |             ensureConsoleToken();
 90 | 
 91 |             // 构建URL
 92 |             String url = buildUrlWithParams(path, queryParams);
 93 | 
 94 |             // Headers
 95 |             HttpHeaders mergedHeaders = new HttpHeaders();
 96 |             if (headers != null) {
 97 |                 mergedHeaders.putAll(headers);
 98 |             }
 99 |             mergedHeaders.add("Cookie", HIGRESS_COOKIE_NAME + "=" + higressToken);
100 | 
101 |             ResponseEntity<T> response = restTemplate.exchange(
102 |                     url,
103 |                     method,
104 |                     new HttpEntity<>(body, mergedHeaders),
105 |                     responseType
106 |             );
107 | 
108 |             log.info("Higress response: status={}, body={}",
109 |                     response.getStatusCode(), JSONUtil.toJsonStr(response.getBody()));
110 | 
111 |             return response.getBody();
112 |         } catch (HttpClientErrorException e) {
113 |             // 401重新登录,且只重试一次
114 |             if (e.getStatusCode() == HttpStatus.UNAUTHORIZED
115 |                     && !Boolean.TRUE.equals(isRetrying.get())) {
116 |                 log.warn("Token expired, trying to relogin");
117 |                 higressToken = null;
118 |                 isRetrying.set(true);
119 |                 return doExecute(path, method, headers, queryParams, body, responseType);
120 |             }
121 |             log.error("HTTP error executing Higress request: status={}, body={}",
122 |                     e.getStatusCode(), e.getResponseBodyAsString());
123 |             throw e;
124 |         } catch (Exception e) {
125 |             log.error("Error executing Higress request: {}", e.getMessage());
126 |             throw new RuntimeException("Failed to execute Higress request", e);
127 |         }
128 |     }
129 | 
130 |     private String buildUrlWithParams(String path, Map<String, String> queryParams) {
131 |         StringBuilder url = new StringBuilder(buildUrl(path));
132 | 
133 |         if (queryParams != null && !queryParams.isEmpty()) {
134 |             url.append('?');
135 |             queryParams.forEach((key, value) -> {
136 |                 if (url.charAt(url.length() - 1) != '?') {
137 |                     url.append('&');
138 |                 }
139 |                 url.append(key).append('=').append(value);
140 |             });
141 |         }
142 | 
143 |         return url.toString();
144 |     }
145 | 
146 |     private String buildUrl(String path) {
147 |         String baseUrl = config.getAddress();
148 | 
149 |         baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
150 |         path = path.startsWith("/") ? path : "/" + path;
151 |         return baseUrl + path;
152 |     }
153 | 
154 |     private void ensureConsoleToken() {
155 |         if (higressToken == null) {
156 |             login();
157 |         }
158 |     }
159 | 
160 |     private void login() {
161 |         Map<Object, Object> loginParam = MapBuilder.create()
162 |                 .put("username", config.getUsername())
163 |                 .put("password", config.getPassword())
164 |                 .build();
165 | 
166 |         HttpHeaders headers = new HttpHeaders();
167 |         headers.setContentType(MediaType.APPLICATION_JSON);
168 | 
169 |         ResponseEntity<String> response = restTemplate.exchange(
170 |                 buildUrl("/session/login"),
171 |                 HttpMethod.POST,
172 |                 new HttpEntity<>(loginParam, headers),
173 |                 String.class
174 |         );
175 | 
176 |         List<String> cookies = response.getHeaders().get("Set-Cookie");
177 |         if (cookies == null || cookies.isEmpty()) {
178 |             throw new RuntimeException("No cookies received from server");
179 |         }
180 | 
181 |         this.higressToken = cookies.stream()
182 |                 .filter(cookie -> cookie.startsWith(HIGRESS_COOKIE_NAME + "="))
183 |                 .findFirst()
184 |                 .map(cookie -> {
185 |                     int endIndex = cookie.indexOf(';');
186 |                     return endIndex == -1
187 |                             ? cookie.substring(HIGRESS_COOKIE_NAME.length() + 1)
188 |                             : cookie.substring(HIGRESS_COOKIE_NAME.length() + 1, endIndex);
189 |                 })
190 |                 .orElseThrow(() -> new RuntimeException("Failed to get Higress session token"));
191 |     }
192 | 
193 |     @Override
194 |     public void close() {
195 |         HTTPClientFactory.closeClient(restTemplate);
196 |     }
197 | 
198 |     public static void main(String[] args) {
199 |         HigressConfig higressConfig = new HigressConfig();
200 |         higressConfig.setAddress("http://demo.higress.io");
201 |         higressConfig.setUsername("admin");
202 |         higressConfig.setPassword("admin");
203 | 
204 |         HigressClient higressClient = new HigressClient(higressConfig);
205 | //        Object  mcpServerInfo = higressClient.execute("/v1/mcpServer", HttpMethod.GET, null, null, new ParameterizedTypeReference<Object>() {
206 | //        });
207 | 
208 |         HigressOperator.HigressPageResponse<HigressOperator.HigressMCPConfig> response = higressClient.execute("/v1/mcpServer", HttpMethod.GET, null, null, new ParameterizedTypeReference<HigressOperator.HigressPageResponse<HigressOperator.HigressMCPConfig>>() {
209 |         });
210 |         System.out.println(JSONUtil.toJsonStr(response));
211 |     }
212 | 
213 | }
```

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

```typescript
  1 | import {Card, Row, Col, Statistic, Button, message} from 'antd'
  2 | import {
  3 |     UserOutlined,
  4 |     ApiOutlined,
  5 |     LinkOutlined,
  6 |     CheckCircleFilled,
  7 |     MinusCircleFilled,
  8 |     EditOutlined,
  9 |     CopyOutlined
 10 | } from '@ant-design/icons'
 11 | import {Portal} from '@/types'
 12 | import {useState, useEffect} from 'react'
 13 | import {portalApi, apiProductApi} from '@/lib/api'
 14 | import {copyToClipboard} from '@/lib/utils'
 15 | import {useNavigate} from 'react-router-dom'
 16 | 
 17 | interface PortalOverviewProps {
 18 |     portal: Portal
 19 |     onEdit?: () => void
 20 | }
 21 | 
 22 | export function PortalOverview({portal, onEdit}: PortalOverviewProps) {
 23 |     const navigate = useNavigate()
 24 |     const [apiCount, setApiCount] = useState(0)
 25 |     const [developerCount, setDeveloperCount] = useState(0)
 26 | 
 27 |     useEffect(() => {
 28 |         if (!portal.portalId) return;
 29 |         
 30 |         portalApi.getDeveloperList(portal.portalId, {
 31 |             page: 1,
 32 |             size: 10
 33 |         }).then((res: any) => {
 34 |             setDeveloperCount(res.data.totalElements || 0)
 35 |         })
 36 |         apiProductApi.getApiProducts({
 37 |             portalId: portal.portalId,
 38 |             page: 1,
 39 |             size: 10
 40 |         }).then((res: any) => {
 41 |             setApiCount(res.data.totalElements || 0)
 42 |         })
 43 | 
 44 |     }, [portal.portalId]) // 只依赖portalId,而不是整个portal对象
 45 | 
 46 |     return (
 47 |         <div className="p-6 space-y-6">
 48 |             <div>
 49 |                 <h1 className="text-2xl font-bold mb-2">概览</h1>
 50 |                 <p className="text-gray-600">Portal概览</p>
 51 |             </div>
 52 | 
 53 |             {/* 基本信息 */}
 54 |             <Card 
 55 |                 title="基本信息"
 56 |                 extra={
 57 |                     onEdit && (
 58 |                         <Button 
 59 |                             type="primary" 
 60 |                             icon={<EditOutlined />} 
 61 |                             onClick={onEdit}
 62 |                         >
 63 |                             编辑
 64 |                         </Button>
 65 |                     )
 66 |                 }
 67 |             >
 68 |                 <div>
 69 |                     <div className="grid grid-cols-6 gap-8 items-center pt-0 pb-2">
 70 |                         <span className="text-xs text-gray-600">Portal名称:</span>
 71 |                         <span className="col-span-2 text-xs text-gray-900">{portal.name}</span>
 72 |                         <span className="text-xs text-gray-600">Portal ID:</span>
 73 |                         <div className="col-span-2 flex items-center gap-2">
 74 |                             <span className="text-xs text-gray-700">{portal.portalId}</span>
 75 |                             <Button 
 76 |                                 type="text" 
 77 |                                 size="small"
 78 |                                 icon={<CopyOutlined />}
 79 |                                 onClick={async () => {
 80 |                                     try {
 81 |                                         await copyToClipboard(portal.portalId);
 82 |                                         message.success('Portal ID已复制');
 83 |                                     } catch {
 84 |                                         message.error('复制失败,请手动复制');
 85 |                                     }
 86 |                                 }}
 87 |                                 className="h-auto p-1 min-w-0"
 88 |                             />
 89 |                         </div>
 90 |                     </div>
 91 |                     
 92 |                     <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
 93 |                         <span className="text-xs text-gray-600">域名:</span>
 94 |                         <div className="col-span-2 flex items-center gap-2">
 95 |                             <LinkOutlined className="text-blue-500" />
 96 |                             <a 
 97 |                                 href={`http://${portal.portalDomainConfig?.[0]?.domain}`} 
 98 |                                 target="_blank"
 99 |                                 rel="noopener noreferrer" 
100 |                                 className="text-xs text-blue-600 hover:underline"
101 |                             >
102 |                                 {portal.portalDomainConfig?.[0]?.domain}
103 |                             </a>
104 |                         </div>
105 |                         <span className="text-xs text-gray-600">账号密码登录:</span>
106 |                         <div className="col-span-2 flex items-center">
107 |                             {portal.portalSettingConfig?.builtinAuthEnabled ? (
108 |                                 <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
109 |                             ) : (
110 |                                 <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
111 |                             )}
112 |                             <span className="text-xs text-gray-900">
113 |                                 {portal.portalSettingConfig?.builtinAuthEnabled ? '已启用' : '已停用'}
114 |                             </span>
115 |                         </div>
116 |                     </div>
117 |                     
118 |                     <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
119 |                         <span className="text-xs text-gray-600">开发者自动审批:</span>
120 |                         <div className="col-span-2 flex items-center">
121 |                             {portal.portalSettingConfig?.autoApproveDevelopers ? (
122 |                                 <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
123 |                             ) : (
124 |                                 <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
125 |                             )}
126 |                             <span className="text-xs text-gray-900">
127 |                                 {portal.portalSettingConfig?.autoApproveDevelopers ? '已启用' : '已停用'}
128 |                             </span>
129 |                         </div>
130 |                         <span className="text-xs text-gray-600">订阅自动审批:</span>
131 |                         <div className="col-span-2 flex items-center">
132 |                             {portal.portalSettingConfig?.autoApproveSubscriptions ? (
133 |                                 <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
134 |                             ) : (
135 |                                 <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
136 |                             )}
137 |                             <span className="text-xs text-gray-900">
138 |                                 {portal.portalSettingConfig?.autoApproveSubscriptions ? '已启用' : '已停用'}
139 |                             </span>
140 |                         </div>
141 |                     </div>
142 | 
143 |                     <div className="grid grid-cols-6 gap-8 items-start pt-2 pb-2">
144 |                         <span className="text-xs text-gray-600">描述:</span>
145 |                         <span className="col-span-5 text-xs text-gray-900 leading-relaxed">
146 |                             {portal.description || '-'}
147 |                         </span>
148 |                     </div>
149 |                 </div>
150 |             </Card>
151 | 
152 |             {/* 统计数据 */}
153 |             <Row gutter={[16, 16]}>
154 |                 <Col xs={24} sm={12} lg={12}>
155 |                     <Card 
156 |                         className="cursor-pointer hover:shadow-md transition-shadow"
157 |                         onClick={() => {
158 |                             navigate(`/portals/detail?id=${portal.portalId}&tab=developers`)
159 |                         }}
160 |                     >
161 |                         <Statistic
162 |                             title="注册开发者"
163 |                             value={developerCount}
164 |                             prefix={<UserOutlined className="text-blue-500" />}
165 |                             valueStyle={{ color: '#1677ff', fontSize: '24px' }}
166 |                         />
167 |                     </Card>
168 |                 </Col>
169 |                 <Col xs={24} sm={12} lg={12}>
170 |                     <Card 
171 |                         className="cursor-pointer hover:shadow-md transition-shadow"
172 |                         onClick={() => {
173 |                             navigate(`/portals/detail?id=${portal.portalId}&tab=published-apis`)
174 |                         }}
175 |                     >
176 |                         <Statistic
177 |                             title="已发布的API"
178 |                             value={apiCount}
179 |                             prefix={<ApiOutlined className="text-blue-500" />}
180 |                             valueStyle={{ color: '#1677ff', fontSize: '24px' }}
181 |                         />
182 |                     </Card>
183 |                 </Col>
184 |             </Row>
185 |         </div>
186 |     )
187 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/consumer/SubscriptionManager.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState } from "react";
  2 | import {
  3 |   Card,
  4 |   Button,
  5 |   message,
  6 |   Input,
  7 |   Modal,
  8 |   Table,
  9 |   Badge,
 10 |   Popconfirm,
 11 |   Select,
 12 | } from "antd";
 13 | import {
 14 |   PlusOutlined,
 15 | } from "@ant-design/icons";
 16 | import api from "../../lib/api";
 17 | import type { Subscription } from "../../types/consumer";
 18 | import type { ApiResponse, Product } from "../../types";
 19 | import { getSubscriptionStatusText, getSubscriptionStatusColor } from "../../lib/statusUtils";
 20 | import { formatDateTime } from "../../lib/utils";
 21 | 
 22 | interface SubscriptionManagerProps {
 23 |   consumerId: string;
 24 |   subscriptions: Subscription[];
 25 |   onSubscriptionsChange: (searchParams?: { productName: string; status: string }) => void;
 26 |   loading?: boolean;
 27 | }
 28 | 
 29 | export function SubscriptionManager({ consumerId, subscriptions, onSubscriptionsChange, loading = false }: SubscriptionManagerProps) {
 30 |   const [productModalVisible, setProductModalVisible] = useState(false);
 31 |   const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
 32 |   const [productLoading, setProductLoading] = useState(false);
 33 |   const [subscribeLoading, setSubscribeLoading] = useState(false);
 34 |   const [selectedProduct, setSelectedProduct] = useState<string>('');
 35 |   const [subscriptionSearch, setSubscriptionSearch] = useState({ productName: '', status: '' as 'PENDING' | 'APPROVED' | '' });
 36 | 
 37 |   // 过滤产品:移除已订阅的产品
 38 |   const filterProducts = (allProducts: Product[]) => {
 39 |     // 获取已订阅的产品ID列表
 40 |     const subscribedProductIds = subscriptions.map(sub => sub.productId);
 41 |     
 42 |     // 过滤掉已订阅的产品
 43 |     return allProducts.filter(product => 
 44 |       !subscribedProductIds.includes(product.productId)
 45 |     );
 46 |   };
 47 | 
 48 |   const openProductModal = async () => {
 49 |     setProductModalVisible(true);
 50 |     setProductLoading(true);
 51 |     try {
 52 |       const response: ApiResponse<{ content: Product[] }> = await api.get("/products?page=0&size=100");
 53 |       if (response?.code === "SUCCESS" && response?.data) {
 54 |         const allProducts = response.data.content || [];
 55 |         // 初始化时过滤掉已订阅的产品
 56 |         const filtered = filterProducts(allProducts);
 57 |         setFilteredProducts(filtered);
 58 |       }
 59 |     } catch (error) {
 60 |       console.error('获取产品列表失败:', error);
 61 |       // message.error('获取产品列表失败');
 62 |     } finally {
 63 |       setProductLoading(false);
 64 |     }
 65 |   };
 66 | 
 67 | 
 68 |   const handleSubscribeProducts = async () => {
 69 |     if (!selectedProduct) {
 70 |       message.warning('请选择要订阅的产品');
 71 |       return;
 72 |     }
 73 | 
 74 |     setSubscribeLoading(true);
 75 |     try {
 76 |       await api.post(`/consumers/${consumerId}/subscriptions`, { productId: selectedProduct });
 77 |       message.success('订阅成功');
 78 |       setProductModalVisible(false);
 79 |       setSelectedProduct('');
 80 |       onSubscriptionsChange();
 81 |     } catch (error) {
 82 |       console.error('订阅失败:', error);
 83 |       // message.error('订阅失败');
 84 |     } finally {
 85 |       setSubscribeLoading(false);
 86 |     }
 87 |   };
 88 | 
 89 |   const handleUnsubscribe = async (productId: string) => {
 90 |     try {
 91 |       await api.delete(`/consumers/${consumerId}/subscriptions/${productId}`);
 92 |       message.success('取消订阅成功');
 93 |       onSubscriptionsChange();
 94 |     } catch (error) {
 95 |       console.error('取消订阅失败:', error);
 96 |       // message.error('取消订阅失败');
 97 |     }
 98 |   };
 99 | 
100 | 
101 | 
102 |   const subscriptionColumns = [
103 |     {
104 |       title: '产品名称',
105 |       dataIndex: 'productName',
106 |       key: 'productName',
107 |       render: (productName: Product['productName']) => productName || '-',
108 |     },
109 |     {
110 |       title: '产品类型',
111 |       dataIndex: 'productType',
112 |       key: 'productType',
113 |       render: (productType: Product['productType']) => {
114 |         const typeMap = {
115 |           'REST_API': 'REST API',
116 |           'HTTP_API': 'HTTP API',
117 |           'MCP_SERVER': 'MCP Server'
118 |         };
119 |         return typeMap[productType as keyof typeof typeMap] || productType || '-';
120 |       }
121 |     },
122 |     {
123 |       title: '订阅状态',
124 |       dataIndex: 'status',
125 |       key: 'status',
126 |       render: (status: string) => (
127 |         <Badge status={getSubscriptionStatusColor(status) as 'success' | 'processing' | 'error' | 'default' | 'warning'} text={getSubscriptionStatusText(status)} />
128 |       ),
129 |     },
130 |     {
131 |       title: '订阅时间',
132 |       dataIndex: 'createAt',
133 |       key: 'createAt',
134 |       render: (date: string) => date ? formatDateTime(date) : '-',
135 |     },
136 |     {
137 |       title: '操作',
138 |       key: 'action',
139 |       render: (record: Subscription) => (
140 |         <Popconfirm
141 |           title="确定要取消订阅吗?"
142 |           onConfirm={() => handleUnsubscribe(record.productId)}
143 |         >
144 |           <Button type="link" danger size="small">
145 |             取消订阅
146 |           </Button>
147 |         </Popconfirm>
148 |       ),
149 |     },
150 |   ];
151 | 
152 |   // 确保 subscriptions 始终是数组
153 |   const safeSubscriptions = Array.isArray(subscriptions) ? subscriptions : [];
154 | 
155 |   return (
156 |     <>
157 |       <Card>
158 |         <div className="mb-4 flex justify-between items-center">
159 |           <div className="flex space-x-4">
160 |             <Button
161 |               type="primary"
162 |               icon={<PlusOutlined />}
163 |               onClick={openProductModal}
164 |             >
165 |               订阅
166 |             </Button>
167 |             <Input.Search
168 |               placeholder="请输入API名称进行搜索"
169 |               style={{ width: 300 }}
170 |               onSearch={(value) => {
171 |                 const newSearch = { ...subscriptionSearch, productName: value };
172 |                 setSubscriptionSearch(newSearch);
173 |                 onSubscriptionsChange(newSearch);
174 |               }}
175 |             />
176 |             <Select
177 |               placeholder="订阅状态"
178 |               style={{ width: 120 }}
179 |               allowClear
180 |               value={subscriptionSearch.status || undefined}
181 |               onChange={(value) => {
182 |                 const newSearch = { ...subscriptionSearch, status: value as 'PENDING' | 'APPROVED' | '' };
183 |                 setSubscriptionSearch(newSearch);
184 |                 onSubscriptionsChange(newSearch);
185 |               }}
186 |             >
187 |               <Select.Option value="PENDING">待审批</Select.Option>
188 |               <Select.Option value="APPROVED">已通过</Select.Option>
189 |             </Select>
190 |           </div>
191 |         </div>
192 |         <Table
193 |           columns={subscriptionColumns}
194 |           dataSource={safeSubscriptions}
195 |           rowKey={(record) => record.productId}
196 |           pagination={false}
197 |           size="small"
198 |           loading={loading}
199 |           locale={{ emptyText: '暂无订阅记录,请点击上方按钮进行订阅' }}
200 |         />
201 |       </Card>
202 | 
203 |       {/* 产品选择弹窗 */}
204 |       <Modal
205 |         title="订阅产品"
206 |         open={productModalVisible}
207 |         onCancel={() => {
208 |           if (!subscribeLoading) {
209 |             setProductModalVisible(false);
210 |             setSelectedProduct('');
211 |           }
212 |         }}
213 |         footer={
214 |           <div className="flex justify-end space-x-2">
215 |             <Button
216 |               onClick={() => {
217 |                 if (!subscribeLoading) {
218 |                   setProductModalVisible(false);
219 |                   setSelectedProduct('');
220 |                 }
221 |               }}
222 |               disabled={subscribeLoading}
223 |             >
224 |               取消
225 |             </Button>
226 |             <Button
227 |               type="primary"
228 |               onClick={handleSubscribeProducts}
229 |               disabled={!selectedProduct}
230 |               loading={subscribeLoading}
231 |             >
232 |               确定订阅
233 |             </Button>
234 |           </div>
235 |         }
236 |         width={500}
237 |         styles={{
238 |           content: {
239 |             borderRadius: '8px',
240 |             padding: 0
241 |           },
242 |           header: {
243 |             borderRadius: '8px 8px 0 0',
244 |             marginBottom: 0,
245 |             paddingBottom: '8px'
246 |           },
247 |           body: {
248 |             padding: '24px'
249 |           }
250 |         }}
251 |       >
252 |         <div>
253 |           <div className="text-sm text-gray-700 mb-3 font-medium">选择要订阅的产品:</div>
254 |           <Select
255 |             placeholder="请输入产品名称进行搜索或直接选择"
256 |             style={{ width: '100%' }}
257 |             value={selectedProduct}
258 |             onChange={setSelectedProduct}
259 |             loading={productLoading}
260 |             showSearch={true}
261 |             filterOption={(input, option) => {
262 |               const product = filteredProducts.find(p => p.productId === option?.value);
263 |               if (!product) return false;
264 |               
265 |               const searchText = input.toLowerCase();
266 |               return (
267 |                 product.name?.toLowerCase().includes(searchText) ||
268 |                 product.description?.toLowerCase().includes(searchText)
269 |               );
270 |             }}
271 |             notFoundContent={productLoading ? '加载中...' : '暂无可订阅的产品'}
272 |           >
273 |             {filteredProducts.map(product => (
274 |               <Select.Option key={product.productId} value={product.productId}>
275 |                 {product.name}
276 |               </Select.Option>
277 |             ))}
278 |           </Select>
279 |         </div>
280 |       </Modal>
281 |     </>
282 |   );
283 | } 
```
Page 5/9FirstPrevNextLast