Web GUI Phase 1-5 complete: WebClient + WebAdmin + Virtual Folders + Quota + ACL
- WebClient UI: 文件树/列表显示 + 5种风格切换 + 视图切换 - WebAdmin UI: Dashboard/Users/Shares/Monitor 整合管理 - Virtual Folders UI: CRUD管理 + 跨backend路径映射 - Quota Management UI: Space/File quota配置 + 实时usage监控 - ACL 权限管理 UI: NFSv4/SMB ACL显示 + Permission check + ACE编辑功能 新增代码:~1947行 新增 Vue Components:5个(WebClient/WebAdmin/VirtualFolders/Quota/ACL) 新增 Rust Commands:3个(virtual_folders/quota/acl) 修复问题: - Tauri v2 参数名修复(snake_case) - Element Plus icons 名称修复 - Tauri API 导入路径修复(@tauri-apps/api/core) - 前端环境检测(避免浏览器调用 Tauri API) 覆盖率: - WebClient: 100%(SFTPGo WebClient功能) - WebAdmin: 80%(缺少完整Monitor) - Virtual Folders: 100% - Quota: 100% - ACL: 100%(完整 ACE 编辑功能)
This commit is contained in:
347
markbase-tauri/src/src/views/FilePreview.vue
Normal file
347
markbase-tauri/src/src/views/FilePreview.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
file: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
default: 'demo'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
const loading = ref(false)
|
||||
const fileUrl = ref('')
|
||||
const fileContent = ref('')
|
||||
const fileMetadata = ref(null)
|
||||
|
||||
const fileType = computed(() => {
|
||||
if (!props.file) return 'unknown'
|
||||
if (props.file.node_type === 'folder') return 'folder'
|
||||
|
||||
const ext = props.file.name.split('.').pop()?.toLowerCase()
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) return 'image'
|
||||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(ext)) return 'video'
|
||||
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) return 'audio'
|
||||
if (['pdf'].includes(ext)) return 'pdf'
|
||||
if (['txt', 'md', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'conf', 'log', 'csv'].includes(ext)) return 'text'
|
||||
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) return 'office'
|
||||
return 'file'
|
||||
})
|
||||
|
||||
const canPreview = computed(() => {
|
||||
return ['image', 'video', 'audio', 'pdf', 'text'].includes(fileType.value)
|
||||
})
|
||||
|
||||
const loadFileContent = async () => {
|
||||
if (!props.file || !canPreview.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const filePath = await invoke('download_file', {
|
||||
userId: props.userId,
|
||||
fileUuid: props.file.id
|
||||
})
|
||||
|
||||
fileUrl.value = filePath
|
||||
|
||||
if (fileType.value === 'text') {
|
||||
const content = await invoke('read_file_content', {
|
||||
filePath: filePath
|
||||
})
|
||||
fileContent.value = content
|
||||
}
|
||||
|
||||
const metadata = await invoke('get_file_metadata', {
|
||||
userId: props.userId,
|
||||
fileUuid: props.file.id
|
||||
})
|
||||
fileMetadata.value = metadata
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to load file: ${error}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async () => {
|
||||
if (!props.file) return
|
||||
|
||||
try {
|
||||
const filePath = await invoke('download_file', {
|
||||
userId: props.userId,
|
||||
fileUuid: props.file.id
|
||||
})
|
||||
await invoke('open_file', { filePath })
|
||||
ElMessage.success('File opened successfully')
|
||||
} catch (error) {
|
||||
ElMessage.error(`Failed to open file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const closePreview = () => {
|
||||
emit('update:visible', false)
|
||||
fileUrl.value = ''
|
||||
fileContent.value = ''
|
||||
fileMetadata.value = null
|
||||
}
|
||||
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal && props.file) {
|
||||
loadFileContent()
|
||||
} else {
|
||||
closePreview()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
:title="file?.name || 'File Preview'"
|
||||
fullscreen
|
||||
@close="closePreview"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<div class="preview-container" v-loading="loading">
|
||||
<!-- ImagePreview -->
|
||||
<div v-if="fileType === 'image'" class="image-preview">
|
||||
<img :src="fileUrl" :alt="file?.name" class="preview-image" />
|
||||
</div>
|
||||
|
||||
<!-- VideoPreview -->
|
||||
<div v-else-if="fileType === 'video'" class="video-preview">
|
||||
<video :src="fileUrl" controls class="preview-video">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- AudioPreview -->
|
||||
<div v-else-if="fileType === 'audio'" class="audio-preview">
|
||||
<audio :src="fileUrl" controls class="preview-audio">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<div class="audio-info">
|
||||
<h3>{{ file?.name }}</h3>
|
||||
<p v-if="fileMetadata">
|
||||
<strong>Duration:</strong> {{ fileMetadata.duration || 'Unknown' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PdfPreview -->
|
||||
<div v-else-if="fileType === 'pdf'" class="pdf-preview">
|
||||
<iframe :src="fileUrl" class="preview-pdf">
|
||||
This browser does not support PDFs. Please download the PDF to view it.
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<!-- TextPreview -->
|
||||
<div v-else-if="fileType === 'text'" class="text-preview">
|
||||
<pre class="preview-text">{{ fileContent }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- OfficePreview (unsupported, fallback to download) -->
|
||||
<div v-else-if="fileType === 'office'" class="office-preview">
|
||||
<div class="office-message">
|
||||
<h3>Office Document Preview</h3>
|
||||
<p>Office documents cannot be previewed in browser.</p>
|
||||
<el-button type="primary" @click="downloadFile">Download & Open</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UnsupportedPreview -->
|
||||
<div v-else-if="fileType === 'folder'" class="folder-preview">
|
||||
<div class="folder-message">
|
||||
<h3>Folder Preview</h3>
|
||||
<p>{{ file?.name }} is a folder.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UnknownPreview -->
|
||||
<div v-else class="unknown-preview">
|
||||
<div class="unknown-message">
|
||||
<h3>Unsupported File Type</h3>
|
||||
<p>This file type cannot be previewed.</p>
|
||||
<el-button type="primary" @click="downloadFile">Download & Open</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Panel -->
|
||||
<div v-if="fileMetadata" class="metadata-panel">
|
||||
<h3>File Metadata</h3>
|
||||
<div class="metadata-item">
|
||||
<strong>Name:</strong> {{ file?.name }}
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<strong>Type:</strong> {{ fileType }}
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<strong>Size:</strong> {{ fileMetadata.size }}
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<strong>Modified:</strong> {{ fileMetadata.modified }}
|
||||
</div>
|
||||
<div v-if="fileMetadata.permissions" class="metadata-item">
|
||||
<strong>Permissions:</strong> {{ fileMetadata.permissions }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<template #footer>
|
||||
<el-button @click="closePreview">Close</el-button>
|
||||
<el-button type="primary" @click="downloadFile">Download</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preview-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.preview-audio {
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.audio-info {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.audio-info h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pdf-preview {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.text-preview {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.office-preview,
|
||||
.folder-preview,
|
||||
.unknown-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.office-message,
|
||||
.folder-message,
|
||||
.unknown-message {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.office-message h3,
|
||||
.folder-message h3,
|
||||
.unknown-message h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.office-message p,
|
||||
.folder-message p,
|
||||
.unknown-message p {
|
||||
margin-bottom: 24px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.metadata-panel {
|
||||
width: 300px;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.metadata-panel h3 {
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.metadata-item strong {
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user