1
This commit is contained in:
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<div class="file-panel">
|
||||
<div class="panel-header">
|
||||
<span>本地文件</span>
|
||||
<div class="header-actions">
|
||||
<el-tooltip content="上传" placement="top" :show-after="300">
|
||||
<el-button class="icon-btn" link @click.stop="toggleUploadMenu">
|
||||
<el-icon><Upload /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="下载项目" placement="top" :show-after="300">
|
||||
<el-button class="icon-btn" link @click="handleDownloadProject">
|
||||
<el-icon><Download /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新" placement="top" :show-after="300">
|
||||
<el-button class="icon-btn" link @click="fetchFiles">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="关闭" placement="top" :show-after="300">
|
||||
<el-button class="icon-btn" link @click="emit('close')">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content" @contextmenu.prevent="handlePanelContextMenu">
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="fileTree.length === 0" class="empty-wrapper">
|
||||
<span>暂无文件</span>
|
||||
</div>
|
||||
<el-scrollbar v-else height="calc(100vh - 200px)">
|
||||
<el-tree
|
||||
:data="fileTree"
|
||||
:props="treeProps"
|
||||
@node-click="handleNodeClick"
|
||||
node-key="path"
|
||||
:expand-on-click-node="false"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node" @contextmenu.stop.prevent="handleContextMenu($event, data)">
|
||||
<el-icon v-if="data.type === 'directory'"><Folder /></el-icon>
|
||||
<el-icon v-else><Document /></el-icon>
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<el-icon
|
||||
v-if="data.type === 'file'"
|
||||
class="download-icon"
|
||||
@click.stop="handleDownload(data)"
|
||||
><Download /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-show="contextMenuVisible"
|
||||
class="context-menu"
|
||||
:style="{ top: contextMenuY + 'px', left: contextMenuX + 'px' }"
|
||||
>
|
||||
<template v-if="contextMenuScope === 'panel'">
|
||||
<div class="context-menu-item" @click="handleContextNewFile">新建文件</div>
|
||||
<div class="context-menu-item" @click="handleContextNewFolder">新建文件夹</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="contextMenuData?.type === 'directory'">
|
||||
<div class="context-menu-item" @click="handleContextNewFile">新建文件</div>
|
||||
<div class="context-menu-item" @click="handleContextNewFolder">新建文件夹</div>
|
||||
<div class="context-menu-item" @click="handleContextDownloadFolder">下载</div>
|
||||
</template>
|
||||
<div v-else-if="contextMenuData?.type === 'file'" class="context-menu-item" @click="handleContextDownload">下载</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item" @click="handleContextRename">重命名</div>
|
||||
<div class="context-menu-item danger" @click="handleContextDelete">删除</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 新建文件/文件夹对话框 -->
|
||||
<el-dialog v-model="createDialogVisible" :title="createDialogTitle" width="400px" :close-on-click-modal="false">
|
||||
<el-input v-model="createName" placeholder="请输入名称" @keyup.enter="confirmCreate" />
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmCreate">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<el-dialog v-model="renameDialogVisible" title="重命名" width="400px" :close-on-click-modal="false">
|
||||
<el-input v-model="renameName" placeholder="请输入新名称" @keyup.enter="confirmRename" />
|
||||
<template #footer>
|
||||
<el-button @click="renameDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmRename">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 上传模式下拉菜单(与 .context-menu 视觉一致) -->
|
||||
<div
|
||||
v-show="uploadMenuVisible"
|
||||
class="context-menu upload-menu"
|
||||
:style="{ top: uploadMenuY + 'px', left: uploadMenuX + 'px' }"
|
||||
>
|
||||
<div class="context-menu-item" @click="pickUpload('upload-file')">上传文件</div>
|
||||
<div class="context-menu-item" @click="pickUpload('upload-folder')">上传文件夹</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传文件/文件夹对话框 -->
|
||||
<input ref="filePickerRef" type="file" multiple style="display:none" @change="onPickerChange" />
|
||||
<input ref="folderPickerRef" type="file" webkitdirectory multiple style="display:none" @change="onPickerChange" />
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
:title="uploadDialogTitle"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
class="upload-dialog"
|
||||
>
|
||||
<div class="upload-body">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="picker-btn"
|
||||
@click="onPickClick"
|
||||
>
|
||||
<el-icon class="el-icon--left"><FolderOpened v-if="isFolderMode" /><Upload v-else /></el-icon>
|
||||
{{ isFolderMode ? '选择文件夹' : '选择文件' }}
|
||||
</el-button>
|
||||
|
||||
<div class="file-list-container">
|
||||
<div v-if="uploadFileList.length === 0" class="empty-tip">未选择文件</div>
|
||||
<div
|
||||
v-for="(file, index) in uploadFileList"
|
||||
:key="index"
|
||||
class="file-list-item"
|
||||
>
|
||||
<span class="path" :title="file._relativePath">{{ file._relativePath }}</span>
|
||||
<span class="size">{{ formatSize(file.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmUpload">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Folder, FolderOpened, Document, Download, Refresh, Upload, Loading } from '@element-plus/icons-vue'
|
||||
import { listFiles, downloadFile, createFileOrDirectory, uploadFileOrDirectory, deleteFileOrDirectory, renameFile, downloadProject } from '@/api/frontend'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
workspacePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'file-click'])
|
||||
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const fileTree = ref([])
|
||||
const defaultExpandedKeys = ref([])
|
||||
|
||||
// 右键菜单
|
||||
const contextMenuVisible = ref(false)
|
||||
const contextMenuX = ref(0)
|
||||
const contextMenuY = ref(0)
|
||||
const contextMenuData = ref(null)
|
||||
const contextMenuScope = ref('node')
|
||||
|
||||
// 新建对话框
|
||||
const createDialogVisible = ref(false)
|
||||
const createName = ref('')
|
||||
const createType = ref('file')
|
||||
const createDialogTitle = ref('')
|
||||
const createTargetPath = ref('/')
|
||||
|
||||
// 重命名对话框
|
||||
const renameDialogVisible = ref(false)
|
||||
const renameName = ref('')
|
||||
|
||||
// 上传对话框
|
||||
const uploadDialogVisible = ref(false)
|
||||
const uploadDialogTitle = ref('')
|
||||
const uploadFileList = ref([])
|
||||
const filePickerRef = ref(null)
|
||||
const folderPickerRef = ref(null)
|
||||
const isFolderMode = computed(() => uploadDialogTitle.value === '上传文件夹')
|
||||
|
||||
// 上传模式下拉菜单(context-menu 风格)
|
||||
const uploadMenuVisible = ref(false)
|
||||
const uploadMenuX = ref(0)
|
||||
const uploadMenuY = ref(0)
|
||||
|
||||
// 点击其他区域关闭右键菜单
|
||||
function onDocClick() {
|
||||
contextMenuVisible.value = false
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocClick)
|
||||
fetchFiles()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onDocClick)
|
||||
})
|
||||
|
||||
async function fetchFiles() {
|
||||
loading.value = true
|
||||
try {
|
||||
let res = await listFiles(props.workspacePath)
|
||||
let arr = res
|
||||
if (res && typeof res.msg === 'string') {
|
||||
try {
|
||||
arr = JSON.parse(res.msg)
|
||||
} catch (parseError) {
|
||||
// msg 不是 JSON(如 "项目文件请求失败: ..." 这种错误描述),按错误处理
|
||||
console.error('解析文件列表响应失败:', parseError, 'raw:', res.msg)
|
||||
ElMessage.error(res.msg)
|
||||
arr = []
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(arr)) arr = []
|
||||
fileTree.value = arr
|
||||
defaultExpandedKeys.value = []
|
||||
} catch (error) {
|
||||
console.error('获取文件列表失败:', error)
|
||||
fileTree.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownload(data) {
|
||||
try {
|
||||
const blob = await downloadFile(props.workspacePath, data.path)
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = data.name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(downloadUrl)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateCommand(command) {
|
||||
if (command === 'upload-file') {
|
||||
uploadDialogTitle.value = '上传文件'
|
||||
uploadFileList.value = []
|
||||
uploadDialogVisible.value = true
|
||||
} else if (command === 'upload-folder') {
|
||||
uploadDialogTitle.value = '上传文件夹'
|
||||
uploadFileList.value = []
|
||||
uploadDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUploadMenu(e) {
|
||||
if (uploadMenuVisible.value) {
|
||||
uploadMenuVisible.value = false
|
||||
return
|
||||
}
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
uploadMenuX.value = rect.left
|
||||
uploadMenuY.value = rect.bottom + 4
|
||||
uploadMenuVisible.value = true
|
||||
}
|
||||
|
||||
function pickUpload(command) {
|
||||
uploadMenuVisible.value = false
|
||||
handleCreateCommand(command)
|
||||
}
|
||||
|
||||
function onUploadMenuDocClick(e) {
|
||||
if (!uploadMenuVisible.value) return
|
||||
if (e.target.closest('.upload-menu')) return
|
||||
uploadMenuVisible.value = false
|
||||
}
|
||||
|
||||
watch(uploadMenuVisible, (val) => {
|
||||
if (val) {
|
||||
setTimeout(() => document.addEventListener('click', onUploadMenuDocClick), 0)
|
||||
} else {
|
||||
document.removeEventListener('click', onUploadMenuDocClick)
|
||||
}
|
||||
})
|
||||
|
||||
async function confirmCreate() {
|
||||
if (!createName.value.trim()) {
|
||||
ElMessage.warning('请输入名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await createFileOrDirectory(props.workspacePath, createType.value, createName.value.trim(), createTargetPath.value)
|
||||
ElMessage.success('创建成功')
|
||||
createDialogVisible.value = false
|
||||
fetchFiles()
|
||||
} catch (error) {
|
||||
console.error('创建失败:', error)
|
||||
ElMessage.error('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
function onPickClick() {
|
||||
if (isFolderMode.value) {
|
||||
folderPickerRef.value?.click()
|
||||
} else {
|
||||
filePickerRef.value?.click()
|
||||
}
|
||||
}
|
||||
|
||||
function onPickerChange(e) {
|
||||
const files = Array.from(e.target.files || [])
|
||||
uploadFileList.value = files
|
||||
uploadFileList.value.forEach(f => {
|
||||
f._relativePath = f.webkitRelativePath || f.name
|
||||
})
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === undefined || bytes === null) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
async function confirmUpload() {
|
||||
if (uploadFileList.value.length === 0) {
|
||||
ElMessage.warning('请选择文件')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const files = uploadFileList.value
|
||||
const isFolder = uploadDialogTitle.value === '上传文件夹'
|
||||
const relativePaths = files.map(f => isFolder ? f._relativePath : f.name)
|
||||
await uploadFileOrDirectory(props.workspacePath, '', relativePaths, files)
|
||||
ElMessage.success('上传成功')
|
||||
uploadDialogVisible.value = false
|
||||
uploadFileList.value = []
|
||||
fetchFiles()
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownloadProject() {
|
||||
try {
|
||||
const { blob, filename } = await downloadProject(props.workspacePath)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleContextDownloadFolder() {
|
||||
const data = contextMenuData.value
|
||||
contextMenuVisible.value = false
|
||||
if (!data) return
|
||||
try {
|
||||
const { blob, filename } = await downloadProject(data.path)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextMenu(e, data) {
|
||||
contextMenuData.value = data
|
||||
contextMenuScope.value = 'node'
|
||||
contextMenuX.value = e.clientX
|
||||
contextMenuY.value = e.clientY
|
||||
contextMenuVisible.value = true
|
||||
}
|
||||
|
||||
function handlePanelContextMenu(e) {
|
||||
contextMenuData.value = null
|
||||
contextMenuScope.value = 'panel'
|
||||
contextMenuX.value = e.clientX
|
||||
contextMenuY.value = e.clientY
|
||||
contextMenuVisible.value = true
|
||||
}
|
||||
|
||||
async function handleContextDownload() {
|
||||
contextMenuVisible.value = false
|
||||
await handleDownload(contextMenuData.value)
|
||||
}
|
||||
|
||||
function handleContextRename() {
|
||||
contextMenuVisible.value = false
|
||||
renameName.value = contextMenuData.value.name
|
||||
renameDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleContextDelete() {
|
||||
contextMenuVisible.value = false
|
||||
try {
|
||||
await deleteFileOrDirectory(props.workspacePath, contextMenuData.value.path, contextMenuData.value.type)
|
||||
ElMessage.success('删除成功')
|
||||
fetchFiles()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRename() {
|
||||
if (!renameName.value.trim()) {
|
||||
ElMessage.warning('请输入新名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await renameFile(props.workspacePath, contextMenuData.value.path, renameName.value.trim())
|
||||
ElMessage.success('重命名成功')
|
||||
renameDialogVisible.value = false
|
||||
fetchFiles()
|
||||
} catch (error) {
|
||||
console.error('重命名失败:', error)
|
||||
ElMessage.error('重命名失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextNewFile() {
|
||||
contextMenuVisible.value = false
|
||||
createType.value = 'file'
|
||||
createDialogTitle.value = '新建文件'
|
||||
createName.value = ''
|
||||
createTargetPath.value = contextMenuScope.value === 'panel' ? '/' : contextMenuData.value.path
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleContextNewFolder() {
|
||||
contextMenuVisible.value = false
|
||||
createType.value = 'directory'
|
||||
createDialogTitle.value = '新建文件夹'
|
||||
createName.value = ''
|
||||
createTargetPath.value = contextMenuScope.value === 'panel' ? '/' : contextMenuData.value.path
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val && fileTree.value.length === 0) {
|
||||
fetchFiles()
|
||||
}
|
||||
})
|
||||
|
||||
function handleNodeClick(data) {
|
||||
if (data.type === 'file') {
|
||||
emit('file-click', data)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-panel {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
background: var(--fe-bg-elevated);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.panel-header {
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid var(--fe-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--fe-text-primary);
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.icon-btn {
|
||||
padding: 4px 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 0 !important;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--fe-text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fe-bg-hover);
|
||||
color: var(--fe-text-secondary);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.loading-wrapper,
|
||||
.empty-wrapper {
|
||||
padding:20px;
|
||||
text-align: center;
|
||||
color: var(--fe-text-muted);
|
||||
font-size: 13px;
|
||||
// position: absolute;
|
||||
// inset: 0;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// color: var(--fe-text-muted);
|
||||
// font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 24px;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
|
||||
.node-label {
|
||||
flex: 0 1 calc(100% - 100px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
color: var(--fe-accent);
|
||||
margin-left: 8px;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--fe-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .download-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: var(--fe-bg-elevated);
|
||||
border: 1px solid var(--fe-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--fe-shadow-md);
|
||||
padding: 4px 0;
|
||||
min-width: 120px;
|
||||
|
||||
.context-menu-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fe-bg-overlay);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--fe-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background: var(--fe-border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.context-menu.upload-menu {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.upload-dialog {
|
||||
.upload-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.picker-btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.file-list-container {
|
||||
border: 1px solid var(--fe-border);
|
||||
border-radius: 4px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--fe-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--fe-border-strong);
|
||||
}
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--fe-border);
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.path {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16px;
|
||||
color: var(--fe-text-primary);
|
||||
}
|
||||
|
||||
.size {
|
||||
color: var(--fe-text-muted);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: var(--fe-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user