feat: 添加i18n,美化界面
This commit is contained in:
27
packages/codemirror/.gitignore
vendored
Normal file
27
packages/codemirror/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
tsconfig.app.tsbuildinfo
|
||||
tsconfig.node.tsbuildinfo
|
||||
33
packages/codemirror/index.html
Normal file
33
packages/codemirror/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Apps</title>
|
||||
<link rel="stylesheet" href="./src/global.css">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ai-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<!-- <script src="/system/lib/app.js"></script> -->
|
||||
<script src="https://kevisual.xiongxiao.me/system/lib/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="ai-root"></div>
|
||||
<!-- <div id="ai-bot-root"></div> -->
|
||||
</body>
|
||||
<script src="./src/main.ts" type="module"></script>
|
||||
|
||||
</html>
|
||||
70
packages/codemirror/package.json
Normal file
70
packages/codemirror/package.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@kevisual/codemirror",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"basename": "/root/codemirror",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"pub": "envision deploy ./dist -k codemirror -v 0.0.1 -u -o root"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/basic-setup": "^0.20.0",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/history": "^0.19.2",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@kevisual/center-components": "workspace:*",
|
||||
"@kevisual/router": "^0.0.9",
|
||||
"@kevisual/store": "^0.0.2",
|
||||
"@mui/material": "^6.4.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@uiw/codemirror-theme-duotone": "^4.23.10",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.10",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"highlight.js": "^11.11.1",
|
||||
"immer": "^10.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.483.0",
|
||||
"marked": "^15.0.7",
|
||||
"marked-highlight": "^2.2.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "19.0.0",
|
||||
"react-datepicker": "^8.2.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-toastify": "^11.0.5",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.6"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./*": "./src/*"
|
||||
}
|
||||
}
|
||||
3
packages/codemirror/src/assets/style.css
Normal file
3
packages/codemirror/src/assets/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
120
packages/codemirror/src/editor/editor.ts
Normal file
120
packages/codemirror/src/editor/editor.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { yaml } from '@codemirror/lang-yaml';
|
||||
import { history } from '@codemirror/history';
|
||||
import { vscodeLight } from '@uiw/codemirror-theme-vscode';
|
||||
import { formatKeymap } from './modules/keymap';
|
||||
import { Compartment, EditorState, Extension } from '@codemirror/state';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { autocompletion, Completion } from '@codemirror/autocomplete';
|
||||
import { getFileType } from './utils/get-file-type';
|
||||
type BaseEditorOpts = {
|
||||
language?: string;
|
||||
filename: string;
|
||||
autoComplete?: {
|
||||
props?: any;
|
||||
open?: boolean;
|
||||
overrideList?: Completion[];
|
||||
};
|
||||
};
|
||||
export class BaseEditor {
|
||||
editor?: EditorView;
|
||||
onChangeCompartment?: Compartment;
|
||||
language: string;
|
||||
filename: string;
|
||||
el?: HTMLElement;
|
||||
autoComplete?: {
|
||||
props?: any;
|
||||
open?: boolean;
|
||||
overrideList?: Completion[];
|
||||
};
|
||||
|
||||
constructor(opts?: BaseEditorOpts) {
|
||||
this.filename = opts?.filename || '';
|
||||
this.language = opts?.language || 'typescript';
|
||||
if (this.filename && !opts?.language) {
|
||||
// 根据文件名自动判断语言
|
||||
const fileType = getFileType(this.filename);
|
||||
this.language = fileType || 'typescript';
|
||||
}
|
||||
this.autoComplete = opts?.autoComplete || { open: true, overrideList: [] };
|
||||
this.onChangeCompartment = new Compartment(); // 创建一个用于 onChange 的独立扩展
|
||||
}
|
||||
onEditorChange(view: EditorView) {
|
||||
console.log('onChange', view);
|
||||
}
|
||||
getFileType(filename: string) {
|
||||
return getFileType(filename);
|
||||
}
|
||||
createEditor(el: HTMLElement) {
|
||||
this.el = el;
|
||||
const onChangeCompartment = this.onChangeCompartment!;
|
||||
const language = this.language;
|
||||
const extensions: Extension[] = [
|
||||
vscodeLight,
|
||||
formatKeymap,
|
||||
keymap.of(defaultKeymap), //
|
||||
// history(),
|
||||
];
|
||||
if (this.autoComplete?.open) {
|
||||
extensions.push(
|
||||
autocompletion({
|
||||
activateOnTyping: true, // 输入时触发补全
|
||||
closeOnBlur: true, // 失去焦点时关闭补全
|
||||
activateOnTypingDelay: 2000, // 输入时触发补全
|
||||
override: this.autoComplete?.overrideList || [],
|
||||
...this.autoComplete?.props,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (language === 'typescript') {
|
||||
extensions.push(javascript({ typescript: true }));
|
||||
} else if (language === 'javascript') {
|
||||
extensions.push(javascript());
|
||||
} else if (language === 'markdown') {
|
||||
extensions.push(markdown());
|
||||
} else if (language === 'html') {
|
||||
extensions.push(html());
|
||||
} else if (language === 'css') {
|
||||
extensions.push(css());
|
||||
} else if (language === 'json') {
|
||||
extensions.push(json());
|
||||
} else if (language === 'yaml') {
|
||||
extensions.push(yaml());
|
||||
}
|
||||
|
||||
this.editor = new EditorView({
|
||||
parent: el,
|
||||
extensions: [
|
||||
...extensions, //
|
||||
onChangeCompartment.of([]),
|
||||
],
|
||||
});
|
||||
}
|
||||
resetEditor(el?: HTMLElement) {
|
||||
this.editor?.destroy?.();
|
||||
this.createEditor(el || (this.el as HTMLElement));
|
||||
}
|
||||
setLanguage(language: string, el?: HTMLElement) {
|
||||
this.language = language;
|
||||
this.editor?.destroy?.();
|
||||
this.createEditor(el || (this.el as HTMLElement));
|
||||
}
|
||||
setContent(content: string) {
|
||||
this.editor?.dispatch({
|
||||
changes: { from: 0, to: this.editor?.state.doc.length, insert: content },
|
||||
});
|
||||
}
|
||||
getContent() {
|
||||
return this.editor?.state.doc.toString();
|
||||
}
|
||||
destroyEditor() {
|
||||
this.editor?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseEditor;
|
||||
94
packages/codemirror/src/editor/modules/ai-completions.ts
Normal file
94
packages/codemirror/src/editor/modules/ai-completions.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { CompletionResult, CompletionSource } from '@codemirror/autocomplete';
|
||||
import { Transaction } from '@codemirror/state';
|
||||
|
||||
export async function fetchAICompletions(context) {
|
||||
const userInput = context.matchBefore(/\w*/);
|
||||
if (!userInput) return null;
|
||||
|
||||
const docText = context.state.doc.toString(); // 获取完整文档内容
|
||||
const cursorPos = context.pos; // 获取光标位置
|
||||
|
||||
// 添加提示词
|
||||
const promptText = `请根据以下代码和光标位置提供代码补全建议:
|
||||
|
||||
代码:
|
||||
${docText}
|
||||
|
||||
光标位置:${cursorPos}
|
||||
|
||||
请提供适当的代码补全。`;
|
||||
|
||||
const aiResponse = await fetch('http://192.168.31.220:11434/api/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: promptText,
|
||||
// 如果 Ollama 支持,可以传递其他参数,如模型名称
|
||||
// model: 'your-model-name',
|
||||
model: 'qwen2.5-coder:14b',
|
||||
// model: 'qwen2.5:14b',
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await aiResponse.json();
|
||||
const data = result;
|
||||
// 提取字段
|
||||
const createdAt = new Date(data.created_at); // 转换为日期对象
|
||||
const isDone = data.done;
|
||||
const doneReason = data.done_reason;
|
||||
const modelName = data.model;
|
||||
const responseText = data.response;
|
||||
const matchCodeBlock = responseText.match(/```javascript\n([\s\S]*?)\n```/);
|
||||
let codeBlock = '';
|
||||
if (matchCodeBlock) {
|
||||
const codeBlock = matchCodeBlock[1]; // 提取代码块
|
||||
console.log('补全代码:', codeBlock);
|
||||
}
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
label: codeBlock,
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
return {
|
||||
from: userInput.from,
|
||||
options: suggestions,
|
||||
validFor: /^\w*$/,
|
||||
};
|
||||
}
|
||||
|
||||
// type AiCompletion = Readonly<CompletionSource>;
|
||||
export const testAiCompletion = async (context: any): Promise<CompletionResult | null> => {
|
||||
const userInput = context.matchBefore(/\w*/);
|
||||
if (!userInput) return null;
|
||||
const docText = context.state.doc.toString(); // 获取完整文档内容
|
||||
const cursorPos = context.pos; // 获取光标位置
|
||||
console.log('testAiCompletion', userInput, docText, cursorPos);
|
||||
|
||||
return {
|
||||
from: userInput.from,
|
||||
options: [
|
||||
{
|
||||
label: '123 测试 skflskdf ',
|
||||
type: 'text',
|
||||
apply: (state, completion, from, to) => {
|
||||
console.log('apply', state, completion, from, to);
|
||||
const newText = '123 测试 skflskdf ';
|
||||
state.dispatch({
|
||||
changes: { from, to, insert: newText },
|
||||
});
|
||||
},
|
||||
info: '这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母这是一个长文本上的反馈是冷酷的父母离开时的父母 这是一个长文本上的反馈是冷酷的父母离开时的父母',
|
||||
},
|
||||
{
|
||||
label: '456',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
validFor: /^\w*$/,
|
||||
};
|
||||
};
|
||||
92
packages/codemirror/src/editor/modules/keymap.ts
Normal file
92
packages/codemirror/src/editor/modules/keymap.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { defaultKeymap, indentSelection, indentWithTab, undo, history } from '@codemirror/commands';
|
||||
import { indentUnit } from '@codemirror/language';
|
||||
import prettier from 'prettier';
|
||||
// import parserBabel from 'prettier/plugins/babel';
|
||||
import parserEstree from 'prettier/plugins/estree';
|
||||
// import parserHtml from 'prettier/plugins/html';
|
||||
import parserTypescript from 'prettier/plugins/typescript';
|
||||
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
// 格式化函数
|
||||
// Function to format the code using Prettier
|
||||
async function formatCode(view: EditorView) {
|
||||
const editor = view;
|
||||
const code = editor.state.doc.toString();
|
||||
try {
|
||||
const formattedCode = await prettier.format(code, {
|
||||
parser: 'typescript',
|
||||
plugins: [parserEstree, parserTypescript],
|
||||
});
|
||||
|
||||
editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
insert: formattedCode.trim(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error formatting code:', error);
|
||||
}
|
||||
}
|
||||
// export const TabFn = (view: EditorView) => {
|
||||
// const { state } = view;
|
||||
// const selection = state.selection.main;
|
||||
// // 判断是否存在补全上下文
|
||||
// if (state.field(autocompletion).active?.length) {
|
||||
// // 插入当前选中的补全内容
|
||||
// const active = state.field(autocompletion).active[0];
|
||||
// const selected = active?.options[active.selected]?.label;
|
||||
// if (selected) {
|
||||
// view.dispatch({
|
||||
// changes: { from: selection.from, to: selection.to, insert: selected },
|
||||
// });
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// 自定义 Tab 键行为
|
||||
function customIndentWithTab(view: EditorView) {
|
||||
const { state, dispatch } = view;
|
||||
const { from, to } = state.selection.main;
|
||||
const _indentUnit = state.facet(indentUnit) as string;
|
||||
|
||||
// 计算插入的缩进
|
||||
const indent = _indentUnit.repeat(1);
|
||||
|
||||
dispatch({
|
||||
changes: { from, to, insert: indent },
|
||||
selection: { anchor: from + indent.length },
|
||||
});
|
||||
|
||||
return true; // 表示按键事件被处理
|
||||
}
|
||||
export const formatKeymap = keymap.of([
|
||||
{
|
||||
// bug, 必须小写
|
||||
key: 'alt-shift-f', // 快捷键绑定
|
||||
// mac: 'cmd-shift-f',
|
||||
run: (view) => {
|
||||
formatCode(view);
|
||||
return true; // 表示按键事件被处理
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Tab',
|
||||
run: customIndentWithTab, // 使用自定义的缩进函数
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-z', // Windows 撤销快捷键
|
||||
// key: 'Alt-z',
|
||||
mac: 'Cmd-z', // Mac 撤销快捷键
|
||||
run: (view) => {
|
||||
console.log('undo', view);
|
||||
return undo(view); // 表示按键事件被处理
|
||||
},
|
||||
},
|
||||
...defaultKeymap, // 默认快捷键
|
||||
]);
|
||||
// console.log('formatKeymap', defaultKeymap);
|
||||
31
packages/codemirror/src/editor/utils/get-file-type.ts
Normal file
31
packages/codemirror/src/editor/utils/get-file-type.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const getExtension = (filename: string) => {
|
||||
const extension = filename.split('.').pop();
|
||||
if (!extension) return '';
|
||||
return extension;
|
||||
};
|
||||
export const supportedExtensions = ['ts', 'js', 'md', 'json', 'yaml', 'yml', 'css', 'html'];
|
||||
|
||||
export function getFileType(filename: string) {
|
||||
const extension = getExtension(filename);
|
||||
if (!supportedExtensions.includes(extension)) return '';
|
||||
switch (extension) {
|
||||
case 'ts':
|
||||
return 'typescript';
|
||||
case 'js':
|
||||
return 'javascript';
|
||||
case 'md':
|
||||
return 'markdown';
|
||||
case 'json':
|
||||
return 'json';
|
||||
case 'yaml':
|
||||
return 'yaml';
|
||||
case 'yml':
|
||||
return 'yaml';
|
||||
case 'css':
|
||||
return 'css';
|
||||
case 'html':
|
||||
return 'html';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
3
packages/codemirror/src/global.css
Normal file
3
packages/codemirror/src/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
@import './style.css';
|
||||
4
packages/codemirror/src/main.ts
Normal file
4
packages/codemirror/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { render } from './pages/Bootstrap';
|
||||
import './styles.css';
|
||||
|
||||
render('#ai-root');
|
||||
40
packages/codemirror/src/pages/App.tsx
Normal file
40
packages/codemirror/src/pages/App.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useMemo } from 'react';
|
||||
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { FileEditor } from './file-editor/FileEditor';
|
||||
|
||||
export const InitProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
type AppProvider = {
|
||||
key: string;
|
||||
use: boolean;
|
||||
};
|
||||
export type AppProps = {
|
||||
providers?: AppProvider[];
|
||||
noProvider?: boolean;
|
||||
/**
|
||||
* 是否是单个应用
|
||||
* 默认是单个应用模块。
|
||||
*/
|
||||
isSingleApp?: boolean;
|
||||
};
|
||||
export const App = ({ providers, noProvider, isSingleApp = true }: AppProps) => {
|
||||
const children = useMemo(() => {
|
||||
return (
|
||||
<InitProvider>
|
||||
<FileEditor />
|
||||
</InitProvider>
|
||||
);
|
||||
}, []);
|
||||
if (noProvider) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return (
|
||||
<CustomThemeProvider>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
};
|
||||
88
packages/codemirror/src/pages/Bootstrap.tsx
Normal file
88
packages/codemirror/src/pages/Bootstrap.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App, AppProps } from './App';
|
||||
import React from 'react';
|
||||
|
||||
export class ReactRenderer {
|
||||
component: any;
|
||||
element: HTMLElement;
|
||||
ref: React.RefObject<any>;
|
||||
props: any;
|
||||
root: any;
|
||||
constructor(component: any, { props, className }: any) {
|
||||
this.component = component;
|
||||
const el = document.createElement('div');
|
||||
this.element = el;
|
||||
this.ref = React.createRef();
|
||||
this.props = {
|
||||
...props,
|
||||
ref: this.ref,
|
||||
};
|
||||
el.className = className;
|
||||
this.root = createRoot(this.element);
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateProps(props: any) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...props,
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.root.render(React.createElement(this.component, this.props));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactRenderer;
|
||||
export const randomId = () => {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
};
|
||||
export const render = (el: HTMLElement | string, props?: AppProps) => {
|
||||
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
||||
if (!root) {
|
||||
console.error('root not found');
|
||||
return;
|
||||
}
|
||||
const hasResourceApp = window.context?.codemirrorApp;
|
||||
if (hasResourceApp) {
|
||||
const render = hasResourceApp as ReactRenderer;
|
||||
render.updateProps({
|
||||
props: { t: randomId(), ...props },
|
||||
});
|
||||
root.innerHTML = '';
|
||||
root.appendChild(render.element);
|
||||
} else {
|
||||
const renderer = new ReactRenderer(App, {
|
||||
props: { ...props },
|
||||
className: 'codemirror-root w-full h-full',
|
||||
});
|
||||
if (window.context) {
|
||||
window.context.codemirrorApp = renderer;
|
||||
} else {
|
||||
window.context = {
|
||||
codemirrorApp: renderer,
|
||||
};
|
||||
}
|
||||
root.appendChild(renderer.element);
|
||||
}
|
||||
};
|
||||
|
||||
export const unmount = (el: HTMLElement | string) => {
|
||||
const root = typeof el === 'string' ? document.querySelector(el) : el;
|
||||
if (!root) {
|
||||
console.error('root not found');
|
||||
return;
|
||||
}
|
||||
const hasResourceApp = window.context?.codemirrorApp;
|
||||
if (hasResourceApp) {
|
||||
const render = hasResourceApp as ReactRenderer;
|
||||
render.destroy();
|
||||
window.context.codemirrorApp = null;
|
||||
}
|
||||
};
|
||||
17
packages/codemirror/src/pages/file-editor/FileEditor.tsx
Normal file
17
packages/codemirror/src/pages/file-editor/FileEditor.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { BaseEditor } from '../../editor/editor';
|
||||
|
||||
export const FileEditor = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const editor = new BaseEditor();
|
||||
if (containerRef.current) {
|
||||
editor.createEditor(containerRef.current);
|
||||
}
|
||||
return () => {
|
||||
editor.destroyEditor();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} className='w-full h-full'></div>;
|
||||
};
|
||||
3
packages/codemirror/src/style.css
Normal file
3
packages/codemirror/src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
39
packages/codemirror/tsconfig.json
Normal file
39
packages/codemirror/tsconfig.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"node_modules/@kevisual/types",
|
||||
],
|
||||
"paths": {
|
||||
"@kevisual/codemirror/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
]
|
||||
}
|
||||
83
packages/codemirror/vite.config.mjs
Normal file
83
packages/codemirror/vite.config.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl';
|
||||
|
||||
const plugins = [basicSsl()];
|
||||
// const plugins = [];
|
||||
plugins.push(tailwindcss());
|
||||
let proxy = {};
|
||||
if (true) {
|
||||
proxy = {
|
||||
'/api': {
|
||||
target: 'https://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
target: 'wss://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/user/login': {
|
||||
target: 'https://kevisual.silkyai.cn',
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/user/, '/user'),
|
||||
},
|
||||
};
|
||||
}
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), ...plugins],
|
||||
|
||||
css: {
|
||||
postcss: {},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@kevisual/codemirror': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
DEV_SERVER: JSON.stringify(process.env.NODE_ENV === 'development'),
|
||||
BASE_NAME: JSON.stringify('/root/codemirror/'),
|
||||
},
|
||||
base: './',
|
||||
// base: isDev ? '/' : '/root/codemirror/',
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// external: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 6022,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/system/lib': {
|
||||
target: 'https://kevisual.xiongxiao.me',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:4005',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
target: 'ws://localhost:4005',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
...proxy,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -17,7 +17,8 @@
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/material": "^6.4.7",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.54.2"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
|
||||
47
packages/components/src/input/index.tsx
Normal file
47
packages/components/src/input/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FormControlLabel, TextField } from '@mui/material';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
export const InputControl = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<TextField
|
||||
variant='outlined'
|
||||
size='small'
|
||||
name={name}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
type FormProps = {
|
||||
onSubmit?: (data: any) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const FormDemo = (props: FormProps) => {
|
||||
const { control, handleSubmit } = useForm();
|
||||
const { onSubmit = () => {}, children } = props;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name='name'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
rules={{ required: 'Name is required' }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label='Name'
|
||||
variant='outlined'
|
||||
margin='normal'
|
||||
fullWidth //
|
||||
error={!!error}
|
||||
helperText={<>{error?.message}</>}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
91
packages/components/src/modal/Confirm.tsx
Normal file
91
packages/components/src/modal/Confirm.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
export const Confirm = ({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
content,
|
||||
onConfirm,
|
||||
confirmText = '确认',
|
||||
cancelText = '取消',
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
content: string;
|
||||
onConfirm?: () => void;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogTitle id='alert-dialog-title' className='text-secondary min-w-[300px]'>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='alert-dialog-description'>{content}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color='primary'>
|
||||
{cancelText || '取消'}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} variant='contained' color='primary' autoFocus>
|
||||
{confirmText || '确认'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
type Fn = () => void;
|
||||
export const useConfirm = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const fns = useRef<{
|
||||
onConfirm: Fn;
|
||||
onCancel: Fn;
|
||||
confirmText: string;
|
||||
cancelText: string;
|
||||
}>({
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
});
|
||||
return {
|
||||
contextHolder: (
|
||||
<Confirm
|
||||
open={open}
|
||||
confirmText={fns.current.confirmText}
|
||||
cancelText={fns.current.cancelText}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
fns.current.onCancel();
|
||||
}}
|
||||
title={title}
|
||||
content={content}
|
||||
onConfirm={fns.current.onConfirm}
|
||||
/>
|
||||
),
|
||||
confirm: (
|
||||
title: string,
|
||||
content: string,
|
||||
opts?: {
|
||||
onConfirm: () => void;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
onCancel?: () => void;
|
||||
},
|
||||
) => {
|
||||
setOpen(true);
|
||||
setTitle(title);
|
||||
setContent(content);
|
||||
fns.current.onConfirm = opts?.onConfirm || (() => {});
|
||||
fns.current.onCancel = opts?.onCancel || (() => {});
|
||||
fns.current.confirmText = opts?.confirmText || '确认';
|
||||
fns.current.cancelText = opts?.cancelText || '取消';
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -5,4 +5,67 @@
|
||||
--color-secondary: #ffa000;
|
||||
--color-success: #28a745;
|
||||
--scrollbar-color: #ffc107; /* 滚动条颜色 */
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
/* font-family */
|
||||
@utility font-family-mon {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
@utility font-family-rob {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
@utility font-family-int {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@utility font-family-orb {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
@utility font-family-din {
|
||||
font-family: 'DIN', sans-serif;
|
||||
}
|
||||
|
||||
@utility flex-row-center {
|
||||
@apply flex flex-row items-center justify-center;
|
||||
}
|
||||
@utility flex-col-center {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
}
|
||||
|
||||
@utility scrollbar {
|
||||
overflow: auto;
|
||||
/* 整个滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
/* 滚动条有滑块的轨道部分 */
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
cursor: pointer;
|
||||
background-color: #c1c1c1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #999999;
|
||||
}
|
||||
|
||||
/* 同时有垂直和水平滚动条时交汇的部分 */
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: block; /* 修复交汇时出现的白块 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Apps</title>
|
||||
<link rel="stylesheet" href="./src/assets/index.css">
|
||||
<link rel="stylesheet" href="./src/global.css">
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
|
||||
@layer components {
|
||||
.test-loading {
|
||||
@apply w-20 h-20 bg-gray-300 rounded-full animate-spin;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--scrollbar-color: #ffbf00;
|
||||
--primary-color: #ffc107;
|
||||
--secondary-color: #ffa000;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#ai-bot-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-color) #fff;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ant-select-outlined.ant-select-multiple .ant-select-selection-item {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.ant-select-selection-item {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.ant-select {
|
||||
.ant-select-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.ant-picker-input {
|
||||
.ant-picker-suffix,
|
||||
.ant-picker-clear {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
4
packages/resources/src/global.css
Normal file
4
packages/resources/src/global.css
Normal file
@@ -0,0 +1,4 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@kevisual/center-components/theme/wind-theme.css';
|
||||
|
||||
@import './style.css';
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { theme } from '@kevisual/center-components/theme/index.tsx';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
|
||||
import { Left } from './layout/Left';
|
||||
import { Main } from './main/index';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
@@ -120,11 +119,11 @@ export const App = ({ providers, noProvider, isSingleApp = true }: AppProps) =>
|
||||
return <>{children}</>;
|
||||
}
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CustomThemeProvider>
|
||||
<AntdConfigProvider>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</AntdConfigProvider>
|
||||
</ThemeProvider>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,107 +6,116 @@ import { getIcon } from '../FileIcon';
|
||||
import { Download, Trash } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
|
||||
import { useConfirm } from '@kevisual/center-components/modal/Confirm.tsx';
|
||||
|
||||
export const FileTable = () => {
|
||||
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore();
|
||||
const { setOpenDrawer, setPrefix } = useResourceFileStore();
|
||||
const { confirm, contextHolder } = useConfirm();
|
||||
return (
|
||||
<TableContainer
|
||||
className='scrollbar'
|
||||
sx={{
|
||||
'&': {
|
||||
// scrollbarWidth: 'none',
|
||||
// scrollbarColor: '#888 #fff',
|
||||
},
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px !important',
|
||||
height: '4px !important',
|
||||
background: '#fff',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#888',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
}}
|
||||
component={Paper}>
|
||||
<Table
|
||||
<>
|
||||
{contextHolder}
|
||||
<TableContainer
|
||||
className='scrollbar'
|
||||
sx={{
|
||||
minWidth: 650,
|
||||
'&': {
|
||||
// scrollbarWidth: 'none',
|
||||
// scrollbarColor: '#888 #fff',
|
||||
},
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px !important',
|
||||
height: '4px !important',
|
||||
background: '#fff',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#888',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
}}
|
||||
aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell sx={{ minWidth: 100 }}>Size</TableCell>
|
||||
<TableCell sx={{ minWidth: 180 }}>Last Modified</TableCell>
|
||||
<TableCell sx={{ minWidth: 110 }}>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{list.map((row) => {
|
||||
const isFile = !!row.name;
|
||||
return (
|
||||
<TableRow key={row.etag || row.name || row.prefix}>
|
||||
<TableCell>
|
||||
<div
|
||||
className={clsx('flex items-center gap-2 max-w-[300px] line-clamp-2 text-ellipsis', {
|
||||
'cursor-pointer': true,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (!row.name) {
|
||||
onOpenPrefix(row.prefix || '');
|
||||
} else {
|
||||
setPrefix(row.name || '');
|
||||
setOpenDrawer(true);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<div className='shrink-0'>{getIcon(row.name)}</div>
|
||||
{row.name ? row.name.replace(prefix, '') : row.prefix?.replace?.(prefix, '')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.size ? prettyBytes(row.size) : ''}</TableCell>
|
||||
<TableCell>{row.lastModified ? dayjs(row.lastModified).format('YYYY-MM-DD HH:mm:ss') : ''}</TableCell>
|
||||
<TableCell>
|
||||
{isFile && (
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
component={Paper}>
|
||||
<Table
|
||||
sx={{
|
||||
minWidth: 650,
|
||||
}}
|
||||
aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell sx={{ minWidth: 100 }}>Size</TableCell>
|
||||
<TableCell sx={{ minWidth: 180 }}>Last Modified</TableCell>
|
||||
<TableCell sx={{ minWidth: 110 }}>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{list.map((row) => {
|
||||
const isFile = !!row.name;
|
||||
return (
|
||||
<TableRow key={row.etag || row.name || row.prefix}>
|
||||
<TableCell>
|
||||
<div
|
||||
className={clsx('flex items-center gap-2 max-w-[300px] line-clamp-2 text-ellipsis', {
|
||||
'cursor-pointer': true,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (!row.name) {
|
||||
onOpenPrefix(row.prefix || '');
|
||||
} else {
|
||||
setPrefix(row.name || '');
|
||||
setOpenDrawer(true);
|
||||
}
|
||||
e.stopPropagation();
|
||||
download(row);
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
minWidth: '32px',
|
||||
padding: '4px',
|
||||
}}>
|
||||
<Download />
|
||||
</Button>
|
||||
)}
|
||||
{isFile && (
|
||||
<Button
|
||||
variant='contained'
|
||||
color='error'
|
||||
className='ml-2!'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFile(row);
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
minWidth: '32px',
|
||||
padding: '4px',
|
||||
}}>
|
||||
<Trash />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div className='shrink-0'>{getIcon(row.name)}</div>
|
||||
{row.name ? row.name.replace(prefix, '') : row.prefix?.replace?.(prefix, '')}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{row.size ? prettyBytes(row.size) : ''}</TableCell>
|
||||
<TableCell>{row.lastModified ? dayjs(row.lastModified).format('YYYY-MM-DD HH:mm:ss') : ''}</TableCell>
|
||||
<TableCell>
|
||||
{isFile && (
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
download(row);
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
minWidth: '32px',
|
||||
padding: '4px',
|
||||
}}>
|
||||
<Download />
|
||||
</Button>
|
||||
)}
|
||||
{isFile && (
|
||||
<Button
|
||||
variant='contained'
|
||||
color='error'
|
||||
className='ml-2!'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
confirm('删除文件', '确定删除该文件吗?', {
|
||||
onConfirm: () => {
|
||||
deleteFile(row);
|
||||
},
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
minWidth: '32px',
|
||||
padding: '4px',
|
||||
}}>
|
||||
<Trash />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
import React, { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
CssBaseline,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Button,
|
||||
useTheme,
|
||||
ListItemIcon,
|
||||
Tooltip,
|
||||
Container,
|
||||
} from '@mui/material';
|
||||
import { Box, Divider, List, ListItem, ListItemButton, ListItemText, useTheme, ListItemIcon, Tooltip, Container } from '@mui/material';
|
||||
import { ActiveMenu, useLayoutStore } from '../store/layout';
|
||||
import { activeMenuList } from '../modules/MenuList';
|
||||
import { amber } from '@mui/material/colors';
|
||||
import { lighten, rgbToHex } from '@mui/material/styles';
|
||||
import { SquareMenu } from 'lucide-react';
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
export type LeftProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const Left = ({ children }: LeftProps) => {
|
||||
const { setActiveMenu, init, showLabel, setShowLabel } = useLayoutStore();
|
||||
const theme = useTheme();
|
||||
// console.log(theme.palette.primary.main, rgbToHex(lighten(theme.palette.primary.main, 0.4)));
|
||||
// console.log(theme.palette.divider);
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
@@ -49,23 +28,24 @@ export const Left = ({ children }: LeftProps) => {
|
||||
}, [activeMenuList]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', backgroundColor: amber[50] }}>
|
||||
<Box sx={{ height: '100%', display: 'flex', backgroundColor: amber[50] }}>
|
||||
<Box component='nav' sx={{ flexShrink: { sm: 0 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box' },
|
||||
height: '100%',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
height: '100vh',
|
||||
borderRight: 1,
|
||||
borderColor: theme.palette.divider,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<List sx={{ flexGrow: 1 }} key={list.length}>
|
||||
<List sx={{ flexGrow: 1, height: '100%' }} key={list.length}>
|
||||
{list.map((item) => (
|
||||
<Fragment key={item.value}>
|
||||
<ListItem disablePadding>
|
||||
@@ -106,7 +86,7 @@ export const Left = ({ children }: LeftProps) => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1, height: '100vh', overflow: 'hidden' }}>
|
||||
<Box sx={{ flexGrow: 1, height: '100%', overflow: 'hidden' }}>
|
||||
<Container sx={{ width: '100%', height: '100%' }}>{children}</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -18,5 +18,5 @@ export const Main = () => {
|
||||
if (activeMenu === ActiveMenu.Statistic) {
|
||||
return <Statistic />;
|
||||
}
|
||||
return <div className='h-full'>{activeMenu}</div>;
|
||||
return <div className='h-full overflow-hidden'>{activeMenu}</div>;
|
||||
};
|
||||
|
||||
@@ -9,10 +9,12 @@ type ConvertOpts = {
|
||||
version?: string;
|
||||
username?: string;
|
||||
directory?: string;
|
||||
isPublic?: boolean;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
const { directory, appKey, version, username } = opts;
|
||||
const { directory, appKey, version, username, isPublic } = opts;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
@@ -21,11 +23,16 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
}
|
||||
|
||||
const taskId = nanoid();
|
||||
const filename = file.name;
|
||||
const filename = opts.filename || file.name;
|
||||
const load = toast.loading(`${filename} 上传中...`);
|
||||
NProgress.start();
|
||||
// const eventSource = new EventSource('http://49.232.155.236:11015/api/s1/events?taskId=' + taskId);
|
||||
const eventSource = new EventSource('/api/s1/events?taskId=' + taskId);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('taskId', taskId);
|
||||
if (isPublic) {
|
||||
searchParams.set('public', 'true');
|
||||
}
|
||||
const eventSource = new EventSource('/api/s1/events?' + searchParams.toString());
|
||||
// 监听服务器推送的进度更新
|
||||
eventSource.onmessage = function (event) {
|
||||
console.log('Progress update:', event.data);
|
||||
@@ -60,9 +67,10 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk, file.name);
|
||||
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);
|
||||
}
|
||||
@@ -83,10 +91,12 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
|
||||
},
|
||||
}).then((response) => response.json());
|
||||
fetch('/api/s1/events/close?taskId=' + taskId);
|
||||
eventSource.close();
|
||||
NProgress.done();
|
||||
toast.dismiss(load);
|
||||
resolve(res);
|
||||
if (isLast) {
|
||||
NProgress.done();
|
||||
eventSource.close();
|
||||
toast.dismiss(load);
|
||||
resolve(res);
|
||||
}
|
||||
// console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
|
||||
} catch (error) {
|
||||
console.log('Error uploading chunk', error);
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
:root {
|
||||
--scrollbar-color: #ffbf00;
|
||||
--primary-color: #ffc107;
|
||||
--secondary-color: #ffa000;
|
||||
}
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#ai-bot-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-color) #fff;
|
||||
@@ -15,5 +34,4 @@
|
||||
|
||||
.scrollbar::-webkit-scrollbar-track {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user