mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat(文件): 图片支持预览
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string>('markedType', { type: String, required: t
|
||||
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
|
||||
const permission = defineModel<boolean>('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<any>()
|
||||
@@ -41,8 +51,8 @@ const options = computed<DropdownOption[]>(() => {
|
||||
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<RowData> = [
|
||||
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<RowData> = [
|
||||
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<RowData> = [
|
||||
{
|
||||
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"
|
||||
/>
|
||||
<edit-modal v-model:show="editorModal" v-model:file="editorFile" />
|
||||
<edit-modal v-model:show="editorModal" v-model:file="currentFile" />
|
||||
<preview-modal v-model:show="previewModal" v-model:path="currentFile" />
|
||||
<n-modal
|
||||
v-model:show="renameModal"
|
||||
preset="card"
|
||||
|
||||
43
web/src/views/file/PreviewModal.vue
Normal file
43
web/src/views/file/PreviewModal.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import file from '@/api/panel/file'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const path = defineModel<string>('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
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="'预览 - ' + path"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-image width="100%" :src="img" preview-disabled :show-toolbar="false">
|
||||
<template #placeholder>
|
||||
<n-spin />
|
||||
</template>
|
||||
</n-image>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user