云文件管理系统上传组件优化
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="批量移动"
|
||||
width="400px"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form>
|
||||
<el-form-item label="目标文件夹">
|
||||
<el-select v-model="targetFolderId" placeholder="请选择目标文件夹" style="width: 100%" clearable>
|
||||
<el-option :label="parentFolderName" :value="'parent'" v-if="canGoParent" />
|
||||
<el-option label="根目录" :value="'root'" />
|
||||
<el-option
|
||||
v-for="folder in folders"
|
||||
:key="folder.id"
|
||||
:label="folder.name"
|
||||
:value="folder.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="form-item">
|
||||
<span class="label">目标目录</span>
|
||||
<el-tree-select
|
||||
v-model="targetFolderId"
|
||||
:data="folderTreeData"
|
||||
:props="treeProps"
|
||||
placeholder="请选择目标目录"
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="flex: 1"
|
||||
node-key="id"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Close, FolderOpened } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -50,18 +50,77 @@ const visible = computed({
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const targetFolderId = ref('')
|
||||
const targetFolderId = ref(null)
|
||||
|
||||
// 树形配置
|
||||
const treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
// 转换目录数据为树形结构
|
||||
const folderTreeData = computed(() => {
|
||||
// 根节点
|
||||
const rootNodes = [
|
||||
{
|
||||
id: 'root',
|
||||
name: '根目录',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
|
||||
// 递归转换后端返回的树形数据
|
||||
const convertFolderTree = (folders) => {
|
||||
if (!folders || folders.length === 0) return []
|
||||
|
||||
return folders.map(folder => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
children: folder.children && folder.children.length > 0
|
||||
? convertFolderTree(folder.children)
|
||||
: undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 将后端返回的目录树挂载到根目录下
|
||||
rootNodes[0].children = convertFolderTree(props.folders)
|
||||
|
||||
return rootNodes
|
||||
})
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', targetFolderId.value)
|
||||
visible.value = false
|
||||
targetFolderId.value = ''
|
||||
targetFolderId.value = null
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
targetFolderId.value = ''
|
||||
targetFolderId.value = null
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 重置选中状态
|
||||
watch(visible, (val) => {
|
||||
if (!val) {
|
||||
targetFolderId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="消息"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="sidebar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
@@ -6,7 +6,7 @@
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item index="my-files">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span>我的文档</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="my-share">
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits, defineProps } from 'vue'
|
||||
import { Document, Share, User, Delete } from '@element-plus/icons-vue'
|
||||
import { FolderOpened, Share, User, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
activeMenu: { type: String, default: 'my-files' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="file-table">
|
||||
<el-table
|
||||
:data="files"
|
||||
@@ -8,14 +8,12 @@
|
||||
@selection-change="$emit('selection-change', $event)"
|
||||
@row-dblclick="$emit('row-dblclick', $event)"
|
||||
>
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column type="selection" width="50" :selectable="checkSelectable" />
|
||||
|
||||
<el-table-column label="文件名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name-cell">
|
||||
<el-icon :size="20" :color="getFileIconColor(row)">
|
||||
<component :is="getFileIcon(row)" />
|
||||
</el-icon>
|
||||
<FileIcon :icon="getFileIconType(row)" :color="getFileIconColor(row)" :size="24" />
|
||||
<span class="file-name">{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -50,32 +48,24 @@
|
||||
<el-icon><View /></el-icon>
|
||||
<span style="margin-left: 2px">预览</span>
|
||||
</el-button>
|
||||
<el-button link @click="$emit('download', row)" v-else-if="row.type !== 'folder'">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span style="margin-left: 2px">下载</span>
|
||||
</el-button>
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, row)">
|
||||
<el-button link>
|
||||
<span>更多</span>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="rename">
|
||||
<el-icon><Edit /></el-icon>
|
||||
<span>重命名</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="share">
|
||||
<el-icon><Share /></el-icon>
|
||||
<span>共享</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="download" v-if="row.type !== 'folder' && canPreview(row)">
|
||||
<el-dropdown-item command="rename">
|
||||
<el-icon><Edit /></el-icon>
|
||||
<span>重命名</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="download" v-if="row.type !== 'folder'">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span>下载</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="preview" v-if="!canPreview(row) && row.type !== 'folder'">
|
||||
<el-icon><View /></el-icon>
|
||||
<span>预览</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span style="color: #f56c6c">删除</span>
|
||||
@@ -115,7 +105,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Document, Folder, Picture, VideoPlay, Headset, RefreshLeft, Delete, View, Share, Download, CloseBold, Edit } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Document, Folder, Picture, VideoPlay, Headset, RefreshLeft, Delete, View, Share, Download, CloseBold, Edit,
|
||||
Tickets, Notebook, Reading, Files, DocumentCopy
|
||||
} from '@element-plus/icons-vue'
|
||||
import FileIcon from './FileIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
files: { type: Array, default: () => [] },
|
||||
@@ -126,13 +120,25 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['selection-change', 'row-dblclick', 'preview', 'share', 'download', 'delete', 'restore', 'delete-permanent', 'cancel-share', 'rename'])
|
||||
|
||||
// 判断行是否可勾选
|
||||
const checkSelectable = (row) => {
|
||||
// 共享给我的页面,目录不可勾选
|
||||
if (props.menuType === 'shared-to-me' && row.type === 'folder') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleCommand = (command, row) => {
|
||||
switch (command) {
|
||||
case 'share':
|
||||
emit('share', row)
|
||||
break
|
||||
case 'rename':
|
||||
emit('rename', row)
|
||||
break
|
||||
case 'share':
|
||||
emit('share', row)
|
||||
case 'download':
|
||||
emit('download', row)
|
||||
break
|
||||
case 'delete':
|
||||
emit('delete', row)
|
||||
@@ -140,19 +146,84 @@ const handleCommand = (command, row) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getFileIcon = (file) => {
|
||||
if (file.type === 'folder') return Folder
|
||||
if (file.type === 'image') return Picture
|
||||
if (file.type === 'video') return VideoPlay
|
||||
if (file.type === 'audio') return Headset
|
||||
return Document
|
||||
// 获取文件图标类型
|
||||
const getFileIconType = (file) => {
|
||||
if (file.type === 'folder') return 'folder'
|
||||
if (file.type === 'image') return 'image'
|
||||
if (file.type === 'video') return 'video'
|
||||
if (file.type === 'audio') return 'audio'
|
||||
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
|
||||
// PDF
|
||||
if (ext === 'pdf') return 'pdf'
|
||||
|
||||
// Word 文档
|
||||
if (['docx', 'doc'].includes(ext)) return 'word'
|
||||
|
||||
// Excel 表格
|
||||
if (['xlsx', 'xls'].includes(ext)) return 'excel'
|
||||
|
||||
// PPT 演示文稿
|
||||
if (['pptx', 'ppt'].includes(ext)) return 'ppt'
|
||||
|
||||
// 代码文件
|
||||
if (['js', 'ts', 'vue', 'jsx', 'tsx', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml'].includes(ext)) return 'code'
|
||||
|
||||
// 文本文件
|
||||
if (['txt', 'md', 'markdown', 'log', 'sql', 'yaml', 'yml', 'sh', 'bat', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs'].includes(ext)) return 'text'
|
||||
|
||||
// OFD
|
||||
if (ext === 'ofd') return 'pdf'
|
||||
|
||||
// 压缩文件
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return 'zip'
|
||||
|
||||
return 'document'
|
||||
}
|
||||
|
||||
// 根据文件扩展名获取图标颜色
|
||||
const getFileIconColor = (file) => {
|
||||
if (file.type === 'folder') return '#f7b32b'
|
||||
if (file.type === 'image') return '#67c23a'
|
||||
if (file.type === 'video') return '#409eff'
|
||||
if (file.type === 'audio') return '#e6a23c'
|
||||
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
|
||||
// PDF - 红色
|
||||
if (ext === 'pdf') return '#f56c6c'
|
||||
|
||||
// Word - 蓝色
|
||||
if (['docx', 'doc'].includes(ext)) return '#2b579a'
|
||||
|
||||
// Excel - 绿色
|
||||
if (['xlsx', 'xls'].includes(ext)) return '#217346'
|
||||
|
||||
// PPT - 橙红色
|
||||
if (['pptx', 'ppt'].includes(ext)) return '#d24726'
|
||||
|
||||
// 代码文件 - 紫色
|
||||
if (['js', 'ts', 'vue', 'jsx', 'tsx', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml'].includes(ext)) return '#9c27b0'
|
||||
|
||||
// 文本文件 - 灰色
|
||||
if (['txt', 'md', 'markdown', 'log', 'yaml', 'yml', 'sh', 'bat'].includes(ext)) return '#606266'
|
||||
|
||||
// SQL - 深蓝色
|
||||
if (ext === 'sql') return '#1976d2'
|
||||
|
||||
// Python - 蓝黄色
|
||||
if (ext === 'py') return '#3776ab'
|
||||
|
||||
// Java - 红棕色
|
||||
if (ext === 'java') return '#f89820'
|
||||
|
||||
// OFD - 红色
|
||||
if (ext === 'ofd') return '#f56c6c'
|
||||
|
||||
// 压缩文件 - 棕色
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return '#8d6e63'
|
||||
|
||||
return '#909399'
|
||||
}
|
||||
|
||||
@@ -175,7 +246,11 @@ const formatDate = (date) => {
|
||||
const canPreview = (file) => {
|
||||
if (file.type === 'folder') return false
|
||||
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'txt', 'md', 'json', 'xml', 'log', 'js', 'ts', 'vue', 'java', 'py', 'css', 'html', 'docx', 'xlsx', 'pptx', 'doc', 'xls', 'ppt', 'ofd'].includes(ext)
|
||||
// 支持预览的格式(注意:doc 旧格式不支持)
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
|
||||
'pdf',
|
||||
'txt', 'md', 'markdown', 'json', 'xml', 'log', 'js', 'ts', 'vue', 'java', 'py', 'css', 'html', 'htm', 'yaml', 'yml', 'sql', 'sh', 'bat',
|
||||
'docx', 'xlsx', 'xls', 'pptx', 'ppt', 'ofd'].includes(ext)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="file-toolbar">
|
||||
<!-- 第一行:视图切换 + 搜索框 + 上传/新建 -->
|
||||
<div class="toolbar-top">
|
||||
@@ -29,7 +29,7 @@
|
||||
</el-button>
|
||||
<el-button @click="$emit('newFolder')">
|
||||
<el-icon><FolderAdd /></el-icon>
|
||||
<span style="margin-left: 4px">新建文件夹</span>
|
||||
<span style="margin-left: 4px">新建目录</span>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
|
||||
@@ -39,28 +39,40 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:前进后退 + 路径 -->
|
||||
<div class="toolbar-bottom">
|
||||
<div class="path-controls">
|
||||
<el-button v-if="showBack" @click="$emit('goBack')" circle title="返回上级">
|
||||
<el-icon><Back /></el-icon>
|
||||
</el-button>
|
||||
<span class="current-path">{{ currentPath }}</span>
|
||||
</div>
|
||||
<!-- 第二行:面包屑导航 -->
|
||||
<div class="toolbar-bottom" v-if="menuType !== 'trash'">
|
||||
<el-icon class="breadcrumb-icon"><Folder /></el-icon>
|
||||
<el-breadcrumb separator="/" v-if="folderStack.length > 0">
|
||||
<el-breadcrumb-item>
|
||||
<span class="breadcrumb-link" @click="$emit('goBack', -1)">根目录</span>
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item
|
||||
v-for="(folder, index) in folderStack"
|
||||
:key="index"
|
||||
>
|
||||
<span
|
||||
class="breadcrumb-link"
|
||||
:class="{ 'is-last': index === folderStack.length - 1 }"
|
||||
@click="handleBreadcrumbClick(index)"
|
||||
>{{ folder.name }}</span>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<span v-else class="current-path">根目录</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { List, Grid, Upload, FolderAdd, Delete, Back, Search } from '@element-plus/icons-vue'
|
||||
import { List, Grid, Upload, FolderAdd, Delete, Search, Folder } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
viewMode: { type: String, default: 'table' },
|
||||
searchKeyword: { type: String, default: '' },
|
||||
menuType: { type: String, default: 'my-files' },
|
||||
currentPath: { type: String, default: '/' },
|
||||
showBack: { type: Boolean, default: false }
|
||||
showBack: { type: Boolean, default: false },
|
||||
folderStack: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -97,6 +109,12 @@ const onSearchInput = (val) => {
|
||||
const doSearch = () => {
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const handleBreadcrumbClick = (index) => {
|
||||
// 点击当前项不跳转
|
||||
if (index === props.folderStack.length - 1) return
|
||||
emit('goBack', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -105,7 +123,6 @@ const doSearch = () => {
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
@@ -122,17 +139,56 @@ const doSearch = () => {
|
||||
.toolbar-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.path-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.breadcrumb-icon {
|
||||
margin-right: 8px;
|
||||
color: #f7b32b;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.current-path {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.breadcrumb-link:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
.breadcrumb-link.is-last {
|
||||
color: #303133;
|
||||
cursor: default;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="新建文件夹"
|
||||
title="新建目录"
|
||||
width="400px"
|
||||
class="custom-dialog"
|
||||
>
|
||||
<el-input
|
||||
v-model="folderName"
|
||||
placeholder="请输入文件夹名称"
|
||||
placeholder="请输入目录名称"
|
||||
clearable
|
||||
@keyup.enter="handleCreate"
|
||||
autofocus
|
||||
|
||||
@@ -1,36 +1,52 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="previewFile?.name || '预览'"
|
||||
width="80%"
|
||||
width="60%"
|
||||
top="5vh"
|
||||
class="custom-dialog preview-dialog"
|
||||
class="preview-dialog-center"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="preview-content">
|
||||
<img v-if="isImage" :src="previewUrl" class="preview-image" />
|
||||
<iframe v-else-if="isPdf" :src="previewUrl" class="preview-iframe" />
|
||||
<pre v-else-if="isText" class="preview-text">{{ previewContent }}</pre>
|
||||
<div v-else-if="isOffice" ref="officeViewer" class="preview-office">
|
||||
<div v-if="!jitViewerLoaded" class="preview-loading">
|
||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
||||
<p>正在加载预览组件...</p>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="preview-loading">
|
||||
<el-icon class="is-loading" :size="32"><Loading /></el-icon>
|
||||
<p>正在加载预览...</p>
|
||||
</div>
|
||||
<div v-else class="preview-unsupported">
|
||||
<el-icon :size="64"><Document /></el-icon>
|
||||
<p>此文件类型暂不支持预览</p>
|
||||
<el-button type="primary" @click="$emit('download', previewFile)">
|
||||
|
||||
<!-- 错误 -->
|
||||
<div v-else-if="error" class="preview-error">
|
||||
<el-icon :size="48"><Warning /></el-icon>
|
||||
<p>{{ error }}</p>
|
||||
<el-button type="primary" @click="handleDownload">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span style="margin-left: 4px">下载文件</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 不支持的 Office 格式 -->
|
||||
<div v-else-if="isUnsupportedFormat" class="preview-unsupported">
|
||||
<el-icon :size="64"><Document /></el-icon>
|
||||
<p>此文件格式暂不支持在线预览</p>
|
||||
<p class="tip" v-if="previewFile?.name?.endsWith('.doc')">建议将文件转换为 .docx 格式后上传</p>
|
||||
<el-button type="primary" @click="handleDownload">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span style="margin-left: 4px">下载文件</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- jit-viewer 容器 (图片、PDF、Office、文本都用) -->
|
||||
<div v-show="!loading && !error && !isUnsupportedFormat" ref="viewerContainer" class="viewer-container"></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, nextTick } from 'vue'
|
||||
import { Document, Download, Loading } from '@element-plus/icons-vue'
|
||||
import { computed, ref, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { Document, Download, Loading, Warning } from '@element-plus/icons-vue'
|
||||
import { createViewer } from 'jit-viewer'
|
||||
import 'jit-viewer/style.css'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
@@ -46,139 +62,244 @@ const visible = computed({
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const officeViewer = ref(null)
|
||||
const jitViewerLoaded = ref(false)
|
||||
const viewerContainer = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
let viewerInstance = null
|
||||
|
||||
const isImage = computed(() => {
|
||||
// 不支持的格式
|
||||
const isUnsupportedFormat = computed(() => {
|
||||
if (!props.previewFile) return false
|
||||
const ext = props.previewFile.name.split('.').pop()?.toLowerCase()
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)
|
||||
return ['doc'].includes(ext)
|
||||
})
|
||||
|
||||
const isPdf = computed(() => {
|
||||
if (!props.previewFile) return false
|
||||
return props.previewFile.name.toLowerCase().endsWith('.pdf')
|
||||
})
|
||||
// 获取文件类型
|
||||
const getFileType = (filename) => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
const typeMap = {
|
||||
'docx': 'docx',
|
||||
'xlsx': 'xlsx',
|
||||
'xls': 'xls',
|
||||
'pptx': 'pptx',
|
||||
'ppt': 'ppt',
|
||||
'ofd': 'ofd',
|
||||
'pdf': 'pdf',
|
||||
'txt': 'txt',
|
||||
'md': 'md',
|
||||
'markdown': 'markdown',
|
||||
'json': 'txt',
|
||||
'xml': 'txt',
|
||||
'log': 'txt',
|
||||
'js': 'txt',
|
||||
'ts': 'txt',
|
||||
'vue': 'txt',
|
||||
'java': 'txt',
|
||||
'py': 'txt',
|
||||
'css': 'txt',
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'yaml': 'txt',
|
||||
'yml': 'txt',
|
||||
'sql': 'txt',
|
||||
'sh': 'txt',
|
||||
'bat': 'txt',
|
||||
'jpg': 'image',
|
||||
'jpeg': 'image',
|
||||
'png': 'image',
|
||||
'gif': 'image',
|
||||
'webp': 'image',
|
||||
'svg': 'image',
|
||||
'bmp': 'image',
|
||||
'ico': 'image'
|
||||
}
|
||||
return typeMap[ext] || ext
|
||||
}
|
||||
|
||||
const isText = computed(() => {
|
||||
if (!props.previewFile) return false
|
||||
const ext = props.previewFile.name.split('.').pop()?.toLowerCase()
|
||||
return ['txt', 'md', 'json', 'xml', 'log', 'js', 'ts', 'vue', 'java', 'py', 'css', 'html'].includes(ext)
|
||||
})
|
||||
// 获取文件源
|
||||
const getFileSource = () => {
|
||||
// 如果有 URL,用 URL
|
||||
if (props.previewUrl) {
|
||||
return props.previewUrl
|
||||
}
|
||||
// 如果有文本内容,返回文本
|
||||
if (props.previewContent) {
|
||||
return new Blob([props.previewContent], { type: 'text/plain' })
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isOffice = computed(() => {
|
||||
if (!props.previewFile) return false
|
||||
const ext = props.previewFile.name.split('.').pop()?.toLowerCase()
|
||||
return ['docx', 'xlsx', 'pptx', 'doc', 'xls', 'ppt', 'ofd'].includes(ext)
|
||||
})
|
||||
|
||||
// 加载 jit-viewer 脚本
|
||||
const loadJitViewer = () => {
|
||||
if (window.jitView) {
|
||||
jitViewerLoaded.value = true
|
||||
// 初始化 jit-viewer
|
||||
const initViewer = async () => {
|
||||
if (!viewerContainer.value) return
|
||||
|
||||
const fileSource = getFileSource()
|
||||
if (!fileSource && !isUnsupportedFormat.value) {
|
||||
error.value = '无法加载文件'
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/jit-viewer@latest/dist/jit-viewer.min.js'
|
||||
script.onload = () => {
|
||||
jitViewerLoaded.value = true
|
||||
initOfficeViewer()
|
||||
}
|
||||
script.onerror = () => {
|
||||
jitViewerLoaded.value = false
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
// 初始化 Office 预览
|
||||
const initOfficeViewer = async () => {
|
||||
if (!officeViewer.value || !props.previewUrl) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
await nextTick()
|
||||
|
||||
// 使用 jit-viewer 预览
|
||||
if (window.jitView) {
|
||||
officeViewer.value.innerHTML = ''
|
||||
const viewer = document.createElement('div')
|
||||
viewer.style.width = '100%'
|
||||
viewer.style.height = '70vh'
|
||||
officeViewer.value.appendChild(viewer)
|
||||
|
||||
try {
|
||||
window.jitView(viewer, {
|
||||
url: props.previewUrl,
|
||||
fileType: props.previewFile?.name.split('.').pop()?.toLowerCase()
|
||||
})
|
||||
} catch (e) {
|
||||
officeViewer.value.innerHTML = '<div class="preview-error">预览加载失败,请下载后查看</div>'
|
||||
try {
|
||||
// 销毁旧实例
|
||||
if (viewerInstance) {
|
||||
viewerInstance.destroy()
|
||||
viewerInstance = null
|
||||
}
|
||||
|
||||
// 清空容器
|
||||
viewerContainer.value.innerHTML = ''
|
||||
|
||||
// 创建新实例
|
||||
viewerInstance = createViewer({
|
||||
target: viewerContainer.value,
|
||||
file: fileSource,
|
||||
filename: props.previewFile?.name,
|
||||
type: getFileType(props.previewFile?.name || ''),
|
||||
toolbar: true,
|
||||
theme: 'light',
|
||||
locale: 'zh-CN',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pdfRender: 'inset',
|
||||
onError: (err) => {
|
||||
console.error('jit-viewer error:', err)
|
||||
error.value = '预览加载失败,请尝试下载后查看'
|
||||
loading.value = false
|
||||
},
|
||||
onLoad: () => {
|
||||
loading.value = false
|
||||
},
|
||||
onReady: () => {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
viewerInstance.mount()
|
||||
} catch (e) {
|
||||
console.error('Failed to init jit-viewer:', e)
|
||||
error.value = '预览组件加载失败,请尝试下载后查看'
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听弹窗显示和文件变化
|
||||
// 销毁 viewer 实例
|
||||
const destroyViewer = () => {
|
||||
if (viewerInstance) {
|
||||
try {
|
||||
viewerInstance.destroy()
|
||||
} catch (e) {
|
||||
console.error('Failed to destroy viewer:', e)
|
||||
}
|
||||
viewerInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下载
|
||||
const handleDownload = () => {
|
||||
emit('download', props.previewFile)
|
||||
}
|
||||
|
||||
// 关闭弹窗时清理
|
||||
const handleClose = () => {
|
||||
destroyViewer()
|
||||
loading.value = false
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
// 监听弹窗显示
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val && isOffice.value) {
|
||||
jitViewerLoaded.value = false
|
||||
loadJitViewer()
|
||||
if (val) {
|
||||
loading.value = true
|
||||
nextTick(() => {
|
||||
initViewer()
|
||||
})
|
||||
} else {
|
||||
destroyViewer()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.previewUrl, (val) => {
|
||||
if (val && isOffice.value && props.modelValue) {
|
||||
initOfficeViewer()
|
||||
// 监听文件变化
|
||||
watch([() => props.previewUrl, () => props.previewContent], () => {
|
||||
if (props.modelValue) {
|
||||
nextTick(() => {
|
||||
initViewer()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onBeforeUnmount(() => {
|
||||
destroyViewer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.preview-content {
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.preview-dialog-center {
|
||||
:deep(.el-dialog) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
margin-right: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: calc(90vh - 100px);
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
.viewer-container {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
border: none;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
/* 确保 jit-viewer 容器铺满 */
|
||||
.viewer-container :deep(> div) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.preview-office {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
background: #fff;
|
||||
.viewer-container :deep(.jit-viewer-container),
|
||||
.viewer-container :deep(.jit-viewer),
|
||||
.viewer-container :deep([class*="viewer"]) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.preview-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
color: #909399;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.preview-loading p {
|
||||
@@ -187,17 +308,38 @@ watch(() => props.previewUrl, (val) => {
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
color: #f56c6c;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.preview-error p {
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-unsupported {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
color: #909399;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.preview-unsupported p {
|
||||
@@ -205,6 +347,12 @@ watch(() => props.previewUrl, (val) => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-unsupported .tip {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
@@ -213,4 +361,13 @@ watch(() => props.previewUrl, (val) => {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 隐藏 jit-viewer 版权信息 */
|
||||
:deep(.jit-viewer-powered),
|
||||
:deep(.jit-viewer-brand),
|
||||
:deep([class*="powered"]),
|
||||
:deep([class*="credit"]),
|
||||
:deep([class*="brand"]) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="个人信息"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="重命名"
|
||||
@@ -81,7 +81,7 @@ const handleConfirm = async () => {
|
||||
|
||||
let newName = form.name.trim()
|
||||
|
||||
// 如果是文件(不是文件夹),自动补回扩展名
|
||||
// 如果是文件(不是目录),自动补回扩展名
|
||||
if (props.file?.type !== 'folder') {
|
||||
const originalExt = getExtension(originalName.value)
|
||||
const newExt = getExtension(newName)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="共享文件"
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="top-navbar">
|
||||
<div class="navbar-left">
|
||||
<el-icon :size="22" color="#ffffff"><Folder /></el-icon>
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="hex-top" x1="24" y1="2" x2="24" y2="46" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#4db6ac"/>
|
||||
<stop offset="50%" stop-color="#009688"/>
|
||||
<stop offset="100%" stop-color="#004d40"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hex-front" x1="10" y1="22" x2="38" y2="46" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#26a69a"/>
|
||||
<stop offset="100%" stop-color="#00796b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hex-side" x1="4" y1="14" x2="24" y2="44" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#00796b"/>
|
||||
<stop offset="100%" stop-color="#004d40"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hex-highlight" x1="24" y1="4" x2="24" y2="26" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.5)"/>
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="0.5" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- 3D Hexagon: side (left) -->
|
||||
<polygon points="4,14 10,10 10,34 4,38" fill="url(#hex-side)" opacity="0.85"/>
|
||||
<!-- 3D Hexagon: front face -->
|
||||
<polygon points="10,10 38,10 44,14 44,38 38,42 10,42 4,38 4,14" fill="url(#hex-front)"/>
|
||||
<!-- 3D Hexagon: top face -->
|
||||
<polygon points="10,10 24,3 38,10 10,10" fill="url(#hex-top)" opacity="0.9"/>
|
||||
<!-- Highlight on top -->
|
||||
<polygon points="12,10 24,5 36,10" fill="url(#hex-highlight)" opacity="0.6"/>
|
||||
<!-- Inner subtle line for depth -->
|
||||
<polygon points="10,10 38,10 44,14 4,14" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="0.5"/>
|
||||
<!-- Center icon: stylized folder/cloud -->
|
||||
<path d="M19 22h-1a3 3 0 00-3 3v6a3 3 0 003 3h12a3 3 0 003-3v-4a3 3 0 00-3-3h-2l-1.5-2H19z" fill="rgba(255,255,255,0.9)" filter="url(#glow)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="system-title">云文件管理系统</span>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
@@ -131,6 +172,17 @@ const handleUserCommand = (command) => {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.system-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="上传文件"
|
||||
width="650px"
|
||||
class="custom-dialog"
|
||||
@open="handleDialogOpen"
|
||||
>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
multiple
|
||||
:limit="11"
|
||||
@@ -20,6 +20,10 @@
|
||||
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择</em>(最多10个)</div>
|
||||
</el-upload>
|
||||
|
||||
<div class="storage-info">
|
||||
<span>剩余空间:{{ formatSize(remainingStorage) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="file-list-preview" v-if="fileList.length > 0">
|
||||
<div class="file-list-header">
|
||||
<span>文件名</span>
|
||||
@@ -37,14 +41,18 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-list-footer">
|
||||
<span>共 {{ fileList.length }} 个文件,总大小:{{ formatSize(totalSize) }}</span>
|
||||
<span v-if="isOverLimit" class="warning-text">(超出剩余空间)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">
|
||||
<el-button @click="handleCancel">
|
||||
<el-icon><Close /></el-icon>
|
||||
<span style="margin-left: 4px">取消</span>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleUpload" :loading="uploading">
|
||||
<el-button type="primary" @click="handleUpload" :loading="uploading" :disabled="isOverLimit">
|
||||
<el-icon><Upload /></el-icon>
|
||||
<span style="margin-left: 4px">开始上传</span>
|
||||
</el-button>
|
||||
@@ -59,7 +67,8 @@ import { UploadFilled, Close, Upload, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
uploading: { type: Boolean, default: false }
|
||||
uploading: { type: Boolean, default: false },
|
||||
remainingStorage: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'upload'])
|
||||
@@ -71,6 +80,16 @@ const visible = computed({
|
||||
|
||||
const fileList = ref([])
|
||||
|
||||
// 计算总大小
|
||||
const totalSize = computed(() => {
|
||||
return fileList.value.reduce((sum, f) => sum + (f.size || 0), 0)
|
||||
})
|
||||
|
||||
// 是否超出剩余空间
|
||||
const isOverLimit = computed(() => {
|
||||
return totalSize.value > props.remainingStorage
|
||||
})
|
||||
|
||||
const handleChange = (file, list) => {
|
||||
if (list.length > 10) {
|
||||
ElMessage.warning('最多一次上传10个文件')
|
||||
@@ -104,9 +123,18 @@ const handleUpload = () => {
|
||||
ElMessage.warning('请选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
emit('upload', fileList.value.map(f => f.raw))
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
// 对外暴露方法
|
||||
defineExpose({
|
||||
clear: () => { fileList.value = [] },
|
||||
@@ -115,6 +143,15 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.storage-info {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.file-list-preview {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
@@ -165,4 +202,18 @@ defineExpose({
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-list-footer {
|
||||
padding: 10px 12px;
|
||||
background: #f5f7fa;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user