Compare commits
11 Commits
b7cfbe3622
...
main
Author | SHA1 | Date | |
---|---|---|---|
bef3d318dd | |||
1c7c5f388d | |||
fc37a99cf8 | |||
3c5e6aa41e | |||
9fc1f29c89 | |||
76bacdb830 | |||
645618d505 | |||
85fd48f403 | |||
1b4bad6d2e | |||
24028c35a5 | |||
47c50a52e3 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,6 +1,3 @@
|
|||||||
[submodule "submodules/query-login"]
|
|
||||||
path = submodules/query-login
|
|
||||||
url = git@git.xiongxiao.me:kevisual/kevsiual-query-login.git
|
|
||||||
[submodule "submodules/query-config"]
|
[submodule "submodules/query-config"]
|
||||||
path = submodules/query-config
|
path = submodules/query-config
|
||||||
url = git@git.xiongxiao.me:kevisual/kevsiual-query-config.git
|
url = git@git.xiongxiao.me:kevisual/kevsiual-query-config.git
|
||||||
|
25
index.html
25
index.html
@@ -1,13 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta charset="UTF-8" />
|
||||||
<title>Center</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://kevisual.xiongxiao.me/system/lib/app.js"></script>
|
<title>Center</title>
|
||||||
</head>
|
<script src="https://kevisual.xiongxiao.me/root/system-lib/app.js"></script>
|
||||||
<body>
|
</head>
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<body>
|
||||||
</body>
|
<div id="root"></div>
|
||||||
</html>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
25
kevisual.json
Normal file
25
kevisual.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://kevisual.xiongxiao.me/root/ai/kevisual/tools/kevisual-sync/schema.json?v=2",
|
||||||
|
"metadata": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"checkDir": {
|
||||||
|
"src/query": {
|
||||||
|
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"syncDirectory": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/query/**/*"
|
||||||
|
],
|
||||||
|
"ignore": [],
|
||||||
|
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
|
||||||
|
"replace": {
|
||||||
|
"src/": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sync": {}
|
||||||
|
}
|
74
package.json
74
package.json
@@ -1,93 +1,95 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/center",
|
"name": "@kevisual/center",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.10",
|
"version": "0.0.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"build:sky": "cross-env CENTER_ENV=sky vite build",
|
||||||
"dev:kv": "cross-env VITE_USE_KV=true vite",
|
"dev:kv": "cross-env VITE_USE_KV=true vite",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"pub": "envision deploy ./dist -k center -v 0.0.10 -u -o root",
|
"pub": "envision deploy ./dist -k center -v 0.0.11 -u -o root",
|
||||||
"turbo:dev": "turbo dev:lib",
|
"turbo:dev": "turbo dev:lib",
|
||||||
"dev:lib": "turbo dev:lib",
|
"dev:lib": "turbo dev:lib",
|
||||||
"dev:query": "turbo dev:lib --filter=@kevisual/query",
|
"dev:query": "turbo dev:lib --filter=@kevisual/query",
|
||||||
"turbo:build": "turbo build --filter=submodules/**",
|
"turbo:build": "turbo build --filter=submodules/**",
|
||||||
"build:lib": "turbo build --filter=@kevisual/query**"
|
"build:lib": "turbo build --filter=@kevisual/query**",
|
||||||
|
"vi": "pnpm dlx vite-bundle-visualizer -o ./dist/stats.html"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@kevisual/cache": "^0.0.1",
|
"@kevisual/cache": "^0.0.3",
|
||||||
"@kevisual/codemirror": "workspace:*",
|
"@kevisual/codemirror": "workspace:*",
|
||||||
"@kevisual/components": "workspace:*",
|
"@kevisual/components": "workspace:*",
|
||||||
"@kevisual/container": "1.0.0",
|
"@kevisual/container": "1.0.0",
|
||||||
"@kevisual/query": "^0.0.15",
|
"@kevisual/query": "^0.0.29",
|
||||||
"@kevisual/query-config": "workspace:*",
|
"@kevisual/query-config": "workspace:*",
|
||||||
"@kevisual/query-login": "workspace:*",
|
|
||||||
"@kevisual/query-upload": "workspace:*",
|
"@kevisual/query-upload": "workspace:*",
|
||||||
"@kevisual/resources": "workspace:*",
|
"@kevisual/resources": "workspace:*",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.1.1",
|
||||||
"@stackblitz/sdk": "^1.11.0",
|
"@stackblitz/sdk": "^1.11.0",
|
||||||
"@tailwindcss/vite": "^4.1.1",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"@uiw/react-textarea-code-editor": "^3.1.0",
|
"@uiw/react-textarea-code-editor": "^3.1.1",
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.26.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"i18next": "^24.2.3",
|
"i18next": "^25.2.1",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.12",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.5.3",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^3.0.3",
|
||||||
"react-router": "^7.4.1",
|
"react-router": "^7.6.2",
|
||||||
"react-router-dom": "^7.4.1",
|
"react-router-dom": "^7.6.2",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"vite-plugin-tsconfig-paths": "^1.4.1",
|
"vite-plugin-tsconfig-paths": "^1.4.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.29.0",
|
||||||
|
"@kevisual/ssl": "^0.0.1",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^24.0.3",
|
||||||
"@types/path-browserify": "^1.0.3",
|
"@types/path-browserify": "^1.0.3",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.1",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.2.0",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.516.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^7.0.0",
|
||||||
"react-is": "19.1.0",
|
"react-is": "19.1.0",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.10",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"turbo": "^2.4.4",
|
"turbo": "^2.5.4",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.29.0",
|
"typescript-eslint": "^8.34.1",
|
||||||
"vite": "^6.2.5"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.7.1"
|
"packageManager": "pnpm@10.12.1"
|
||||||
}
|
}
|
Submodule packages/components updated: 967c2f94f2...59d53bb1e6
Submodule packages/kevisual-official updated: 313cba38de...59edfd8105
@@ -21,26 +21,26 @@
|
|||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@kevisual/components": "workspace:*",
|
"@kevisual/components": "workspace:*",
|
||||||
"@kevisual/query-upload": "workspace:*",
|
"@kevisual/query-upload": "workspace:*",
|
||||||
"@kevisual/router": "^0.0.9",
|
"@kevisual/router": "^0.0.22",
|
||||||
"@kevisual/store": "^0.0.2",
|
"@kevisual/store": "^0.0.9",
|
||||||
"@mui/material": "^6.4.8",
|
"@mui/material": "^7.1.1",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.482.0",
|
"lucide-react": "^0.516.0",
|
||||||
"nanoid": "^5.1.4",
|
"nanoid": "^5.1.5",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^7.0.0",
|
||||||
"react": "19.0.0",
|
"react": "19.1.0",
|
||||||
"react-datepicker": "^8.2.1",
|
"react-datepicker": "^8.4.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/types": "^0.0.6",
|
"@kevisual/types": "^0.0.10",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/nprogress": "^0.2.3"
|
"@types/nprogress": "^0.2.3"
|
||||||
},
|
},
|
||||||
|
@@ -3,7 +3,7 @@ import 'nprogress/nprogress.css';
|
|||||||
import { Id, toast } from 'react-toastify';
|
import { Id, toast } from 'react-toastify';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
|
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
|
||||||
import { uploadFiles, UploadProgress } from '@kevisual/query-upload/query-upload';
|
import { uploadFiles, UploadProgress } from '@kevisual/query-upload/query-upload.js';
|
||||||
|
|
||||||
export type ConvertOpts = {
|
export type ConvertOpts = {
|
||||||
appKey?: string;
|
appKey?: string;
|
||||||
|
4529
pnpm-lock.yaml
generated
4529
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- 'packages/*'
|
- packages/*
|
||||||
- '!packages/webshell/webshell-node'
|
- '!packages/webshell/webshell-node'
|
||||||
- 'submodules/*'
|
- submodules/*
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@tailwindcss/oxide'
|
||||||
|
BIN
public/panda.png
Normal file
BIN
public/panda.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
42
src/App.tsx
42
src/App.tsx
@@ -1,26 +1,36 @@
|
|||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||||
import { App as ContainerApp } from './pages/container';
|
// import { App as ContainerApp } from './pages/container';
|
||||||
import { App as MapApp } from './pages/map';
|
|
||||||
import { App as UserApp } from './pages/user';
|
|
||||||
import { App as UserAppApp } from './pages/app';
|
import { App as UserAppApp } from './pages/app';
|
||||||
import { App as FileApp } from './pages/file';
|
|
||||||
import { App as OrgApp } from './pages/org';
|
|
||||||
import { App as ConfigApp } from './pages/config';
|
|
||||||
import { App as PayApp } from './pages/pay';
|
|
||||||
import { App as DomainApp } from './pages/domain';
|
|
||||||
import { App as HomeApp } from './pages/home';
|
import { App as HomeApp } from './pages/home';
|
||||||
|
|
||||||
|
// import { App as MapApp } from './pages/map';
|
||||||
|
// import { App as UserApp } from './pages/user';
|
||||||
|
// import { App as FileApp } from './pages/file';
|
||||||
|
// import { App as OrgApp } from './pages/org';
|
||||||
|
// import { App as ConfigApp } from './pages/config';
|
||||||
|
// import { App as DomainApp } from './pages/domain';
|
||||||
|
|
||||||
import { basename } from './modules/basename';
|
import { basename } from './modules/basename';
|
||||||
import { Redirect } from './modules/Redirect';
|
import { Redirect } from './modules/Redirect';
|
||||||
import { CustomThemeProvider, useTheme, themeOptions, theme } from '@kevisual/components/theme/index.tsx';
|
import { useTheme, theme } from '@kevisual/components/theme/index.tsx';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
import 'dayjs/locale/en';
|
import 'dayjs/locale/en';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import enUS from 'antd/locale/en_US';
|
import enUS from 'antd/locale/en_US';
|
||||||
import ConfigProvider from 'antd/es/config-provider';
|
import ConfigProvider from 'antd/es/config-provider';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ThemeProvider, createTheme } from '@mui/material';
|
import { ThemeProvider } from '@mui/material';
|
||||||
|
const ContainerApp = lazy(() => import('./pages/container'));
|
||||||
|
const MapApp = lazy(() => import('./pages/map'));
|
||||||
|
const UserApp = lazy(() => import('./pages/user'));
|
||||||
|
const FileApp = lazy(() => import('./pages/file'));
|
||||||
|
const OrgApp = lazy(() => import('./pages/org'));
|
||||||
|
const ConfigApp = lazy(() => import('./pages/config'));
|
||||||
|
const DomainApp = lazy(() => import('./pages/domain'));
|
||||||
|
|
||||||
export const CustomThemeProvider2 = ({ children }: { children: React.ReactNode }) => {
|
export const CustomThemeProvider2 = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||||
};
|
};
|
||||||
@@ -75,7 +85,14 @@ export const App = () => {
|
|||||||
<Router basename={basename}>
|
<Router basename={basename}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Redirect to='/app/' />} />
|
<Route path='/' element={<Redirect to='/app/' />} />
|
||||||
<Route path='/container/*' element={<ContainerApp />} />
|
<Route
|
||||||
|
path='/container/*'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={'loading container'}>
|
||||||
|
<ContainerApp />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path='/map/*' element={<MapApp />} />
|
<Route path='/map/*' element={<MapApp />} />
|
||||||
<Route path='/user-center/*' element={<UserApp />} />
|
<Route path='/user-center/*' element={<UserApp />} />
|
||||||
<Route path='/user/*' element={<UserApp />} />
|
<Route path='/user/*' element={<UserApp />} />
|
||||||
@@ -83,7 +100,6 @@ export const App = () => {
|
|||||||
<Route path='/config/*' element={<ConfigApp />} />
|
<Route path='/config/*' element={<ConfigApp />} />
|
||||||
<Route path='/app/*' element={<UserAppApp />} />
|
<Route path='/app/*' element={<UserAppApp />} />
|
||||||
<Route path='/file/*' element={<FileApp />} />
|
<Route path='/file/*' element={<FileApp />} />
|
||||||
<Route path='/pay/*' element={<PayApp />} />
|
|
||||||
<Route path='/domain/*' element={<DomainApp />} />
|
<Route path='/domain/*' element={<DomainApp />} />
|
||||||
<Route path='/home/*' element={<HomeApp />} />
|
<Route path='/home/*' element={<HomeApp />} />
|
||||||
<Route path='/404' element={<div>404</div>} />
|
<Route path='/404' element={<div>404</div>} />
|
||||||
|
@@ -1,3 +1,45 @@
|
|||||||
@import "./D-DIN/font.css";
|
@import 'tailwindcss';
|
||||||
@import "./Montserrat/font.css";
|
|
||||||
@import "./Orbitron/font.css";
|
@import './D-DIN/font.css';
|
||||||
|
@import './Montserrat/font.css';
|
||||||
|
@import './Orbitron/font.css';
|
||||||
|
|
||||||
|
/* D-DIN */
|
||||||
|
@utility font-d-din {
|
||||||
|
font-family: 'D-DIN', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-bold {
|
||||||
|
font-family: 'D-DIN-Bold', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-italic {
|
||||||
|
font-family: 'D-DIN-Italic', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-condensed {
|
||||||
|
font-family: 'D-DIN Condensed', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-condensed-bold {
|
||||||
|
font-family: 'D-DIN Condensed-Bold', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-exp {
|
||||||
|
font-family: 'D-DIN Exp', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-exp-italic {
|
||||||
|
font-family: 'D-DIN Exp-Italic', sans-serif;
|
||||||
|
}
|
||||||
|
@utility font-d-din-exp-bold {
|
||||||
|
font-family: 'D-DIN Exp-Bold', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Montserrat */
|
||||||
|
@utility font-montserrat {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility font-montserrat-italic {
|
||||||
|
font-family: 'Montserrat-Italic', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Orbitron */
|
||||||
|
@utility font-orbitron {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
4
src/assets/index.tsx
Normal file
4
src/assets/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import PandaPNG from '@/assets/panda.jpg';
|
||||||
|
// import PandaPNG from '@/assets/panda.png';
|
||||||
|
|
||||||
|
export { PandaPNG };
|
BIN
src/assets/panda-320*320.jpg
Normal file
BIN
src/assets/panda-320*320.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/panda.jpg
Normal file
BIN
src/assets/panda.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
@@ -1,7 +1,6 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { App } from './App.tsx';
|
import { App } from './App.tsx';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import { basename } from './modules/basename.ts';
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { I18NextProvider } from '@kevisual/components/translate/I18Next.tsx';
|
import { I18NextProvider } from '@kevisual/components/translate/I18Next.tsx';
|
||||||
|
|
||||||
|
@@ -1 +1,4 @@
|
|||||||
export const basename = DEV_SERVER ? '' : '/root/center';
|
export const basename = DEV_SERVER ? '' : '/root/center';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const isSky = CENTER_ENV === 'sky';
|
||||||
|
@@ -12,7 +12,6 @@ import { useNewNavigate } from '../navicate';
|
|||||||
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
|
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useQuickMenu } from './Menu';
|
|
||||||
|
|
||||||
export const LayoutUser = () => {
|
export const LayoutUser = () => {
|
||||||
const { open, setOpen, isAdmin, ...store } = useLayoutStore(
|
const { open, setOpen, isAdmin, ...store } = useLayoutStore(
|
||||||
|
@@ -16,6 +16,11 @@ import { Map } from 'lucide-react';
|
|||||||
export const useQuickMenu = () => {
|
export const useQuickMenu = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
title: t('Home'),
|
||||||
|
icon: <HomeOutlined />,
|
||||||
|
link: '/home',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('User App'),
|
title: t('User App'),
|
||||||
icon: <AppstoreOutlined />,
|
icon: <AppstoreOutlined />,
|
||||||
|
@@ -6,7 +6,7 @@ import { useLayoutStore, usePlatformStore } from './store';
|
|||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { LayoutUser } from './LayoutUser';
|
import { LayoutUser } from './LayoutUser';
|
||||||
import PandaPNG from '@/assets/panda.png';
|
import { PandaPNG } from '@/assets/index.tsx';
|
||||||
import QRCodePNG from '@/assets/qrcode-8x8.jpg';
|
import QRCodePNG from '@/assets/qrcode-8x8.jpg';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -18,6 +18,7 @@ import { Languages, QrCode } from 'lucide-react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useTheme } from '@kevisual/components/theme/index.tsx';
|
import { useTheme } from '@kevisual/components/theme/index.tsx';
|
||||||
|
import { isSky } from '../basename';
|
||||||
|
|
||||||
type LayoutMainProps = {
|
type LayoutMainProps = {
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
@@ -92,7 +93,7 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<div className='flex grow justify-between pl-4 items-center'>
|
<div className='flex grow justify-between pl-4 items-center'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<div className='text-xl font-bold'>{props.title}</div>
|
<div className='text-xl font-bold min-w-[140px]'>{props.title}</div>
|
||||||
<div className='ml-4 flex items-center gap-2 text-sm '>
|
<div className='ml-4 flex items-center gap-2 text-sm '>
|
||||||
{quickMenu.map((item, index) => {
|
{quickMenu.map((item, index) => {
|
||||||
const isActive = location.pathname.includes(item.link);
|
const isActive = location.pathname.includes(item.link);
|
||||||
@@ -112,15 +113,18 @@ export const LayoutMain = (props: LayoutMainProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mr-4 flex gap-4 items-center no-drag'>
|
<div className='mr-4 flex gap-4 items-center no-drag'>
|
||||||
<div className='group relative'>
|
{!isSky && (
|
||||||
<IconButton>
|
<div className='group relative'>
|
||||||
<QrCode size={16} />
|
<IconButton>
|
||||||
</IconButton>
|
<QrCode size={16} />
|
||||||
<div className='absolute hidden group-hover:flex bg-white p-2 border shadow-md top-10 -left-15 w-40 z-[9999] flex-col items-center justify-center rounded-md'>
|
</IconButton>
|
||||||
<img src={QRCodePNG} alt='QR Code' />
|
|
||||||
<div className='text-sm text-black'>逸闻设计</div>
|
<div className='absolute hidden group-hover:flex bg-white p-2 border shadow-md top-10 -left-15 w-40 z-[9999] flex-col items-center justify-center rounded-md'>
|
||||||
|
<img src={QRCodePNG} alt='QR Code' />
|
||||||
|
<div className='text-sm text-black'>逸闻设计</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title={currentLanguage === 'en' ? 'English' : 'Chinese'}>
|
<Tooltip title={currentLanguage === 'en' ? 'English' : 'Chinese'}>
|
||||||
<IconButton onClick={handleClick} variant='contained'>
|
<IconButton onClick={handleClick} variant='contained'>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { QueryClient } from '@kevisual/query';
|
import { QueryClient } from '@kevisual/query';
|
||||||
import { QueryLoginBrowser } from '@kevisual/query-login';
|
import { QueryLoginBrowser } from '@/query/query-login/query-login-browser';
|
||||||
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin.tsx';
|
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin.tsx';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
export const query = new QueryClient({
|
export const query = new QueryClient({
|
||||||
@@ -11,16 +11,6 @@ export const query = new QueryClient({
|
|||||||
export const queryLogin = new QueryLoginBrowser({
|
export const queryLogin = new QueryLoginBrowser({
|
||||||
query: query as any,
|
query: query as any,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
if (window.context) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.context.queryLogin = queryLogin;
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
window.context = {
|
|
||||||
queryLogin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
query.afterResponse = async (res, ctx) => {
|
query.afterResponse = async (res, ctx) => {
|
||||||
const newRes = await queryLogin.run401Action(res, ctx, {
|
const newRes = await queryLogin.run401Action(res, ctx, {
|
||||||
afterAlso401: () => {
|
afterAlso401: () => {
|
||||||
@@ -32,11 +22,10 @@ query.afterResponse = async (res, ctx) => {
|
|||||||
toast.success('刷新登陆信息');
|
toast.success('刷新登陆信息');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return newRes as any;
|
return newRes as any;
|
||||||
};
|
};
|
||||||
export const request = query.post;
|
export const request = query.post;
|
||||||
|
39
src/pages/app/edit/AIEditorLink.tsx
Normal file
39
src/pages/app/edit/AIEditorLink.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
import { Folder } from 'lucide-react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
|
||||||
|
import { useLayoutStore } from '@/modules/layout/store';
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
type Props = {
|
||||||
|
pathname?: string;
|
||||||
|
};
|
||||||
|
export const AIEditorLink = (props: Props) => {
|
||||||
|
const layoutUser = useLayoutStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
user: state.me?.username || '',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Tooltip title={'打开对应的文件夹'}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (!layoutUser.user) {
|
||||||
|
toast.error('请先登录');
|
||||||
|
}
|
||||||
|
let folder = `${layoutUser.user}/resources/${props.pathname}`;
|
||||||
|
if (folder.endsWith('/')) {
|
||||||
|
folder = folder.slice(0, -1);
|
||||||
|
}
|
||||||
|
let baseUri = location.origin;
|
||||||
|
if (DEV_SERVER) {
|
||||||
|
baseUri = 'http://localhost:3005';
|
||||||
|
}
|
||||||
|
const openUrl = `${baseUri}/root/ai-pages/ai-editor/?folder=${folder}/`;
|
||||||
|
window.open(openUrl, '_blank');
|
||||||
|
}}>
|
||||||
|
<Folder size={16} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
@@ -23,6 +23,7 @@ import { TextField } from '@mui/material';
|
|||||||
import { pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
|
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { AIEditorLink } from './AIEditorLink';
|
||||||
|
|
||||||
const FormModal = () => {
|
const FormModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -211,6 +212,7 @@ export const AppVersionList = () => {
|
|||||||
<LinkOutlined />
|
<LinkOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<AIEditorLink pathname={item.key + '/' + item.version} />
|
||||||
<Tooltip title='文件管理'>
|
<Tooltip title='文件管理'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@@ -26,7 +26,8 @@ import { pick } from 'lodash-es';
|
|||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { useLayoutStore } from '@/modules/layout/store';
|
import { useLayoutStore } from '@/modules/layout/store';
|
||||||
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
|
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
|
||||||
import { AppWindow, Edit, Link, RefreshCcw, Share2, Trash } from 'lucide-react';
|
import { AppWindow, Edit, Folder, Link, RefreshCcw, Share2, Trash } from 'lucide-react';
|
||||||
|
import { AIEditorLink } from './AIEditorLink';
|
||||||
|
|
||||||
const FormModal = () => {
|
const FormModal = () => {
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
@@ -433,6 +434,7 @@ export const List = () => {
|
|||||||
<Link size={16} />
|
<Link size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<AIEditorLink pathname={item.key} />
|
||||||
<Tooltip title={'Delete'}>
|
<Tooltip title={'Delete'}>
|
||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@@ -14,3 +14,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
@@ -12,3 +12,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
@@ -16,3 +16,4 @@ export const App = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { lazy, useEffect, useRef, useState } from 'react';
|
||||||
import { BaseEditor } from '@kevisual/codemirror/editor/editor.ts';
|
|
||||||
import { Box, Drawer } from '@mui/material';
|
import { Box, Drawer } from '@mui/material';
|
||||||
import { useShallow } from 'zustand/shallow';
|
import { useShallow } from 'zustand/shallow';
|
||||||
import { useContainerStore } from '../store';
|
import { useContainerStore } from '../store';
|
||||||
@@ -8,6 +7,7 @@ import { IconButton } from '@kevisual/components/button/index.tsx';
|
|||||||
import { LeftOutlined, SaveOutlined } from '@ant-design/icons';
|
import { LeftOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
// import { previewCode } from './preview-code';
|
// import { previewCode } from './preview-code';
|
||||||
// import { StackIcons } from './StackIcons';
|
// import { StackIcons } from './StackIcons';
|
||||||
|
import { BaseEditor } from '@kevisual/codemirror/editor/editor.ts';
|
||||||
|
|
||||||
export const DrawEdit = () => {
|
export const DrawEdit = () => {
|
||||||
const editorElRef = useRef<HTMLDivElement>(null);
|
const editorElRef = useRef<HTMLDivElement>(null);
|
||||||
|
@@ -12,3 +12,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
@@ -12,3 +12,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@@ -10,3 +10,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
@@ -9,18 +9,22 @@ import { toast } from 'react-toastify';
|
|||||||
import { uploadFile } from './upload-file';
|
import { uploadFile } from './upload-file';
|
||||||
import { useAppVersionStore } from '@/pages/app/store';
|
import { useAppVersionStore } from '@/pages/app/store';
|
||||||
import { useShallow } from 'zustand/shallow';
|
import { useShallow } from 'zustand/shallow';
|
||||||
|
import { RefreshCcw } from 'lucide-react';
|
||||||
|
|
||||||
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
|
||||||
export const UploadModal = () => {
|
export const UploadModal = () => {
|
||||||
const { appKey, version, filename, openUploadModal, text, setOpenUploadModal, setAppKey, setVersion, setFilename, setOpenSuccessModal } = useHomeStore();
|
const { appKey, version, filename, openUploadModal, text, setOpenUploadModal, setAppKey, setVersion, setFilename, setOpenSuccessModal } = useHomeStore();
|
||||||
const { control, handleSubmit, reset } = useForm();
|
const { control, handleSubmit, reset, setValue } = useForm();
|
||||||
const { publishVersion } = useAppVersionStore(useShallow((state) => ({ publishVersion: state.publishVersion })));
|
const { publishVersion } = useAppVersionStore(useShallow((state) => ({ publishVersion: state.publishVersion })));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openUploadModal) {
|
if (openUploadModal) {
|
||||||
const randomAppKey = nanoid(4) + nanoid(4);
|
reset({ appKey: appKey || randomAppKey(), version: version || '1.0.0', filename: filename || 'index.html' });
|
||||||
reset({ appKey: appKey || randomAppKey, version: version || '1.0.0', filename: filename || 'index.html' });
|
|
||||||
}
|
}
|
||||||
}, [openUploadModal]);
|
}, [openUploadModal]);
|
||||||
|
const randomAppKey = () => {
|
||||||
|
const randomAppKey = nanoid(4) + nanoid(4);
|
||||||
|
return randomAppKey;
|
||||||
|
};
|
||||||
const onSubmit = async (data: any) => {
|
const onSubmit = async (data: any) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -71,7 +75,15 @@ export const UploadModal = () => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<form className='flex flex-col gap-3 pt-1 w-[500px]' onSubmit={handleSubmit(onSubmit)}>
|
<form className='flex flex-col gap-3 pt-1 w-[500px]' onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<label className='text-sm'>应用key</label>
|
<label className='text-sm flex gap-2 items-center'>
|
||||||
|
应用key
|
||||||
|
<RefreshCcw
|
||||||
|
className='cursor-pointer w-4 h-4'
|
||||||
|
onClick={() => {
|
||||||
|
setValue('appKey', randomAppKey());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<Controller control={control} name='appKey' render={({ field }) => <TextField {...field} />} />
|
<Controller control={control} name='appKey' render={({ field }) => <TextField {...field} />} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
|
@@ -81,3 +81,6 @@ const ServerPath = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const App = ServerPath;
|
export const App = ServerPath;
|
||||||
|
|
||||||
|
|
||||||
|
export default App;
|
@@ -15,3 +15,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@@ -2,8 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { X, ChevronRight } from 'lucide-react';
|
import { X, ChevronRight } from 'lucide-react';
|
||||||
import { usePayStore } from './store/pay';
|
import { usePayStore } from './store/pay';
|
||||||
import { createQrcode } from './modules/create-qrcode';
|
import { createQrcode } from './modules/create-qrcode';
|
||||||
import Panda from '@/assets/panda.png';
|
import { Button } from '@mui/material';
|
||||||
import Button from '@mui/material/Button/Button';
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [isAgreed, setIsAgreed] = useState(false);
|
const [isAgreed, setIsAgreed] = useState(false);
|
||||||
const qrcodeRef = useRef<HTMLImageElement>(null);
|
const qrcodeRef = useRef<HTMLImageElement>(null);
|
||||||
@@ -38,7 +37,7 @@ export const App = () => {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className='bg-white p-3 flex justify-between items-center border-b border-gray-200'>
|
<div className='bg-white p-3 flex justify-between items-center border-b border-gray-200'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<img src={user?.avatar ?? Panda} alt='User Avatar' className='w-8 h-8 rounded-full' />
|
<img src={user?.avatar} alt='User Avatar' className='w-8 h-8 rounded-full' />
|
||||||
<div>
|
<div>
|
||||||
<h2 className='text-base font-semibold'>{username}</h2>
|
<h2 className='text-base font-semibold'>{username}</h2>
|
||||||
<span className='text-gray-500 text-xs'>会员:已过期</span>
|
<span className='text-gray-500 text-xs'>会员:已过期</span>
|
||||||
@@ -89,3 +88,5 @@ export const App = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
@@ -7,7 +7,7 @@ import { useShallow } from 'zustand/react/shallow';
|
|||||||
import { isObjectNull } from '@/utils/is-null';
|
import { isObjectNull } from '@/utils/is-null';
|
||||||
import { useLayoutStore } from '@/modules/layout/store';
|
import { useLayoutStore } from '@/modules/layout/store';
|
||||||
import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
import UploadOutlined from '@ant-design/icons/UploadOutlined';
|
||||||
import PandaPNG from '@/assets/panda.png';
|
import { PandaPNG } from '@/assets/index.tsx';
|
||||||
import { FileUpload } from '../module/FileUpload';
|
import { FileUpload } from '../module/FileUpload';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Edit, UserCog } from 'lucide-react';
|
import { Edit, UserCog } from 'lucide-react';
|
||||||
|
@@ -18,3 +18,5 @@ export const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default App;
|
@@ -39,8 +39,8 @@ export const FileUpload = forwardRef<any, Props>((props, ref) => {
|
|||||||
})) as any;
|
})) as any;
|
||||||
console.log('res', res);
|
console.log('res', res);
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
const resource = res.data?.resource;
|
const [resource] = res.data?.upload || {};
|
||||||
props?.onChange?.(resource);
|
props?.onChange?.(resource?.path || '');
|
||||||
} else {
|
} else {
|
||||||
message.error(res.message || 'Request failed');
|
message.error(res.message || 'Request failed');
|
||||||
}
|
}
|
||||||
|
7
src/query/index.ts
Normal file
7
src/query/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Query } from '@kevisual/query';
|
||||||
|
|
||||||
|
export const query = new Query();
|
||||||
|
|
||||||
|
export const clientQuery = new Query({ url: '/client/router' });
|
||||||
|
|
||||||
|
export { QueryUtil } from '@kevisual/router/define';
|
25
src/query/kevisual.json
Normal file
25
src/query/kevisual.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://kevisual.xiongxiao.me/root/ai/kevisual/tools/kevisual-sync/schema.json?v=2",
|
||||||
|
"metadata": {
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"checkDir": {
|
||||||
|
"src/query": {
|
||||||
|
"url": "https://kevisual.xiongxiao.me/root/ai/code/registry/query",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"syncDirectory": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"src/query/**/*"
|
||||||
|
],
|
||||||
|
"ignore": [],
|
||||||
|
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
|
||||||
|
"replace": {
|
||||||
|
"src/": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sync": {}
|
||||||
|
}
|
42
src/query/query-ai/defines/ai.ts
Normal file
42
src/query/query-ai/defines/ai.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { QueryUtil } from '@/query/index.ts';
|
||||||
|
|
||||||
|
type Message = {
|
||||||
|
role?: 'user' | 'assistant' | 'system' | 'tool';
|
||||||
|
content?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
export type PostChat = {
|
||||||
|
messages?: Message[];
|
||||||
|
model?: string;
|
||||||
|
group?: string;
|
||||||
|
user?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatDataOpts = {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
messages?: any[];
|
||||||
|
data?: any;
|
||||||
|
type?: 'temp' | 'keep' | string;
|
||||||
|
};
|
||||||
|
export type ChatOpts = {
|
||||||
|
username: string;
|
||||||
|
model: string;
|
||||||
|
/**
|
||||||
|
* 获取完整消息回复
|
||||||
|
*/
|
||||||
|
getFull?: boolean;
|
||||||
|
group: string;
|
||||||
|
/**
|
||||||
|
* openai的参数
|
||||||
|
*/
|
||||||
|
options?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appDefine = QueryUtil.create({
|
||||||
|
chat: {
|
||||||
|
path: 'ai',
|
||||||
|
key: 'chat',
|
||||||
|
description: '与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。',
|
||||||
|
},
|
||||||
|
});
|
101
src/query/query-ai/query-ai.ts
Normal file
101
src/query/query-ai/query-ai.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { appDefine } from './defines/ai.ts';
|
||||||
|
import { PostChat, ChatOpts, ChatDataOpts } from './defines/ai.ts';
|
||||||
|
|
||||||
|
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||||
|
|
||||||
|
export { appDefine };
|
||||||
|
|
||||||
|
export class QueryApp<T extends Query = Query> extends BaseQuery<T, typeof appDefine> {
|
||||||
|
constructor(opts?: { query: T }) {
|
||||||
|
super({
|
||||||
|
...opts,
|
||||||
|
query: opts?.query!,
|
||||||
|
queryDefine: appDefine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 与 AI 进行对话, 调用 GPT 的AI 服务,生成结果,并返回。
|
||||||
|
* @param data
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
postChat(data: PostChat, opts?: DataOpts) {
|
||||||
|
return this.chain('chat').post(data, opts);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取模型列表
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getModelList(data?: { usernames?: string[] }, opts?: DataOpts) {
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'ai',
|
||||||
|
key: 'get-model-list',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 聊天对话模型
|
||||||
|
* @param data
|
||||||
|
* @param chatOpts
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
chat(data: ChatDataOpts, chatOpts: ChatOpts, opts?: DataOpts) {
|
||||||
|
const { username, model, group, getFull = true } = chatOpts;
|
||||||
|
if (!username || !model || !group) {
|
||||||
|
throw new Error('username, model, group is required');
|
||||||
|
}
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'ai',
|
||||||
|
key: 'chat',
|
||||||
|
...chatOpts,
|
||||||
|
getFull,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
clearConfigCache(opts?: DataOpts) {
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'ai',
|
||||||
|
key: 'clear-cache',
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取聊天使用情况
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getChatUsage(opts?: DataOpts) {
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'ai',
|
||||||
|
key: 'get-chat-usage',
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除当前用户模型自己的统计
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
clearSelfUsage(opts?: DataOpts) {
|
||||||
|
return this.query.post(
|
||||||
|
{
|
||||||
|
path: 'ai',
|
||||||
|
key: 'clear-chat-limit',
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
3
src/query/query-app/defines/index.ts
Normal file
3
src/query/query-app/defines/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { appDefine } from './user-app-list';
|
||||||
|
import { userAppDefine } from './user-app';
|
||||||
|
export { appDefine, userAppDefine };
|
62
src/query/query-app/defines/user-app-list.ts
Normal file
62
src/query/query-app/defines/user-app-list.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { QueryUtil } from '@/query/index.ts';
|
||||||
|
|
||||||
|
export const appDefine = QueryUtil.create({
|
||||||
|
getApp: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取应用信息',
|
||||||
|
},
|
||||||
|
|
||||||
|
updateApp: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'update',
|
||||||
|
description: '更新应用信息',
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteApp: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'delete',
|
||||||
|
description: '删除应用信息',
|
||||||
|
},
|
||||||
|
|
||||||
|
listApps: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'list',
|
||||||
|
description: '列出所有应用信息',
|
||||||
|
},
|
||||||
|
|
||||||
|
canUploadFiles: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'canUploadFiles',
|
||||||
|
description: '检查是否可以上传文件',
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFiles: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'uploadFiles',
|
||||||
|
description: '上传文件',
|
||||||
|
},
|
||||||
|
|
||||||
|
publishApp: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'publish',
|
||||||
|
description: '发布应用',
|
||||||
|
},
|
||||||
|
|
||||||
|
getMinioList: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'get-minio-list',
|
||||||
|
description: '获取 MinIO 文件列表',
|
||||||
|
},
|
||||||
|
|
||||||
|
detectVersionList: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'detectVersionList',
|
||||||
|
description: '检测版本列表并同步 MinIO 数据',
|
||||||
|
},
|
||||||
|
publicList: {
|
||||||
|
path: 'app',
|
||||||
|
key: 'public-list',
|
||||||
|
description: '获取公开应用列表',
|
||||||
|
},
|
||||||
|
});
|
33
src/query/query-app/defines/user-app.ts
Normal file
33
src/query/query-app/defines/user-app.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { QueryUtil } from '@/query/index.ts';
|
||||||
|
|
||||||
|
export const userAppDefine = QueryUtil.create({
|
||||||
|
listUserApps: {
|
||||||
|
path: 'user-app',
|
||||||
|
key: 'list',
|
||||||
|
description: '列出当前用户的所有应用(不包含 data 字段)',
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserApp: {
|
||||||
|
path: 'user-app',
|
||||||
|
key: 'get',
|
||||||
|
description: '获取用户应用信息,可以指定 id 或 key',
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUserApp: {
|
||||||
|
path: 'user-app',
|
||||||
|
key: 'update',
|
||||||
|
description: '更新或创建用户应用',
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUserApp: {
|
||||||
|
path: 'user-app',
|
||||||
|
key: 'delete',
|
||||||
|
description: '删除用户应用及关联数据',
|
||||||
|
},
|
||||||
|
|
||||||
|
testUserApp: {
|
||||||
|
path: 'user-app',
|
||||||
|
key: 'test',
|
||||||
|
description: '对 user-app 的数据进行测试,获取版本信息',
|
||||||
|
},
|
||||||
|
});
|
1
src/query/query-app/query-app-define.ts
Normal file
1
src/query/query-app/query-app-define.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './defines/index.ts';
|
18
src/query/query-app/query-app.ts
Normal file
18
src/query/query-app/query-app.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { appDefine, userAppDefine } from './defines/index.ts';
|
||||||
|
|
||||||
|
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||||
|
|
||||||
|
export { appDefine, userAppDefine };
|
||||||
|
|
||||||
|
export class QueryApp extends BaseQuery {
|
||||||
|
appDefine = appDefine;
|
||||||
|
userAppDefine = userAppDefine;
|
||||||
|
constructor(opts?: { query: Query }) {
|
||||||
|
super(opts!);
|
||||||
|
this.appDefine.query = this.query;
|
||||||
|
this.userAppDefine.query = this.query;
|
||||||
|
}
|
||||||
|
getList(data: any, opts?: DataOpts) {
|
||||||
|
return this.appDefine.queryChain('listApps').post(data, opts);
|
||||||
|
}
|
||||||
|
}
|
204
src/query/query-login/login-cache.ts
Normal file
204
src/query/query-login/login-cache.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
export interface Cache {
|
||||||
|
/**
|
||||||
|
* @update 获取缓存
|
||||||
|
*/
|
||||||
|
get(key: string): Promise<any>;
|
||||||
|
/**
|
||||||
|
* @update 设置缓存
|
||||||
|
*/
|
||||||
|
set(key: string, value: any): Promise<any>;
|
||||||
|
/**
|
||||||
|
* @update 删除缓存
|
||||||
|
*/
|
||||||
|
del(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
init?: () => Promise<any>;
|
||||||
|
}
|
||||||
|
export type User = {
|
||||||
|
avatar?: string;
|
||||||
|
description?: string;
|
||||||
|
id?: string;
|
||||||
|
needChangePassword?: boolean;
|
||||||
|
orgs?: string[];
|
||||||
|
type?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CacheLoginUser = {
|
||||||
|
user?: User;
|
||||||
|
id?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
};
|
||||||
|
type CacheLogin = {
|
||||||
|
loginUsers: CacheLoginUser[];
|
||||||
|
} & CacheLoginUser;
|
||||||
|
|
||||||
|
export type CacheStore<T = Cache> = {
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 缓存数据
|
||||||
|
* @important 需要先调用init
|
||||||
|
*/
|
||||||
|
cacheData: CacheLogin;
|
||||||
|
/**
|
||||||
|
* 实际操作的cache, 需要先调用init
|
||||||
|
*/
|
||||||
|
cache: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前用户
|
||||||
|
*/
|
||||||
|
setLoginUser(user: CacheLoginUser): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 获取当前用户
|
||||||
|
*/
|
||||||
|
getCurrentUser(): Promise<User>;
|
||||||
|
/**
|
||||||
|
* 获取当前用户列表
|
||||||
|
*/
|
||||||
|
getCurrentUserList(): Promise<CacheLoginUser[]>;
|
||||||
|
/**
|
||||||
|
* 获取缓存的refreshToken
|
||||||
|
*/
|
||||||
|
getRefreshToken(): Promise<string>;
|
||||||
|
/**
|
||||||
|
* 获取缓存的accessToken
|
||||||
|
*/
|
||||||
|
getAccessToken(): Promise<string>;
|
||||||
|
/**
|
||||||
|
* 清除当前用户
|
||||||
|
*/
|
||||||
|
clearCurrentUser(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* 清除所有用户
|
||||||
|
*/
|
||||||
|
clearAll(): Promise<void>;
|
||||||
|
|
||||||
|
getValue(): Promise<CacheLogin>;
|
||||||
|
setValue(value: CacheLogin): Promise<CacheLogin>;
|
||||||
|
delValue(): Promise<void>;
|
||||||
|
init(): Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginCacheStoreOpts = {
|
||||||
|
name: string;
|
||||||
|
cache: Cache;
|
||||||
|
};
|
||||||
|
export class LoginCacheStore implements CacheStore<any> {
|
||||||
|
cache: Cache;
|
||||||
|
name: string;
|
||||||
|
cacheData: CacheLogin;
|
||||||
|
constructor(opts: LoginCacheStoreOpts) {
|
||||||
|
if (!opts.cache) {
|
||||||
|
throw new Error('cache is required');
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
this.cache = opts.cache;
|
||||||
|
this.cacheData = {
|
||||||
|
loginUsers: [],
|
||||||
|
user: undefined,
|
||||||
|
id: undefined,
|
||||||
|
accessToken: undefined,
|
||||||
|
refreshToken: undefined,
|
||||||
|
};
|
||||||
|
this.name = opts.name;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置缓存
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async setValue(value: CacheLogin) {
|
||||||
|
await this.cache.set(this.name, value);
|
||||||
|
this.cacheData = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除缓存
|
||||||
|
*/
|
||||||
|
async delValue() {
|
||||||
|
await this.cache.del();
|
||||||
|
}
|
||||||
|
getValue(): Promise<CacheLogin> {
|
||||||
|
return this.cache.get(this.name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 初始化,设置默认值
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
const defaultData = {
|
||||||
|
loginUsers: [],
|
||||||
|
user: null,
|
||||||
|
id: null,
|
||||||
|
accessToken: null,
|
||||||
|
refreshToken: null,
|
||||||
|
};
|
||||||
|
if (this.cache.init) {
|
||||||
|
try {
|
||||||
|
const cacheData = await this.cache.init();
|
||||||
|
this.cacheData = cacheData || defaultData;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('cacheInit error', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.cacheData = (await this.getValue()) || defaultData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置当前用户
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
async setLoginUser(user: CacheLoginUser) {
|
||||||
|
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||||
|
if (has) {
|
||||||
|
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||||
|
}
|
||||||
|
this.cacheData.loginUsers.push(user);
|
||||||
|
this.cacheData.user = user.user;
|
||||||
|
this.cacheData.id = user.id;
|
||||||
|
this.cacheData.accessToken = user.accessToken;
|
||||||
|
this.cacheData.refreshToken = user.refreshToken;
|
||||||
|
await this.setValue(this.cacheData);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser(): Promise<CacheLoginUser> {
|
||||||
|
const cacheData = this.cacheData;
|
||||||
|
return Promise.resolve(cacheData.user!);
|
||||||
|
}
|
||||||
|
getCurrentUserList(): Promise<CacheLoginUser[]> {
|
||||||
|
return Promise.resolve(this.cacheData.loginUsers.filter((u) => u?.id));
|
||||||
|
}
|
||||||
|
getRefreshToken(): Promise<string> {
|
||||||
|
const cacheData = this.cacheData;
|
||||||
|
return Promise.resolve(cacheData.refreshToken || '');
|
||||||
|
}
|
||||||
|
getAccessToken(): Promise<string> {
|
||||||
|
const cacheData = this.cacheData;
|
||||||
|
return Promise.resolve(cacheData.accessToken || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCurrentUser() {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
const has = this.cacheData.loginUsers.find((u) => u.id === user.id);
|
||||||
|
if (has) {
|
||||||
|
this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id);
|
||||||
|
}
|
||||||
|
this.cacheData.user = undefined;
|
||||||
|
this.cacheData.id = undefined;
|
||||||
|
this.cacheData.accessToken = undefined;
|
||||||
|
this.cacheData.refreshToken = undefined;
|
||||||
|
await this.setValue(this.cacheData);
|
||||||
|
}
|
||||||
|
async clearAll() {
|
||||||
|
this.cacheData.loginUsers = [];
|
||||||
|
this.cacheData.user = undefined;
|
||||||
|
this.cacheData.id = undefined;
|
||||||
|
this.cacheData.accessToken = undefined;
|
||||||
|
this.cacheData.refreshToken = undefined;
|
||||||
|
await this.setValue(this.cacheData);
|
||||||
|
}
|
||||||
|
}
|
132
src/query/query-login/login-node-cache.ts
Normal file
132
src/query/query-login/login-node-cache.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Cache } from './login-cache.ts';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { readFileSync, writeFileSync, accessSync } from 'node:fs';
|
||||||
|
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
||||||
|
export const fileExists = async (
|
||||||
|
filePath: string,
|
||||||
|
{ createIfNotExists = true, isFile = true, isDir = false }: { createIfNotExists?: boolean; isFile?: boolean; isDir?: boolean } = {},
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
accessSync(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (createIfNotExists && isDir) {
|
||||||
|
await mkdir(filePath, { recursive: true });
|
||||||
|
return true;
|
||||||
|
} else if (createIfNotExists && isFile) {
|
||||||
|
await mkdir(dirname(filePath), { recursive: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const readConfigFile = (filePath: string) => {
|
||||||
|
try {
|
||||||
|
const data = readFileSync(filePath, 'utf-8');
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
return jsonData;
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const writeConfigFile = (filePath: string, data: any) => {
|
||||||
|
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||||
|
};
|
||||||
|
export const getHostName = () => {
|
||||||
|
const configDir = join(homedir(), '.config', 'envision');
|
||||||
|
const configFile = join(configDir, 'config.json');
|
||||||
|
const config = readConfigFile(configFile);
|
||||||
|
const baseURL = config.baseURL || 'https://kevisual.cn';
|
||||||
|
const hostname = new URL(baseURL).hostname;
|
||||||
|
return hostname;
|
||||||
|
};
|
||||||
|
export class StorageNode implements Storage {
|
||||||
|
cacheData: any;
|
||||||
|
filePath: string;
|
||||||
|
constructor() {
|
||||||
|
this.cacheData = {};
|
||||||
|
const configDir = join(homedir(), '.config', 'envision');
|
||||||
|
const hostname = getHostName();
|
||||||
|
this.filePath = join(configDir, 'config', `${hostname}-storage.json`);
|
||||||
|
fileExists(this.filePath, { isFile: true });
|
||||||
|
}
|
||||||
|
async loadCache() {
|
||||||
|
const filePath = this.filePath;
|
||||||
|
try {
|
||||||
|
const data = await readConfigFile(filePath);
|
||||||
|
this.cacheData = data;
|
||||||
|
} catch (error) {
|
||||||
|
this.cacheData = {};
|
||||||
|
await writeFile(filePath, JSON.stringify(this.cacheData, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get length() {
|
||||||
|
return Object.keys(this.cacheData).length;
|
||||||
|
}
|
||||||
|
getItem(key: string) {
|
||||||
|
return this.cacheData[key];
|
||||||
|
}
|
||||||
|
setItem(key: string, value: any) {
|
||||||
|
this.cacheData[key] = value;
|
||||||
|
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||||
|
}
|
||||||
|
removeItem(key: string) {
|
||||||
|
delete this.cacheData[key];
|
||||||
|
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.cacheData = {};
|
||||||
|
writeFile(this.filePath, JSON.stringify(this.cacheData, null, 2));
|
||||||
|
}
|
||||||
|
key(index: number) {
|
||||||
|
return Object.keys(this.cacheData)[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class LoginNodeCache implements Cache {
|
||||||
|
filepath: string;
|
||||||
|
|
||||||
|
constructor(filepath?: string) {
|
||||||
|
this.filepath = filepath || join(homedir(), '.config', 'envision', 'config', `${getHostName()}-login.json`);
|
||||||
|
fileExists(this.filepath, { isFile: true });
|
||||||
|
}
|
||||||
|
async get(_key: string) {
|
||||||
|
try {
|
||||||
|
const filePath = this.filepath;
|
||||||
|
const data = readConfigFile(filePath);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('get error', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async set(_key: string, value: any) {
|
||||||
|
try {
|
||||||
|
const data = readConfigFile(this.filepath);
|
||||||
|
const newData = { ...data, ...value };
|
||||||
|
writeConfigFile(this.filepath, newData);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('set error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async del() {
|
||||||
|
await unlink(this.filepath);
|
||||||
|
}
|
||||||
|
async loadCache(filePath: string) {
|
||||||
|
try {
|
||||||
|
const data = await readFile(filePath, 'utf-8');
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
return jsonData;
|
||||||
|
} catch (error) {
|
||||||
|
// console.log('loadCache error', error);
|
||||||
|
console.log('create new cache file:', filePath);
|
||||||
|
const defaultData = { loginUsers: [] };
|
||||||
|
writeConfigFile(filePath, defaultData);
|
||||||
|
return defaultData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
return await this.loadCache(this.filepath);
|
||||||
|
}
|
||||||
|
}
|
12
src/query/query-login/query-login-browser.ts
Normal file
12
src/query/query-login/query-login-browser.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||||
|
import { MyCache } from '@kevisual/cache';
|
||||||
|
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||||
|
|
||||||
|
export class QueryLoginBrowser extends QueryLogin {
|
||||||
|
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||||
|
super({
|
||||||
|
...opts,
|
||||||
|
cache: new MyCache('login'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
14
src/query/query-login/query-login-node.ts
Normal file
14
src/query/query-login/query-login-node.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { QueryLogin, QueryLoginOpts } from './query-login.ts';
|
||||||
|
import { LoginNodeCache, StorageNode } from './login-node-cache.ts';
|
||||||
|
type QueryLoginNodeOptsWithoutCache = Omit<QueryLoginOpts, 'cache'>;
|
||||||
|
export const storage = new StorageNode();
|
||||||
|
await storage.loadCache();
|
||||||
|
export class QueryLoginNode extends QueryLogin {
|
||||||
|
constructor(opts: QueryLoginNodeOptsWithoutCache) {
|
||||||
|
super({
|
||||||
|
...opts,
|
||||||
|
storage,
|
||||||
|
cache: new LoginNodeCache(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
468
src/query/query-login/query-login.ts
Normal file
468
src/query/query-login/query-login.ts
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
import { Query, BaseQuery } from '@kevisual/query';
|
||||||
|
import type { Result, DataOpts } from '@kevisual/query/query';
|
||||||
|
import { setBaseResponse } from '@kevisual/query/query';
|
||||||
|
import { LoginCacheStore, CacheStore, User } from './login-cache.ts';
|
||||||
|
import { Cache } from './login-cache.ts';
|
||||||
|
|
||||||
|
export type QueryLoginOpts = {
|
||||||
|
query?: Query;
|
||||||
|
isBrowser?: boolean;
|
||||||
|
onLoad?: () => void;
|
||||||
|
storage?: Storage;
|
||||||
|
cache: Cache;
|
||||||
|
};
|
||||||
|
export type QueryLoginData = {
|
||||||
|
username?: string;
|
||||||
|
password: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
export type QueryLoginResult = {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QueryLogin extends BaseQuery {
|
||||||
|
/**
|
||||||
|
* query login cache, 非实际操作, 一个cache的包裹模块
|
||||||
|
*/
|
||||||
|
cacheStore: CacheStore;
|
||||||
|
isBrowser: boolean;
|
||||||
|
load?: boolean;
|
||||||
|
storage: Storage;
|
||||||
|
onLoad?: () => void;
|
||||||
|
|
||||||
|
constructor(opts?: QueryLoginOpts) {
|
||||||
|
super({
|
||||||
|
query: opts?.query || new Query(),
|
||||||
|
});
|
||||||
|
this.cacheStore = new LoginCacheStore({ name: 'login', cache: opts?.cache! });
|
||||||
|
this.isBrowser = opts?.isBrowser ?? true;
|
||||||
|
this.init();
|
||||||
|
this.onLoad = opts?.onLoad;
|
||||||
|
this.storage = opts?.storage || localStorage;
|
||||||
|
}
|
||||||
|
setQuery(query: Query) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
private async init() {
|
||||||
|
await this.cacheStore.init();
|
||||||
|
this.load = true;
|
||||||
|
this.onLoad?.();
|
||||||
|
}
|
||||||
|
async post<T = any>(data: any, opts?: DataOpts) {
|
||||||
|
try {
|
||||||
|
return this.query.post<T>({ path: 'user', ...data }, opts);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
return {
|
||||||
|
code: 400,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 登录,
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async login(data: QueryLoginData) {
|
||||||
|
const res = await this.post<QueryLoginResult>({ key: 'login', ...data });
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { accessToken, refreshToken } = res?.data || {};
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 手机号登录
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async loginByCode(data: { phone: string; code: string }) {
|
||||||
|
const res = await this.post<QueryLoginResult>({ path: 'sms', key: 'login', data });
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { accessToken, refreshToken } = res?.data || {};
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 设置token
|
||||||
|
* @param token
|
||||||
|
*/
|
||||||
|
async setLoginToken(token: { accessToken: string; refreshToken: string }) {
|
||||||
|
const { accessToken, refreshToken } = token;
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
async loginByWechat(data: { code: string }) {
|
||||||
|
const res = await this.post<QueryLoginResult>({ path: 'wx', key: 'open-login', code: data.code });
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { accessToken, refreshToken } = res?.data || {};
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检测微信登录,登陆成功后,调用onSuccess,否则调用onError
|
||||||
|
* @param param0
|
||||||
|
*/
|
||||||
|
async checkWechat({ onSuccess, onError }: { onSuccess?: (res: QueryLoginResult) => void; onError?: (res: any) => void }) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const code = url.searchParams.get('code');
|
||||||
|
const state = url.searchParams.get('state');
|
||||||
|
if (code && state) {
|
||||||
|
const res = await this.loginByWechat({ code });
|
||||||
|
if (res.code === 200) {
|
||||||
|
onSuccess?.(res.data);
|
||||||
|
} else {
|
||||||
|
onError?.(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 登陆成功,需要获取用户信息进行缓存
|
||||||
|
* @param param0
|
||||||
|
*/
|
||||||
|
async beforeSetLoginUser({ accessToken, refreshToken, check401 }: { accessToken?: string; refreshToken?: string; check401?: boolean }) {
|
||||||
|
if (accessToken && refreshToken) {
|
||||||
|
const resUser = await this.getMe(accessToken, check401);
|
||||||
|
if (resUser.code === 200) {
|
||||||
|
const user = resUser.data;
|
||||||
|
if (user) {
|
||||||
|
this.cacheStore.setLoginUser({
|
||||||
|
user,
|
||||||
|
id: user.id,
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('登录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 刷新token
|
||||||
|
* @param refreshToken
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async queryRefreshToken(refreshToken?: string) {
|
||||||
|
const _refreshToken = refreshToken || this.cacheStore.getRefreshToken();
|
||||||
|
let data = { refreshToken: _refreshToken };
|
||||||
|
if (!_refreshToken) {
|
||||||
|
await this.cacheStore.clearCurrentUser();
|
||||||
|
return {
|
||||||
|
code: 401,
|
||||||
|
message: '请先登录',
|
||||||
|
data: {} as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.post(
|
||||||
|
{ key: 'refreshToken', data },
|
||||||
|
{
|
||||||
|
noStop: true,
|
||||||
|
beforeRequest: async (config) => {
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
afterResponse: async (response, ctx) => {
|
||||||
|
setBaseResponse(response);
|
||||||
|
return response as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查401错误,并刷新token, 如果refreshToken存在,则刷新token, 否则返回401
|
||||||
|
* 拦截请求,请使用run401Action, 不要直接使用 afterCheck401ToRefreshToken
|
||||||
|
* @param response
|
||||||
|
* @param ctx
|
||||||
|
* @param refetch
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async afterCheck401ToRefreshToken(response: Result, ctx?: { req?: any; res?: any; fetch?: any }, refetch?: boolean) {
|
||||||
|
const that = this;
|
||||||
|
if (response?.code === 401) {
|
||||||
|
const hasRefreshToken = await that.cacheStore.getRefreshToken();
|
||||||
|
if (hasRefreshToken) {
|
||||||
|
const res = await that.queryRefreshToken(hasRefreshToken);
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { accessToken, refreshToken } = res?.data || {};
|
||||||
|
that.storage.setItem('token', accessToken || '');
|
||||||
|
await that.beforeSetLoginUser({ accessToken, refreshToken, check401: false });
|
||||||
|
if (refetch && ctx && ctx.req && ctx.req.url && ctx.fetch) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
const url = ctx.req?.url;
|
||||||
|
const body = ctx.req?.body;
|
||||||
|
const headers = ctx.req?.headers;
|
||||||
|
const res = await ctx.fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body,
|
||||||
|
headers: { ...headers, Authorization: `Bearer ${accessToken}` },
|
||||||
|
});
|
||||||
|
setBaseResponse(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
that.storage.removeItem('token');
|
||||||
|
await that.cacheStore.clearCurrentUser();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response as any;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 一个简单的401处理, 如果401,则刷新token, 如果refreshToken不存在,则返回401
|
||||||
|
* refetch 是否重新请求, 会有bug,无限循环,按需要使用
|
||||||
|
* TODO:
|
||||||
|
* @param response
|
||||||
|
* @param ctx
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async run401Action(
|
||||||
|
response: Result,
|
||||||
|
ctx?: { req?: any; res?: any; fetch?: any },
|
||||||
|
opts?: {
|
||||||
|
/**
|
||||||
|
* 是否重新请求, 会有bug,无限循环,按需要使用
|
||||||
|
*/
|
||||||
|
refetch?: boolean;
|
||||||
|
/**
|
||||||
|
* check之后的回调
|
||||||
|
*/
|
||||||
|
afterCheck?: (res: Result) => any;
|
||||||
|
/**
|
||||||
|
* 401处理后, 还是401, 则回调
|
||||||
|
*/
|
||||||
|
afterAlso401?: (res: Result) => any;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const that = this;
|
||||||
|
const refetch = opts?.refetch ?? false;
|
||||||
|
|
||||||
|
if (response?.code === 401) {
|
||||||
|
if (that.query.stop === true) {
|
||||||
|
return { code: 500, success: false, message: 'refresh token loading...' };
|
||||||
|
}
|
||||||
|
that.query.stop = true;
|
||||||
|
const res = await that.afterCheck401ToRefreshToken(response, ctx, refetch);
|
||||||
|
that.query.stop = false;
|
||||||
|
opts?.afterCheck?.(res);
|
||||||
|
if (res.code === 401) {
|
||||||
|
opts?.afterAlso401?.(res);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
return response as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getMe(token?: string, check401: boolean = true) {
|
||||||
|
const _token = token || this.storage.getItem('token');
|
||||||
|
const that = this;
|
||||||
|
return that.post(
|
||||||
|
{ key: 'me' },
|
||||||
|
{
|
||||||
|
noStop: true,
|
||||||
|
beforeRequest: async (config) => {
|
||||||
|
if (config.headers) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${_token}`;
|
||||||
|
}
|
||||||
|
if (!_token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
afterResponse: async (response, ctx) => {
|
||||||
|
if (response?.code === 401 && check401 && !token) {
|
||||||
|
return await that.afterCheck401ToRefreshToken(response, ctx);
|
||||||
|
}
|
||||||
|
return response as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查本地用户,如果本地用户存在,则返回本地用户,否则返回null
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async checkLocalUser() {
|
||||||
|
const user = await this.cacheStore.getCurrentUser();
|
||||||
|
if (user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查本地token是否存在,简单的判断是否已经属于登陆状态
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async checkLocalToken() {
|
||||||
|
const token = this.storage.getItem('token');
|
||||||
|
return !!token;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查本地用户列表
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getToken() {
|
||||||
|
const token = this.storage.getItem('token');
|
||||||
|
return token || '';
|
||||||
|
}
|
||||||
|
async beforeRequest(opts: any = {}) {
|
||||||
|
const token = this.storage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
opts.headers = { ...opts.headers, Authorization: `Bearer ${token}` };
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 请求更新,切换用户, 使用switchUser
|
||||||
|
* @param username
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async postSwitchUser(username: string) {
|
||||||
|
return this.post({ key: 'switchCheck', data: { username } });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 切换用户
|
||||||
|
* @param username
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async switchUser(username: string) {
|
||||||
|
const localUserList = await this.cacheStore.getCurrentUserList();
|
||||||
|
const user = localUserList.find((userItem) => userItem.user!.username === username);
|
||||||
|
if (user) {
|
||||||
|
this.storage.setItem('token', user.accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken: user.accessToken, refreshToken: user.refreshToken });
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
accessToken: user.accessToken,
|
||||||
|
refreshToken: user.refreshToken,
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
message: '切换用户成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const res = await this.postSwitchUser(username);
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
const { accessToken, refreshToken } = res?.data || {};
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken });
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 退出登陆,去掉token, 并删除缓存
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async logout() {
|
||||||
|
this.storage.removeItem('token');
|
||||||
|
const users = await this.cacheStore.getCurrentUserList();
|
||||||
|
const tokens = users
|
||||||
|
.map((user) => {
|
||||||
|
return user?.accessToken;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
this.cacheStore.delValue();
|
||||||
|
return this.post<Result>({ key: 'logout', data: { tokens } });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查用户名的组,这个用户是否存在
|
||||||
|
* @param username
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async hasUser(username: string) {
|
||||||
|
const that = this;
|
||||||
|
return this.post<Result>(
|
||||||
|
{
|
||||||
|
path: 'org',
|
||||||
|
key: 'hasUser',
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
afterResponse: async (response, ctx) => {
|
||||||
|
if (response?.code === 401) {
|
||||||
|
const res = await that.afterCheck401ToRefreshToken(response, ctx, true);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return response as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async getLoginUser() {
|
||||||
|
const that = this;
|
||||||
|
const userInfo = await that.checkLocalUser();
|
||||||
|
const token = await that.getToken();
|
||||||
|
if (userInfo) {
|
||||||
|
return userInfo;
|
||||||
|
} else if (token) {
|
||||||
|
const userinfo = await that.getLoginUserByToken(token);
|
||||||
|
if (userinfo) {
|
||||||
|
return userinfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async getLoginUserByToken(token: string): Promise<User | null> {
|
||||||
|
const me = await this.getMe(token, false);
|
||||||
|
if (me.code === 200) {
|
||||||
|
const user = me.data;
|
||||||
|
this.cacheStore.setLoginUser({
|
||||||
|
user,
|
||||||
|
id: user.id,
|
||||||
|
accessToken: token,
|
||||||
|
refreshToken: me.data?.refreshToken || '',
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查登录状态,根据 login by web
|
||||||
|
* @param token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async checkLoginStatus(token: string) {
|
||||||
|
const res = await this.post({
|
||||||
|
path: 'user',
|
||||||
|
key: 'checkLoginStatus',
|
||||||
|
loginToken: token,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
const accessToken = res.data?.accessToken;
|
||||||
|
this.storage.setItem('token', accessToken || '');
|
||||||
|
await this.beforeSetLoginUser({ accessToken, refreshToken: res.data?.refreshToken });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 使用web登录,创建url地址, 需要MD5和jsonwebtoken
|
||||||
|
*/
|
||||||
|
loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) {
|
||||||
|
const randomId = Math.random().toString(36).substring(2, 15);
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const tokenSecret = 'xiao' + randomId;
|
||||||
|
const sign = MD5(`${tokenSecret}${timestamp}`).toString();
|
||||||
|
const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, {
|
||||||
|
// 10分钟过期
|
||||||
|
expiresIn: 60 * 10, // 10分钟
|
||||||
|
});
|
||||||
|
const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`;
|
||||||
|
return { url, token, tokenSecret };
|
||||||
|
}
|
||||||
|
}
|
154
src/query/query-mark/query-mark.ts
Normal file
154
src/query/query-mark/query-mark.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { Query } from '@kevisual/query';
|
||||||
|
import type { Result, DataOpts } from '@kevisual/query/query';
|
||||||
|
|
||||||
|
export type SimpleObject = Record<string, any>;
|
||||||
|
export const markType = ['simple', 'md', 'mdx', 'wallnote', 'excalidraw', 'chat'] as const;
|
||||||
|
export type MarkType = (typeof markType)[number];
|
||||||
|
export type MarkData = {
|
||||||
|
nodes?: any[];
|
||||||
|
edges?: any[];
|
||||||
|
elements?: any[];
|
||||||
|
permission?: any;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export type Mark = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
markType: MarkType;
|
||||||
|
link: string;
|
||||||
|
data?: MarkData;
|
||||||
|
uid: string;
|
||||||
|
puid: string;
|
||||||
|
summary: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
tags: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
export type ShowMarkPick = Pick<Mark, 'id' | 'title' | 'description' | 'summary' | 'link' | 'tags' | 'thumbnail' | 'updatedAt'>;
|
||||||
|
|
||||||
|
export type SearchOpts = {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: string; // DESC, ASC
|
||||||
|
markType?: MarkType; // 类型
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryMarkOpts<T extends SimpleObject = SimpleObject> = {
|
||||||
|
query?: Query;
|
||||||
|
isBrowser?: boolean;
|
||||||
|
onLoad?: () => void;
|
||||||
|
} & T;
|
||||||
|
|
||||||
|
export type ResultMarkList = {
|
||||||
|
list: Mark[];
|
||||||
|
pagination: {
|
||||||
|
pageSize: number;
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type QueryMarkData = {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export type QueryMarkResult = {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QueryMarkBase<T extends SimpleObject = SimpleObject> {
|
||||||
|
query: Query;
|
||||||
|
isBrowser: boolean;
|
||||||
|
load?: boolean;
|
||||||
|
storage?: Storage;
|
||||||
|
onLoad?: () => void;
|
||||||
|
|
||||||
|
constructor(opts?: QueryMarkOpts<T>) {
|
||||||
|
this.query = opts?.query || new Query();
|
||||||
|
this.isBrowser = opts?.isBrowser ?? true;
|
||||||
|
this.init();
|
||||||
|
this.onLoad = opts?.onLoad;
|
||||||
|
}
|
||||||
|
setQuery(query: Query) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
private async init() {
|
||||||
|
this.load = true;
|
||||||
|
this.onLoad?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T = Result<any>>(data: any, opts?: DataOpts): Promise<T> {
|
||||||
|
try {
|
||||||
|
return this.query.post({ path: 'mark', ...data }, opts) as Promise<T>;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error', error);
|
||||||
|
return {
|
||||||
|
code: 400,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMarkList(search: SearchOpts, opts?: DataOpts) {
|
||||||
|
return this.post<Result<ResultMarkList>>({ key: 'list', ...search }, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMark(id: string, opts?: DataOpts) {
|
||||||
|
return this.post<Result<Mark>>({ key: 'get', id }, opts);
|
||||||
|
}
|
||||||
|
async getVersion(id: string, opts?: DataOpts) {
|
||||||
|
return this.post<Result<{ version: number; id: string }>>({ key: 'getVersion', id }, opts);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查版本
|
||||||
|
* 当需要更新时,返回true
|
||||||
|
* @param id
|
||||||
|
* @param version
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async checkVersion(id: string, version?: number, opts?: DataOpts) {
|
||||||
|
if (!version) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const res = await this.getVersion(id, opts);
|
||||||
|
if (res.code === 200) {
|
||||||
|
if (res.data!.version > version) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMark(data: any, opts?: DataOpts) {
|
||||||
|
return this.post<Result<Mark>>({ key: 'update', data }, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMark(id: string, opts?: DataOpts) {
|
||||||
|
return this.post<Result<Mark>>({ key: 'delete', id }, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class QueryMark extends QueryMarkBase<SimpleObject> {
|
||||||
|
markType: string;
|
||||||
|
constructor(opts?: QueryMarkOpts & { markType?: MarkType }) {
|
||||||
|
super(opts);
|
||||||
|
this.markType = opts?.markType || 'simple';
|
||||||
|
}
|
||||||
|
async getMarkList(search?: SearchOpts, opts?: DataOpts) {
|
||||||
|
return this.post<Result<ResultMarkList>>({ key: 'list', ...search, markType: this.markType }, opts);
|
||||||
|
}
|
||||||
|
async updateMark(data: any, opts?: DataOpts) {
|
||||||
|
if (!data.id) {
|
||||||
|
data.markType = this.markType || 'simple';
|
||||||
|
}
|
||||||
|
return super.updateMark(data, opts);
|
||||||
|
}
|
||||||
|
}
|
155
src/query/query-resources/index.ts
Normal file
155
src/query/query-resources/index.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Content } from './../../apps/ai-editor/content';
|
||||||
|
import { DataOpts, Result } from '@kevisual/query';
|
||||||
|
import { adapter } from '@kevisual/query';
|
||||||
|
import path from 'path-browserify-esm';
|
||||||
|
import { hashContent } from './utils';
|
||||||
|
|
||||||
|
type QueryResourcesOptions = {
|
||||||
|
prefix?: string;
|
||||||
|
storage?: Storage;
|
||||||
|
username?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
export class QueryResources {
|
||||||
|
prefix: string; // /root/resources
|
||||||
|
storage: Storage;
|
||||||
|
constructor(opts: QueryResourcesOptions) {
|
||||||
|
if (opts.username) {
|
||||||
|
this.prefix = `/${opts.username}/resources/`;
|
||||||
|
} else {
|
||||||
|
this.prefix = opts.prefix || '';
|
||||||
|
}
|
||||||
|
this.storage = opts.storage || localStorage;
|
||||||
|
}
|
||||||
|
setUsername(username: string) {
|
||||||
|
const prefix = `/${username}/resources/`;
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
setPrefix(prefix: string) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
}
|
||||||
|
header(headers?: Record<string, string>, json = true): Record<string, string> {
|
||||||
|
const token = this.storage.getItem('token');
|
||||||
|
const _headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
};
|
||||||
|
if (!json) {
|
||||||
|
delete _headers['Content-Type'];
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
return _headers;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
..._headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async get(data: any, opts: DataOpts): Promise<any> {
|
||||||
|
return adapter({
|
||||||
|
url: opts.url!,
|
||||||
|
method: 'GET',
|
||||||
|
body: data,
|
||||||
|
...opts,
|
||||||
|
headers: this.header(opts?.headers),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
|
||||||
|
return this.get(data, {
|
||||||
|
url: `${this.prefix}${prefix}`,
|
||||||
|
body: data,
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
|
||||||
|
const url = `${this.prefix}${filepath}`;
|
||||||
|
return this.get({}, { url, method: 'GET', headers: this.header(opts?.headers, false), isText: true });
|
||||||
|
}
|
||||||
|
async uploadFile(filepath: string, content: string, opts?: DataOpts): Promise<Result<any>> {
|
||||||
|
const pathname = `${this.prefix}${filepath}`;
|
||||||
|
const filename = path.basename(pathname);
|
||||||
|
const type = getContentType(filename);
|
||||||
|
const url = new URL(pathname, window.location.origin);
|
||||||
|
const hash = hashContent(content);
|
||||||
|
url.searchParams.set('hash', hash);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', new Blob([content], { type }));
|
||||||
|
return adapter({
|
||||||
|
url: url.toString(),
|
||||||
|
headers: { ...this.header(opts?.headers, false) },
|
||||||
|
isPostFile: true,
|
||||||
|
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContentType = (filename: string): string => {
|
||||||
|
const ext = path.extname(filename);
|
||||||
|
let type = 'text/plain';
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case '':
|
||||||
|
type = 'application/octet-stream';
|
||||||
|
break;
|
||||||
|
case '.json':
|
||||||
|
type = 'application/json';
|
||||||
|
break;
|
||||||
|
case '.txt':
|
||||||
|
type = 'text/plain';
|
||||||
|
break;
|
||||||
|
case '.csv':
|
||||||
|
type = 'text/csv';
|
||||||
|
break;
|
||||||
|
case '.md':
|
||||||
|
type = 'text/markdown';
|
||||||
|
break;
|
||||||
|
case '.html':
|
||||||
|
case '.htm':
|
||||||
|
type = 'text/html';
|
||||||
|
break;
|
||||||
|
case '.xml':
|
||||||
|
type = 'application/xml';
|
||||||
|
break;
|
||||||
|
case '.js':
|
||||||
|
type = 'application/javascript';
|
||||||
|
break;
|
||||||
|
case '.css':
|
||||||
|
type = 'text/css';
|
||||||
|
break;
|
||||||
|
case '.ts':
|
||||||
|
type = 'application/typescript';
|
||||||
|
break;
|
||||||
|
case '.pdf':
|
||||||
|
type = 'application/pdf';
|
||||||
|
break;
|
||||||
|
case '.zip':
|
||||||
|
type = 'application/zip';
|
||||||
|
break;
|
||||||
|
case '.docx':
|
||||||
|
type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||||
|
break;
|
||||||
|
case '.xlsx':
|
||||||
|
type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
break;
|
||||||
|
case '.mp3':
|
||||||
|
type = 'audio/mpeg';
|
||||||
|
break;
|
||||||
|
case '.mp4':
|
||||||
|
type = 'video/mp4';
|
||||||
|
break;
|
||||||
|
case '.png':
|
||||||
|
case '.jpg':
|
||||||
|
case '.jpeg':
|
||||||
|
case '.gif':
|
||||||
|
case '.webp':
|
||||||
|
type = `image/${ext.slice(1)}`;
|
||||||
|
break;
|
||||||
|
case '.svg':
|
||||||
|
type = 'image/svg+xml';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
};
|
42
src/query/query-resources/utils.ts
Normal file
42
src/query/query-resources/utils.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import MD5 from 'crypto-js/md5';
|
||||||
|
|
||||||
|
export const hashContent = (str: string | Buffer): string => {
|
||||||
|
if (typeof str === 'string') {
|
||||||
|
return MD5(str).toString();
|
||||||
|
} else if (Buffer.isBuffer(str)) {
|
||||||
|
return MD5(str.toString()).toString();
|
||||||
|
}
|
||||||
|
console.error('hashContent error: input must be a string or Buffer');
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
export const hashFile = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const content = event.target?.result;
|
||||||
|
if (content instanceof ArrayBuffer) {
|
||||||
|
const contentString = new TextDecoder().decode(content);
|
||||||
|
const hashHex = MD5(contentString).toString();
|
||||||
|
resolve(hashHex);
|
||||||
|
} else if (typeof content === 'string') {
|
||||||
|
const hashHex = MD5(content).toString();
|
||||||
|
resolve(hashHex);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid content type');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('hashFile error', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取文件为 ArrayBuffer
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
};
|
27
src/query/query-shop/defines/query-shop-define.ts
Normal file
27
src/query/query-shop/defines/query-shop-define.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { QueryUtil } from '@/query/index.ts';
|
||||||
|
|
||||||
|
export const shopDefine = QueryUtil.create({
|
||||||
|
getRegistry: {
|
||||||
|
path: 'shop',
|
||||||
|
key: 'get-registry',
|
||||||
|
description: '获取应用商店注册表信息',
|
||||||
|
},
|
||||||
|
|
||||||
|
listInstalled: {
|
||||||
|
path: 'shop',
|
||||||
|
key: 'list-installed',
|
||||||
|
description: '列出当前已安装的所有应用',
|
||||||
|
},
|
||||||
|
|
||||||
|
install: {
|
||||||
|
path: 'shop',
|
||||||
|
key: 'install',
|
||||||
|
description: '安装指定的应用,可以指定 id、type、force 和 yes 参数',
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall: {
|
||||||
|
path: 'shop',
|
||||||
|
key: 'uninstall',
|
||||||
|
description: '卸载指定的应用,可以指定 id 和 type 参数',
|
||||||
|
},
|
||||||
|
});
|
17
src/query/query-shop/query-shop.ts
Normal file
17
src/query/query-shop/query-shop.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { shopDefine } from './defines/query-shop-define.ts';
|
||||||
|
|
||||||
|
import { BaseQuery, DataOpts, Query } from '@kevisual/query/query';
|
||||||
|
|
||||||
|
export { shopDefine };
|
||||||
|
|
||||||
|
export class QueryShop<T extends Query = Query> extends BaseQuery<T, typeof shopDefine> {
|
||||||
|
constructor(opts?: { query: T }) {
|
||||||
|
super({
|
||||||
|
query: opts?.query!,
|
||||||
|
queryDefine: shopDefine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getInstall(data: any, opts?: DataOpts) {
|
||||||
|
return this.queryDefine.queryChain('install').post(data, opts);
|
||||||
|
}
|
||||||
|
}
|
134
src/query/query-upload/core/upload-chunk.ts
Normal file
134
src/query/query-upload/core/upload-chunk.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { randomId } from '../utils/random-id.ts';
|
||||||
|
import { UploadProgress } from './upload-progress.ts';
|
||||||
|
export type ConvertOpts = {
|
||||||
|
appKey?: string;
|
||||||
|
version?: string;
|
||||||
|
username?: string;
|
||||||
|
directory?: string;
|
||||||
|
isPublic?: boolean;
|
||||||
|
filename?: string;
|
||||||
|
/**
|
||||||
|
* 是否不检查应用文件, 默认 true,默认不检测
|
||||||
|
*/
|
||||||
|
noCheckAppFiles?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// createEventSource: (baseUrl: string, searchParams: URLSearchParams) => {
|
||||||
|
// return new EventSource(baseUrl + '/api/s1/events?' + searchParams.toString());
|
||||||
|
// },
|
||||||
|
export type UploadOpts = {
|
||||||
|
uploadProgress: UploadProgress;
|
||||||
|
/**
|
||||||
|
* 创建 EventSource 兼容 nodejs
|
||||||
|
* @param baseUrl 基础 URL
|
||||||
|
* @param searchParams 查询参数
|
||||||
|
* @returns EventSource
|
||||||
|
*/
|
||||||
|
createEventSource: (baseUrl: string, searchParams: URLSearchParams) => EventSource;
|
||||||
|
baseUrl?: string;
|
||||||
|
token: string;
|
||||||
|
FormDataFn: any;
|
||||||
|
};
|
||||||
|
export const uploadFileChunked = async (file: File, opts: ConvertOpts, opts2: UploadOpts) => {
|
||||||
|
const { directory, appKey, version, username, isPublic, noCheckAppFiles = true } = opts;
|
||||||
|
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const taskId = randomId();
|
||||||
|
const filename = opts.filename || file.name;
|
||||||
|
uploadProgress?.start(`${filename} 上传中...`);
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.set('taskId', taskId);
|
||||||
|
if (isPublic) {
|
||||||
|
searchParams.set('public', 'true');
|
||||||
|
}
|
||||||
|
if (noCheckAppFiles) {
|
||||||
|
searchParams.set('noCheckAppFiles', '1');
|
||||||
|
}
|
||||||
|
const eventSource = createEventSource(baseUrl + '/api/s1/events', searchParams);
|
||||||
|
let isError = false;
|
||||||
|
// 监听服务器推送的进度更新
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
console.log('Progress update:', event.data);
|
||||||
|
const parseIfJson = (data: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const receivedData = parseIfJson(event.data);
|
||||||
|
if (typeof receivedData === 'string') return;
|
||||||
|
const progress = Number(receivedData.progress);
|
||||||
|
const progressFixed = progress.toFixed(2);
|
||||||
|
uploadProgress?.set(progress, { ...receivedData, progressFixed, filename, taskId });
|
||||||
|
};
|
||||||
|
eventSource.onerror = function (event) {
|
||||||
|
console.log('eventSource.onerror', event);
|
||||||
|
isError = true;
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const chunkSize = 1 * 1024 * 1024; // 1MB
|
||||||
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) {
|
||||||
|
const start = currentChunk * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
const formData = new FormDataFn();
|
||||||
|
formData.append('file', chunk, filename);
|
||||||
|
formData.append('chunkIndex', currentChunk.toString());
|
||||||
|
formData.append('totalChunks', totalChunks.toString());
|
||||||
|
const isLast = currentChunk === totalChunks - 1;
|
||||||
|
if (directory) {
|
||||||
|
formData.append('directory', directory);
|
||||||
|
}
|
||||||
|
if (appKey && version) {
|
||||||
|
formData.append('appKey', appKey);
|
||||||
|
formData.append('version', version);
|
||||||
|
}
|
||||||
|
if (username) {
|
||||||
|
formData.append('username', username);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(baseUrl + '/api/s1/resources/upload/chunk?taskId=' + taskId, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'task-id': taskId,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}).then((response) => response.json());
|
||||||
|
|
||||||
|
if (res?.code !== 200) {
|
||||||
|
console.log('uploadChunk error', res);
|
||||||
|
uploadProgress?.error(res?.message || '上传失败');
|
||||||
|
isError = true;
|
||||||
|
eventSource.close();
|
||||||
|
|
||||||
|
uploadProgress?.done();
|
||||||
|
reject(new Error(res?.message || '上传失败'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLast) {
|
||||||
|
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
||||||
|
eventSource.close();
|
||||||
|
uploadProgress?.done();
|
||||||
|
resolve(res);
|
||||||
|
}
|
||||||
|
// console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error uploading chunk', error);
|
||||||
|
fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 循环结束
|
||||||
|
if (!uploadProgress?.end) {
|
||||||
|
uploadProgress?.done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
103
src/query/query-upload/core/upload-progress.ts
Normal file
103
src/query/query-upload/core/upload-progress.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
interface UploadNProgress {
|
||||||
|
start: (msg?: string) => void;
|
||||||
|
done: () => void;
|
||||||
|
set: (progress: number) => void;
|
||||||
|
}
|
||||||
|
export type UploadProgressData = {
|
||||||
|
progress: number;
|
||||||
|
progressFixed: number;
|
||||||
|
filename?: string;
|
||||||
|
taskId?: string;
|
||||||
|
};
|
||||||
|
type UploadProgressOpts = {
|
||||||
|
onStart?: () => void;
|
||||||
|
onDone?: () => void;
|
||||||
|
onProgress?: (progress: number, data?: UploadProgressData) => void;
|
||||||
|
};
|
||||||
|
export class UploadProgress implements UploadNProgress {
|
||||||
|
/**
|
||||||
|
* 进度
|
||||||
|
*/
|
||||||
|
progress: number;
|
||||||
|
/**
|
||||||
|
* 开始回调
|
||||||
|
*/
|
||||||
|
onStart: (() => void) | undefined;
|
||||||
|
/**
|
||||||
|
* 结束回调
|
||||||
|
*/
|
||||||
|
onDone: (() => void) | undefined;
|
||||||
|
/**
|
||||||
|
* 消息回调
|
||||||
|
*/
|
||||||
|
onProgress: ((progress: number, data?: UploadProgressData) => void) | undefined;
|
||||||
|
/**
|
||||||
|
* 数据
|
||||||
|
*/
|
||||||
|
data: any;
|
||||||
|
/**
|
||||||
|
* 是否结束
|
||||||
|
*/
|
||||||
|
end: boolean;
|
||||||
|
constructor(uploadOpts: UploadProgressOpts) {
|
||||||
|
this.progress = 0;
|
||||||
|
this.end = false;
|
||||||
|
const mockFn = () => {};
|
||||||
|
this.onStart = uploadOpts.onStart || mockFn;
|
||||||
|
this.onDone = uploadOpts.onDone || mockFn;
|
||||||
|
this.onProgress = uploadOpts.onProgress || mockFn;
|
||||||
|
}
|
||||||
|
start(msg?: string) {
|
||||||
|
this.progress = 0;
|
||||||
|
msg && this.info(msg);
|
||||||
|
this.end = false;
|
||||||
|
this.onStart?.();
|
||||||
|
}
|
||||||
|
done() {
|
||||||
|
this.progress = 100;
|
||||||
|
this.end = true;
|
||||||
|
this.onDone?.();
|
||||||
|
}
|
||||||
|
set(progress: number, data?: UploadProgressData) {
|
||||||
|
this.progress = progress;
|
||||||
|
this.data = data;
|
||||||
|
this.onProgress?.(progress, data);
|
||||||
|
console.log('uploadProgress set', progress, data);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 开始回调
|
||||||
|
*/
|
||||||
|
setOnStart(callback: () => void) {
|
||||||
|
this.onStart = callback;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 结束回调
|
||||||
|
*/
|
||||||
|
setOnDone(callback: () => void) {
|
||||||
|
this.onDone = callback;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 消息回调
|
||||||
|
*/
|
||||||
|
setOnProgress(callback: (progress: number, data?: UploadProgressData) => void) {
|
||||||
|
this.onProgress = callback;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 打印信息
|
||||||
|
*/
|
||||||
|
info(msg: string) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 打印错误
|
||||||
|
*/
|
||||||
|
error(msg: string) {
|
||||||
|
console.error(msg);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 打印警告
|
||||||
|
*/
|
||||||
|
warn(msg: string) {
|
||||||
|
console.warn(msg);
|
||||||
|
}
|
||||||
|
}
|
113
src/query/query-upload/core/upload.ts
Normal file
113
src/query/query-upload/core/upload.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { randomId } from '../utils/random-id.ts';
|
||||||
|
import type { UploadOpts } from './upload-chunk.ts';
|
||||||
|
type ConvertOpts = {
|
||||||
|
appKey?: string;
|
||||||
|
version?: string;
|
||||||
|
username?: string;
|
||||||
|
directory?: string;
|
||||||
|
/**
|
||||||
|
* 文件大小限制
|
||||||
|
*/
|
||||||
|
maxSize?: number;
|
||||||
|
/**
|
||||||
|
* 文件数量限制
|
||||||
|
*/
|
||||||
|
maxCount?: number;
|
||||||
|
/**
|
||||||
|
* 是否不检查应用文件, 默认 true,默认不检测
|
||||||
|
*/
|
||||||
|
noCheckAppFiles?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadFiles = async (files: File[], opts: ConvertOpts, opts2: UploadOpts) => {
|
||||||
|
const { directory, appKey, version, username, noCheckAppFiles = true } = opts;
|
||||||
|
const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {};
|
||||||
|
const length = files.length;
|
||||||
|
const maxSize = opts.maxSize || 20 * 1024 * 1024; // 20MB
|
||||||
|
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||||
|
if (totalSize > maxSize) {
|
||||||
|
const maxSizeMB = maxSize / 1024 / 1024;
|
||||||
|
uploadProgress?.error('有文件大小不能超过' + maxSizeMB + 'MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxCount = opts.maxCount || 10;
|
||||||
|
if (length > maxCount) {
|
||||||
|
uploadProgress?.error(`最多只能上传${maxCount}个文件`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadProgress?.info(`上传中,共${length}个文件`);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormDataFn();
|
||||||
|
const webkitRelativePath = files[0]?.webkitRelativePath;
|
||||||
|
const keepDirectory = webkitRelativePath !== '';
|
||||||
|
const root = keepDirectory ? webkitRelativePath.split('/')[0] : '';
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (keepDirectory) {
|
||||||
|
// relativePath 去除第一级
|
||||||
|
const webkitRelativePath = file.webkitRelativePath.replace(root + '/', '');
|
||||||
|
formData.append('file', file, webkitRelativePath); // 保留文件夹路径
|
||||||
|
} else {
|
||||||
|
formData.append('file', files[i], files[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (directory) {
|
||||||
|
formData.append('directory', directory);
|
||||||
|
}
|
||||||
|
if (appKey && version) {
|
||||||
|
formData.append('appKey', appKey);
|
||||||
|
formData.append('version', version);
|
||||||
|
}
|
||||||
|
if (username) {
|
||||||
|
formData.append('username', username);
|
||||||
|
}
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
const taskId = randomId();
|
||||||
|
searchParams.set('taskId', taskId);
|
||||||
|
|
||||||
|
if (noCheckAppFiles) {
|
||||||
|
searchParams.set('noCheckAppFiles', '1');
|
||||||
|
}
|
||||||
|
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
|
||||||
|
|
||||||
|
uploadProgress?.start('上传中...');
|
||||||
|
eventSource.onopen = async function (event) {
|
||||||
|
const res = await fetch('/api/s1/resources/upload?' + searchParams.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'task-id': taskId,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}).then((response) => response.json());
|
||||||
|
|
||||||
|
console.log('upload success', res);
|
||||||
|
fetch('/api/s1/events/close?taskId=' + taskId);
|
||||||
|
eventSource.close();
|
||||||
|
uploadProgress?.done();
|
||||||
|
resolve(res);
|
||||||
|
};
|
||||||
|
// 监听服务器推送的进度更新
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
console.log('Progress update:', event.data);
|
||||||
|
const parseIfJson = (data: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const receivedData = parseIfJson(event.data);
|
||||||
|
if (typeof receivedData === 'string') return;
|
||||||
|
const progress = Number(receivedData.progress);
|
||||||
|
const progressFixed = progress.toFixed(2);
|
||||||
|
console.log('progress', progress);
|
||||||
|
uploadProgress?.set(progress, { ...receivedData, taskId, progressFixed });
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function (event) {
|
||||||
|
console.log('eventSource.onerror', event);
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
51
src/query/query-upload/query-upload-browser.ts
Normal file
51
src/query/query-upload/query-upload-browser.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { UploadProgress, UploadProgressData } from './core/upload-progress.ts';
|
||||||
|
import { uploadFileChunked } from './core/upload-chunk.ts';
|
||||||
|
import { toFile, uploadFiles, randomId } from './query-upload.ts';
|
||||||
|
|
||||||
|
export { toFile, randomId };
|
||||||
|
export { uploadFiles, uploadFileChunked, UploadProgress };
|
||||||
|
|
||||||
|
type UploadFileProps = {
|
||||||
|
onStart?: () => void;
|
||||||
|
onDone?: () => void;
|
||||||
|
onProgress?: (progress: number, data: UploadProgressData) => void;
|
||||||
|
onSuccess?: (res: any) => void;
|
||||||
|
onError?: (err: any) => void;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
export type ConvertOpts = {
|
||||||
|
appKey?: string;
|
||||||
|
version?: string;
|
||||||
|
username?: string;
|
||||||
|
directory?: string;
|
||||||
|
isPublic?: boolean;
|
||||||
|
filename?: string;
|
||||||
|
/**
|
||||||
|
* 是否不检查应用文件, 默认 true,默认不检测
|
||||||
|
*/
|
||||||
|
noCheckAppFiles?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadChunk = async (file: File, opts: ConvertOpts, props?: UploadFileProps) => {
|
||||||
|
const uploadProgress = new UploadProgress({
|
||||||
|
onStart: function () {
|
||||||
|
props?.onStart?.();
|
||||||
|
},
|
||||||
|
onDone: () => {
|
||||||
|
props?.onDone?.();
|
||||||
|
},
|
||||||
|
onProgress: (progress, data) => {
|
||||||
|
props?.onProgress?.(progress, data!);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await uploadFileChunked(file, opts, {
|
||||||
|
uploadProgress,
|
||||||
|
token: props?.token!,
|
||||||
|
createEventSource: (url: string, searchParams: URLSearchParams) => {
|
||||||
|
return new EventSource(url + '?' + searchParams.toString());
|
||||||
|
},
|
||||||
|
FormDataFn: FormData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
1
src/query/query-upload/query-upload-node.ts
Normal file
1
src/query/query-upload/query-upload-node.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// console.log('upload)
|
11
src/query/query-upload/query-upload.ts
Normal file
11
src/query/query-upload/query-upload.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { uploadFiles } from './core/upload.ts';
|
||||||
|
|
||||||
|
import { uploadFileChunked } from './core/upload-chunk.ts';
|
||||||
|
import { UploadProgress } from './core/upload-progress.ts';
|
||||||
|
|
||||||
|
export { uploadFiles, uploadFileChunked, UploadProgress };
|
||||||
|
|
||||||
|
export * from './utils/to-file.ts';
|
||||||
|
export { randomId } from './utils/random-id.ts';
|
||||||
|
|
||||||
|
export { filterFiles } from './utils/filter-files.ts';
|
23
src/query/query-upload/utils/filter-files.ts
Normal file
23
src/query/query-upload/utils/filter-files.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 过滤文件, 过滤 .DS_Store, node_modules, 以.开头的文件, 过滤 __开头的文件
|
||||||
|
* @param files
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const filterFiles = (files: File[]) => {
|
||||||
|
files = files.filter((file) => {
|
||||||
|
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 过滤node_modules
|
||||||
|
if (file.webkitRelativePath.includes('node_modules')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 过滤文件 .DS_Store
|
||||||
|
if (file.name === '.DS_Store') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 过滤以.开头的文件
|
||||||
|
return !file.name.startsWith('.');
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
};
|
3
src/query/query-upload/utils/index.ts
Normal file
3
src/query/query-upload/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './to-file.ts';
|
||||||
|
export * from './filter-files.ts';
|
||||||
|
export * from './random-id.ts';
|
3
src/query/query-upload/utils/random-id.ts
Normal file
3
src/query/query-upload/utils/random-id.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const randomId = () => {
|
||||||
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
};
|
105
src/query/query-upload/utils/to-file.ts
Normal file
105
src/query/query-upload/utils/to-file.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const getFileExtension = (filename: string) => {
|
||||||
|
return filename.split('.').pop();
|
||||||
|
};
|
||||||
|
const getFileType = (extension: string) => {
|
||||||
|
switch (extension) {
|
||||||
|
case 'js':
|
||||||
|
return 'text/javascript';
|
||||||
|
case 'css':
|
||||||
|
return 'text/css';
|
||||||
|
case 'html':
|
||||||
|
return 'text/html';
|
||||||
|
case 'json':
|
||||||
|
return 'application/json';
|
||||||
|
case 'png':
|
||||||
|
return 'image/png';
|
||||||
|
case 'jpg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case 'jpeg':
|
||||||
|
return 'image/jpeg';
|
||||||
|
case 'gif':
|
||||||
|
return 'image/gif';
|
||||||
|
case 'svg':
|
||||||
|
return 'image/svg+xml';
|
||||||
|
case 'webp':
|
||||||
|
return 'image/webp';
|
||||||
|
case 'ico':
|
||||||
|
return 'image/x-icon';
|
||||||
|
default:
|
||||||
|
return 'text/plain';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const checkIsBase64 = (content: string) => {
|
||||||
|
return content.startsWith('data:');
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 获取文件的目录和文件名
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns 目录和文件名
|
||||||
|
*/
|
||||||
|
export const getDirectoryAndName = (filename: string) => {
|
||||||
|
if (!filename) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (filename.startsWith('.')) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
filename = filename.replace(/^\/+/, ''); // Remove all leading slashes
|
||||||
|
}
|
||||||
|
const hasDirectory = filename.includes('/');
|
||||||
|
if (!hasDirectory) {
|
||||||
|
return { directory: '', name: filename };
|
||||||
|
}
|
||||||
|
const parts = filename.split('/');
|
||||||
|
const name = parts.pop()!; // Get the last part as the file name
|
||||||
|
const directory = parts.join('/'); // Join the remaining parts as the directory
|
||||||
|
return { directory, name };
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 把字符串转为文件流,并返回文件流,根据filename的扩展名,自动设置文件类型.
|
||||||
|
* 当不是文本类型,自动需要把base64的字符串转为blob
|
||||||
|
* @param content 字符串
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns 文件流
|
||||||
|
*/
|
||||||
|
export const toFile = (content: string, filename: string) => {
|
||||||
|
// 如果文件名是 a/d/a.js 格式的,则需要把d作为目录,a.js作为文件名
|
||||||
|
const directoryAndName = getDirectoryAndName(filename);
|
||||||
|
if (!directoryAndName) {
|
||||||
|
throw new Error('Invalid filename');
|
||||||
|
}
|
||||||
|
const { name } = directoryAndName;
|
||||||
|
const extension = getFileExtension(name);
|
||||||
|
if (!extension) {
|
||||||
|
throw new Error('Invalid filename');
|
||||||
|
}
|
||||||
|
const isBase64 = checkIsBase64(content);
|
||||||
|
const type = getFileType(extension);
|
||||||
|
|
||||||
|
if (isBase64) {
|
||||||
|
// Decode base64 string
|
||||||
|
const base64Data = content.split(',')[1]; // Remove the data URL prefix
|
||||||
|
const byteCharacters = atob(base64Data);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
const blob = new Blob([byteArray], { type });
|
||||||
|
return new File([blob], filename, { type });
|
||||||
|
} else {
|
||||||
|
const blob = new Blob([content], { type });
|
||||||
|
return new File([blob], filename, { type });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把字符串转为文本文件
|
||||||
|
* @param content 字符串
|
||||||
|
* @param filename 文件名
|
||||||
|
* @returns 文件流
|
||||||
|
*/
|
||||||
|
export const toTextFile = (content: string = 'keep directory exist', filename: string = 'keep.txt') => {
|
||||||
|
const file = toFile(content, filename);
|
||||||
|
return file;
|
||||||
|
};
|
Submodule submodules/query-login deleted from f8af24506b
Submodule submodules/query-mark updated: 757be9fc2f...f12e6896a9
Submodule submodules/store updated: 351b2e3366...bbf826b765
@@ -2,8 +2,9 @@ import { defineConfig } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import basicSsl from '@vitejs/plugin-basic-ssl';
|
import basicSsl from '@kevisual/ssl';
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const centerEnv = process.env.CENTER_ENV;
|
||||||
const plugins: any[] = [basicSsl()];
|
const plugins: any[] = [basicSsl()];
|
||||||
plugins.push(tailwindcss());
|
plugins.push(tailwindcss());
|
||||||
const devBackend = 'https://kevisual.silkyai.cn';
|
const devBackend = 'https://kevisual.silkyai.cn';
|
||||||
@@ -13,7 +14,7 @@ const meBackend = 'https://kevisual.xiongxiao.me';
|
|||||||
const backendWss = devBackend.replace(/^https:/, 'wss:');
|
const backendWss = devBackend.replace(/^https:/, 'wss:');
|
||||||
const backend = meBackend;
|
const backend = meBackend;
|
||||||
let proxy = {};
|
let proxy = {};
|
||||||
if (false) {
|
if (true) {
|
||||||
proxy = {
|
proxy = {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: backend,
|
target: backend,
|
||||||
@@ -48,16 +49,18 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
|
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
|
||||||
|
CENTER_ENV: JSON.stringify(centerEnv),
|
||||||
},
|
},
|
||||||
base: isDev ? '/' : '/root/center/',
|
base: isDev ? '/' : '/root/center/',
|
||||||
build: {
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// 控制输出
|
// 控制输出
|
||||||
// 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果
|
// 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果
|
||||||
assetFileNames: (chunkInfo) => {
|
assetFileNames: (chunkInfo) => {
|
||||||
console.log(chunkInfo.names);
|
console.log(chunkInfo.names);
|
||||||
if (chunkInfo.names?.includes('panda.png')) {
|
if (chunkInfo.names?.includes('panda.jpg')) {
|
||||||
return '[name].[ext]';
|
return '[name].[ext]';
|
||||||
}
|
}
|
||||||
const qrcode = ['qrcode-8x8.jpg'];
|
const qrcode = ['qrcode-8x8.jpg'];
|
||||||
@@ -67,6 +70,20 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
return '[name].[hash].[ext]';
|
return '[name].[hash].[ext]';
|
||||||
},
|
},
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
if (id.includes('react')) {
|
||||||
|
return 'vendor-react';
|
||||||
|
}
|
||||||
|
// prettier
|
||||||
|
if (id.includes('prettier')) {
|
||||||
|
return 'vendor-prettier';
|
||||||
|
}
|
||||||
|
// codemirror
|
||||||
|
// console.log(id);
|
||||||
|
return 'vendor';
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user