vue3+antdv仿百度网盘样式文件夹管理组件

实现:

  1. 默认进入页面时,文件夹全选;
  2. 文件夹状态,以及文件夹内的文件选择状态,与组件联动
  3. 文件夹数量,根据后端数据动态生成

实现思路:

将后端数据存到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 } = item
      if (all) {
        if (empty) {
          state.ExportData.forEach((obj) => {
            obj.chooseNum = 0
            obj.idlist = []
          });
          state.ExportData[0].chooseNum = 0
        } else {
          state.ExportData.forEach((obj) => {
            obj.chooseNum = obj.fileIdList.length
            obj.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].totalNum
        let first = state.ExportData[0].chooseNum
        console.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.checkedList
    allState.checkedList = []
    newValue.value.forEach((item, index, arr) => {
        // // 处理总文件
        if (item.typeName == '全部文件') {
            allnum.value = item.chooseNum
            let { indeterminate, checkAll } = setChoose(item.chooseNum, item.totalNum)
            allState.indeterminate = indeterminate
            allState.checkAll = checkAll
            plainOptions.value = item.fileIdList

            if (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 = false
    let checkAll = false
    if (chooseNum == 0) {
        if (totalNum == 0) {
            indeterminate = false
            checkAll = true
        } else {
            indeterminate = false
            checkAll = false
        }
    } else if (chooseNum < totalNum && chooseNum > 0) {
        indeterminate = true
        checkAll = false
    } else {
        indeterminate = false
        checkAll = true
    }

    return { indeterminate, checkAll }
}
const includ = (s) => {
    if (s) {
        return allState.checkedList.includes(s)
    }
}

const onCheckAllChange = (e: any) => {
    //打印checkAll
    Object.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.checked
        return
    }
    //打印sele-checkAll
    console.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";

//从后端获取数据后存入vuex

    tabs.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.idlist
    store.commit("setFilderCheck", { all: true, name: '', empty: false });

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
你可以使用以下步骤来实现Vue3+AntdV+JavaScript的身份证正反面上传功能: 1. 首先,确保你已经安装了Vue3和AntdV,并且已经创建了一个Vue项目。 2. 在你的Vue组件中,引入AntdV的Upload组件和Button组件: ```javascript <template> <div> <a-upload :before-upload="beforeUpload" :show-upload-list="false"> <a-button>选择图片</a-button> </a-upload> </div> </template> <script> import { ref } from 'vue'; export default { setup() { // 创建一个ref来保存上传的图片 const image = ref(''); // 在上传之前的钩子函数中处理文件 const beforeUpload = (file) => { // 使用FileReader读取文件内容 const reader = new FileReader(); reader.onload = (e) => { // 将读取的文件内容赋值给image image.value = e.target.result; }; // 读取文件内容 reader.readAsDataURL(file); // 返回false,阻止默认的上传行为 return false; }; return { beforeUpload, image, }; }, }; </script> ``` 3. 在上面的代码中,我们使用了Vue的`ref`函数来创建一个响应式的数据`image`,用来保存上传的图片。 4. 在`beforeUpload`钩子函数中,我们使用`FileReader`来读取上传的文件内容,并将其赋值给`image`。最后,我们返回`false`来阻止默认的上传行为。 5. 在模板中,我们使用AntdV的`Upload`组件来实现文件上传,并使用`before-upload`属性来绑定`beforeUpload`钩子函数。 6. 最后,你可以根据需要在组件中使用`image`来显示上传的图片。 这样,你就可以实现Vue3+AntdV+JavaScript的身份证正反面上传功能了。记得根据你的实际需求做相应的调整和样式美化。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值