feat:文件管理批量选择

This commit is contained in:
jxxghp
2024-06-20 15:32:17 +08:00
parent 5dd071adf4
commit 4f67bb0250
3 changed files with 164 additions and 69 deletions
+46 -44
View File
@@ -13,7 +13,7 @@ const display = useDisplay()
// 输入参数 // 输入参数
const props = defineProps({ const props = defineProps({
path: String, paths: Array<string>,
target: String, target: String,
logids: Array<number>, logids: Array<number>,
}) })
@@ -50,7 +50,18 @@ const progressText = ref('请稍候 ...')
// 整理进度 // 整理进度
const progressValue = ref(0) const progressValue = ref(0)
// 文件转移表单 // 标题
const dialogTitle = computed(() => {
if (props.paths) {
if (props.paths.length > 1) return `整理 - 共 ${props.paths.length}`
return `整理 - ${props.paths[0]}`
} else if (props.logids) {
return `整理 - 共 ${props.logids.length}`
}
return '手动整理'
})
// 表单
const transferForm = reactive({ const transferForm = reactive({
logid: 0, logid: 0,
path: '', path: '',
@@ -77,12 +88,6 @@ const targetDirectories = computed(() => {
return [...new Set(directories)] return [...new Set(directories)]
}) })
// 监听输入变化
watchEffect(() => {
transferForm.path = props.path ?? ''
transferForm.target = props.target ?? null
})
// 监听目的路径变化,自动查询目录的刮削配置 // 监听目的路径变化,自动查询目录的刮削配置
watch(transferForm, async () => { watch(transferForm, async () => {
if (transferForm.target) { if (transferForm.target) {
@@ -117,47 +122,25 @@ function stopLoadingProgress() {
} }
// 整理文件 // 整理文件
// eslint-disable-next-line sonarjs/cognitive-complexity
async function transfer() { async function transfer() {
if (!props.logids && !props.path) return if (!props.logids && !props.paths) return
// 显示进度条 // 显示进度条
progressDialog.value = true progressDialog.value = true
// 开始监听进度 // 开始监听进度
startLoadingProgress() startLoadingProgress()
if (props.path) { // 文件整理
// 文件整理 if (props.paths) {
try { for (const path of props.paths) {
const result: { [key: string]: any } = await api.post( await handleTransfer(path)
'transfer/manual',
{},
{
params: transferForm,
},
)
// 显示结果
if (result.success) $toast.success(`${props.path} 整理完成!`)
else $toast.error(`${props.path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
} }
} else if (props.logids) { }
// 日志整理
// 日志整理
if (props.logids) {
for (const logid of props.logids) { for (const logid of props.logids) {
transferForm.logid = logid await handleTransferLog(logid)
try {
const result: { [key: string]: any } = await api.post(
'transfer/manual',
{},
{
params: transferForm,
},
)
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
} }
} }
@@ -169,6 +152,28 @@ async function transfer() {
emit('done') emit('done')
} }
// 整理文件
async function handleTransfer(path: string) {
transferForm.path = path
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
if (!result.success) $toast.error(`文件 ${path} 整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 整理日志
async function handleTransferLog(logid: number) {
transferForm.logid = logid
try {
const result: { [key: string]: any } = await api.post('transfer/manual', {}, { params: transferForm })
if (!result.success) $toast.error(`历史记录 ${logid} 重新整理失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 调用API,加载当前系统环境设置 // 调用API,加载当前系统环境设置
async function loadSystemSettings() { async function loadSystemSettings() {
try { try {
@@ -199,10 +204,7 @@ onMounted(() => {
<template> <template>
<VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value"> <VDialog scrollable max-width="50rem" :fullscreen="!display.mdAndUp.value">
<VCard <VCard :title="dialogTitle" class="rounded-t">
:title="`${props.path ? `整理 - ${props.path}` : `整理 - 共 ${props.logids?.length} 条记录`}`"
class="rounded-t"
>
<DialogCloseBtn @click="emit('close')" /> <DialogCloseBtn @click="emit('close')" />
<VDivider /> <VDivider />
<VCardText> <VCardText>
+117 -24
View File
@@ -46,6 +46,9 @@ const createConfirm = useConfirm()
// 提示框 // 提示框
const $toast = useToast() const $toast = useToast()
// 是否选择模式
const selectMode = ref(false)
// 是否正在加载 // 是否正在加载
const loading = ref(true) const loading = ref(true)
@@ -82,6 +85,9 @@ const renameAll = ref(false)
// 当前操作项 // 当前操作项
const currentItem = ref<FileItem>() const currentItem = ref<FileItem>()
// 选中的项目
const selected = ref<FileItem[]>([])
// 识别结果 // 识别结果
const nameTestResult = ref<Context>() const nameTestResult = ref<Context>()
@@ -105,6 +111,16 @@ const isDir = computed(() => inProps.item.path?.endsWith('/'))
// 是否文件 // 是否文件
const isFile = computed(() => !isDir.value) const isFile = computed(() => !isDir.value)
// 需要整理的path
const transferPaths = ref<string[]>([])
// 大小控制
const scrollStyle = computed(() => {
return appMode
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
})
// 是否为图片文件 // 是否为图片文件
const isImage = computed(() => { const isImage = computed(() => {
const ext = inProps.item.path?.split('.').pop()?.toLowerCase() const ext = inProps.item.path?.split('.').pop()?.toLowerCase()
@@ -158,12 +174,61 @@ async function deleteItem(item: FileItem) {
} }
} }
// 批量删除
async function batchDelete() {
const confirmed = await createConfirm({
title: '确认',
content: `是否确认删除选中的 ${selected.value.length} 个项目?`,
})
if (confirmed) {
emit('loading', true)
// 显示进度条
progressDialog.value = true
progressValue.value = 0
// 删除选中的项目
selected.value.every(async item => {
progressText.value = `正在删除 ${item.name} ...`
const url = inProps.endpoints?.delete.url.replace(/{storage}/g, inProps.storage)
const config: AxiosRequestConfig<FileItem> = {
url,
method: inProps.endpoints?.delete.method || 'post',
data: item,
}
await inProps.axios.request(config)
})
// 关闭进度条
progressDialog.value = false
emit('filedeleted')
emit('loading', false)
// 重新加载
list_files()
}
}
// 切换路径 // 切换路径
function changePath(item: FileItem) { function changePath(item: FileItem) {
item.path = inProps.item.path + item.name + (item.type === 'dir' ? '/' : '') item.path = inProps.item.path + item.name + (item.type === 'dir' ? '/' : '')
emit('pathchanged', item) emit('pathchanged', item)
} }
// 点击列表项
function listItemClick(item: FileItem) {
if (selectMode.value) {
if (selected.value.includes(item)) {
selected.value = selected.value.filter(i => i !== item)
} else {
selected.value.push(item)
}
return false
}
changePath(item)
}
// 新窗口中下载文件 // 新窗口中下载文件
async function download(item: FileItem) { async function download(item: FileItem) {
const url = inProps.endpoints?.download.url.replace(/{storage}/g, inProps.storage) const url = inProps.endpoints?.download.url.replace(/{storage}/g, inProps.storage)
@@ -264,10 +329,22 @@ async function rename() {
// 显示整理对话框 // 显示整理对话框
function showTransfer(item: FileItem) { function showTransfer(item: FileItem) {
currentItem.value = item transferPaths.value = [item.path || '']
transferPopper.value = true transferPopper.value = true
} }
// 显示批量整理对话框
function showBatchTransfer() {
transferPaths.value = selected.value.map(item => item.path || '')
transferPopper.value = true
}
// 整理完成
function transferDone() {
transferPopper.value = false
list_files()
}
// 将文件修改时间(timestape)转换为本地时间 // 将文件修改时间(timestape)转换为本地时间
function formatTime(timestape: number) { function formatTime(timestape: number) {
return new Date(timestape * 1000).toLocaleString() return new Date(timestape * 1000).toLocaleString()
@@ -392,6 +469,13 @@ async function scrape(path: string) {
} }
} }
// 批量刮削
async function batchScrape() {
selected.value.map(item => {
scrape(item.path || '')
})
}
// 使用SSE监听加载进度 // 使用SSE监听加载进度
function startLoadingProgress() { function startLoadingProgress() {
progressText.value = '请稍候 ...' progressText.value = '请稍候 ...'
@@ -436,6 +520,10 @@ onMounted(() => {
rounded="0" rounded="0"
/> />
<VSpacer v-if="isFile" /> <VSpacer v-if="isFile" />
<IconBtn v-if="!isFile" @click="selectMode = !selectMode">
<VIcon color="primary" v-if="selectMode"> mdi-selection-remove </VIcon>
<VIcon color="primary" v-else>mdi-select</VIcon>
</IconBtn>
<IconBtn v-if="isFile" @click="recognize(inProps.item.path || '')"> <IconBtn v-if="isFile" @click="recognize(inProps.item.path || '')">
<VIcon color="primary"> mdi-text-recognition </VIcon> <VIcon color="primary"> mdi-text-recognition </VIcon>
</IconBtn> </IconBtn>
@@ -445,6 +533,18 @@ onMounted(() => {
<IconBtn v-if="!isFile" @click="list_files"> <IconBtn v-if="!isFile" @click="list_files">
<VIcon color="primary"> mdi-refresh </VIcon> <VIcon color="primary"> mdi-refresh </VIcon>
</IconBtn> </IconBtn>
<!-- 批量操作按钮 -->
<span v-if="selected.length > 0">
<IconBtn @click.stop="batchScrape">
<VIcon color="primary" icon="mdi-auto-fix" />
</IconBtn>
<IconBtn @click.stop="showBatchTransfer">
<VIcon color="primary" icon="mdi-folder-arrow-right" />
</IconBtn>
<IconBtn @click.stop="batchDelete">
<VIcon icon="mdi-delete-outline" color="error" />
</IconBtn>
</span>
</VToolbar> </VToolbar>
<VCardText v-if="loading" class="text-center flex flex-col items-center"> <VCardText v-if="loading" class="text-center flex flex-col items-center">
<VProgressCircular size="48" indeterminate color="primary" /> <VProgressCircular size="48" indeterminate color="primary" />
@@ -471,31 +571,29 @@ onMounted(() => {
<!-- 目录和文件列表 --> <!-- 目录和文件列表 -->
<VCardText v-else-if="dirs.length || files.length" class="p-0"> <VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList subheader> <VList subheader>
<VVirtualScroll <VVirtualScroll :items="[...dirs, ...files]" :style="scrollStyle">
:items="[...dirs, ...files]"
:style="
appMode
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
"
>
<template #default="{ item }"> <template #default="{ item }">
<VHover> <VHover>
<template #default="hover"> <template #default="hover">
<VListItem v-bind="hover.props" class="px-3 pe-1" @click="changePath(item)"> <VListItem v-bind="hover.props" class="px-3 pe-1" @click="listItemClick(item)">
<template #prepend> <template #prepend>
<VIcon <VListItemAction v-if="selectMode">
v-if="inProps.icons && item.extension" <VCheckbox v-model="selected" :value="item" />
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other" </VListItemAction>
/> <template v-else>
<VIcon v-else icon="mdi-folder-outline" /> <VIcon
v-if="inProps.icons && item.extension"
:icon="inProps.icons[item.extension.toLowerCase()] || inProps.icons?.other"
/>
<VIcon v-else icon="mdi-folder-outline" />
</template>
</template> </template>
<VListItemTitle v-text="item.name" /> <VListItemTitle v-text="item.name" />
<VListItemSubtitle v-if="item.size"> <VListItemSubtitle v-if="item.size">
{{ formatBytes(item.size) }} {{ formatBytes(item.size) }}
</VListItemSubtitle> </VListItemSubtitle>
<template #append> <template #append>
<IconBtn v-if="display.smAndDown.value"> <IconBtn v-if="display.smAndDown.value && !selectMode">
<VIcon icon="mdi-dots-vertical" /> <VIcon icon="mdi-dots-vertical" />
<VMenu activator="parent" close-on-content-click> <VMenu activator="parent" close-on-content-click>
<VList> <VList>
@@ -515,7 +613,7 @@ onMounted(() => {
</VList> </VList>
</VMenu> </VMenu>
</IconBtn> </IconBtn>
<span v-if="hover.isHovering && display.mdAndUp" class="flex"> <span v-if="hover.isHovering && display.mdAndUp.value && !selectMode" class="flex">
<VTooltip text="识别"> <VTooltip text="识别">
<template #activator="{ props }"> <template #activator="{ props }">
<IconBtn v-bind="props" @click.stop="recognize(item.path)"> <IconBtn v-bind="props" @click.stop="recognize(item.path)">
@@ -594,13 +692,8 @@ onMounted(() => {
<ReorganizeDialog <ReorganizeDialog
v-if="transferPopper" v-if="transferPopper"
v-model="transferPopper" v-model="transferPopper"
:path="currentItem?.path" :paths="transferPaths"
@done=" @done="transferDone"
() => {
transferPopper = false
list_files()
}
"
@close="transferPopper = false" @close="transferPopper = false"
/> />
<!-- 进度框 --> <!-- 进度框 -->
+1 -1
View File
@@ -347,7 +347,7 @@ function reloadPage() {
function ensureNumber(value: any, defaultValue: number = 0) { function ensureNumber(value: any, defaultValue: number = 0) {
value = Number(value) value = Number(value)
// 如果不是数字 // 如果不是数字
if (value !== value) { if (Number.isNaN(value)) {
value = defaultValue value = defaultValue
} }
return value return value