文件选择器添加表单类型选项,目前支持selector和image类型

This commit is contained in:
阿辉
2024-11-19 12:39:11 +08:00
parent 836b645507
commit 2f4a6e6b1f
2 changed files with 211 additions and 154 deletions

View File

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