feat: 增加文件编辑功能

This commit is contained in:
zhengkunwang223 2022-09-01 19:02:33 +08:00
parent 1afe79068a
commit b172a5124e
15 changed files with 235 additions and 15 deletions

View File

@ -104,3 +104,30 @@ func (b *BaseApi) DeCompressFile(c *gin.Context) {
} }
helper.SuccessWithData(c, nil) 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)
}

View File

@ -47,3 +47,8 @@ type FileDeCompress struct {
Type string Type string
Path string Path string
} }
type FileEdit struct {
Path string
Content string
}

View File

@ -9,6 +9,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"path/filepath" "path/filepath"
"strings"
) )
type FileService struct { 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)) 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 { func getUuid() string {
b := make([]byte, 16) b := make([]byte, 16)
io.ReadFull(rand.Reader, b) io.ReadFull(rand.Reader, b)

View File

@ -22,6 +22,8 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/mode", baseApi.ChangeFileMode) fileRouter.POST("/mode", baseApi.ChangeFileMode)
fileRouter.POST("/compress", baseApi.CompressFile) fileRouter.POST("/compress", baseApi.CompressFile)
fileRouter.POST("/decompress", baseApi.DeCompressFile) fileRouter.POST("/decompress", baseApi.DeCompressFile)
fileRouter.POST("/content", baseApi.GetContent)
fileRouter.POST("/save", baseApi.SaveContent)
} }
} }

View File

@ -7,7 +7,6 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path"
"path/filepath" "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 { 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) file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil { if err != nil {
return err return err

View File

@ -2,6 +2,7 @@ package files
import ( import (
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/spf13/afero" "github.com/spf13/afero"
"os" "os"
"path" "path"
@ -129,5 +130,15 @@ func (f *FileInfo) listChildren() error {
} }
func (f *FileInfo) getContent() 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!")
}
} }

View File

@ -16,12 +16,14 @@
"element-plus": "^2.2.13", "element-plus": "^2.2.13",
"fit2cloud-ui-plus": "^0.0.1-beta.15", "fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"monaco-editor": "^0.34.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.12", "pinia": "^2.0.12",
"pinia-plugin-persistedstate": "^1.6.1", "pinia-plugin-persistedstate": "^1.6.1",
"qs": "^6.10.3", "qs": "^6.10.3",
"sass-loader": "^13.0.2", "sass-loader": "^13.0.2",
"unplugin-vue-define-options": "^0.7.3", "unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"vue-router": "^4.0.12", "vue-router": "^4.0.12",
@ -6549,6 +6551,11 @@
"node": ">=0.10.0" "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": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -9274,6 +9281,14 @@
"vite": "^2.0.0 || ^3.0.0" "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": { "node_modules/vite-plugin-vue-setup-extend": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", "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==", "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==",
"dev": true "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": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -17007,6 +17027,12 @@
"markdown-it": "^12.0.0" "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": { "vite-plugin-vue-setup-extend": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",

View File

@ -29,6 +29,7 @@
"element-plus": "^2.2.13", "element-plus": "^2.2.13",
"fit2cloud-ui-plus": "^0.0.1-beta.15", "fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"monaco-editor": "^0.34.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.12", "pinia": "^2.0.12",
"pinia-plugin-persistedstate": "^1.6.1", "pinia-plugin-persistedstate": "^1.6.1",
@ -36,6 +37,7 @@
"sass-loader": "^13.0.2", "sass-loader": "^13.0.2",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3", "unplugin-vue-define-options": "^0.7.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"vue-router": "^4.0.12", "vue-router": "^4.0.12",

View File

@ -59,4 +59,9 @@ export namespace File {
dst: string; dst: string;
type: string; type: string;
} }
export interface FileEdit {
path: string;
content: string;
}
} }

View File

