123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- <!--
- 树形结构数据展示
- @Author: mosowe
- @Date:2023-05-11 11:17:58
- -->
- <template>
- <view class="" v-if="treeData.length">
- <view class="mosowe-tree-list-component" v-for="(item, index) in treeData" :key="index">
- <view class="mosowe-tree-title-wrap">
- <view class="mosowe-tree-title-icon">
- <view class="icon" v-if="item[children] && item[children].length" :class="{
- open: showChidren.includes(item[nodeKey]),
- close: !showChidren.includes(item[nodeKey])
- }" @click="toggleChildrens(item[nodeKey])"></view>
- </view>
- <view class="mosowe-tree-title-selection" :class="{
- disabled: item.disabled,
- 'part-select':
- item[children] &&
- treeToArray(JSON.parse(JSON.stringify(item[children])), children).filter((ci) =>
- selectionList.includes(ci[nodeKey])
- ).length > 0 &&
- !selectionList.includes(item[nodeKey]),
- 'all-select': selectionList.includes(item[nodeKey])
- }" v-if="multiple" @click="selectionClick(item)"></view>
- <view class="mosowe-tree-title-name" :class="{'mosowe-tree-title-name-disable':!item.enable}" @click.stop="nodeClick(item)">
- {{ item[text] }}
- </view>
- </view>
- <template v-if="item[children] && item[children].length">
- <template v-if="showChidren.indexOf(item[nodeKey]) > -1">
- <view class="mosowe-tree-children-wrap">
- <!-- <my-mosowe-tree-list v-model="selectionList" :treeData="item[children]" :text="text" :children="children"
- :multiple="multiple" :nodeKey="nodeKey" :defaultShowChildren="defaultShowChildren"
- :textClickToggle="textClickToggle" :accordion="accordion" :index="index + 1" :isOnceShow="onceShow"
- @nodeClick="nodeClick" @childCancel="childCancel(item)" @childChoosed="childChoosed(item)">
- {{ node[text] }}
- </my-mosowe-tree-list> -->
- <view style="text-align: left;line-height: 70rpx;" class="">
- <span style="font-weight: 400;display: inline-block;padding: 0rpx 20rpx;border: 1rpx solid #bdbdbe;margin: 10rpx 20rpx;font-size: 30rpx;border-radius: 10rpx;" v-for="(iitem,iindex) in item[children]" class="">
- {{iitem.text}}
- </span>
- </view>
- </view>
- </template>
- </template>
- </view>
- </view>
- </template>
- <script>
- export default {
- name: 'my-mosowe-tree-list',
- emits: ['nodeClick', 'childCancel', 'childChoosed', 'update:modelValue'],
- props: {
- modelValue: {
- type: Array,
- default: () => []
- },
- value: {
- type: Array,
- default: () => []
- },
- treeData: {
- // 数据
- type: Array,
- default: () => []
- },
- text: {
- // 显示的文案标题字段
- type: String,
- default: 'text'
- },
- nodeKey: {
- // 目标字段
- type: String,
- default: 'value'
- },
- children: {
- // 子集字段
- type: String,
- default: 'childrens'
- },
- multiple: {
- // 显示多选框
- type: Boolean,
- default: false
- },
- textClickToggle: {
- // 标题点击触发子集列表显隐
- type: Boolean,
- default: false
- },
- defaultShowChildren: {
- // 默认展开所有,或展开几级
- type: [Boolean, Number],
- default: false
- },
- accordion: {
- // 手风琴模式
- type: Boolean,
- default: false
- },
- index: {
- // 当前层级
- type: Number,
- default: 1
- },
- isOnceShow: {
- // 是否显示过了
- type: Boolean,
- default: false
- }
- },
- data() {
- return {
- showChidren: [],
- once: false,
- onceShow: false
- };
- },
- computed: {
- selectionList: {
- get() {
- let list = [];
- // TODO 兼容 vue2
- // #ifdef VUE2
- list = this.value;
- // #endif
- // TODO 兼容 vue3
- // #ifdef VUE3
- list = this.modelValue;
- // #endif
- if (!this.once && list.length !== 0) {
- // 只执行一次,初始化时
- const flatArr = this.getValueByKeyForObject({
- value: list,
- source: this.treeData,
- valueKey: this.nodeKey,
- targetKey: this.children,
- treeDataAll: false,
- treeChildKey: this.children
- })
- .flat(Infinity)
- .filter((item) => item)
- .map((item) => {
- return item[this.nodeKey];
- });
- this.once = true;
- const r = [...list, ...flatArr];
- return [...new Set(r)];
- }
- return list;
- },
- set(value) {
- // TODO 兼容 vue2
- // #ifdef VUE2
- this.$emit('input', value);
- // #endif
- // TODO 兼容 vue3
- // #ifdef VUE3
- this.$emit('update:modelValue', value);
- // #endif
- }
- }
- },
- watch: {
- defaultShowChildren: {
- handler() {
- if (!this.isOnceShow) {
- if (this.defaultShowChildren === true) {
- this.showChidren = this.treeToArray(
- JSON.parse(JSON.stringify(this.treeData)),
- this.children
- ).map((item) => item[this.nodeKey]);
- } else if (typeof this.defaultShowChildren === 'number') {
- if (this.index < this.defaultShowChildren) {
- const findIndex = (treeData, index) => {
- let r = [];
- treeData.forEach((item) => {
- r.push(item[this.nodeKey]);
- if (
- index < this.defaultShowChildren &&
- item[this.children] &&
- item[this.children].length
- ) {
- r.push(...findIndex(item[this.children], index + 1));
- }
- });
- return r;
- };
- const list = findIndex(JSON.parse(JSON.stringify(this.treeData)), 0);
- this.showChidren = list;
- }
- }
- let t = setTimeout(() => {
- clearTimeout(t);
- t = null;
- this.onceShow = this.index === 1 ? true : this.isOnceShow;
- }, 10);
- }
- },
- immediate: true
- }
- },
- methods: {
- nodeClick(e) {
- //this.$emit('nodeClick', e);
- console.log(e);
- this.toggleChildrens(e[this.nodeKey]);
- },
- // 选择
- selectionClick(node) {
- const list = [...this.selectionList];
- const value = node[this.nodeKey];
- if (node[this.children] && node[this.children].length) {
- // 此项有子集
- if (list.includes(value)) {
- // 已选->改取消选择
- const index = list.findIndex((item) => item === value);
- list.splice(index, 1);
- // 子集的也要取消掉
- const childrenData = JSON.parse(JSON.stringify(node[this.children]));
- this.treeToArray(childrenData, this.children).forEach((item) => {
- const findex = list.findIndex((fitem) => fitem === item[this.nodeKey]);
- list.splice(findex, 1);
- });
- // 父级也要取消
- this.$emit('childCancel');
- } else {
- // 加入已选
- list.push(value);
- // 子集的也要全部加入
- const childrenData = JSON.parse(JSON.stringify(node[this.children]));
- this.treeToArray(childrenData, this.children).forEach((item) => {
- list.push(item[this.nodeKey]);
- });
- // 父级看情况选择不选择
- this.$emit('childChoosed');
- }
- } else {
- // 没有子集
- if (list.includes(value)) {
- // 已选->改取消选择
- const index = list.findIndex((item) => item === value);
- list.splice(index, 1);
- // 父级也要取消
- this.$emit('childCancel');
- } else {
- // 加入已选
- list.push(value);
- // 父级看情况选择不选择
- this.$emit('childChoosed');
- }
- }
- this.selectionList = [...new Set(list)];
- },
- childChoosed(node) {
- let t = setTimeout(() => {
- clearTimeout(t);
- t = null;
- const list = [...this.selectionList];
- const value = node[this.nodeKey];
- let join = true;
- node[this.children].forEach((item) => {
- if (!list.includes(item[this.nodeKey])) {
- join = false;
- }
- });
- if (join) {
- list.push(value);
- }
- // 父级也要考虑下选择不
- this.selectionList = [...new Set(list)];
- this.$emit('childChoosed');
- }, 10);
- },
- childCancel(node) {
- let t = setTimeout(() => {
- clearTimeout(t);
- t = null;
- const list = [...this.selectionList];
- const value = node[this.nodeKey];
- const index = list.findIndex((item) => item === value);
- if (index > -1) {
- list.splice(index, 1);
- this.selectionList = [...new Set(list)];
- } // 父级也要取消
- this.$emit('childCancel');
- }, 10);
- },
- // 展开收起
- toggleChildrens(key) {
- if (this.showChidren.includes(key)) {
- const index = this.showChidren.findIndex((item) => item === key);
- this.showChidren.splice(index, 1);
- } else {
- if (this.accordion) {
- this.showChidren = [key];
- } else {
- this.showChidren.push(key);
- }
- }
- },
- // 树转数组
- treeToArray(nodes, childKey) {
- var r = [];
- if (Array.isArray(nodes)) {
- for (var i = 0, l = nodes.length; i < l; i++) {
- r.push(nodes[i]); // 取每项数据放入一个新数组
- if (Array.isArray(nodes[i][childKey]) && nodes[i][childKey].length > 0)
- // 若存在leaf则递归调用,把数据拼接到新数组中,并且删除该leaf
- r = r.concat(this.treeToArray(nodes[i][childKey]));
- delete nodes[i][childKey];
- }
- }
- return r;
- },
- getValueByKeyForObject(options) {
- if (!options) {
- return [];
- }
- const {
- value,
- source,
- valueKey,
- targetKey,
- treeDataAll = false,
- treeChildKey = 'leaf'
- } = options;
- let treePrefix = '';
- let result = [];
- if (!(value ?? '') || !source || !valueKey || !targetKey) {
- requiredKey(options, ['value', 'source', 'valueKey', 'targetKey']);
- return [];
- }
- let val = value;
- if (typeof value === 'string') {
- val = [value];
- }
- // 树数据处理
- const treeDeal = (data) => {
- if (data[treeChildKey]) {
- // 是个树
- let r = this.getValueByKeyForObject({
- ...options,
- source: data[treeChildKey]
- });
- if (treeDataAll && r.length) {
- // 需要返回父级数据
- treePrefix = data[targetKey];
- r = r.map((item) => {
- return treePrefix + '/' + item;
- });
- }
- result.push(...r);
- }
- };
- if (Object.prototype.toString.call(source) === '[object Object]') {
- // object类型,假设是树
- if (val.includes(source[valueKey])) {
- result.push(source[targetKey]);
- }
- treeDeal(source);
- } else {
- // 数组,也可能是数组树
- source.forEach((item) => {
- if (Object.prototype.toString.call(item) === '[object Object]') {
- // object类型,假设是树
- if (val.includes(item[valueKey])) {
- result.push(item[targetKey]);
- }
- treeDeal(item); // 树处理
- }
- if (Array.isArray(item)) {
- // 多维数组
- result.push(
- ...this.getValueByKeyForObject({
- ...options,
- source: item
- })
- );
- }
- });
- }
- return result;
- },
- requiredKey(obj, keys) {
- let r = [];
- keys.forEach((item) => {
- if (ZoIsEmpty(obj[item])) {
- console.error(`${item} is required!`);
- r.push(item);
- }
- });
- return r;
- }
- }
- };
- </script>
- <style scoped lang="scss">
- .mosowe-tree-list-component {
- font-size: 40rpx;
- color: #333;
- line-height: 70rpx;
- font-weight: 700;
- .mosowe-tree-title-wrap {
- margin-top: 10rpx;
- border-radius: 10rpx;
- border:1rpx solid #007aff;
- display: flex;
- align-items: center;
- .mosowe-tree-title-icon {
- flex: none;
- width: 20px;
- height: 20px;
- .icon {
- width: 100%;
- height: 100%;
- display: flex;
- position: relative;
- cursor: pointer;
- transition: all 0.3s;
- &::before {
- content: '';
- display: inline-block;
- width: 0;
- height: 0;
- border: 6px solid;
- border-color: transparent transparent transparent #999;
- margin: auto;
- margin-right: -2rpx;
- }
- &.open {
- transform: rotate(90deg);
- transform-origin: center;
- }
- }
- }
- .mosowe-tree-title-selection {
- flex: none;
- width: 30rpx;
- height: 30rpx;
- border: 1rpx solid #ccc;
- border-radius: 2rpx;
- margin-right: 18rpx;
- cursor: pointer;
- position: relative;
- &.part-select {
- background-color: $uni-color-primary;
- border-color: $uni-color-primary;
- &::after {
- content: '';
- display: block;
- width: 8px;
- height: 2px;
- background-color: #fff;
- border-radius: 2px;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translateX(-50%) translateY(-50%);
- }
- }
- &.all-select {
- background-color: $uni-color-primary;
- border-color: $uni-color-primary;
- &::after {
- content: '';
- display: block;
- width: 6px;
- height: 3px;
- border: 2px solid;
- border-color: transparent transparent #fff #fff;
- border-radius: 2px;
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translateX(-50%) translateY(-80%) rotate(-45deg);
- }
- }
- &.disabled {
- background-color: rgba(0, 0, 0, 0.2);
- cursor: no-drop;
- }
- }
- .mosowe-tree-title-name {
- // flex: 1;
- cursor: pointer;
- }
- .mosowe-tree-title-name-disable {
- // flex: 1;
- color: #999;
- cursor: pointer;
- }
- }
- .mosowe-tree-children-wrap {
- padding-left: 10px;
- }
- }
- </style>
|