Files
django-vue3-admin/web/src/components/fileSelector/index.vue

315 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-show="props.showInput" style="width: 100%;">
<el-select v-if="props.inputType === 'selector'" v-model="data" suffix-icon="arrow-down" clearable
:multiple="props.multiple" placeholder="请选择文件" @click="selectVisiable = true" @clear="selectedInit"
@remove-tag="selectedInit">
<el-option v-for="item, index in listAllData" :key="index" :value="item[props.valueKey]" :label="item.name" />
</el-select>
<div v-if="props.inputType === 'image'" style="position: relative;"
:style="{ width: props.inputImageSize + 'px', height: props.inputImageSize + 'px' }">
<el-image :src="data" fit="scale-down" :style="{ width: props.inputImageSize + 'px', aspectRatio: '1 / 1' }">
<template #error>
<div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
</template>
</el-image>
<div @click="selectVisiable = true" class="addControllorHover"></div>
<el-icon v-show="!!data" class="closeHover" :size="16" @click="dataClear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'video'"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputImageSize * 2 + 'px', height: props.inputImageSize + 'px' }">
<video :src="data" :controls="false" :autoplay="true" :muted="true" :loop="true"></video>
<div v-show="!(!!data)"
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true" class="addControllorHover"></div>
<el-icon v-show="!!data" class="closeHover" :size="16" @click="dataClear">
<Close />
</el-icon>
</div>
<div v-if="props.inputType === 'audio'"
style="position: relative; display: flex; align-items: center; justify-items: center;"
:style="{ width: props.inputImageSize * 2 + 'px', height: props.inputImageSize + 'px' }">
<audio style="width: 100%;" :src="data" :controls="!!data" :autoplay="false" :muted="true" :loop="true"></audio>
<div v-show="!(!!data)"
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<el-icon :size="24">
<Plus />
</el-icon>
</div>
<div @click="selectVisiable = true" class="addControllorHover"></div>
<el-icon v-show="!!data" class="closeHover" :size="16" @click="dataClear">
<Close />
</el-icon>
</div>
</div>
<el-dialog v-model="selectVisiable" :draggable="false" width="50%" :align-center="false"
@open="if (listData.length === 0) listRequest();" @closed="clear">
<template #header>
<span class="el-dialog__title">文件选择</span>
<el-divider style="margin: 0;" />
</template>
<div style="padding: 4px;">
<el-tabs v-model="tabsActived" :type="props.tabsType" :stretch="true" @tab-change="handleTabChange">
<el-tab-pane v-if="props.tabsShow & SHOW.IMAGE" :name="0" label="图片" />
<el-tab-pane v-if="props.tabsShow & SHOW.VIDEO" :name="1" label="视频" />
<el-tab-pane v-if="props.tabsShow & SHOW.AUDIO" :name="2" label="音频" />
<el-tab-pane v-if="props.tabsShow & SHOW.OTHER" :name="3" label="其他" />
</el-tabs>
<el-row justify="space-between" class="headerBar">
<el-span :span="16">
<el-input v-model="filterForm.name" :placeholder="`请输入${TypeLabel[tabsActived]}名`" prefix-icon="search"
clearable @change="listRequest" />
<div>
<el-tag v-if="props.multiple" type="primary" effect="light">
一共选中&nbsp;{{ data?.length || 0 }}&nbsp;个文件
</el-tag>
</div>
</el-span>
<el-span :span="8">
<el-button type="default" circle icon="refresh" @click="listRequest" />
<!-- 这里 show-file-list clearFiles 一起使用确保不会显示上传列表 -->
<el-upload ref="uploadRef" :action="getBaseURL() + 'api/system/file/'" :multiple="false"
:data="{ upload_method: 1 }" :drag="false" :show-file-list="false" :accept="AcceptList[tabsActived]"
:on-success="() => { listRequest(); listRequestAll(); uploadRef.clearFiles(); }">
<el-button type="primary" icon="plus">上传{{ TypeLabel[tabsActived] }}</el-button>
</el-upload>
</el-span>
</el-row>
<el-empty v-if="!listData.length" description="无内容请上传"
style="width: 100%; height: calc(50vh); margin-top: 24px; padding: 4px;" />
<div ref="listContainerRef" class="listContainer" v-else>
<div v-for="item in listData" :style="{ width: (props.itemSize || 100) + 'px' }" :key="item.id"
@click="onItemClick($event)" :data-id="item[props.valueKey]">
<FileItem :fileData="item" />
</div>
</div>
<div class="listPaginator">
<el-pagination background size="small" layout="total, sizes, prev, pager, next" :total="pageForm.total"
v-model:page-size="pageForm.limit" :page-sizes="[10, 20, 30, 40, 50]" v-model:current-page="pageForm.page"
:hide-on-single-page="false" @change="handlePageChange" />
</div>
</div>
<template #footer v-if="props.showInput">
<el-button type="default" @click="selectVisiable = false">取消</el-button>
<el-button type="primary" @click="selectVisiable = false">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { useUi, UserPageQuery, AddReq, EditReq, DelReq } from '@fast-crud/fast-crud';
import { ref, reactive, defineProps, PropType, watch, onMounted, nextTick } from 'vue';
import { getBaseURL } from '/@/utils/baseUrl';
import { request } from '/@/utils/service';
import { SHOW } from './types';
import FileItem from './fileItem.vue';
const TypeLabel = ['图片', '视频', '音频', '文件']
const AcceptList = ['image/*', 'video/*', 'audio/*', ''];
const props = defineProps({
modelValue: {},
tabsType: { type: Object as PropType<'' | 'card' | 'border-card'>, default: '' },
itemSize: { type: Number, default: 100 },
// 1000图片 100视频 10音频 1 其他 控制tabs的显示
tabsShow: { type: Number, default: SHOW.ALL },
// 是否可以多选,默认单选
// 该值为true时inputType必须是selector暂不支持其他type的多选
multiple: { type: Boolean, default: false },
// 是否可选该参数用于只上传和展示而不选择和绑定model的情况
selectable: { type: Boolean, default: true },
// 该参数用于控制是否显示表单item。若赋值为false则不会显示表单item也不会显示底部按钮
// 如果不显示表单item则无法触发dialog需要父组件通过修改本组件暴露的 selectVisiable 状态来控制dialog
showInput: { type: Boolean, default: true },
// 表单item类型不为selector是需要设置valueKey否则可能获取不到媒体数据
inputType: { type: Object as PropType<'selector' | 'image' | 'video' | 'audio'>, default: 'selector' },
// inputType为image时生效
inputImageSize: { type: Number, default: 100 },
// v-model绑定的值是file数据的哪个key默认是id
valueKey: { type: String, default: 'id' },
} as any);
const selectVisiable = ref<boolean>(false);
const tabsActived = ref<number>([3, 2, 1, 0][((props.tabsShow & (props.tabsShow - 1)) === 0) ? Math.log2(props.tabsShow) : 3]);
const fileApiPrefix = '/api/system/file/';
const fileApi = {
GetList: (query: UserPageQuery) => request({ url: fileApiPrefix, method: 'get', params: query }),
GetAll: () => request({ url: fileApiPrefix + 'get_all/' }),
};
// 过滤表单
const filterForm = reactive({ name: '' });
// 分页表单
const pageForm = reactive({ page: 1, limit: 10, total: 0 });
// 展示的数据列表
const listData = ref<any[]>([]);
const listAllData = ref<any[]>([]);
const listRequest = async () => {
let res = await fileApi.GetList({ page: pageForm.page, limit: pageForm.limit, file_type: tabsActived.value, ...filterForm });
listData.value = res.data;
pageForm.total = res.total;
pageForm.page = res.page;
pageForm.limit = res.limit;
selectedInit();
};
const listRequestAll = async () => {
if (props.inputType !== 'selector') return;
let res = await fileApi.GetAll();
listAllData.value = res.data;
};
// tab改变时触发
const handleTabChange = (name: string) => { pageForm.page = 1; listRequest(); };
// 分页器改变时触发
const handlePageChange = (currentPage: number, pageSize: number) => { pageForm.page = currentPage; pageForm.limit = pageSize; listRequest(); };
// 选择的行为
const listContainerRef = ref<any>();
const onItemClick = async (e: MouseEvent) => {
if (!props.selectable) return;
let target = e.target as HTMLElement;
let flat = 0; // -1删除 0不变 1添加
while (!target.dataset.id) target = target.parentElement as HTMLElement;
let fileId = target.dataset.id;
if (props.multiple) {
if (target.classList.contains('active')) { target.classList.remove('active'); flat = -1; }
else { target.classList.add('active'); flat = 1; }
if (data.value) {
if (flat === 1) data.value.push(fileId);
else data.value.splice(data.value.indexOf(fileId), 1);
} else data.value = [fileId];
// 去重排序,<降序,>升序
data.value = Array.from(new Set(data.value)).sort();
} else {
for (let i of listContainerRef.value?.children) (i as HTMLElement).classList.remove('active');
target.classList.add('active');
data.value = fileId;
}
onDataChange(data.value);
};
// 每次列表刷新都得更新一下选择状态,因为所有标签页共享列表
const selectedInit = async () => {
if (!props.selectable) return;
await nextTick();
for (let i of (listContainerRef.value?.children || [])) {
i.classList.remove('active');
let fid = (i as HTMLElement).dataset.id;
if (props.multiple) { if (data.value?.includes(fid)) i.classList.add('active'); }
else { if (fid === data.value) i.classList.add('active'); }
}
};
const uploadRef = ref<any>();
// 清空状态
const clear = () => {
filterForm.name = '';
pageForm.page = 1;
pageForm.limit = 10;
pageForm.total = 0;
listData.value = [];
// all数据不能清因为all只会在挂载的时候赋值一次
// listAllData.value = [];
};
const dataClear = () => { data.value = null; onDataChange(null); }
// fs-crud部分
const data = ref<any>();
const emit = defineEmits(['update:modelValue']);
watch(() => props.modelValue, (val) => { data.value = val; }, { immediate: true });
const { ui } = useUi();
const formValidator = ui.formItem.injectFormItemContext();
const onDataChange = (value: any) => {
emit('update:modelValue', value);
formValidator.onChange();
formValidator.onBlur();
};
defineExpose({ data, onDataChange, selectVisiable, clear, dataClear });
onMounted(() => {
if (props.multiple && props.inputType !== 'selector')
throw new Error('FileSelector组件属性multiple为true时inputType必须为selector');
listRequestAll();
});
</script>
<style scoped>
.headerBar>* {
display: flex;
justify-content: space-between;
gap: 12px;
}
.listContainer {
display: grid;
justify-items: center;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: min-content;
grid-gap: 36px;
margin-top: 24px;
padding: 8px;
height: calc(50vh);
overflow-y: auto;
scrollbar-width: thin;
}
.listContainer>* {
aspect-ratio: 1 / 1;
/* border: 1px solid rgba(0, 0, 0, .1); */
/* div阴影2px范围均匀投影0偏移 */
box-shadow: 0 0 4px rgba(0, 0, 0, .2);
cursor: pointer;
border-radius: 8px;
overflow: hidden;
}
.active {
box-shadow: 0 0 8px var(--el-color-primary);
}
.listPaginator {
display: flex;
justify-content: flex-end;
justify-items: center;
padding-top: 24px;
}
.addControllorHover {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
border-radius: 8px;
border: 1px solid #dcdfe6;
}
.addControllorHover:hover {
border-color: #c0c4cc;
}
.closeHover {
position: absolute;
right: 2px;
top: 2px;
cursor: pointer;
}
.closeHover:hover {
color: black;
}
</style>