init
This commit is contained in:
92
src/components/Material/file.vue
Normal file
92
src/components/Material/file.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="relative flex items-center justify-center file-item" :style="{ height: fileSize, width: fileSize }">
|
||||
<el-image class="image" v-if="type === 'image'" fit="contain" :src="uri"></el-image>
|
||||
<video class="video" v-else-if="type === 'video'" :src="uri"></video>
|
||||
<div
|
||||
v-if="type == 'video'"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] rounded-full w-5 h-5 flex justify-center items-center bg-[rgba(0,0,0,0.3)]"
|
||||
>
|
||||
<el-icon><CaretRight /></el-icon>
|
||||
</div>
|
||||
|
||||
<div v-if="type === 'file'" class="flex items-center justify-center">
|
||||
<img class="w-16" :src="getFileImage(uri)" />
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import txt from '/@/assets/txt.png';
|
||||
import word from '/@/assets/word.png';
|
||||
import excel from '/@/assets/excel.png';
|
||||
import pdf from '/@/assets/pdf.png';
|
||||
import ppt from '/@/assets/ppt.png';
|
||||
import folder from '/@/assets/icon_folder.png';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// 图片地址
|
||||
uri: {
|
||||
type: String,
|
||||
},
|
||||
// 图片尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
methods: {
|
||||
getFileImage(uri?: string) {
|
||||
if (uri?.includes('txt')) {
|
||||
return txt;
|
||||
}
|
||||
|
||||
if (uri?.includes('xls')) {
|
||||
return excel;
|
||||
}
|
||||
|
||||
if (uri?.includes('doc')) {
|
||||
return word;
|
||||
}
|
||||
|
||||
if (uri?.includes('pdf')) {
|
||||
return pdf;
|
||||
}
|
||||
|
||||
if (uri?.includes('ppt')) {
|
||||
return ppt;
|
||||
}
|
||||
|
||||
return folder;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-item {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@apply bg-br-extra-light border border-br-extra-light;
|
||||
.image,
|
||||
.video {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
src/components/Material/hook.ts
Normal file
200
src/components/Material/hook.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { fileGroupAdd, fileGroupDelete, fileGroupUpdate, fileCateLists, fileDelete, fileList, fileMove, fileRename } from '/@/api/admin/file';
|
||||
import { usePaging } from './usePaging';
|
||||
import { ElMessage, ElTree, type CheckboxValueType } from 'element-plus';
|
||||
import { shallowRef, type Ref } from 'vue';
|
||||
import { useMessageBox } from '/@/hooks/message';
|
||||
|
||||
// 左侧分组的钩子函数
|
||||
export function useCate(type: number) {
|
||||
const treeRef = shallowRef<InstanceType<typeof ElTree>>();
|
||||
// 分组列表
|
||||
const cateLists = ref<any[]>([]);
|
||||
|
||||
// 选中的分组id
|
||||
const cateId = ref<number | string>('');
|
||||
|
||||
// 获取分组列表
|
||||
const getCateLists = async () => {
|
||||
const { data } = await fileCateLists({
|
||||
type,
|
||||
});
|
||||
const item: any[] = [
|
||||
{
|
||||
name: '全部',
|
||||
id: '',
|
||||
},
|
||||
{
|
||||
name: '未分组',
|
||||
id: -1,
|
||||
},
|
||||
];
|
||||
cateLists.value = data;
|
||||
cateLists.value?.unshift(...item);
|
||||
setTimeout(() => {
|
||||
treeRef.value?.setCurrentKey(cateId.value);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// 添加分组
|
||||
const handleAddCate = async (value: string) => {
|
||||
await fileGroupAdd({
|
||||
type,
|
||||
name: value,
|
||||
pid: -1,
|
||||
});
|
||||
getCateLists();
|
||||
};
|
||||
|
||||
// 编辑分组
|
||||
const handleEditCate = async (value: string, id: number) => {
|
||||
await fileGroupUpdate({
|
||||
id,
|
||||
name: value,
|
||||
});
|
||||
getCateLists();
|
||||
};
|
||||
|
||||
// 删除分组
|
||||
const handleDeleteCate = async (id: number) => {
|
||||
try {
|
||||
await useMessageBox().confirm('确定要删除?');
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
await fileGroupDelete({ id });
|
||||
cateId.value = '';
|
||||
getCateLists();
|
||||
};
|
||||
|
||||
//选中分类
|
||||
const handleCatSelect = (item: any) => {
|
||||
cateId.value = item.id;
|
||||
};
|
||||
|
||||
return {
|
||||
treeRef,
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
getCateLists,
|
||||
handleCatSelect,
|
||||
};
|
||||
}
|
||||
|
||||
// 处理文件的钩子函数
|
||||
export function useFile(cateId: Ref<string | number>, type: Ref<number>, limit: Ref<number>, size: number) {
|
||||
const tableRef = shallowRef();
|
||||
const listShowType = ref('normal');
|
||||
const moveId = ref(-1);
|
||||
const select = ref<any[]>([]);
|
||||
const isCheckAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
const fileParams = reactive({
|
||||
original: '',
|
||||
type: type,
|
||||
groupId: cateId,
|
||||
});
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: fileList,
|
||||
params: fileParams,
|
||||
firstLoading: true,
|
||||
size,
|
||||
});
|
||||
|
||||
const getFileList = () => {
|
||||
getLists();
|
||||
};
|
||||
const refresh = () => {
|
||||
resetPage();
|
||||
};
|
||||
|
||||
const isSelect = (id: number) => {
|
||||
return !!select.value.find((item: any) => item.id == id);
|
||||
};
|
||||
|
||||
const batchFileDelete = async (id?: number[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm('确认删除后本地将同步删除,如文件已被使用,请谨慎操作!');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const ids = id ? id : select.value.map((item: any) => item.id);
|
||||
await fileDelete({ ids });
|
||||
getFileList();
|
||||
clearSelect();
|
||||
};
|
||||
|
||||
const batchFileMove = async () => {
|
||||
const ids = select.value.map((item: any) => item.id);
|
||||
await fileMove({ ids, groupId: moveId.value });
|
||||
moveId.value = -1;
|
||||
getFileList();
|
||||
clearSelect();
|
||||
};
|
||||
|
||||
const selectFile = (item: any) => {
|
||||
const index = select.value.findIndex((items: any) => items.id == item.id);
|
||||
if (index != -1) {
|
||||
select.value.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
if (select.value.length == limit.value) {
|
||||
if (limit.value == 1) {
|
||||
select.value = [];
|
||||
select.value.push(item);
|
||||
return;
|
||||
}
|
||||
ElMessage.warning('已达到选择上限');
|
||||
return;
|
||||
}
|
||||
select.value.push(item);
|
||||
};
|
||||
|
||||
const clearSelect = () => {
|
||||
select.value = [];
|
||||
};
|
||||
|
||||
const cancelSelete = (id: number) => {
|
||||
select.value = select.value.filter((item: any) => item.id != id);
|
||||
};
|
||||
|
||||
const selectAll = (value: CheckboxValueType) => {
|
||||
isIndeterminate.value = false;
|
||||
tableRef.value?.toggleAllSelection();
|
||||
if (value) {
|
||||
select.value = [...pager.lists];
|
||||
return;
|
||||
}
|
||||
clearSelect();
|
||||
};
|
||||
|
||||
const handleFileRename = async (value: string, id: number) => {
|
||||
await fileRename({
|
||||
id,
|
||||
original: value,
|
||||
});
|
||||
getFileList();
|
||||
};
|
||||
return {
|
||||
listShowType,
|
||||
tableRef,
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
isCheckAll,
|
||||
isIndeterminate,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
isSelect,
|
||||
clearSelect,
|
||||
cancelSelete,
|
||||
selectAll,
|
||||
handleFileRename,
|
||||
};
|
||||
}
|
||||
18
src/components/Material/i18n/en.ts
Normal file
18
src/components/Material/i18n/en.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
material: {
|
||||
uploadFileTip: 'upload',
|
||||
addGroup: 'add group',
|
||||
editGroup: 'edit group',
|
||||
delGroup: 'del group',
|
||||
moveBtn: 'move',
|
||||
preview: 'preview',
|
||||
edit: 'edit',
|
||||
view: 'view',
|
||||
add: 'add',
|
||||
allCheck: 'all check',
|
||||
rename: 'rename',
|
||||
download: 'download',
|
||||
list: 'list',
|
||||
grid: 'grid',
|
||||
},
|
||||
};
|
||||
18
src/components/Material/i18n/zh-cn.ts
Normal file
18
src/components/Material/i18n/zh-cn.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
material: {
|
||||
uploadFileTip: '上传',
|
||||
addGroup: '新增分组',
|
||||
editGroup: '修改分组',
|
||||
delGroup: '删除分组',
|
||||
moveBtn: '移动',
|
||||
preview: '预览',
|
||||
edit: '修改',
|
||||
view: '查看',
|
||||
add: '添加',
|
||||
allCheck: '全选',
|
||||
rename: '重命名',
|
||||
download: '下载',
|
||||
list: '列表',
|
||||
grid: '平铺',
|
||||
},
|
||||
};
|
||||
511
src/components/Material/index.vue
Normal file
511
src/components/Material/index.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<div class="material">
|
||||
<div class="material__left">
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-scrollbar>
|
||||
<div class="pt-4 material-left__content p-b-4">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
:data="cateLists"
|
||||
empty-text="''"
|
||||
:highlight-current="true"
|
||||
:expand-on-click-node="false"
|
||||
:current-node-key="cateId"
|
||||
@node-click="handleCatSelect"
|
||||
>
|
||||
<template v-slot="{ data }">
|
||||
<div class="flex flex-1 items-center pr-4 min-w-0">
|
||||
<img class="w-[20px] h-[16px] mr-3" src="/@/assets/icon_folder.png"/>
|
||||
<span class="flex-1 mr-2 truncate">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
<el-dropdown v-if="data.id > 0" :hide-on-click="false">
|
||||
<span class="muted m-r-10">···</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<popover-input
|
||||
@confirm="handleEditCate($event, data.id)"
|
||||
size="default"
|
||||
:value="data.name"
|
||||
width="400px"
|
||||
:limit="20"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<div>
|
||||
<el-dropdown-item> {{ $t('material.editGroup') }}</el-dropdown-item>
|
||||
</div>
|
||||
</popover-input>
|
||||
<div @click="handleDeleteCate(data.id)">
|
||||
<el-dropdown-item>{{ $t('material.delGroup') }}</el-dropdown-item>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center p-2 border-t border-br">
|
||||
<popover-input @confirm="handleAddCate" size="default" width="400px" :limit="20" show-limit teleported>
|
||||
<el-button> {{ $t('material.addGroup') }}</el-button>
|
||||
</popover-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col material__center">
|
||||
<div class="flex operate-btn">
|
||||
<div class="flex flex-1">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" v-auth="'sys_file_del'" @click="visibleUpload = true"
|
||||
>{{ $t('material.uploadFileTip') }}
|
||||
</el-button>
|
||||
|
||||
<el-button v-if="mode == 'page'" :disabled="!select.length" @click.stop="batchFileDelete()">
|
||||
{{ $t('common.delBtn') }}
|
||||
</el-button>
|
||||
|
||||
<popup v-if="mode == 'page'" class="ml-3" @confirm="batchFileMove" :disabled="!select.length"
|
||||
:title="$t('material.moveBtn')">
|
||||
<template #trigger>
|
||||
<el-button :disabled="!select.length">{{ $t('material.moveBtn') }}</el-button>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<span class="mr-5">移动文件至</span>
|
||||
<el-select v-model="moveId" placeholder="请选择">
|
||||
<template v-for="item in cateLists" :key="item.id">
|
||||
<el-option v-if="item.id !== ''" :label="item.name" :value="item.id"></el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
</div>
|
||||
</popup>
|
||||
</div>
|
||||
<el-input class="mr-16 ml-80" :placeholder="$t('file.inputfileNameTip')" v-model="fileParams.original"
|
||||
@keyup.enter="refresh">
|
||||
<template #append>
|
||||
<el-button @click="refresh">
|
||||
<template #icon>
|
||||
<el-icon>
|
||||
<Search/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-tooltip :content="$t('material.list')" placement="top">
|
||||
<div
|
||||
class="flex justify-center items-center w-8 h-8 list-icon"
|
||||
:class="{
|
||||
'bg-primary-light-8 text-primary': listShowType === 'table'
|
||||
}"
|
||||
@click="listShowType = 'table'"
|
||||
>
|
||||
<el-icon>
|
||||
<Expand/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('material.grid')" placement="top">
|
||||
<div
|
||||
class="flex justify-center items-center w-8 h-8 list-icon"
|
||||
:class="{
|
||||
'bg-primary-light-8 text-primary': listShowType === 'normal'
|
||||
}"
|
||||
@click="listShowType = 'normal'"
|
||||
>
|
||||
<el-icon>
|
||||
<Menu/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3" v-if="mode == 'page'">
|
||||
<el-checkbox :disabled="!pager.lists.length" v-model="isCheckAll" @change="selectAll"
|
||||
:indeterminate="isIndeterminate">
|
||||
{{ $t('material.allCheck') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 mb-1 min-h-0 material-center__content">
|
||||
<el-scrollbar v-if="pager.lists.length" v-show="listShowType == 'normal'">
|
||||
<ul class="flex flex-wrap mt-4 file-list">
|
||||
<li class="file-item-wrap" v-for="item in pager.lists" :key="item.id" :style="{ width: fileSize }">
|
||||
<del-wrap @close="batchFileDelete([item.id])">
|
||||
<file-item :uri="getFileUri(item)" :file-size="fileSize" :type="type" @click="selectFile(item)">
|
||||
<div class="item-selected" v-if="isSelect(item.id)">
|
||||
<el-icon class="el-input__icon">
|
||||
<Check/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</file-item>
|
||||
</del-wrap>
|
||||
<div class="flex justify-center items-center mt-2">
|
||||
{{ item.original }}
|
||||
</div>
|
||||
<div class="flex justify-center items-center operation-btns">
|
||||
<popover-input
|
||||
@confirm="handleFileRename($event, item.id)"
|
||||
size="default"
|
||||
:value="item.name"
|
||||
width="400px"
|
||||
:limit="50"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<el-button type="primary" link> {{ $t('material.rename') }}</el-button>
|
||||
</popover-input>
|
||||
<el-button type="primary" link @click="handleDownFile(item)"> {{ $t('material.download') }}</el-button>
|
||||
<el-button type="primary" link @click="handlePreview(item)"> {{ $t('material.view') }}</el-button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="mt-4"
|
||||
v-show="listShowType == 'table'"
|
||||
:data="pager.lists"
|
||||
width="100%"
|
||||
height="100%"
|
||||
size="large"
|
||||
@row-click="selectFile"
|
||||
>
|
||||
<el-table-column width="55">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :modelValue="isSelect(row.id)" @change="selectFile(row)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="图片" width="100">
|
||||
<template #default="{ row }">
|
||||
<file-item :uri="getFileUri(row)" file-size="50px" :type="type"></file-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link @click.stop="handlePreview(getFileUri(row))" :underline="false">
|
||||
{{ row.original }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="上传时间" min-width="100"/>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="inline-block">
|
||||
<popover-input
|
||||
@confirm="handleFileRename($event, row.id)"
|
||||
size="default"
|
||||
:value="row.name"
|
||||
width="400px"
|
||||
:limit="50"
|
||||
show-limit
|
||||
teleported
|
||||
>
|
||||
<el-button type="primary" link> 重命名</el-button>
|
||||
</popover-input>
|
||||
</div>
|
||||
<div class="inline-block">
|
||||
<el-button type="primary" link @click.stop="handlePreview(getFileUri(row))"> 查看</el-button>
|
||||
</div>
|
||||
<div class="inline-block">
|
||||
<el-button type="primary" link @click.stop="batchFileDelete([row.id])"> 删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex flex-1 justify-center items-center" v-if="!pager.lists.length">{{
|
||||
$t('el.transfer.noData')
|
||||
}}~
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pagination v-bind="pager" @current-change="currentChangeHandle" layout="total, prev, pager, next, jumper"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="material__right" v-if="mode == 'picker'">
|
||||
<div class="flex flex-wrap justify-between p-2">
|
||||
<div class="flex items-center sm">
|
||||
已选择 {{ select.length }}
|
||||
<span v-if="limit">/{{ limit }}</span>
|
||||
</div>
|
||||
<el-button type="primary" link @click="clearSelect">清空</el-button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<el-scrollbar class="ls-scrollbar">
|
||||
<ul class="flex flex-col select-lists p-t-3">
|
||||
<li class="mb-4" v-for="item in select" :key="item.id">
|
||||
<div class="select-item">
|
||||
<del-wrap @close="cancelSelete(item.id)">
|
||||
<file-item :uri="item.uri" file-size="100px" :type="type"></file-item>
|
||||
</del-wrap>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<preview v-model="showPreview" :url="previewUrl" :type="type" :fileName="fileName"/>
|
||||
</div>
|
||||
|
||||
<el-dialog :title="$t('material.uploadFileTip')" v-model="visibleUpload" :destroy-on-close="true" draggable>
|
||||
<upload-file @change="refresh" v-if="props.type === 'image'" :data="{ groupId: cateId, type: typeValue }"
|
||||
:fileType="['png', 'jpg', 'jpeg']"/>
|
||||
|
||||
<upload-file @change="refresh" v-if="props.type === 'video'" :data="{ groupId: cateId, type: typeValue }"
|
||||
:fileType="['mp4']"/>
|
||||
|
||||
<upload-file
|
||||
@change="refresh"
|
||||
v-if="props.type === 'file'"
|
||||
:data="{ cid: cateId, type: typeValue }"
|
||||
:fileType="['doc', 'xls', 'ppt', 'txt', 'pdf', 'docx', 'xlsx', 'pptx']"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const Popup = defineAsyncComponent(() => import('/@/components/Popup/index.vue'));
|
||||
const PopoverInput = defineAsyncComponent(() => import('/@/components/PopoverInput/index.vue'));
|
||||
import {useCate, useFile} from './hook';
|
||||
import FileItem from './file.vue';
|
||||
import Preview from './preview.vue';
|
||||
import type {Ref} from 'vue';
|
||||
import other from '/@/utils/other';
|
||||
|
||||
const {proxy} = getCurrentInstance();
|
||||
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL
|
||||
const props = defineProps({
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'picker',
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['change']);
|
||||
const {limit} = toRefs(props);
|
||||
const typeValue = computed<number>(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return 10;
|
||||
case 'video':
|
||||
return 20;
|
||||
case 'file':
|
||||
return 30;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
const visible: Ref<boolean> = ref(false);
|
||||
const visibleUpload: Ref<boolean> = ref(false);
|
||||
const previewUrl = ref('');
|
||||
const fileName = ref('');
|
||||
const showPreview = ref(false);
|
||||
const {
|
||||
treeRef,
|
||||
cateId,
|
||||
cateLists,
|
||||
handleAddCate,
|
||||
handleEditCate,
|
||||
handleDeleteCate,
|
||||
getCateLists,
|
||||
handleCatSelect
|
||||
} = useCate(typeValue.value);
|
||||
|
||||
const {
|
||||
tableRef,
|
||||
listShowType,
|
||||
moveId,
|
||||
pager,
|
||||
fileParams,
|
||||
select,
|
||||
isCheckAll,
|
||||
isIndeterminate,
|
||||
getFileList,
|
||||
refresh,
|
||||
batchFileDelete,
|
||||
batchFileMove,
|
||||
selectFile,
|
||||
isSelect,
|
||||
clearSelect,
|
||||
cancelSelete,
|
||||
selectAll,
|
||||
handleFileRename,
|
||||
} = useFile(cateId, typeValue, limit, props.pageSize);
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
const getData = async () => {
|
||||
await getCateLists();
|
||||
treeRef.value?.setCurrentKey(cateId.value);
|
||||
getFileList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 当前页码改变事件处理函数
|
||||
* @param val 新的页码
|
||||
*/
|
||||
const currentChangeHandle = (val: number) => {
|
||||
// 修改state.pagination中的current属性
|
||||
pager.current = val;
|
||||
// 再次发起查询操作
|
||||
getFileList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理预览
|
||||
*
|
||||
* @param {string} item - 资源
|
||||
*/
|
||||
const handlePreview = (item: { fileName: string }) => {
|
||||
previewUrl.value = getFileUri(item);
|
||||
showPreview.value = true;
|
||||
fileName.value = item.fileName;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理下载文件
|
||||
*
|
||||
* @param {any} item - 文件项对象
|
||||
*/
|
||||
const handleDownFile = (item: any) => {
|
||||
other.downBlobFile(`/admin/sys-file/oss/file?fileName=${item.fileName}`, {}, item.original);
|
||||
};
|
||||
|
||||
watch(
|
||||
visible,
|
||||
async (val: boolean) => {
|
||||
if (val) {
|
||||
getData();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
watch(cateId, () => {
|
||||
fileParams.name = '';
|
||||
refresh();
|
||||
});
|
||||
|
||||
watch(
|
||||
select,
|
||||
(val: any[]) => {
|
||||
emit('change', val);
|
||||
if (val.length == pager.lists.length && val.length !== 0) {
|
||||
isIndeterminate.value = false;
|
||||
isCheckAll.value = true;
|
||||
return;
|
||||
}
|
||||
if (val.length > 0) {
|
||||
isIndeterminate.value = true;
|
||||
} else {
|
||||
isCheckAll.value = false;
|
||||
isIndeterminate.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const getFileUri = (item: any) => {
|
||||
return `${proxy.baseURL}/admin/sys-file/oss/file?fileName=${item.fileName}`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
props.mode == 'page' && getData();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
clearSelect,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material {
|
||||
@apply h-full min-h-0 flex flex-1;
|
||||
&__left {
|
||||
@apply border-r border-br flex flex-col w-[200px];
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&__center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 16px 16px 0;
|
||||
|
||||
.list-icon {
|
||||
@apply rounded transition-colors duration-200 cursor-pointer hover:bg-gray-100;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
.file-item-wrap {
|
||||
margin-right: 16px;
|
||||
line-height: 1.3;
|
||||
cursor: pointer;
|
||||
|
||||
.item-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operation-btns {
|
||||
height: 28px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover .operation-btns {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
@apply border-l border-br flex flex-col;
|
||||
width: 130px;
|
||||
|
||||
.select-lists {
|
||||
padding: 10px;
|
||||
|
||||
.select-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
283
src/components/Material/picker.vue
Normal file
283
src/components/Material/picker.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="material-select">
|
||||
<popup ref="popupRef" width="830px" custom-class="body-padding" :title="`选择${tipsText}`" @confirm="handleConfirm" @close="handleClose">
|
||||
<template v-if="!hiddenUpload" #trigger>
|
||||
<div class="clearfix material-select__trigger" @click.stop>
|
||||
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
|
||||
<template v-slot:item="{ element, index }">
|
||||
<div
|
||||
class="material-preview"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1,
|
||||
}"
|
||||
@click="showPopup(index)"
|
||||
>
|
||||
<del-wrap @close="deleteImg(index)">
|
||||
<file-item :uri="element" :file-size="size" :type="type"></file-item>
|
||||
</del-wrap>
|
||||
<div class="text-xs text-center operation-btns">
|
||||
<span>{{ $t('material.edit') }}</span>
|
||||
|
|
||||
<span @click.stop="handlePreview(element)">{{ $t('material.view') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="material-upload"
|
||||
@click="showPopup(-1)"
|
||||
v-show="showUpload"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-one': limit == 1,
|
||||
[uploadClass]: true,
|
||||
}"
|
||||
>
|
||||
<slot name="upload">
|
||||
<div
|
||||
class="upload-btn"
|
||||
:style="{
|
||||
width: size,
|
||||
height: size,
|
||||
}"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>{{ $t('material.add') }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar>
|
||||
<div class="material-wrap">
|
||||
<material ref="materialRef" :type="type" :file-size="fileSize" :limit="meterialLimit" @change="selectChange" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</popup>
|
||||
<preview v-model="showPreview" :url="previewUrl" :type="type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Draggable from 'vuedraggable';
|
||||
import Popup from '/@/components/popup/index.vue';
|
||||
import FileItem from './file.vue';
|
||||
import Material from './index.vue';
|
||||
import Preview from './preview.vue';
|
||||
import { useThrottleFn } from '@vueuse/shared';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popup,
|
||||
Draggable,
|
||||
FileItem,
|
||||
Material,
|
||||
Preview,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: () => [],
|
||||
},
|
||||
// 文件类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
// 选择器尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
// 文件尺寸
|
||||
fileSize: {
|
||||
type: String,
|
||||
default: '100px',
|
||||
},
|
||||
// 选择数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
// 禁用选择
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 隐藏上传框*(目前在富文本中使用到)
|
||||
hiddenUpload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
//选择的url排出域名
|
||||
excludeDomain: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const popupRef = ref<InstanceType<typeof Popup>>();
|
||||
const materialRef = ref<InstanceType<typeof Material>>();
|
||||
const previewUrl = ref('');
|
||||
const showPreview = ref(false);
|
||||
const fileList = ref<any[]>([]);
|
||||
const select = ref<any[]>([]);
|
||||
const isAdd = ref(true);
|
||||
const currentIndex = ref(-1);
|
||||
const { disabled, limit, modelValue } = toRefs(props);
|
||||
const tipsText = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'image':
|
||||
return '图片';
|
||||
case 'video':
|
||||
return '视频';
|
||||
case 'file':
|
||||
return '文件';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const showUpload = computed(() => {
|
||||
return props.limit - fileList.value.length > 0;
|
||||
});
|
||||
const meterialLimit: any = computed(() => {
|
||||
if (!isAdd.value) {
|
||||
return 1;
|
||||
}
|
||||
if (limit.value == -1) return null;
|
||||
return limit.value - fileList.value.length;
|
||||
});
|
||||
const handleConfirm = useThrottleFn(
|
||||
() => {
|
||||
const selectUri = select.value.map((item) => (props.excludeDomain ? item.path : item.uri));
|
||||
if (!isAdd.value) {
|
||||
fileList.value.splice(currentIndex.value, 1, selectUri.shift());
|
||||
} else {
|
||||
fileList.value = [...fileList.value, ...selectUri];
|
||||
}
|
||||
handleChange();
|
||||
},
|
||||
1000,
|
||||
false
|
||||
);
|
||||
const showPopup = (index: number) => {
|
||||
if (disabled.value) return;
|
||||
if (index >= 0) {
|
||||
isAdd.value = false;
|
||||
currentIndex.value = index;
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
}
|
||||
popupRef.value?.open();
|
||||
};
|
||||
|
||||
const selectChange = (val: any[]) => {
|
||||
select.value = val;
|
||||
};
|
||||
const handleChange = () => {
|
||||
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || '';
|
||||
emit('update:modelValue', valueImg);
|
||||
emit('change', valueImg);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const deleteImg = (index: number) => {
|
||||
fileList.value.splice(index, 1);
|
||||
handleChange();
|
||||
};
|
||||
|
||||
const handlePreview = (url: string) => {
|
||||
previewUrl.value = url;
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
nextTick(() => {
|
||||
if (props.hiddenUpload) fileList.value = [];
|
||||
materialRef.value?.clearSelect();
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
(val: any[] | string) => {
|
||||
fileList.value = Array.isArray(val) ? val : val == '' ? [] : [val];
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
provide('limit', props.limit);
|
||||
provide('hiddenUpload', props.hiddenUpload);
|
||||
return {
|
||||
popupRef,
|
||||
materialRef,
|
||||
fileList,
|
||||
tipsText,
|
||||
handleConfirm,
|
||||
meterialLimit,
|
||||
showUpload,
|
||||
showPopup,
|
||||
selectChange,
|
||||
deleteImg,
|
||||
previewUrl,
|
||||
showPreview,
|
||||
handlePreview,
|
||||
handleClose,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.material-select {
|
||||
.material-upload,
|
||||
.material-preview {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&.is-one {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
.operation-btns {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.operation-btns {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
line-height: 2;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
.material-upload {
|
||||
:deep(.upload-btn) {
|
||||
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.material-wrap {
|
||||
min-width: 720px;
|
||||
height: 430px;
|
||||
@apply border-t border-b border-br;
|
||||
}
|
||||
</style>
|
||||
93
src/components/Material/preview.vue
Normal file
93
src/components/Material/preview.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div v-show="modelValue">
|
||||
<div v-if="type == 'image'">
|
||||
<el-image-viewer v-if="previewLists.length" :url-list="previewLists" hide-on-click-modal @close="handleClose"/>
|
||||
</div>
|
||||
<div v-if="type == 'video'">
|
||||
<el-dialog v-model="visible" width="740px" :title="$t('material.preview')" :before-close="handleClose">
|
||||
<video-player ref="playerRef" :src="url" width="100%" height="450px"/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<div v-if="type == 'file'">
|
||||
<el-drawer v-model="visible" size="100%">
|
||||
<iframe
|
||||
:src="src"
|
||||
width="100%" height="100%" frameborder="0" class="h-screen" v-if="src"></iframe>
|
||||
<span v-else>未配置预览服务器,请参考文档配置</span>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {Base64} from 'js-base64';
|
||||
import {validateNull} from "/@/utils/validate";
|
||||
|
||||
const VideoPlayer = defineAsyncComponent(() => import('/@/components/VideoPlayer/index.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const playerRef = shallowRef();
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
emit('update:modelValue', value);
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:modelValue', false);
|
||||
};
|
||||
|
||||
const previewLists = ref<any[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (value) {
|
||||
nextTick(() => {
|
||||
previewLists.value = [props.url];
|
||||
playerRef.value?.play();
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
previewLists.value = [];
|
||||
playerRef.value?.pause();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const kkServerURL = import.meta.env.VITE_KK_SERVER_URL
|
||||
const localURL = import.meta.env.VITE_KK_LOCAL_URL
|
||||
const src = computed(() => {
|
||||
if (validateNull(kkServerURL)) {
|
||||
return undefined;
|
||||
}
|
||||
return `${kkServerURL}?url=` + encodeURIComponent(Base64.encode(`${localURL}${props.url}&fullfilename=${props.fileName}`));
|
||||
});
|
||||
</script>
|
||||
76
src/components/Material/usePaging.ts
Normal file
76
src/components/Material/usePaging.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { isFunction } from 'lodash';
|
||||
import { reactive, toRaw } from 'vue';
|
||||
|
||||
// 分页钩子函数
|
||||
interface Options {
|
||||
page?: number;
|
||||
size?: number;
|
||||
fetchFun: (_arg: any) => Promise<any>;
|
||||
params?: Record<any, any>;
|
||||
firstLoading?: boolean;
|
||||
beforeRequest?(params: Record<any, any>): Record<any, any>;
|
||||
afterRequest?(res: Record<any, any>): void;
|
||||
}
|
||||
|
||||
export function usePaging(options: Options) {
|
||||
const { page = 1, size = 15, fetchFun, params = {}, firstLoading = false, beforeRequest, afterRequest } = options;
|
||||
// 记录分页初始参数
|
||||
const paramsInit: Record<any, any> = Object.assign({}, toRaw(params));
|
||||
// 分页数据
|
||||
const pager = reactive({
|
||||
current: page,
|
||||
size,
|
||||
loading: firstLoading,
|
||||
count: 0,
|
||||
total: 0,
|
||||
lists: [] as any[],
|
||||
extend: {} as Record<any, any>,
|
||||
});
|
||||
// 请求分页接口
|
||||
const getLists = () => {
|
||||
pager.loading = true;
|
||||
let requestParams = params;
|
||||
if (isFunction(beforeRequest)) {
|
||||
requestParams = beforeRequest(params);
|
||||
}
|
||||
return fetchFun({
|
||||
current: pager.current,
|
||||
size: pager.size,
|
||||
...requestParams,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
pager.count = data?.total;
|
||||
pager.total = data?.total;
|
||||
pager.lists = data?.records;
|
||||
pager.extend = data?.extend;
|
||||
if (isFunction(afterRequest)) {
|
||||
afterRequest(data);
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
return Promise.reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
pager.loading = false;
|
||||
});
|
||||
};
|
||||
// 重置为第一页
|
||||
const resetPage = () => {
|
||||
pager.current = 1;
|
||||
getLists();
|
||||
};
|
||||
// 重置参数
|
||||
const resetParams = () => {
|
||||
Object.keys(paramsInit).forEach((item) => {
|
||||
params[item] = paramsInit[item];
|
||||
});
|
||||
getLists();
|
||||
};
|
||||
return {
|
||||
pager,
|
||||
getLists,
|
||||
resetParams,
|
||||
resetPage,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user