mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-12 18:11:34 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1942344373 | |||
| 96ad58fe28 |
@@ -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
@@ -6,7 +6,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
.next/
|
||||
node_modules
|
||||
dist
|
||||
market_dist
|
||||
|
||||
@@ -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!');
|
||||
@@ -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!');
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "independent"
|
||||
|
||||
}
|
||||
@@ -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 重新安装依赖。');
|
||||
Vendored
+4
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
VITE_APP_MODE=openSource
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: {}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
|
||||
</a>
|
||||
<span className="align-middle leading-[16px]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
{$t('点击')}
|
||||
|
||||
<span className="align-middle leading-[16px]">
|
||||
<Icon icon="emojione:star" width="16" height="16" />
|
||||
</span>
|
||||
Star
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B pl-[40px]">
|
||||
<AIModelGuide />
|
||||
<div className="flex flex-col gap-[15px] pb-PAGE_INSIDE_B">
|
||||
{showGuide && (
|
||||
<Collapse
|
||||
size="large"
|
||||
expandIconPosition="end"
|
||||
defaultActiveKey={['1']}
|
||||
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="">
|
||||
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
|
||||
<span>🚀</span>
|
||||
<span>{`${$t('快速入门')}`}</span>{' '}
|
||||
</p>
|
||||
<p className="text-[12px]">{$t('我们提供了一些任务来帮你快速了解 APIPark')}</p>
|
||||
</div>
|
||||
),
|
||||
children: <QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{showAdvancedGuide && (
|
||||
<Collapse
|
||||
size="large"
|
||||
expandIconPosition="end"
|
||||
defaultActiveKey={['1']}
|
||||
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<div className="">
|
||||
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
|
||||
<span>🏍️</span>
|
||||
<span>{`${$t('进阶教程')}`}</span>{' '}
|
||||
</p>
|
||||
<p className="text-[12px]">{$t('了解 APIPark 如何更好地管理 API 和 AI')}</p>
|
||||
</div>
|
||||
),
|
||||
children: <QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,319 @@
|
||||
'use client'
|
||||
|
||||
import { StyleProvider } from '@ant-design/cssinjs'
|
||||
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import AvatarPic from '@common/assets/default-avatar.png'
|
||||
import Logo from '@common/assets/layout-logo.png'
|
||||
import LanguageSetting from '@common/components/aoplatform/LanguageSetting'
|
||||
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const'
|
||||
import { PERMISSION_DEFINITION } from '@common/const/permissions'
|
||||
import { UserInfoType } from '@common/const/type'
|
||||
import { GlobalProvider, useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { LocaleProvider, useLocaleContext } from '@common/contexts/LocaleContext'
|
||||
import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'
|
||||
import { PluginSlotHubProvider, usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { transformMenuData } from '@common/utils/navigation'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { App as AppAntd, Button, ConfigProvider, Dropdown, MenuProps, Spin } from 'antd'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const themeToken = {
|
||||
bgLayout: '#17163E;',
|
||||
header: {
|
||||
heightLayoutHeader: 72
|
||||
},
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 0,
|
||||
paddingInlinePageContainerContent: 0
|
||||
}
|
||||
}
|
||||
|
||||
function AdminShell({ children, project = 'core' }: { children: ReactNode; project?: string }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
|
||||
useGlobalContext()
|
||||
const [currentPath, setCurrentPath] = useState(pathname)
|
||||
const mainPage = state.mainPage || (project === 'core' ? '/guide/page' : '/portal/list')
|
||||
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
|
||||
const pluginSlotHub = usePluginSlotHub()
|
||||
const { message } = AppAntd.useApp()
|
||||
const [userInfo, setUserInfo] = useState<UserInfoType>()
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
setMenuItems(transformMenuData(menuList))
|
||||
}, [menuList, state.language, accessInit])
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === '/') {
|
||||
router.push(mainPage)
|
||||
}
|
||||
}, [pathname, mainPage, router])
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPath(pathname)
|
||||
}, [pathname])
|
||||
|
||||
const headerMenuData = useMemo(() => {
|
||||
const hasAccess = (access: unknown) => checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
|
||||
|
||||
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
|
||||
return [...menu]
|
||||
.filter((x) => x)
|
||||
.map((item: any) => {
|
||||
if (item.routes && item.routes.length > 0) {
|
||||
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes)
|
||||
if (filteredRoutes.length === 0) {
|
||||
return false
|
||||
}
|
||||
return { ...item, routes: filteredRoutes, name: $t(item.name) }
|
||||
}
|
||||
if (item.access) {
|
||||
return item.access === 'all' || hasAccess(item.access) ? { ...item, name: $t(item.name) } : null
|
||||
}
|
||||
return { ...item, name: $t(item.name) }
|
||||
})
|
||||
.filter((x) => x)
|
||||
}
|
||||
|
||||
const res = [...(menuItems || [])]
|
||||
.filter((x) => x)
|
||||
.map((x: any) =>
|
||||
x.routes ? { ...x, name: $t(x.name), routes: filterMenu(x.routes) } : { ...x, name: $t(x.name) }
|
||||
)
|
||||
|
||||
return {
|
||||
path: '/',
|
||||
routes: res
|
||||
.map((x) => ({ ...x, routes: x.routes?.filter((routeItem: any) => routeItem.access || routeItem.routes?.length > 0) }))
|
||||
.filter((x) => x.access || x.routes?.length > 0)
|
||||
}
|
||||
}, [accessData, state.language, menuItems, checkPermission])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setUserInfo(data.profile)
|
||||
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
getGlobalAccessData()
|
||||
}, [])
|
||||
|
||||
const logOut = () => {
|
||||
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
dispatch({ type: 'LOGOUT' })
|
||||
resetAccess()
|
||||
router.push('/admin/login')
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() =>
|
||||
[
|
||||
!['guest', 'third-user'].includes(userInfo?.type as string) && {
|
||||
key: '2',
|
||||
label: (
|
||||
<Button
|
||||
key="changePsw"
|
||||
type="text"
|
||||
className="flex items-center p-0 bg-transparent border-none"
|
||||
onClick={() => router.push('/userProfile/changepsw')}
|
||||
>
|
||||
{$t('账号设置')}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none" onClick={logOut}>
|
||||
{$t('退出登录')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
].filter(Boolean),
|
||||
[userInfo, router]
|
||||
)
|
||||
|
||||
const actionRender = useMemo(() => {
|
||||
return [
|
||||
<LanguageSetting key="lang" />,
|
||||
<Button
|
||||
key="docs"
|
||||
className="text-[#ffffffb3] hover:text-[#fff] border-none"
|
||||
type="default"
|
||||
ghost
|
||||
onClick={() => window.open('https://docs.apipark.com', '_blank')}
|
||||
>
|
||||
<span className="flex items-center gap-[8px]">
|
||||
<Icon icon="ic:baseline-help" width="14" height="14" />
|
||||
{$t('文档')}
|
||||
</span>
|
||||
</Button>,
|
||||
...(((pluginSlotHub.getSlot('basicLayoutAfterBtns') as ReactNode[]) || []) as ReactNode[])
|
||||
]
|
||||
}, [state.language, pluginSlotHub])
|
||||
|
||||
const logoSrc = typeof Logo === 'string' ? Logo : (Logo as any)?.src
|
||||
const avatarSrc = userInfo?.avatar || (typeof AvatarPic === 'string' ? AvatarPic : (AvatarPic as any)?.src)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="test-pro-layout"
|
||||
style={{
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<ProConfigProvider hashed={false}>
|
||||
<ConfigProvider
|
||||
getTargetContainer={() => {
|
||||
return document.getElementById('test-pro-layout') || document.body
|
||||
}}
|
||||
>
|
||||
<ProLayout
|
||||
prefixCls="apipark-layout"
|
||||
location={{ pathname: currentPath }}
|
||||
siderWidth={220}
|
||||
breakpoint={'lg'}
|
||||
route={headerMenuData as any}
|
||||
token={themeToken}
|
||||
siderMenuType="group"
|
||||
menu={{ type: 'group', collapsedShowGroupTitle: true }}
|
||||
disableMobile={true}
|
||||
avatarProps={{
|
||||
src: avatarSrc,
|
||||
size: 'small',
|
||||
title: userInfo?.username || 'unknown',
|
||||
render: (props, dom) => (
|
||||
<Dropdown menu={{ items }}>
|
||||
<div className="avatar-dom">{dom}</div>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
actionsRender={(props) => {
|
||||
if (props.isMobile) return []
|
||||
return actionRender
|
||||
}}
|
||||
headerTitleRender={() => (
|
||||
<div className="w-[192px] flex items-center">
|
||||
<img className="h-[20px] cursor-pointer" src={logoSrc} onClick={() => router.push(mainPage)} alt="logo" />
|
||||
<a
|
||||
className="align-text-top ml-[5px] h-[25px] relative"
|
||||
href="https://github.com/APIParkLab/APIPark"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social"
|
||||
className="absolute top-[6px]"
|
||||
width={75}
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
logo={logoSrc}
|
||||
pageTitleRender={() => $t('APIPark')}
|
||||
menuFooterRender={(props) => {
|
||||
if (props?.collapsed) return undefined
|
||||
}}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (
|
||||
item.key &&
|
||||
routerKeyMap.get(item.key as string) &&
|
||||
(routerKeyMap.get(item.key as string) as string[])?.length > 0 &&
|
||||
(routerKeyMap.get(item.key as string) as string[])?.indexOf(currentPath.split('/')[1]) !== -1
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (item.key === currentPath.split('/')[1]) {
|
||||
return
|
||||
}
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
}
|
||||
setCurrentPath(item.path || '')
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</div>
|
||||
)}
|
||||
fixSiderbar={true}
|
||||
layout="mix"
|
||||
splitMenus={true}
|
||||
collapsed={false}
|
||||
collapsedButtonRender={false}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-calc-100vh-minus-navbar ${currentPath.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
|
||||
} ${currentPath.startsWith('/guide/page') ? '' : 'pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ProLayout>
|
||||
</ConfigProvider>
|
||||
</ProConfigProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AdminProviders({ children }: { children: ReactNode }) {
|
||||
const { locale } = useLocaleContext()
|
||||
|
||||
return (
|
||||
<StyleProvider hashPriority="high">
|
||||
<ConfigProvider locale={locale} wave={{ disabled: true }}>
|
||||
<PluginEventHubProvider>
|
||||
<GlobalProvider>
|
||||
<AppAntd className="h-full" message={{ maxCount: 1 }}>
|
||||
<PluginSlotHubProvider>
|
||||
<AdminShell project="core">{children}</AdminShell>
|
||||
</PluginSlotHubProvider>
|
||||
</AppAntd>
|
||||
</GlobalProvider>
|
||||
</PluginEventHubProvider>
|
||||
</ConfigProvider>
|
||||
</StyleProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={true}
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleProvider>
|
||||
<AdminProviders>{children}</AdminProviders>
|
||||
</LocaleProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function ServiceLegacyFallbackPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const { slug } = await params
|
||||
|
||||
if (slug.length >= 4 && (slug[1] === 'inside' || slug[1] === 'aiInside')) {
|
||||
redirect(`/service/${slug[0]}/${slug[1]}/${slug[2]}/overview`)
|
||||
}
|
||||
|
||||
redirect('/service/list')
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function AiServiceApprovalRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="aiInside" type="approval" />
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ReactNode } from 'react'
|
||||
import { ServiceDetailLayout } from '../../../_components/ServicePages'
|
||||
|
||||
const serviceKeys = [
|
||||
'overview',
|
||||
'route',
|
||||
'api',
|
||||
'document',
|
||||
'servicepolicy',
|
||||
'publish',
|
||||
'approval',
|
||||
'subscriber',
|
||||
'setting',
|
||||
'logs'
|
||||
] as const
|
||||
|
||||
function getActiveKey(pathname: string) {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const active = segments[4]
|
||||
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
|
||||
}
|
||||
|
||||
export default function AiServiceDetailLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: ReactNode
|
||||
params: { teamId: string; serviceId: string }
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const { teamId, serviceId } = params
|
||||
|
||||
return (
|
||||
<ServiceDetailLayout
|
||||
teamId={teamId}
|
||||
serviceId={serviceId}
|
||||
side="aiInside"
|
||||
activeKey={getActiveKey(pathname)}
|
||||
>
|
||||
{children}
|
||||
</ServiceDetailLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function AiServiceOverviewRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceOverviewPage serviceType="aiService" teamId={teamId} serviceId={serviceId} />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function AiServiceEntryPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
redirect(`/service/${teamId}/aiInside/${serviceId}/overview`)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function AiServicePublishRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="aiInside" type="publish" />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function AiServiceRouteListRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="aiInside" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function RestServiceApprovalRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="inside" type="approval" />
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ReactNode } from 'react'
|
||||
import { ServiceDetailLayout } from '../../../_components/ServicePages'
|
||||
|
||||
const serviceKeys = [
|
||||
'overview',
|
||||
'route',
|
||||
'api',
|
||||
'upstream',
|
||||
'document',
|
||||
'servicepolicy',
|
||||
'publish',
|
||||
'approval',
|
||||
'subscriber',
|
||||
'setting',
|
||||
'logs'
|
||||
] as const
|
||||
|
||||
function getActiveKey(pathname: string) {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const active = segments[4]
|
||||
return (serviceKeys.find((key) => key === active) || 'overview') as (typeof serviceKeys)[number]
|
||||
}
|
||||
|
||||
export default function RestServiceDetailLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: ReactNode
|
||||
params: { teamId: string; serviceId: string }
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const { teamId, serviceId } = params
|
||||
|
||||
return (
|
||||
<ServiceDetailLayout
|
||||
teamId={teamId}
|
||||
serviceId={serviceId}
|
||||
side="inside"
|
||||
activeKey={getActiveKey(pathname)}
|
||||
>
|
||||
{children}
|
||||
</ServiceDetailLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceOverviewPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function RestServiceOverviewPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceOverviewPage serviceType="restService" teamId={teamId} serviceId={serviceId} />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function RestServiceEntryPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
redirect(`/service/${teamId}/inside/${serviceId}/overview`)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceDetailLegacyTabs } from '../../../../_components/ServiceDetailLegacyTabs'
|
||||
|
||||
export default function RestServicePublishRoutePage() {
|
||||
return <ServiceDetailLegacyTabs side="inside" type="publish" />
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ServiceRouteListPage } from '../../../../_components/ServicePages'
|
||||
|
||||
export default async function RestServiceRouteListRoutePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ teamId: string; serviceId: string }>
|
||||
}) {
|
||||
const { teamId, serviceId } = await params
|
||||
return <ServiceRouteListPage teamId={teamId} serviceId={serviceId} side="inside" />
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { $t } from '@common/locales'
|
||||
import { SYSTEM_INSIDE_APPROVAL_TAB_ITEMS, SYSTEM_PUBLISH_TAB_ITEMS } from '@core/const/system/const'
|
||||
import AiServiceInsideApprovalList from '@core/pages/aiService/approval/AiServiceInsideApprovalList'
|
||||
import AiServiceInsidePublishList from '@core/pages/aiService/publish/AiServiceInsidePublishList'
|
||||
import SystemInsideApprovalList from '@core/pages/system/approval/SystemInsideApprovalList'
|
||||
import SystemInsidePublishList from '@core/pages/system/publish/SystemInsidePublishList'
|
||||
import { Tabs } from 'antd'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||
|
||||
type ServiceDetailLegacyTabsProps = {
|
||||
side: 'inside' | 'aiInside'
|
||||
type: 'approval' | 'publish'
|
||||
}
|
||||
|
||||
function buildTabHref(pathname: string, searchParams: URLSearchParams, key: string) {
|
||||
const nextSearchParams = new URLSearchParams(searchParams.toString())
|
||||
|
||||
if (key === '0') {
|
||||
nextSearchParams.delete('status')
|
||||
} else {
|
||||
nextSearchParams.set('status', key)
|
||||
}
|
||||
|
||||
const nextQuery = nextSearchParams.toString()
|
||||
return nextQuery ? `${pathname}?${nextQuery}` : pathname
|
||||
}
|
||||
|
||||
function LegacyRouteBridge({
|
||||
pathname,
|
||||
search,
|
||||
routeType,
|
||||
element
|
||||
}: {
|
||||
pathname: string
|
||||
search: string
|
||||
routeType: 'approval' | 'publish'
|
||||
element: ReactElement
|
||||
}) {
|
||||
const entry = `${pathname}${search ? `?${search}` : ''}`
|
||||
const routePath = `/service/:teamId/:side/:serviceId/${routeType}`
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={[entry]} key={entry}>
|
||||
<Routes>
|
||||
<Route path={routePath} element={element} />
|
||||
<Route path={`${routePath}/*`} element={element} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceDetailLegacyTabs({ side, type }: ServiceDetailLegacyTabsProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const { state } = useGlobalContext()
|
||||
const status = searchParams.get('status') || '0'
|
||||
const search = searchParams.toString()
|
||||
|
||||
const tabItems = useMemo(
|
||||
() =>
|
||||
(type === 'approval' ? SYSTEM_INSIDE_APPROVAL_TAB_ITEMS : SYSTEM_PUBLISH_TAB_ITEMS)?.map((item) => ({
|
||||
...item,
|
||||
label: typeof item?.label === 'string' ? $t(item.label) : item?.label
|
||||
})),
|
||||
[type, state.language]
|
||||
)
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (type === 'approval') {
|
||||
return side === 'aiInside' ? <AiServiceInsideApprovalList /> : <SystemInsideApprovalList />
|
||||
}
|
||||
|
||||
return side === 'aiInside' ? <AiServiceInsidePublishList /> : <SystemInsidePublishList />
|
||||
}, [side, type])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
activeKey={status}
|
||||
size="small"
|
||||
className="h-auto bg-MAIN_BG"
|
||||
tabBarStyle={{ paddingLeft: '10px' }}
|
||||
tabBarGutter={20}
|
||||
items={tabItems}
|
||||
destroyInactiveTabPane={true}
|
||||
onChange={(key) => {
|
||||
router.push(buildTabHref(pathname, new URLSearchParams(search), key))
|
||||
}}
|
||||
/>
|
||||
<LegacyRouteBridge pathname={pathname} search={search} routeType={type} element={content} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
'use client'
|
||||
|
||||
import PageList from '@common/components/aoplatform/PageList'
|
||||
import ServiceInfoCard from '@common/components/aoplatform/serviceInfoCard'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
|
||||
import { TimeRange } from '@common/components/aoplatform/TimeRangeSelector'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { SimpleMemberItem, SimpleTeamItem } from '@common/const/type'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { App as AppAntd, Card, Menu, MenuProps, Spin, Tag } from 'antd'
|
||||
import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const'
|
||||
import { AiServiceRouterTableListItem } from '@core/const/ai-service/type'
|
||||
import { SERVICE_KIND_OPTIONS, SYSTEM_API_TABLE_COLUMNS, SYSTEM_TABLE_COLUMNS } from '@core/const/system/const'
|
||||
import { SystemApiTableListItem, SystemTableListItem } from '@core/const/system/type'
|
||||
import RankingList from '@core/pages/serviceOverview/rankingList/RankingList'
|
||||
import ServiceAreaChart from '@core/pages/serviceOverview/charts/ServiceAreaChart'
|
||||
import ServiceBarChar, { BarChartInfo } from '@core/pages/serviceOverview/charts/ServiceBarChar'
|
||||
import DateSelectFilter, { TimeOption } from '@core/pages/serviceOverview/filter/DateSelectFilter'
|
||||
import { setBarChartInfoData } from '@core/pages/serviceOverview/utils'
|
||||
import { LogsFooter } from '@core/pages/system/serviceDeployment/ServiceDeployMentFooter'
|
||||
import { ServiceDeployment } from '@core/pages/system/serviceDeployment/ServiceDeployment'
|
||||
import {
|
||||
abbreviateFloat,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatNumberWithUnit,
|
||||
getTime
|
||||
} from '@dashboard/utils/dashboard'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export type ServiceSide = 'inside' | 'aiInside'
|
||||
export type ServiceMenuKey =
|
||||
| 'overview'
|
||||
| 'route'
|
||||
| 'api'
|
||||
| 'upstream'
|
||||
| 'document'
|
||||
| 'servicepolicy'
|
||||
| 'publish'
|
||||
| 'approval'
|
||||
| 'subscriber'
|
||||
| 'setting'
|
||||
| 'logs'
|
||||
|
||||
function ServiceOverviewIndicator({
|
||||
indicatorInfo,
|
||||
onNavigate
|
||||
}: {
|
||||
indicatorInfo: any
|
||||
onNavigate: (path: string) => void
|
||||
}) {
|
||||
const side = indicatorInfo?.serviceKind === 'ai' ? 'aiInside' : 'inside'
|
||||
const items = [
|
||||
{
|
||||
title: indicatorInfo?.enableMcp ? 'APIs / Tools' : 'APIs',
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/route`,
|
||||
content: indicatorInfo?.apiNum ?? 0
|
||||
},
|
||||
{
|
||||
title: $t('订阅数量'),
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/subscriber`,
|
||||
content: indicatorInfo?.subscriberNum ?? 0
|
||||
},
|
||||
{
|
||||
title: 'MCP',
|
||||
link: `/service/${indicatorInfo?.teamId}/${side}/${indicatorInfo?.serviceId}/setting`,
|
||||
content: indicatorInfo?.enableMcp ? $t('已开启') : $t('开启 MCP')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
{items.map((item, index) => (
|
||||
<Card
|
||||
key={item.title}
|
||||
className={`flex-1 rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[20px] px-[18px]' }}
|
||||
onClick={() => {
|
||||
if (item.link) {
|
||||
onNavigate(item.link)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-[14px] text-[#999999] mb-[10px]" style={{ fontFamily: 'Microsoft YaHei' }}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div
|
||||
className={`${index < 2 ? 'text-[32px] font-medium text-[#101010]' : 'text-[14px]'}`}
|
||||
style={{ fontFamily: 'Microsoft YaHei' }}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceOverviewPage({
|
||||
serviceType,
|
||||
teamId,
|
||||
serviceId
|
||||
}: {
|
||||
serviceType: 'aiService' | 'restService'
|
||||
teamId: string
|
||||
serviceId: string
|
||||
}) {
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = AppAntd.useApp()
|
||||
const { state } = useGlobalContext()
|
||||
const router = useRouter()
|
||||
const [dashboardLoading, setDashboardLoading] = useState(true)
|
||||
const [defaultTime] = useState<TimeOption>('day')
|
||||
const [timeRange, setTimeRange] = useState<TimeRange | undefined>()
|
||||
const [barChartInfo, setBarChartInfo] = useState<any>()
|
||||
const [perBarChartInfo, setPerBarChartInfo] = useState<any>()
|
||||
const [indicatorInfo, setIndicatorInfo] = useState<any>([])
|
||||
const [topRankingList, setTopRankingList] = useState<any>([])
|
||||
const [aiServiceOverview, setAiServiceOverview] = useState<any>()
|
||||
const [restServiceOverview, setRestServiceOverview] = useState<any>()
|
||||
|
||||
const selectCallback = (date: TimeRange) => {
|
||||
setTimeRange(date)
|
||||
}
|
||||
|
||||
const setRestChartInfo = (serviceOverview: any) => {
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceKind: serviceOverview.serviceKind,
|
||||
serviceId
|
||||
})
|
||||
setBarChartInfo([
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('请求次数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: formatNumberWithUnit(serviceOverview.requestTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
|
||||
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
|
||||
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('网络流量'),
|
||||
data: serviceOverview.trafficOverview,
|
||||
value: formatBytes(serviceOverview.trafficTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
traffic2xxTotal: formatBytes(serviceOverview.traffic2xxTotal),
|
||||
traffic4xxTotal: formatBytes(serviceOverview.traffic4xxTotal),
|
||||
traffic5xxTotal: formatBytes(serviceOverview.traffic5xxTotal)
|
||||
}
|
||||
])
|
||||
setPerBarChartInfo([
|
||||
{
|
||||
title: $t('平均响应时间'),
|
||||
data: serviceOverview.avgResponseTimeOverview,
|
||||
value: formatDuration(serviceOverview.avgResponseTime),
|
||||
originValue: serviceOverview.avgResponseTime,
|
||||
date: serviceOverview.date,
|
||||
max: formatDuration(serviceOverview.maxResponseTime),
|
||||
min: formatDuration(serviceOverview.minResponseTime),
|
||||
type: 'area',
|
||||
showXAxis: false
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的请求次数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
date: serviceOverview.date,
|
||||
showXAxis: false
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的网络流量'),
|
||||
data: serviceOverview.avgTrafficPerSubscriberOverview,
|
||||
date: serviceOverview.date,
|
||||
showXAxis: false
|
||||
}),
|
||||
max: formatBytes(serviceOverview.maxTrafficPerSubscriber),
|
||||
min: formatBytes(serviceOverview.minTrafficPerSubscriber)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const setAiChartInfo = (serviceOverview: any) => {
|
||||
setIndicatorInfo({
|
||||
apiNum: serviceOverview.apiNum,
|
||||
subscriberNum: serviceOverview.subscriberNum,
|
||||
teamId,
|
||||
enableMcp: serviceOverview.enableMcp,
|
||||
serviceKind: serviceOverview.serviceKind,
|
||||
serviceId
|
||||
})
|
||||
setBarChartInfo([
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('请求次数'),
|
||||
data: serviceOverview.requestOverview,
|
||||
value: formatNumberWithUnit(serviceOverview.requestTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
request2xxTotal: formatNumberWithUnit(serviceOverview.request2xxTotal),
|
||||
request4xxTotal: formatNumberWithUnit(serviceOverview.request4xxTotal),
|
||||
request5xxTotal: formatNumberWithUnit(serviceOverview.request5xxTotal)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('Token 消耗'),
|
||||
data: serviceOverview.tokenOverview.map((item: { inputToken: number; outputToken: number }) => ({
|
||||
inputToken: item.inputToken,
|
||||
outputToken: item.outputToken
|
||||
})),
|
||||
value: formatNumberWithUnit(serviceOverview.tokenTotal),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
inputTokenTotal: formatNumberWithUnit(serviceOverview.inputTokenTotal),
|
||||
outputTokenTotal: formatNumberWithUnit(serviceOverview.outputTokenTotal)
|
||||
}
|
||||
])
|
||||
setPerBarChartInfo([
|
||||
{
|
||||
title: $t('平均 Token 消耗'),
|
||||
data: serviceOverview.avgTokenOverview,
|
||||
value: `${formatNumberWithUnit(serviceOverview.avgToken)} Token/s`,
|
||||
originValue: serviceOverview.avgToken,
|
||||
date: serviceOverview.date,
|
||||
min: `${formatNumberWithUnit(serviceOverview.minToken)} Token/s`,
|
||||
max: `${formatNumberWithUnit(serviceOverview.maxToken)} Token/s`,
|
||||
type: 'area'
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的请求次数'),
|
||||
data: serviceOverview.avgRequestPerSubscriberOverview,
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxRequestPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minRequestPerSubscriber)
|
||||
},
|
||||
{
|
||||
...setBarChartInfoData({
|
||||
title: $t('平均每消费者的 Token 消耗'),
|
||||
data: serviceOverview.avgTokenPerSubscriberOverview.map((item: { inputToken: number; outputToken: number }) => ({
|
||||
inputToken: item.inputToken,
|
||||
outputToken: item.outputToken
|
||||
})),
|
||||
date: serviceOverview.date
|
||||
}),
|
||||
max: abbreviateFloat(serviceOverview.maxTokenPerSubscriber),
|
||||
min: abbreviateFloat(serviceOverview.minTokenPerSubscriber)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const getAIServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/ai', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'token_overview',
|
||||
'avg_token_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_token_per_subscriber_overview',
|
||||
'request_total',
|
||||
'token_total',
|
||||
'avg_token',
|
||||
'max_token',
|
||||
'min_token',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_token_per_subscriber',
|
||||
'input_token',
|
||||
'output_token',
|
||||
'total_token',
|
||||
'request_2xx_total',
|
||||
'request_4xx_total',
|
||||
'request_5xx_total',
|
||||
'input_token_total',
|
||||
'output_token_total',
|
||||
'max_token_per_subscriber',
|
||||
'min_token_per_subscriber',
|
||||
'max_request_per_subscriber',
|
||||
'min_request_per_subscriber'
|
||||
]
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setAiServiceOverview(data.overview)
|
||||
setAiChartInfo(data.overview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const getRestServiceOverview = () => {
|
||||
fetchData<BasicResponse<{ overview: any }>>('service/overview/monitor/rest', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end },
|
||||
eoTransformKeys: [
|
||||
'enable_mcp',
|
||||
'subscriber_num',
|
||||
'api_num',
|
||||
'service_kind',
|
||||
'avaliable_monitor',
|
||||
'request_overview',
|
||||
'traffic_overview',
|
||||
'avg_request_per_subscriber_overview',
|
||||
'avg_response_time_overview',
|
||||
'avg_traffic_per_subscriber_overview',
|
||||
'request_total',
|
||||
'traffic_total',
|
||||
'max_response_time',
|
||||
'min_response_time',
|
||||
'avg_response_time',
|
||||
'avg_request_per_subscriber',
|
||||
'avg_traffic_per_subscriber',
|
||||
'request_2xx_total',
|
||||
'request_4xx_total',
|
||||
'request_5xx_total',
|
||||
'traffic_2xx_total',
|
||||
'traffic_4xx_total',
|
||||
'traffic_5xx_total',
|
||||
'max_request_per_subscriber',
|
||||
'min_request_per_subscriber',
|
||||
'max_traffic_per_subscriber',
|
||||
'min_traffic_per_subscriber'
|
||||
]
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setRestServiceOverview(data.overview)
|
||||
setRestChartInfo(data.overview)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const getTopRankingList = () => {
|
||||
fetchData<BasicResponse<any>>('service/monitor/top10', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, start: timeRange?.start, end: timeRange?.end }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTopRankingList({
|
||||
'TOP API': data.apis,
|
||||
'TOP Consumer': data.consumers
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
setDashboardLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { startTime, endTime } = getTime(defaultTime, [])
|
||||
setTimeRange({ start: startTime, end: endTime })
|
||||
}, [defaultTime])
|
||||
|
||||
useEffect(() => {
|
||||
if (timeRange) {
|
||||
setDashboardLoading(true)
|
||||
if (serviceType === 'aiService') {
|
||||
getAIServiceOverview()
|
||||
} else {
|
||||
getRestServiceOverview()
|
||||
}
|
||||
getTopRankingList()
|
||||
}
|
||||
}, [timeRange])
|
||||
|
||||
useEffect(() => {
|
||||
if (serviceType === 'aiService') {
|
||||
if (aiServiceOverview) {
|
||||
setAiChartInfo(aiServiceOverview)
|
||||
}
|
||||
} else if (restServiceOverview) {
|
||||
setRestChartInfo(restServiceOverview)
|
||||
}
|
||||
}, [state.language])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full pb-[20px]"
|
||||
wrapperClassName="h-full min-h-[150px]"
|
||||
indicator={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ transform: 'scale(1.5)' }}>
|
||||
<LoadingOutlined style={{ fontSize: 30 }} spin />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
spinning={dashboardLoading}
|
||||
>
|
||||
<div className="mr-[30px]">
|
||||
<ServiceOverviewIndicator indicatorInfo={indicatorInfo} onNavigate={(path) => router.push(path)} />
|
||||
<div className="mt-[20px]">
|
||||
<DateSelectFilter selectCallback={selectCallback} defaultTime={defaultTime} />
|
||||
</div>
|
||||
<div className="mt-[20px] flex mb-[10px]">
|
||||
{barChartInfo?.map((item: BarChartInfo, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 min-w-[430px] rounded-[10px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[15px] px-[0px]' }}
|
||||
>
|
||||
<ServiceBarChar showLegendIndicator={true} height={400} dataInfo={item} customClassNames="flex-1" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex mb-[10px]">
|
||||
{perBarChartInfo?.map((item: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 rounded-[10px] min-w-[284px] ${index > 0 ? 'ml-[10px]' : ''}`}
|
||||
classNames={{ body: 'py-[15px] px-[0px]' }}
|
||||
>
|
||||
{item.type === 'area' ? (
|
||||
<ServiceAreaChart
|
||||
height={270}
|
||||
dataInfo={item}
|
||||
showAvgLine={true}
|
||||
customClassNames="flex-1 relative"
|
||||
/>
|
||||
) : (
|
||||
<ServiceBarChar height={270} dataInfo={item} hideIndicatorValue={true} customClassNames="flex-1" />
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<RankingList topRankingList={topRankingList} serviceType={serviceType} />
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceRouteListPage({
|
||||
teamId,
|
||||
serviceId,
|
||||
side
|
||||
}: {
|
||||
teamId: string
|
||||
serviceId: string
|
||||
side: ServiceSide
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { fetchData } = useFetch()
|
||||
const { modal, message } = AppAntd.useApp()
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const [searchWord, setSearchWord] = useState('')
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const [tableListDataSource, setTableListDataSource] = useState<Array<SystemApiTableListItem | AiServiceRouterTableListItem>>([])
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const isAiService = side === 'aiInside'
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true)
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const getMemberList = async () => {
|
||||
setMemberValueEnum([])
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
|
||||
method: 'GET'
|
||||
})
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setMemberValueEnum(data.members)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId, side])
|
||||
|
||||
const getRoutesList = (): Promise<{ data: Array<SystemApiTableListItem | AiServiceRouterTableListItem>; success: boolean }> => {
|
||||
if (!tableHttpReload) {
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({ data: tableListDataSource, success: true })
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<any>>(isAiService ? 'service/ai-routers' : 'service/routers', {
|
||||
method: 'GET',
|
||||
eoParams: { service: serviceId, team: teamId, keyword: searchWord },
|
||||
eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const items = isAiService ? data.apis : data.routers
|
||||
setTableListDataSource(items)
|
||||
setTableHttpReload(false)
|
||||
return { data: items, success: true }
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
})
|
||||
.catch(() => ({ data: [], success: false }))
|
||||
}
|
||||
|
||||
const deleteRoute = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetchData<BasicResponse<null>>(isAiService ? 'service/ai-router' : 'service/router', {
|
||||
method: 'DELETE',
|
||||
eoParams: { service: serviceId, team: teamId, router: entity.id }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
}
|
||||
|
||||
const openDeleteModal = (entity: SystemApiTableListItem | AiServiceRouterTableListItem) => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
content: $t('确认删除该数据?'),
|
||||
onOk: () =>
|
||||
deleteRoute(entity).then((res) => {
|
||||
if (res === true) {
|
||||
manualReloadTable()
|
||||
}
|
||||
}),
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const routeColumns = useMemo(() => {
|
||||
const baseColumns = (isAiService ? AI_SERVICE_ROUTER_TABLE_COLUMNS : SYSTEM_API_TABLE_COLUMNS).map((column) => {
|
||||
const nextColumn = { ...column }
|
||||
const dataIndex = nextColumn.dataIndex as string[] | string | undefined
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('creator')) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
memberValueEnum.forEach((item) => {
|
||||
valueEnum[item.name] = { text: item.name }
|
||||
})
|
||||
nextColumn.valueEnum = valueEnum
|
||||
}
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && (dataIndex.includes('disable') || dataIndex.includes('disabled'))) {
|
||||
nextColumn.valueEnum = {
|
||||
true: { text: <span className="text-red-500">{$t('拦截')}</span> },
|
||||
false: { text: <span className="text-green-500">{$t('放行')}</span> }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...nextColumn,
|
||||
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
...baseColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'option',
|
||||
btnNums: 2,
|
||||
fixed: 'right' as const,
|
||||
valueType: 'option' as const,
|
||||
render: (_: ReactNode, entity: SystemApiTableListItem | AiServiceRouterTableListItem) => [
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.edit"
|
||||
key="edit"
|
||||
btnType="edit"
|
||||
onClick={() => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/${entity.id}`)
|
||||
}}
|
||||
btnTitle="编辑"
|
||||
/>,
|
||||
<TableBtnWithPermission
|
||||
access="team.service.router.delete"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => {
|
||||
openDeleteModal(entity)
|
||||
}}
|
||||
btnTitle="删除"
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
}, [isAiService, memberValueEnum, state.language, router, teamId, side, serviceId])
|
||||
|
||||
return (
|
||||
<PageList
|
||||
id={`service_route_${side}`}
|
||||
ref={pageListRef}
|
||||
columns={routeColumns as any}
|
||||
request={() => getRoutesList()}
|
||||
dataSource={tableListDataSource}
|
||||
addNewBtnTitle={$t('添加路由')}
|
||||
searchPlaceholder={$t('输入 URL 查找路由')}
|
||||
onAddNewBtnClick={() => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/create`)
|
||||
}}
|
||||
addNewBtnAccess="team.service.router.add"
|
||||
tableClickAccess="team.service.router.view"
|
||||
manualReloadTable={manualReloadTable}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onRowClick={(row: SystemApiTableListItem | AiServiceRouterTableListItem) =>
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/route/${row.id}`)
|
||||
}
|
||||
tableClass="mr-PAGE_INSIDE_X"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceListPage() {
|
||||
const router = useRouter()
|
||||
const { message, modal } = AppAntd.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { checkPermission, accessInit, getGlobalAccessData, state } = useGlobalContext()
|
||||
const [tableSearchWord, setTableSearchWord] = useState('')
|
||||
const [teamList, setTeamList] = useState<{ [k: string]: { text: string } }>()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<SystemTableListItem[]>([])
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({})
|
||||
const [stateColumnMap] = useState<{ [k: string]: { text: string; className?: string } }>({
|
||||
normal: { text: '正常' },
|
||||
deploying: { text: '部署中', className: 'text-[#2196f3]' },
|
||||
error: { text: '异常', className: 'text-[#ff4d4f]' },
|
||||
public: { text: '公共服务' },
|
||||
private: { text: '私有服务' }
|
||||
})
|
||||
|
||||
const getSystemList = () => {
|
||||
if (!accessInit) {
|
||||
getGlobalAccessData()?.then?.(() => {
|
||||
getSystemList()
|
||||
})
|
||||
return Promise.resolve({ data: [], success: false })
|
||||
}
|
||||
|
||||
if (!tableHttpReload) {
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({ data: tableListDataSource, success: true })
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<{ services: SystemTableListItem[] }>>(
|
||||
!checkPermission('system.workspace.service.view_all') ? 'my_services' : 'services',
|
||||
{
|
||||
method: 'GET',
|
||||
eoParams: { keyword: tableSearchWord },
|
||||
eoTransformKeys: ['api_num', 'service_num', 'create_time']
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTableListDataSource(data.services)
|
||||
setTableHttpReload(false)
|
||||
return { data: data.services, success: true }
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
})
|
||||
.catch(() => ({ data: [], success: false }))
|
||||
}
|
||||
|
||||
const getTeamsList = () => {
|
||||
if (!accessInit) {
|
||||
getGlobalAccessData()?.then?.(() => {
|
||||
getTeamsList()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
|
||||
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
|
||||
{ method: 'GET', eoTransformKeys: [] }
|
||||
).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
data.teams?.forEach((x: SimpleMemberItem) => {
|
||||
valueEnum[x.name] = { text: x.name }
|
||||
})
|
||||
setTeamList(valueEnum)
|
||||
return
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
const getMemberList = async () => {
|
||||
setMemberValueEnum({})
|
||||
const { code, data, msg } = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member', {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const valueEnum: Record<string, { text: string }> = {}
|
||||
data.members?.forEach((x: SimpleMemberItem) => {
|
||||
valueEnum[x.name] = { text: x.name }
|
||||
})
|
||||
setMemberValueEnum(valueEnum)
|
||||
return
|
||||
}
|
||||
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true)
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
|
||||
const openLogsModal = (record: SystemTableListItem) => {
|
||||
const closeModal = (reload = true) => {
|
||||
modalInstance.destroy()
|
||||
if (reload) {
|
||||
manualReloadTable()
|
||||
}
|
||||
}
|
||||
|
||||
const updateFooter = () => {
|
||||
record.state = 'error'
|
||||
modalInstance.update({})
|
||||
}
|
||||
|
||||
let cancelCb: () => void = () => {}
|
||||
const cancel = (cb: () => void) => {
|
||||
cancelCb = cb
|
||||
}
|
||||
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署过程'),
|
||||
content: <ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />,
|
||||
footer: () => <LogsFooter record={record} closeModal={closeModal} />,
|
||||
afterClose: () => {
|
||||
cancelCb()
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTeamsList()
|
||||
getMemberList()
|
||||
}, [])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return SYSTEM_TABLE_COLUMNS.map((column) => {
|
||||
const nextColumn = { ...column }
|
||||
const dataIndex = nextColumn.dataIndex as string | string[] | undefined
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('master')) {
|
||||
nextColumn.valueEnum = memberValueEnum
|
||||
}
|
||||
|
||||
if (nextColumn.filters && Array.isArray(dataIndex) && dataIndex.includes('team')) {
|
||||
nextColumn.valueEnum = teamList
|
||||
}
|
||||
|
||||
if (nextColumn.dataIndex === 'service_kind') {
|
||||
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem & { enable_mcp?: boolean }) => (
|
||||
<span className="text-[13px]">
|
||||
<Tag
|
||||
color={`#${record.service_kind === 'ai' ? 'EADEFF' : 'DEFFE7'}`}
|
||||
className="text-[#000] font-normal border-0 mr-[10px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title={record.service_kind || '-'}
|
||||
>
|
||||
{SERVICE_KIND_OPTIONS.find((item) => item.value === record.service_kind)?.label || '-'}
|
||||
</Tag>
|
||||
{record.enable_mcp && (
|
||||
<Tag
|
||||
color="#FFF0C1"
|
||||
className="text-[#000] font-normal border-0 mr-[12px] max-w-[150px] truncate"
|
||||
bordered={false}
|
||||
title="MCP"
|
||||
>
|
||||
MCP
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (nextColumn.dataIndex === 'state') {
|
||||
nextColumn.render = (_dom: ReactNode, record: SystemTableListItem) => (
|
||||
<span
|
||||
className={`text-[13px] ${stateColumnMap[record.state]?.className || ''}`}
|
||||
onClick={(event) => {
|
||||
if (['deploying', 'error'].includes(record.state)) {
|
||||
event.stopPropagation()
|
||||
openLogsModal(record)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t(stateColumnMap[record.state]?.text || '-')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...nextColumn,
|
||||
title: typeof nextColumn.title === 'string' ? $t(nextColumn.title) : nextColumn.title
|
||||
}
|
||||
})
|
||||
}, [memberValueEnum, teamList, state.language])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
<div className="border-[0px] mr-PAGE_INSIDE_X mb-[30px]">
|
||||
<div className="flex justify-between mb-[20px] items-center">
|
||||
<div className="flex items-center gap-TAG_LEFT">
|
||||
<div className="text-theme text-[26px]">{$t('服务')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{$t(
|
||||
'服务提供了高性能 API 网关,并且可以无缝接入多种大型 AI 模型,并将这些 AI 能力打包成 API 进行调用,从而大幅简化了 AI 模型的使用门槛。同时,我们的平台提供了完善的 API 管理功能,支持 API 的创建、监控、访问控制等,保障开发者可以高效、安全地开发和管理 API 服务。'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B overflow-hidden">
|
||||
<PageList
|
||||
id="global_system"
|
||||
ref={pageListRef}
|
||||
columns={columns}
|
||||
request={() => getSystemList()}
|
||||
searchPlaceholder={$t('输入名称、ID、所属团队、负责人查找服务')}
|
||||
manualReloadTable={manualReloadTable}
|
||||
onChange={() => {
|
||||
setTableHttpReload(false)
|
||||
}}
|
||||
onSearchWordChange={(event) => {
|
||||
setTableSearchWord(event.target.value)
|
||||
}}
|
||||
onRowClick={(row: SystemTableListItem) => {
|
||||
router.push(`/service/${row.team.id}/${row.service_kind === 'ai' ? 'aiInside' : 'inside'}/${row.id}/overview`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceDetailLayout({
|
||||
teamId,
|
||||
serviceId,
|
||||
side,
|
||||
activeKey,
|
||||
children
|
||||
}: {
|
||||
teamId: string
|
||||
serviceId: string
|
||||
side: ServiceSide
|
||||
activeKey: ServiceMenuKey
|
||||
children: ReactNode
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { state, checkPermission } = useGlobalContext()
|
||||
|
||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||
const items: Array<{ key: ServiceMenuKey; label: string; access?: string }> = side === 'aiInside'
|
||||
? [
|
||||
{ key: 'overview', label: $t('总览') },
|
||||
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
|
||||
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
|
||||
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
|
||||
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
|
||||
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
|
||||
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
|
||||
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
|
||||
{ key: 'setting', label: $t('设置') },
|
||||
{ key: 'logs', label: $t('日志') }
|
||||
]
|
||||
: [
|
||||
{ key: 'overview', label: $t('总览') },
|
||||
{ key: 'route', label: $t('API 路由'), access: 'team.service.router.view' },
|
||||
{ key: 'api', label: $t('API 文档'), access: 'team.service.api_doc.view' },
|
||||
{ key: 'upstream', label: $t('上游'), access: 'team.service.upstream.view' },
|
||||
{ key: 'document', label: $t('使用说明'), access: 'team.service.service_intro.view' },
|
||||
{ key: 'servicepolicy', label: $t('服务策略'), access: 'team.service.policy.view' },
|
||||
{ key: 'publish', label: $t('发布'), access: 'team.service.release.view' },
|
||||
{ key: 'approval', label: $t('订阅审核'), access: 'team.service.subscription.view' },
|
||||
{ key: 'subscriber', label: $t('订阅方管理'), access: 'team.service.subscription.view' },
|
||||
{ key: 'setting', label: $t('设置') },
|
||||
{ key: 'logs', label: $t('日志') }
|
||||
]
|
||||
|
||||
return items
|
||||
.filter((item) => (item.access ? checkPermission(item.access as any) : true))
|
||||
.map((item) => ({
|
||||
key: item.key,
|
||||
label: item.label
|
||||
}))
|
||||
}, [side, state.language, checkPermission])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
<div className="mr-PAGE_INSIDE_X mb-[20px]">
|
||||
<ServiceInfoCard serviceId={serviceId} teamId={teamId} />
|
||||
</div>
|
||||
<div className="flex flex-1 h-full overflow-hidden">
|
||||
<Menu
|
||||
className="overflow-y-auto h-full"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeKey]}
|
||||
mode="inline"
|
||||
items={menuItems}
|
||||
onClick={({ key }) => {
|
||||
router.push(`/service/${teamId}/${side}/${serviceId}/${key}`)
|
||||
}}
|
||||
/>
|
||||
<div className="w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceListPage } from '../../_components/ServicePages'
|
||||
|
||||
export default function TeamServiceListRoutePage() {
|
||||
return <ServiceListPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ServiceListPage } from '../_components/ServicePages'
|
||||
|
||||
export default function ServiceListRoutePage() {
|
||||
return <ServiceListPage />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function ServiceRootPage() {
|
||||
redirect('/service/list')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@/app/_legacy/LegacyAppPage";
|
||||
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export default function FrontendLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0b1020] text-white">
|
||||
<header className="border-b border-white/10 bg-[#0b1020]/90 backdrop-blur">
|
||||
<div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
|
||||
<Link href="/" className="text-lg font-semibold tracking-wide text-white">
|
||||
APIPark
|
||||
</Link>
|
||||
<nav className="flex items-center gap-3 text-sm">
|
||||
<Link href="/admin/login" className="rounded-full border border-white/15 px-4 py-2 text-white/85 hover:text-white">
|
||||
后台登录
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.apipark.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-full bg-[#3d46f2] px-4 py-2 font-medium text-white hover:bg-[#5860ff]"
|
||||
>
|
||||
产品文档
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const McpPage = () => {
|
||||
return <div>MCP</div>
|
||||
}
|
||||
|
||||
export default McpPage
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user