admin/src/components/piAsset/picker.vue

664 lines
16 KiB
Vue

<template>
<el-dialog title="选择文件" v-model="visible" :width="900" destroy-on-close @closed="$emit('closed')"
append-to-body>
<div class="pi-file-select">
<div class="pi-file-select__side" v-loading="menuLoading">
<div class="pi-file-select__side-menu">
<el-tree ref="menuRef" class="menu" :data="menu" :node-key="treeProps.key" :props="treeProps"
:expand-on-click-node="false" check-strictly show-checkbox
:current-node-key="menu.length>0?menu[0][treeProps.key]:''" highlight-current
@node-click="groupClick" :check-on-click-leaf="false">
<template #default="{ node, data }">
<span class="custom-tree-node el-tree-node__label">
<span class="label">
{{ node.label }}
</span>
<span class="do">
<el-icon v-if="data.category_id != 0" @click.stop="editCategory(data)"><el-icon-edit/></el-icon>
</span>
</span>
</template>
</el-tree>
</div>
<div class="pi-file-select__side-msg">
<el-button type="primary" size="small" icon="el-icon-plus" @click="addCategory"></el-button>
<el-button type="danger" size="small" plain icon="el-icon-delete" @click="delCategory"></el-button>
</div>
</div>
<div class="pi-file-select__files" v-loading="listLoading">
<div class="pi-file-select__top">
<div class="upload" v-if="!hideUpload">
<el-upload class="pi-file-select__upload" action="" multiple :show-file-list="false"
:accept="accept" :on-change="uploadChange" :before-upload="uploadBefore"
:on-progress="uploadProcess" :on-success="uploadSuccess" :on-error="uploadError"
:http-request="uploadRequest">
<el-button type="primary" icon="el-icon-upload">本地上传</el-button>
</el-upload>
<el-button type="danger" icon="el-icon-delete" @click="delFile">删除</el-button>
<el-button type="primary" icon="el-icon-rank" @click="moveFile">移动</el-button>
</div>
<div class="keyword">
<el-input v-model="file_name" style="width: 220px;" prefix-icon="el-icon-search"
placeholder="文件名搜索" clearable @keyup.enter="search" @input="search"
@clear="search"></el-input>
</div>
</div>
<div class="pi-file-select__list">
<el-scrollbar ref="scrollbarRef">
<el-empty v-if="fileList.length==0 && data.length==0" description="无数据"
:image-size="80"></el-empty>
<div v-for="(file, index) in fileList" :key="index" class="pi-file-select__item">
<div class="pi-file-select__item__file">
<div class="pi-file-select__item__upload">
<el-progress type="circle" :percentage="file.progress" :width="70"></el-progress>
</div>
<el-image :src="file.tempImg" fit="contain"></el-image>
</div>
<p>{{ file.name }}</p>
</div>
<div v-for="item in data" :key="item[fileProps.id]" class="pi-file-select__item"
:class="{active: active(item) }" @click="select(item)">
<div class="pi-file-select__item__file">
<div class="pi-file-select__item__checkbox" v-if="multiple">
<el-icon>
<el-icon-check/>
</el-icon>
</div>
<div class="pi-file-select__item__select" v-else>
<el-icon>
<el-icon-check/>
</el-icon>
</div>
<div class="pi-file-select__item__box"></div>
<el-image v-if="obj.isImg(item[fileProps.url])"
:src="config.API_URL + '/' + item[fileProps.url]" fit="contain"
lazy></el-image>
<div v-else-if="obj.isVideo(item[fileProps.url])" class="item-video">
<el-icon size="32px">
<component :is="'pi-icon-video'"/>
</el-icon>
</div>
<div v-else class="item-file item-file-doc">
<el-icon size="32px">
<component :is="'pi-icon-task'"/>
</el-icon>
</div>
</div>
<p :title="item[fileProps.fileName]">{{ item[fileProps.fileName] }}</p>
</div>
</el-scrollbar>
</div>
<div class="pi-file-select__pagination">
<el-pagination size="small" background layout="prev, pager, next" :total="total"
:page-size="pageSize"
v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
<div class="pi-file-select__do">
<div><span v-if="multiple">已选择 <b>{{ value.length }}</b> / <b>{{ max }}</b> 项</span></div>
<el-button type="primary" :disabled="value.length<=0" @click="submit">确 定</el-button>
</div>
</div>
</div>
<save-dialog v-if="dialog.save" ref="saveRef" @success="getMenu" @closed="dialog.save=false"></save-dialog>
<move-dialog v-if="dialog.move" ref="moveRef" @success="moveSuccess"
@closed="dialog.move=false"></move-dialog>
</el-dialog>
</template>
<script setup>
import assetConfig from "@/config/asset"
import saveDialog from "./save.vue";
import moveDialog from "./move.vue";
import * as imageConversion from 'image-conversion';
import {ref, onMounted, nextTick, getCurrentInstance} from "vue"
import tools from "@/utils/tools"
import config from "@/config"
defineExpose({
open
})
const emit = defineEmits(['success', 'closed'])
const props = defineProps({
hideUpload: {type: Boolean, default: false},
multiple: {type: Boolean, default: false},
max: {type: Number, default: assetConfig.max},
maxSize: {type: Number, default: assetConfig.maxSize},
type: {type: String, default: "image"}
})
const {proxy} = getCurrentInstance()
const saveRef = ref(null)
const moveRef = ref(null)
const scrollbarRef = ref(null)
const menuRef = ref(null)
let dialog = ref({
save: false,
move: false
})
let file_name = ref(null)
let pageSize = ref(20)
let total = ref(0)
let currentPage = ref(1)
let data = ref([])
let menu = ref([])
let category_id = ref(0)
let value = ref(props.multiple ? [] : '')
let fileList = ref([])
let accept = ref(assetConfig.fileType(props.type)['accept'])
let listLoading = ref(false)
let menuLoading = ref(false)
let treeProps = ref(assetConfig.menuProps)
let fileProps = ref(assetConfig.fileProps)
let visible = ref(false)
let obj = ref(assetConfig)
onMounted(() => {
getMenu()
getData()
})
async function open() {
visible.value = true;
}
async function getMenu() {
menuLoading.value = true
const res = await assetConfig.menuListObj({type: props.type});
menu.value = tools.makeTreeData(res.data, 0, "category_id")
menu.value.unshift({
'category_id': 0,
'category_name': '未分类',
'parent_id': 0
})
menuLoading.value = false
}
async function getData() {
listLoading.value = true
const reqData = {
[assetConfig.request.menuKey]: category_id.value,
[assetConfig.request.page]: currentPage.value,
[assetConfig.request.pageSize]: pageSize.value,
};
reqData.type = props.type
reqData.ori_name = file_name.value
const res = await assetConfig.fileListObj(reqData);
const parseData = assetConfig.listParseData(res);
data.value = parseData.rows
total.value = parseData.total
listLoading.value = false
}
function groupClick(data) {
category_id.value = data.category_id
currentPage.value = 1
file_name.value = null
getData()
}
function reload() {
getData()
}
function search() {
currentPage.value = 1
getData()
}
function active(item) {
if (props.multiple) {
let choice = false;
value.value.forEach(i => {
if (i[assetConfig.fileProps.id] === item[assetConfig.fileProps.id]) {
choice = true
}
})
return choice
} else {
return value.value[assetConfig.fileProps.id] === item[assetConfig.fileProps.id]
}
}
function select(item) {
if (props.multiple) {
if (value.value.length >= props.max) {
proxy.$message.error("选择文件过多")
return;
}
if (active(item)) {
value.value.splice(value.value.findIndex(f => f[assetConfig.fileProps.id] === item[assetConfig.fileProps.id]), 1)
} else {
value.value.push(item)
}
} else {
if (value.value[assetConfig.fileProps.id] === item[assetConfig.fileProps.id]) {
value.value = ''
} else {
value.value = item
}
}
}
function submit() {
if (props.multiple) {
emit('success', value.value.map(i => i[assetConfig.fileProps.url]));
} else {
emit('success', value.value[assetConfig.fileProps.url]);
}
visible.value = false
}
async function delFile() {
const _value = JSON.parse(JSON.stringify(value.value))
const ids = props.multiple ? _value.map(i => i[assetConfig.fileProps.id]) : [_value[assetConfig.fileProps.id]];
if (!ids) return;
const res = await assetConfig.delFileObj({ids: ids});
// 删除文件成功,取消选择
value.value = props.multiple ? [] : ''
proxy.$message.success(res.msg)
getData()
}
function moveFile() {
dialog.value.move = true
nextTick(() => {
moveRef.value.open(menu.value)
})
}
async function moveSuccess(category_id) {
const _value = JSON.parse(JSON.stringify(value.value))
const ids = props.multiple ? _value.map(i => i[assetConfig.fileProps.id]) : [_value[assetConfig.fileProps.id]];
if (!ids) return;
const res = await assetConfig.moveFileObj({ids: ids, category_id: category_id});
proxy.$message.success(res.msg)
// 清空选中值
value.value = props.multiple ? [] : ''
getData()
}
function uploadChange(file, _fileList) {
file.tempImg = URL.createObjectURL(file.raw);
fileList.value = _fileList
}
function uploadBefore(file) {
const fileSize = file.size / 1024;
if (assetConfig.isImg(file.name) && fileSize > 800) {
return new Promise((resolve) => {
imageConversion.compressAccurately(file, 800).then((res) => {
resolve(res);
});
});
} else {
if (fileSize > props.maxSize * 1024) {
proxy.$message.warning(`上传文件大小不能超过 ${props.maxSize}MB!`);
return false;
}
}
}
function uploadRequest(param) {
// 获取文件类型
const _type = assetConfig.getType(param.file.name)
if (props.type !== "" && proxy.type !== _type) {
proxy.$message.warning(`不允许的文件类型!`);
clearFiles(param.file)
return false
}
const data = new FormData();
data.append("file", param.file);
if (category_id.value) {
data.append(assetConfig.request.menuKey, category_id.value);
}
assetConfig.uploadObj(data, {
onUploadProgress: e => {
param.onProgress(e)
}
}).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
}
function uploadProcess(event, file) {
file.progress = Number((event.loaded / event.total * 100).toFixed(2))
}
function clearFiles(file) {
fileList.value.splice(fileList.value.findIndex(f => f.uid == file.uid), 1)
}
function uploadSuccess(res, file) {
fileList.value.splice(fileList.value.findIndex(f => f.uid == file.uid), 1)
var response = assetConfig.uploadParseData(res);
data.value.unshift(response)
if (!props.multiple) {
value.value = response
}
}
function uploadError(err) {
proxy.$notify.error({
title: '上传文件错误',
message: err
})
}
function editCategory(data) {
dialog.value.save = true
nextTick(() => {
saveRef.value.setType(props.type).setMenu(menu.value).open('edit', data)
})
}
function addCategory() {
dialog.value.save = true
nextTick(() => {
saveRef.value.setType(props.type).setMenu(menu.value).open('add', null, menuRef.value.getCurrentKey())
})
}
async function delCategory() {
var CheckedNodes = menuRef.value.getCheckedNodes()
if (CheckedNodes.length === 0) {
proxy.$message.warning("请选择需要删除的项")
return false;
}
var confirm = await proxy.$confirm('确认删除已选择的菜单吗?', '提示', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger'
}).catch(() => {
})
if (confirm !== 'confirm') {
return false
}
menuLoading.value = true
var res = await assetConfig.menuDelObj({ids: CheckedNodes.map(item => item.category_id)})
menuLoading.value = false
proxy.$message.success(res.msg)
CheckedNodes.forEach(item => {
var node = menuRef.value.getNode(item)
if (node == null) {
reload()
}
menuRef.value.remove(item)
})
}
</script>
<style scoped>
.pi-file-select {
display: flex;
}
.pi-file-select__files {
flex: 1;
}
.pi-file-select__list {
height: 400px;
}
.pi-file-select__item {
display: inline-block;
float: left;
margin: 0 15px 25px 0;
width: 110px;
cursor: pointer;
}
.pi-file-select__item__file {
width: 110px;
height: 110px;
position: relative;
}
.pi-file-select__item__file .el-image {
width: 110px;
height: 110px;
}
.pi-file-select__item__box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 2px solid var(--el-color-success);
z-index: 1;
display: none;
}
.pi-file-select__item__box::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: var(--el-color-success);
opacity: 0.2;
display: none;
}
.pi-file-select__item:hover .pi-file-select__item__box {
display: block;
}
.pi-file-select__item.active .pi-file-select__item__box {
display: block;
}
.pi-file-select__item.active .pi-file-select__item__box::before {
display: block;
}
.pi-file-select__item p {
margin-top: 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
-webkit-text-overflow: ellipsis;
text-align: center;
}
.pi-file-select__item__checkbox {
position: absolute;
width: 20px;
height: 20px;
top: 7px;
right: 7px;
z-index: 2;
background: rgba(0, 0, 0, 0.2);
border: 1px solid #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pi-file-select__item__checkbox i {
font-size: 14px;
color: #fff;
font-weight: bold;
display: none;
}
.pi-file-select__item__select {
position: absolute;
width: 20px;
height: 20px;
top: 0px;
right: 0px;
z-index: 2;
background: var(--el-color-success);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pi-file-select__item__select i {
font-size: 14px;
color: #fff;
font-weight: bold;
}
.pi-file-select__item.active .pi-file-select__item__checkbox {
background: var(--el-color-success);
}
.pi-file-select__item.active .pi-file-select__item__checkbox i {
display: block;
}
.pi-file-select__item.active .pi-file-select__item__select {
display: flex;
}
.pi-file-select__item__file .item-file {
width: 110px;
height: 110px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pi-file-select__item__file .item-file i {
font-size: 40px;
}
.pi-file-select__item__file .item-file.item-file-doc {
color: #409eff;
}
.pi-file-select__item .item-video {
width: 110px;
height: 110px;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pi-file-select__item .item-video video {
width: 110px;
object-fit: cover;
}
.pi-file-select__item__upload {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: rgba(255, 255, 255, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.pi-file-select__side {
width: 200px;
margin-right: 15px;
border-right: 1px solid rgba(128, 128, 128, 0.2);
display: flex;
flex-flow: column;
}
.pi-file-select__side-menu {
flex: 1;
}
.pi-file-select__side-msg {
height: 32px;
line-height: 32px;
}
.pi-file-select__top {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
}
.pi-file-select__upload {
display: inline-block;
}
.pi-file-select__top .tips {
font-size: 12px;
margin-left: 10px;
color: #999;
}
.pi-file-select__top .tips i {
font-size: 14px;
margin-right: 5px;
position: relative;
bottom: -0.125em;
}
.pi-file-select__pagination {
margin: 15px 0;
}
.pi-file-select__do {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-tree-node {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 24px;
height: 100%;
}
.custom-tree-node .label {
display: flex;
align-items: center;;
height: 100%;
}
.custom-tree-node .label .el-tag {
margin-left: 5px;
}
.custom-tree-node .do {
display: none;
}
.custom-tree-node .do i {
margin-left: 5px;
color: #999;
}
.custom-tree-node .do i:hover {
color: #333;
}
.custom-tree-node:hover .do {
display: inline-block;
}
:deep(.el-upload) {
display: block;
margin-right: 12px;
}
</style>