实现:
- 默认进入页面时,文件夹全选;
- 文件夹状态,以及文件夹内的文件选择状态,与组件联动
- 文件夹数量,根据后端数据动态生成
实现思路:
将后端数据存到vuex中,增加(多选框状态控制)的参数
文件夹状态,通过监听vuex 中对应文件夹的,文件选择情况处理
选中/取消 单个文件和文件夹时,更新vuex数据
选中/取消 所有文件夹时,更新vuex数据
Checkbox 多选框状态控制
全选: indeterminate: false, checkAll: true半选: indeterminate: true,//半选 checkAll: false,//全选
未选:indeterminate: false, checkAll: false,//全选
vuex
import { createStore } from 'vuex';export default createStore({state: {ExportData: [],//文件夹数据},mutations: {// 存文件夹数据setExportData(state, file) {console.log('存文件夹数据api数据', file)state.ExportData = file;},updateChooseNum(state, item) {// 首个元素的 typeName 是 '全部文件' // const firstFileType = state.ExportData.filter((item) => item.typeName === '全部文件');let fileIndex = state.ExportData.findIndex((df) => df.typeName == item.name);if (fileIndex != -1) {// 使用扩展运算符和 Object.assign 合并对象,并添加新字段 idlist:已选文件的id数组const updatedFileType = {...state.ExportData[fileIndex], idlist: item.idlist || [],chooseNum: item.idlist.length || 0};console.log('12221更新数据', updatedFileType)state.ExportData.splice(fileIndex, 1, updatedFileType);}console.log('11更新导出数据', state.ExportData)if (state.ExportData[0]) {// 累加除首个外的 chooseNum let sum = 0;for (let i = 1; i < state.ExportData.length; i++) {sum += state.ExportData[i].chooseNum;}// 更新首个元素的 chooseNum state.ExportData[0].chooseNum = sum;}console.log('更新导出数据', state.ExportData)},// 全部文件-文件夹全选,(是否顶部全部文件夹,typename,是否清空)setFilderCheck(state, item) {let { all, name, empty } = itemif (all) {if (empty) {state.ExportData.forEach((obj) => {obj.chooseNum = 0obj.idlist = []});state.ExportData[0].chooseNum = 0} else {state.ExportData.forEach((obj) => {obj.chooseNum = obj.fileIdList.lengthobj.idlist = obj.fileIdList});state.ExportData[0].chooseNum = state.ExportData[0].totalNum}} else {let fileIndex = state.ExportData.findIndex((df) => df.typeCode == name);let currnum = state.ExportData[fileIndex].totalNumlet first = state.ExportData[0].chooseNumconsole.log("vuex|c,totle", currnum, first)if (fileIndex != -1) {// 使用扩展运算符和 Object.assign 合并对象,并添加新字段 const updatedFileType = {...state.ExportData[fileIndex], idlist: empty ? [] : state.ExportData[fileIndex].fileIdList,chooseNum: empty ? 0 : state.ExportData[fileIndex].fileIdList.length};state.ExportData.splice(fileIndex, 1, updatedFileType);if (empty) {state.ExportData[0].chooseNum = first - currnum} else {state.ExportData[0].chooseNum = first + currnum}}console.log('xx更新导出数据', state.ExportData)}},},
});
组件 :fileManager.vue
<template><div class="top-box"><a-checkbox v-model:checked="allState.checkAll" :indeterminate="allState.indeterminate"@change="onCheckAllChange">全选</a-checkbox><div class="allnum">已选择{{ allnum }}个文件</div></div><div class="file-folder-card"><a-checkbox-group v-model:value="allState.checkedList" :plainOptions="plainOptions"><div v-if="folderlist.length > 1" class="folder" @mouseover="item.isHovered = true"@mouseleave="item.isHovered = false" v-for="(item, index) in folderlist"><div class="filer-box":class="{ seleback: includ(item.typeCode) || item.indeterminate || item.checkAll }"><a-checkbox @change="onCheckChange($event, item, index)" v-if="item.chooseNum == 0 && item.totalNum == 0":value="item.typeCode" v-model:checked="item.checkAll" :indeterminate="item.indeterminate"class="check"></a-checkbox><a-checkbox @change="onCheckChange($event, item, index)"v-else-if="item.isHovered || item.chooseNum || item.indeterminate || item.checkAll || includ(item.typeCode)"class="check" :value="item.typeCode" v-model:checked="item.checkAll":indeterminate="item.indeterminate"></a-checkbox><div class="tip">({{ item.chooseNum }}/{{ item.totalNum }})</div><img src="@/assets/files2x.png" class="fileimg" @click="openFilled(item, index)" /></div><div class="name">{{ item.typeName }} </div><!-- <div class="name">{{ item.indeterminate + '|' + item.checkAll }} </div> --></div><!-- <div class="name">{{ allState.checkedList }} </div> --></a-checkbox-group></div></template><script lang="ts" setup>
import { FolderOpenFilled } from '@ant-design/icons-vue';
import { reactive, watch, ref, onMounted, onUnmounted, computed } from 'vue';
import { useStore } from 'vuex' // 引入useStore 方法
import { arrayEach } from 'xe-utils';
const store = useStore()
const emits = defineEmits<{(e: "custom-event", value?: any): void;(e: "change", value?: any): void;
}>();
const someState = computed(() => store.state.ExportData);const plainOptions = ref([]);
const allnum = ref(0)//选中总数
// 处理过的文件夹数据
const folderlist = ref([])
const allState = reactive({indeterminate: false,//半选checkAll: false,//全选checkedList: []
});
function openFilled(item, index) {emits("custom-event", { tab: item, index });// this.$emit('custom-event');
}watch(() => someState, (newValue, oldValue) => {// console.log('文件夹监听菜单data', oldValue, 'to', newValue);if (!newValue.value || newValue.value.length == 0) {return}let oldsll=allState.checkedListallState.checkedList = []newValue.value.forEach((item, index, arr) => {// // 处理总文件if (item.typeName == '全部文件') {allnum.value = item.chooseNumlet { indeterminate, checkAll } = setChoose(item.chooseNum, item.totalNum)allState.indeterminate = indeterminateallState.checkAll = checkAllplainOptions.value = item.fileIdListif (checkAll && !indeterminate) {allState.checkedList = item.fileIdList}} else {const first = arr[0]let { indeterminate, checkAll } = setChoose(item.chooseNum, item.totalNum)const updatedFileType = {...folderlist.value[index],...item,indeterminate: indeterminate,checkAll: checkAll,isHovered: false};if (item.typeName == '其他资料') {console.log(updatedFileType)}if ((first.chooseNum > 0 && first.chooseNum < first.totalNum) && item.chooseNum > 0 && item.chooseNum == item.totalNum) {allState.checkedList.push(item.typeCode)}if ((item.chooseNum == 0 && item.totalNum == 0) && (first.chooseNum < first.totalNum) && oldsll.includes(item.typeCode)) {allState.checkedList.push(item.typeCode)}// console.log("|___|",index,folderlist.value,updatedFileType)folderlist.value.splice(--index, 1, updatedFileType);}// console.log("xxx", folderlist.value, allState.checkedList)});
}, {deep: true,immediate: true
})
watch(() => allState.checkedList,val => {allState.indeterminate = val.length > 0 && val.length < plainOptions.value.length;allState.checkAll = val.length === plainOptions.value.length;},
);// 处理选框状态
function setChoose(chooseNum, totalNum) {let indeterminate = falselet checkAll = falseif (chooseNum == 0) {if (totalNum == 0) {indeterminate = falsecheckAll = true} else {indeterminate = falsecheckAll = false}} else if (chooseNum < totalNum && chooseNum > 0) {indeterminate = truecheckAll = false} else {indeterminate = falsecheckAll = true}return { indeterminate, checkAll }
}
const includ = (s) => {if (s) {return allState.checkedList.includes(s)}
}const onCheckAllChange = (e: any) => {//打印checkAllObject.assign(allState, {checkedList: e.target.checked ? plainOptions.value : [],indeterminate: false,});store.commit("setFilderCheck", { all: true, name: '', empty: !e.target.checked });// emits('change',e.target.checked)
};
const onCheckChange = (e, item: any, index) => {if (item.totalNum == 0) {folderlist.value[index].checkAll = !e.target.checkedreturn}//打印sele-checkAllconsole.log("wj", e.target.checked, item)if (e.target) {store.commit("setFilderCheck", { all: false, name: item.typeCode, empty: !e.target.checked });}
};
</script><style lang="less" scoped>
.top-box {display: flex;
}
.allnum {margin-left: 24px;font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 12px;color: #1D2129;line-height: 22px;text-align: left;font-style: normal;text-transform: none;
}/* 添加你的样式 */
.file-folder-card {width: 100%;padding: 24px 12px;display: flex;gap: 40px;/deep/.ant-checkbox-group {display: flex;gap: 40px;flex-wrap: wrap;max-height: calc(100vh - 400px);overflow-y: auto;}/deep/.ant-checkbox-wrapper::after {height: 0;display: none !important;}.folder {// display: flex;.filer-box {width: 88px;height: 88px;// background: #F4F6F8;border-radius: 4px 4px 4px 4px;position: relative;margin-bottom: 4px;.check {position: absolute;top: 2px;left: 4px;}&:hover {background: #F4F6F8;}.tip {// width: 28px;height: 17px;font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 12px;color: #606368;line-height: 18px;text-align: right;font-style: normal;text-transform: none;position: absolute;top: 0px;right: 4px;}}.seleback {background: rgba(22, 93, 255, 0.08) !important;}.fileimg {width: 68px;height: 60px;margin: 14px 10px;}.name {width: 88px;height: 36px;font-family: PingFang SC, PingFang SC;font-weight: 400;font-size: 12px;color: #1D2129;line-height: 18px;font-style: normal;text-transform: none;overflow: hidden;text-overflow: ellipsis;text-align: center;}}}
</style>
组件使用:
<file-manager></file-manager>import fileManager from "../components/fileManager.vue";//从后端获取数据后存入vuextabs.value = res.data;store.commit("setExportData", res.data);tabs.value 格式示例:
[{"typeCode": "allFile","typeName": "全部文件","chooseNum": 0,"totalNum": 4,"index": 1,"icon": "icon-tz-icon_wjj","fileIdList": ["declarationDraft","attestationReport","contractVoucher","otherDocuments"]},{"typeCode": "declarationDraft","typeName": "文件夹1","chooseNum": 0,"totalNum": 2,"index": 2,"icon": "icon-tz-icon_sjdg1","fileIdList": ["208c57896ef3_11","208c5f07896ef3_10"]},{"typeCode": "attestationReport","typeName": "文件夹2","chooseNum": 0,"totalNum": 1,"index": 3,"icon": "icon-tz-icon_jzbg","fileIdList": ["208c5f0303644"]},{"typeCode": "contractVoucher","typeName": "文件夹3","chooseNum": 0,"totalNum": 1,"index": 6,"icon": "icon-tz-icon_xmht","fileIdList": ["6402"]},{"typeCode": "otherDocuments","typeName": "其他资料","chooseNum": 0,"totalNum": 0,"index": 7,"icon": "icon-tz-icon_qtcc","fileIdList": []}
]//各文件夹,默认选中 按需使用const arr = res.data.find(item => {return item.typeCode == 'otherDocuments'})selectedRowKeys.value = arr.idliststore.commit("setFilderCheck", { all: true, name: '', empty: false });