Feature: add grid view for file explorer

This commit is contained in:
萌萌哒赫萝 2023-02-24 17:09:32 +08:00
parent d533f6f5f3
commit 69e1b48ecf
6 changed files with 349 additions and 50 deletions

View File

@ -0,0 +1,63 @@
<template>
<el-image
:src="isShowThumbnail && item.isImage ?
base64Url
: require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
>
<template #placeholder>
<el-icon>
<Loading />
</el-icon>
</template>
<template #error>
<el-image
:src="require(`../manage/pages/assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
/>
</template>
</el-image>
</template>
<script lang="ts" setup>
import { ref, onBeforeMount } from 'vue'
import { getFileIconPath } from '@/manage/utils/common'
import { Loading } from '@element-plus/icons-vue'
const base64Url = ref('')
const props = defineProps(
{
isShowThumbnail: {
type: Boolean,
required: true
},
item: {
type: Object,
required: true
},
url: {
type: String,
required: true
},
headers: {
type: Object,
required: true
}
}
)
const urlCreateObjectURL = async () => {
await fetch(props.url, {
method: 'GET',
headers: props.headers
}).then(res => res.blob()).then(blob => {
base64Url.value = URL.createObjectURL(blob)
})
}
onBeforeMount(async () => {
await urlCreateObjectURL()
})
</script>

View File

