|
@@ -0,0 +1,438 @@
|
|
|
+<template>
|
|
|
+ <van-field
|
|
|
+ v-model="data.names"
|
|
|
+ is-link
|
|
|
+ readonly
|
|
|
+ v-bind="$attrs"
|
|
|
+ @click="sendWordOpen"
|
|
|
+ />
|
|
|
+ <van-popup
|
|
|
+ v-model:show="sendWordShow"
|
|
|
+ round
|
|
|
+ position="bottom"
|
|
|
+ class="pop-model"
|
|
|
+ >
|
|
|
+ <div class="tree-box">
|
|
|
+ <van-search
|
|
|
+ label-align="left"
|
|
|
+ v-model="data.treeParam"
|
|
|
+ show-action
|
|
|
+ placeholder="请输入接收人"
|
|
|
+ @search="searchTreeByParam"
|
|
|
+ >
|
|
|
+ <template #action>
|
|
|
+ <div @click="searchTreeByParam">搜索</div>
|
|
|
+ </template>
|
|
|
+ </van-search>
|
|
|
+ <div class="tree-container">
|
|
|
+ <div class="tree-btns" v-if="props.selectAll">
|
|
|
+ <van-checkbox
|
|
|
+ style="margin-right: 24px"
|
|
|
+ v-if="props.multiple"
|
|
|
+ shape="square"
|
|
|
+ @click.stop="allSelectChange"
|
|
|
+ v-model="data.allSelect"
|
|
|
+ >全选</van-checkbox
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <div class="tree-data">
|
|
|
+ <TreeSelect
|
|
|
+ ref="treeSelectRef"
|
|
|
+ @change="checkChange"
|
|
|
+ :labelKey="props.labelKey"
|
|
|
+ :idKey="props.idKey"
|
|
|
+ :pidKey="props.pidKey"
|
|
|
+ :isLink="props.isLink"
|
|
|
+ :list="data.list"
|
|
|
+ :listObj="data.listObj"
|
|
|
+ :treeParamAlready="data.treeParamAlready"
|
|
|
+ :multiple="props.multiple"
|
|
|
+ @confirm="onConfirm"
|
|
|
+ :defaultId="props.modelValue[0]"
|
|
|
+ ></TreeSelect>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="tree-confirm">
|
|
|
+ <van-button
|
|
|
+ v-if="multiple"
|
|
|
+ type="primary"
|
|
|
+ block
|
|
|
+ @click="handleConfirm"
|
|
|
+ class="submit"
|
|
|
+ >确定</van-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+/**
|
|
|
+ * @description 用户选择组件
|
|
|
+ * @author chinyan
|
|
|
+ */
|
|
|
+import { reactive, watch, ref, nextTick, onMounted } from "vue";
|
|
|
+import TreeSelect from "./tree.vue";
|
|
|
+import request from "@/utils/request";
|
|
|
+
|
|
|
+const emits = defineEmits(["update:modelValue", "change", "confirm"]);
|
|
|
+const props = defineProps({
|
|
|
+ // 绑定值
|
|
|
+ modelValue: {
|
|
|
+ type: Array,
|
|
|
+ default: [],
|
|
|
+ },
|
|
|
+ // 传入数据,数组或树形数组
|
|
|
+ listData: {
|
|
|
+ type: Array,
|
|
|
+ default() {
|
|
|
+ return [];
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // label key
|
|
|
+ labelKey: {
|
|
|
+ type: String,
|
|
|
+ default() {
|
|
|
+ return "name";
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // id key
|
|
|
+ idKey: {
|
|
|
+ type: String,
|
|
|
+ default() {
|
|
|
+ return "id";
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // pid key
|
|
|
+ pidKey: {
|
|
|
+ type: String,
|
|
|
+ default() {
|
|
|
+ return "pid";
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 是否联动勾选
|
|
|
+ isLink: {
|
|
|
+ type: Boolean,
|
|
|
+ default() {
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 是否多选
|
|
|
+ multiple: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ // 是否支持全选
|
|
|
+ selectAll: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const data = reactive({
|
|
|
+ treeParam: "",
|
|
|
+ treeParamAlready: "", // 已经进行了搜索的参数
|
|
|
+ list: props.listData, // 树数组
|
|
|
+ listObj: {}, // 数组对象
|
|
|
+ selectList: [], // 选中的数据
|
|
|
+ searchSomeDataList: [], // 搜索相同对象数组
|
|
|
+ canCheckList: [], // 能够选择的数据集合
|
|
|
+ canCheckListFixed: [], // 固定的能够选择的数据集合
|
|
|
+ allSelect: false, // 是否全选
|
|
|
+ names: "",
|
|
|
+});
|
|
|
+
|
|
|
+const treeSelectRef = ref(null);
|
|
|
+
|
|
|
+const init = (type) => {
|
|
|
+ if (type) {
|
|
|
+ data.names = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ data.treeParam = "";
|
|
|
+ data.treeParamAlready = "";
|
|
|
+ data.canCheckList = [];
|
|
|
+ data.canCheckListFixed = [];
|
|
|
+};
|
|
|
+const initData = (options) => {
|
|
|
+ if (options && options.length) {
|
|
|
+ data.list = options;
|
|
|
+ init();
|
|
|
+ data.listObj = setListObj(options);
|
|
|
+ }
|
|
|
+};
|
|
|
+// 将树形数据转为扁平对象
|
|
|
+const setListObj = (list, pid) => {
|
|
|
+ let listObj = {};
|
|
|
+ list.forEach((itm) => {
|
|
|
+ if (pid) {
|
|
|
+ itm[props.pidKey] = pid;
|
|
|
+ }
|
|
|
+ data.canCheckList.push(itm);
|
|
|
+ data.canCheckListFixed.push(itm);
|
|
|
+ listObj[itm[props.idKey]] = itm;
|
|
|
+ if (itm.children && itm.children.length) {
|
|
|
+ listObj = {
|
|
|
+ ...listObj,
|
|
|
+ ...setListObj(itm.children, itm[props.idKey]),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return listObj;
|
|
|
+};
|
|
|
+
|
|
|
+// 确认
|
|
|
+const handleConfirm = () => {
|
|
|
+ const showSelectList = filterData(data.selectList);
|
|
|
+ // data.names = showSelectList.map((itm) => itm[props.labelKey]).join(',')
|
|
|
+ emits(
|
|
|
+ "update:modelValue",
|
|
|
+ showSelectList.map((itm) => itm[props.idKey])
|
|
|
+ );
|
|
|
+ emits("confirm", showSelectList);
|
|
|
+ sendWordShow.value = false;
|
|
|
+};
|
|
|
+const onConfirm = (e) => {
|
|
|
+ // data.names = e.map((itm) => itm[props.labelKey]).join(',')
|
|
|
+ emits(
|
|
|
+ "update:modelValue",
|
|
|
+ e.map((itm) => itm[props.idKey])
|
|
|
+ );
|
|
|
+ emits("confirm", e);
|
|
|
+ sendWordShow.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 过滤数据
|
|
|
+const filterData = (selectList) => {
|
|
|
+ if (
|
|
|
+ data.canCheckList.length ===
|
|
|
+ selectList.filter(
|
|
|
+ (itm) => itm[props.labelKey].indexOf(data.treeParamAlready) !== -1
|
|
|
+ ).length
|
|
|
+ ) {
|
|
|
+ data.allSelect = true;
|
|
|
+ } else {
|
|
|
+ data.allSelect = false;
|
|
|
+ }
|
|
|
+ // 过滤出展示中,且打勾的数据
|
|
|
+ const showSelectList = selectList.filter((itm) => {
|
|
|
+ return !itm.isHide && !itm.isShowChildren;
|
|
|
+ });
|
|
|
+ return showSelectList;
|
|
|
+};
|
|
|
+// 该方法在 树形数据变化 和 全选变化 时会执行
|
|
|
+const checkChange = (selectList) => {
|
|
|
+ data.selectList = selectList;
|
|
|
+ const showSelectList = filterData(selectList);
|
|
|
+ emits("change", showSelectList);
|
|
|
+};
|
|
|
+// 根据参数搜索
|
|
|
+const searchTreeByParam = () => {
|
|
|
+ const someDataList = []; // 搜索数据
|
|
|
+ const someDataCanCheckList = []; // 搜索且能够check的数据
|
|
|
+ data.treeParamAlready = data.treeParam;
|
|
|
+ treeSelectRef.value?.outDataBuffer();
|
|
|
+ for (const id in data.listObj) {
|
|
|
+ if (data.treeParam) {
|
|
|
+ data.listObj[id].isHide = true;
|
|
|
+ data.listObj[id].isShowChildren = false;
|
|
|
+ if (
|
|
|
+ data.listObj[id][props.labelKey].indexOf(data.treeParam) !== -1 ||
|
|
|
+ data.listObj[id].checked
|
|
|
+ ) {
|
|
|
+ data.listObj[id].isHide = false;
|
|
|
+ someDataList.push(data.listObj[id]);
|
|
|
+ someDataCanCheckList.push(data.listObj[id]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ data.listObj[id].isHide = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ data.searchSomeDataList = someDataList;
|
|
|
+ data.canCheckList = someDataCanCheckList.length
|
|
|
+ ? someDataCanCheckList
|
|
|
+ : deepClone(data.canCheckListFixed);
|
|
|
+ setShowData(someDataList);
|
|
|
+ checkChange(data.selectList);
|
|
|
+};
|
|
|
+// 设置展示和展开数据
|
|
|
+const setShowData = (datas, bool) => {
|
|
|
+ const d = [];
|
|
|
+ datas.forEach((itm) => {
|
|
|
+ if (itm[props.pidKey]) {
|
|
|
+ if (
|
|
|
+ !d.find(
|
|
|
+ (item) =>
|
|
|
+ item[props.idKey] === data.listObj[itm[props.pidKey]][props.idKey]
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ d.push(data.listObj[itm[props.pidKey]]);
|
|
|
+ }
|
|
|
+ if (bool === false || bool === true) {
|
|
|
+ data.listObj[itm[props.pidKey]].checked = bool;
|
|
|
+ }
|
|
|
+ data.listObj[itm[props.pidKey]].isHide = false;
|
|
|
+ data.listObj[itm[props.pidKey]].isShowChildren = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (d.length) {
|
|
|
+ setShowData(d, bool);
|
|
|
+ }
|
|
|
+};
|
|
|
+// 获取全部可选择数据,进行全选/取消
|
|
|
+const toggleAllSelectData = (bool) => {
|
|
|
+ let selectData = [];
|
|
|
+ for (const id in data.listObj) {
|
|
|
+ data.listObj[id].isShowChildren = false;
|
|
|
+ if (data.listObj[id][props.labelKey].indexOf(data.treeParam) !== -1) {
|
|
|
+ data.listObj[id].checked = bool;
|
|
|
+ data.listObj[id].isHide = false;
|
|
|
+ selectData.push(data.listObj[id]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setShowData(selectData, bool);
|
|
|
+};
|
|
|
+// 全选状态改变
|
|
|
+const allSelectChange = () => {
|
|
|
+ toggleAllSelectData(data.allSelect);
|
|
|
+};
|
|
|
+const deepClone = (obj) => {
|
|
|
+ const type = Object.prototype.toString.call(obj); // 通过原型对象获取对象类型
|
|
|
+ let newObj;
|
|
|
+ if (type === "[object Array]") {
|
|
|
+ // 数组
|
|
|
+ newObj = [];
|
|
|
+ if (obj.length > 0) {
|
|
|
+ for (let i = 0; i < obj.length; i++) {
|
|
|
+ newObj.push(deepClone(obj[i]));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (type === "[object Object]") {
|
|
|
+ // 对象
|
|
|
+ newObj = {};
|
|
|
+ for (const i in obj) {
|
|
|
+ newObj[i] = deepClone(obj[i]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 基本类型和方法可以直接赋值
|
|
|
+ newObj = obj;
|
|
|
+ }
|
|
|
+ return newObj;
|
|
|
+};
|
|
|
+
|
|
|
+// 设置默认值
|
|
|
+const setDefault = () => {
|
|
|
+ const someDataList = []; // 默认数据
|
|
|
+ const someDataCanCheckList = []; // 搜索且能够check的数据
|
|
|
+ treeSelectRef.value?.outDataBuffer();
|
|
|
+ for (const id in data.listObj) {
|
|
|
+ data.listObj[id].checked = false;
|
|
|
+ data.listObj[id].isShowChildren = false;
|
|
|
+ props.modelValue.forEach((mid) => {
|
|
|
+ if (data.listObj[id][props.idKey] === mid) {
|
|
|
+ data.listObj[id].checked = true;
|
|
|
+ someDataList.push(data.listObj[id]);
|
|
|
+ someDataCanCheckList.push(data.listObj[id]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ data.names = someDataList.map((item) => item.name).join(",");
|
|
|
+
|
|
|
+ data.searchSomeDataList = someDataList;
|
|
|
+ data.canCheckList = someDataCanCheckList.length
|
|
|
+ ? someDataCanCheckList
|
|
|
+ : deepClone(data.canCheckListFixed);
|
|
|
+ setShowData(someDataList);
|
|
|
+ // filterData();
|
|
|
+ // checkChange(data.selectList);
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.listData,
|
|
|
+ () => {
|
|
|
+ initData(props.listData);
|
|
|
+ },
|
|
|
+ { deep: true, immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.modelValue,
|
|
|
+ () => {
|
|
|
+ setDefault();
|
|
|
+ },
|
|
|
+ { deep: true, immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ setDefaultTime();
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+const setDefaultTime = () => {
|
|
|
+ if (props.listData.length === 0) {
|
|
|
+ setTimeout(() => {
|
|
|
+ setDefaultTime();
|
|
|
+ }, 100);
|
|
|
+ } else {
|
|
|
+ setDefault();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const sendWordShow = ref(false);
|
|
|
+const sendWordOpen = () => {
|
|
|
+ sendWordShow.value = true;
|
|
|
+ if (props.multiple) {
|
|
|
+ setDefault();
|
|
|
+ }
|
|
|
+ // searchTreeByParam();
|
|
|
+};
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ init,
|
|
|
+ setListObj,
|
|
|
+ checkChange,
|
|
|
+ searchTreeByParam,
|
|
|
+ setShowData,
|
|
|
+ toggleAllSelectData,
|
|
|
+ allSelectChange,
|
|
|
+ deepClone,
|
|
|
+ setDefault,
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.pop-model {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.tree-box {
|
|
|
+ --van-search-content-background-color: #eeeeee;
|
|
|
+ --van-search-content-background: #eeeeee;
|
|
|
+ .tree-container {
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+ .tree-data {
|
|
|
+ height: 60vh;
|
|
|
+ padding: 0 12px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+ .tree-btns {
|
|
|
+ padding-left: 12px;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.tree-confirm {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ .submit {
|
|
|
+ width: 90%;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|