mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
745 lines
21 KiB
Vue
745 lines
21 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
NButton,
|
|
NDataTable,
|
|
NEllipsis,
|
|
NFlex,
|
|
NInput,
|
|
NPopconfirm,
|
|
NPopselect,
|
|
NTag
|
|
} from 'naive-ui'
|
|
import { useGettext } from 'vue3-gettext'
|
|
|
|
import type { DataTableColumns, DropdownOption } from 'naive-ui'
|
|
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,
|
|
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 { $gettext } = useGettext()
|
|
const sort = ref<string>('')
|
|
const path = defineModel<string>('path', { type: String, required: true })
|
|
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
|
|
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
|
|
const markedType = defineModel<string>('markedType', { type: String, required: true })
|
|
const compress = defineModel<boolean>('compress', { type: Boolean, required: true })
|
|
const permission = defineModel<boolean>('permission', { type: Boolean, required: true })
|
|
const editorModal = ref(false)
|
|
const previewModal = ref(false)
|
|
const currentFile = ref('')
|
|
|
|
const showDropdown = ref(false)
|
|
const selectedRow = ref<any>()
|
|
const dropdownX = ref(0)
|
|
const dropdownY = ref(0)
|
|
|
|
const renameModal = ref(false)
|
|
const renameModel = ref({
|
|
source: '',
|
|
target: ''
|
|
})
|
|
const unCompressModal = ref(false)
|
|
const unCompressModel = ref({
|
|
path: '',
|
|
file: ''
|
|
})
|
|
|
|
const options = computed<DropdownOption[]>(() => {
|
|
if (selectedRow.value == null) return []
|
|
const options = [
|
|
{
|
|
label: selectedRow.value.dir
|
|
? $gettext('Open')
|
|
: isImage(selectedRow.value.name)
|
|
? $gettext('Preview')
|
|
: $gettext('Edit'),
|
|
key: selectedRow.value.dir ? 'open' : isImage(selectedRow.value.name) ? 'preview' : 'edit'
|
|
},
|
|
{ label: $gettext('Copy'), key: 'copy' },
|
|
{ label: $gettext('Move'), key: 'move' },
|
|
{ label: $gettext('Permission'), key: 'permission' },
|
|
{
|
|
label: selectedRow.value.dir ? $gettext('Compress') : $gettext('Download'),
|
|
key: selectedRow.value.dir ? 'compress' : 'download'
|
|
},
|
|
{
|
|
label: $gettext('Uncompress'),
|
|
key: 'uncompress',
|
|
show: isCompress(selectedRow.value.full),
|
|
disabled: !isCompress(selectedRow.value.full)
|
|
},
|
|
{ label: $gettext('Rename'), key: 'rename' },
|
|
{ label: () => h('span', { style: { color: 'red' } }, $gettext('Delete')), key: 'delete' }
|
|
]
|
|
if (marked.value.length) {
|
|
options.unshift({
|
|
label: $gettext('Paste'),
|
|
key: 'paste'
|
|
})
|
|
}
|
|
|
|
return options
|
|
})
|
|
|
|
const columns: DataTableColumns<RowData> = [
|
|
{
|
|
type: 'selection',
|
|
fixed: 'left'
|
|
},
|
|
{
|
|
title: $gettext('Name'),
|
|
key: 'name',
|
|
minWidth: 180,
|
|
defaultSortOrder: false,
|
|
sorter: 'default',
|
|
render(row) {
|
|
let icon = 'bi:file-earmark'
|
|
if (row.dir) {
|
|
icon = 'bi:folder'
|
|
} else {
|
|
icon = getIconByExt(getExt(row.name))
|
|
}
|
|
|
|
return h(
|
|
NFlex,
|
|
{
|
|
class: 'cursor-pointer hover:opacity-60',
|
|
onClick: () => {
|
|
if (row.dir) {
|
|
path.value = row.full
|
|
} else {
|
|
currentFile.value = row.full
|
|
editorModal.value = true
|
|
}
|
|
}
|
|
},
|
|
() => [
|
|
h(TheIcon, { icon, size: 24 }),
|
|
h(NEllipsis, null, {
|
|
default: () => {
|
|
if (row.symlink) {
|
|
return row.name + ' -> ' + row.link
|
|
} else {
|
|
return row.name
|
|
}
|
|
}
|
|
})
|
|
]
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: $gettext('Permission'),
|
|
key: 'mode',
|
|
minWidth: 80,
|
|
render(row: any): any {
|
|
return h(
|
|
NTag,
|
|
{ type: 'success', size: 'small', bordered: false },
|
|
{ default: () => row.mode }
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: $gettext('Owner / Group'),
|
|
key: 'owner/group',
|
|
minWidth: 120,
|
|
render(row: any): any {
|
|
return h('div', null, [
|
|
h(NTag, { type: 'primary', size: 'small', bordered: false }, { default: () => row.owner }),
|
|
' / ',
|
|
h(NTag, { type: 'primary', size: 'small', bordered: false }, { default: () => row.group })
|
|
])
|
|
}
|
|
},
|
|
{
|
|
title: $gettext('Size'),
|
|
key: 'size',
|
|
minWidth: 80,
|
|
render(row: any): any {
|
|
return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size })
|
|
}
|
|
},
|
|
{
|
|
title: $gettext('Modification Time'),
|
|
key: 'modify',
|
|
minWidth: 200,
|
|
render(row: any): any {
|
|
return h(
|
|
NTag,
|
|
{ type: 'warning', size: 'small', bordered: false },
|
|
{ default: () => row.modify }
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: $gettext('Actions'),
|
|
key: 'action',
|
|
width: 400,
|
|
render(row) {
|
|
return h(
|
|
NFlex,
|
|
{},
|
|
{
|
|
default: () => [
|
|
h(
|
|
NButton,
|
|
{
|
|
size: 'small',
|
|
type: row.dir ? 'success' : 'primary',
|
|
tertiary: true,
|
|
onClick: () => {
|
|
if (!row.dir && !row.symlink) {
|
|
currentFile.value = row.full
|
|
if (isImage(row.name)) {
|
|
previewModal.value = true
|
|
} else {
|
|
editorModal.value = true
|
|
}
|
|
} else {
|
|
path.value = row.full
|
|
}
|
|
}
|
|
},
|
|
{
|
|
default: () => {
|
|
if (!row.dir && !row.symlink) {
|
|
return isImage(row.name) ? $gettext('Preview') : $gettext('Edit')
|
|
} else {
|
|
return $gettext('Open')
|
|
}
|
|
}
|
|
}
|
|
),
|
|
h(
|
|
NButton,
|
|
{
|
|
size: 'small',
|
|
type: row.dir ? 'primary' : 'info',
|
|
tertiary: true,
|
|
onClick: () => {
|
|
if (row.dir) {
|
|
selected.value = [row.full]
|
|
compress.value = true
|
|
} else {
|
|
window.open('/api/file/download?path=' + encodeURIComponent(row.full))
|
|
}
|
|
}
|
|
},
|
|
{
|
|
default: () => {
|
|
if (row.dir) {
|
|
return $gettext('Compress')
|
|
} else {
|
|
return $gettext('Download')
|
|
}
|
|
}
|
|
}
|
|
),
|
|
h(
|
|
NButton,
|
|
{
|
|
type: 'warning',
|
|
size: 'small',
|
|
tertiary: true,
|
|
onClick: () => {
|
|
renameModel.value.source = getFilename(row.name)
|
|
renameModel.value.target = getFilename(row.name)
|
|
renameModal.value = true
|
|
}
|
|
},
|
|
{ default: () => $gettext('Rename') }
|
|
),
|
|
h(
|
|
NPopconfirm,
|
|
{
|
|
onPositiveClick: () => {
|
|
useRequest(file.delete(row.full)).onComplete(() => {
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Deleted successfully'))
|
|
})
|
|
},
|
|
onNegativeClick: () => {}
|
|
},
|
|
{
|
|
default: () => {
|
|
return $gettext('Are you sure you want to delete %{ name }?', { name: row.name })
|
|
},
|
|
trigger: () => {
|
|
return h(
|
|
NButton,
|
|
{
|
|
size: 'small',
|
|
type: 'error',
|
|
tertiary: true
|
|
},
|
|
{ default: () => $gettext('Delete') }
|
|
)
|
|
}
|
|
}
|
|
),
|
|
h(
|
|
NPopselect,
|
|
{
|
|
options: [
|
|
{ label: $gettext('Copy'), value: 'copy' },
|
|
{ label: $gettext('Move'), value: 'move' },
|
|
{ label: $gettext('Permission'), value: 'permission' },
|
|
{ label: $gettext('Compress'), value: 'compress' },
|
|
{
|
|
label: $gettext('Uncompress'),
|
|
value: 'uncompress',
|
|
disabled: !isCompress(row.name)
|
|
}
|
|
],
|
|
onUpdateValue: (value) => {
|
|
switch (value) {
|
|
case 'copy':
|
|
markedType.value = 'copy'
|
|
marked.value = [
|
|
{
|
|
name: row.name,
|
|
source: row.full,
|
|
force: false
|
|
}
|
|
]
|
|
window.$message.success(
|
|
$gettext(
|
|
'Marked successfully, please navigate to the destination path to paste'
|
|
)
|
|
)
|
|
break
|
|
case 'move':
|
|
markedType.value = 'move'
|
|
marked.value = [
|
|
{
|
|
name: row.name,
|
|
source: row.full,
|
|
force: false
|
|
}
|
|
]
|
|
window.$message.success(
|
|
$gettext(
|
|
'Marked successfully, please navigate to the destination path to paste'
|
|
)
|
|
)
|
|
break
|
|
case 'permission':
|
|
selected.value = [row.full]
|
|
permission.value = true
|
|
break
|
|
case 'compress':
|
|
selected.value = [row.full]
|
|
compress.value = true
|
|
break
|
|
case 'uncompress':
|
|
unCompressModel.value.file = row.full
|
|
unCompressModel.value.path = path.value
|
|
unCompressModal.value = true
|
|
break
|
|
}
|
|
}
|
|
},
|
|
{
|
|
default: () => {
|
|
return h(
|
|
NButton,
|
|
{
|
|
tertiary: true,
|
|
size: 'small'
|
|
},
|
|
{ default: () => $gettext('More') }
|
|
)
|
|
}
|
|
}
|
|
)
|
|
]
|
|
}
|
|
)
|
|
}
|
|
}
|
|
]
|
|
|
|
const rowProps = (row: any) => {
|
|
return {
|
|
onContextmenu: (e: MouseEvent) => {
|
|
e.preventDefault()
|
|
showDropdown.value = false
|
|
nextTick().then(() => {
|
|
showDropdown.value = true
|
|
selectedRow.value = row
|
|
dropdownX.value = e.clientX
|
|
dropdownY.value = e.clientY
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
|
(page, pageSize) => file.list(path.value, page, pageSize, sort.value),
|
|
{
|
|
initialData: { total: 0, list: [] },
|
|
initialPageSize: 100,
|
|
total: (res: any) => res.total,
|
|
data: (res: any) => res.items
|
|
}
|
|
)
|
|
|
|
const handleRename = () => {
|
|
const source = path.value + '/' + renameModel.value.source
|
|
const target = path.value + '/' + renameModel.value.target
|
|
if (!checkName(renameModel.value.source) || !checkName(renameModel.value.target)) {
|
|
window.$message.error($gettext('Invalid name'))
|
|
return
|
|
}
|
|
|
|
useRequest(file.exist([target])).onSuccess(({ data }) => {
|
|
if (data[0]) {
|
|
window.$dialog.warning({
|
|
title: $gettext('Warning'),
|
|
content: $gettext('There are items with the same name. Do you want to overwrite?'),
|
|
positiveText: $gettext('Overwrite'),
|
|
negativeText: $gettext('Cancel'),
|
|
onPositiveClick: () => {
|
|
useRequest(file.move([{ source, target, force: true }]))
|
|
.onSuccess(() => {
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success(
|
|
$gettext('Renamed %{ source } to %{ target } successfully', {
|
|
source: renameModel.value.source,
|
|
target: renameModel.value.target
|
|
})
|
|
)
|
|
})
|
|
.onComplete(() => {
|
|
renameModal.value = false
|
|
})
|
|
}
|
|
})
|
|
} else {
|
|
useRequest(file.move([{ source, target, force: false }]))
|
|
.onSuccess(() => {
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success(
|
|
$gettext('Renamed %{ source } to %{ target } successfully', {
|
|
source: renameModel.value.source,
|
|
target: renameModel.value.target
|
|
})
|
|
)
|
|
})
|
|
.onComplete(() => {
|
|
renameModal.value = false
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleUnCompress = () => {
|
|
// 移除首位的 / 去检测
|
|
if (
|
|
!unCompressModel.value.path.startsWith('/') ||
|
|
!checkPath(unCompressModel.value.path.slice(1))
|
|
) {
|
|
window.$message.error($gettext('Invalid path'))
|
|
return
|
|
}
|
|
const message = window.$message.loading($gettext('Uncompressing...'), {
|
|
duration: 0
|
|
})
|
|
useRequest(file.unCompress(unCompressModel.value.file, unCompressModel.value.path))
|
|
.onSuccess(() => {
|
|
unCompressModal.value = false
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Uncompressed successfully'))
|
|
})
|
|
.onComplete(() => {
|
|
message?.destroy()
|
|
})
|
|
}
|
|
|
|
const handlePaste = () => {
|
|
if (!marked.value.length) {
|
|
window.$message.error($gettext('Please mark the files/folders to copy or move first'))
|
|
return
|
|
}
|
|
|
|
// 查重
|
|
let flag = false
|
|
const paths = marked.value.map((item) => {
|
|
return {
|
|
name: item.name,
|
|
source: item.source,
|
|
target: path.value + '/' + item.name,
|
|
force: false
|
|
}
|
|
})
|
|
const sources = paths.map((item: any) => item.target)
|
|
useRequest(file.exist(sources)).onSuccess(({ data }) => {
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (data[i]) {
|
|
flag = true
|
|
paths[i].force = true
|
|
}
|
|
}
|
|
if (flag) {
|
|
window.$dialog.warning({
|
|
title: $gettext('Warning'),
|
|
content: $gettext(
|
|
'There are items with the same name. %{ items } Do you want to overwrite?',
|
|
{
|
|
items: `${paths
|
|
.filter((item) => item.force)
|
|
.map((item) => item.name)
|
|
.join(', ')}`
|
|
}
|
|
),
|
|
positiveText: $gettext('Overwrite'),
|
|
negativeText: $gettext('Cancel'),
|
|
onPositiveClick: () => {
|
|
if (markedType.value == 'copy') {
|
|
useRequest(file.copy(paths)).onSuccess(() => {
|
|
marked.value = []
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Copied successfully'))
|
|
})
|
|
} else {
|
|
useRequest(file.move(paths)).onSuccess(() => {
|
|
marked.value = []
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Moved successfully'))
|
|
})
|
|
}
|
|
},
|
|
onNegativeClick: () => {
|
|
marked.value = []
|
|
window.$message.info($gettext('Canceled'))
|
|
}
|
|
})
|
|
} else {
|
|
if (markedType.value == 'copy') {
|
|
useRequest(file.copy(paths)).onSuccess(() => {
|
|
marked.value = []
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Copied successfully'))
|
|
})
|
|
} else {
|
|
useRequest(file.move(paths)).onSuccess(() => {
|
|
marked.value = []
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Moved successfully'))
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleSelect = (key: string) => {
|
|
switch (key) {
|
|
case 'paste':
|
|
handlePaste()
|
|
break
|
|
case 'open':
|
|
path.value = selectedRow.value.full
|
|
break
|
|
case 'edit':
|
|
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 = [
|
|
{
|
|
name: selectedRow.value.name,
|
|
source: selectedRow.value.full,
|
|
force: false
|
|
}
|
|
]
|
|
window.$message.success(
|
|
$gettext('Marked successfully, please navigate to the destination path to paste')
|
|
)
|
|
break
|
|
case 'move':
|
|
markedType.value = 'move'
|
|
marked.value = [
|
|
{
|
|
name: selectedRow.value.name,
|
|
source: selectedRow.value.full,
|
|
force: false
|
|
}
|
|
]
|
|
window.$message.success(
|
|
$gettext('Marked successfully, please navigate to the destination path to paste')
|
|
)
|
|
break
|
|
case 'permission':
|
|
selected.value = [selectedRow.value.full]
|
|
permission.value = true
|
|
break
|
|
case 'compress':
|
|
selected.value = [selectedRow.value.full]
|
|
compress.value = true
|
|
break
|
|
case 'download':
|
|
window.open('/api/file/download?path=' + encodeURIComponent(selectedRow.value.full))
|
|
break
|
|
case 'uncompress':
|
|
unCompressModel.value.file = selectedRow.value.full
|
|
unCompressModel.value.path = path.value
|
|
unCompressModal.value = true
|
|
break
|
|
case 'rename':
|
|
renameModel.value.source = getFilename(selectedRow.value.name)
|
|
renameModel.value.target = getFilename(selectedRow.value.name)
|
|
renameModal.value = true
|
|
break
|
|
case 'delete':
|
|
useRequest(file.delete(selectedRow.value.full)).onSuccess(() => {
|
|
window.$bus.emit('file:refresh')
|
|
window.$message.success($gettext('Deleted successfully'))
|
|
})
|
|
break
|
|
}
|
|
onCloseDropdown()
|
|
}
|
|
|
|
const onCloseDropdown = () => {
|
|
selectedRow.value = null
|
|
showDropdown.value = false
|
|
}
|
|
|
|
const handleSorterChange = (sorter: {
|
|
columnKey: string | number | null
|
|
order: 'ascend' | 'descend' | false
|
|
}) => {
|
|
if (!sorter || sorter.columnKey === 'name') {
|
|
if (!loading.value) {
|
|
switch (sorter.order) {
|
|
case 'ascend':
|
|
sort.value = 'asc'
|
|
refresh()
|
|
break
|
|
case 'descend':
|
|
sort.value = 'desc'
|
|
refresh()
|
|
break
|
|
default:
|
|
sort.value = ''
|
|
refresh()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
watch(
|
|
path,
|
|
() => {
|
|
selected.value = []
|
|
refresh()
|
|
window.$bus.emit('push-history', path.value)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
window.$bus.on('file:refresh', refresh)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.$bus.off('file:refresh')
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<n-data-table
|
|
remote
|
|
striped
|
|
virtual-scroll
|
|
size="small"
|
|
:scroll-x="1200"
|
|
:columns="columns"
|
|
:data="data"
|
|
:row-props="rowProps"
|
|
:loading="loading"
|
|
:row-key="(row: any) => row.full"
|
|
max-height="60vh"
|
|
@update:sorter="handleSorterChange"
|
|
v-model:checked-row-keys="selected"
|
|
v-model:page="page"
|
|
v-model:pageSize="pageSize"
|
|
:pagination="{
|
|
page: page,
|
|
pageCount: pageCount,
|
|
pageSize: pageSize,
|
|
itemCount: total,
|
|
showQuickJumper: true,
|
|
showSizePicker: true,
|
|
pageSizes: [100, 200, 500, 1000, 1500, 2000, 5000]
|
|
}"
|
|
/>
|
|
<n-dropdown
|
|
placement="bottom-start"
|
|
trigger="manual"
|
|
:x="dropdownX"
|
|
:y="dropdownY"
|
|
:options="options"
|
|
:show="showDropdown"
|
|
:on-clickoutside="onCloseDropdown"
|
|
@select="handleSelect"
|
|
/>
|
|
<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"
|
|
:title="$gettext('Rename - %{ source }', { source: renameModel.source })"
|
|
style="width: 60vw"
|
|
size="huge"
|
|
:bordered="false"
|
|
:segmented="false"
|
|
>
|
|
<n-flex vertical>
|
|
<n-form>
|
|
<n-form-item :label="$gettext('New Name')">
|
|
<n-input v-model:value="renameModel.target" />
|
|
</n-form-item>
|
|
</n-form>
|
|
<n-button type="primary" @click="handleRename">{{ $gettext('Save') }}</n-button>
|
|
</n-flex>
|
|
</n-modal>
|
|
<n-modal
|
|
v-model:show="unCompressModal"
|
|
preset="card"
|
|
:title="$gettext('Uncompress - %{ file }', { file: unCompressModel.file })"
|
|
style="width: 60vw"
|
|
size="huge"
|
|
:bordered="false"
|
|
:segmented="false"
|
|
>
|
|
<n-flex vertical>
|
|
<n-form>
|
|
<n-form-item :label="$gettext('Uncompress to')">
|
|
<n-input v-model:value="unCompressModel.path" />
|
|
</n-form-item>
|
|
</n-form>
|
|
<n-button type="primary" @click="handleUnCompress">{{ $gettext('Uncompress') }}</n-button>
|
|
</n-flex>
|
|
</n-modal>
|
|
</template>
|