This commit is contained in:
xion 2025-04-03 19:29:57 +08:00
parent 521255c1af
commit 9add82dc7e
28 changed files with 749 additions and 267 deletions

View File

@ -20,12 +20,14 @@
"@ant-design/icons": "^6.0.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@kevisual/cache": "^0.0.1",
"@kevisual/codemirror": "workspace:*",
"@kevisual/components": "workspace:*",
"@kevisual/container": "1.0.0",
"@kevisual/query": "^0.0.15",
"@kevisual/query-config": "workspace:*",
"@kevisual/query-login": "workspace:*",
"@kevisual/query-upload": "workspace:*",
"@kevisual/resources": "workspace:*",
"@monaco-editor/react": "^4.7.0",
"@mui/material": "^7.0.1",
@ -47,6 +49,7 @@
"qrcode": "^1.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.55.0",
"react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7",

View File

@ -20,6 +20,7 @@ type BaseEditorOpts = {
open?: boolean;
overrideList?: Completion[];
};
onChange?: (value: string) => void;
};
export class BaseEditor {
editor?: EditorView;
@ -32,7 +33,7 @@ export class BaseEditor {
open?: boolean;
overrideList?: Completion[];
};
onChange?: (value: string) => void;
constructor(opts?: BaseEditorOpts) {
this.filename = opts?.filename || '';
this.language = opts?.language || 'typescript';
@ -43,6 +44,7 @@ export class BaseEditor {
}
this.autoComplete = opts?.autoComplete || { open: true, overrideList: [] };
this.onChangeCompartment = new Compartment(); // 创建一个用于 onChange 的独立扩展
this.onChange = opts?.onChange;
}
onEditorChange(view: EditorView) {
console.log('onChange', view);
@ -54,6 +56,7 @@ export class BaseEditor {
this.el = el;
const onChangeCompartment = this.onChangeCompartment!;
const language = this.language;
const onChange = this.onChange;
const extensions: Extension[] = [
vscodeLight,
formatKeymap,
@ -91,7 +94,15 @@ export class BaseEditor {
parent: el,
extensions: [
...extensions, //
onChangeCompartment.of([]),
onChangeCompartment.of([
[
EditorView.updateListener.of((update) => {
if (update.changes.length > 0) {
onChange?.(update.state.doc.toString());
}
}),
],
]),
],
});
}

View File

@ -3,7 +3,7 @@ export { PermissionManager } from './pages/file/modules/PermissionManager.tsx';
export { PermissionModal, usePermissionModal } from './pages/file/modules/PermissionModal.tsx';
export { iText } from './i-text/index.ts';
export { uploadFiles, uploadFileChunked, getDirectoryAndName, toFile, createDirectory } from './pages/upload/app';
export { uploadFiles, uploadFileChunked, getDirectoryAndName, createDirectory } from './pages/upload/app';
export { DialogDirectory, DialogDeleteDirectory } from './pages/upload/DialogDirectory';
export { uploadChunkV2 } from './pages/upload/v2/upload-chunk';
export { uploadChunkV2, toFile } from './pages/upload/v2/upload-chunk';

View File

@ -4,7 +4,7 @@ import { Id, toast } from 'react-toastify';
import { nanoid } from 'nanoid';
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
import { uploadFileChunked, UploadProgress } from '@kevisual/query-upload/query-upload';
import { toFile } from '@kevisual/query-upload/query-upload';
export type ConvertOpts = {
appKey?: string;
version?: string;
@ -12,6 +12,10 @@ export type ConvertOpts = {
directory?: string;
isPublic?: boolean;
filename?: string;
/**
* , true
*/
noCheckAppFiles?: boolean;
};
export const uploadChunkV2 = async (file: File, opts: ConvertOpts) => {
@ -53,3 +57,4 @@ export const uploadChunkV2 = async (file: File, opts: ConvertOpts) => {
});
return result;
};
export { toFile };

197
pnpm-lock.yaml generated
View File

@ -17,6 +17,9 @@ importers:
'@emotion/styled':
specifier: ^11.14.0
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
'@kevisual/cache':
specifier: ^0.0.1
version: 0.0.1(rollup@4.37.0)(tslib@2.8.1)(typescript@5.8.2)
'@kevisual/codemirror':
specifier: workspace:*
version: link:packages/codemirror
@ -35,6 +38,9 @@ importers:
'@kevisual/query-login':
specifier: workspace:*
version: link:submodules/query-login
'@kevisual/query-upload':
specifier: workspace:*
version: link:submodules/query-upload
'@kevisual/resources':
specifier: workspace:*
version: link:packages/resources
@ -98,6 +104,9 @@ importers:
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
react-dropzone:
specifier: ^14.3.8
version: 14.3.8(react@19.1.0)
react-hook-form:
specifier: ^7.55.0
version: 7.55.0(react@19.1.0)
@ -339,29 +348,29 @@ importers:
dependencies:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/react@19.1.0)(react@19.0.0)
version: 11.14.0(@types/react@19.1.0)(react@19.1.0)
'@emotion/styled':
specifier: ^11.14.0
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)
version: 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
'@mui/material':
specifier: ^6.4.7
version: 6.4.7(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: ^7.0.1
version: 7.0.1(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react:
specifier: 19.0.0
version: 19.0.0
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
react-hook-form:
specifier: ^7.54.2
version: 7.54.2(react@19.0.0)
specifier: ^7.55.0
version: 7.55.0(react@19.1.0)
devDependencies:
clsx:
specifier: ^2.1.1
version: 2.1.1
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
specifier: ^3.1.0
version: 3.1.0
packages/kevisual-official:
dependencies:
@ -1548,35 +1557,12 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@mui/core-downloads-tracker@6.4.7':
resolution: {integrity: sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==}
'@mui/core-downloads-tracker@6.4.8':
resolution: {integrity: sha512-vjP4+A1ybyCRhDZC7r5EPWu/gLseFZxaGyPdDl94vzVvk6Yj6gahdaqcjbhkaCrJjdZj90m3VioltWPAnWF/zw==}
'@mui/core-downloads-tracker@7.0.1':
resolution: {integrity: sha512-T5DNVnSD9pMbj4Jk/Uphz+yvj9dfpl2+EqsOuJtG12HxEihNG5pd3qzX5yM1Id4dDwKRvM3dPVcxyzavTFhJeA==}
'@mui/material@6.4.7':
resolution: {integrity: sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@mui/material-pigment-css': ^6.4.7
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/material-pigment-css':
optional: true
'@types/react':
optional: true
'@mui/material@6.4.8':
resolution: {integrity: sha512-5S9UTjKZZBd9GfbcYh/nYfD9cv6OXmj5Y7NgKYfk7JcSoshp8/pW5zP4wecRiroBSZX8wcrywSgogpVNO+5W0Q==}
engines: {node: '>=14.0.0'}
@ -1617,16 +1603,6 @@ packages:
'@types/react':
optional: true
'@mui/private-theming@6.4.6':
resolution: {integrity: sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/private-theming@6.4.8':
resolution: {integrity: sha512-sWwQoNSn6elsPTAtSqCf+w5aaGoh7AASURNmpy+QTTD/zwJ0Jgwt0ZaaP6mXq2IcgHxYnYloM/+vJgHPMkRKTQ==}
engines: {node: '>=14.0.0'}
@ -1647,19 +1623,6 @@ packages:
'@types/react':
optional: true
'@mui/styled-engine@6.4.6':
resolution: {integrity: sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/styled-engine@6.4.8':
resolution: {integrity: sha512-oyjx1b1FvUCI85ZMO4trrjNxGm90eLN3Ohy0AP/SqK5gWvRQg1677UjNf7t6iETOKAleHctJjuq0B3aXO2gtmw==}
engines: {node: '>=14.0.0'}
@ -1686,22 +1649,6 @@ packages:
'@emotion/styled':
optional: true
'@mui/system@6.4.7':
resolution: {integrity: sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/system@6.4.8':
resolution: {integrity: sha512-gV7iBHoqlsIenU2BP0wq14BefRoZcASZ/4LeyuQglayBl+DfLX5rEd3EYR3J409V2EZpR0NOM1LATAGlNk2cyA==}
engines: {node: '>=14.0.0'}
@ -1734,14 +1681,6 @@ packages:
'@types/react':
optional: true
'@mui/types@7.2.21':
resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/types@7.2.24':
resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==}
peerDependencies:
@ -1758,16 +1697,6 @@ packages:
'@types/react':
optional: true
'@mui/utils@6.4.6':
resolution: {integrity: sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@6.4.8':
resolution: {integrity: sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ==}
engines: {node: '>=14.0.0'}
@ -6280,33 +6209,10 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@mui/core-downloads-tracker@6.4.7': {}
'@mui/core-downloads-tracker@6.4.8': {}
'@mui/core-downloads-tracker@7.0.1': {}
'@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.0
'@mui/core-downloads-tracker': 6.4.7
'@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)
'@mui/types': 7.2.21(@types/react@19.1.0)
'@mui/utils': 6.4.6(@types/react@19.1.0)(react@19.0.0)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@19.1.0)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
react-is: 19.1.0
react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)
'@types/react': 19.1.0
'@mui/material@6.4.8(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
@ -6370,15 +6276,6 @@ snapshots:
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
'@types/react': 19.1.0
'@mui/private-theming@6.4.6(@types/react@19.1.0)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
'@mui/utils': 6.4.8(@types/react@19.1.0)(react@19.0.0)
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@types/react': 19.1.0
'@mui/private-theming@6.4.8(@types/react@19.0.12)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
@ -6406,19 +6303,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.0
'@mui/styled-engine@6.4.6(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0
csstype: 3.1.3
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)
'@mui/styled-engine@6.4.8(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
@ -6458,22 +6342,6 @@ snapshots:
'@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.1.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
'@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
'@mui/private-theming': 6.4.6(@types/react@19.1.0)(react@19.0.0)
'@mui/styled-engine': 6.4.6(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0))(react@19.0.0)
'@mui/types': 7.2.24(@types/react@19.1.0)
'@mui/utils': 6.4.8(@types/react@19.1.0)(react@19.0.0)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 19.0.0
optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.1.0)(react@19.0.0)
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.0.0))(@types/react@19.1.0)(react@19.0.0)
'@types/react': 19.1.0
'@mui/system@6.4.8(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
@ -6522,10 +6390,6 @@ snapshots:
'@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.0)(react@19.1.0))(@types/react@19.1.0)(react@19.1.0)
'@types/react': 19.1.0
'@mui/types@7.2.21(@types/react@19.1.0)':
optionalDependencies:
'@types/react': 19.1.0
'@mui/types@7.2.24(@types/react@19.0.12)':
optionalDependencies:
'@types/react': 19.0.12
@ -6540,18 +6404,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.0
'@mui/utils@6.4.6(@types/react@19.1.0)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
'@mui/types': 7.2.24(@types/react@19.1.0)
'@types/prop-types': 15.7.14
clsx: 2.1.1
prop-types: 15.8.1
react: 19.0.0
react-is: 19.1.0
optionalDependencies:
'@types/react': 19.1.0
'@mui/utils@6.4.8(@types/react@19.0.12)(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
@ -9603,6 +9455,13 @@ snapshots:
prop-types: 15.8.1
react: 19.0.0
react-dropzone@14.3.8(react@19.1.0):
dependencies:
attr-accept: 2.2.5
file-selector: 2.1.2
prop-types: 15.8.1
react: 19.1.0
react-hook-form@7.54.2(react@19.0.0):
dependencies:
react: 19.0.0

