From 1dc0042507dcf58902f9a4b8f9f1a9514685af4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Thu, 28 Nov 2024 20:32:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6):=20=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/file.go | 14 ++++++-- pkg/io/file.go | 5 +++ web/src/components/common/CodeEditor.vue | 2 +- web/src/utils/file/index.ts | 20 +++++++++++ web/src/views/file/ListTable.vue | 39 +++++++++++++++------ web/src/views/file/PreviewModal.vue | 43 ++++++++++++++++++++++++ web/src/views/task/CronView.vue | 2 +- 7 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 web/src/views/file/PreviewModal.vue diff --git a/internal/service/file.go b/internal/service/file.go index a23c4184..fdf5762d 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -3,6 +3,7 @@ package service import ( + "encoding/base64" "fmt" stdio "io" "net/http" @@ -15,6 +16,7 @@ import ( "time" "github.com/go-rat/chix" + "github.com/go-rat/utils/file" "github.com/spf13/cast" "github.com/TheTNB/panel/internal/app" @@ -81,13 +83,21 @@ func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { return } - content, err := io.Read(req.Path) + content, err := io.ReadBytes(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + mime, err := file.MimeType(req.Path) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - Success(w, content) + Success(w, chix.M{ + "mime": mime, + "content": base64.StdEncoding.EncodeToString(content), + }) } func (s *FileService) Save(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/io/file.go b/pkg/io/file.go index 358b7a76..393ca512 100644 --- a/pkg/io/file.go +++ b/pkg/io/file.go @@ -91,6 +91,11 @@ func Read(path string) (string, error) { return string(data), err } +// ReadBytes 读取文件 +func ReadBytes(path string) ([]byte, error) { + return os.ReadFile(path) +} + // FileInfo 获取文件大小 func FileInfo(path string) (os.FileInfo, error) { return os.Stat(path) diff --git a/web/src/components/common/CodeEditor.vue b/web/src/components/common/CodeEditor.vue index 03546bb8..6a76a846 100644 --- a/web/src/components/common/CodeEditor.vue +++ b/web/src/components/common/CodeEditor.vue @@ -21,7 +21,7 @@ const get = async () => { await file .content(props.path) .then((res) => { - data.value = res.data + data.value = atob(res.data.content) window.$message.success('获取成功') }) .catch(() => { diff --git a/web/src/utils/file/index.ts b/web/src/utils/file/index.ts index c2c2dab4..fe90a499 100644 --- a/web/src/utils/file/index.ts +++ b/web/src/utils/file/index.ts @@ -293,6 +293,25 @@ const isCompress = (name: string) => { return ['zip', 'bz2', 'tar', 'gz', 'tgz', 'xz', '7z'].includes(ext) } +const isImage = (name: string) => { + const ext = getExt(name) + return [ + 'png', + 'jpg', + 'jpeg', + 'gif', + 'bmp', + 'ico', + 'svg', + 'webp', + 'avif', + 'tiff', + 'heif', + 'heic', + 'jxl' + ].includes(ext) +} + const formatPercent = (num: any) => { num = Number(num) return Number(num.toFixed(2)) @@ -326,6 +345,7 @@ export { getFilename, getIconByExt, isCompress, + isImage, languageByPath, lastDirectory } diff --git a/web/src/views/file/ListTable.vue b/web/src/views/file/ListTable.vue index cd8fcd3d..7a451971 100644 --- a/web/src/views/file/ListTable.vue +++ b/web/src/views/file/ListTable.vue @@ -6,8 +6,17 @@ import type { RowData } from 'naive-ui/es/data-table/src/interface' import file from '@/api/panel/file' import TheIcon from '@/components/custom/TheIcon.vue' -import { checkName, checkPath, getExt, getFilename, getIconByExt, isCompress } from '@/utils/file' +import { + checkName, + checkPath, + getExt, + getFilename, + getIconByExt, + isCompress, + isImage +} from '@/utils/file' import EditModal from '@/views/file/EditModal.vue' +import PreviewModal from '@/views/file/PreviewModal.vue' import type { Marked } from '@/views/file/types' const loading = ref(false) @@ -19,7 +28,8 @@ const markedType = defineModel('markedType', { type: String, required: t const compress = defineModel('compress', { type: Boolean, required: true }) const permission = defineModel('permission', { type: Boolean, required: true }) const editorModal = ref(false) -const editorFile = ref('') +const previewModal = ref(false) +const currentFile = ref('') const showDropdown = ref(false) const selectedRow = ref() @@ -41,8 +51,8 @@ const options = computed(() => { if (selectedRow.value == null) return [] const options = [ { - label: selectedRow.value.dir ? '打开' : '编辑', - key: selectedRow.value.dir ? 'open' : 'edit' + label: selectedRow.value.dir ? '打开' : isImage(selectedRow.value.name) ? '预览' : '编辑', + key: selectedRow.value.dir ? 'open' : isImage(selectedRow.value.name) ? 'preview' : 'edit' }, { label: '复制', key: 'copy' }, { label: '移动', key: 'move' }, @@ -97,7 +107,7 @@ const columns: DataTableColumns = [ if (row.dir) { path.value = row.full } else { - editorFile.value = row.full + currentFile.value = row.full editorModal.value = true } } @@ -179,8 +189,12 @@ const columns: DataTableColumns = [ tertiary: true, onClick: () => { if (!row.dir && !row.symlink) { - editorFile.value = row.full - editorModal.value = true + currentFile.value = row.full + if (isImage(row.name)) { + previewModal.value = true + } else { + editorModal.value = true + } } else { path.value = row.full } @@ -189,7 +203,7 @@ const columns: DataTableColumns = [ { default: () => { if (!row.dir && !row.symlink) { - return '编辑' + return isImage(row.name) ? '预览' : '编辑' } else { return '打开' } @@ -529,9 +543,13 @@ const handleSelect = (key: string) => { path.value = selectedRow.value.full break case 'edit': - editorFile.value = selectedRow.value.full + currentFile.value = selectedRow.value.full editorModal.value = true break + case 'preview': + currentFile.value = selectedRow.value.full + previewModal.value = true + break case 'copy': markedType.value = 'copy' marked.value = [ @@ -662,7 +680,8 @@ onUnmounted(() => { :on-clickoutside="onCloseDropdown" @select="handleSelect" /> - + + +import file from '@/api/panel/file' + +const show = defineModel('show', { type: Boolean, required: true }) +const path = defineModel('path', { type: String, required: true }) + +const mime = ref('') +const content = ref('') +const img = computed(() => { + return `data:${mime.value};base64,${content.value}` +}) + +watch( + () => path.value, + () => { + content.value = '' + file.content(path.value).then((res) => { + mime.value = res.data.mime + content.value = res.data.content + }) + } +) + + + + + diff --git a/web/src/views/task/CronView.vue b/web/src/views/task/CronView.vue index 35078b30..77735616 100644 --- a/web/src/views/task/CronView.vue +++ b/web/src/views/task/CronView.vue @@ -209,7 +209,7 @@ const handleEdit = async (row: any) => { editTask.value.id = row.id editTask.value.name = row.name editTask.value.time = row.time - editTask.value.script = res.data + editTask.value.script = atob(res.data.content) editModal.value = true }) })