mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-04 10:13:53 +08:00
feat: 三端合一
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,546 @@
|
||||
'use client'
|
||||
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import localAIPic from '@common/assets/localAI.svg'
|
||||
import onlineAIPic from '@common/assets/onlineAI.svg'
|
||||
import restAPIPic from '@common/assets/restAPI.svg'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { checkAccess } from '@common/utils/permission'
|
||||
import AiSettingModalContent, { AiSettingModalContentHandle } from '@core/pages/aiSetting/AiSettingModal'
|
||||
import LocalAiDeploy, { LocalAiDeployHandle } from '@core/pages/guide/LocalAiDeploy'
|
||||
import RestAIDeploy, { RestAIDeployHandle } from '@core/pages/guide/RestAIDeploy'
|
||||
import useDeployLocalModel from '@core/pages/guide/deployModelUtil'
|
||||
import { App as AppAntd, Button, Card, Collapse } from 'antd'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
function AIModelGuide() {
|
||||
const { message, modal } = AppAntd.useApp()
|
||||
const entityData = useRef<any>(null)
|
||||
const router = useRouter()
|
||||
const { accessData } = useGlobalContext()
|
||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||
const localAiDeployRef = useRef<LocalAiDeployHandle>()
|
||||
const restAiDeployRef = useRef<RestAIDeployHandle>()
|
||||
const { deployLocalModel } = useDeployLocalModel()
|
||||
const { fetchData } = useFetch()
|
||||
const [ollamaAddress, setOllamaAddress] = useState<string>('')
|
||||
|
||||
const dumpServerPage = () => {
|
||||
router.push('/service/list')
|
||||
}
|
||||
|
||||
const restCardClick = async () => {
|
||||
const permission = checkAccess('system.workspace.service.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
modal.confirm({
|
||||
title: $t('添加 Rest 服务'),
|
||||
content: <RestAIDeploy ref={restAiDeployRef}></RestAIDeploy>,
|
||||
onOk: () => {
|
||||
return restAiDeployRef.current?.deployRestAIServer().then((res) => {
|
||||
if (res === true) {
|
||||
dumpServerPage()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const aiCardClick = () => {
|
||||
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
const updateEntityData = (data: any) => {
|
||||
entityData.current = data
|
||||
modalInstance.update({})
|
||||
}
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('模型配置'),
|
||||
content: (
|
||||
<AiSettingModalContent
|
||||
ref={modalRef}
|
||||
modelMode="manual"
|
||||
updateEntityData={updateEntityData}
|
||||
source="guide"
|
||||
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
||||
/>
|
||||
),
|
||||
onOk: () => {
|
||||
return modalRef.current?.deployAIServer().then((res) => {
|
||||
if (res === true) {
|
||||
dumpServerPage()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
footer: (_, { OkBtn, CancelBtn }) => (
|
||||
<div className="flex justify-between items-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={entityData.current?.getApikeyUrl}
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn />
|
||||
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setOllamaAddress(response.data?.config?.address || '')
|
||||
} else {
|
||||
message.error(response.msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const localModelCardClick = async () => {
|
||||
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
if (!ollamaAddress) {
|
||||
router.push('/aisetting?status=unconfigure')
|
||||
return
|
||||
}
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署本地模型'),
|
||||
content: (
|
||||
<LocalAiDeploy
|
||||
ref={localAiDeployRef}
|
||||
onClose={() => {
|
||||
modalInstance.destroy()
|
||||
dumpServerPage()
|
||||
}}
|
||||
></LocalAiDeploy>
|
||||
),
|
||||
onOk: () => {
|
||||
return localAiDeployRef.current?.deployLocalAIServer().then((res) => {
|
||||
if (res === true) {
|
||||
dumpServerPage()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const deployDeepSeek = async (e: any) => {
|
||||
e.stopPropagation()
|
||||
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
if (!ollamaAddress) {
|
||||
router.push('/aisetting?status=unconfigure')
|
||||
return
|
||||
}
|
||||
await deployLocalModel({ modelID: 'deepseek-r1' })
|
||||
dumpServerPage()
|
||||
}
|
||||
|
||||
const cardList = [
|
||||
{
|
||||
imgSrc: restAPIPic,
|
||||
title: $t('添加 Rest 服务'),
|
||||
description: $t('导入OpenAPI文档,将现有系统的API发布到APIPark。'),
|
||||
click: restCardClick
|
||||
},
|
||||
{
|
||||
imgSrc: onlineAIPic,
|
||||
title: $t('添加在线 AI API'),
|
||||
description: $t('添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。'),
|
||||
click: aiCardClick
|
||||
},
|
||||
{
|
||||
imgSrc: localAIPic,
|
||||
title: $t('本地部署 AI 并生成 API'),
|
||||
description: $t('快速在本地部署开源模型并自动生成 API。'),
|
||||
click: localModelCardClick,
|
||||
bottomRender: (
|
||||
<span className="text-[#2196f3] text-[13px] hover:text-[#1976d2]" onClick={deployDeepSeek}>
|
||||
<Icon className="align-sub mr-[5px]" icon="lsicon:lightning-filled" width="15" height="15" />
|
||||
{$t('部署')} Deepseek-R1
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{$t('⚡您可快速通过以下方式开放API供大家使用:')}</p>
|
||||
<div className="mb-[30px] pt-[25px] flex justify-between space-x-4">
|
||||
{cardList.map((item, itemIndex) => (
|
||||
<Card
|
||||
key={itemIndex}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] overflow-visible cursor-pointer flex-1 transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
|
||||
classNames={{
|
||||
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
|
||||
body: 'p-[20px] pt-[50px] pb-[50px] text-[12px] text-[#666] text-center'
|
||||
}}
|
||||
onClick={item.click}
|
||||
>
|
||||
<img src={item.imgSrc} alt="" width={60} height={60} />
|
||||
<p className="text-[13px] font-bold text-black mt-[10px] mb-[10px]">{item.title}</p>
|
||||
<p className="break-words mb-[10px]">{item.description}</p>
|
||||
{item.bottomRender ? item.bottomRender : null}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function QuickGuideContent({
|
||||
changeGuideShow,
|
||||
guideSections
|
||||
}: {
|
||||
changeGuideShow: Dispatch<SetStateAction<boolean>>
|
||||
guideSections: {
|
||||
title: string
|
||||
items: {
|
||||
title: string
|
||||
description: string
|
||||
link: string
|
||||
}[]
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<div className="">
|
||||
{guideSections.map((section, index) => (
|
||||
<div key={index}>
|
||||
<p className="flex gap-[8px] items-center text-[14px] font-bold">
|
||||
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
|
||||
{section.title}
|
||||
</p>
|
||||
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
|
||||
<div
|
||||
className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start"
|
||||
style={{
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
|
||||
gridAutoRows: '1fr'
|
||||
}}
|
||||
>
|
||||
{section.items.map((item, itemIndex) => (
|
||||
<Card
|
||||
key={itemIndex}
|
||||
title={item.title}
|
||||
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
|
||||
classNames={{
|
||||
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
|
||||
body: 'p-[20px] pt-0 text-[12px] text-[#666]'
|
||||
}}
|
||||
onClick={() => window.open(item.link, '_blank')}
|
||||
>
|
||||
<span>{item.description}</span>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-[8px] items-center">
|
||||
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
|
||||
<div className="flex items-center w-full gap-4">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />}
|
||||
iconPosition="end"
|
||||
classNames={{ icon: 'h-[22px] flex items-center' }}
|
||||
href="https://docs.apipark.com"
|
||||
target="_blank"
|
||||
className="text-[14px] font-bold px-0"
|
||||
>
|
||||
{$t('了解更多功能')}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />}
|
||||
onClick={() => changeGuideShow((prev) => !prev)}
|
||||
classNames={{ icon: 'h-[22px] flex items-center' }}
|
||||
className="text-[14px] font-bold"
|
||||
>
|
||||
{$t('隐藏该教程')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function GuidePage() {
|
||||
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false')
|
||||
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false')
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
setShowGuide(window.localStorage.getItem('showGuide') !== 'false')
|
||||
setShowAdvancedGuide(window.localStorage.getItem('showAdvancedGuide') !== 'false')
|
||||
}, [])
|
||||
|
||||
const guideSections = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: $t('快速接入 AI'),
|
||||
items: [
|
||||
{
|
||||
title: $t('配置你的 AI 模型'),
|
||||
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
|
||||
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
|
||||
},
|
||||
{
|
||||
title: $t('创建 AI 服务和 API'),
|
||||
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
|
||||
link: 'https://docs.apipark.com/docs/services/ai_services'
|
||||
},
|
||||
{
|
||||
title: $t('创建调用 Token'),
|
||||
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
|
||||
link: 'https://docs.apipark.com/docs/consumers'
|
||||
},
|
||||
{
|
||||
title: $t('调用'),
|
||||
description: $t('现在你可以通过 Token 来调用这些 API。'),
|
||||
link: 'https://docs.apipark.com/docs/call_api'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: $t('快速接入 REST API'),
|
||||
items: [
|
||||
{
|
||||
title: $t('创建 REST 服务和 API'),
|
||||
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
|
||||
link: 'https://docs.apipark.com/docs/services/rest_services'
|
||||
},
|
||||
{
|
||||
title: $t('创建调用 Token'),
|
||||
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
|
||||
link: 'https://docs.apipark.com/docs/consumers'
|
||||
},
|
||||
{
|
||||
title: $t('调用'),
|
||||
description: $t('现在你可以通过 Token 来调用这些 API。'),
|
||||
link: 'https://docs.apipark.com/docs/call_api'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: $t('仪表盘'),
|
||||
items: [
|
||||
{
|
||||
title: $t('统计 API 调用情况'),
|
||||
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
|
||||
link: 'https://docs.apipark.com/docs/analysis'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[state.language]
|
||||
)
|
||||
|
||||
const advanceGuideSections = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: $t('核心功能'),
|
||||
items: [
|
||||
{
|
||||
title: $t('账号与角色'),
|
||||
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
|
||||
link: 'https://docs.apipark.com/docs/system_setting/account_role'
|
||||
},
|
||||
{
|
||||
title: $t('团队'),
|
||||
description: $t(
|
||||
'团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'
|
||||
),
|
||||
link: 'https://docs.apipark.com/docs/teams'
|
||||
},
|
||||
{
|
||||
title: $t('服务'),
|
||||
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
|
||||
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: $t('权限管理'),
|
||||
items: [
|
||||
{
|
||||
title: $t('订阅服务'),
|
||||
description: $t(
|
||||
'如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'
|
||||
),
|
||||
link: 'https://docs.apipark.com/docs/developer_portal'
|
||||
},
|
||||
{
|
||||
title: $t('审核订阅申请'),
|
||||
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
|
||||
link: 'https://docs.apipark.com/docs/services/review_consumers'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: $t('集成'),
|
||||
items: [
|
||||
{
|
||||
title: $t('日志'),
|
||||
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
|
||||
link: 'https://docs.apipark.com/docs/system_setting/log/'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[state.language]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem('showGuide', showGuide.toString())
|
||||
}, [showGuide])
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
|
||||
}, [showAdvancedGuide])
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === '/guide') {
|
||||
router.replace('/guide/page')
|
||||
}
|
||||
}, [pathname, router])
|
||||
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-auto">
|
||||
<div className="border-[0px] mr-PAGE_INSIDE_X pt-[30px] pl-[40px]">
|
||||
<div className="mb-[30px]">
|
||||
<div className="flex justify-between mb-[20px] items-center">
|
||||
<div className="flex items-center gap-[8px] text-theme text-[26px]">
|
||||
<span>👋</span>
|
||||
<span>{$t('Hello!欢迎使用 APIPark')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[8px]">
|
||||
<p>
|
||||
<span className="font-bold">🦄 APIPark </span>
|
||||
{$t(
|
||||
'是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{$t('✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。')}
|
||||
<span className="font-bold">
|
||||
{$t('点击这里')}
|
||||
<span className="align-middle leading-[16px]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
|
||||
</a>
|
||||
<span className="align-middle leading-[16px]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
{$t('点击')}
|
||||
|
||||
<span className="align-middle leading-[16px]">
|
||||
<Icon icon="emojione:star" width="16" height="16" />
|
||||
</span>
|
||||
Star
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B pl-[40px]">
|
||||
<AIModelGuide />
|
||||
<div className="flex flex-col gap-[15px] pb-PAGE_INSIDE_B">
|
||||
{showGuide && (
|
||||
<Collapse
|
||||
size="large"
|
||||
expandIconPosition="end"
|
||||
defaultActiveKey={['1']}
|
||||
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="">
|
||||
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
|
||||
<span>🚀</span>
|
||||
<span>{`${$t('快速入门')}`}</span>{' '}
|
||||
</p>
|
||||
<p className="text-[12px]">{$t('我们提供了一些任务来帮你快速了解 APIPark')}</p>
|
||||
</div>
|
||||
),
|
||||
children: <QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{showAdvancedGuide && (
|
||||
<Collapse
|
||||
size="large"
|
||||
expandIconPosition="end"
|
||||
defaultActiveKey={['1']}
|
||||
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="">
|
||||
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
|
||||
<span>🏍️</span>
|
||||
<span>{`${$t('进阶教程')}`}</span>{' '}
|
||||
</p>
|
||||
<p className="text-[12px]">{$t('了解 APIPark 如何更好地管理 API 和 AI')}</p>
|
||||
</div>
|
||||
),
|
||||
children: <QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,319 @@
|
||||
'use client'
|
||||
|
||||
import { StyleProvider } from '@ant-design/cssinjs'
|
||||
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import AvatarPic from '@common/assets/default-avatar.png'
|
||||
import Logo from '@common/assets/layout-logo.png'
|
||||
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
|
||||
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions'
|
||||
import { UserInfoType } from '@common/const/type'
|
||||
import { GlobalProvider, useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { LocaleProvider, useLocaleContext } from '@common/contexts/LocaleContext'
|
||||
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
|
||||
import { PluginSlotHubProvider, usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { transformMenuData } from '@common/utils/navigation'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App as AppAntd, Button, ConfigProvider, Dropdown, MenuProps, Spin } from 'antd'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const themeToken = {
|
||||
bgLayout: '#17163E;',
|
||||
header: {
|
||||
heightLayoutHeader: 72
|
||||
},
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 0,
|
||||
paddingInlinePageContainerContent: 0
|
||||
}
|
||||
}
|
||||
|
||||
function AdminShell({ children, project = 'core' }: { children: ReactNode; project?: string }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
|
||||
useGlobalContext()
|
||||
const [currentPath, setCurrentPath] = useState(pathname)
|
||||
const mainPage = state.mainPage || (project === 'core' ? '/guide/page' : '/portal/list')
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
|
||||
const pluginSlotHub = usePluginSlotHub()
|
||||
const { message } = AppAntd.useApp()
|
||||
const [userInfo, setUserInfo] = useState<UserInfoType>()
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
setMenuItems(transformMenuData(menuList))
|
||||
}, [menuList, state.language, accessInit])
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === '/') {
|
||||
router.push(mainPage)
|
||||
}
|
||||
}, [pathname, mainPage, router])
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPath(pathname)
|
||||
}, [pathname])
|
||||
|
||||
const headerMenuData = useMemo(() => {
|
||||
const hasAccess = (access: unknown) => checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
|
||||
|
||||
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
|
||||
return [...menu]
|
||||
.filter((x) => x)
|
||||
.map((item: any) => {
|
||||
if (item.routes && item.routes.length > 0) {
|
||||
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes)
|
||||
if (filteredRoutes.length === 0) {
|
||||
return false
|
||||
}
|
||||
return { ...item, routes: filteredRoutes, name: $t(item.name) }
|
||||
}
|
||||
if (item.access) {
|
||||
return item.access === 'all' || hasAccess(item.access) ? { ...item, name: $t(item.name) } : null
|
||||
}
|
||||
return { ...item, name: $t(item.name) }
|
||||
})
|
||||
.filter((x) => x)
|
||||
}
|
||||
|
||||
const res = [...(menuItems || [])]
|
||||
.filter((x) => x)
|
||||
.map((x: any) =>
|
||||
x.routes ? { ...x, name: $t(x.name), routes: filterMenu(x.routes) } : { ...x, name: $t(x.name) }
|
||||
)
|
||||
|
||||
return {
|
||||
path: '/',
|
||||
routes: res
|
||||
.map((x) => ({ ...x, routes: x.routes?.filter((routeItem: any) => routeItem.access || routeItem.routes?.length > 0) }))
|
||||
.filter((x) => x.access || x.routes?.length > 0)
|
||||
}
|
||||
}, [accessData, state.language, menuItems, checkPermission])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setUserInfo(data.profile)
|
||||
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
getGlobalAccessData()
|
||||
}, [])
|
||||
|
||||
const logOut = () => {
|
||||
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
resetAccess()
|
||||
router.push('/admin/login')
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() =>
|
||||
[
|
||||
!['guest', 'third-user'].includes(userInfo?.type as string) && {
|
||||
key: '2',
|
||||
label: (
|
||||
<Button
|
||||
key="changePsw"
|
||||
type="text"
|
||||
className="flex items-center p-0 bg-transparent border-none"
|
||||
onClick={() => router.push('/userProfile/changepsw')}
|
||||
>
|
||||
{$t('账号设置')}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none" onClick={logOut}>
|
||||
{$t('退出登录')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
].filter(Boolean),
|
||||
[userInfo, router]
|
||||
)
|
||||
|
||||
const actionRender = useMemo(() => {
|
||||
return [
|
||||
<LanguageSetting key="lang" />,
|
||||
<Button
|
||||
key="docs"
|
||||
className="text-[#ffffffb3] hover:text-[#fff] border-none"
|
||||
type="default"
|
||||
ghost
|
||||
onClick={() => window.open('https://docs.apipark.com', '_blank')}
|
||||
>
|
||||
<span className="flex items-center gap-[8px]">
|
||||
<Icon icon="ic:baseline-help" width="14" height="14" />
|
||||
{$t('文档')}
|
||||
</span>
|
||||
</Button>,
|
||||
...(((pluginSlotHub.getSlot('basicLayoutAfterBtns') as ReactNode[]) || []) as ReactNode[])
|
||||
]
|
||||
}, [state.language, pluginSlotHub])
|
||||
|
||||
const logoSrc = typeof Logo === 'string' ? Logo : (Logo as any)?.src
|
||||
const avatarSrc = userInfo?.avatar || (typeof AvatarPic === 'string' ? AvatarPic : (AvatarPic as any)?.src)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="test-pro-layout"
|
||||
style={{
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<ProConfigProvider hashed={false}>
|
||||
<ConfigProvider
|
||||
getTargetContainer={() => {
|
||||
return document.getElementById('test-pro-layout') || document.body
|
||||
}}
|
||||
>
|
||||
<ProLayout
|
||||
prefixCls="apipark-layout"
|
||||
location={{ pathname: currentPath }}
|
||||
siderWidth={220}
|
||||
breakpoint={'lg'}
|
||||
route={headerMenuData as any}
|
||||
token={themeToken}
|
||||
siderMenuType="group"
|
||||
menu={{ type: 'group', collapsedShowGroupTitle: true }}
|
||||
disableMobile={true}
|
||||
avatarProps={{
|
||||
src: avatarSrc,
|
||||
size: 'small',
|
||||
title: userInfo?.username || 'unknown',
|
||||
render: (props, dom) => (
|
||||
<Dropdown menu={{ items }}>
|
||||
<div className="avatar-dom">{dom}</div>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
actionsRender={(props) => {
|
||||
if (props.isMobile) return []
|
||||
return actionRender
|
||||
}}
|
||||
headerTitleRender={() => (
|
||||
<div className="w-[192px] flex items-center">
|
||||
<img className="h-[20px] cursor-pointer" src={logoSrc} onClick={() => router.push(mainPage)} alt="logo" />
|
||||
<a
|
||||
className="align-text-top ml-[5px] h-[25px] relative"
|
||||
href="https://github.com/APIParkLab/APIPark"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social"
|
||||
className="absolute top-[6px]"
|
||||
width={75}
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
logo={logoSrc}
|
||||
pageTitleRender={() => $t('APIPark')}
|
||||
menuFooterRender={(props) => {
|
||||
if (props?.collapsed) return undefined
|
||||
}}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (
|
||||
item.key &&
|
||||
routerKeyMap.get(item.key as string) &&
|
||||
(routerKeyMap.get(item.key as string) as string[])?.length > 0 &&
|
||||
(routerKeyMap.get(item.key as string) as string[])?.indexOf(currentPath.split('/')[1]) !== -1
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (item.key === currentPath.split('/')[1]) {
|
||||
return
|
||||
}
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
setCurrentPath(item.path || '')
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</div>
|
||||
)}
|
||||
fixSiderbar={true}
|
||||
layout="mix"
|
||||
splitMenus={true}
|
||||
collapsed={false}
|
||||
collapsedButtonRender={false}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-calc-100vh-minus-navbar ${currentPath.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
|
||||
} ${currentPath.startsWith('/guide/page') ? '' : 'pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ProLayout>
|
||||
</ConfigProvider>
|
||||
</ProConfigProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AdminProviders({ children }: { children: ReactNode }) {
|
||||
const { locale } = useLocaleContext()
|
||||
|
||||
return (
|
||||
<StyleProvider hashPriority="high">
|
||||
<ConfigProvider locale={locale} wave={{ disabled: true }}>
|
||||
<PluginEventHubProvider>
|
||||
<GlobalProvider>
|
||||
<AppAntd className="h-full" message={{ maxCount: 1 }}>
|
||||
<PluginSlotHubProvider>
|
||||
<AdminShell project="core">{children}</AdminShell>
|
||||
</PluginSlotHubProvider>
|
||||
</AppAntd>
|
||||
</GlobalProvider>
|
||||
</PluginEventHubProvider>
|
||||
</ConfigProvider>
|
||||
</StyleProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={true}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleProvider>
|
||||
<AdminProviders>{children}</AdminProviders>
|
||||
</LocaleProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function ServiceLegacyFallbackPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const { slug } = await params
|
||||
|
||||
if (slug.length >= 4 && (slug[1] === 'inside' || slug[1] === 'aiInside')) {
|
||||
redirect(`/service/${slug[0]}/${slug[1]}/${slug[2]}/overview`)
|
||||
}
|
||||
|
||||
redirect('/service/list')
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function AiServiceApprovalRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="aiInside" type="approval" />
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ReactNode } from 'react'
|
||||
import { ServiceDetailLayout } from '../../../_components/ServicePages'
|
||||
|
||||
const serviceKeys = [
|
||||
'overview',
|
||||
'route',
|
||||
'api',
|
||||
'document',
|
||||
'servicepolicy',
|
||||
'publish',
|
||||
'approval',
|
||||
'subscriber',
|
||||
'setting',
|
||||
'logs'
|
||||
] as const
|
||||
|
||||
function getActiveKey(pathname: string) {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const active = segments[4]
|
||||
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
|
||||
}
|
||||
|
||||
export default function AiServiceDetailLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: ReactNode
|
||||
params: { teamId: string; serviceId: string }
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const { teamId, serviceId } = params
|
||||
|
||||
return (
|
||||
<ServiceDetailLayout
|
||||
teamId={teamId}
|
||||
serviceId={serviceId}
|
||||
side="aiInside"
|
||||
activeKey={getActiveKey(pathname)}
|
||||
>
|
||||
{children}
|
||||
</ServiceDetailLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function AiServiceOverviewRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceOverviewPage serviceType="aiService" teamId={teamId} serviceId={serviceId} />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function AiServiceEntryPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
redirect(`/service/${teamId}/aiInside/${serviceId}/overview`)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function AiServicePublishRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="aiInside" type="publish" />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function AiServiceRouteListRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="aiInside" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function RestServiceApprovalRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="inside" type="approval" />
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ReactNode } from 'react'
|
||||
import { ServiceDetailLayout } from '../../../_components/ServicePages'
|
||||
|
||||
const serviceKeys = [
|
||||
'overview',
|
||||
'route',
|
||||
'api',
|
||||
'upstream',
|
||||
'document',
|
||||
'servicepolicy',
|
||||
'publish',
|
||||
'approval',
|
||||
'subscriber',
|
||||
'setting',
|
||||
'logs'
|
||||
] as const
|
||||
|
||||
function getActiveKey(pathname: string) {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const active = segments[4]
|
||||
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
|
||||
}
|
||||
|
||||
export default function RestServiceDetailLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: ReactNode
|
||||
params: { teamId: string; serviceId: string }
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const { teamId, serviceId } = params
|
||||
|
||||
return (
|
||||
<ServiceDetailLayout
|
||||
teamId={teamId}
|
||||
serviceId={serviceId}
|
||||
side="inside"
|
||||
activeKey={getActiveKey(pathname)}
|
||||
>
|
||||
{children}
|
||||
</ServiceDetailLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function RestServiceOverviewPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceOverviewPage serviceType="restService" teamId={teamId} serviceId={serviceId} />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function RestServiceEntryPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
redirect(`/service/${teamId}/inside/${serviceId}/overview`)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function RestServicePublishRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="inside" type="publish" />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function RestServiceRouteListRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="inside" />
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { $t } from '@common/locales'
|
||||
import { SYSTEM_INSIDE_APPROVAL_TAB_ITEMS, SYSTEM_PUBLISH_TAB_ITEMS } from '@core/const/system/const'
|
||||
import AiServiceInsideApprovalList from '@core/pages/aiService/approval/AiServiceInsideApprovalList'
|
||||
import AiServiceInsidePublishList from '@core/pages/aiService/publish/AiServiceInsidePublishList'
|
||||
import SystemInsideApprovalList from '@core/pages/system/approval/SystemInsideApprovalList'
|
||||
import SystemInsidePublishList from '@core/pages/system/publish/SystemInsidePublishList'
|
||||
import { Tabs } from 'antd'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||
|
||||
type ServiceDetailLegacyTabsProps = {
|
||||
side: 'inside' | 'aiInside'
|
||||
type: 'approval' | 'publish'
|
||||
}
|
||||
|
||||
function buildTabHref(pathname: string, searchParams: URLSearchParams, key: string) {
|
||||
const nextSearchParams = new URLSearchParams(searchParams.toString())
|
||||
|
||||
if (key === '0') {
|
||||
nextSearchParams.delete('status')
|
||||
} else {
|
||||
nextSearchParams.set('status', key)
|
||||
}
|
||||
|
||||
const nextQuery = nextSearchParams.toString()
|
||||
return nextQuery ? `${pathname}?${nextQuery}` : pathname
|
||||
}
|
||||
|
||||
function LegacyRouteBridge({
|
||||
pathname,
|
||||
search,
|
||||
routeType,
|
||||
element
|
||||
}: {
|
||||
pathname: string
|
||||
search: string
|
||||
routeType: 'approval' | 'publish'
|
||||
element: ReactElement
|
||||
}) {
|
||||
const entry = `${pathname}${search ? `?${search}` : ''}`
|
||||
const routePath = `/service/:teamId/:side/:serviceId/${routeType}`
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[entry]} key={entry}>
|
||||
<Routes>
|
||||
<Route path={routePath} element={element} />
|
||||
<Route path={`${routePath}/*`} element={element} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceDetailLegacyTabs({ side, type }: ServiceDetailLegacyTabsProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const { state } = useGlobalContext()
|
||||
const status = searchParams.get('status') || '0'
|
||||
const search = searchParams.toString()
|
||||
|
||||
const tabItems = useMemo(
|
||||
() =>
|
||||
(type === 'approval' ? SYSTEM_INSIDE_APPROVAL_TAB_ITEMS : SYSTEM_PUBLISH_TAB_ITEMS)?.map((item) => ({
|
||||
...item,
|
||||
label: typeof item?.label === 'string' ? $t(item.label) : item?.label
|
||||
})),
|
||||
[type, state.language]
|
||||
)
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (type === 'approval') {
|
||||
return side === 'aiInside' ? <AiServiceInsideApprovalList /> : <SystemInsideApprovalList />
|
||||
}
|
||||
|
||||
return side === 'aiInside' ? <AiServiceInsidePublishList /> : <SystemInsidePublishList />
|
||||
}, [side, type])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
activeKey={status}
|
||||
size="small"
|
||||
className="h-auto bg-MAIN_BG"
|
||||
tabBarStyle={{ paddingLeft: '10px' }}
|
||||
tabBarGutter={20}
|
||||
items={tabItems}
|
||||
destroyInactiveTabPane={true}
|
||||
onChange={(key) => {
|
||||
router.push(buildTabHref(pathname, new URLSearchParams(search), key))
|
||||
}}
|
||||
/>
|
||||
<LegacyRouteBridge pathname={pathname} search={search} routeType={type} element={content} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
'use client'
|
||||
|
||||
import PageList from '@common/components/aoplatform/PageList'
|
||||
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
|
||||
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { SimpleMemberItem, SimpleTeamItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { App as AppAntd, Card, Menu, MenuProps, Spin, Tag } from 'antd'
|
||||
import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const'
|
||||
import { AiServiceRouterTableListItem } from '@core/const/ai-service/type'
|
||||
import { SERVICE_KIND_OPTIONS, SYSTEM_API_TABLE_COLUMNS, SYSTEM_TABLE_COLUMNS } from '@core/const/system/const'
|
||||
import { SystemApiTableListItem, SystemTableListItem } from '@core/const/system/type'
|
||||
import RankingList from '@core/pages/serviceOverview/rankingList/RankingList'
|
||||
import ServiceAreaChart from '@core/pages/serviceOverview/charts/ServiceAreaChart'
|
||||
import ServiceBarChar, { BarChartInfo } from '@core/pages/serviceOverview/charts/ServiceBarChar'
|
||||
import DateSelectFilter, { TimeOption } from '@core/pages/serviceOverview/filter/DateSelectFilter'
|
||||
import { setBarChartInfoData } from '@core/pages/serviceOverview/utils'
|
||||
import { LogsFooter } from '@core/pages/system/serviceDeployment/ServiceDeployMentFooter'
|
||||
import { ServiceDeployment } from '@core/pages/system/serviceDeployment/ServiceDeployment'
|
||||
import {
|
||||
abbreviateFloat,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatNumberWithUnit,
|
||||
getTime
|
||||
} from '@dashboard/utils/dashboard'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export type ServiceSide = 'inside' | 'aiInside'
|
||||
export type ServiceMenuKey =
|
||||
| 'overview'
|
||||
| 'route'
|
||||
| 'api'
|
||||
| 'upstream'
|
||||
| 'document'
|
||||
| 'servicepolicy'
|
||||
| 'publish'
|
||||
| 'approval'
|
||||
| 'subscriber'
|
||||
| 'setting'
|
||||
| 'logs'
|
||||
|
||||
function ServiceOverviewIndicator({
|
||||
indicatorInfo,
|
||||
onNavigate
|
||||
}: {
|
||||
indicatorInfo: any
|
||||
onNavigate: (path: string) => void
|
||||
}) {
|
||||
const side = indicatorInfo?.serviceKind === 'ai' ? 'aiInside' : 'inside'
|
||||
const items = [
|
||||
{
|
||||
title: indicatorInfo?.enableMcp ? 'APIs / Tools' : 'APIs',
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/route`,
|
||||
content: indicatorInfo?.apiNum ?? 0
|
||||
},
|
||||
{
|
||||
title: $t('订阅数量'),
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/subscriber`,
|
||||
content: indicatorInfo?.subscriberNum ?? 0
|
||||
},
|
||||
{
|
||||
title: 'MCP',
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/setting`,
|
||||
content: indicatorInfo?.enableMcp ? $t('已开启') : $t('开启 MCP')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={item.title}
|
||||
className={`flex-1 rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[20px] px-[18px]' }}
|
||||
onClick={() => {
|
||||
if (item.link) {
|
||||
onNavigate(item.link)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-[14px] text-[#999999] mb-[10px]" style={{ fontFamily: 'Microsoft YaHei' }}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div
|
||||
className={`${index < 2 ? 'text-[32px] font-medium text-[#101010]' : 'text-[14px]'}`}
|
||||
style={{ fontFamily: 'Microsoft YaHei' }}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceOverviewPage({
|
||||
serviceType,
|
||||
teamId,
|
||||
serviceId
|
||||
}: {
|
||||
serviceType: 'aiService' | 'restService'
|
||||
teamId: string
|
||||
serviceId: string
|
||||
}) {
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = AppAntd.useApp()
|
||||
const { state } = useGlobalContext()
|
||||
const router = useRouter()
|
||||
const [dashboardLoading, setDashboardLoading] = useState(true)
|
||||
const [defaultTime] = useState<TimeOption>('day')
|
||||
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
|
||||
const [barChartInfo, setBarChartInfo] = useState<any>()
|
||||
const [perBarChartInfo, setPerBarChartInfo] = useState<any>()
|
||||
const [indicatorInfo, setIndicatorInfo] = useState<any>([])
|
||||
const [topRankingList, setTopRankingList] = useState<any>([])
|
||||
const [aiServiceOverview, setAiServiceOverview] = useState<any>()
|
||||
const [restServiceOverview, setRestServiceOverview] = useState<any>()
|
||||
|
||||
const selectCallback = (date: TimeRange) => {
|
||||
setTimeRange(date)
|
||||
}
|
||||
|
||||
const setRestChartInfo = (serviceOverview: any) => {
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceKind: serviceOverview.serviceKind,
|
||||
serviceId
|
||||
})
|
||||
setBarChartInfo([
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('请求次数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: formatNumberWithUnit(serviceOverview.requestTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
|
||||
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
|
||||
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('网络流量'),
|
||||
data: serviceOverview.trafficOverview,
|
||||
value: formatBytes(serviceOverview.trafficTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
traffic2xxTotal: formatBytes(serviceOverview.traffic2xxTotal),
|
||||
traffic4xxTotal: formatBytes(serviceOverview.traffic4xxTotal),
|
||||
traffic5xxTotal: formatBytes(serviceOverview.traffic5xxTotal)
|
||||
}
|
||||
])
|
||||
setPerBarChartInfo([
|
||||
{
|
||||
title: $t('平均响应时间'),
|
||||
data: serviceOverview.avgResponseTimeOverview,
|
||||
value: formatDuration(serviceOverview.avgResponseTime),
|
||||
originValue: serviceOverview.avgResponseTime,
|
||||
date: serviceOverview.date,
|
||||
max: formatDuration(serviceOverview.maxResponseTime),
|
||||
min: formatDuration(serviceOverview.minResponseTime),
|
||||
type: 'area',
|
||||
showXAxis: false
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的请求次数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
date: serviceOverview.date,
|
||||
showXAxis: false
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的网络流量'),
|
||||
data: serviceOverview.avgTrafficPerSubscriberOverview,
|
||||
date: serviceOverview.date,
|
||||
showXAxis: false
|
||||
}),
|
||||
max: formatBytes(serviceOverview.maxTrafficPerSubscriber),
|
||||
min: formatBytes(serviceOverview.minTrafficPerSubscriber)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const setAiChartInfo = (serviceOverview: any) => {
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceKind: serviceOverview.serviceKind,
|
||||
serviceId
|
||||
})
|
||||
setBarChartInfo([
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('请求次数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: formatNumberWithUnit(serviceOverview.requestTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
|
||||
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
|
||||
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('Token 消耗'),
|
||||
data: serviceOverview.tokenOverview.map((item: { inputToken: number; outputToken: number }) => ({
|
||||
inputToken: item.inputToken,
|
||||
outputToken: item.outputToken
|
||||
})),
|
||||
value: formatNumberWithUnit(serviceOverview.tokenTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
inputTokenTotal: formatNumberWithUnit(serviceOverview.inputTokenTotal),
|
||||
outputTokenTotal: formatNumberWithUnit(serviceOverview.outputTokenTotal)
|
||||
}
|
||||
])
|
||||
setPerBarChartInfo([
|
||||
{
|
||||
title: $t('平均 Token 消耗'),
|
||||
data: serviceOverview.avgTokenOverview,
|
||||
value: `${formatNumberWithUnit(serviceOverview.avgToken)} Token/s`,
|
||||
originValue: serviceOverview.avgToken,
|
||||
date: serviceOverview.date,
|
||||
min: `${formatNumberWithUnit(serviceOverview.minToken)} Token/s`,
|
||||
max: `${formatNumberWithUnit(serviceOverview.maxToken)} Token/s`,
|
||||
type: 'area'
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的请求次数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的 Token 消耗'),
|
||||
data: serviceOverview.avgTokenPerSubscriberOverview.map((item: { inputToken: number; outputToken: number }) => ({
|
||||
inputToken: item.inputToken,
|
||||
outputToken: item.outputToken
|
||||
})),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxTokenPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minTokenPerSubscriber)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const getAIServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/ai', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'token_overview',
|
||||
'avg_token_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_token_per_subscriber_overview',
|
||||
'request_total',
|
||||
'token_total',
|
||||
'avg_token',
|
||||
'max_token',
|
||||
'min_token',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_token_per_subscriber',
|
||||
'input_token',
|
||||
'output_token',
|
||||
'total_token',
|
||||
'request_2xx_total',
|
||||
'request_4xx_total',
|
||||
'request_5xx_total',
|
||||
'input_token_total',
|
||||
'output_token_total',
|
||||
'max_token_per_subscriber',
|
||||
'min_token_per_subscriber',
|
||||
'max_request_per_subscriber',
|
||||
'min_request_per_subscriber'
|
||||
]
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setAiServiceOverview(data.overview)
|
||||
setAiChartInfo(data.overview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const getRestServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/rest', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'traffic_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_response_time_overview',
|
||||
'avg_traffic_per_subscriber_overview',
|
||||
'request_total',
|
||||
'traffic_total',
|
||||
'max_response_time',
|
||||
'min_response_time',
|
||||
'avg_response_time',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_traffic_per_subscriber',
|
||||
'request_2xx_total',
|
||||
'request_4xx_total',
|
||||
'request_5xx_total',
|
||||
'traffic_2xx_total',
|
||||
'traffic_4xx_total',
|
||||
'traffic_5xx_total',
|
||||
'max_request_per_subscriber',
|
||||
'min_request_per_subscriber',
|
||||
'max_traffic_per_subscriber',
|
||||
'min_traffic_per_subscriber'
|
||||
]
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setRestServiceOverview(data.overview)
|
||||
setRestChartInfo(data.overview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const getTopRankingList = () => {
|
||||
fetchData<BasicResponse<any>>('service/monitor/top10', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTopRankingList({
|
||||
'TOP API': data.apis,
|
||||
'TOP Consumer': data.consumers
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { startTime, endTime } = getTime(defaultTime, [])
|
||||
setTimeRange({ start: startTime, end: endTime })
|
||||
}, [defaultTime])
|
||||
|
||||
useEffect(() => {
|
||||
if (timeRange) {
|
||||
setDashboardLoading(true)
|
||||
if (serviceType === 'aiService') {
|
||||
getAIServiceOverview()
|
||||
} else {
|
||||
getRestServiceOverview()
|
||||
}
|
||||
getTopRankingList()
|
||||
}
|
||||
}, [timeRange])
|
||||
|
||||
useEffect(() => {
|
||||
if (serviceType === 'aiService') {
|
||||
if (aiServiceOverview) {
|
||||
setAiChartInfo(aiServiceOverview)
|
||||
}
|
||||
} else if (restServiceOverview) {
|
||||
setRestChartInfo(restServiceOverview)
|
||||
}
|
||||
}, [state.language])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName="h-full min-h-[150px]"
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<div className="mr-[30px]">
|
||||
<ServiceOverviewIndicator indicatorInfo={indicatorInfo} onNavigate={(path) => router.push(path)} />
|
||||
<div className="mt-[20px]">
|
||||
<DateSelectFilter selectCallback={selectCallback} defaultTime={defaultTime} />
|
||||
</div>
|
||||
<div className="mt-[20px] flex mb-[10px]">
|
||||
{barChartInfo?.map((item: BarChartInfo, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 min-w-[430px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[15px] px-[0px]' }}
|
||||
>
|
||||
<ServiceBarChar showLegendIndicator={true} height={400} dataInfo={item} customClassNames="flex-1" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex mb-[10px]">
|
||||
{perBarChartInfo?.map((item: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 rounded-[10px] min-w-[284px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[15px] px-[0px]' }}
|
||||
>
|
||||
{item.type === 'area' ? (
|
||||
<ServiceAreaChart
|
||||
height={270}
|
||||
dataInfo={item}
|
||||
showAvgLine={true}
|
||||
customClassNames="flex-1 relative"
|
||||
/>
|
||||
) : (
|
||||
<ServiceBarChar height={270} dataInfo={item} hideIndicatorValue={true} customClassNames="flex-1" />
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<RankingList topRankingList={topRankingList} serviceType={serviceType} />
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceRouteListPage({
|
||||
teamId,
|
||||
serviceId,
|
||||
side
|
||||
}: {
|
||||
teamId: string
|
||||
serviceId: string
|
||||
side: ServiceSide
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { fetchData } = useFetch()
|
||||
const { modal, message } = AppAntd.useApp()
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const [searchWord, setSearchWord] = useState('')
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const [tableListDataSource, setTableListDataSource] = useState<Array<SystemApiTableListItem | AiServiceRouterTableListItem>>([])
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const isAiService = side === 'aiInside'
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true)
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const getMemberList = async () => {
|
||||
setMemberValueEnum([])
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
|
||||
method: 'GET'
|
||||
})
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMemberValueEnum(data.members)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId, side])
|
||||
|
||||
const getRoutesList = (): Promise<{ data: Array<SystemApiTableListItem | AiServiceRouterTableListItem>; success: boolean }> => {
|
||||
if (!tableHttpReload) {
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({ data: tableListDataSource, success: true })
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<any>>(isAiService ? 'service/ai-routers' : 'service/routers', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, keyword: searchWord },
|
||||
eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const items = isAiService ? data.apis : data.routers
|
||||
setTableListDataSource(items)
|
||||
setTableHttpReload(false)
|
||||
return { data: items, success: true }
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
})
|
||||
.catch(() => ({ data: [], success: false }))
|
||||
}
|
||||
|
||||
const deleteRoute = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(isAiService ? 'service/ai-router' : 'service/router', {
|
||||
method: 'DELETE',
|
||||
eoParams: { service: serviceId, team: teamId, router: entity.id }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const openDeleteModal = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
content: $t('确认删除该数据?'),
|
||||
onOk: () =>
|
||||
deleteRoute(entity).then((res) => {
|
||||
if (res === true) {
|
||||
manualReloadTable()
|
||||
}
|
||||
}),
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const routeColumns = useMemo(() => {
|
||||
const baseColumns = (isAiService ? AI_SERVICE_ROUTER_TABLE_COLUMNS : SYSTEM_API_TABLE_COLUMNS).map((column) => {
|
||||
const nextColumn = { ...column }
|
||||
const dataIndex = nextColumn.dataIndex as string[] | string | undefined
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('creator')) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
memberValueEnum.forEach((item) => {
|
||||
valueEnum[item.name] = { text: item.name }
|
||||
})
|
||||
nextColumn.valueEnum = valueEnum
|
||||
}
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && (dataIndex.includes('disable') || dataIndex.includes('disabled'))) {
|
||||
nextColumn.valueEnum = {
|
||||
true: { text: <span className="text-red-500">{$t('拦截')}</span> },
|
||||
false: { text: <span className="text-green-500">{$t('放行')}</span> }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...nextColumn,
|
||||
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
...baseColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'option',
|
||||
btnNums: 2,
|
||||
fixed: 'right' as const,
|
||||
valueType: 'option' as const,
|
||||
render: (_: ReactNode, entity: SystemApiTableListItem | AiServiceRouterTableListItem) => [
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.edit"
|
||||
key="edit"
|
||||
btnType="edit"
|
||||
onClick={() => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/${entity.id}`)
|
||||
}}
|
||||
btnTitle="编辑"
|
||||
/>,
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.delete"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => {
|
||||
openDeleteModal(entity)
|
||||
}}
|
||||
btnTitle="删除"
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
}, [isAiService, memberValueEnum, state.language, router, teamId, side, serviceId])
|
||||
|
||||
return (
|
||||
<PageList
|
||||
id={`service_route_${side}`}
|
||||
ref={pageListRef}
|
||||
columns={routeColumns as any}
|
||||
request={() => getRoutesList()}
|
||||
dataSource={tableListDataSource}
|
||||
addNewBtnTitle={$t('添加路由')}
|
||||
searchPlaceholder={$t('输入 URL 查找路由')}
|
||||
onAddNewBtnClick={() => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/create`)
|
||||
}}
|
||||
addNewBtnAccess="team.service.router.add"
|
||||
tableClickAccess="team.service.router.view"
|
||||
manualReloadTable={manualReloadTable}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onRowClick={(row: SystemApiTableListItem | AiServiceRouterTableListItem) =>
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/${row.id}`)
|
||||
}
|
||||
tableClass="mr-PAGE_INSIDE_X"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceListPage() {
|
||||
const router = useRouter()
|
||||
const { message, modal } = AppAntd.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { checkPermission, accessInit, getGlobalAccessData, state } = useGlobalContext()
|
||||
const [tableSearchWord, setTableSearchWord] = useState('')
|
||||
const [teamList, setTeamList] = useState<{ [k: string]: { text: string } }>()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<SystemTableListItem[]>([])
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({})
|
||||
const [stateColumnMap] = useState<{ [k: string]: { text: string; className?: string } }>({
|
||||
normal: { text: '正常' },
|
||||
deploying: { text: '部署中', className: 'text-[#2196f3]' },
|
||||
error: { text: '异常', className: 'text-[#ff4d4f]' },
|
||||
public: { text: '公共服务' },
|
||||
private: { text: '私有服务' }
|
||||
})
|
||||
|
||||
const getSystemList = () => {
|
||||
if (!accessInit) {
|
||||
getGlobalAccessData()?.then?.(() => {
|
||||
getSystemList()
|
||||
})
|
||||
return Promise.resolve({ data: [], success: false })
|
||||
}
|
||||
|
||||
if (!tableHttpReload) {
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({ data: tableListDataSource, success: true })
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<{ services: SystemTableListItem[] }>>(
|
||||
!checkPermission('system.workspace.service.view_all') ? 'my_services' : 'services',
|
||||
{
|
||||
method: 'GET',
|
||||
eoParams: { keyword: tableSearchWord },
|
||||
eoTransformKeys: ['api_num', 'service_num', 'create_time']
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTableListDataSource(data.services)
|
||||
setTableHttpReload(false)
|
||||
return { data: data.services, success: true }
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
})
|
||||
.catch(() => ({ data: [], success: false }))
|
||||
}
|
||||
|
||||
const getTeamsList = () => {
|
||||
if (!accessInit) {
|
||||
getGlobalAccessData()?.then?.(() => {
|
||||
getTeamsList()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
|
||||
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
|
||||
{ method: 'GET', eoTransformKeys: [] }
|
||||
).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
data.teams?.forEach((x: SimpleMemberItem) => {
|
||||
valueEnum[x.name] = { text: x.name }
|
||||
})
|
||||
setTeamList(valueEnum)
|
||||
return
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
const getMemberList = async () => {
|
||||
setMemberValueEnum({})
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
data.members?.forEach((x: SimpleMemberItem) => {
|
||||
valueEnum[x.name] = { text: x.name }
|
||||
})
|
||||
setMemberValueEnum(valueEnum)
|
||||
return
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true)
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const openLogsModal = (record: SystemTableListItem) => {
|
||||
const closeModal = (reload = true) => {
|
||||
modalInstance.destroy()
|
||||
if (reload) {
|
||||
manualReloadTable()
|
||||
}
|
||||
}
|
||||
|
||||
const updateFooter = () => {
|
||||
record.state = 'error'
|
||||
modalInstance.update({})
|
||||
}
|
||||
|
||||
let cancelCb: () => void = () => {}
|
||||
const cancel = (cb: () => void) => {
|
||||
cancelCb = cb
|
||||
}
|
||||
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署过程'),
|
||||
content: <ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />,
|
||||
footer: () => <LogsFooter record={record} closeModal={closeModal} />,
|
||||
afterClose: () => {
|
||||
cancelCb()
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTeamsList()
|
||||
getMemberList()
|
||||
}, [])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return SYSTEM_TABLE_COLUMNS.map((column) => {
|
||||
const nextColumn = { ...column }
|
||||
const dataIndex = nextColumn.dataIndex as string | string[] | undefined
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('master')) {
|
||||
nextColumn.valueEnum = memberValueEnum
|
||||
}
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('team')) {
|
||||
nextColumn.valueEnum = teamList
|
||||
}
|
||||
|
||||
if (nextColumn.dataIndex === 'service_kind') {
|
||||
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem & { enable_mcp?: boolean }) => (
|
||||
<span className="text-[13px]">
|
||||
<Tag
|
||||
color={`#${record.service_kind === 'ai' ? 'EADEFF' : 'DEFFE7'}`}
|
||||
className="text-[#000] font-normal border-0 mr-[10px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title={record.service_kind || '-'}
|
||||
>
|
||||
{SERVICE_KIND_OPTIONS.find((item) => item.value === record.service_kind)?.label || '-'}
|
||||
</Tag>
|
||||
{record.enable_mcp && (
|
||||
<Tag
|
||||
color="#FFF0C1"
|
||||
className="text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title="MCP"
|
||||
>
|
||||
MCP
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (nextColumn.dataIndex === 'state') {
|
||||
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem) => (
|
||||
<span
|
||||
className={`text-[13px] ${stateColumnMap[record.state]?.className || ''}`}
|
||||
onClick={(event) => {
|
||||
if (['deploying', 'error'].includes(record.state)) {
|
||||
event.stopPropagation()
|
||||
openLogsModal(record)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t(stateColumnMap[record.state]?.text || '-')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...nextColumn,
|
||||
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
|
||||
}
|
||||
})
|
||||
}, [memberValueEnum, teamList, state.language])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
<div className="border-[0px] mr-PAGE_INSIDE_X mb-[30px]">
|
||||
<div className="flex justify-between mb-[20px] items-center">
|
||||
<div className="flex items-center gap-TAG_LEFT">
|
||||
<div className="text-theme text-[26px]">{$t('服务')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{$t(
|
||||
'服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B overflow-hidden">
|
||||
<PageList
|
||||
id="global_system"
|
||||
ref={pageListRef}
|
||||
columns={columns}
|
||||
request={() => getSystemList()}
|
||||
searchPlaceholder={$t('输入名称、ID、所属团队、负责人查找服务')}
|
||||
manualReloadTable={manualReloadTable}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onSearchWordChange={(event) => {
|
||||
setTableSearchWord(event.target.value)
|
||||
}}
|
||||
onRowClick={(row: SystemTableListItem) => {
|
||||
router.push(`/service/${row.team.id}/${row.service_kind === 'ai' ? 'aiInside' : 'inside'}/${row.id}/overview`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceDetailLayout({
|
||||
teamId,
|
||||
serviceId,
|
||||
side,
|
||||
activeKey,
|
||||
children
|
||||
}: {
|
||||
teamId: string
|
||||
serviceId: string
|
||||
side: ServiceSide
|
||||
activeKey: ServiceMenuKey
|
||||
children: ReactNode
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { state, checkPermission } = useGlobalContext()
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const items: Array<{ key: ServiceMenuKey; label: string; access?: string }> = side === 'aiInside'
|
||||
? [
|
||||
{ key: 'overview', label: $t('总览') },
|
||||
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
|
||||
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
|
||||
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
|
||||
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
|
||||
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
|
||||
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
|
||||
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
|
||||
{ key: 'setting', label: $t('设置') },
|
||||
{ key: 'logs', label: $t('日志') }
|
||||
]
|
||||
: [
|
||||
{ key: 'overview', label: $t('总览') },
|
||||
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
|
||||
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
|
||||
{ key: 'upstream', label: $t('上游'), access: 'team.service.upstream.view' },
|
||||
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
|
||||
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
|
||||
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
|
||||
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
|
||||
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
|
||||
{ key: 'setting', label: $t('设置') },
|
||||
{ key: 'logs', label: $t('日志') }
|
||||
]
|
||||
|
||||
return items
|
||||
.filter((item) => (item.access ? checkPermission(item.access as any) : true))
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
}, [side, state.language, checkPermission])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
<div className="mr-PAGE_INSIDE_X mb-[20px]">
|
||||
<ServiceInfoCard serviceId={serviceId} teamId={teamId} />
|
||||
</div>
|
||||
<div className="flex flex-1 h-full overflow-hidden">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeKey]}
|
||||
mode="inline"
|
||||
items={menuItems}
|
||||
onClick={({ key }) => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/${key}`)
|
||||
}}
|
||||
/>
|
||||
<div className="w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceListPage } from '../../_components/ServicePages'
|
||||
|
||||
export default function TeamServiceListRoutePage() {
|
||||
return <ServiceListPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceListPage } from '../_components/ServicePages'
|
||||
|
||||
export default function ServiceListRoutePage() {
|
||||
return <ServiceListPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function ServiceRootPage() {
|
||||
redirect('/service/list')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export default function FrontendLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0b1020] text-white">
|
||||
<header className="border-b border-white/10 bg-[#0b1020]/90 backdrop-blur">
|
||||
<div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
|
||||
<Link href="/" className="text-lg font-semibold tracking-wide text-white">
|
||||
APIPark
|
||||
</Link>
|
||||
<nav className="flex items-center gap-3 text-sm">
|
||||
<Link href="/admin/login" className="rounded-full border border-white/15 px-4 py-2 text-white/85 hover:text-white">
|
||||
后台登录
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.apipark.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-full bg-[#3d46f2] px-4 py-2 font-medium text-white hover:bg-[#5860ff]"
|
||||
>
|
||||
产品文档
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const McpPage = () => {
|
||||
return <div>MCP</div>
|
||||
}
|
||||
|
||||
export default McpPage
|
||||
@@ -0,0 +1,63 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
title: '多协议 AI 服务',
|
||||
description: '统一接入 REST API、AI Service 与 MCP 服务,后续前台能力可以逐步承接现有 market 模块。'
|
||||
},
|
||||
{
|
||||
title: '服务市场入口',
|
||||
description: '前台首页先作为公共门户,后续再继续拆分服务列表、详情页、申请接入等完整链路。'
|
||||
},
|
||||
{
|
||||
title: '三套系统骨架',
|
||||
description: '当前先落前台 root 页面;后台继续保留现有 `(admin)`,未来可再补独立用户后台。'
|
||||
}
|
||||
]
|
||||
|
||||
export default function FrontendHomePage() {
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[calc(100vh-4rem)] w-full max-w-7xl flex-col justify-center px-6 py-16">
|
||||
<div className="max-w-3xl">
|
||||
<div className="mb-4 inline-flex rounded-full border border-[#5860ff]/40 bg-[#5860ff]/10 px-4 py-1 text-sm text-[#c7cbff]">
|
||||
前台初始架构
|
||||
</div>
|
||||
<h1 className="text-5xl font-semibold leading-tight text-white">
|
||||
APIPark 前台入口
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-white/70">
|
||||
这里先作为前台的公共首页,不复用后台 layout,也不强绑旧路由。
|
||||
先把系统分层立住,后续再把服务市场和服务详情逐步迁进来。
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/admin/login"
|
||||
className="rounded-full bg-[#3d46f2] px-6 py-3 text-sm font-medium text-white hover:bg-[#5860ff]"
|
||||
>
|
||||
进入后台
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.apipark.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-full border border-white/15 px-6 py-3 text-sm font-medium text-white/85 hover:text-white"
|
||||
>
|
||||
查看文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid gap-6 md:grid-cols-3">
|
||||
{highlights.map((item) => (
|
||||
<section
|
||||
key={item.title}
|
||||
className="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_20px_60px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
<h2 className="text-xl font-medium text-white">{item.title}</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-white/65">{item.description}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
'use client'
|
||||
|
||||
import { StyleProvider } from '@ant-design/cssinjs'
|
||||
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
|
||||
import { BasicResponse, STATUS_CODE } from '@common/const/const'
|
||||
import { GlobalProvider, useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { LocaleProvider, useLocaleContext } from '@common/contexts/LocaleContext'
|
||||
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import FeishuLogo from '@common/assets/feishu.png'
|
||||
import Logo from '@common/assets/layout-logo.png'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App as AppAntd, Button, ConfigProvider, Divider, Form, FormInstance, Input, Spin, Tooltip } from 'antd'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
function LoginContent() {
|
||||
const { state, dispatch } = useGlobalContext()
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = AppAntd.useApp()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const formRef = useRef<FormInstance>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [allowGuest, setAllowGuest] = useState<boolean>(false)
|
||||
const [spinning, setSpinning] = useState<boolean>(false)
|
||||
const [allowFeishuLogin, setAllowFeishuLogin] = useState<boolean>(false)
|
||||
const [feishuAppId, setFeishuAppId] = useState<string>()
|
||||
const [isFeishuLogin, setIsFeishuLogin] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeishuLogin) {
|
||||
const callbackUrl = searchParams.get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
router.push(callbackUrl)
|
||||
} else {
|
||||
router.push(state.mainPage)
|
||||
}
|
||||
setIsFeishuLogin(false)
|
||||
}
|
||||
}, [isFeishuLogin, router, searchParams, state.mainPage])
|
||||
|
||||
const feishuLogin = async (feishuCode: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const feishuCallbackUrl = localStorage.getItem('feishuCallbackUrl')
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
|
||||
method: 'POST',
|
||||
eoBody: {
|
||||
code: feishuCode,
|
||||
redirect_uri: feishuCallbackUrl
|
||||
}
|
||||
})
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
setIsFeishuLogin(true)
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setIsFeishuLogin(false)
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isInFeishuClient = () => {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
const isLark = ua.includes('lark') || ua.includes('feishu')
|
||||
const hasSDK = typeof (window as any).h5sdk !== 'undefined' || typeof (window as any).tt !== 'undefined'
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const hasFeishuParams = params.has('from') || params.has('required_launch_ability')
|
||||
return isLark || hasSDK || hasFeishuParams
|
||||
}
|
||||
|
||||
const openFeishuLogin = (id?: string) => {
|
||||
const href = window.location.origin + window.location.pathname
|
||||
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${id || feishuAppId}&redirect_uri=${href}`
|
||||
localStorage.setItem('feishuCallbackUrl', href)
|
||||
window.location.href = authUrl
|
||||
}
|
||||
|
||||
const check = useCallback(() => {
|
||||
state.isAuthenticated && setSpinning(true)
|
||||
fetchData<BasicResponse<{ channel: Array<{ name: string; config: { [key: string]: any } }>; status: string }>>(
|
||||
'account/login',
|
||||
{ method: 'GET' }
|
||||
)
|
||||
.then((response) => {
|
||||
const { code, data } = response || {}
|
||||
if (code === STATUS_CODE.SUCCESS && data && data.status !== 'anonymous') {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
router.replace(state.mainPage)
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
if (data && data.channel) {
|
||||
setAllowGuest(data.channel.filter((x: any) => x.name === 'guest_access').length > 0)
|
||||
const feishu = data.channel.find((x: any) => x.name === 'feishu')
|
||||
if (feishu) {
|
||||
setFeishuAppId(feishu.config.client_id)
|
||||
setAllowFeishuLogin(true)
|
||||
}
|
||||
const code = searchParams.get('code')
|
||||
if (code) {
|
||||
feishuLogin(code)
|
||||
setSpinning(false)
|
||||
return
|
||||
}
|
||||
if (isInFeishuClient() && feishu) {
|
||||
openFeishuLogin(feishu.config.client_id)
|
||||
}
|
||||
}
|
||||
setSpinning(false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to fetch login status:', err)
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setSpinning(false)
|
||||
})
|
||||
}, [dispatch, fetchData, router, searchParams, state.isAuthenticated, state.mainPage])
|
||||
|
||||
const getSystemInfo = useCallback(() => {
|
||||
fetchData<BasicResponse<{ version: string; buildTime: string }>>('common/version', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['build_time']
|
||||
}).then((response) => {
|
||||
const { code, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'UPDATE_VERSION', version: data.version })
|
||||
dispatch({ type: 'UPDATE_DATE', updateDate: data.buildTime })
|
||||
}
|
||||
})
|
||||
}, [dispatch, fetchData])
|
||||
|
||||
const fetchLogin = async (values: any) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { username, password } = values
|
||||
const body = { name: username, password: password }
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/username', {
|
||||
method: 'POST',
|
||||
eoBody: body
|
||||
})
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
const callbackUrl = searchParams.get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
router.push(callbackUrl)
|
||||
} else {
|
||||
router.push(state.mainPage)
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
if (formRef.current) {
|
||||
const values = await formRef.current.validateFields()
|
||||
fetchLogin(values)
|
||||
}
|
||||
}
|
||||
|
||||
const loginAsGuest = () => {
|
||||
fetchLogin({ username: 'guest', password: '12345678' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, [check, getSystemInfo])
|
||||
|
||||
return spinning ? (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={spinning}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
|
||||
<div id="glow-background" className="background-container">
|
||||
<svg className="background-pattern" aria-hidden="true">
|
||||
<defs>
|
||||
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
|
||||
<path d="M.5 200V.5H200" fill="none"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 800 450"
|
||||
opacity="1"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="bbblurry-filter"
|
||||
x="-100%"
|
||||
y="-100%"
|
||||
width="400%"
|
||||
height="400%"
|
||||
filterUnits="objectBoundingBox"
|
||||
primitiveUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="99"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
in="SourceGraphic"
|
||||
edgeMode="none"
|
||||
result="blur"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#bbblurry-filter)">
|
||||
<ellipse rx="80.5" ry="66.5" cx="623.0285107902043" cy="25.708028895006635" fill="hsla(187, 67%, 50%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)" dur="6s" repeatCount="indefinite" />
|
||||
</ellipse>
|
||||
<ellipse rx="80.5" ry="66.5" cx="446.471435546875" cy="-11.694503784179688" fill="hsla(234, 78%, 61%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)" dur="8s" repeatCount="indefinite" />
|
||||
</ellipse>
|
||||
<ellipse rx="80.5" ry="66.5" cx="200.54574247724838" cy="-19.02454901710908" fill="hsla(167, 87%, 56%, 1.00)">
|
||||
<animate attributeName="fill" values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)" dur="10s" repeatCount="indefinite" />
|
||||
</ellipse>
|
||||
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
|
||||
<animate attributeName="fill" values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)" dur="8s" repeatCount="indefinite" />
|
||||
</ellipse>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]">
|
||||
<div className="mx-auto">
|
||||
<span className="flex items-center justify-center">
|
||||
<img className="h-[40px] mr-[8px]" src={typeof Logo === 'string' ? Logo : (Logo as any)?.src} alt="logo" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
|
||||
<div className="h-full">
|
||||
<div className="">
|
||||
<Form onFinish={login} className="w-[350px]" ref={formRef}>
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none"
|
||||
name="username"
|
||||
rules={[{ required: true, message: $t('请输入账号'), whitespace: true }]}
|
||||
>
|
||||
<Input
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('账号')}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none"
|
||||
name="password"
|
||||
rules={[{ required: true, message: $t('请输入密码') }]}
|
||||
>
|
||||
<Input.Password
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('密码')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] mt-mbase w-full inline-flex justify-center items-center"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{$t('登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{allowFeishuLogin && (
|
||||
<>
|
||||
<Divider />
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={() => openFeishuLogin(feishuAppId)}
|
||||
>
|
||||
<img className="h-[30px]" src={typeof FeishuLogo === 'string' ? FeishuLogo : (FeishuLogo as any)?.src} alt="feishu" />
|
||||
{$t('飞书授权登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{allowGuest && (
|
||||
<>
|
||||
<Divider />
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={loginAsGuest}
|
||||
>
|
||||
{$t('访客模式')}{' '}
|
||||
<Tooltip
|
||||
title={$t(
|
||||
'您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。'
|
||||
)}
|
||||
>
|
||||
<Icon icon="ic:baseline-help" height={18} width={18} />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
|
||||
<p className="leading-[28px]">
|
||||
{$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')}
|
||||
</p>
|
||||
<LanguageSetting mode="light" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginProviders() {
|
||||
const { locale } = useLocaleContext()
|
||||
return (
|
||||
<StyleProvider hashPriority="high">
|
||||
<ConfigProvider locale={locale} wave={{ disabled: true }}>
|
||||
<PluginEventHubProvider>
|
||||
<GlobalProvider>
|
||||
<AppAntd className="h-full" message={{ maxCount: 1 }}>
|
||||
<LoginContent />
|
||||
</AppAntd>
|
||||
</GlobalProvider>
|
||||
</PluginEventHubProvider>
|
||||
</ConfigProvider>
|
||||
</StyleProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleProvider>
|
||||
<LoginProviders />
|
||||
</LocaleProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AdminEntryPage() {
|
||||
redirect('/admin/login')
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +1,16 @@
|
||||
export { default } from "../_legacy/LegacyAppPage";
|
||||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function LoginRedirectPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
const query = searchParams.toString()
|
||||
router.replace(query ? `/admin/login?${query}` : '/admin/login')
|
||||
}, [router, searchParams])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "../../_legacy/LegacyAppPage";
|
||||
@@ -8,7 +8,6 @@ import { $t } from '@common/locales'
|
||||
import { MenuItem } from '@common/utils/navigation'
|
||||
import { checkAccess } from '@common/utils/permission'
|
||||
import { ProtectedRoute } from '@core/components/aoplatform/RenderRoutes'
|
||||
import Login from '@core/pages/Login'
|
||||
import Root from '@core/pages/Root'
|
||||
import Playground from '@core/pages/playground'
|
||||
import DataMaskingCompare from '@core/pages/policy/dataMasking/DataMaskingCompare'
|
||||
@@ -277,30 +276,30 @@ const mockData = [
|
||||
*/
|
||||
export const GlobalContext = createContext<
|
||||
| {
|
||||
state: GlobalState
|
||||
dispatch: Dispatch<GlobalAction>
|
||||
accessData: Map<string, string[]>
|
||||
pluginAccessDictionary: { [k: string]: string }
|
||||
menuList: MenuItem[]
|
||||
getGlobalAccessData: () => Promise<{ access: string[] }>
|
||||
getTeamAccessData: (teamId: string) => void
|
||||
getPluginAccessDictionary: (pluginData: { [k: string]: string }) => void
|
||||
getMenuList: () => void
|
||||
resetAccess: () => void
|
||||
cleanTeamAccessData: () => void
|
||||
checkPermission: (
|
||||
access: keyof (typeof PERMISSION_DEFINITION)[0] | Array<keyof (typeof PERMISSION_DEFINITION)[0]>
|
||||
) => boolean
|
||||
teamDataFlushed: boolean
|
||||
accessInit: boolean
|
||||
aiConfigFlushed: boolean
|
||||
setAiConfigFlushed: (flush: boolean) => void
|
||||
routeConfig: RouteConfig[]
|
||||
setRouterConfig: (isRoot: boolean, config: RouteConfig) => void
|
||||
addRouteConfig: (parentRoute: RouteConfig, config: RouteConfig) => void
|
||||
fetchData: ReturnType<typeof useFetch>['fetchData']
|
||||
$t: typeof $t
|
||||
}
|
||||
state: GlobalState
|
||||
dispatch: Dispatch<GlobalAction>
|
||||
accessData: Map<string, string[]>
|
||||
pluginAccessDictionary: { [k: string]: string }
|
||||
menuList: MenuItem[]
|
||||
getGlobalAccessData: () => Promise<{ access: string[] }>
|
||||
getTeamAccessData: (teamId: string) => void
|
||||
getPluginAccessDictionary: (pluginData: { [k: string]: string }) => void
|
||||
getMenuList: () => void
|
||||
resetAccess: () => void
|
||||
cleanTeamAccessData: () => void
|
||||
checkPermission: (
|
||||
access: keyof (typeof PERMISSION_DEFINITION)[0] | Array<keyof (typeof PERMISSION_DEFINITION)[0]>
|
||||
) => boolean
|
||||
teamDataFlushed: boolean
|
||||
accessInit: boolean
|
||||
aiConfigFlushed: boolean
|
||||
setAiConfigFlushed: (flush: boolean) => void
|
||||
routeConfig: RouteConfig[]
|
||||
setRouterConfig: (isRoot: boolean, config: RouteConfig) => void
|
||||
addRouteConfig: (parentRoute: RouteConfig, config: RouteConfig) => void
|
||||
fetchData: ReturnType<typeof useFetch>['fetchData']
|
||||
$t: typeof $t
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
@@ -357,9 +356,25 @@ const globalReducer = (state: GlobalState, action: GlobalAction): GlobalState =>
|
||||
}
|
||||
}
|
||||
|
||||
function RedirectToNextLogin() {
|
||||
useEffect(() => {
|
||||
window.location.replace(`/admin/login${window.location.search || ''}`)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getStoredLanguage() {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'en-US'
|
||||
}
|
||||
|
||||
return window.sessionStorage.getItem('i18nextLng') || 'en-US'
|
||||
}
|
||||
|
||||
export const DefaultRouteConfig = [
|
||||
{ path: '/', pathMatch: 'full', component: <Root />, key: 'root' },
|
||||
{ path: '/login', component: <Login />, key: 'login' },
|
||||
{ path: '/login', component: <RedirectToNextLogin />, key: 'login' },
|
||||
{ path: '/dataMaskCompare/:logId/:serviceId?/:teamId?', component: <DataMaskingCompare />, key: 'dataMaskCompare' },
|
||||
{
|
||||
path: '/',
|
||||
@@ -391,7 +406,7 @@ export const GlobalProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
updateDate: '2024-07-01',
|
||||
powered: 'Powered by https://apipark.com',
|
||||
mainPage: '/guide/page',
|
||||
language: sessionStorage.getItem('i18nextLng') || 'en-US',
|
||||
language: getStoredLanguage(),
|
||||
pluginsLoaded: false
|
||||
})
|
||||
const [accessData, setAccessData] = useState<Map<string, string[]>>(new Map())
|
||||
@@ -559,27 +574,27 @@ export const useGlobalContext = () => {
|
||||
updateDate: '',
|
||||
powered: '',
|
||||
mainPage: '',
|
||||
language: sessionStorage.getItem('i18nextLng') || 'en-US',
|
||||
language: getStoredLanguage(),
|
||||
pluginsLoaded: false
|
||||
},
|
||||
dispatch: () => {},
|
||||
dispatch: () => { },
|
||||
accessData: new Map(),
|
||||
pluginAccessDictionary: {},
|
||||
menuList: [],
|
||||
getGlobalAccessData: async () => ({ access: [] }),
|
||||
getTeamAccessData: () => {},
|
||||
getPluginAccessDictionary: () => {},
|
||||
getMenuList: () => {},
|
||||
resetAccess: () => {},
|
||||
cleanTeamAccessData: () => {},
|
||||
getTeamAccessData: () => { },
|
||||
getPluginAccessDictionary: () => { },
|
||||
getMenuList: () => { },
|
||||
resetAccess: () => { },
|
||||
cleanTeamAccessData: () => { },
|
||||
checkPermission: () => false,
|
||||
teamDataFlushed: false,
|
||||
accessInit: false,
|
||||
aiConfigFlushed: false,
|
||||
setAiConfigFlushed: () => {},
|
||||
setAiConfigFlushed: () => { },
|
||||
routeConfig: [],
|
||||
setRouterConfig: () => {},
|
||||
addRouteConfig: () => {},
|
||||
setRouterConfig: () => { },
|
||||
addRouteConfig: () => { },
|
||||
fetchData: async () => ({}),
|
||||
$t: (key: string) => key
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export function useFetch() {
|
||||
if (response.status === STATUS_CODE.UNANTHORIZED) {
|
||||
// 处理401未登录的逻辑,比如跳转到登录页面或弹出登录框
|
||||
console.log('Unauthorized access, redirecting to login...')
|
||||
window.location.href = '/login' // 示例:重定向到登录
|
||||
window.location.href = '/admin/login' // 示例:重定向到登录
|
||||
|
||||
return // 返回或抛出错误,确保不继续执行后续的响应处理
|
||||
}
|
||||
@@ -189,7 +189,7 @@ export function useFetch() {
|
||||
if (response.status === STATUS_CODE.FORBIDDEN) {
|
||||
// 处理403无权限,比如跳转到登录页面或弹出登录框
|
||||
console.log('Unauthorized access, redirecting to login...')
|
||||
// window.location.href = '/login' // 示例:重定向到登录
|
||||
// window.location.href = '/admin/login' // 示例:重定向到登录
|
||||
|
||||
return // 返回或抛出错误,确保不继续执行后续的响应处理
|
||||
}
|
||||
|
||||
@@ -4,24 +4,39 @@ import { AiServiceProvider } from '@core/contexts/AiServiceContext'
|
||||
import { SystemProvider } from '@core/contexts/SystemContext'
|
||||
import { TeamProvider } from '@core/contexts/TeamContext'
|
||||
import AiServiceOutlet from '@core/pages/aiService/AiServiceOutlet'
|
||||
import Guide from '@core/pages/guide/Guide'
|
||||
import Login from '@core/pages/Login'
|
||||
import ServicePolicyLayout from '@core/pages/policy/ServicePolicyLayout'
|
||||
import SystemOutlet from '@core/pages/system/SystemOutlet'
|
||||
import { TenantManagementProvider } from '@market/contexts/TenantManagementContext'
|
||||
import { lazy } from 'react'
|
||||
import { lazy, useEffect } from 'react'
|
||||
import { Navigate, Outlet } from 'react-router-dom'
|
||||
|
||||
function RedirectToNextLogin() {
|
||||
useEffect(() => {
|
||||
window.location.replace(`/admin/login${window.location.search || ''}`)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function RedirectToNextGuide() {
|
||||
useEffect(() => {
|
||||
const query = window.location.search || ''
|
||||
window.location.replace(`/guide/page${query}`)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 内置插件与对应组件/模块
|
||||
export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
['basicLayout', { type: 'component', component: <ProtectedRoute /> }],
|
||||
['navHidden', { type: 'component', component: <ProtectedRoute /> }],
|
||||
['login', { type: 'component', component: <Login /> }],
|
||||
['login', { type: 'component', component: <RedirectToNextLogin /> }],
|
||||
[
|
||||
'guide',
|
||||
{
|
||||
type: 'component',
|
||||
component: <Guide />
|
||||
component: <RedirectToNextGuide />
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { App, Button, Divider, Form, FormInstance, Input, Spin, Tooltip } from 'antd'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { BasicResponse, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
// import {useCrypto} from "../hooks/crypto.ts";
|
||||
import Logo from '@common/assets/layout-logo.png'
|
||||
import FeishuLogo from '@common/assets/feishu.png'
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
|
||||
const Login: FC = () => {
|
||||
const { state, dispatch } = useGlobalContext()
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = App.useApp()
|
||||
const navigate = useNavigate()
|
||||
const formRef = useRef<FormInstance>(null)
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
const [allowGuest, setAllowGuest] = useState<boolean>(false)
|
||||
const [spinning, setSpinning] = useState<boolean>(false)
|
||||
// 是否允许飞书登录
|
||||
const [allowFeishuLogin, setAllowFeishuLogin] = useState<boolean>(false)
|
||||
// 飞书登录app_id
|
||||
const [feishuAppId, setFeishuAppId] = useState<string>()
|
||||
// 获取 url 参数
|
||||
const query = new URLSearchParams(useLocation().search)
|
||||
// 是否是飞书登录
|
||||
const [isFeishuLogin, setIsFeishuLogin] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isFeishuLogin) {
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl)
|
||||
} else {
|
||||
navigate(state.mainPage)
|
||||
}
|
||||
setIsFeishuLogin(false)
|
||||
}
|
||||
}, [isFeishuLogin])
|
||||
/**
|
||||
* 飞书登录
|
||||
* @param feishuCode 飞书 code
|
||||
*/
|
||||
const feishuLogin = async (feishuCode: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const feishuCallbackUrl = localStorage.getItem('feishuCallbackUrl')
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/feishu', {
|
||||
method: 'POST',
|
||||
eoBody: {
|
||||
code: feishuCode,
|
||||
redirect_uri: feishuCallbackUrl
|
||||
}
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
setIsFeishuLogin(true)
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setIsFeishuLogin(false)
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const check = useCallback(() => {
|
||||
state.isAuthenticated && setSpinning(true)
|
||||
fetchData<BasicResponse<{ channel: Array<{ name: string; config: { [key: string]: any } }>; status: string }>>(
|
||||
'account/login',
|
||||
{ method: 'GET' }
|
||||
).then((response) => {
|
||||
const { code, data } = response || {}
|
||||
if (code === STATUS_CODE.SUCCESS && data && data.status !== 'anonymous') {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
navigate(state.mainPage, { replace: true })
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
if (data && data.channel) {
|
||||
setAllowGuest(data.channel.filter((x: any) => x.name === 'guest_access').length > 0)
|
||||
const feishu = data.channel.find((x: any) => x.name === 'feishu')
|
||||
if (feishu) {
|
||||
setFeishuAppId(feishu.config.client_id)
|
||||
setAllowFeishuLogin(true)
|
||||
}
|
||||
const code = query.get('code')
|
||||
if (code) {
|
||||
feishuLogin(code)
|
||||
setSpinning(false)
|
||||
return
|
||||
}
|
||||
if (isInFeishuClient() && feishu) {
|
||||
openFeishuLogin(feishu.config.client_id)
|
||||
}
|
||||
}
|
||||
setSpinning(false)
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('Failed to fetch login status:', err)
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
setSpinning(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getSystemInfo = useCallback(() => {
|
||||
fetchData<BasicResponse<{ version: string; buildTime: string }>>('common/version', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['build_time']
|
||||
}).then((response) => {
|
||||
const { code, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'UPDATE_VERSION', version: data.version })
|
||||
dispatch({ type: 'UPDATE_DATE', updateDate: data.buildTime })
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const fetchLogin = async (values: any) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { username, password } = values
|
||||
// const encryptedPassword = encryptByEnAES(username, password);
|
||||
|
||||
const body = {
|
||||
name: username,
|
||||
password: password
|
||||
// client: 1,
|
||||
// type: 1,
|
||||
// app_type: 4,
|
||||
}
|
||||
|
||||
const { code, msg } = await fetchData<BasicResponse<null>>('account/login/username', {
|
||||
method: 'POST',
|
||||
eoBody: body
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGIN' })
|
||||
// message.success($t(RESPONSE_TIPS.loginSuccess));
|
||||
const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl')
|
||||
if (callbackUrl && callbackUrl !== 'null') {
|
||||
navigate(callbackUrl)
|
||||
} else {
|
||||
navigate(state.mainPage)
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
message.error(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const login = async () => {
|
||||
if (formRef.current) {
|
||||
const values = await formRef.current.validateFields()
|
||||
fetchLogin(values)
|
||||
}
|
||||
}
|
||||
|
||||
const loginAsGuest = () => {
|
||||
fetchLogin({ username: 'guest', password: '12345678' })
|
||||
}
|
||||
|
||||
const isInFeishuClient = () => {
|
||||
// 方法1:检查User-Agent
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isLark = ua.includes('lark') || ua.includes('feishu');
|
||||
|
||||
// 方法2:检查全局对象
|
||||
const hasSDK = typeof window.h5sdk !== 'undefined' || typeof window.tt !== 'undefined';
|
||||
|
||||
// 方法3:检查URL参数
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const hasFeishuParams = params.has('from') || params.has('required_launch_ability');
|
||||
|
||||
return isLark || hasSDK || hasFeishuParams;
|
||||
}
|
||||
|
||||
// 打开飞书授权页面
|
||||
const openFeishuLogin = (id?: string) => {
|
||||
const href = window.location.origin + window.location.pathname
|
||||
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${id || feishuAppId}&redirect_uri=${href}`
|
||||
localStorage.setItem('feishuCallbackUrl', href)
|
||||
window.location.href = authUrl
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
check()
|
||||
getSystemInfo()
|
||||
}, [])
|
||||
|
||||
return spinning ? (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={spinning}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
></Spin>
|
||||
) : (
|
||||
<div className="h-full w-full flex flex-col items-center overflow-auto min-h-[490px] bg-[#0d1117]">
|
||||
<div id="glow-background" className="background-container">
|
||||
<svg className="background-pattern" aria-hidden="true">
|
||||
<defs>
|
||||
<pattern id="pattern-bg" width="200" height="200" patternUnits="userSpaceOnUse">
|
||||
<path d="M.5 200V.5H200" fill="none"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pattern-bg)"></rect>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 800 450"
|
||||
opacity="1"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
id="bbblurry-filter"
|
||||
x="-100%"
|
||||
y="-100%"
|
||||
width="400%"
|
||||
height="400%"
|
||||
filterUnits="objectBoundingBox"
|
||||
primitiveUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feGaussianBlur
|
||||
stdDeviation="99"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
in="SourceGraphic"
|
||||
edgeMode="none"
|
||||
result="blur"
|
||||
></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#bbblurry-filter)">
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="623.0285107902043"
|
||||
cy="25.708028895006635"
|
||||
fill="hsla(187, 67%, 50%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(187, 67%, 50%, 1.00); hsla(340, 85%, 60%, 1.00); hsla(60, 90%, 55%, 1.00); hsla(187, 67%, 50%, 1.00)"
|
||||
dur="6s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="446.471435546875"
|
||||
cy="-11.694503784179688"
|
||||
fill="hsla(234, 78%, 61%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(234, 78%, 61%, 1.00); hsla(100, 75%, 60%, 1.00); hsla(290, 80%, 70%, 1.00); hsla(234, 78%, 61%, 1.00)"
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse
|
||||
rx="80.5"
|
||||
ry="66.5"
|
||||
cx="200.54574247724838"
|
||||
cy="-19.02454901710908"
|
||||
fill="hsla(167, 87%, 56%, 1.00)"
|
||||
>
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsla(167, 87%, 56%, 1.00); hsla(10, 90%, 65%, 1.00); hsla(300, 85%, 50%, 1.00); hsla(167, 87%, 56%, 1.00)"
|
||||
dur="10s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
|
||||
<ellipse rx="80.5" ry="66.5" cx="340.05827594708103" cy="-9.424536458161867" fill="hsl(25, 100%, 64%)">
|
||||
<animate
|
||||
attributeName="fill"
|
||||
values="hsl(25, 100%, 64%); hsl(200, 100%, 70%); hsl(50, 95%, 55%); hsl(25, 100%, 64%)"
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</ellipse>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
{/* <div className="w-full border-box text-right pr-[40px]"></div> */}
|
||||
<div className="mx-auto flex-1 flex flex-col items-center justify-center z-[3]">
|
||||
<div className="mx-auto">
|
||||
<span className="flex items-center justify-center">
|
||||
<img className="h-[40px] mr-[8px]" src={typeof Logo === 'string' ? Logo : Logo?.src} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section className="block w-[410px] mx-auto mt-[46px] p-[30px] box-border rounded-[10px] shadow-[0_5px_20px_0_rgba(0,0,0,5%)] login-block">
|
||||
<div className="h-full">
|
||||
<div className="">
|
||||
<Form onFinish={login} className="w-[350px]" ref={formRef}>
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none"
|
||||
name="username"
|
||||
rules={[{ required: true, message: $t('请输入账号'), whitespace: true }]}
|
||||
>
|
||||
<Input
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('账号')}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="p-0 bg-transparent rounded border-none "
|
||||
name="password"
|
||||
rules={[{ required: true, message: $t('请输入密码') }]}
|
||||
>
|
||||
<Input.Password
|
||||
className="w-[350px] h-[40px] login-input"
|
||||
placeholder={$t('密码')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none ">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] mt-mbase w-full inline-flex justify-center items-center"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{$t('登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
{allowFeishuLogin && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={() => openFeishuLogin(feishuAppId)}
|
||||
>
|
||||
<img className="h-[30px]" src={typeof FeishuLogo === 'string' ? FeishuLogo : FeishuLogo?.src} />
|
||||
{$t('飞书授权登录')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{allowGuest && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
<Form.Item className="p-0 bg-transparent rounded border-none mb-0">
|
||||
<Button
|
||||
loading={loading}
|
||||
className="h-[40px] w-full inline-flex justify-center items-center"
|
||||
type="default"
|
||||
onClick={loginAsGuest}
|
||||
>
|
||||
{$t('访客模式')}{' '}
|
||||
<Tooltip
|
||||
title={$t(
|
||||
'您可通过访客模式查看所有页面和功能,但是无法编辑数据。访客模式仅用于了解产品功能,您可以在正式产品中关闭该功能。'
|
||||
)}
|
||||
>
|
||||
<Icon icon="ic:baseline-help" height={18} width={18} />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col items-center mt-[46px] text-SECOND_TEXT">
|
||||
<p className="leading-[28px]">
|
||||
{$t('Version (0)-(1)', [state?.version, state?.updateDate])}, {$t(state?.powered || '-')}
|
||||
</p>
|
||||
<LanguageSetting mode="light" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Login
|
||||
@@ -409,7 +409,8 @@ const AiServiceInsidePublicList:FC = ()=>{
|
||||
}, [query]);
|
||||
|
||||
useEffect(()=>{
|
||||
setPageType(currLocation.split('/')[0] === 'service' ? 'insidePage' : 'global')
|
||||
const normalizedPath = currLocation.replace(/^\/+/, '')
|
||||
setPageType(normalizedPath.split('/')[0] === 'service' ? 'insidePage' : 'global')
|
||||
},[currLocation])
|
||||
|
||||
const manualReloadTable = () => {
|
||||
@@ -513,4 +514,4 @@ const AiServiceInsidePublicList:FC = ()=>{
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsidePublicList
|
||||
export default AiServiceInsidePublicList
|
||||
|
||||
@@ -408,7 +408,8 @@ const SystemInsidePublicList:FC = ()=>{
|
||||
}, [query]);
|
||||
|
||||
useEffect(()=>{
|
||||
setPageType(currLocation.split('/')[0] === 'service' ? 'insidePage' : 'global')
|
||||
const normalizedPath = currLocation.replace(/^\/+/, '')
|
||||
setPageType(normalizedPath.split('/')[0] === 'service' ? 'insidePage' : 'global')
|
||||
},[currLocation])
|
||||
|
||||
const manualReloadTable = () => {
|
||||
@@ -512,4 +513,4 @@ const SystemInsidePublicList:FC = ()=>{
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default SystemInsidePublicList
|
||||
export default SystemInsidePublicList
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom'
|
||||
import Login from '@core/pages/Login.tsx'
|
||||
import BasicLayout from '@common/components/aoplatform/BasicLayout'
|
||||
import { createElement, ReactElement, ReactNode, Suspense } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { App, Skeleton } from 'antd'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { FC, lazy } from 'react'
|
||||
import { FC, lazy, useEffect } from 'react'
|
||||
import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx'
|
||||
|
||||
type RouteConfig = {
|
||||
@@ -36,15 +35,23 @@ export type RouterParams = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
function RedirectToNextLogin() {
|
||||
useEffect(() => {
|
||||
window.location.replace(`/admin/login${window.location.search || ''}`)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const PUBLIC_ROUTES: RouteConfig[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: <Login />,
|
||||
component: <RedirectToNextLogin />,
|
||||
key: uuidv4()
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: <Login />,
|
||||
component: <RedirectToNextLogin />,
|
||||
key: uuidv4()
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user