优化文件管理器

This commit is contained in:
pushuo 2022-07-18 09:08:10 +08:00
parent c45bc8ca29
commit cf035f824e
8 changed files with 251 additions and 41 deletions

View File

@ -1,3 +1,4 @@
@charset "UTF-8";
[v-cloak] {
display: none;
}
@ -6,6 +7,14 @@ body.page-filemanager {
height: 100vh;
overflow: hidden;
font-size: 12px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* CSS3属性 */
}
body.page-filemanager [class*=" el-icon-"], body.page-filemanager [class^=el-icon-] {
font-weight: 600;
}
body.page-filemanager .filemanager-wrap {
display: flex;
@ -17,6 +26,15 @@ body.page-filemanager .filemanager-wrap .filemanager-navbar {
background-color: #293042;
overflow-y: auto;
}
body.page-filemanager .filemanager-wrap .filemanager-navbar::-webkit-scrollbar {
width: 2px;
}
body.page-filemanager .filemanager-wrap .filemanager-navbar::-webkit-scrollbar-thumb {
background: #409EFF;
}
body.page-filemanager .filemanager-wrap .filemanager-navbar::-webkit-scrollbar-track {
background: transparent;
}
body.page-filemanager .filemanager-wrap .filemanager-navbar .el-tree {
background-color: transparent;
}
@ -139,7 +157,6 @@ body.page-filemanager .filemanager-wrap .filemanager-content .content-center .im
body.page-filemanager .filemanager-wrap .filemanager-content .content-center .image-list .text .el-icon-check {
color: #409EFF;
font-size: 18px;
font-weight: 600;
}
body.page-filemanager .filemanager-wrap .filemanager-content .content-footer {
height: 56px;

View File

@ -2102,17 +2102,18 @@ __webpack_require__.r(__webpack_exports__);
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
window.axios = __webpack_require__(/*! axios */ "./node_modules/axios/index.js");
var token = document.querySelector('meta[name="csrf-token"]').content;
var base = document.querySelector('base').href;
var instance = axios.create({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
'X-CSRF-TOKEN': token
}
});
axios.defaults.timeout = 5000; // 请求超时
// axios.defaults.baseURL = process.env.NODE_ENV == 'production' ? process.env.VUE_APP_BASE_URL + '/' : '/';
// console.log(process.env.VUE_APP_BASE_URL)
console.log($('base').attr('href'));
axios.defaults.baseURL = $('base').attr('href');
axios.defaults.baseURL = base;
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
/**
* get 请求

View File

@ -2068,17 +2068,18 @@ __webpack_require__.r(__webpack_exports__);
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
window.axios = __webpack_require__(/*! axios */ "./node_modules/axios/index.js");
var token = document.querySelector('meta[name="csrf-token"]').content;
var base = document.querySelector('base').href;
var instance = axios.create({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
'X-CSRF-TOKEN': token
}
});
axios.defaults.timeout = 5000; // 请求超时
// axios.defaults.baseURL = process.env.NODE_ENV == 'production' ? process.env.VUE_APP_BASE_URL + '/' : '/';
// console.log(process.env.VUE_APP_BASE_URL)
console.log($('base').attr('href'));
axios.defaults.baseURL = $('base').attr('href');
axios.defaults.baseURL = base;
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
/**
* get 请求

100
public/vendor/vue/batch_select.js vendored Normal file
View File

@ -0,0 +1,100 @@
// 自 https://blog.wy310.cn/2020/12/17/vue-drag-batch-select/ 修改
Vue.directive('batch-select', {
// 当被绑定的元素插入到 DOM 中时……
// inserted: (el, binding) => {}
componentUpdated: (el, binding) => {
// 设置被绑定元素el即上述的box的position为relative目的是让蓝色半透明遮罩area相对其定位
el.style.position = 'relative';
// 记录el在视窗中的位置elPos
const { x, y } = el.getBoundingClientRect()
const elPos = { x, y }
// 获取该指令调用者传递过来的参数className / selectArr 是选中的 index 要放的数组
// v-batch-select="{ className: '.el-checkbox', selectArr: [] }"
// 表示要使用鼠标框选类名为'.el-checkbox'的元素
const optionClassName = binding.value.className
const options = [].slice.call(el.querySelectorAll(optionClassName))
// 获取被框选对象们的x、y、width、height
const optionsXYWH = []
options.forEach(v => {
const obj = v.getBoundingClientRect()
optionsXYWH.push({
x: obj.x - elPos.x,
y: obj.y - elPos.y,
w: obj.width,
h: obj.height
})
})
// 创建一个div作为area区域注意定位是absolutevisibility初始值是hidden
const area = document.createElement('div')
area.style = 'position: absolute; border: 1px solid #409eff; background-color: rgba(64, 158, 255, 0.1); z-index: 10; visibility: hidden;'
area.className = 'area'
area.innerHTML = ''
el.appendChild(area)
el.onmousedown = (e) => {
// 获取鼠标按下时相对box的坐标
const startX = e.clientX - elPos.x
const startY = e.clientY - elPos.y
// 判断鼠标按下后是否发生移动的标识
let hasMove = false
document.onmousemove = (e) => {
hasMove = true
binding.value.setSelectStatus(true)
// 显示area
area.style.visibility = 'visible'
// 获取鼠标移动过程中指针的实时位置
const endX = e.clientX - elPos.x
const endY = e.clientY - elPos.y
// 这里使用绝对值是为了兼容鼠标从各个方向框选的情况
const width = Math.abs(endX - startX)
const height = Math.abs(endY - startY)
// 根据初始位置和实时位置计算出area的left、top、width、height
const left = Math.min(startX, endX)
const top = Math.min(startY, endY)
// 实时绘制
area.style.left = `${left}px`
area.style.top = `${top}px`
area.style.width = `${width}px`
area.style.height = `${height}px`
const areaTop = parseInt(top)
const areaRight = parseInt(left) + parseInt(width)
const areaBottom = parseInt(top) + parseInt(height)
const areaLeft = parseInt(left)
binding.value.selectImageIndex.length = 0
optionsXYWH.forEach((v, i) => {
const optionTop = v.y
const optionRight = v.x + v.w
const optionBottom = v.y + v.h
const optionLeft = v.x
if (!(areaTop > optionBottom || areaRight < optionLeft || areaBottom < optionTop || areaLeft > optionRight)) {
// 该指令的调用者可以监听到selectIdxs的变化
binding.value.selectImageIndex.push(i)
}
})
}
document.onmouseup = (e) => {
document.onmousemove = document.onmouseup = null
if (hasMove) {
// 鼠标抬起时,如果之前发生过移动,则执行碰撞检测
const { left, top, width, height } = area.style
setTimeout(() => {
binding.value.setSelectStatus(false)
}, 100);
}
// 恢复以下数据
hasMove = false
area.style.visibility = 'hidden'
area.style.left = 0
area.style.top = 0
area.style.width = 0
area.style.height = 0
return false
}
}
},
// update: (el, binding) => {}
})

View File

@ -11,6 +11,11 @@ body.page-filemanager {
height: 100vh;
overflow: hidden;
font-size: 12px;
user-select:none; /* CSS3属性 */
[class*=" el-icon-"], [class^=el-icon-] {
font-weight: 600;
}
.filemanager-wrap {
display: flex;
@ -22,6 +27,18 @@ body.page-filemanager {
background-color: #293042;
overflow-y: auto;
&::-webkit-scrollbar {
width: 2px;
}
&::-webkit-scrollbar-thumb {
background: $primary;
}
&::-webkit-scrollbar-track {
background: transparent;
}
.el-tree {
background-color: transparent;
.el-tree-node__content {
@ -206,7 +223,6 @@ body.page-filemanager {
.el-icon-check {
color: $primary;
font-size: 18px;
font-weight: 600;
}
}
}

View File

@ -115,9 +115,9 @@
that.loading = false;
if (that.isLanguage) {
that.src[that.tabActiveId] = images.path;
that.src[that.tabActiveId] = images[0].path;
} else {
that.src = images.path;
that.src = images[0].path;
}
// console.log(that.src);
}

View File

@ -2,17 +2,19 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<base href="{{ $admin_base_url }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="{{ asset('vendor/vue/2.6.12/vue.js') }}"></script>
<script src="{{ asset('vendor/element-ui/2.15.9/index.js') }}"></script>
{{-- <script src="{{ asset('vendor/element-ui/2.15.6/js.js') }}"></script> --}}
<script src="{{ asset('vendor/jquery/jquery-3.6.0.min.js') }}"></script>
<script src="{{ asset('vendor/layer/3.5.1/layer.js') }}"></script>
<script src="{{ asset('vendor/vue/batch_select.js') }}"></script>
<link href="{{ mix('/build/beike/admin/css/bootstrap.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ asset('vendor/element-ui/2.15.9/index.css') }}">
<link href="{{ mix('build/beike/admin/css/filemanager.css') }}" rel="stylesheet">
<script src="{{ mix('build/beike/admin/js/app.js') }}"></script>
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>beike filemanager</title>
</head>
<body class="page-filemanager">
@ -54,15 +56,16 @@
<div class="filemanager-content" v-loading="loading" element-loading-background="rgba(255, 255, 255, 0.5)">
<div class="content-head">
<div class="left">
<el-link :underline="false" :disabled="editingImageIndex === null" icon="el-icon-download">下载</el-link>
<el-link :underline="false" :disabled="editingImageIndex === null" @click="deleteFile" icon="el-icon-delete">删除</el-link>
<el-link :underline="false" :disabled="editingImageIndex === null" @click="openInputBox('image')" icon="el-icon-edit">重命名</el-link>
<el-link :underline="false" :disabled="!!!selectImageIndex.length" icon="el-icon-download" @click="downloadImages">下载</el-link>
<el-link :underline="false" :disabled="!!!selectImageIndex.length" @click="deleteFile" icon="el-icon-delete">删除</el-link>
<el-link :underline="false" :disabled="selectImageIndex.length == 1 ? false : true" @click="openInputBox('image')" icon="el-icon-edit">重命名</el-link>
<el-link :underline="false" :disabled="!!!images.length && !!!selectImageIndex.length" @click="selectAll()" icon="el-icon-finished">全选</el-link>
</div>
<div class="right">
<el-button size="small" type="primary" @click="openUploadFile" icon="el-icon-upload2">上传文件</el-button>
</div>
</div>
<div class="content-center">
<div class="content-center" v-batch-select="{ className: '.image-list', selectImageIndex, setSelectStatus: updateSelectStatus }">
<div :class="['image-list', file.selected ? 'active' : '']" v-for="file, index in images" :key="index" @click="checkedImage(index)">
<div class="img"><img :src="file.url"></div>
<div class="text">
@ -81,7 +84,7 @@
:total="image_total">
</el-pagination>
</div>
<div class="right"><el-button size="small" icon="el-icon-check" type="primary" @click="fileChecked" :disabled="editingImageIndex === null">选择</el-button></div>
<div class="right"><el-button size="small" icon="el-icon-check" type="primary" @click="fileChecked" :disabled="!!!selectImageIndex.length">选择</el-button></div>
</div>
</div>
@ -133,10 +136,13 @@
max: 40,
paneLengthPercent: 26,
triggerLength: 10,
isShift: false,
ssss:[],
loading: false,
isBatchSelect: false,
editingImageIndex: null,
selectImageIndex: [],
treeData: [{name: '图片空间', path: '/', children: @json($folders)}],
@ -145,6 +151,7 @@
label: 'name',
isLeaf: 'leaf'
},
selectIdxs: [],
uploadFileDialog: {
show: false,
@ -168,7 +175,20 @@
},
// 侦听器
watch: {
images: {
handler(val) {
if (this.isBatchSelect) return;
// 将选中的图片索引放入 selectImageIndex未选中则清空
this.selectImageIndex = val.filter(item => item.selected).map(e => this.images.indexOf(e));
},
deep: true
},
selectImageIndex(indexs) {
this.images.forEach((item, index) => {
item.selected = indexs.includes(index);
});
},
},
// 组件方法
methods: {
@ -181,6 +201,10 @@
this.loadData(e, node)
},
updateSelectStatus(status) {
this.isBatchSelect = status
},
pageCurrentChange(e) {
this.image_page = e
this.loadData()
@ -298,13 +322,32 @@
},
checkedImage(index) {
this.editingImageIndex = index;
this.images.map(e => !e.index ? e.selected = false : '')
// 获取当前选中的 index
const selectedIndex = this.images.findIndex(e => e.selected);
if (this.isShift) {
// 获取 selectedIndex 与 index 之间的所有图片
let selectedImages = this.images.slice(Math.min(selectedIndex, index), Math.max(selectedIndex, index) + 1);
selectedImages.map(e => e.selected = true)
return;
}
if (this.isCtrl) {
this.images[index].selected = !this.images[index].selected;
return;
}
if (this.selectImageIndex.length > 1) {
this.images.map((e,i) => i != index ? e.selected = false : e.selected = true)
return;
}
this.images.map((e,i) => i != index ? e.selected = false : '')
this.images[index].selected = !this.images[index].selected
},
fileChecked() {
let typedFiles = this.images[this.editingImageIndex];
let typedFiles = this.images.filter(e => e.selected)
if (callback !== null) {
callback(typedFiles);
@ -319,7 +362,10 @@
this.$confirm('是否要删除选中文件', '提示', {
type: 'warning'
}).then(() => {
this.images.splice(this.editingImageIndex, 1);
// 删除选中的文件
console.log(this.selectImageIndex);
// this.images.splice(this.selectImageIndex, 1);
this.$message({type: 'success',message: '删除成功!'});
}).catch(_=>{});
},
@ -340,13 +386,34 @@
}
},
selectAll() {
// 获取 this.images 中的 selected 是否全部为 true
const isAllSelected = this.images.every(e => e.selected);
this.images.map(e => e.selected = !isAllSelected)
},
downloadImages() {
// 获取选中的图片
const selectedImages = this.images.filter(e => e.selected);
// 创建 a 标签
selectedImages.forEach(e => {
const a = document.createElement('a');
// 设置 a 标签的 href 属性
a.href = e.origin_url;
// 设置 a 标签的 download 属性
a.download = e.name;
// 触发 a 标签的 click 事件
a.click();
});
},
openInputBox(type, node, data) {
this.$prompt('', type == 'addFolder' ? '新建文件夹' : '重命名', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^.+$/,
closeOnClickModal: false,
inputValue: type == 'image' ? this.images[this.editingImageIndex].name : (type == 'renameFolder' ? data.name : '新建文件夹'),
inputValue: type == 'image' ? this.images[this.selectImageIndex].name : (type == 'renameFolder' ? data.name : '新建文件夹'),
inputErrorMessage: '不能为空'
}).then(({ value }) => {
@ -396,10 +463,7 @@
}
}
},
// 在实例初始化之后组件属性计算之前如data属性等
beforeCreate () {
},
// 在实例创建完成后被立即同步调用
created () {
const defaultkeyarr = sessionStorage.getItem('defaultkeyarr');
@ -407,22 +471,31 @@
// this.defaultkeyarr = defaultkeyarr.split(',');
}
},
// 在挂载开始之前被调用:相关的 render 函数首次被调用
beforeMount () {
},
// 实例被挂载后调用
mounted () {
// 获取键盘事件 是否按住 shift/ctrl 键 兼容 mac 和 windows
document.addEventListener('keydown', (e) => {
this.isShift = e.shiftKey;
this.isCtrl = e.ctrlKey || e.metaKey;
})
// 获取键盘事件 是否松开 shift/ctrl 键
document.addEventListener('keyup', (e) => {
this.isShift = e.shiftKey;
this.isCtrl = e.ctrlKey || e.metaKey;
})
// 判断鼠标是否点击 .image-list 元素
document.addEventListener('click', (e) => {
if (this.isBatchSelect) return;
const targets = ['filemanager-navbar', 'content-center']
if (targets.indexOf(e.target.className) > -1) {
this.selectImageIndex = [];
this.images.map(e => e.selected = false)
}
})
},
})
$(document).ready(function() {
$(document).on('click', function (e) {
if ($(e.target).closest('.content-center .image-list, .content-head, .content-footer, .el-message-box').length === 0) {
app.editingImageIndex = null;
app.images.map(e => e.selected = false)
}
})
});
</script>
</body>
</html>

View File

@ -1,14 +1,16 @@
window.axios = require('axios');
const token = document.querySelector('meta[name="csrf-token"]').content;
const base = document.querySelector('base').href;
const instance = axios.create({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
headers: {'X-CSRF-TOKEN': token},
});
axios.defaults.timeout = 5000; // 请求超时
// axios.defaults.baseURL = process.env.NODE_ENV == 'production' ? process.env.VUE_APP_BASE_URL + '/' : '/';
// console.log(process.env.VUE_APP_BASE_URL)
console.log($('base').attr('href'))
axios.defaults.baseURL = $('base').attr('href');
axios.defaults.baseURL = base;
export default {
/**
* get 请求