mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Merge remote-tracking branch 'github-pro/feature/1.4' into feature/ai-balance
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
type TableBtnWithPermissionProps = {
|
||||
btnTitle: string
|
||||
@@ -25,6 +25,7 @@ const TableIconName = {
|
||||
copy: 'ic:baseline-file-copy',
|
||||
view: 'ic:baseline-remove-red-eye',
|
||||
publish: 'ic:baseline-publish',
|
||||
offline: 'ic:baseline-file-download-off',
|
||||
approval: 'ic:baseline-approval',
|
||||
stop: 'ic:baseline-stop-circle',
|
||||
online: 'ic:baseline-check-circle',
|
||||
@@ -86,7 +87,7 @@ const TableBtnWithPermission = ({
|
||||
<Button
|
||||
type="text"
|
||||
disabled={true}
|
||||
className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`}
|
||||
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
|
||||
key={btnType}
|
||||
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
|
||||
>
|
||||
@@ -104,7 +105,7 @@ const TableBtnWithPermission = ({
|
||||
<Button
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `}
|
||||
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
|
||||
key={btnType}
|
||||
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
|
||||
onClick={handleClick}
|
||||
|
||||
+17
-17
@@ -1,21 +1,21 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { ActionType, ParamsType } from '@ant-design/pro-components'
|
||||
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { App, Divider, Spin } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useLocation, useOutletContext, useParams } from 'react-router-dom'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { ActionType, ParamsType } from '@ant-design/pro-components'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { IntelligentPluginConfig, IntelligentPluginConfigHandle } from './IntelligentPluginConfig.tsx'
|
||||
import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { EntityItem } from '@common/const/type.ts'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
|
||||
import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
|
||||
type DynamicTableField = {
|
||||
name: string
|
||||
@@ -218,8 +218,8 @@ export default function IntelligentPluginList() {
|
||||
render: (_: React.ReactNode, entity: DynamicTableItem) => [
|
||||
<TableBtnWithPermission
|
||||
access={`${accessPrefix}.publish`}
|
||||
key="publish"
|
||||
btnType="publish"
|
||||
key={entity.status === $t('已发布') ? 'offline' : 'publish'}
|
||||
btnType={entity.status === $t('已发布') ? 'offline' : 'publish'}
|
||||
onClick={() => {
|
||||
openModal('publish', entity)
|
||||
}}
|
||||
@@ -233,7 +233,7 @@ export default function IntelligentPluginList() {
|
||||
onClick={() => {
|
||||
openDrawer('edit', entity)
|
||||
}}
|
||||
btnTitle={$t('查看')}
|
||||
btnTitle={$t('查看 ')}
|
||||
/>,
|
||||
<Divider type="vertical" className="mx-0" key="div2" />,
|
||||
<TableBtnWithPermission
|
||||
@@ -322,6 +322,7 @@ export default function IntelligentPluginList() {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
manualReloadTable()
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -329,7 +330,6 @@ export default function IntelligentPluginList() {
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => Promise.reject(errorInfo))
|
||||
message.destroy()
|
||||
return
|
||||
}
|
||||
case 'delete':
|
||||
|
||||
@@ -219,22 +219,22 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.view'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.add': {
|
||||
'system.settings.log_configuration.add': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.edit': {
|
||||
'system.settings.log_configuration.edit': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.publish': {
|
||||
'system.settings.log_configuration.publish': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
},
|
||||
'system.devops.log_configuration.delete': {
|
||||
'system.settings.log_configuration.delete': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
|
||||
}
|
||||
@@ -674,10 +674,21 @@ export const PERMISSION_DEFINITION = [
|
||||
anyOf: [{ backend: ['project.permission_manager'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.ai_key_resource.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_key_resource.view'] }]
|
||||
}
|
||||
},
|
||||
'system.settings.ai_key_resource.manager': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_key_resource.manager'] }]
|
||||
}
|
||||
},
|
||||
|
||||
'system.settings.ai_api.view': {
|
||||
granted: {
|
||||
anyOf: [{ backend: ['system.settings.ai_api.view'] }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -151,15 +151,15 @@ const mockData = [
|
||||
name: 'APIKey 资源池',
|
||||
key: 'aiKeys',
|
||||
path: '/keysetting',
|
||||
icon: 'ic:baseline-key'
|
||||
// access: 'system.settings.ai_key_resource.view'
|
||||
icon: 'ic:baseline-key',
|
||||
access: 'system.settings.ai_key_resource.view'
|
||||
},
|
||||
{
|
||||
name: 'AI API',
|
||||
key: 'aiApiList',
|
||||
path: '/aiApis',
|
||||
icon: 'ic:baseline-api'
|
||||
// access: 'system.settings.ai_api.view'
|
||||
icon: 'ic:baseline-api',
|
||||
access: 'system.settings.ai_api.view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -333,13 +333,13 @@
|
||||
"不是有效邮箱地址": "Kcbee3f8",
|
||||
"获取 AI providers 失败": "K94b48734",
|
||||
"AI 供应商": "Kf23a8988",
|
||||
"预览": "K4d81a657",
|
||||
"AI 服务": "Kd2c34e2c",
|
||||
"模型": "Kfede1c7c",
|
||||
"已用 Token": "K89f135a7",
|
||||
"拦截": "Kb7df6ac1",
|
||||
"放行": "K5c1722fe",
|
||||
"编辑时间": "K1acc30b2",
|
||||
"预览": "K4d81a657",
|
||||
"查看详情": "K35f990b0",
|
||||
"AI API 列表": "K91144ebd",
|
||||
"重置": "K50d471b2",
|
||||
@@ -398,6 +398,8 @@
|
||||
"配置好 AI 模型后,你可以使用对应的大模型来创建 AI 服务": "K2260837a",
|
||||
"已设置": "Kf97448b3",
|
||||
"未设置": "K30d4d8df",
|
||||
"保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。": "Ke32702ac",
|
||||
"保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。": "Ka08c28d4",
|
||||
"默认模型": "Kc2ee5223",
|
||||
"负载优先级": "K608af899",
|
||||
"负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。": "K65b7a96",
|
||||
@@ -405,6 +407,7 @@
|
||||
"请输入优先级": "Kfcf02780",
|
||||
"API Key(默认 Key)": "K5c6dcf58",
|
||||
"LLM 状态管理": "K59bf8ed9",
|
||||
"当前调用状态:": "Kab8fe398",
|
||||
"正常": "Ke039b9b5",
|
||||
"停用": "Kedd64e4d",
|
||||
"异常": "K23a3bd72",
|
||||
@@ -477,6 +480,7 @@
|
||||
"过期": "Kb9e7ceda",
|
||||
"错误": "Kac405b50",
|
||||
"请选择状态": "K3fde5b49",
|
||||
"添加 (0) APIKey": "K4880fd04",
|
||||
"编辑 APIKey": "K434b7e76",
|
||||
"删除成功": "K28190dbc",
|
||||
"停用成功": "Kb5fcf5b8",
|
||||
|
||||
@@ -804,5 +804,9 @@
|
||||
"Kefb03657": "APIKey Resource Pool",
|
||||
"Kc0352e64": "Supports creating multiple APIKeys under a single API model provider for intelligent load balancing",
|
||||
"Kd25acba1": "Please enter name to search",
|
||||
"K6d0388a0": "Add APIKey"
|
||||
"K6d0388a0": "Add APIKey",
|
||||
"Ke32702ac": "After saving, the supplier status will become [Disabled]. APIs using this supplier will temporarily use the normal supplier with the highest load priority.",
|
||||
"Ka08c28d4": "After saving, the supplier status will become [Normal], restoring the AI capabilities of this supplier.",
|
||||
"Kab8fe398": "Current Call Status:",
|
||||
"K4880fd04": "Add (0) APIKey"
|
||||
}
|
||||
|
||||
@@ -620,7 +620,7 @@
|
||||
"K1cc07937": "有効期限",
|
||||
"K39686a7f": "先頭はアルファベットで、英数字とハイフン、アンダースコアの組み合わせをサポート",
|
||||
"Ka4ecfa40": "英数字またはアンダースコア、先頭は必ずアルファベット",
|
||||
"K37318b68": "クラスタに接続できません。クラスタアドレスが正しいか、ファイアウォール設定を確認してください。",
|
||||
"K37318b68": "クラスターに接続できません。クラスタアドレスが正しいか、ファイアウォール設定を確認してください。",
|
||||
"Kac172626": "申請を拒否する際は、拒否理由を記入してください。",
|
||||
"K7f0c746d": "成功",
|
||||
"K6a365d01": "失敗",
|
||||
@@ -826,5 +826,9 @@
|
||||
"Kefb03657": "API キーリソースプール",
|
||||
"Kc0352e64": "単一の API モデルプロバイダーで複数の API キーを作成し、インテリジェントな負荷分散を実現できます",
|
||||
"Kd25acba1": "名前を入力して検索",
|
||||
"K6d0388a0": "API キーを追加"
|
||||
"K6d0388a0": "API キーを追加",
|
||||
"Ke32702ac": "保存後、サプライヤーのステータスは【無効】となり、このサプライヤーのAPIは一時的に負荷優先度が最も高い正常なサプライヤーを使用します。",
|
||||
"Ka08c28d4": "保存後、サプライヤーのステータスは【正常】となり、このサプライヤーのAI機能が復元されます。",
|
||||
"Kab8fe398": "現在の呼び出し状態:",
|
||||
"K4880fd04": "APIKeyを追加 (0)"
|
||||
}
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -44,8 +44,12 @@
|
||||
"Kd2850420": "待删除",
|
||||
"K83237c89": "输入的IP或CIDR不符合格式",
|
||||
"K5ae2c87a": "请正确输入路径,如/usr/*或*/usr/*",
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K508d8bf4": "集成地址",
|
||||
"K67f4e9bb": "与外部平台集成时,获取 API 市场中文档信息的域名",
|
||||
"K4880fd04": "添加 (0) APIKey",
|
||||
"Kc82b8374": "编辑策略",
|
||||
"K4b34a5e5": "策略类型",
|
||||
"K57f0fee8": "匹配条件",
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
{}
|
||||
{
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"Kc0e5ef9f": "Workspace",
|
||||
"K4de11e23": "Home",
|
||||
"Kfe93ef35": "Application",
|
||||
"K61c89f5f": "API Portal",
|
||||
"Kc0e5ef9f": "ワークスペース",
|
||||
"K4de11e23": "ホーム",
|
||||
"Kfe93ef35": "アプリケーション",
|
||||
"K61c89f5f": "API ポータル",
|
||||
"K3fe97dcc": "設定",
|
||||
"Kecbb0e45": "システム",
|
||||
"Ka358e23d": "一般",
|
||||
|
||||
@@ -757,5 +757,9 @@
|
||||
"Kefb03657": "APIKey 资源池",
|
||||
"Kc0352e64": "支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡",
|
||||
"Kd25acba1": "请输入名称搜索",
|
||||
"K6d0388a0": "添加 APIKey"
|
||||
"K6d0388a0": "添加 APIKey",
|
||||
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
|
||||
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
|
||||
"Kab8fe398": "当前调用状态:",
|
||||
"K4880fd04": "添加 (0) APIKey"
|
||||
}
|
||||
|
||||
@@ -826,5 +826,9 @@
|
||||
"Kefb03657": "APIKey 资源池",
|
||||
"Kc0352e64": "支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡",
|
||||
"Kd25acba1": "请输入名称搜索",
|
||||
"K6d0388a0": "添加 APIKey"
|
||||
"K6d0388a0": "添加 APIKey",
|
||||
"Ke32702ac": "儲存後供應商狀態變為【停用】,使用本供應商的 API 將暫時使用負載優先級最高的正常供應商。",
|
||||
"Ka08c28d4": "儲存後供應商狀態變為【正常】,恢復調用本供應商的 AI 能力。",
|
||||
"Kab8fe398": "目前調用狀態:",
|
||||
"K4880fd04": "新增 (0) APIKey"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns<AiServiceRouterTabl
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'requestPath',
|
||||
ellipsis: true
|
||||
ellipsis: true,
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
|
||||
@@ -478,17 +478,22 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
{
|
||||
path: 'list',
|
||||
key: 'apiList',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx')),
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx'))
|
||||
},
|
||||
{
|
||||
path: 'service/:teamId/aiInside/:serviceId/route/:routeId/:type',
|
||||
key: 'apiDetail',
|
||||
lazy: lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate.tsx'
|
||||
path: 'service/:teamId/aiInside/:serviceId',
|
||||
key: 'aiApisServiceInside',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')),
|
||||
children: [
|
||||
{
|
||||
path: 'route/:routeId/:type',
|
||||
key: 'aiApisServiceInsideRouteDetail',
|
||||
lazy: lazy(
|
||||
() =>
|
||||
import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')
|
||||
)
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,12 +39,14 @@ const ApiSettings: React.FC = () => {
|
||||
const handlePreview = (record: APIs) => {
|
||||
navigate(`../service/${record.team.id}/aiInside/${record.service.id}/route/${record.id}/apiDetail`)
|
||||
}
|
||||
const requestApis = async (params: any & {
|
||||
pageSize: number;
|
||||
current: number;
|
||||
},
|
||||
const requestApis = async (
|
||||
params: any & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>) => {
|
||||
filter: Record<string, string>
|
||||
) => {
|
||||
if (!selectedProvider) return
|
||||
setQueryBtnLoading(true)
|
||||
try {
|
||||
@@ -75,16 +77,22 @@ const ApiSettings: React.FC = () => {
|
||||
setTotal(response.data.total)
|
||||
const modalMap: {
|
||||
[key: string]: string
|
||||
} = response.data?.condition?.models.reduce((acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
}, {})
|
||||
} = response.data?.condition?.models.reduce(
|
||||
(acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
const serviceMap: {
|
||||
[key: string]: string
|
||||
} = response.data?.condition?.services.reduce((acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
}, {})
|
||||
} = response.data?.condition?.services.reduce(
|
||||
(acc: { [key: string]: string }, item: { id: string; name: string }) => {
|
||||
acc[item.id] = $t(item.name)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
setTableColumns(modalMap, serviceMap)
|
||||
return {
|
||||
data: response.data.apis || [],
|
||||
@@ -107,11 +115,14 @@ const ApiSettings: React.FC = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
const setTableColumns = (modalMap: {
|
||||
[key: string]: string
|
||||
}, serviceMap: {
|
||||
[key: string]: string
|
||||
}) => {
|
||||
const setTableColumns = (
|
||||
modalMap: {
|
||||
[key: string]: string
|
||||
},
|
||||
serviceMap: {
|
||||
[key: string]: string
|
||||
}
|
||||
) => {
|
||||
setColumns([
|
||||
{
|
||||
title: $t('AI 服务'),
|
||||
@@ -287,15 +298,18 @@ const ApiSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
request={async (params: any & {
|
||||
pageSize: number;
|
||||
current: number;
|
||||
},
|
||||
request={async (
|
||||
params: any & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>) => requestApis(params, sort, filter)}
|
||||
filter: Record<string, string>
|
||||
) => requestApis(params, sort, filter)}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
onRowClick={(row: APIs) => handlePreview(row)}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入 APIURL 搜索')}
|
||||
columns={columns}
|
||||
|
||||
@@ -1,166 +1,262 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { getItem } from '@common/utils/navigation.tsx'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { App, Menu, MenuProps } from 'antd'
|
||||
import { ItemType, MenuItemGroupType, MenuItemType } from 'antd/es/menu/interface'
|
||||
import Paragraph from 'antd/es/typography/Paragraph'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useAiServiceContext } from '../../contexts/AiServiceContext.tsx'
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE
|
||||
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {Link, Outlet, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx";
|
||||
import {App, Menu, MenuProps} from "antd";
|
||||
import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { useAiServiceContext} from "../../contexts/AiServiceContext.tsx";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { PERMISSION_DEFINITION } from "@common/const/permissions.ts";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import { getItem } from "@common/utils/navigation.tsx";
|
||||
import { AiServiceConfigFieldType } from "@core/const/ai-service/type.ts";
|
||||
import { MenuItemGroupType, MenuItemType, ItemType } from "antd/es/menu/interface";
|
||||
const APP_MODE = import.meta.env.VITE_APP_MODE;
|
||||
const AiServiceInsidePage: FC = () => {
|
||||
const { message } = App.useApp()
|
||||
const { teamId, serviceId, apiId, routeId, policyId } = useParams<RouterParams>()
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const { fetchData } = useFetch()
|
||||
const { setPrefixForce, setApiPrefix, aiServiceInfo, setAiServiceInfo } = useAiServiceContext()
|
||||
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const navigateTo = useNavigate()
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||
|
||||
const AiServiceInsidePage:FC = ()=> {
|
||||
const { message } = App.useApp()
|
||||
const { teamId,serviceId,apiId, routeId,policyId } = useParams<RouterParams>();
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const {fetchData} = useFetch()
|
||||
const { setPrefixForce,setApiPrefix ,aiServiceInfo ,setAiServiceInfo} = useAiServiceContext()
|
||||
const { accessData,checkPermission,accessInit,state} = useGlobalContext()
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const navigateTo = useNavigate()
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||
const getAiServiceInfo = () => {
|
||||
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('service/info', {
|
||||
method: 'GET',
|
||||
eoParams: { team: teamId, service: serviceId }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setAiServiceInfo(data.service)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getAiServiceInfo = ()=>{
|
||||
fetchData<BasicResponse<{ service:AiServiceConfigFieldType }>>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId}}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setAiServiceInfo(data.service)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
const getApiDefine = () => {
|
||||
setApiPrefix('')
|
||||
setPrefixForce(false)
|
||||
fetchData<BasicResponse<{ prefix: string; force: boolean }>>('service/router/define', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setApiPrefix(data.prefix)
|
||||
setPrefixForce(data.force)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getApiDefine = ()=>{
|
||||
setApiPrefix('')
|
||||
setPrefixForce(false)
|
||||
fetchData<BasicResponse<{ prefix:string, force:boolean }>>('service/router/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setApiPrefix(data.prefix)
|
||||
setPrefixForce(data.force)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const SYSTEM_PAGE_MENU_ITEMS = useMemo(()=>[
|
||||
getItem($t('服务'), 'assets', null,
|
||||
const SYSTEM_PAGE_MENU_ITEMS = useMemo(
|
||||
() => [
|
||||
getItem(
|
||||
$t('服务'),
|
||||
'assets',
|
||||
null,
|
||||
[
|
||||
getItem(<Link to="./route">{$t('API 路由')}</Link>, 'route',undefined,undefined,undefined,'team.service.router.view'),
|
||||
getItem(<Link to="./api">{$t('API 文档')}</Link>, 'api',undefined,undefined,undefined,'team.service.api_doc.view'),
|
||||
getItem(<Link to="./document">{$t('使用说明')}</Link>, 'document',undefined,undefined,undefined,'team.service.service_intro.view'),
|
||||
getItem(<Link to="./servicepolicy">{$t('服务策略')}</Link>, 'servicepolicy', undefined, undefined, undefined, 'team.service.policy.view'),
|
||||
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish',undefined,undefined,undefined,'team.service.release.view'),
|
||||
],
|
||||
'group'),
|
||||
getItem($t('订阅管理'), 'provideSer', null,
|
||||
[
|
||||
getItem(<Link to="./approval">{$t('订阅审核')}</Link>, 'approval',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
getItem(<Link to="./subscriber">{$t('订阅方管理')}</Link>, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
getItem(
|
||||
<Link to="./route">{$t('API 路由')}</Link>,
|
||||
'route',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.router.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./api">{$t('API 文档')}</Link>,
|
||||
'api',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.api_doc.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./document">{$t('使用说明')}</Link>,
|
||||
'document',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.service_intro.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./servicepolicy">{$t('服务策略')}</Link>,
|
||||
'servicepolicy',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.policy.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./publish">{$t('发布')}</Link>,
|
||||
'publish',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.release.view'
|
||||
)
|
||||
],
|
||||
'group'),
|
||||
getItem($t('管理'), 'mng', null,
|
||||
'group'
|
||||
),
|
||||
getItem(
|
||||
$t('订阅管理'),
|
||||
'provideSer',
|
||||
null,
|
||||
[
|
||||
APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.myAiService.topology.view'):null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting',undefined,undefined,undefined,'')],
|
||||
'group'),
|
||||
],[state.language])
|
||||
getItem(
|
||||
<Link to="./approval">{$t('订阅审核')}</Link>,
|
||||
'approval',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.subscription.view'
|
||||
),
|
||||
getItem(
|
||||
<Link to="./subscriber">{$t('订阅方管理')}</Link>,
|
||||
'subscriber',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'team.service.subscription.view'
|
||||
)
|
||||
],
|
||||
'group'
|
||||
),
|
||||
getItem(
|
||||
$t('管理'),
|
||||
'mng',
|
||||
null,
|
||||
[
|
||||
APP_MODE === 'pro'
|
||||
? getItem(
|
||||
<Link to="./topology">{$t('调用拓扑图')}</Link>,
|
||||
'topology',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'project.myAiService.topology.view'
|
||||
)
|
||||
: null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting', undefined, undefined, undefined, '')
|
||||
],
|
||||
'group'
|
||||
)
|
||||
],
|
||||
[state.language]
|
||||
)
|
||||
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const filterMenu = (menu:MenuItemGroupType<MenuItemType>[])=>{
|
||||
const newMenu = cloneDeep(menu)
|
||||
return newMenu!.filter((m:MenuItemGroupType )=>{
|
||||
if(m&&m.children && m.children.length > 0){
|
||||
m.children = m.children.filter(
|
||||
(c)=>{
|
||||
if(!c) return false
|
||||
return (((c as MenuItemType&{access:string} ).access ?
|
||||
checkPermission((c as MenuItemType&{access:string} ).access as keyof typeof PERMISSION_DEFINITION[0]):
|
||||
true))})
|
||||
}
|
||||
return m.children && m.children.length > 0
|
||||
})
|
||||
const menuData = useMemo(() => {
|
||||
const filterMenu = (menu: MenuItemGroupType<MenuItemType>[]) => {
|
||||
const newMenu = cloneDeep(menu)
|
||||
return newMenu!.filter((m: MenuItemGroupType) => {
|
||||
if (m && m.children && m.children.length > 0) {
|
||||
m.children = m.children.filter((c) => {
|
||||
if (!c) return false
|
||||
return (c as MenuItemType & { access: string }).access
|
||||
? checkPermission(
|
||||
(c as MenuItemType & { access: string }).access as keyof (typeof PERMISSION_DEFINITION)[0]
|
||||
)
|
||||
: true
|
||||
})
|
||||
}
|
||||
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
|
||||
const menu = activeMenu ?? filteredMenu[0]?.children ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
|
||||
if(menu && currentUrl.split('/')[-1] !== menu){
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${menu}`)
|
||||
}
|
||||
return filteredMenu || []
|
||||
},[accessData,accessInit, SYSTEM_PAGE_MENU_ITEMS])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// route edit and policy edit page don't need to show menu
|
||||
setShowMenu(!routeId && !currentUrl.includes('route/create') && !policyId &&!currentUrl.includes('servicepolicy/datamasking/create'))
|
||||
return m.children && m.children.length > 0
|
||||
})
|
||||
}
|
||||
const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType<MenuItemType>[])
|
||||
const menu = (activeMenu ?? filteredMenu[0]?.children) ? filteredMenu[0]?.children?.[0]?.key : filteredMenu[0]?.key
|
||||
if (menu && currentUrl.split('/')[-1] !== menu) {
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${menu}`)
|
||||
}
|
||||
return filteredMenu || []
|
||||
}, [accessData, accessInit, SYSTEM_PAGE_MENU_ITEMS])
|
||||
|
||||
if(apiId !== undefined){
|
||||
setActiveMenu('api')
|
||||
} else if(currentUrl.includes('servicepolicy')){
|
||||
setActiveMenu('servicepolicy')
|
||||
} else if(serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]){
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
}else{
|
||||
setActiveMenu('route')
|
||||
}
|
||||
}, [currentUrl]);
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(accessData && checkPermission('team.service.router.view')){
|
||||
getApiDefine()
|
||||
}
|
||||
},[accessData])
|
||||
|
||||
useEffect(()=>{
|
||||
if( activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]){
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${activeMenu}`)
|
||||
}
|
||||
},[activeMenu])
|
||||
|
||||
useEffect(() => {
|
||||
serviceId && getAiServiceInfo()
|
||||
}, [serviceId]);
|
||||
|
||||
return (
|
||||
<>{showMenu ?
|
||||
<InsidePage pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[{label:
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>{$t('服务 ID')}:{serviceId || '-'}</Paragraph>
|
||||
}]}
|
||||
backUrl="/service/list">
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
className="h-full overflow-y-auto"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeMenu!]}
|
||||
mode="inline"
|
||||
items={menuData as unknown as ItemType<MenuItemType>[] }
|
||||
/>
|
||||
<div className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' :''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B ` }>
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>: <Outlet/> }
|
||||
</>
|
||||
useEffect(() => {
|
||||
// route edit and policy edit page don't need to show menu
|
||||
setShowMenu(
|
||||
!routeId &&
|
||||
!currentUrl.includes('route/create') &&
|
||||
!policyId &&
|
||||
!currentUrl.includes('servicepolicy/datamasking/create')
|
||||
)
|
||||
|
||||
if (apiId !== undefined) {
|
||||
setActiveMenu('api')
|
||||
} else if (currentUrl.includes('servicepolicy')) {
|
||||
setActiveMenu('servicepolicy')
|
||||
} else if (serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1])
|
||||
} else {
|
||||
setActiveMenu('route')
|
||||
}
|
||||
}, [currentUrl])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessData && checkPermission('team.service.router.view')) {
|
||||
getApiDefine()
|
||||
}
|
||||
}, [accessData])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]) {
|
||||
navigateTo(`/service/${teamId}/aiInside/${serviceId}/${activeMenu}`)
|
||||
}
|
||||
}, [activeMenu])
|
||||
|
||||
useEffect(() => {
|
||||
serviceId && getAiServiceInfo()
|
||||
}, [serviceId])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showMenu ? (
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
{$t('服务 ID')}:{serviceId || '-'}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
]}
|
||||
backUrl="/service/list"
|
||||
>
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
className="overflow-y-auto h-full"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeMenu!]}
|
||||
mode="inline"
|
||||
items={menuData as unknown as ItemType<MenuItemType>[]}
|
||||
/>
|
||||
<div
|
||||
className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' : ''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B `}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsidePage
|
||||
export default AiServiceInsidePage
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter
|
||||
import EditableTableNotAutoGen from '@common/components/aoplatform/EditableTableNotAutoGen.tsx'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
@@ -16,11 +17,10 @@ import { API_PATH_MATCH_RULES } from '@core/const/system/const'
|
||||
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Switch, Tag } from 'antd'
|
||||
import { App, Button, Form, Input, InputNumber, Row, Space, Spin, Switch, Tag } from 'antd'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
|
||||
type AiServiceRouterField = {
|
||||
name: string
|
||||
@@ -104,8 +104,8 @@ const AiServiceInsideRouterCreate = () => {
|
||||
})
|
||||
.catch((errInfo) => Promise.reject(errInfo))
|
||||
}
|
||||
const isDelete = type === 'apiDetail'
|
||||
const backUrl = isDelete ? `/aiApis/list` : `/service/${teamId}/aiInside/${serviceId}/route`
|
||||
const isAIApiPreview = type === 'apiDetail'
|
||||
const backUrl = isAIApiPreview ? `/aiApis/list` : `/service/${teamId}/aiInside/${serviceId}/route`
|
||||
const openDrawer = (type: 'edit') => {
|
||||
setDrawerType(type)
|
||||
}
|
||||
@@ -210,7 +210,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
}, [])
|
||||
|
||||
const addVariable = () => {
|
||||
if (isDelete) return
|
||||
if (isAIApiPreview) return
|
||||
form.setFieldsValue({
|
||||
variables: [...form.getFieldValue('variables'), { key: '', value: '', require: true }]
|
||||
})
|
||||
@@ -273,7 +273,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
<Button
|
||||
icon={<Icon icon="ic:baseline-tune" height={18} width={18} />}
|
||||
iconPosition="end"
|
||||
disabled={isDelete}
|
||||
disabled={isAIApiPreview}
|
||||
onClick={() => openDrawer('edit')}
|
||||
>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
@@ -285,11 +285,11 @@ const AiServiceInsideRouterCreate = () => {
|
||||
{defaultLlm?.scopes?.map((x) => <Tag>{x?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
</Button>
|
||||
{
|
||||
type !== 'apiDetail' && (<Button type="primary" onClick={onFinish}>
|
||||
{!isAIApiPreview && (
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>)
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -298,7 +298,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
spinning={loading}
|
||||
wrapperClassName=" pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
|
||||
>
|
||||
<WithPermission disabled={isDelete}>
|
||||
<WithPermission disabled={isAIApiPreview}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
@@ -323,22 +323,6 @@ const AiServiceInsideRouterCreate = () => {
|
||||
|
||||
<Form.Item className="flex-1" label={$t('请求路径')}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={apiPathMatchRulesOptions}
|
||||
className="w-[30%] min-w-[100px]"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
name="path"
|
||||
rules={[
|
||||
@@ -365,7 +349,11 @@ const AiServiceInsideRouterCreate = () => {
|
||||
</Row>
|
||||
|
||||
<Form.Item<AiServiceRouterField> label={$t('提示词')} name="prompt">
|
||||
<PromptEditorResizable disabled={isDelete} variablesChange={handleVariablesChange} promptVariables={variablesTable} />
|
||||
<PromptEditorResizable
|
||||
disabled={isAIApiPreview}
|
||||
variablesChange={handleVariablesChange}
|
||||
promptVariables={variablesTable}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
@@ -373,7 +361,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<span>{$t('变量')}</span>
|
||||
<a
|
||||
className={`flex items-center gap-[4px] ${isDelete ? 'cursor-not-allowed' : ''}`}
|
||||
className={`flex items-center gap-[4px] ${isAIApiPreview ? 'cursor-not-allowed' : ''}`}
|
||||
onClick={addVariable}
|
||||
>
|
||||
<Icon icon="ic:baseline-add" width={16} height={16} />
|
||||
|
||||
@@ -101,6 +101,7 @@ const AIFlowChart = () => {
|
||||
id: 'apiService',
|
||||
type: 'serviceCard',
|
||||
position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY },
|
||||
draggable: false,
|
||||
data: {
|
||||
title: 'API Services',
|
||||
count: modelData.length
|
||||
@@ -239,8 +240,10 @@ const AIFlowChart = () => {
|
||||
proOptions={{ hideAttribution: true }}
|
||||
draggable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
elementsSelectable={false}
|
||||
edgeTypes={edgeTypes}
|
||||
zoomOnScroll={false}
|
||||
panOnDrag={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={true}
|
||||
|
||||
@@ -97,9 +97,9 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
|
||||
const getTooltipText = (isChecked: boolean) => {
|
||||
if (!isChecked) {
|
||||
return '保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。'
|
||||
return $t('保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。')
|
||||
}
|
||||
return '保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。'
|
||||
return $t('保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。')
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -177,7 +177,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<span className="text-gray-600">当前调用状态:</span>
|
||||
<span className="text-gray-600">{$t('当前调用状态:')}</span>
|
||||
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
|
||||
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
|
||||
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { t } from 'i18next'
|
||||
import React from 'react'
|
||||
import { useAiSetting } from '../contexts/AiSettingContext'
|
||||
import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types'
|
||||
@@ -59,7 +59,7 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t('默认:')}
|
||||
{$t('默认:')}
|
||||
{defaultLlm}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,6 @@ export const LAYOUT = {
|
||||
SERVICE_NODE_X: 0,
|
||||
NODE_START_Y: 20,
|
||||
NODE_GAP: 120,
|
||||
MODEL_NODE_X: 500,
|
||||
KEY_NODE_X: 900,
|
||||
MODEL_NODE_X: 700,
|
||||
KEY_NODE_X: 1200,
|
||||
} as const;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
.react-flow__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(0px, 0px) !important;
|
||||
}
|
||||
|
||||
.react-flow__renderer {
|
||||
|
||||
@@ -67,7 +67,7 @@ const KeySettings: React.FC = () => {
|
||||
const newEntity = entity as EditAPIKey
|
||||
|
||||
modal.confirm({
|
||||
title: mode === 'add' ? $t(`添加 ${provider?.name} APIKey`) : $t('编辑 APIKey'),
|
||||
title: mode === 'add' ? $t('添加 (0) APIKey', [provider?.name]) : $t('编辑 APIKey'),
|
||||
content: <ApiKeyContent ref={modalRef} entity={newEntity} provider={provider} />,
|
||||
onOk: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -363,6 +363,7 @@ const KeySettings: React.FC = () => {
|
||||
request={requestApiKeys}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
pageListRef.current?.reload()
|
||||
}}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入名称搜索')}
|
||||
|
||||
@@ -1,93 +1,95 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { DynamicMenuItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { getItem } from '@common/utils/navigation'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
import { Menu, MenuProps, Skeleton, message } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { Menu, MenuProps, Skeleton, message } from "antd";
|
||||
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
|
||||
import { DynamicMenuItem, } from "@common/const/type";
|
||||
import { useFetch } from "@common/hooks/http";
|
||||
import { getItem } from "@common/utils/navigation";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
|
||||
import { $t } from "@common/locales";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
const LogSettings = () => {
|
||||
const { moduleId } = useParams<RouterParams>()
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const LogSettings = ()=>{
|
||||
const {moduleId} = useParams<RouterParams>();
|
||||
const [menuItems, setMenuItems ] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const getDynamicMenuList = ()=>{
|
||||
return fetchData<BasicResponse<{ dynamics:DynamicMenuItem[] }>>(`simple/dynamics/log`,{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
|
||||
setMenuItems(data.dynamics)
|
||||
if(!activeMenu || activeMenu.length === 0){
|
||||
navigateTo(`/logsettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
return Promise.resolve(data.dynamics)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const newMenu = menuItems?.map((x:DynamicMenuItem)=>{
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view')
|
||||
})
|
||||
return newMenu
|
||||
},[state.language,menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [ moduleId]);
|
||||
|
||||
useEffect(()=>{
|
||||
setLoading(true)
|
||||
Promise.all([getDynamicMenuList()]).finally(()=>setLoading(false))
|
||||
},[])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('日志配置')}
|
||||
description={'APIPark '+$t("提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。")}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="h-full overflow-y-auto"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`w-full flex flex-1 flex-col h-full overflow-auto bg-MAIN_BG pt-btnbase pl-btnbase pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B overflow-x-hidden`}>
|
||||
<Outlet context={{accessPrefix:'system.devops.log_configuration'}}/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
const getDynamicMenuList = () => {
|
||||
return fetchData<BasicResponse<{ dynamics: DynamicMenuItem[] }>>(`simple/dynamics/log`, { method: 'GET' }).then(
|
||||
(response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMenuItems(data.dynamics)
|
||||
if (!activeMenu || activeMenu.length === 0) {
|
||||
navigateTo(`/logsettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
return Promise.resolve(data.dynamics)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view'
|
||||
)
|
||||
})
|
||||
return newMenu
|
||||
}, [state.language, menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [moduleId])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
Promise.all([getDynamicMenuList()]).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="m-btnbase w-calc-100vw-minus-padding-r" active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('日志配置')}
|
||||
description={'APIPark ' + $t('提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。')}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div
|
||||
className={`flex overflow-auto overflow-x-hidden flex-col flex-1 w-full h-full bg-MAIN_BG pt-btnbase pl-btnbase pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B`}
|
||||
>
|
||||
<Outlet context={{ accessPrefix: 'system.settings.log_configuration' }} />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogSettings;
|
||||
export default LogSettings
|
||||
|
||||
@@ -1,95 +1,89 @@
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { DynamicMenuItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { getItem } from '@common/utils/navigation'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes'
|
||||
import { Menu, MenuProps, Skeleton, message } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { Menu, MenuProps, Skeleton, message } from "antd";
|
||||
import { Link, Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
|
||||
import { DynamicMenuItem } from "@common/const/type";
|
||||
import { useFetch } from "@common/hooks/http";
|
||||
import { getItem } from "@common/utils/navigation";
|
||||
import { RouterParams } from "@core/components/aoplatform/RenderRoutes";
|
||||
import { $t } from "@common/locales";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
|
||||
const LogSettings = () => {
|
||||
const { moduleId } = useParams<RouterParams>()
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const { state } = useGlobalContext()
|
||||
|
||||
const LogSettings = ()=>{
|
||||
const {moduleId} = useParams<RouterParams>();
|
||||
const [menuItems, setMenuItems ] = useState<MenuProps['items']>([])
|
||||
const [activeMenu, setActiveMenu] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const navigateTo = useNavigate()
|
||||
const {state} = useGlobalContext()
|
||||
|
||||
const getDynamicMenuList = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ dynamics:DynamicMenuItem[] }>>(`simple/dynamics/resource`,{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
|
||||
|
||||
setMenuItems(data.dynamics)
|
||||
if(!activeMenu || activeMenu.length === 0){
|
||||
navigateTo(`/resourcesettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).finally(()=>setLoading(false))
|
||||
}
|
||||
const getDynamicMenuList = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ dynamics: DynamicMenuItem[] }>>(`simple/dynamics/resource`, { method: 'GET' })
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMenuItems(data.dynamics)
|
||||
if (!activeMenu || activeMenu.length === 0) {
|
||||
navigateTo(`/resourcesettings/template/${data.dynamics[0].name}`)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const newMenu = menuItems?.map((x:DynamicMenuItem)=>{
|
||||
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view')
|
||||
})
|
||||
return newMenu
|
||||
},[state.language,menuItems])
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'system.settings.log_configuration.view'
|
||||
)
|
||||
})
|
||||
return newMenu
|
||||
}, [state.language, menuItems])
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setActiveMenu(key)
|
||||
}
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = ({key}) => {
|
||||
setActiveMenu(key)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [ moduleId]);
|
||||
|
||||
useEffect(()=>{
|
||||
setLoading(true)
|
||||
getDynamicMenuList()
|
||||
},[])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className='m-btnbase w-calc-100vw-minus-padding-r' active loading={loading}>
|
||||
<InsidePage
|
||||
pageTitle={$t('资源配置')}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="h-full overflow-y-auto"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`w-full flex flex-1 flex-col h-full overflow-auto bg-MAIN_BG`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
useEffect(() => {
|
||||
setActiveMenu(moduleId)
|
||||
}, [moduleId])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
getDynamicMenuList()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="m-btnbase w-calc-100vw-minus-padding-r" active loading={loading}>
|
||||
<InsidePage pageTitle={$t('资源配置')}>
|
||||
<div className="flex h-full">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
selectedKeys={[activeMenu || '']}
|
||||
onClick={onMenuClick}
|
||||
style={{ width: 220 }}
|
||||
mode="inline"
|
||||
items={menuData}
|
||||
/>
|
||||
<div className={`flex overflow-auto flex-col flex-1 w-full h-full bg-MAIN_BG`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</Skeleton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogSettings;
|
||||
export default LogSettings
|
||||
|
||||
@@ -1,251 +1,322 @@
|
||||
import {App, Button, Col, Form, Input, Row, Select, Space, Spin, Switch} from "antd";
|
||||
import {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react";
|
||||
import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx";
|
||||
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
|
||||
import {useFetch} from "@common/hooks/http.ts";
|
||||
import { API_PATH_MATCH_RULES, API_PROTOCOL, HTTP_METHOD, MATCH_CONFIG, MatchPositionEnum, MatchTypeEnum } from "../../../const/system/const.tsx";
|
||||
import { SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps, SystemApiProxyFieldType, SystemInsideApiProxyHandle } from "../../../const/system/type.ts";
|
||||
import { MatchItem, RouterParams } from "@common/const/type.ts";
|
||||
import { validateUrlSlash } from "@common/utils/validate.ts";
|
||||
import { $t } from "@common/locales/index.ts";
|
||||
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy.tsx";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSystemContext } from "@core/contexts/SystemContext.tsx";
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import EditableTableWithModal from '@common/components/aoplatform/EditableTableWithModal.tsx'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage.tsx'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { MatchItem, RouterParams } from '@common/const/type.ts'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate.ts'
|
||||
import { useSystemContext } from '@core/contexts/SystemContext.tsx'
|
||||
import SystemInsideApiProxy from '@core/pages/system/api/SystemInsideApiProxy.tsx'
|
||||
import { App, Button, Col, Form, Input, Row, Select, Space, Spin, Switch } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
API_PATH_MATCH_RULES,
|
||||
API_PROTOCOL,
|
||||
HTTP_METHOD,
|
||||
MATCH_CONFIG,
|
||||
MatchPositionEnum,
|
||||
MatchTypeEnum
|
||||
} from '../../../const/system/const.tsx'
|
||||
import {
|
||||
SystemApiProxyFieldType,
|
||||
SystemInsideApiProxyHandle,
|
||||
SystemInsideRouterCreateHandle,
|
||||
SystemInsideRouterCreateProps
|
||||
} from '../../../const/system/type.ts'
|
||||
|
||||
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,SystemInsideRouterCreateProps>((props, ref) => {
|
||||
const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle, SystemInsideRouterCreateProps>(
|
||||
(props, ref) => {
|
||||
const { message } = App.useApp()
|
||||
const {serviceId, teamId, routeId} = useParams<RouterParams>()
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const { serviceId, teamId, routeId } = useParams<RouterParams>()
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const {apiPrefix, prefixForce} = useSystemContext()
|
||||
const { apiPrefix, prefixForce } = useSystemContext()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const onFinish = ()=>{
|
||||
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([,formValue])=>{
|
||||
const body = {...formValue,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${formValue.path.trim()}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
proxy:{...formValue.proxy,path:formValue.proxy.path ? (formValue.proxy.path.startsWith('/')? formValue.proxy.path: '/'+ formValue.proxy.path) : undefined}}
|
||||
return fetchData<BasicResponse<null>>('service/router',{
|
||||
method: routeId ? 'PUT' :'POST' ,eoBody:(body), eoParams: {service:serviceId,team:teamId, router:routeId },eoTransformKeys:['matchType','disable']}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/inside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch(errInfo=>Promise.reject(errInfo))
|
||||
const onFinish = () => {
|
||||
return Promise.all([proxyRef.current?.validate?.(), form.validateFields()]).then(([, formValue]) => {
|
||||
const body = {
|
||||
...formValue,
|
||||
path: `${prefixForce ? apiPrefix + '/' : ''}${formValue.path.trim()}${formValue.pathMatch === 'prefix' ? '/*' : ''}`,
|
||||
proxy: {
|
||||
...formValue.proxy,
|
||||
path: formValue.proxy.path
|
||||
? formValue.proxy.path.startsWith('/')
|
||||
? formValue.proxy.path
|
||||
: '/' + formValue.proxy.path
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
return fetchData<BasicResponse<null>>('service/router', {
|
||||
method: routeId ? 'PUT' : 'POST',
|
||||
eoBody: body,
|
||||
eoParams: { service: serviceId, team: teamId, router: routeId },
|
||||
eoTransformKeys: ['matchType', 'disable']
|
||||
})
|
||||
}
|
||||
|
||||
const copy: ()=>Promise<boolean | string> = ()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
return form.validateFields().then((value)=>{
|
||||
fetchData<BasicResponse<{api:SystemApiProxyFieldType}>>('service/api/copy',{method:'POST',eoParams:{service:serviceId,team:teamId, api:routeId},eoBody:({...value,path:value.path.trim()})}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return resolve(data.api.id)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
}).catch((errorInfo)=> reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, ()=>({
|
||||
copy,
|
||||
save:onFinish
|
||||
})
|
||||
)
|
||||
|
||||
const getRouterConfig = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{router:SystemApiProxyFieldType}>>('service/router/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const {disable, protocols, path, methods, description, match, proxy} = data.router
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if(prefixForce && path?.startsWith(apiPrefix + '/')){
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if(newPath.endsWith('/*')){
|
||||
newPath = newPath.slice(0,-2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
disable,
|
||||
protocols,
|
||||
path:newPath,
|
||||
pathMatch,
|
||||
methods,
|
||||
description,
|
||||
match,
|
||||
proxy
|
||||
})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
navigator(`/service/${teamId}/inside/${serviceId}/route`)
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
.finally(()=>setLoading(false))
|
||||
})
|
||||
.catch((errInfo) => Promise.reject(errInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const copy: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
fetchData<BasicResponse<{ api: SystemApiProxyFieldType }>>('service/api/copy', {
|
||||
method: 'POST',
|
||||
eoParams: { service: serviceId, team: teamId, api: routeId },
|
||||
eoBody: { ...value, path: value.path.trim() }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return resolve(data.api.id)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
copy,
|
||||
save: onFinish
|
||||
}))
|
||||
|
||||
const getRouterConfig = () => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ router: SystemApiProxyFieldType }>>('service/router/detail', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, router: routeId },
|
||||
eoTransformKeys: ['create_time', 'update_time', 'match_type', 'upstream_id', 'opt_type']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const { disable, protocols, path, methods, description, match, proxy } = data.router
|
||||
let newPath = path
|
||||
let pathMatch = 'full'
|
||||
if (prefixForce && path?.startsWith(apiPrefix + '/')) {
|
||||
newPath = path.slice((apiPrefix?.length || 0) + 1)
|
||||
}
|
||||
if (newPath.endsWith('/*')) {
|
||||
newPath = newPath.slice(0, -2)
|
||||
pathMatch = 'prefix'
|
||||
}
|
||||
form.setFieldsValue({
|
||||
disable,
|
||||
protocols,
|
||||
path: newPath,
|
||||
pathMatch,
|
||||
methods,
|
||||
description,
|
||||
match,
|
||||
proxy
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(routeId){
|
||||
getRouterConfig()
|
||||
}else{
|
||||
form.setFieldValue('prefix',apiPrefix)
|
||||
form.setFieldValue(['proxy','timeout'],10000)
|
||||
form.setFieldValue(['proxy','retry'],0)
|
||||
form.setFieldValue('protocols',['HTTP','HTTPS'])
|
||||
form.setFieldValue('pathMatch','prefix')
|
||||
if (routeId) {
|
||||
getRouterConfig()
|
||||
} else {
|
||||
form.setFieldValue('prefix', apiPrefix)
|
||||
form.setFieldValue(['proxy', 'timeout'], 10000)
|
||||
form.setFieldValue(['proxy', 'retry'], 0)
|
||||
form.setFieldValue('protocols', ['HTTP', 'HTTPS'])
|
||||
form.setFieldValue('pathMatch', 'prefix')
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [])
|
||||
|
||||
const translatedMatchConfig = useMemo(() => {
|
||||
return MATCH_CONFIG.map((item) => {
|
||||
if (item.key === 'position') {
|
||||
return {
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchPositionEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (form.setFieldsValue({}))
|
||||
}, []);
|
||||
|
||||
|
||||
const translatedMatchConfig = useMemo(()=>{
|
||||
return MATCH_CONFIG.map((item)=>{
|
||||
if(item.key === 'position'){
|
||||
return ({...item,component:<Select className="w-INPUT_NORMAL" options={Object.entries(MatchPositionEnum)?.map(([key,value])=>{
|
||||
return { label:$t(value), value:key}
|
||||
})}/>})
|
||||
}
|
||||
if(item.key === 'matchType'){
|
||||
return ({...item, component: <Select className="w-INPUT_NORMAL" options={Object.entries(MatchTypeEnum)?.map(([key,value])=>{
|
||||
return { label:$t(value), value:key}
|
||||
})}/>})
|
||||
}
|
||||
return {...item}
|
||||
})
|
||||
if (item.key === 'matchType') {
|
||||
return {
|
||||
...item,
|
||||
component: (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
options={Object.entries(MatchTypeEnum)?.map(([key, value]) => {
|
||||
return { label: $t(value), value: key }
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
return { ...item }
|
||||
})
|
||||
}, [state.language])
|
||||
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(()=>API_PATH_MATCH_RULES.map(
|
||||
x=>({label:$t(x.label), value:x.value})),[state.language])
|
||||
|
||||
const apiPathMatchRulesOptions = useMemo(
|
||||
() => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })),
|
||||
[state.language]
|
||||
)
|
||||
|
||||
return (
|
||||
<InsidePage pageTitle={ $t('API 路由设置')|| '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/service/${teamId}/inside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex gap-btnbase items-center">
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="SystemInsideRouterCreate"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="mb-btnybase" > <Col ><span className="font-bold mr-[13px]">{$t('API 基础信息')}</span></Col></Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("拦截该接口的请求")}
|
||||
name="disable"
|
||||
extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("请求协议")}
|
||||
name="protocols"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={API_PROTOCOL}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={$t("请求路径")}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Select placeholder={$t(PLACEHOLDER.select)} options={apiPathMatchRulesOptions} className="w-[30%] min-w-[100px]"/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
name="path"
|
||||
rules={[{ required: true,whitespace:true },
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
noStyle
|
||||
>
|
||||
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)} onChange={(e)=>{
|
||||
if((e.target.value as string).endsWith('/*')){
|
||||
form.setFieldValue('path',e.target.value.slice(0,-2))
|
||||
form.setFieldValue('pathMatch','prefix')
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<InsidePage
|
||||
pageTitle={$t('API 路由设置') || '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/service/${teamId}/inside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex items-center gap-btnbase">
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="">
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="SystemInsideRouterCreate"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="mb-btnybase">
|
||||
{' '}
|
||||
<Col>
|
||||
<span className="font-bold mr-[13px]">{$t('API 基础信息')}</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t('拦截该接口的请求')}
|
||||
name="disable"
|
||||
extra={$t('开启拦截后,网关会拦截所有该路径的请求,相当于防火墙禁用了特定路径的访问。')}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("请求方式")}
|
||||
name="methods"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.select)} mode="multiple" options={HTTP_METHOD.map((method:string)=>{
|
||||
return { label:method, value:method}
|
||||
})}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求协议')} name="protocols" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
options={API_PROTOCOL}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={$t('请求路径')}>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="pathMatch"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={apiPathMatchRulesOptions}
|
||||
className="w-[30%] min-w-[100px]"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
name="path"
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
validator: validateUrlSlash
|
||||
}
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
prefix={prefixForce ? `${apiPrefix}/` : '/'}
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
onChange={(e) => {
|
||||
if ((e.target.value as string).endsWith('/*')) {
|
||||
form.setFieldValue('path', e.target.value.slice(0, -2))
|
||||
form.setFieldValue('pathMatch', 'prefix')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('请求方式')} name="methods" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
mode="multiple"
|
||||
options={HTTP_METHOD.map((method: string) => {
|
||||
return { label: method, value: method }
|
||||
})}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
label={$t("高级匹配")}
|
||||
name="match"
|
||||
>
|
||||
<EditableTableWithModal<MatchItem & {_id:string}>
|
||||
configFields={translatedMatchConfig}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('描述')} name="description">
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="mb-btnybase mt-[40px]"><Col ><span className="font-bold mr-[13px]">{$t('转发规则设置')} </span></Col></Row>
|
||||
<Form.Item<SystemApiProxyFieldType>
|
||||
className="mb-0 bg-transparent border-none p-0"
|
||||
name="proxy"
|
||||
>
|
||||
<SystemInsideApiProxy type={routeId ? 'edit' : 'add'} ref={proxyRef} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
<Form.Item<SystemApiProxyFieldType> label={$t('高级匹配')} name="match">
|
||||
<EditableTableWithModal<MatchItem & { _id: string }> configFields={translatedMatchConfig} />
|
||||
</Form.Item>
|
||||
|
||||
<Row className="mb-btnybase mt-[40px]">
|
||||
<Col>
|
||||
<span className="font-bold mr-[13px]">{$t('转发规则设置')} </span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item<SystemApiProxyFieldType> className="p-0 mb-0 bg-transparent border-none" name="proxy">
|
||||
<SystemInsideApiProxy type={routeId ? 'edit' : 'add'} ref={proxyRef} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
)
|
||||
})
|
||||
export default SystemInsideRouterCreate
|
||||
}
|
||||
)
|
||||
export default SystemInsideRouterCreate
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Translation Workflow in Windsurf
|
||||
|
||||
Follow these steps to manage translations in the project:
|
||||
|
||||
1. **Scan for New Translations**
|
||||
* Navigate to the `frontend` directory
|
||||
* Run `pnpm run scan` to detect new translatable content
|
||||
|
||||
2. **Locate New Translation Fields**
|
||||
* Go to `packages/common/src/locales/scan/newJson`
|
||||
* Find the language-specific JSON files (e.g., en-US.json, ja-JP.json)
|
||||
* These files contain the new fields that need translation
|
||||
|
||||
3. **Apply Translations**
|
||||
* After translating the content, go to `packages/common/src/locales/scan`
|
||||
* Open the corresponding language JSON file include ja-JP.json,en-US.json,zh-CH.json,zh-TW.json
|
||||
* Paste the translated content into the appropriate file
|
||||
|
||||
4. **Save and Apply**
|
||||
* Save the file
|
||||
* Changes will take effect immediately
|
||||
* No additional build or restart is required
|
||||
|
||||
Note: Available language files are en-US.json, ja-JP.json, zh-CN.json, and zh-TW.json.
|
||||
Reference in New Issue
Block a user