@ -369,6 +369,22 @@ ea/*
</el-dropdown-item> </el-dropdown-item>
</template> </template>
</el-dropdown> </el-dropdown>
<el-button-group
size="small"
style="margin-left: 10px;width: 80px;flex-shrink: 0;"
type="primary"
>
<el-button
:icon="Grid"
:type="showFileStyle === 'grid' ? 'primary' : 'info'"
@click="handleViewChange('grid')"
/>
<el-button
:icon="Fold"
:type="showFileStyle === 'list' ? 'primary' : 'info'"
@click="handleViewChange('list')"
/>
</el-button-group>
<el-input-number <el-input-number
v-if="paging" v-if="paging"
v-model="currentPage" v-model="currentPage"
@ -409,14 +425,17 @@ https://www.baidu.com/img/bd_logo1.png"
</template> </template>
</el-dialog> </el-dialog>
<div <div
v-show="showFileStyle === 'list'"
class="layout-table" class="layout-table"
style="margin: 0 15px 15px 15px;overflow-y: auto;overflow-x: hidden;height: 80vh;" style="margin: 0 15px 15px 15px;overflow-y: auto;overflow-x: hidden;height: 80vh;"
> >
<el-auto-resizer> <el-auto-resizer>
<template #default="{ height, width }"> <template
#default="{ height, width }"
>
<el-table-v2 <el-table-v2
ref="elTable" ref="elTable"
:columns="columns" :columns="columns "
:data="currentPageFilesInfo" :data="currentPageFilesInfo"
:row-class="rowClass" :row-class="rowClass"
:width="width" :width="width"
@ -425,6 +444,184 @@ https://www.baidu.com/img/bd_logo1.png"
</template> </template>
</el-auto-resizer> </el-auto-resizer>
</div> </div>
<div
v-show="showFileStyle === 'grid'"
class="layout-grid"
style="margin: 0 15px 15px 15px;overflow-y: auto;overflow-x: hidden;height: 80vh;"
>
<el-col
:span="24"
>
<el-row
:gutter="16"
>
<el-col
v-for="(item,index) in currentPageFilesInfo"
:key="index"
:xs="24"
:sm="12"
:md="8"
:lg="3"
:xl="2"
>
<el-card
v-if="item.match || !searchText"
:body-style="{ padding: '0px', height: '150px', width: '100%', background: item.checked ? '#f2f2f2' : '#fff' }"
style="margin-bottom: 10px;"
shadow="hover"
>
<el-image
v-if="!item.isDir && currentPicBedName !== 'webdavplist'"
:src="isShowThumbnail && item.isImage ?
item.url
: require(`./assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
@click="handleClickFile(item)"
>
<template #placeholder>
<el-icon>
<Loading />
</el-icon>
</template>
<template #error>
<el-image
:src="require(`./assets/icons/${getFileIconPath(item.fileName ?? '')}`)"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
/>
</template>
</el-image>
<ImageWebdav
v-else-if="!item.isDir && currentPicBedName === 'webdavplist'"
:is-show-thumbnail="isShowThumbnail"
:item="item"
:headers="getBase64ofWebdav()"
:url="item.url"
@click="handleClickFile(item)"
/>
<el-image
v-else
:src="require('./assets/icons/folder.png')"
fit="contain"
style="height: 100px;width: 100%;margin: 0 auto;"
@click="handleClickFile(item)"
/>
<div
style="align-items: center;display: flex;justify-content: center;"
@click="copyToClipboard(item.fileName ?? '')"
>
<el-tooltip
placement="top"
effect="light"
:content="item.fileName"
>
<el-link
style="font-size: 12px;font-family: Arial, Helvetica, sans-serif;"
:underline="false"
:type="item.checked ? 'primary' : 'info'"
>
{{ formatFileName(item.fileName ?? '', 10) }}
</el-link>
</el-tooltip>
</div>
<el-row
style="display: flex;"
justify="space-between"
align="middle"
>
<el-row>
<el-icon
v-if="!item.isDir || !showRenameFileIcon"
size="20"
style="cursor: pointer;"
color="#409EFF"
@click="handleRenameFile(item)"
>
<Edit />
</el-icon>
<el-dropdown>
<template #default>
<el-icon
size="20"
style="cursor: pointer;"
color="#409EFF"
@click="copyToClipboard(formatLink(item.url, item.fileName, manageStore.config.settings.pasteForma ?? '$markdown', manageStore.config.settings.customPasteFormat ?? '$url'))"
>
<CopyDocument />
</el-icon>
</template>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'url'))"
>
Url
</el-dropdown-item>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'markdown'))"
>
Markdown
</el-dropdown-item>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'markdown-with-link'))"
>
Markdown-link
</el-dropdown-item>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'html'))"
>
Html
</el-dropdown-item>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'bbcode'))"
>
BBCode
</el-dropdown-item>
<el-dropdown-item
@click="copyToClipboard(formatLink(item.url, item.fileName, 'custom', manageStore.config.settings.customPasteFormat))"
>
自定义
</el-dropdown-item>
<el-dropdown-item
v-if="showPresignedUrl"
@click="async () => {
copyToClipboard(await getPreSignedUrl(item))
}"
>
预签名链接
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-icon
size="20"
style="cursor: pointer;"
color="#409EFF"
@click="handleShowFileInfo(item)"
>
<Document />
</el-icon>
<el-icon
size="20"
style="cursor: pointer;"
color="#FFB6C1"
@click="handleDeleteFile(item)"
>
<DeleteFilled />
</el-icon>
</el-row>
<el-checkbox
v-model="item.checked"
size="large"
@change="handleCheckChange(item)"
/>
</el-row>
</el-card>
</el-col>
</el-row>
</el-col>
</div>
<el-image-viewer <el-image-viewer
v-if="showImagePreview" v-if="showImagePreview"
:url-list="ImagePreviewList" :url-list="ImagePreviewList"
@ -997,9 +1194,9 @@ https://www.baidu.com/img/bd_logo1.png"
<script lang="tsx" setup> <script lang="tsx" setup>
import { ref, reactive, watch, onBeforeMount, computed, onBeforeUnmount } from 'vue' import { ref, reactive, watch, onBeforeMount, computed, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Close, Folder, FolderAdd, Upload, CircleClose, Loading, CopyDocument, Edit, DocumentAdd, Link, Refresh, ArrowRight, HomeFilled, Document, Coin, Download, DeleteFilled, Sort, FolderOpened } from '@element-plus/icons-vue' import { Grid, Fold, Close, Folder, FolderAdd, Upload, CircleClose, Loading, CopyDocument, Edit, DocumentAdd, Link, Refresh, ArrowRight, HomeFilled, Document, Coin, Download, DeleteFilled, Sort, FolderOpened } from '@element-plus/icons-vue'
import { useManageStore } from '../store/manageStore' import { useManageStore } from '../store/manageStore'
import { renameFile, formatLink, formatFileName, getFileIconPath, formatFileSize, getExtension, isValidUrl } from '../utils/common' import { renameFile, formatLink, formatFileName, getFileIconPath, formatFileSize, getExtension, isValidUrl, svg } from '../utils/common'
import { ipcRenderer, clipboard, IpcRendererEvent } from 'electron' import { ipcRenderer, clipboard, IpcRendererEvent } from 'electron'
import { fileCacheDbInstance } from '../store/bucketFileDb' import { fileCacheDbInstance } from '../store/bucketFileDb'
import { trimPath } from '~/main/manage/utils/common' import { trimPath } from '~/main/manage/utils/common'
@ -1017,7 +1214,8 @@ import {
ElDropdownMenu, ElDropdownMenu,
ElProgress, ElProgress,
ElLink, ElLink,
ElTag ElTag,
ElCard
} from 'element-plus' } from 'element-plus'
import type { Column, RowClassNameGetter } from 'element-plus' import type { Column, RowClassNameGetter } from 'element-plus'
import { useFileTransferStore } from '@/manage/store/manageStore' import { useFileTransferStore } from '@/manage/store/manageStore'
@ -1029,6 +1227,8 @@ import { getConfig, saveConfig } from '../utils/dataSender'
import { marked } from 'marked' import { marked } from 'marked'
import { textFileExt } from '../utils/textfile' import { textFileExt } from '../utils/textfile'
import { videoExt } from '../utils/videofile' import { videoExt } from '../utils/videofile'
import ImageWebdav from '@/components/ImageWebdav.vue'
/* /*
configMap:{ configMap:{
prefix: string, -> baseDir prefix: string, -> baseDir
@ -1040,16 +1240,6 @@ configMap:{
bucketConfig bucketConfig
} }
*/ */
const svg = `
<path class="path" d="
M 30 15
L 28 17
M 25.61 25.61
A 15 15, 0, 0, 1, 15 30
A 15 15, 0, 1, 1, 27.99 7.5
L 15 15
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`
const linkArray = [ const linkArray = [
{ key: 'Url', value: 'url' }, { key: 'Url', value: 'url' },
@ -1108,6 +1298,7 @@ const textfileContent = ref('')
const isShowVideoFileDialog = ref(false) const isShowVideoFileDialog = ref(false)
const videoFileUrl = ref('') const videoFileUrl = ref('')
const videoPlayerHeaders = ref({}) const videoPlayerHeaders = ref({})
const showFileStyle = ref<'list' | 'grid'>('grid')
const showCustomUrlSelectList = computed(() => ['tcyun', 'aliyun', 'qiniu', 'github'].includes(currentPicBedName.value)) const showCustomUrlSelectList = computed(() => ['tcyun', 'aliyun', 'qiniu', 'github'].includes(currentPicBedName.value))
@ -1129,6 +1320,17 @@ const downloadedTaskList = computed(() => downloadTaskList.value.filter(item =>
const isAutoCustomUrl = computed(() => manageStore.config.picBed[configMap.alias].isAutoCustomUrl === undefined ? true : manageStore.config.picBed[configMap.alias].isAutoCustomUrl) const isAutoCustomUrl = computed(() => manageStore.config.picBed[configMap.alias].isAutoCustomUrl === undefined ? true : manageStore.config.picBed[configMap.alias].isAutoCustomUrl)
function handleViewChange (val: 'list' | 'grid') {
showFileStyle.value = val
}
function getBase64ofWebdav () {
const headers = {
Authorization: 'Basic ' + Buffer.from(`${manageStore.config.picBed[configMap.alias].username}:${manageStore.config.picBed[configMap.alias].password}`).toString('base64')
}
return headers
}
function startRefreshUploadTask () { function startRefreshUploadTask () {
refreshUploadTaskId.value = setInterval(() => { refreshUploadTaskId.value = setInterval(() => {
ipcRenderer.invoke('getUploadTaskList').then((res: any) => { ipcRenderer.invoke('getUploadTaskList').then((res: any) => {
@ -1613,6 +1815,7 @@ async function resetParam (force: boolean = false) {
previewedImage.value = '' previewedImage.value = ''
isShowFileInfo.value = false isShowFileInfo.value = false
lastChoosed.value = -1 lastChoosed.value = -1
showFileStyle.value = manageStore.config.picBed[configMap.alias].isShowList ? 'list' : 'grid'
if (!isAutoRefresh.value && !force && !paging.value) { if (!isAutoRefresh.value && !force && !paging.value) {
const cachedData = await searchExistFileList() const cachedData = await searchExistFileList()
if (cachedData.length > 0) { if (cachedData.length > 0) {
@ -2769,21 +2972,14 @@ const columns: Column<any>[] = [
item.match || !searchText.value item.match || !searchText.value
? item.isDir || !showRenameFileIcon.value ? item.isDir || !showRenameFileIcon.value
? <span></span> ? <span></span>
: <ElTooltip : <ElIcon
placement="top" size="20"
content="重命名" style="cursor: pointer;"
effect='light' color="#409EFF"
hide-after={150} onClick={() => handleRenameFile(item)}
> >
<ElIcon <Edit />
size="20" </ElIcon>
style="cursor: pointer;"
color="#409EFF"
onClick={() => handleRenameFile(item)}
>
<Edit />
</ElIcon>
</ElTooltip>
: <template></template> : <template></template>
) )
}, },
@ -2933,20 +3129,13 @@ const columns: Column<any>[] = [
width: 30, width: 30,
cellRenderer: ({ rowData: item }) => ( cellRenderer: ({ rowData: item }) => (
item.match || !searchText.value item.match || !searchText.value
? <ElTooltip ? <ElIcon
placement="top" style="cursor: pointer;"
content="删除" color="red"
effect='light' onClick={() => handleDeleteFile(item)}
hide-after={150}
> >
<ElIcon <DeleteFilled />
style="cursor: pointer;" </ElIcon>
color="red"
onClick={() => handleDeleteFile(item)}
>
<DeleteFilled />
</ElIcon>
</ElTooltip>
: <template></template> : <template></template>
) )
} }

View File

@ -19,6 +19,15 @@
show-icon show-icon
center center
/> />
<div
v-if="isLoading"
v-loading="isLoading"
element-loading-text="加载中..."
:element-loading-spinner="svg"
element-loading-svg-view-box="0, 0, 50, 50"
element-loading-background="rgba(122, 122, 122, 0.5)"
style="width: 100%;height: 100%;"
/>
<el-row> <el-row>
<el-col <el-col
v-for="item in sortedAllConfigAliasMap" v-for="item in sortedAllConfigAliasMap"
@ -255,7 +264,7 @@ import { getConfig, saveConfig, removeConfig } from '../utils/dataSender'
import { shell } from 'electron' import { shell } from 'electron'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useManageStore } from '../store/manageStore' import { useManageStore } from '../store/manageStore'
import { formObjToTableData } from '../utils/common' import { formObjToTableData, svg } from '../utils/common'
import { getConfig as getPicBedsConfig } from '@/utils/dataSender' import { getConfig as getPicBedsConfig } from '@/utils/dataSender'
import { formatEndpoint } from '~/main/manage/utils/common' import { formatEndpoint } from '~/main/manage/utils/common'
@ -266,6 +275,7 @@ const dataForTable = reactive([] as any[])
const allConfigAliasMap = reactive({} as IStringKeyMap) const allConfigAliasMap = reactive({} as IStringKeyMap)
const router = useRouter() const router = useRouter()
const manageStore = useManageStore() const manageStore = useManageStore()
const isLoading = ref(false)
const sortedAllConfigAliasMap = computed(() => { const sortedAllConfigAliasMap = computed(() => {
const sorted = Object.values(allConfigAliasMap).sort((a, b) => { const sorted = Object.values(allConfigAliasMap).sort((a, b) => {
@ -517,6 +527,7 @@ const handleConfigClick = async (item: any) => {
} }
function handleConfigImport (alias: string) { function handleConfigImport (alias: string) {
isLoading.value = true
const selectedConfig = existingConfiguration[alias] const selectedConfig = existingConfiguration[alias]
if (selectedConfig) { if (selectedConfig) {
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => { supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
@ -528,6 +539,7 @@ function handleConfigImport (alias: string) {
} }
}) })
} }
isLoading.value = false
} }
async function getCurrentConfigList () { async function getCurrentConfigList () {

View File

@ -105,6 +105,23 @@
@change="handelIsShowThumbnailChange" @change="handelIsShowThumbnailChange"
/> />
</el-form-item> </el-form-item>
<el-form-item>
<template #label>
<span
style="position:absolute;left: 0;"
>文件列表默认显示方式
</span>
</template>
<el-switch
v-model="form.isShowList"
style="position:absolute;right: 0;"
active-text="列表"
inactive-text="卡片"
active-color="#13ce66"
inactive-color="orange"
@change="handelIsShowListChange"
/>
</el-form-item>
<el-form-item> <el-form-item>
<template #label> <template #label>
<span <span
@ -431,7 +448,8 @@ const form = reactive<IStringKeyMap>({
customRename: false, customRename: false,
isAutoRefresh: false, isAutoRefresh: false,
isIgnoreCase: false, isIgnoreCase: false,
isForceCustomUrlHttps: false isForceCustomUrlHttps: false,
isShowList: false
}) })
const downloadDir = ref('') const downloadDir = ref('')
@ -517,6 +535,12 @@ function handelIsShowThumbnailChange (val:ICheckBoxValueType) {
}) })
} }
function handelIsShowListChange (val:ICheckBoxValueType) {
saveConfig({
'settings.isShowList': val
})
}
function handelisIgnoreCaseChange (val:ICheckBoxValueType) { function handelisIgnoreCaseChange (val:ICheckBoxValueType) {
saveConfig({ saveConfig({
'settings.isIgnoreCase': val 'settings.isIgnoreCase': val

View File

@ -83,10 +83,10 @@ export function formatFileSize (size: number) {
return `${(size / Math.pow(2, index * 10)).toFixed(2)} ${units[index]}` return `${(size / Math.pow(2, index * 10)).toFixed(2)} ${units[index]}`
} }
export function formatFileName (fileName: string) { export function formatFileName (fileName: string, length: number = 20) {
const ext = path.extname(fileName) const ext = path.extname(fileName)
const name = path.basename(fileName, ext) const name = path.basename(fileName, ext)
return name.length > 20 ? `${name.slice(0, 20)}...${ext}` : fileName return name.length > length ? `${name.slice(0, length)}...${ext}` : fileName
} }
export const getExtension = (fileName: string) => path.extname(fileName).slice(1) export const getExtension = (fileName: string) => path.extname(fileName).slice(1)
@ -143,3 +143,14 @@ export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'str
} }
} }
} }
export const svg = `
<path class="path" d="
M 30 15
L 28 17
M 25.61 25.61
A 15 15, 0, 0, 1, 15 30
A 15 15, 0, 1, 1, 27.99 7.5
L 15 15
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`

View File

@ -138,10 +138,10 @@
v-for="(item, index) in filterList" v-for="(item, index) in filterList"
:key="item.id" :key="item.id"
:xs="24" :xs="24"
:sm="8" :sm="12"
:md="6" :md="8"
:lg="4" :lg="3"
:xl="3" :xl="2"
class="gallery-list__img" class="gallery-list__img"
> >
<div <div