View File

@ -87,5 +87,9 @@
"delete_directory_success": "Delete directory success",
"create_directory": "Create Directory",
"create_directory_success": "Create directory success",
"Apply Change Name": "Apply Change Name"
"Apply Change Name": "Apply Change Name",
"Tips": "Tips",
"Check username introduction": "Check your username, if the username is not available, you can contact the customer service to modify.",
"Delete and remove file": "Delete and remove file",
"Delete App Introduce": "Delete App, when you click the delete button, the file will be deleted from the file management system."
}

View File

@ -87,5 +87,9 @@
"delete_directory_success": "删除目录成功",
"create_directory": "创建目录",
"create_directory_success": "创建目录成功",
"Apply Change Name": "申请修改名称"
"Apply Change Name": "申请修改名称",
"Tips": "提示",
"Check username introduction": "查看是否可以取名,如果当前用户名已存在,请更换其他用户名。其中用户名以@开头",
"Delete and remove file": "删除并删除文件",
"Delete App Introduce": "删除应用, 当点击同时删除文件,会把文件管理中,应用相关的存储的资源同步删除。"
}

View File

@ -8,6 +8,7 @@ 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 { basename } from './modules/basename';
import { Redirect } from './modules/Redirect';
import { CustomThemeProvider } from '@kevisual/components/theme/index.tsx';
@ -82,6 +83,7 @@ export const App = () => {
<Route path='/file/*' element={<FileApp />} />
<Route path='/pay/*' element={<PayApp />} />
<Route path='/domain/*' element={<DomainApp />} />
<Route path='/home/*' element={<HomeApp />} />
<Route path='/404' element={<div>404</div>} />
<Route path='*' element={<div>404</div>} />
</Routes>

View File

@ -12,6 +12,7 @@ import { useNewNavigate } from '../navicate';
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { useQuickMenu } from './Menu';
export const LayoutUser = () => {
const { open, setOpen, isAdmin, ...store } = useLayoutStore(
@ -77,7 +78,6 @@ export const LayoutUser = () => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

View File

@ -12,14 +12,29 @@ import SmileOutlined from '@ant-design/icons/SmileOutlined';
import { X, Settings } from 'lucide-react';
import { useNewNavigate } from '../navicate';
import { useTranslation } from 'react-i18next';
import { Map } from 'lucide-react';
export const useQuickMenu = () => {
const { t } = useTranslation();
return [
{
title: t('User App'),
icon: <AppstoreOutlined />,
link: '/app/edit/list',
},
{
title: t('File App'),
icon: <FolderOutlined />,
link: '/file/edit/list',
},
];
};
export const LayoutMenu = () => {
const { t } = useTranslation();
const meun = [
{
title: t('Home'),
icon: <HomeOutlined />,
link: '/map',
link: '/home',
},
{
title: t('User App'),
@ -37,6 +52,7 @@ export const LayoutMenu = () => {
link: '/container/edit/list',
},
{ title: t('Config'), icon: <Settings size={16} />, link: '/config/edit/list' },
{ title: t('Map'), icon: <Map size={16} />, link: '/map' },
{
title: t('About'),
icon: <SmileOutlined />,

View File

@ -1,7 +1,7 @@
import { MenuOutlined, SwapOutlined } from '@ant-design/icons';
import { Tooltip } from '@mui/material';
import { Outlet } from 'react-router-dom';
import { LayoutMenu } from './Menu';
import { Outlet, useNavigate } from 'react-router-dom';
import { LayoutMenu, useQuickMenu } from './Menu';
import { useLayoutStore, usePlatformStore } from './store';
import { useShallow } from 'zustand/react/shallow';
import { useEffect, useLayoutEffect, useState } from 'react';
@ -46,6 +46,8 @@ export const LayoutMain = (props: LayoutMainProps) => {
}),
);
const { isMac, mount, isElectron } = platformStore;
const navigate = useNavigate();
const quickMenu = useQuickMenu();
useLayoutEffect(() => {
platformStore.init();
@ -88,7 +90,26 @@ export const LayoutMain = (props: LayoutMainProps) => {
<MenuOutlined />
</IconButton>
<div className='flex grow justify-between pl-4 items-center'>
{props.title}
<div className='flex items-center gap-2'>
<div className='text-xl font-bold'>{props.title}</div>
<div className='ml-4 flex items-center gap-2 text-sm '>
{quickMenu.map((item, index) => {
const isActive = location.pathname.includes(item.link);
return (
<div
key={index}
className={clsx('flex items-center gap-2 px-1', isActive && 'border border-white')}
onClick={() => {
navigate(item.link);
}}>
<Tooltip title={item.title}>
<div className='cursor-pointer'>{item.icon}</div>
</Tooltip>
</div>
);
})}
</div>
</div>
<div className='mr-4 flex gap-4 items-center no-drag'>
<div className='group relative'>
<IconButton>

View File

@ -64,8 +64,9 @@ export type LayoutStore = {
switchOrg: (username?: string, type?: 'user' | 'org') => Promise<void>;
isAdmin: boolean;
setIsAdmin: (isAdmin: boolean) => void;
checkHasOrg: () => boolean;
};
export const useLayoutStore = create<LayoutStore>((set) => ({
export const useLayoutStore = create<LayoutStore>((set, get) => ({
open: false,
setOpen: (open) => set({ open }),
me: {},
@ -92,4 +93,11 @@ export const useLayoutStore = create<LayoutStore>((set) => ({
},
isAdmin: false,
setIsAdmin: (isAdmin) => set({ isAdmin }),
checkHasOrg: () => {
const user = get().me || {};
if (!user.orgs) {
return false;
}
return user?.orgs?.length > 0;
},
}));

View File

@ -8,7 +8,6 @@ import FileOutlined from '@ant-design/icons/FileOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { useModal } from '@kevisual/components/modal/Confirm.tsx';
import { Tooltip } from '@mui/material';
import { isObjectNull } from '@/utils/is-null';
import { FileUpload } from '../modules/FileUpload';
@ -22,6 +21,7 @@ import { IconButton } from '@kevisual/components/button/index.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
const FormModal = () => {
const { t } = useTranslation();
@ -108,8 +108,14 @@ export const AppVersionList = () => {
};
}),
);
const appDeleteModalStore = useAppDeleteModalStore(
useShallow((state) => {
return {
onClickDelete: state.onClickDelete,
};
}),
);
const navigate = useNewNavigate();
const [modal, contextHolder] = useModal();
const [isUpload, setIsUpload] = useState(false);
useEffect(() => {
// fetch app version list
@ -178,13 +184,7 @@ export const AppVersionList = () => {
<Tooltip title='Delete'>
<Button
onClick={(e) => {
modal.confirm({
title: 'Delete',
content: 'Are you sure delete this data?',
onOk: () => {
versionStore.deleteData(item.id);
},
});
appDeleteModalStore.onClickDelete('app-version', item);
e.stopPropagation();
}}>
<DeleteOutlined />
@ -229,7 +229,6 @@ export const AppVersionList = () => {
</div>
</div>
</div>
{contextHolder}
<div className='shark h-full'>
{isUpload && (
<div className='bg-white p-2 w-[600px] h-full flex flex-col'>
@ -249,6 +248,7 @@ export const AppVersionList = () => {
)}
</div>
<FormModal />
<AppDeleteModal />
</div>
);
};
@ -268,8 +268,8 @@ export const AppVersionFile = () => {
return (
<>
<div>version: {versionStore.formData.version}</div>
<div className='border rounded-md my-2 grow overflow-hidden'>
<div className='flex gap-2 items-center border-b py-2 px-2'>
<div className='border border-gray-200 rounded-md my-2 grow overflow-hidden'>
<div className='flex gap-2 items-center border-b border-b-gray-200 py-2 px-2'>
Files
<FileUpload />
</div>
@ -284,7 +284,7 @@ export const AppVersionFile = () => {
const _path = file.path || '';
const path = _path.replace(prefix, '');
return (
<div className='flex gap-2 px-4 py-2 border-b' key={index}>
<div className='flex gap-2 px-4 py-2 border-b border-b-gray-200' key={index}>
{/* <div className='w-[100px] truncate'>{file.name}</div> */}
<div>
<FileOutlined />

View File

@ -1,15 +1,11 @@
import { useShallow } from 'zustand/react/shallow';
import { useUserAppStore } from '../store';
import { useAppVersionStore, useUserAppStore } from '../store';
import { useEffect, useMemo, useState } from 'react';
import { useModal } from '@kevisual/components/modal/Confirm.tsx';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined';
import CodeOutlined from '@ant-design/icons/CodeOutlined';
import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined';
import { FormControlLabel, Switch, useTheme } from '@mui/material';
import { isObjectNull } from '@/utils/is-null';
import { queryLogin, useNewNavigate } from '@/modules';
@ -29,6 +25,8 @@ import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
import copy from 'copy-to-clipboard';
import { useLayoutStore } from '@/modules/layout/store';
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
import { AppWindow, Edit, Link, RefreshCcw, Share2, Trash } from 'lucide-react';
const FormModal = () => {
const defaultValues = {
@ -57,7 +55,6 @@ const FormModal = () => {
const open = containerStore.showEdit;
if (open) {
const isNull = isObjectNull(containerStore.userApp);
console.log('isNull', containerStore.userApp);
if (isNull) {
reset(defaultValues);
} else {
@ -152,7 +149,7 @@ const ShareModal = () => {
const permission = containerStore.userApp?.data?.permission || {};
const runtime = containerStore.userApp?.data?.runtime || [];
if (isObjectNull(permission)) {
setPermission(null);
setPermission({ share: 'private' });
} else {
setPermission(permission);
}
@ -173,9 +170,9 @@ const ShareModal = () => {
containerStore.setShowEdit(false);
};
const { t } = useTranslation();
console.log('runtime', runtime);
const theme = useTheme();
const defaultProps = theme.components?.MuiTextField?.defaultProps as any;
const isAdmin = useLayoutStore(useShallow((state) => state.isAdmin));
return (
<Dialog
open={containerStore.showEdit}
@ -191,6 +188,7 @@ const ShareModal = () => {
setPermission(value);
}}
/>
{isAdmin && (
<FormControlLabel
label={t('app.runtime')}
labelPlacement='top'
@ -220,6 +218,7 @@ const ShareModal = () => {
/>
}
/>
)}
</div>
</DialogContent>
<DialogActions>
@ -250,6 +249,20 @@ export const List = () => {
};
}),
);
const appVersionStore = useAppVersionStore(
useShallow((state) => {
return {
publishVersion: state.publishVersion,
};
}),
);
const appDeleteModalStore = useAppDeleteModalStore(
useShallow((state) => {
return {
onClickDelete: state.onClickDelete,
};
}),
);
const navicate = useNewNavigate();
useEffect(() => {
userAppStore.getList();
@ -299,11 +312,22 @@ export const List = () => {
{userAppStore.list.map((item) => {
const isRunning = item.status === 'running';
const hasDescription = !!item.description;
const content = marked.parse(item.description);
// const content = marked.parse(item.description);
const content = item.description;
return (
<div className='card w-[300px] relative pb-10 ' key={item.id}>
<div className='card-title flex font-bold justify-between' onClick={() => {}}>
{item.title}
<Tooltip
title={
<pre className=''>
<span className='text-sm'>{item.title}</span>
<i className='text-xs text-white ml-4'>{item.key}</i>
</pre>
}>
<div>
{item.title} <i className='text-xs text-gray-400'>{item.key}</i>
</div>
</Tooltip>
<div>
<Tooltip title={isRunning ? '网页可正常访问' : '网页被关闭'}>
<div className={`${isRunning ? 'bg-green-500' : 'bg-red-500'} w-4 h-4 rounded-full`}></div>
@ -329,8 +353,10 @@ export const List = () => {
<div className='text-xs'>
{t('app.version')}: {item.version}
</div>
<div className={clsx('text-sm border border-gray-200 p-2 max-h-[140px] scrollbar my-1', !hasDescription && 'hidden')}>
<div dangerouslySetInnerHTML={{ __html: content }}></div>
{/* <div dangerouslySetInnerHTML={{ __html: content }}></div> */}
<div className='text-sm whitespace-pre-wrap'>{content}</div>
</div>
</div>
<div className='py-4'></div>
@ -346,7 +372,7 @@ export const List = () => {
userAppStore.setFormData(item);
userAppStore.setShowEdit(true);
}}>
<EditOutlined />
<Edit size={16} />
</Button>
</Tooltip>
<Tooltip title={'App Version List'}>
@ -354,7 +380,7 @@ export const List = () => {
onClick={() => {
navicate(`/app/${item.key}/version/list`);
}}>
<UnorderedListOutlined />
<AppWindow size={16} />
</Button>
</Tooltip>
<Tooltip title={iText.share.tips}>
@ -364,7 +390,15 @@ export const List = () => {
userAppStore.setFormData(item);
userAppStore.setShowShareEdit(true);
}}>
<ShareAltOutlined />
<Share2 size={16} />
</Button>
</Tooltip>
<Tooltip title={'reload'}>
<Button
onClick={() => {
appVersionStore.publishVersion({ appKey: item.key, version: item.version }, { showToast: false });
}}>
<RefreshCcw size={16} />
</Button>
</Tooltip>
<Tooltip title={'To App'}>
@ -396,23 +430,16 @@ export const List = () => {
message.error('The app is not running');
}
}}>
<LinkOutlined />
<Link size={16} />
</Button>
</Tooltip>
<Tooltip title={'Delete'}>
<Button
onClick={(e) => {
console.log('delete', item);
modal.confirm({
title: 'Delete',
content: 'Are you sure delete this data?',
onOk: () => {
userAppStore.deleteData(item.id);
},
});
appDeleteModalStore.onClickDelete('user-app', item);
e.stopPropagation();
}}>
<DeleteOutlined />
<Trash size={16} />
</Button>
</Tooltip>
</ButtonGroup>
@ -427,6 +454,7 @@ export const List = () => {
{contextHolder}
<FormModal />
<ShareModal />
<AppDeleteModal />
</div>
);
};

View File

@ -0,0 +1,79 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { create } from 'zustand';
import { useAppVersionStore, useUserAppStore } from '../store';
import { useShallow } from 'zustand/shallow';
type AppDeleteModalStore = {
open: boolean;
setOpen: (open: boolean) => void;
app: any;
setApp: (app: any) => void;
type: 'user-app' | 'app-version';
setType: (type: 'user-app' | 'app-version') => void;
onClickDelete: (type: 'user-app' | 'app-version', data: any) => void;
};
export const useAppDeleteModalStore = create<AppDeleteModalStore>((set) => ({
open: false,
setOpen: (open) => set({ open }),
app: null,
setApp: (app) => set({ app }),
type: 'user-app',
setType: (type) => set({ type }),
onClickDelete: (type, data) => {
set({ open: true, type, app: data });
},
}));
export const AppDeleteModal = () => {
const { t } = useTranslation();
const { open, setOpen, app, type, setType } = useAppDeleteModalStore();
const userAppStore = useUserAppStore(
useShallow((state) => {
return {
deleteData: state.deleteData,
};
}),
);
const appVersionStore = useAppVersionStore(
useShallow((state) => {
return {
deleteData: state.deleteData,
};
}),
);
const onClose = () => {
setOpen(false);
};
const onDelete = (deleteFile = false) => {
if (type === 'user-app') {
userAppStore.deleteData(app.id, deleteFile);
} else {
appVersionStore.deleteData(app.id, deleteFile);
}
setOpen(false);
};
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t('Tips')}</DialogTitle>
<DialogContent>
<div className='w-[400px]'>
<p className='text-sm text-gray-500'>{t('Delete App Introduce')}</p>
</div>
</DialogContent>
<DialogActions>
<Button variant='contained' onClick={() => onDelete()}>{t('Delete')}</Button>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button
color='error'
onClick={() => {
onDelete(true);
}}>
{t('Delete and remove file')}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -17,8 +17,14 @@ type AppVersionStore = {
app: any;
getApp: (key: string, force?: boolean) => Promise<void>;
updateData: (data: any) => Promise<void>;
deleteData: (id: string) => Promise<void>;
publishVersion: (data: any) => Promise<void>;
/**
*
* @param id id
* @param deleteFile
* @returns
*/
deleteData: (id: string, deleteFile?: boolean) => Promise<void>;
publishVersion: (data: { id?: string; appKey?: string; version?: string }, opts?: { showToast?: boolean }) => Promise<any>;
};
export const useAppVersionStore = create<AppVersionStore>((set, get) => {
return {
@ -94,12 +100,13 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => {
message.error(res.message || 'Request failed');
}
},
deleteData: async (id) => {
deleteData: async (id, deleteFile = false) => {
const { getList } = get();
const res = await query.post({
path: 'app',
key: 'delete',
id,
deleteFile,
});
if (res.code === 200) {
getList();
@ -108,18 +115,24 @@ export const useAppVersionStore = create<AppVersionStore>((set, get) => {
message.error(res.message || 'Request failed');
}
},
publishVersion: async (data) => {
publishVersion: async (data, opts) => {
const showToast = opts?.showToast ?? true;
const res = await query.post({
path: 'app',
key: 'publish',
data,
});
if (res.code === 200) {
if (showToast) {
message.success('Success');
get().getApp(get().key, true);
}
} else {
if (showToast) {
message.error(res.message || 'Request failed');
}
}
return res;
},
};
});

View File

@ -11,7 +11,13 @@ type UserAppStore = {
list: any[];
getList: () => Promise<void>;
updateData: (data: any) => Promise<void>;
deleteData: (id: string) => Promise<void>;
/**
*
* @param id id
* @param deleteFile
* @returns
*/
deleteData: (id: string, deleteFile?: boolean) => Promise<void>;
showShareEdit: boolean;
setShowShareEdit: (showShareEdit: boolean) => void;
userApp: any;
@ -56,12 +62,13 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
message.error(res.message || 'Request failed');
}
},
deleteData: async (id) => {
deleteData: async (id, deleteFile = false) => {
const { getList } = get();
const res = await query.post({
path: 'user-app',
key: 'delete',
id,
deleteFile,
});
if (res.code === 200) {
getList();

107
src/pages/home/Home.tsx Normal file
View File

@ -0,0 +1,107 @@
import CloudUploadOutlined from '@ant-design/icons/CloudUploadOutlined';
import { BaseEditor } from '@kevisual/codemirror/editor/editor.ts';
import { IconButton } from '@kevisual/components/button/index.tsx';
import { Button, Tooltip } from 'antd';
import { UploadIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { CacheWorkspace } from '@kevisual/cache';
import { useHomeStore } from './store/index.ts';
import { UploadModal } from './module/UploadModal.tsx';
import { useShallow } from 'zustand/shallow';
import { toast } from 'react-toastify';
import { SuccessModal } from './module/SuccessModal.tsx';
export const Home = () => {
const editorElRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<BaseEditor>(null);
const { initApp, setOpenUploadModal, setText, filename } = useHomeStore(
useShallow((state) => ({ initApp: state.initApp, setOpenUploadModal: state.setOpenUploadModal, setText: state.setText, filename: state.filename })),
);
const onDrop = (acceptedFiles) => {
console.log(acceptedFiles);
const file = acceptedFiles[0];
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
editorRef.current!.setContent(content);
};
reader.readAsText(file);
};
const { getRootProps, getInputProps } = useDropzone({ onDrop, accept: { 'text/html': ['.html'], 'text/javascript': ['.js'], 'text/css': ['.css'] } });
useEffect(() => {
initApp();
initEditor();
return () => {
if (editorRef.current) {
editorRef.current.destroyEditor();
}
};
}, []);
const initEditor = async () => {
if (!editorElRef.current) return;
const cache = new CacheWorkspace();
let cacheData = '';
try {
cacheData = (await cache.storage.get('html-editor')) || '';
} catch (error) {
console.error(error);
}
editorRef.current = new BaseEditor({
filename: filename || 'index.html',
onChange: (value) => {
cache.storage.set('html-editor', value);
},
});
editorRef.current.createEditor(editorElRef.current);
setTimeout(() => {
editorRef.current!.setContent(cacheData);
}, 300);
};
return (
<div className='flex flex-row w-full h-full'>
<div className='flex flex-col text-primary border-r border-gray-200 px-2 pt-2'>
<div>
<Tooltip title='部署应用' placement='right'>
<IconButton
onClick={() => {
const editorContent = editorRef.current?.getContent();
if (editorContent) {
setOpenUploadModal(true);
setText(editorContent);
} else {
toast.error('请先输入代码');
}
}}>
<CloudUploadOutlined />
</IconButton>
</Tooltip>
</div>
<div className=' mt-2' {...getRootProps()}>
<Tooltip title='点击上传html或者js文件 ' placement='right'>
<IconButton>
<UploadIcon size={16} />
</IconButton>
<input type='file' style={{ display: 'none' }} {...getInputProps()} />
</Tooltip>
</div>
</div>
<div className='h-full grow px-2'>
<div className=' py-1'>
<i className=' text-gray-500' style={{ fontSize: 10 }}>
{'>'} html小应用, html代码()
</i>
</div>
<div className=' border rounded-md border-gray-200 scrollbar' style={{ height: 'calc(100% - 50px)' }}>
<div className='w-full h-full ' ref={editorElRef}></div>
</div>
</div>
<UploadModal />
<SuccessModal />
</div>
);
};

12
src/pages/home/index.tsx Normal file
View File

@ -0,0 +1,12 @@
import { Route, Routes } from 'react-router-dom';
import { Main } from './layouts';
import { Home } from './Home';
export const App = () => {
return (
<Routes>
<Route element={<Main />}>
<Route path='/' element={<Home />}></Route>
</Route>
</Routes>
);
};

View File

@ -0,0 +1,5 @@
import { LayoutMain } from '@/modules/layout';
export const Main = () => {
return <LayoutMain title='Home' />;
};

View File

@ -0,0 +1,55 @@
import { Dialog, DialogContent, DialogTitle } from '@mui/material';
import { useShallow } from 'zustand/shallow';
import { useHomeStore } from '../store';
import { useMemo } from 'react';
import { useLayoutStore } from '@/modules/layout/store';
export const Label = ({ label, children }: { label: string; children: React.ReactNode }) => {
return (
<div className='text-sm text-gray-500 w-full flex gap-2'>
<div className='min-w-[60px]'>{label}</div>
<div className=''>{children}</div>
</div>
);
};
export const SuccessModal = () => {
const { openSuccessModal, setOpenSuccessModal, appKey, version, filename } = useHomeStore(
useShallow((state) => ({
openSuccessModal: state.openSuccessModal,
setOpenSuccessModal: state.setOpenSuccessModal,
appKey: state.appKey, //
version: state.version, //
filename: state.filename, //
})),
);
const { me } = useLayoutStore(useShallow((state) => ({ me: state.me })));
const link = useMemo(() => {
const _currentHref = new URL(window.location.href);
const username = me?.username;
const newHref = new URL(`/${username}/${appKey}/`, _currentHref.origin);
return newHref.toString();
}, [me, appKey]);
return (
<Dialog open={openSuccessModal} onClose={() => setOpenSuccessModal(false)}>
<DialogTitle className='text-black'></DialogTitle>
<DialogContent>
<div className='flex flex-col gap-2 w-[400px] min-h-[100px] text-black'>
<Label label='应用 Key: '>{appKey}</Label>
<Label label='版本:'>{version}</Label>
<Label label='访问地址:'>
<a href={link} className='text-blue-500' target='_blank' rel='noreferrer'>
{link}
</a>
</Label>
<Label label='配置地址:'>
<a href={`/app/edit/list`} className='text-blue-500' target='_self' rel='noreferrer'>
{`/app/edit/list`}
</a>
</Label>
<div className='mt-1 text-gray-500 italic' style={{ fontSize: 10 }}>
: 如果需要其他人访问
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,98 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import { useHomeStore } from '../store';
import { Controller, useForm } from 'react-hook-form';
import { TextField } from '@mui/material';
import { useEffect } from 'react';
import { customAlphabet } from 'nanoid';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { uploadFile } from './upload-file';
import { useAppVersionStore } from '@/pages/app/store';
import { useShallow } from 'zustand/shallow';
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
export const UploadModal = () => {
const { appKey, version, filename, openUploadModal, text, setOpenUploadModal, setAppKey, setVersion, setFilename, setOpenSuccessModal } = useHomeStore();
const { control, handleSubmit, reset } = useForm();
const { publishVersion } = useAppVersionStore(useShallow((state) => ({ publishVersion: state.publishVersion })));
useEffect(() => {
if (openUploadModal) {
const randomAppKey = nanoid(4) + nanoid(4);
reset({ appKey: appKey || randomAppKey, version: version || '1.0.0', filename: filename || 'index.html' });
}
}, [openUploadModal]);
const onSubmit = async (data: any) => {
console.log(data);
if (!text) {
toast.error('代码不能为空');
return;
}
if (!data.appKey) {
toast.error('应用key不能为空');
return;
}
if (!data.version) {
toast.error('版本不能为空');
return;
}
if (!data.filename) {
toast.error('文件名不能为空');
return;
}
setAppKey(data.appKey);
setVersion(data.version);
setFilename(data.filename);
const res = await uploadFile({
appKey: data.appKey,
version: data.version,
filename: data.filename,
text,
});
if (res?.code === 200) {
toast.success('部署成功');
const toastId = toast.loading('发布中...');
await new Promise((resolve) => setTimeout(resolve, 2000));
const res = await publishVersion({ appKey: data.appKey, version: data.version }, { showToast: false });
toast.dismiss(toastId);
if (res?.code === 200) {
toast.success('发布成功');
setOpenSuccessModal(true);
} else {
toast.error(res?.message || '发布失败');
}
} else {
toast.error(res?.message || '部署失败');
}
};
const { t } = useTranslation();
return (
<Dialog open={openUploadModal} onClose={() => setOpenUploadModal(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<form className='flex flex-col gap-3 pt-1 w-[500px]' onSubmit={handleSubmit(onSubmit)}>
<div className='flex flex-col gap-1'>
<label className='text-sm'>key</label>
<Controller control={control} name='appKey' render={({ field }) => <TextField {...field} />} />
</div>
<div className='flex flex-col gap-1'>
<label className='text-sm'></label>
<Controller control={control} name='version' render={({ field }) => <TextField {...field} />} />
</div>
<div className='flex flex-col gap-1'>
<label className='text-sm'></label>
<Controller control={control} name='filename' render={({ field }) => <TextField {...field} />} />
</div>
<DialogActions>
<Button
onClick={() => {
setOpenUploadModal(false);
}}>
{t('Cancel')}
</Button>
<Button type='submit'>{t('Submit')}</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,29 @@
import { uploadChunkV2, toFile } from '@kevisual/resources/index.ts';
import { toast } from 'react-toastify';
type UploadFileOpts = {
appKey: string;
version: string;
filename: string;
text: string;
};
const getFilenameExtension = (filename: string) => {
return filename.split('.').pop() || '';
};
const allowFilesName = ['js', 'css', 'json', 'html'];
export const uploadFile = async (uploadFileOpts: UploadFileOpts) => {
const { appKey, version, filename, text } = uploadFileOpts;
const extension = getFilenameExtension(filename);
if (!allowFilesName.includes(extension)) {
toast.error('文件类型不支持');
return;
}
const file = toFile(text, filename);
const res = await uploadChunkV2(file, {
appKey,
version,
filename,
noCheckAppFiles: false,
});
return res as any;
};

View File

@ -0,0 +1,51 @@
import { create } from 'zustand';
export type HomeStore = {
appKey: string;
version: string;
setAppKey: (appKey: string) => void;
setVersion: (version: string) => void;
filename: string;
setFilename: (filename: string) => void;
initApp: () => void;
openUploadModal: boolean;
setOpenUploadModal: (open: boolean) => void;
text: string;
setText: (text: string) => void;
openSuccessModal: boolean;
setOpenSuccessModal: (open: boolean) => void;
};
export const useHomeStore = create<HomeStore>((set) => ({
appKey: '',
version: '',
setAppKey: (appKey: string) => {
set({ appKey });
localStorage.setItem('home-app-key', appKey);
},
setVersion: (version: string) => {
set({ version });
localStorage.setItem('home-app-version', version);
},
filename: '',
setFilename: (filename: string) => {
set({ filename });
localStorage.setItem('home-file-name', filename);
},
initApp: () => {
const appKey = localStorage.getItem('home-app-key') || '';
const version = localStorage.getItem('home-app-version') || '';
const filename = localStorage.getItem('home-file-name') || '';
set({ appKey, version, filename });
},
openUploadModal: false,
setOpenUploadModal: (open: boolean) => {
set({ openUploadModal: open });
},
text: '',
setText: (text: string) => {
set({ text });
},
openSuccessModal: false,
setOpenSuccessModal: (open: boolean) => {
set({ openSuccessModal: open });
},
}));

View File

@ -1,10 +1,21 @@
import clsx from 'clsx';
import { useNewNavigate } from '@/modules';
import { useTranslation } from 'react-i18next';
import { useLayoutStore } from '@/modules/layout/store';
import { useShallow } from 'zustand/shallow';
import { useMemo } from 'react';
const ServerPath = () => {
const navigate = useNewNavigate();
const { t } = useTranslation();
const layoutStore = useLayoutStore(
useShallow((state) => {
return {
isAdmin: state.isAdmin,
checkHasOrg: state.checkHasOrg,
};
}),
);
const serverPath = [
{
path: 'container',
@ -28,9 +39,9 @@ const ServerPath = () => {
},
];
return (
<div className='p-2 w-full h-full bg-gray-200'>
<h1 className='p-4 w-1/2 m-auto h1'>{t('Site Map')}</h1>
<div className='w-1/2 m-auto bg-white p-4 border rounded-md shadow-md min-w-[700px] max-h-[80vh] overflow-auto scrollbar'>
<div className='p-2 w-full h-full bg-gray-200 '>
<h1 className='py-4 w-1/2 m-auto h1 text-primary'>{t('Site Map')}</h1>
<div className='w-1/2 m-auto bg-white p-4 border border-gray-200 rounded-md shadow-md min-w-[700px] max-h-[80vh] overflow-auto scrollbar'>
<div className='flex flex-col w-full'>
{serverPath.map((item) => {
const links = item.links.map((link) => {
@ -54,7 +65,7 @@ const ServerPath = () => {
navigate(`/${item.path}`);
}
}}>
<div className={clsx('border rounded-md p-2 m-2', hasId && 'bg-gray-200')}>{_path}</div>
<div className={clsx('border border-gray-200 rounded-md p-2 m-2', hasId && 'bg-gray-200')}>{_path}</div>
</div>
);
});

View File

@ -1,4 +1,4 @@
import { LayoutMain } from '@/modules/layout';
export const Main = () => {
return <LayoutMain title='User' />;
return <LayoutMain title='User Org' />;
};

View File

@ -1,4 +1,4 @@
import { InputAdornment, TextField, Tooltip } from '@mui/material';
import { Dialog, DialogContent, DialogTitle, InputAdornment, TextField, Tooltip } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { Button } from '@mui/material';
import { useUserStore } from '../store';
@ -12,6 +12,56 @@ import { FileUpload } from '../module/FileUpload';
import { useTranslation } from 'react-i18next';
import { Edit } from 'lucide-react';
import { toast } from 'react-toastify';
import { useAdminStore } from '../admin/store/admin-store';
export const CheckUserExistModal = () => {
const { t } = useTranslation();
const userStore = useUserStore(
useShallow((state) => {
return {
showCheckUserExist: state.showCheckUserExist,
setShowCheckUserExist: state.setShowCheckUserExist,
};
}),
);
const adminStore = useAdminStore(
useShallow((state) => {
return {
checkUserExist: state.checkUserExist,
};
}),
);
const onClose = () => {
userStore.setShowCheckUserExist(false);
};
const [username, setUsername] = useState('@');
const onCheck = async () => {
const res = await adminStore.checkUserExist(username);
console.log(res);
if (res === false) {
toast.info('当前用户名可以修改,点击右上角关注公众号联系客服修改。', {
autoClose: 20000,
});
// toast.success(t('Check success'));
} else {
toast.error('当前用户名已存在,请更换其他用户名。');
}
};
return (
<Dialog open={userStore.showCheckUserExist} onClose={onClose}>
<DialogTitle>{t('Tips')}</DialogTitle>
<DialogContent>
<div className='flex flex-col pt-4 gap-6 w-[400px]'>
<div className='text-sm text-secondary'>{t('Check username introduction')}</div>
<TextField label='username' value={username} onChange={(e) => setUsername(e.target.value)} />
<Button variant='contained' color='primary' onClick={onCheck}>
{t('Submit')}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export const Profile = () => {
const { t } = useTranslation();
@ -32,6 +82,7 @@ export const Profile = () => {
updateData: state.updateData,
setFormData: state.setFormData,
updateSelf: state.updateSelf,
setShowCheckUserExist: state.setShowCheckUserExist,
};
}),
);
@ -104,9 +155,7 @@ export const Profile = () => {
className='w-4 h-4 cursor-pointer text-primary'
onClick={() => {
console.log('onClick edit');
toast.info('联系客服修改,因为名称和上传文件绑定了。', {
autoClose: 20000,
});
userStore.setShowCheckUserExist(true);
}}
/>
</InputAdornment>
@ -153,6 +202,7 @@ export const Profile = () => {
</form>
</div>
</div>
<CheckUserExistModal />
</div>
</div>
);

View File

@ -6,6 +6,8 @@ type UserStore = {
setShowEdit: (showEdit: boolean) => void;
showNameEdit: boolean;
setShowNameEdit: (showNameEdit: boolean) => void;
showCheckUserExist: boolean;
setShowCheckUserExist: (showCheckUserExist: boolean) => void;
formData: any;
setFormData: (formData: any) => void;
loading: boolean;
@ -22,6 +24,8 @@ export const useUserStore = create<UserStore>((set, get) => {
setShowEdit: (showEdit) => set({ showEdit }),
showNameEdit: false,
setShowNameEdit: (showNameEdit) => set({ showNameEdit }),
showCheckUserExist: false,
setShowCheckUserExist: (showCheckUserExist) => set({ showCheckUserExist }),
formData: {},
setFormData: (formData) => set({ formData }),
loading: false,