Compare commits

...

2 Commits

Author SHA1 Message Date
lcx 1942344373 feat: 三端合一 2026-05-06 13:53:14 +08:00
lcx 96ad58fe28 feat: 三端合一 2026-04-29 15:39:28 +08:00
560 changed files with 5481 additions and 3829 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
"plugins": ["react", "@typescript-eslint", "prettier", "unused-imports"],
"rules": {
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"prettier/prettier": "off",
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
+1 -1
View File
@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.next/
node_modules
dist
market_dist
+42
View File
@@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const srcDir = path.join(__dirname, 'src');
const filesToDelete = [
'package.json',
'tsconfig.json',
'tsconfig.node.json',
'vite.config.ts',
'postcss.config.js',
'tailwind.config.js',
'.eslintrc.cjs',
'index.html',
'start-vite.js'
];
function walkDirAndClean(dir) {
if (!fs.existsSync(dir)) return;
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
try {
let stat = fs.statSync(dirPath);
if (stat.isDirectory() && f !== 'node_modules') {
// If it's a top-level module directory like src/core, src/common
if (dir === srcDir) {
filesToDelete.forEach(file => {
const fileToDelete = path.join(dirPath, file);
if (fs.existsSync(fileToDelete)) {
fs.unlinkSync(fileToDelete);
console.log(`Deleted: ${fileToDelete}`);
}
});
}
}
} catch(e) {}
});
}
walkDirAndClean(srcDir);
console.log('Cleanup completed!');
+50
View File
@@ -0,0 +1,50 @@
const fs = require('fs');
const path = require('path');
const srcDir = path.join(__dirname, 'src');
function walkDir(dir, callback) {
if (dir.includes('node_modules')) return;
fs.readdirSync(dir).forEach(f => {
let dirPath = path.join(dir, f);
try {
let isDirectory = fs.statSync(dirPath).isDirectory();
isDirectory ? walkDir(dirPath, callback) : callback(path.join(dir, f));
} catch(e) {}
});
}
const filesToRename = [];
const filesToUpdate = [];
walkDir(srcDir, (filePath) => {
if (filePath.endsWith('.module.css')) {
filesToRename.push(filePath);
} else if (filePath.endsWith('.tsx') || filePath.endsWith('.ts')) {
filesToUpdate.push(filePath);
}
});
filesToRename.forEach(oldPath => {
const newPath = oldPath.replace(/\.module\.css$/, '.css');
// Read and remove :global wrappers entirely
let content = fs.readFileSync(oldPath, 'utf8');
content = content.replace(/:global\(([^)]+)\)/g, '$1'); // replace :global(.foo) with .foo
content = content.replace(/:global\s+/g, ''); // replace :global .foo with .foo
fs.writeFileSync(oldPath, content);
fs.renameSync(oldPath, newPath);
console.log(`Renamed and cleaned: ${path.basename(oldPath)} -> ${path.basename(newPath)}`);
});
filesToUpdate.forEach(filePath => {
let content = fs.readFileSync(filePath, 'utf8');
if (content.includes('.module.css')) {
content = content.replace(/\.module\.css/g, '.css');
fs.writeFileSync(filePath, content);
console.log(`Updated imports in: ${path.basename(filePath)}`);
}
});
console.log('Done!');
-7
View File
@@ -1,7 +0,0 @@
{
"packages": [
"packages/*"
],
"version": "independent"
}
+144
View File
@@ -0,0 +1,144 @@
const fs = require('fs');
const path = require('path');
const rootDir = __dirname;
const packagesDir = path.join(rootDir, 'packages');
const srcDir = path.join(rootDir, 'src');
const appDir = path.join(srcDir, 'app');
console.log('🚀 开始拆除 Lerna 并迁移至 Next.js...');
// 1. 创建基础目录
if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir);
if (!fs.existsSync(appDir)) fs.mkdirSync(appDir);
// 2. 读取并合并 package.json
const rootPkgPath = path.join(rootDir, 'package.json');
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8'));
const mergedDeps = { ...rootPkg.dependencies };
const mergedDevDeps = { ...rootPkg.devDependencies };
if (fs.existsSync(packagesDir)) {
const packages = fs.readdirSync(packagesDir);
for (const pkg of packages) {
const pkgPath = path.join(packagesDir, pkg);
if (fs.statSync(pkgPath).isDirectory()) {
// 合并依赖
const childPkgPath = path.join(pkgPath, 'package.json');
if (fs.existsSync(childPkgPath)) {
const childPkg = JSON.parse(fs.readFileSync(childPkgPath, 'utf8'));
Object.assign(mergedDeps, childPkg.dependencies || {});
Object.assign(mergedDevDeps, childPkg.devDependencies || {});
}
// 移动目录到 src 下
const destPath = path.join(srcDir, pkg);
if (!fs.existsSync(destPath)) {
fs.renameSync(pkgPath, destPath);
console.log(`📦 已迁移模块: packages/${pkg} -> src/${pkg}`);
}
}
}
// 删除空的 packages 文件夹
try { fs.rmdirSync(packagesDir); } catch (e) { console.error('Failed to remove packages dir, skipping', e) }
}
// 3. 清理并更新根 package.json
delete rootPkg.workspaces; // 移除 lerna workspaces
rootPkg.scripts = {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
};
// 移除 Vite 和 Lerna 相关依赖
const removeDeps = ['lerna', 'vite', '@originjs/vite-plugin-federation', '@vitejs/plugin-react', 'vite-tsconfig-paths'];
removeDeps.forEach(dep => {
delete mergedDeps[dep];
delete mergedDevDeps[dep];
});
// 添加 Next.js 和 React 最新核心依赖 (与 xroute-ui 对齐)
mergedDeps['next'] = "15.4.5";
mergedDeps['react'] = "19.1.0";
mergedDeps['react-dom'] = "19.1.0";
mergedDevDeps['@types/react'] = "^19";
mergedDevDeps['@types/react-dom'] = "^19";
rootPkg.dependencies = mergedDeps;
rootPkg.devDependencies = mergedDevDeps;
fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2));
console.log('✅ package.json 依赖已合并并重写');
// 4. 生成 tsconfig.json (配置路径别名)
const tsconfigPath = path.join(rootDir, 'tsconfig.json');
const tsconfig = {
compilerOptions: {
target: "es5",
lib: ["dom", "dom.iterable", "esnext"],
allowJs: true,
skipLibCheck: true,
strict: false,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "bundler",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
baseUrl: ".",
paths: {
"@/*": ["src/*"],
// 欺骗原有代码,使其能找到拍平后的新路径
"@apipark/common/*": ["src/common/src/*"],
"@apipark/core/*": ["src/core/src/*"],
"@apipark/dashboard/*": ["src/dashboard/src/*"],
"@apipark/market/*": ["src/market/src/*"],
"@apipark/openApi/*": ["src/openApi/src/*"],
"@apipark/systemRunning/*": ["src/systemRunning/src/*"]
}
},
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
exclude: ["node_modules"]
};
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
console.log('✅ tsconfig.json 别名映射已配置');
// 5. 创建 Next.js App Router 挂载点
const layoutCode = `export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}`;
fs.writeFileSync(path.join(appDir, 'layout.tsx'), layoutCode);
const slugDir = path.join(appDir, '[[...slug]]');
if (!fs.existsSync(slugDir)) fs.mkdirSync(slugDir, { recursive: true });
const pageCode = `"use client";
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
// 动态导入原有的 Vite SPA 根组件,禁用 SSR 避免 window 报错
const ApiParkApp = dynamic(() => import('@/core/src/App'), {
ssr: false,
loading: () => <div style={{ padding: 50 }}>Loading APIPark...</div>
});
export default function Page() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <ApiParkApp />;
}`;
fs.writeFileSync(path.join(slugDir, 'page.tsx'), pageCode);
console.log('✅ Next.js 路由挂载点创建完毕!');
console.log('🎉 迁移完成!请执行 pnpm install 重新安装依赖。');
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="next" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+42
View File
@@ -0,0 +1,42 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
disableStaticImages: true,
},
experimental: {
optimizePackageImports: ["@heroui/react"],
},
transpilePackages: ['@heroui/react', '@heroui/theme', '@ant-design', 'antd', 'rc-util', 'rc-pagination', 'rc-picker', 'rc-tree', 'rc-table'],
async rewrites() {
return [
{
source: '/api/v1/:path*',
destination: 'http://172.18.166.219:8288/api/v1/:path*', // Proxy to backend
},
{
source: '/api2/v1/:path*',
destination: 'http://172.18.166.219:8288/api2/v1/:path*', // Proxy to backend 2
}
];
},
webpack: (config) => {
config.module.rules.push({
test: /\.(svg|png|jpe?g|gif|webp)$/i,
type: 'asset/resource',
generator: {
filename: 'static/media/[name].[hash][ext]'
}
});
// 解决一些 Node.js polyfill 在浏览器端缺失的问题
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
path: false,
os: false,
};
return config;
},
};
module.exports = nextConfig;
+59 -30
View File
@@ -2,20 +2,12 @@
"name": "frontend",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"description": "",
"scripts": {
"test": "jest",
"build": "set NODE_OPTIONS=--max-old-space-size=8192 && lerna run build --scope=core --stream --verbose ",
"serve": "lerna run preview --parallel",
"serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream",
"stop": "kill-port --port 5000",
"scan": "i18next-scanner --config i18next-scanner.config.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix && prettier --write ."
"dev": "next dev -p 5000",
"build": "next build",
"start": "next start -p 5000",
"lint": "next lint"
},
"keywords": [],
"author": "",
@@ -23,51 +15,88 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@ant-design/pro-components": "2.7.19",
"@originjs/vite-plugin-federation": "^1.3.3",
"@emotion/react": "^11.14.0",
"@floating-ui/react": "^0.26.24",
"@formkit/auto-animate": "^0.8.1",
"@heroui/react": "^3.0.3",
"@heroui/styles": "^3.0.3",
"@heroui/theme": "^2.4.20",
"@lexical/code": "^0.17.1",
"@lexical/react": "^0.17.1",
"@lexical/selection": "^0.17.1",
"@lexical/text": "^0.17.1",
"@lexical/utils": "^0.17.1",
"@modelcontextprotocol/sdk": "^1.9.0",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "5.0.0-alpha.150",
"@mui/material": "5.14.14",
"@mui/x-data-grid-pro": "6.18.1",
"@rollup/plugin-dynamic-import-vars": "^2.1.2",
"@tinymce/tinymce-react": "^4.3.2",
"@types/dompurify": "^3.0.5",
"@types/lodash-es": "^4.17.12",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"@xyflow/react": "^12.3.6",
"ahooks": "^3.8.1",
"allotment": "^1.20.0",
"autoprefixer": "^10.4.16",
"copy-to-clipboard": "^3.3.3",
"crc": "^4.3.2",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
"echarts": "^5.5.0",
"echarts-for-react": "^3.0.2",
"framer-motion": "^10.16.4",
"fs-extra": "^11.2.0",
"highlight.js": "^11.9.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"js-base64": "^3.7.5",
"lexical": "^0.17.1",
"mockjs": "^1.1.0",
"next": "15.4.5",
"postcss": "^8.4.31",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.5",
"react": "^18.2.0",
"rc-picker": "^4.1.1",
"react": "19.1.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.0.1",
"react-joyride": "^2.8.2",
"react-router-dom": "6.20.0",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
"vite-tsconfig-paths": "^4.3.2",
"react-json-view": "^1.21.3",
"zod": "^3.23.8",
"@modelcontextprotocol/sdk": "^1.9.0",
"echarts-for-react": "^3.0.2"
"react-router-dom": "6.20.0",
"react-virtuoso": "^4.7.11",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^4.2.1",
"tinymce": "^6.8.1",
"use-context-selector": "^2.0.0",
"uuid": "^9.0.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@ant-design/cssinjs": "^1.18.2",
"@antv/g6": "^4.8.24",
"@formily/antd-v5": "^1.2.1",
"@formily/core": "^2.2.13",
"@formily/react": "^2.2.13",
"@formily/reactive": "^2.2.13",
"@iconify/react": "^5.0.2",
"@monaco-editor/react": "^4.6.0",
"@tailwindcss/postcss": "^4.2.1",
"lightningcss": "^1.32.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@testing-library/react-hooks": "^8.0.1",
"@types/file-saver": "^2.0.7",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"antd": "^5.19.4",
"babel-jest": "^29.7.0",
"eslint": "^8.53.0",
@@ -77,22 +106,22 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-unused-imports": "^4.1.4",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"i18next-scanner": "^4.5.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jsdom": "^24.0.0",
"lerna": "^8.1.3",
"less": "^4.2.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.45.0",
"postcss-nested": "^6.0.1",
"prettier": "^3.1.1",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.2",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vite-jest": "^0.1.4"
}
}
}
-41
View File
@@ -1,41 +0,0 @@
{
"name": "common",
"version": "1.0.0",
"description": "Common library for AO Platform",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "node ./__tests__/common.test.js"
},
"dependencies": {
"@floating-ui/react": "^0.26.24",
"@formkit/auto-animate": "^0.8.1",
"@lexical/code": "^0.17.1",
"@lexical/react": "^0.17.1",
"@lexical/selection": "^0.17.1",
"@lexical/text": "^0.17.1",
"@lexical/utils": "^0.17.1",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "5.0.0-alpha.150",
"@mui/material": "5.14.14",
"@mui/x-data-grid-pro": "6.18.1",
"ahooks": "^3.8.1",
"allotment": "^1.20.0",
"echarts": "^5.5.0",
"lexical": "^0.17.1",
"mockjs": "^1.1.0",
"rc-picker": "^4.1.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.49.3",
"use-context-selector": "^2.0.0"
},
"devDependencies": {
"@formily/antd-v5": "^1.2.1",
"@formily/core": "^2.2.13",
"@formily/react": "^2.2.13",
"@formily/reactive": "^2.2.13",
"@monaco-editor/react": "^4.6.0",
"exceljs": "^4.4.0",
"monaco-editor": "^0.45.0"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-4
View File
@@ -1,4 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -1,26 +0,0 @@
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new jsonWorker()
}
if (label === 'css' || label === 'scss' || label === 'less') {
return new cssWorker()
}
if (label === 'html' || label === 'handlebars' || label === 'razor') {
return new htmlWorker()
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker()
}
return new editorWorker()
}
}
export { monaco }
@@ -1,98 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
important:true,
content: [
`../*/src/**/*.{js,ts,jsx,tsx}`,
]
,
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
backgroundImage:{
LAYOUT_BG:'linear-gradient(107.97deg, rgba(32,41,117,1) 4.41%,rgba(16,13,27,1) 86.11%)',
LAYOUT_BG_DARK:'#fff',
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);',
'navbar-height': 'var(--layout-header-height)',
TAG_LEFT:'10px',
PAGE_INSIDE_X:'40px',
PAGE_INSIDE_T:'30px',
PAGE_INSIDE_B:'20px',
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [
function({ addUtilities }) {
addUtilities({
'.h-calc-100vh-minus-navbar': {
height: 'calc(100vh - var(--layout-header-height))',
},
'.w-calc-100vw-minus-padding-r': {
width: 'calc(100% - 40px)',
},
}, ['responsive', 'hover']);
}
],
corePlugins: {
preflight: false,
},
}
-28
View File
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["./src/*"],
"@core/*": ["../core/src/*"],
"@market/*": ["../market/src/*"]
},
},
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-49
View File
@@ -1,49 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'
export default defineConfig({
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true
}
},
modules: {
localsConvention: 'camelCase',
generateScopedName: '[local]_[hash:base64:2]'
}
},
plugins: [
react(),
dynamicImportVars({
include: ['src'],
exclude: [],
warnOnError: false
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@common', replacement: path.resolve(__dirname, './src') },
{ find: '@market', replacement: path.resolve(__dirname, '/./market/src') },
{ find: '@core', replacement: path.resolve(__dirname, '../core/src') }
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true
}
}
},
logLevel: 'info'
})
-1
View File
@@ -1 +0,0 @@
VITE_APP_MODE=openSource
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs','public','code-snippet','ace-editor'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
-36
View File
@@ -1,36 +0,0 @@
<!doctype html>
<html lang="en">
<head id="head">
<meta charset="UTF-8" />
<link id="favicon" rel="icon" type="image/svg+xml" href="/frontend/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="eo-body">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const eoBody = document.getElementById('eo-body');
const favicon = document.getElementById('favicon');
const createScript = (id, src) => {
const script = document.createElement('script');
script.id = id;
script.async = true;
script.src = src;
return script;
};
const iconparkApintoSrc = window.location.hostname === 'localhost' ? '/iconpark_apinto.js' : '/frontend/iconpark_apinto.js';
const iconparkEolinkSrc = window.location.hostname === 'localhost' ? '/iconpark_eolink.js' : '/frontend/iconpark_eolink.js';
const faviconSrc = window.location.hostname === 'localhost' ? '/favicon.ico' : '/frontend/favicon.ico';
favicon.href = faviconSrc;
eoBody.appendChild(createScript('iconpark_apinto', iconparkApintoSrc));
eoBody.appendChild(createScript('iconpark_eolink', iconparkEolinkSrc));
});
</script>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
{
"name": "core",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": " vite --mode open --port 5000 --strictPort",
"dev:pro": " vite --config ./vite.pro.config.ts --mode pro --port 5000 --strictPort ",
"build": "vite build --mode open",
"build:pro": "vite --config ./vite.pro.config.ts build --mode pro",
"postinstall": "node scripts/moveTinymce.js",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --port 5000 --strictPort",
"serve": "vite preview --port 5000 --strictPort"
},
"dependencies": {
"@tinymce/tinymce-react": "^4.3.2",
"@xyflow/react": "^12.3.6",
"fs-extra": "^11.2.0",
"highlight.js": "^11.9.0",
"tinymce": "^6.8.1"
}
}
-10
View File
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-309
View File
@@ -1,309 +0,0 @@
@tailwind base;
@tailwind components;
@layer components {
.button-bottom-default {
@apply border-[0px] border-b-[1px] border-solid border-BORDER;
}
}
@tailwind utilities;
#root {
width: 100vw;
height:100vh;
}
:global.ant-tree-node-content-wrapper{
overflow: hidden;
}
.tree-title-hover{
display: flex;
justify-content: space-between;
align-items:center;
.tree-title-span{
text-overflow: ellipsis;
}
.tree-title-more{
display: none;
}
&:hover .tree-title-more{
display: flex;
height:22px;
width:22px;
}
}
.ant-layout-content.apipark-layout-layout-content{
border-radius:10px 0 0 0 ;
overflow:hidden;
background-color:'transparent'
}
.apipark-layout-global-header-collapsed-button{
color:hsl(0, 0%, 100%);
}
.apipark-layout-top-nav-header-main{
display: flex;
align-items: center;
}
.apipark-layout-top-nav-header-menu {
height:50px;
line-height:50px;
.ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-selected::after{
border-bottom:2px solid #fff !important;
}
.ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-active:not(.ant-menu-item-selected)::after{
border-bottom:2px solid transparent !important;
}
}
.apipark-layout-base-menu-inline-group .ant-menu-item-group-title{
color:rgb(255 255 255 / 70%) !important;
}
.avatar-dom > div{
display: flex;
flex-direction: row-reverse;
align-items: center;
gap:8px;
}
.apipark-layout-layout{
.apipark-layout-layout-bg-list{
background-image: radial-gradient(circle farthest-corner at 450px 350px, #050eb7, #17163e 500px);
}
.ant-layout-header.apipark-layout-layout-header{
backdrop-filter: unset !important;
height:var(--layout-header-height);
line-height: var(--layout-header-height);
background-color: transparent;
li.apipark-layout-base-menu-horizontal-menu-item{
color:rgb(255 255 255 / 70%) !important;
&.ant-menu-item-selected{
color:#fff !important;
}
&.ant-menu-item-active{
color:#fff !important;
}
}
li.ant-menu-submenu-horizontal.ant-menu-overflow-item-rest .ant-menu-submenu-title{
color:#fff !important;
}
}
.ant-layout-sider.apipark-layout-sider{
height:calc(100vh - var(--layout-header-height)) !important;
inset-block-start: var(--layout-header-height);
.ant-menu {
.ant-menu-item-group-title{
font-size:12px;
padding:12px 16px;
}
.ant-menu-item{
margin-block:0 !important;
}
.ant-menu-light:not(.ant-menu-horizontal) .ant-menu-item:not(.ant-menu-item-selected):active{
background-color: unset;
}
}
.apipark-layout-sider-collapsed-button{
display: none;
}
ul.ant-menu.ant-menu-root.ant-menu-inline,
ul.ant-menu.ant-menu-root.ant-menu-vertical{
> li {
color:rgb(255 255 255 / 70%) !important;
/* border-radius: 10px;
background-color: rgba(255,255,255,0.1) !important;
border: 1px solid rgba(255,255,255,0.15); */
}
> li.ant-menu-item-active {
color:#fff !important;
}
> li.ant-menu-item-selected {
background-color: #fff !important;
border: 1px solid #fff !important;
color:#333 !important;
}
}
ul.apipark-layout-sider-menu .ant-menu-item-group-list{
> li {
color:rgb(255 255 255 / 70%) !important;
}
> li:active{
background-color: transparent;
}
> li.ant-menu-item-active {
color:#fff !important;
}
> li.ant-menu-item-selected {
background-color: #fff !important;
border: 1px solid #fff !important;
color:#333 !important;
}
}
.ant-menu-item {
height:40px;
margin-block:10px;
}
}
.apipark-layout-drawer-sider{
background:#17163E;
padding-top:20px;
.ant-layout-sider.apipark-layout-sider{
height: 100% !important;
inset-block: 20px;
}
}
.apipark-layout-layout-container{
>.ant-layout-header{
height:var(--layout-header-height) !important;
line-height:var(--layout-header-height) !important;
}
>.apipark-layout-layout-content.apipark-layout-layout-has-header{
padding-block:0px;
padding-inline:0px;
background-color: #fff !important;
}
}
.ant-pro-global-header-header-actions-avatar > div{
color:#fff !important;
}
.ant-menu-item-divider.apipark-layout-base-menu-inline-divider{
border-color: rgb(255 255 255 / 15%) !important;
}
}
.tox-tinymce{
border:none !important;
}
a{
transition:none !important;
}
.ant-result ant-result-error{
background-color: #fff !important;
}
.ant-tabs-tab-btn{
display: flex;
align-items:center;
.ant-tabs-tab-icon{
display: inline-flex;
align-items:center;
}
}
.eo_page_list .ant-pro-table{
overflow: hidden;
border-radius: 10px;
border:1px solid var(--table-border-color) !important;
}
.swagger-ui{
width: 100%;
.model-box-control:focus,.models-control:focus, .opblock-summary-control:focus{
outline:unset !important;
}
.information-container{
.info{
display: none;
}
}
}
.ant-pro-table .ant-popover .ant-popover-inner-content{
.ant-form-item{
background-color: transparent;
border:none;
}
}
.ant-menu .ant-menu-title-content{
display:unset !important;
}
.ai-setting-svg-container svg{
width: 100%;
height:100%;
display:block;
}
.ai-service-api-preview .swagger-ui h3.opblock-tag{
display: none;
}
/* 整个背景容器设置 */
.background-container {
background: radial-gradient(ellipse 80% 900px at top, rgb(255 255 255 / 10%) 0%, rgb(4 0 71) 30%, rgb(13 17 23) 100%);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
z-index: 1;
isolate: isolate;
}
/* SVG背景图案 */
.background-pattern {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
stroke: rgba(255, 255, 255, 0.1);
mask-image: radial-gradient(100% 100% at top right, white, transparent);
}
.login-block{
background: rgba(255, 255, 255, 0.1) !important;
.login-input{
color:#fff !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
&:hover, &:focus, &.ant-input-status-error, &.ant-input-status-error:hover, &.ant-input-status-error:focus-within{
background: rgba(255, 255, 255, 0.2) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
}
}
input:-webkit-autofill,
input:-webkit-autofill:focus {
transition: background-color 0s 600000s, color 0s 600000s !important;
}
}
.ant-select-selection-overflow-item:first-child {
max-width: calc(100% - 60px);
margin-right: 4px;
}
a[disabled]:hover {
color: #BBB;
cursor: not-allowed;
}
.ant-input-group-addon{
height:32px !important;
.ant-btn.ant-btn-default{
height:32px !important;
}
}
-405
View File
@@ -1,405 +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.status !== 'anonymous') {
dispatch({ type: 'LOGIN' })
navigate(state.mainPage, { replace: true })
} else {
dispatch({ type: 'LOGOUT' })
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)
}
})
}, [])
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"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:svgjs="http://svgjs.dev/svgjs"
viewBox="0 0 800 450"
opacity="1"
>
<defs>
<filter
id="bbblurry-filter"
x="-100%"
y="-100%"
width="400%"
height="400%"
filterUnits="objectBoundingBox"
primitiveUnits="userSpaceOnUse"
color-interpolation-filters="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={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={FeishuLogo} />
{$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
@@ -1,656 +0,0 @@
import { App, Button, Card, CascaderProps, Empty, Select } from 'antd'
import { $t } from '@common/locales/index.ts'
import { Icon } from '@iconify/react/dist/iconify.js'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import ReactJson from 'react-json-view'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useConnection } from './hook/useConnection'
import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { useNavigate } from 'react-router-dom'
import { ServiceDetailType } from '@market/const/serviceHub/type'
import useCopyToClipboard from '@common/hooks/copy'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Cascader } from 'antd/lib'
type ConfigList = {
openApi?: {
title: string
configContent: string
apiKeys: string[]
}
mcp: {
title: string
configContent: string
apiKeys: string[]
}
}
type ApiKeyItem = {
expired: number
id: string
name: string
value: string
}
interface Option {
value: string
label: string
children?: Option[]
}
type ServiceApiKeyList = {
id: string
name: string
apikeys: Array<{
id: string
name: string
value: string
expired: number
}>
}
type ConsumerParamsType = {
consumerId: string
teamId: string
}
export interface IntegrationAIContainerRef {
getServiceKeysList: () => void;
}
export interface IntegrationAIContainerProps {
type: 'global' | 'service' | 'consumer'
handleToolsChange: (value: Tool[]) => void
customClassName?: string
service?: ServiceDetailType
serviceId?: string
currentTab?: string
openModal?: (type: 'apply') => void
consumerParams?: ConsumerParamsType
}
export const IntegrationAIContainer = forwardRef<IntegrationAIContainerRef, IntegrationAIContainerProps>(
({
type,
handleToolsChange,
customClassName,
service,
serviceId,
currentTab,
openModal,
consumerParams
}: IntegrationAIContainerProps, ref) => {
/** 当前激活的标签 */
const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp')
/** 弹窗组件 */
const { message } = App.useApp()
/** 配置内容 */
const [configContent, setConfigContent] = useState<string>('')
/** 当前选中 API Key */
const [apiKey, setApiKey] = useState<string>('')
/** API Key 列表 */
const [apiKeyList, setApiKeyList] = useState<any[]>([])
/** Cascader Key 列表 */
const [cascaderKeyList, setCascaderKeyList] = useState<string[]>([])
/** MCP 服务器地址 */
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
/** 全局状态 */
const { state } = useGlobalContext()
const navigator = useNavigate()
/** 复制组件 */
const { copyToClipboard } = useCopyToClipboard()
/** 错误提示 */
const [errors, setErrors] = useState<Record<string, string | null>>({
resources: null,
prompts: null,
tools: null
})
/** 标签内容 */
const [tabContent, setTabContent] = useState<ConfigList>({
mcp: {
title: $t('MCP 配置'),
configContent: '',
apiKeys: []
}
})
/** HTTP 请求 */
const { fetchData } = useFetch()
/**
* 初始化标签数据
*/
const initTabsData = () => {
const params: ConfigList = {
mcp: {
title: $t('MCP 配置'),
configContent: service?.mcpAccessConfig || '',
apiKeys: []
}
}
if (type === 'service') {
params.openApi = {
title: $t('Open API 文档'),
configContent: service?.openapiAddress || '',
apiKeys: []
}
}
setTabContent(params)
}
/**
* 复制
* @param value
* @returns
*/
const handleCopy = async (value: string): Promise<void> => {
if (value) {
copyToClipboard(value)
message.success($t(RESPONSE_TIPS.copySuccess))
}
}
/**
* 选择 API Key
* @param value
*/
const handleSelectChange = (value: string) => {
setApiKey(value)
}
/**
* Cascader 选择
* @param value
*/
const handleCascaderChange: CascaderProps<Option>['onChange'] = (value) => {
setApiKey(value.at(-1) || '')
setCascaderKeyList(value)
}
/**
* 获取全局 MCP 配置
* @returns
*/
const getGlobalMcpConfig = () => {
fetchData<BasicResponse<null>>('global/mcp/config', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
setTabContent((prevTabContent) => ({
...prevTabContent,
mcp: {
...prevTabContent.mcp,
configContent: data.config || ''
}
}))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 获取消费者 MCP 配置
* @returns
*/
const getConsumerMcpConfig = () => {
fetchData<BasicResponse<null>>('app/mcp/config', {
method: 'GET',
eoParams: { app: consumerParams?.consumerId, team: consumerParams?.teamId }
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
setTabContent((prevTabContent) => ({
...prevTabContent,
mcp: {
...prevTabContent.mcp,
configContent: data.config || ''
}
}))
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 全局 MCP 跳转
*/
const addKey = () => {
navigator('/mcpKey')
}
const dropAuthPage = () => {
navigator(`/consumer/${consumerParams?.teamId}/inside/${consumerParams?.consumerId}/authorization`)
}
/**
* 获取全局 API Key 列表
*/
const getGlobalKeysList = () => {
fetchData<BasicResponse<null>>('simple/system/apikeys', {
method: 'GET'
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
if (data.apikeys && data.apikeys.length > 0) {
setApiKeyList(
data.apikeys.map((item: ApiKeyItem) => {
return {
label: item.name,
value: item.value
}
})
)
setApiKey(data.apikeys[0].value)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 抛出获取服务 API Key 列表
*/
useImperativeHandle(ref, () => ({
getServiceKeysList
}))
/**
* 获取 API Key 列表
*/
const getServiceKeysList = (consumerId?: string) => {
fetchData<BasicResponse<null>>(`my/app/apikeys`, {
method: 'GET',
eoParams: consumerId ? { app: consumerId } : { service: serviceId }
})
.then((response) => {
const { code, msg, data } = response
if (code === STATUS_CODE.SUCCESS) {
if (data.apps && data.apps.length > 0) {
// 转换数据结构为 Cascader 所需格式
const transformedData = data.apps.map((app: ServiceApiKeyList) => ({
value: app.id,
label: app.name,
children: app.apikeys.map((key) => ({
...key,
label: key.name
}))
}))
setApiKeyList(transformedData)
if (data.apps[0].apikeys?.length) {
setApiKey(data.apps[0].apikeys[0].value)
setCascaderKeyList([data.apps[0].id, data.apps[0].apikeys[0].value])
}
}
}
})
.catch((errorInfo) => {
message.error(errorInfo?.toString() || $t(RESPONSE_TIPS.error))
})
}
/**
* 清除错误提示
*/
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }))
}
/**
* 发送请求
*/
const makeRequest = async <T extends z.ZodType>(request: ClientRequest, schema: T, tabKey?: keyof typeof errors) => {
try {
const response = await makeConnectionRequest(request, schema)
if (tabKey !== undefined) {
clearError(tabKey)
}
return response
} catch (e) {
const errorString = (e as Error).message ?? String(e)
if (tabKey !== undefined) {
setErrors((prev) => ({
...prev,
[tabKey]: errorString
}))
}
throw e
}
}
/**
* 获取 MCP 的 tools
*/
const listTools = async () => {
const response = await makeRequest(
{
method: 'tools/list' as const,
params: {}
},
ListToolsResultSchema,
'tools'
)
handleToolsChange(response.tools)
}
/**
* 初始化连接 mcp
*/
const {
connectionStatus,
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
handleCompletion,
completionsSupported,
connect: connectMcpServer,
disconnect: disconnectMcpServer
} = useConnection({
transportType: 'sse',
sseUrl: '',
proxyServerUrl: mcpServerUrl,
requestTimeout: 1000
})
// 使用 useRef 保存最新的连接状态和断开函数
const connectionStatusRef = useRef(connectionStatus)
const disconnectFnRef = useRef(disconnectMcpServer)
// 当连接状态或断开函数变化时更新 ref
useEffect(() => {
connectionStatusRef.current = connectionStatus
disconnectFnRef.current = disconnectMcpServer
}, [connectionStatus, disconnectMcpServer])
/**
* 初始化数据
*/
const setupComponent = () => {
initTabsData()
if (type === 'global') {
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
getGlobalKeysList()
} else if (type === 'consumer'){
getConsumerMcpConfig()
setMcpServerUrl(`mcp/app/${consumerParams?.consumerId}/sse`)
getServiceKeysList(consumerParams?.consumerId)
} else {
service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`)
getServiceKeysList()
}
}
/**
* 初始化数据
*/
useEffect(() => {
setupComponent()
}, [service])
/**
* 初始化标签数据
*/
useEffect(() => {
initTabsData()
type === 'global' && getGlobalMcpConfig()
type === 'consumer' && getConsumerMcpConfig()
}, [state.language])
/**
* 切换标签
*/
useEffect(() => {
if (type === 'service') {
currentTab === 'MCP' ? setActiveTab('mcp') : setActiveTab('openApi')
}
}, [currentTab])
/**
* 仅在组件加载时执行初始化逻辑
*/
useEffect(() => {
// 返回清理函数,只会在组件卸载时执行
return () => {
try {
// 使用 ref 中保存的最新函数强制断开连接
const disconnectFn = disconnectFnRef.current
if (disconnectFn) {
disconnectFn()
}
} catch (err) {
console.error('断开连接时出错:', err)
}
}
}, [type])
/**
* 切换标签时更新配置内容
*/
useEffect(() => {
if (activeTab === 'openApi' && tabContent?.openApi?.configContent) {
setConfigContent(tabContent?.openApi?.configContent)
} else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) {
setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
}
}, [service, apiKey, activeTab, tabContent])
/**
* 连接 MCP 服务器
*/
useEffect(() => {
if (mcpServerUrl) {
if (connectionStatus === 'connected') {
disconnectMcpServer()
}
connectMcpServer()
}
}, [mcpServerUrl, ...(type === 'global' || type === 'consumer' ? [state.language] : [])])
/**
* 获取 MCP tools
*/
useEffect(() => {
if (connectionStatus === 'connected') {
listTools()
}
}, [connectionStatus])
return (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-[400px] h-fit ${customClassName}`}
classNames={{
body: 'p-[10px]'
}}
>
<p>
<Icon
icon="icon-park-solid:connection-point-two"
className="align-text-bottom mr-[5px]"
width="16"
height="16"
/>
{$t('AI 代理集成')}
</p>
{type === 'service' && service?.basic.enableMcp && (
<div className="mt-3 tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('openApi')}
>
Open API
</div>
<div
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
onClick={() => setActiveTab('mcp')}
>
MCP
</div>
</div>
)}
{(type === 'service' || type === 'consumer') && !apiKeyList.length ? (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-full mt-3`}
classNames={{
body: 'p-[10px]'
}}
>
{
type === 'service' ? (
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('请先订阅该服务')}</span>
<Button type="primary" onClick={() => openModal?.('apply')}>
{$t('申请')}
</Button>
</div>
) : (
<div className="flex flex-col items-center justify-center py-3">
<span className="text-[14px] mb-5">{$t('未配置 API Key')}</span>
<Button type="primary" onClick={() => dropAuthPage()}>
{$t('配置')}
</Button>
</div>
)
}
</Card>
</>
) : (
<>
<div className="tab-container mt-3">
<div className="tab-content font-semibold mt-[10px]">
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
</div>
{/* 标签页内容区域 */}
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
{activeTab === 'mcp' ? (
<ReactJson
src={
configContent
? typeof configContent === 'string'
? (() => {
try {
return JSON.parse(configContent)
} catch (e) {
return {}
}
})()
: configContent
: {}
}
theme="monokai"
indentWidth={2}
displayDataTypes={false}
displayObjectSize={false}
name={false}
collapsed={false}
enableClipboard={false}
style={{
backgroundColor: 'transparent',
wordBreak: 'break-word',
whiteSpace: 'normal'
}}
/>
) : (
<>
<pre className="whitespace-pre-wrap break-words">{configContent || ''}</pre>
</>
)}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '5px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</div>
{activeTab === 'mcp' && (
<>
<div className="tab-content font-semibold my-[10px]">API Key</div>
{apiKeyList.length ? (
<>
{type === 'global' ? (
<>
<Select
showSearch
optionFilterProp="label"
value={apiKey}
className="w-full"
onChange={handleSelectChange}
options={apiKeyList}
/>
<Card
style={{ borderRadius: '5px' }}
className="w-full mt-[5px] "
classNames={{
body: 'p-[5px]'
}}
>
<div className="relative h-[25px]">
{apiKey}
<IconButton
name="copy"
onClick={() => handleCopy(apiKey)}
sx={{
position: 'absolute',
top: '0px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</Card>
</>
) : (
<>
<Cascader
className='w-full'
allowClear={false}
options={apiKeyList}
value={cascaderKeyList}
onChange={handleCascaderChange}
placeholder={$t('选择 API Key')}
/>
</>
)}
</>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={''}>
<Button onClick={addKey} type="primary">
{$t('新增 API Key')}
</Button>
</Empty>
)}
</>
)}
</>
)}
</Card>
</>
)
})
@@ -1,199 +0,0 @@
import PageList from "@common/components/aoplatform/PageList.tsx"
import {ActionType} from "@ant-design/pro-components";
import {FC, useEffect, useMemo, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom";
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx";
import {App, Modal} from "antd";
import {BasicResponse, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import { SimpleMemberItem } from "@common/const/type.ts";
import {useFetch} from "@common/hooks/http.ts";
import { TEAM_TABLE_COLUMNS } from "../../const/team/const.tsx";
import { TeamConfigFieldType, TeamConfigHandle, TeamTableListItem } from "../../const/team/type.ts";
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx";
import { checkAccess } from "@common/utils/permission.ts";
import TeamConfig from "./TeamConfig.tsx";
import InsidePage from "@common/components/aoplatform/InsidePage.tsx";
import { $t } from "@common/locales/index.ts";
const TeamList:FC = ()=>{
const [searchWord, setSearchWord] = useState<string>('')
const navigate = useNavigate();
const location = useLocation()
const currentUrl = location.pathname
const { setBreadcrumb } = useBreadcrumb()
const { modal,message } = App.useApp()
const pageListRef = useRef<ActionType>(null);
const {fetchData} = useFetch()
const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({})
const teamConfigRef = useRef<TeamConfigHandle>(null)
const {accessData,checkPermission,accessInit, getGlobalAccessData,state} = useGlobalContext()
const [curTeam, setCurTeam] = useState<TeamConfigFieldType>({} as TeamConfigFieldType)
const [modalVisible, setModalVisible] = useState<boolean>(false)
const [modalType, setModalType] = useState<'add'|'edit'>('add')
const getTeamList = ()=>{
if(!accessInit){
getGlobalAccessData()?.then?.(()=>{getTeamList()})
return
}
return fetchData<BasicResponse<{teams:TeamTableListItem}>>(!checkPermission('system.workspace.team.view_all') ? 'teams':'manager/teams',{method:'GET',eoParams:{keyword:searchWord},eoTransformKeys:['create_time','service_num','can_delete']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
return {data:data.teams, success: true}
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return {data:[], success:false}
}
}).catch(() => {
return {data:[], success:false}
})
}
const deleteTeam = (entity:TeamTableListItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>(`manager/team`,{method:'DELETE',eoParams:{id: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 getMemberList = async ()=>{
setMemberValueEnum({})
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
if(code === STATUS_CODE.SUCCESS){
const tmpValueEnum:{[k:string]:{text:string}} = {}
data.members?.forEach((x:SimpleMemberItem)=>{
tmpValueEnum[x.name] = {text:x.name}
})
setMemberValueEnum(tmpValueEnum)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
const manualReloadTable = () => {
pageListRef.current?.reload()
};
const openModal = async (type:'add'|'edit'|'delete',entity?:TeamTableListItem)=>{
let title:string = ''
let content:string | React.ReactNode= ''
switch (type){
case 'add':{
setModalType('add')
setModalVisible(true)
return;}
case 'edit':{
message.loading($t(RESPONSE_TIPS.loading))
const {code,data,msg} = await fetchData<BasicResponse<{team:TeamConfigFieldType}>>(`manager/team`,{method:'GET',eoParams:{id:entity!.id}})
message.destroy()
if(code === STATUS_CODE.SUCCESS){
setCurTeam({...data.team,master:data.team.master.id})
setModalVisible(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return
}
setModalType('edit')
return;}
case 'delete':
title=$t('删除')
content=$t(DELETE_TIPS.default)
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){
case 'delete':
return deleteTeam(entity!).then((res)=>{if(res === true) manualReloadTable()})
}
},
width:600,
okText:$t('确认'),
okButtonProps:{
disabled : !checkAccess( `system.organization.team.${type}`, accessData)
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
})
}
useEffect(() => {
setBreadcrumb([
{title: $t('团队')}
])
manualReloadTable()
}, [currentUrl]);
useEffect(()=>{
getMemberList()
},[])
const columns = useMemo(()=>{
return TEAM_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('master') !== -1 ) ){x.valueEnum = memberValueEnum} return {...x, title:typeof x.title === 'string' ? $t(x.title as string) : x.title}})
},[memberValueEnum,state.language])
return (
<InsidePage
pageTitle={$t('团队')}
description={$t("设置团队和成员,然后你可以在团队内创建服务和消费者、订阅API,成员只能看到所属团队内的服务和消费者。")}
showBorder={false}
contentClassName=" pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<PageList
id="global_team"
className="pl-btnbase"
ref={pageListRef}
columns = {[...columns]}
request = {()=>getTeamList()}
showPagination={false}
addNewBtnTitle={$t('添加团队')}
addNewBtnAccess = "system.organization.team.add"
searchPlaceholder={$t("输入名称、ID、负责人查找团队")}
onAddNewBtnClick={()=>{openModal('add')}}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onRowClick={(row:TeamTableListItem)=>(navigate(`../inside/${row.id}/setting`))}
/>
<Modal
title={modalType === 'add' ? $t("添加团队") : $t("配置团队")}
open={modalVisible}
width={600}
destroyOnClose={true}
maskClosable={false}
afterOpenChange={(open:boolean)=>{
if(!open){
setModalVisible(false)
setCurTeam({} as unknown as TeamConfigFieldType)
}
}}
onCancel={() => {setModalVisible(false)}}
okText={$t("确认")}
okButtonProps={{disabled : !checkAccess( `system.organization.team.edit`, accessData)}}
cancelText={$t('取消')}
closable={true}
onOk={()=>teamConfigRef.current?.save().then((res)=>{
if(res){
setModalVisible(false)
manualReloadTable()
}
return res})}
>
<TeamConfig ref={teamConfigRef} entity={modalType === 'add' ? undefined : curTeam} />
</Modal>
</InsidePage>
)
}
export default TeamList
-17
View File
@@ -1,17 +0,0 @@
// start-vite.js// start-vite.js
import { exec } from 'child_process';
const viteProcess = exec('pnpm run build');
viteProcess.stdout.on('data', (data) => {
console.log(data.toString());
});
viteProcess.stderr.on('data', (data) => {
console.error(data.toString());
});
viteProcess.on('close', (code) => {
console.log(`Vite process exited with code ${code}`);
});
-82
View File
@@ -1,82 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);',
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-31
View File
@@ -1,31 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"typeRoots": ["./node_modules/@types", "../common/src/types"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@core/*": ["./src/*"],
"@common/*": ["../common/src/*"],
"@market/*": ["../market/src/*"],
"@dashboard/*": ["../dashboard/src/*"],
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../common/src/components/aoplatform/ScrollableSection.tsx", "../common/src/utils/postcat.tsx", "../common/src/utils/curl.ts", "../common/src/components/aoplatform/ResetPsw.tsx", "../common/src/components/aoplatform/SubscribeApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePageForHub.tsx", "src/components/aoplatform/RenderRoutes.tsx", "../common/src/components/aoplatform/PublishApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePage.tsx", "../common/src/const/type.ts", "../common/src/components/aoplatform/intelligent-plugin", "../common/src/const/domain"],
"references": [{ "path": "./tsconfig.node.json" }]
}
-10
View File
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-84
View File
@@ -1,84 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
import federation from "@originjs/vite-plugin-federation";
export default defineConfig({
cacheDir: './node_modules/.vite',
build:{
target: 'esnext',
outDir:'../../dist',
sourcemap: false,
chunkSizeWarningLimit: 50,
cacheDir: './node_modules/.vite',
rollupOptions: {
output: {
chunkFileNames: 'assets/eo-[name]-[hash].js',
},
},
},
css: {
postcss: {
plugins: [
tailwindcss(path.resolve(__dirname, '../common/tailwind.config.js')),
autoprefixer
],
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
},
},
modules:{
localsConvention:"camelCase",
generateScopedName:"[local]_[hash:base64:2]"
}
},
plugins: [react(),
dynamicImportVars({
include:["src"],
exclude:[],
warnOnError:false
}),
federation({
name:"container",
remotes:{
remoteApp: 'http://localhost:5001/assets/remoteEntry.js' // 远程项目的URL
},
shared:[
"react",
"react-dom",
]
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@common', replacement: path.resolve(__dirname, '../common/src') },
{ find: '@market', replacement: path.resolve(__dirname, '../market/src') },
{ find: '@core', replacement: path.resolve(__dirname, './src') },
{ find: '@dashboard', replacement: path.resolve(__dirname, '../dashboard/src') },
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8288/',
changeOrigin: true,
}
},
open: true
},
logLevel:'info'
})
-11
View File
@@ -1,11 +0,0 @@
{
"name": "dashboard",
"version": "0.0.0",
"description": "dashboard for AO Platform",
"author": "maggieyyy ",
"homepage": "",
"license": "ISC",
"dependencies": {
"echarts-for-react": "^3.0.2"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@dashboard/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-16
View File
@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/frontend/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>APIPark - 企业API数据开放平台</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/frontend/iconpark_eolink.js"></script>
<script src="/frontend/iconpark_apinto.js"></script>
</body>
</html>
-17
View File
@@ -1,17 +0,0 @@
{
"name": "market",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "service market project",
"scripts": {
"dev": " vite --port 5000 --strictPort",
"build": "vite build",
"test": "node ./__tests__/market.test.js"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"dompurify": "^3.1.6",
"react-virtuoso": "^4.7.11"
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
-30
View File
@@ -1,30 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
#root {
width: 100vw;
height:100vh;
}
:global.ant-tree-node-content-wrapper{
overflow: hidden;
}
.tree-title-hover{
display: flex;
justify-content: space-between;
align-items:center;
.tree-title-span{
text-overflow: ellipsis;
}
.tree-title-more{
display: none;
}
&:hover .tree-title-more{
display: flex;
height:22px;
width:22px;
}
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@market/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-52
View File
@@ -1,52 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'
export default defineConfig({
build: {
outDir: '../../tenant_dist',
sourcemap: false,
chunkSizeWarningLimit: 50000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
}
}
}
},
plugins: [
react(),
dynamicImportVars({
include: ['src'],
exclude: [],
warnOnError: false
})
],
resolve: {
alias: [
{ find: /^~/, replacement: '' },
{ find: '@market', replacement: path.resolve(__dirname, './src') },
{ find: '@common', replacement: path.resolve(__dirname, '../common/src') },
{ find: '@core', replacement: path.resolve(__dirname, '../core/src') }
]
},
server: {
proxy: {
'/api/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8488/',
changeOrigin: true
},
'/api2/v1': {
// target: 'http://uat.apikit.com:11204/mockApi/aoplatform/',
target: 'http://172.18.166.219:8488/',
changeOrigin: true
}
}
},
logLevel: 'info'
})
-15
View File
@@ -1,15 +0,0 @@
{
"name": "open-api",
"version": "0.0.0",
"description": "openApi module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "node ./__tests__/common.test.js"
},
"dependencies": {
"copy-to-clipboard": "^3.3.3"
},
"devDependencies": {
}
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
"@openApi/*": ["./src/*"]
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
@@ -1,8 +0,0 @@
{
"name": "systemrunning",
"version": "1.0.0",
"description": "> TODO: description",
"author": "maggieyyy <61950669+maggieyyy@users.noreply.github.com>",
"homepage": "",
"license": "ISC"
}
@@ -1,10 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
},
}
@@ -1,81 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
important:true,
content: [
`./index.html`,
`../*/src/**/*.{js,ts,jsx,tsx}`,
],
theme: {
extend: {
width: {
INPUT_NORMAL: '100%',
// INPUT_NORMAL: '346px',
INPUT_LARGE: '508px',
GROUP: '240px',
SEARCH: '276px',
LOG: '254px'
},
minHeight:{
TEXTAREA:'68px'
},
borderRadius: {
DEFAULT: 'var(--border-radius)',
SEARCH_RADIUS: '50px'
},
boxShadow:{
SCROLL: '0 2px 2px #0000000d',
SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)'
},
colors: {
DISABLE_BG: 'var(--disabled-background-color)',
MAIN_TEXT: 'var(--text-color)',
MAIN_HOVER_TEXT: 'var(--text-hover-color)',
SECOND_TEXT:'var(--disabled-text-color)',
MAIN_BG: 'var(--background-color)',
MENU_BG:'var(--MENU-BG-COLOR)',
'bar-theme': 'var(--bar-background-color)',
BORDER: 'var(--border-color)',
NAVBAR_BTN_BG: 'var(--item-active-background-color)',
MAIN_DISABLED_BG: 'var(--disabled-background-color)',
theme: 'var(--primary-color)',
DESC_TEXT: 'var(--TITLE_TEXT)',
HOVER_BG: 'var(--item-hover-background-color)',
guide_cluster: '#ee6760',
guide_upstream: '#f9a429',
guide_api: '#71d24d',
guide_publishApi: '#5884ff',
guide_final: '#915bf9',
table_text: 'var(--table-text-color)',
status_success:'#138913',
status_fail:"#ff3b30",
status_update:"#03a9f4",
status_pending:"#ffa500",
status_offline:"#8f8e93",
A_HOVER:'var(--button-primary-hover-background-color)'
},
spacing: {
mbase: 'var(--FORM_SPAN)',
label: '12px', // 选择器和label之间的间距,待删
btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距
btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距
btnrbase: '20px', // 页面最右侧边距20px
formtop: 'var(--FORM_SPAN)',
icon: '5px',
blockbase: '40px',
DEFAULT_BORDER_RADIUS: 'var(--border-radius)',
TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);'
},
borderColor: {
'color-base': 'var(--border-color)'
}
}
},
plugins: [],
corePlugins: {
preflight: false,
},
}
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"paths": {
"@common/*": ["../common/src/*"],
"@core/*": ["../core/src/*"],
},
},
"include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
@@ -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]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</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]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
{$t('点击')}
&nbsp;
<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";
+319
View File
@@ -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";
+30
View File
@@ -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>
)
}
+5
View File
@@ -0,0 +1,5 @@
const McpPage = () => {
return <div>MCP</div>
}
export default McpPage

Some files were not shown because too many files have changed in this diff Show More