@ -18,12 +18,21 @@ export const DeleteFile = (form: File.FileDelete) => {
}; };
export const ChangeFileMode = (form: File.FileCreate) => { export const ChangeFileMode = (form: File.FileCreate) => {
return http.post<File.FileCreate>('files/mode', form); return http.post<File.File>('files/mode', form);
}; };
export const CompressFile = (form: File.FileCompress) => { export const CompressFile = (form: File.FileCompress) => {
return http.post<File.FileCompress>('files/compress', form); return http.post<File.File>('files/compress', form);
}; };
export const DeCompressFile = (form: File.FileDeCompress) => { export const DeCompressFile = (form: File.FileDeCompress) => {
return http.post<File.FileCompress>('files/decompress', form); return http.post<File.File>('files/decompress', form);
};
export const GetFileContent = (params: File.ReqFile) => {
return http.post<File.File>('files/content', params);
};
export const SaveFileContent = (params: File.FileEdit) => {
return http.post<File.File>('files/save', params);
}; };

View File

@ -11,6 +11,7 @@ export default {
reset: '重置', reset: '重置',
login: '登陆', login: '登陆',
conn: '连接', conn: '连接',
login: '登录',
}, },
table: { table: {
name: '名称', name: '名称',

View File

@ -0,0 +1,81 @@
<template>
<el-dialog v-model="open" :title="'code editor'" @opened="onOpen" :before-close="handleClose">
<div>
<div id="codeBox" style="height: 600px"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="saveContent">{{ $t('commons.button.confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import * as monaco from 'monaco-editor';
import { reactive } from 'vue';
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
const props = defineProps({
open: {
type: Boolean,
default: false,
},
language: {
type: String,
default: 'json',
},
content: {
type: String,
default: '',
},
});
let data = reactive({
content: '',
language: '',
});
const em = defineEmits(['close', 'save']);
const handleClose = () => {
if (editor) {
editor.dispose();
}
em('close', false);
};
const initEditor = () => {
if (editor) {
editor.dispose();
}
const codeBox = document.getElementById('codeBox');
editor = monaco.editor.create(codeBox as HTMLElement, {
theme: 'vs-dark', //vs, hc-black, or vs-dark
value: data.content,
readOnly: false,
automaticLayout: true,
language: data.language,
folding: true, //
roundedSelection: false, //
});
editor.onDidChangeModelContent(() => {
if (editor) {
data.content = editor.getValue();
}
});
};
const saveContent = () => {
em('save', data.content);
};
const onOpen = () => {
data.content = props.content;
data.language = props.language;
initEditor();
};
</script>

View File

@ -82,7 +82,7 @@ const getMode = (val: number) => {
}; };
let getPath = computed(() => { let getPath = computed(() => {
if (addForm.path === '/') { if (addForm.path.endsWith('/')) {
return addForm.path + addForm.name; return addForm.path + addForm.name;
} else { } else {
return addForm.path + '/' + addForm.name; return addForm.path + '/' + addForm.name;

View File

@ -60,9 +60,9 @@
<el-button type="primary" plain> {{ $t('file.upload') }}</el-button> <el-button type="primary" plain> {{ $t('file.upload') }}</el-button>
<el-button type="primary" plain> {{ $t('file.search') }}</el-button> <el-button type="primary" plain> {{ $t('file.search') }}</el-button>
<el-button type="primary" plain> {{ $t('file.remoteFile') }}</el-button> <el-button type="primary" plain> {{ $t('file.remoteFile') }}</el-button>
<el-button type="primary" plain> {{ $t('file.sync') }}</el-button> <!-- <el-button type="primary" plain> {{ $t('file.sync') }}</el-button>
<el-button type="primary" plain> {{ $t('file.terminal') }}</el-button> <el-button type="primary" plain> {{ $t('file.terminal') }}</el-button>
<el-button type="primary" plain> {{ $t('file.shareList') }}</el-button> <el-button type="primary" plain> {{ $t('file.shareList') }}</el-button> -->
</template> </template>
<el-table-column :label="$t('commons.table.name')" min-width="250" fix show-overflow-tooltip> <el-table-column :label="$t('commons.table.name')" min-width="250" fix show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
@ -114,6 +114,13 @@
:mimeType="deCompressPage.mimeType" :mimeType="deCompressPage.mimeType"
@close="closeDeCompress" @close="closeDeCompress"
></Decompress> ></Decompress>
<CodeEditor
:open="editorPage.open"
:language="'json'"
:content="editorPage.content"
@close="closeCodeEditor"
@save="saveContent"
></CodeEditor>
</el-row> </el-row>
</LayoutContent> </LayoutContent>
</template> </template>
@ -123,7 +130,7 @@ import { onMounted, reactive, ref } from '@vue/runtime-core';
import LayoutContent from '@/layout/layout-content.vue'; import LayoutContent from '@/layout/layout-content.vue';
import ComplexTable from '@/components/complex-table/index.vue'; import ComplexTable from '@/components/complex-table/index.vue';
import i18n from '@/lang'; 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 { dateFromat } from '@/utils/util';
import { File } from '@/api/interface/file'; import { File } from '@/api/interface/file';
import BreadCrumbs from '@/components/bread-crumbs/index.vue'; 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 Compress from './compress/index.vue';
import Decompress from './decompress/index.vue'; import Decompress from './decompress/index.vue';
import { useDeleteData } from '@/hooks/use-delete-data'; import { useDeleteData } from '@/hooks/use-delete-data';
import CodeEditor from './code-editor/index.vue';
let data = ref(); let data = ref();
let selects = ref<any>([]); let selects = ref<any>([]);
@ -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 modePage = reactive({ open: false, modeForm: { path: '/', isDir: false, mode: 0o755 } });
let compressPage = reactive({ open: false, files: [''], name: '', dst: '' }); let compressPage = reactive({ open: false, files: [''], name: '', dst: '' });
let deCompressPage = reactive({ open: false, path: '', name: '', dst: '', mimeType: '' }); let deCompressPage = reactive({ open: false, path: '', name: '', dst: '', mimeType: '' });
let editorPage = reactive({ open: false, content: '' });
let codeReq = reactive({ path: '', expand: false });
const defaultProps = { const defaultProps = {
children: 'children', children: 'children',
@ -183,12 +193,14 @@ const open = async (row: File.File) => {
if (row.isDir) { if (row.isDir) {
const name = row.name; const name = row.name;
paths.value.push(name); paths.value.push(name);
if (req.path === '/') { if (req.path.endsWith('/')) {
req.path = req.path + name; req.path = req.path + name;
} else { } else {
req.path = req.path + '/' + name; req.path = req.path + '/' + name;
} }
search(req); search(req);
} else {
openCodeEditor(row);
} }
}; };
@ -296,6 +308,25 @@ const closeDeCompress = () => {
search(req); 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(() => { onMounted(() => {
search(req); search(req);
}); });

View File

@ -8,8 +8,8 @@ import viteCompression from 'vite-plugin-compression';
import VueSetupExtend from 'vite-plugin-vue-setup-extend'; import VueSetupExtend from 'vite-plugin-vue-setup-extend';
import eslintPlugin from 'vite-plugin-eslint'; import eslintPlugin from 'vite-plugin-eslint';
import vueJsx from '@vitejs/plugin-vue-jsx'; import vueJsx from '@vitejs/plugin-vue-jsx';
import DefineOptions from 'unplugin-vue-define-options/vite'; import DefineOptions from 'unplugin-vue-define-options/vite';
import MonacoEditorPlugin from 'vite-plugin-monaco-editor';
// @see: https://vitejs.dev/config/ // @see: https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => { export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
@ -36,6 +36,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
server: { server: {
port: viteEnv.VITE_PORT, port: viteEnv.VITE_PORT,
open: viteEnv.VITE_OPEN, open: viteEnv.VITE_OPEN,
host: '0.0.0.0',
// https: false, // https: false,
proxy: { proxy: {
'/api/v1': { '/api/v1': {
@ -64,6 +65,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// * name 可以写在 script 标签上 // * name 可以写在 script 标签上
VueSetupExtend(), VueSetupExtend(),
MonacoEditorPlugin({}),
viteEnv.VITE_REPORT && visualizer(), viteEnv.VITE_REPORT && visualizer(),
// * gzip compress // * gzip compress
viteEnv.VITE_BUILD_GZIP && viteEnv.VITE_BUILD_GZIP &&