diff --git a/backend/app/api/v1/file.go b/backend/app/api/v1/file.go index 19e6b3e26..66b8b19b9 100644 --- a/backend/app/api/v1/file.go +++ b/backend/app/api/v1/file.go @@ -104,3 +104,30 @@ func (b *BaseApi) DeCompressFile(c *gin.Context) { } helper.SuccessWithData(c, nil) } + +func (b *BaseApi) GetContent(c *gin.Context) { + var req dto.FileOption + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + info, err := fileService.GetContent(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, info) +} + +func (b *BaseApi) SaveContent(c *gin.Context) { + var req dto.FileEdit + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := fileService.SaveContent(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/backend/app/dto/file.go b/backend/app/dto/file.go index b211eb912..1ed800c92 100644 --- a/backend/app/dto/file.go +++ b/backend/app/dto/file.go @@ -47,3 +47,8 @@ type FileDeCompress struct { Type string Path string } + +type FileEdit struct { + Path string + Content string +} diff --git a/backend/app/service/file.go b/backend/app/service/file.go index eb4bc5387..a8fc7ad1e 100644 --- a/backend/app/service/file.go +++ b/backend/app/service/file.go @@ -9,6 +9,7 @@ import ( "io" "io/fs" "path/filepath" + "strings" ) type FileService struct { @@ -92,6 +93,28 @@ func (f FileService) DeCompress(c dto.FileDeCompress) error { return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type)) } +func (f FileService) GetContent(c dto.FileOption) (dto.FileInfo, error) { + info, err := files.NewFileInfo(c.FileOption) + if err != nil { + return dto.FileInfo{}, err + } + return dto.FileInfo{*info}, nil +} + +func (f FileService) SaveContent(c dto.FileEdit) error { + + info, err := files.NewFileInfo(files.FileOption{ + Path: c.Path, + Expand: false, + }) + if err != nil { + return err + } + + fo := files.NewFileOp() + return fo.WriteFile(c.Path, strings.NewReader(c.Content), info.FileMode) +} + func getUuid() string { b := make([]byte, 16) io.ReadFull(rand.Reader, b) diff --git a/backend/router/ro_file.go b/backend/router/ro_file.go index 3b3125947..a17e139ee 100644 --- a/backend/router/ro_file.go +++ b/backend/router/ro_file.go @@ -22,6 +22,8 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) { fileRouter.POST("/mode", baseApi.ChangeFileMode) fileRouter.POST("/compress", baseApi.CompressFile) fileRouter.POST("/decompress", baseApi.DeCompressFile) + fileRouter.POST("/content", baseApi.GetContent) + fileRouter.POST("/save", baseApi.SaveContent) } } diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index 40d6fd431..cd82f3f1a 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -7,7 +7,6 @@ import ( "io" "io/fs" "os" - "path" "path/filepath" ) @@ -58,10 +57,6 @@ func (f FileOp) DeleteFile(dst string) error { } func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error { - dir, _ := path.Split(dst) - if err := f.Fs.MkdirAll(dir, mode); err != nil { - return err - } file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return err diff --git a/backend/utils/files/fileinfo.go b/backend/utils/files/fileinfo.go index 75e293a88..38aac9c0d 100644 --- a/backend/utils/files/fileinfo.go +++ b/backend/utils/files/fileinfo.go @@ -2,6 +2,7 @@ package files import ( "fmt" + "github.com/pkg/errors" "github.com/spf13/afero" "os" "path" @@ -129,5 +130,15 @@ func (f *FileInfo) listChildren() error { } func (f *FileInfo) getContent() error { - return nil + if f.Size <= 10*1024*1024 { + afs := &afero.Afero{Fs: f.Fs} + cByte, err := afs.ReadFile(f.Path) + if err != nil { + return nil + } + f.Content = string(cByte) + return nil + } else { + return errors.New("file is too large!") + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 84e5bd7aa..12e86e8ae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,12 +16,14 @@ "element-plus": "^2.2.13", "fit2cloud-ui-plus": "^0.0.1-beta.15", "js-md5": "^0.7.3", + "monaco-editor": "^0.34.0", "nprogress": "^0.2.0", "pinia": "^2.0.12", "pinia-plugin-persistedstate": "^1.6.1", "qs": "^6.10.3", "sass-loader": "^13.0.2", "unplugin-vue-define-options": "^0.7.3", + "vite-plugin-monaco-editor": "^1.1.0", "vue": "^3.2.25", "vue-i18n": "^9.1.9", "vue-router": "^4.0.12", @@ -6549,6 +6551,11 @@ "node": ">=0.10.0" } }, + "node_modules/monaco-editor": { + "version": "0.34.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.34.0.tgz", + "integrity": "sha512-VF+S5zG8wxfinLKLrWcl4WUizMx+LeJrG4PM/M78OhcwocpV0jiyhX/pG6Q9jIOhrb/ckYi6nHnaR5OojlOZCQ==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9274,6 +9281,14 @@ "vite": "^2.0.0 || ^3.0.0" } }, + "node_modules/vite-plugin-monaco-editor": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz", + "integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==", + "peerDependencies": { + "monaco-editor": ">=0.33.0" + } + }, "node_modules/vite-plugin-vue-setup-extend": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", @@ -14930,6 +14945,11 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "monaco-editor": { + "version": "0.34.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.34.0.tgz", + "integrity": "sha512-VF+S5zG8wxfinLKLrWcl4WUizMx+LeJrG4PM/M78OhcwocpV0jiyhX/pG6Q9jIOhrb/ckYi6nHnaR5OojlOZCQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17007,6 +17027,12 @@ "markdown-it": "^12.0.0" } }, + "vite-plugin-monaco-editor": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz", + "integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==", + "requires": {} + }, "vite-plugin-vue-setup-extend": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5dcc0bc10..f452508d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "element-plus": "^2.2.13", "fit2cloud-ui-plus": "^0.0.1-beta.15", "js-md5": "^0.7.3", + "monaco-editor": "^0.34.0", "nprogress": "^0.2.0", "pinia": "^2.0.12", "pinia-plugin-persistedstate": "^1.6.1", @@ -36,6 +37,7 @@ "sass-loader": "^13.0.2", "screenfull": "^6.0.2", "unplugin-vue-define-options": "^0.7.3", + "vite-plugin-monaco-editor": "^1.1.0", "vue": "^3.2.25", "vue-i18n": "^9.1.9", "vue-router": "^4.0.12", diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index fe1b71d12..c039df342 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -59,4 +59,9 @@ export namespace File { dst: string; type: string; } + + export interface FileEdit { + path: string; + content: string; + } } diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 52d1783dc..21c2acaa6 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -18,12 +18,21 @@ export const DeleteFile = (form: File.FileDelete) => { }; export const ChangeFileMode = (form: File.FileCreate) => { - return http.post('files/mode', form); + return http.post('files/mode', form); }; export const CompressFile = (form: File.FileCompress) => { - return http.post('files/compress', form); + return http.post('files/compress', form); }; + export const DeCompressFile = (form: File.FileDeCompress) => { - return http.post('files/decompress', form); + return http.post('files/decompress', form); +}; + +export const GetFileContent = (params: File.ReqFile) => { + return http.post('files/content', params); +}; + +export const SaveFileContent = (params: File.FileEdit) => { + return http.post('files/save', params); }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index f68cbe19e..7dba2bf3c 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -11,6 +11,7 @@ export default { reset: '重置', login: '登陆', conn: '连接', + login: '登录', }, table: { name: '名称', diff --git a/frontend/src/views/file-management/code-editor/index.vue b/frontend/src/views/file-management/code-editor/index.vue new file mode 100644 index 000000000..639b5d52f --- /dev/null +++ b/frontend/src/views/file-management/code-editor/index.vue @@ -0,0 +1,81 @@ + + + diff --git a/frontend/src/views/file-management/create/index.vue b/frontend/src/views/file-management/create/index.vue index 322949dda..6dc937343 100644 --- a/frontend/src/views/file-management/create/index.vue +++ b/frontend/src/views/file-management/create/index.vue @@ -82,7 +82,7 @@ const getMode = (val: number) => { }; let getPath = computed(() => { - if (addForm.path === '/') { + if (addForm.path.endsWith('/')) { return addForm.path + addForm.name; } else { return addForm.path + '/' + addForm.name; diff --git a/frontend/src/views/file-management/index.vue b/frontend/src/views/file-management/index.vue index 8857e5d17..0b73764e4 100644 --- a/frontend/src/views/file-management/index.vue +++ b/frontend/src/views/file-management/index.vue @@ -60,9 +60,9 @@ {{ $t('file.upload') }} {{ $t('file.search') }} {{ $t('file.remoteFile') }} - {{ $t('file.sync') }} + @@ -123,7 +130,7 @@ import { onMounted, reactive, ref } from '@vue/runtime-core'; import LayoutContent from '@/layout/layout-content.vue'; import ComplexTable from '@/components/complex-table/index.vue'; import i18n from '@/lang'; -import { GetFilesList, GetFilesTree, DeleteFile } from '@/api/modules/files'; +import { GetFilesList, GetFilesTree, DeleteFile, GetFileContent, SaveFileContent } from '@/api/modules/files'; import { dateFromat } from '@/utils/util'; import { File } from '@/api/interface/file'; import BreadCrumbs from '@/components/bread-crumbs/index.vue'; @@ -133,6 +140,7 @@ import ChangeRole from './change-role/index.vue'; import Compress from './compress/index.vue'; import Decompress from './decompress/index.vue'; import { useDeleteData } from '@/hooks/use-delete-data'; +import CodeEditor from './code-editor/index.vue'; let data = ref(); let selects = ref([]); @@ -147,6 +155,8 @@ let filePage = reactive({ open: false, createForm: { path: '/', isDir: false, mo let modePage = reactive({ open: false, modeForm: { path: '/', isDir: false, mode: 0o755 } }); let compressPage = reactive({ open: false, files: [''], name: '', dst: '' }); let deCompressPage = reactive({ open: false, path: '', name: '', dst: '', mimeType: '' }); +let editorPage = reactive({ open: false, content: '' }); +let codeReq = reactive({ path: '', expand: false }); const defaultProps = { children: 'children', @@ -183,12 +193,14 @@ const open = async (row: File.File) => { if (row.isDir) { const name = row.name; paths.value.push(name); - if (req.path === '/') { + if (req.path.endsWith('/')) { req.path = req.path + name; } else { req.path = req.path + '/' + name; } search(req); + } else { + openCodeEditor(row); } }; @@ -296,6 +308,25 @@ const closeDeCompress = () => { search(req); }; +const openCodeEditor = (row: File.File) => { + codeReq.path = row.path; + codeReq.expand = true; + GetFileContent(codeReq).then((res) => { + editorPage.content = res.data.content; + }); + editorPage.open = true; +}; + +const closeCodeEditor = () => { + editorPage.open = false; +}; + +const saveContent = (content: string) => { + SaveFileContent({ path: codeReq.path, content: content }).then(() => { + editorPage.open = false; + }); +}; + onMounted(() => { search(req); }); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index f4141b8a1..9518a2837 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,8 +8,8 @@ import viteCompression from 'vite-plugin-compression'; import VueSetupExtend from 'vite-plugin-vue-setup-extend'; import eslintPlugin from 'vite-plugin-eslint'; import vueJsx from '@vitejs/plugin-vue-jsx'; - import DefineOptions from 'unplugin-vue-define-options/vite'; +import MonacoEditorPlugin from 'vite-plugin-monaco-editor'; // @see: https://vitejs.dev/config/ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { @@ -36,6 +36,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { server: { port: viteEnv.VITE_PORT, open: viteEnv.VITE_OPEN, + host: '0.0.0.0', // https: false, proxy: { '/api/v1': { @@ -64,6 +65,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { // * name 可以写在 script 标签上 VueSetupExtend(), + MonacoEditorPlugin({}), viteEnv.VITE_REPORT && visualizer(), // * gzip compress viteEnv.VITE_BUILD_GZIP &&