hookehuyr

fix 树型选择器操作逻辑完善,和生成和校验逻辑调整

1 +<!--
2 + * @Date: 2022-08-29 14:31:20
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2024-06-03 13:15:47
5 + * @FilePath: /data-table/src/components/TreeField/MyComponent.vue
6 + * @Description: 树形组件
7 +-->
8 +<template>
9 + <div class="tree-field-page">
10 + <div class="select-tree-box" @click="openTree">
11 + <div class="select-tree-item" v-for="(dept) in emitCheckedGroup.dept" :key="dept.id">
12 + {{ dept.name }}
13 + </div>
14 + <div class="select-tree-item" v-for="(role) in emitCheckedGroup.role" :key="role.id">
15 + {{ role.name }}
16 + </div>
17 + <div class="select-tree-item" v-for="(user) in emitCheckedGroup.user" :key="user.id">
18 + {{ user.name }}
19 + </div>
20 + </div>
21 +
22 + <van-popup
23 + v-model:show="showPopover"
24 + position="bottom"
25 + :close-on-click-overlay="false"
26 + :style="{ height: '90vh' }"
27 + >
28 + <div v-if="!is_search" class="search-box" @click="onSearchFocus">
29 + <van-icon name="search" size="1.1rem" />&nbsp;点击搜索
30 + </div>
31 + <van-field
32 + v-else
33 + ref="searchInputRef"
34 + v-model="searchValue"
35 + placeholder="可通过名称,手机号或邮箱查询"
36 + :border="false"
37 + @blur="onSearchBlur"
38 + @focus="onSearchFocus"
39 + >
40 + <template #button>
41 + <van-button size="small" type="primary" @click="onCloseSearch">关闭</van-button>
42 + </template>
43 + </van-field>
44 +
45 + <div class="select-box">
46 + <div class="select-item" v-for="(dept) in checkedGroup.dept" :key="dept.id">
47 + {{ dept.name }}&nbsp;<van-icon @click="onRemoveDeptTag(dept)" name="close" />
48 + </div>
49 + <div class="select-item" v-for="(role) in checkedGroup.role" :key="role.id">
50 + {{ role.name }}&nbsp;<van-icon @click="onRemoveRoleTag(role)" name="close" />
51 + </div>
52 + <div class="select-item" v-for="(user) in checkedGroup.user" :key="user.id">
53 + {{ user.name }}&nbsp;<van-icon @click="onRemoveUserTag(user)" name="close" />
54 + </div>
55 + </div>
56 +
57 + <div v-show="!is_search" class="tab-tree-container">
58 + <van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" style="margin-bottom: 1rem;">
59 + <van-tab title="组织结构" :name="0"></van-tab>
60 + <van-tab title="角色" :name="1"></van-tab>
61 + <van-tab title="成员" :name="2"></van-tab>
62 + </van-tabs>
63 +
64 + <div v-show="tabActive === 0" style="padding: 0 0 1rem 1rem;">
65 + <Vtree
66 + ref="deptTreeRef"
67 + v-model="select_dept_value"
68 + checkable
69 + titleField="name"
70 + keyField="id"
71 + :expandOnFilter="false"
72 + :showCheckedButton="false"
73 + :showFooter="false"
74 + :cascade="false"
75 + :defaultExpandAll="false"
76 + @checked-change="deptTreeCheckedChange"
77 + style=" height: 55vh; overflow: scroll;"
78 + >
79 + <span slot="empty">暂无数据</span>
80 + </Vtree>
81 + </div>
82 +
83 + <div v-if="tabActive === 1" style="padding: 0 0 1rem 1rem;">
84 + <van-checkbox-group
85 + v-model="role_checked"
86 + @change="roleChangeMethod"
87 + >
88 + <van-checkbox
89 + v-for="(role, index) in roleList"
90 + :key="index"
91 + :name="role.id"
92 + shape="square"
93 + icon-size="13px"
94 + :checked-color="styleColor.baseColor"
95 + style="margin-bottom: 0.5rem;"
96 + >{{ role.name }}</van-checkbox>
97 + </van-checkbox-group>
98 + </div>
99 +
100 + <div v-if="tabActive === 2" style="padding: 0 0 0 1rem;">
101 + <van-row gutter="">
102 + <van-col span="10" style="border-right: 1px solid #eee; height: 55vh; overflow: scroll;">
103 + <Vtree
104 + ref="userTreeRef"
105 + v-model="select_user_value"
106 + selectable
107 + titleField="name"
108 + keyField="id"
109 + :expandOnFilter="false"
110 + :showCheckedButton="false"
111 + @update:modelValue="() => {}"
112 + @click="onUserTreeClick"
113 + >
114 + <span slot="empty">暂无数据</span>
115 + </Vtree>
116 + </van-col>
117 + <van-col span="14">
118 + <van-checkbox-group
119 + v-model="user_checked"
120 + style="padding: 0 0 1rem 1rem;"
121 + @change="onUserChange"
122 + >
123 + <van-checkbox
124 +
125 + @click="onCheckUserChange(user, $event)"
126 + v-for="(user, index) in userList"
127 + :id="user.id"
128 + :type="user.type"
129 + :text="user.name"
130 + :key="index"
131 + :name="user.id"
132 + shape="square"
133 + icon-size="13px"
134 + :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">{{ user.name }}</van-checkbox>
135 + </van-checkbox-group>
136 + </van-col>
137 + </van-row>
138 + </div>
139 + </div>
140 +
141 + <div v-show="is_search" class="search-container">
142 + <van-checkbox-group
143 + v-model="search_result_checked"
144 + style="padding: 0 0 1rem 1rem;"
145 + @change="onSearchResultChange"
146 + >
147 + <div>
148 + <p>部门</p>
149 + <div>
150 + <van-checkbox
151 + @click="onSearchResultClick(dept, $event)"
152 + v-for="(dept) in user_dept_role.dept"
153 + :id="dept.id"
154 + :type="dept.type"
155 + :text="dept.name"
156 + :key="dept.id"
157 + :name="dept.id"
158 + shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">
159 + {{ dept.name }}
160 + </van-checkbox>
161 + </div>
162 + </div>
163 + <div>
164 + <p>角色</p>
165 + <div>
166 + <van-checkbox
167 + @click="onSearchResultClick(role, $event)"
168 + v-for="(role) in user_dept_role.role"
169 + :id="role.id"
170 + :type="role.type"
171 + :text="role.name"
172 + :key="role.id"
173 + :name="role.id"
174 + shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.8rem;">
175 + {{ role.name }}
176 + </van-checkbox>
177 + </div>
178 + </div>
179 + <div>
180 + <p>成员</p>
181 + <div>
182 + <van-checkbox
183 + @click="onSearchResultClick(user, $event)"
184 + v-for="(user) in user_dept_role.user"
185 + :id="user.id"
186 + :type="user.type"
187 + :text="user.name"
188 + :key="user.id"
189 + :name="user.id"
190 + shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">
191 + {{ user.name }}
192 + </van-checkbox>
193 + </div>
194 + </div>
195 +
196 + {{ search_result_checked }}
197 + </van-checkbox-group>
198 + </div>
199 +
200 + <div style="position: fixed; bottom: 0; left: 0; width: 100%;">
201 + <van-row gutter="0">
202 + <van-col span="12">
203 + <van-button block type="default" @click="onCancelClick">取消</van-button>
204 + </van-col>
205 + <van-col span="12">
206 + <van-button block type="primary" @click="onConfirmClick">确定</van-button>
207 + </van-col>
208 + </van-row>
209 + </div>
210 + </van-popup>
211 +
212 + </div>
213 +</template>
214 +
215 +<script setup>
216 +import { inject, ref } from 'vue'
217 +import { useCustomFieldValue } from '@vant/use';
218 +import { styleColor } from "@/constant.js";
219 +// 大家可以根据需要是否引入VTreeNode, VTreeSearch, VTreeDrop
220 +import Vtree, { VTreeNode, VTreeSearch, VTreeDrop } from '@wsfe/vue-tree'
221 +import '@wsfe/vue-tree/style.css';
222 +import role_list from './flow_role_list.json'
223 +import dept_list from './flow_dept_list.json'
224 +import $ from 'jquery';
225 +import _ from 'lodash';
226 +
227 +// 获取父组件传值
228 +const props = inject('props');
229 +console.log("🚀 ~ file: MyComponent.vue:227 ~ props:", props);
230 +
231 +const emit = defineEmits(["active"]);
232 +
233 +onMounted(() => {
234 + // props.item.value = props.item?.component_props.default;
235 + // TODO:获取已选中数据
236 + // emitCheckedGroup.value = {
237 + // dept: [{
238 + // "id": 107691,
239 + // "name": "插花组",
240 + // "type": "dept"
241 + // }],
242 + // role: [{
243 + // "id": 137902,
244 + // "name": "大道大商营员组长",
245 + // "type": "role"
246 + // }],
247 + // user: [{
248 + // "id": 107707,
249 + // "name": "场地组长",
250 + // "type": "user"
251 + // }]
252 + // }
253 +});
254 +
255 +const openTree = () => {
256 + showPopover.value = true;
257 + // TODO:获取数据
258 + nextTick(() => {
259 + getDeptTreeData();
260 + // 获取已选择的数据
261 + checkedGroup.value = _.cloneDeep(emitCheckedGroup.value);
262 + syncResultToList(); // 同步勾选状态
263 + });
264 +}
265 +
266 +const emitCheckedGroup = ref({
267 + dept: [], // 组织结构
268 + role: [], // 角色
269 + user: [] // 成员
270 +});
271 +
272 +const tree_select_value = ref([]);
273 +
274 +const onCancelClick = () => {
275 + showPopover.value = false;
276 +}
277 +const onConfirmClick = () => {
278 + showPopover.value = false;
279 + if (is_search.value) {
280 + onCloseSearch()
281 + }
282 + //
283 + emitCheckedGroup.value = _.cloneDeep(checkedGroup.value);
284 + // 发送到表单数据
285 + tree_select_value.value = [].concat(...Object.values(emitCheckedGroup.value));
286 +}
287 +
288 +/******* 搜索输入项 *******/
289 +const showPopover = ref(false); // 显示/隐藏弹框
290 +
291 +const searchInputRef = ref(null);
292 +const searchValue = ref('');
293 +const is_search = ref(false); // 默认不显示搜索框
294 +/**
295 + * 搜索选中结果集
296 + * @param {Number} id
297 + */
298 +const search_result_checked = ref([]);
299 +
300 +const onSearchBlur = () => { // 搜索框失去焦点
301 +}
302 +
303 +const onSearchFocus = () => { // 搜索框获取焦点回调
304 + is_search.value = true; // 打开搜索状态
305 +
306 + // 自动选中搜索框
307 + nextTick(() => {
308 + searchInputRef.value.focus();
309 + });
310 +
311 + // 如果选中框有值,点击搜索框后把结果选中到搜索结果集里面
312 + // TODO:待实现 真实情况应该是请求搜索结果后做
313 + handleSelectToSearch();
314 +}
315 +
316 +const handleSelectToSearch = () => { // 把选中结果集同步,到搜索结果集上勾中显示
317 + let dept = checkedGroup.value.dept.map(item => item.id);
318 + let role = checkedGroup.value.role.map(item => item.id);
319 + let user = checkedGroup.value.user.map(item => item.id);
320 + search_result_checked.value = [...dept, ...role, ...user]; // 搜索选中结果集
321 +}
322 +
323 +const syncResultToList = () => { // 把弹框结果集同步到树形选择树的勾选状态
324 + // 组织结构,勾选状态还原
325 + deptTreeRef.value?.setCheckedKeys(checkedGroup.value.dept.map(item => item.id), true);
326 + // 角色选中,勾选状态还原
327 + role_checked.value = checkedGroup.value.role.map(item => item.id);
328 + // 成员选中,勾选状态还原
329 + user_checked.value = checkedGroup.value.user.map(item => item.id)
330 +}
331 +
332 +const onCloseSearch = () => { // 点击搜索关闭按钮回调
333 + tabActive.value = 0; // 默认选中组织结构
334 + is_search.value = false; // 关闭搜索状态
335 + syncResultToList(); // 同步勾选状态
336 +}
337 +/****************************** END ********************************/
338 +
339 +/**
340 + * 中间通用显示勾选结果集
341 + */
342 +
343 +const checkedGroup = ref({
344 + dept: [], // 组织结构
345 + role: [], // 角色
346 + user: [] // 成员
347 +});
348 +
349 +const onRemoveDeptTag = (dept) => { // 移除部门标签
350 + // 移除选中框显示
351 + const index = checkedGroup.value.dept.findIndex(item => JSON.stringify(item) === JSON.stringify(dept));
352 + checkedGroup.value.dept.splice(index, 1);
353 + // 组织结构移除对应ID
354 + deptTreeRef.value?.setChecked(dept.id, false);
355 + // 移除搜索结果选中显示
356 + const idx = search_result_checked.value.findIndex(item => JSON.stringify(item) === JSON.stringify(dept));
357 + search_result_checked.value.splice(idx, 1);
358 +}
359 +
360 +const onRemoveRoleTag = (role) => { // 移除角色标签
361 + const index = checkedGroup.value.role.findIndex(item => JSON.stringify(item) === JSON.stringify(role));
362 + checkedGroup.value.role.splice(index, 1);
363 + //
364 + const idx = role_checked.value.findIndex(item => JSON.stringify(item) === JSON.stringify(role));
365 + role_checked.value.splice(index, 1);
366 + // 移除搜索结果选中显示
367 + const i = search_result_checked.value.findIndex(item => JSON.stringify(item) === JSON.stringify(role));
368 + search_result_checked.value.splice(i, 1);
369 +}
370 +
371 +const onRemoveUserTag = (user) => { // 移除成员标签
372 + const index = checkedGroup.value.user.findIndex(item => JSON.stringify(item) === JSON.stringify(user));
373 + checkedGroup.value.user.splice(index, 1);
374 + //
375 + const idx = user_checked.value.findIndex(item => JSON.stringify(item) === JSON.stringify(user));
376 + user_checked.value.splice(index, 1);
377 + // 移除搜索结果选中显示
378 + const i = search_result_checked.value.findIndex(item => JSON.stringify(item) === JSON.stringify(user));
379 + search_result_checked.value.splice(i, 1);
380 +}
381 +
382 +/*************** Tab 功能模块 ****************/
383 +const tabRef = ref(null);
384 +const tabActive = ref(0);
385 +const deptTreeRef = ref();
386 +
387 +
388 +const userList = ref([]);
389 +
390 +const onClickTab = ({ title }) => { // tab点击事件
391 + nextTick(() => {
392 + if (title === '组织结构') {
393 + deptListReset();
394 + }
395 + if (title === '角色') {
396 + roleListReset();
397 + }
398 + if (title === '成员') {
399 + userListReset();
400 + }
401 + });
402 +};
403 +
404 +const deptListReset = () => { // 组织重置列表
405 + deptTreeRef.value.setData(role_list);
406 + deptTreeRef.value.setExpand(35697, true)
407 +}
408 +
409 +const roleListReset = () => { // 角色重置列表
410 + roleList.value = dept_list;
411 +}
412 +
413 +const userListReset = () => { // 成员重置列表
414 + userTreeRef.value.setData(role_list);
415 + userTreeRef.value.setExpand(35697, true)
416 +}
417 +/**************** END *****************/
418 +
419 +/************* 组织结构模块 ***************/
420 +const select_dept_value = ref(); // 组织结构树形选中值
421 +
422 +const getDeptTreeData = () => { // 获取组织结构数据
423 + deptTreeRef.value.setData(role_list);
424 + // 默认展开第一个
425 + deptTreeRef.value.setExpand(35697, true);
426 +}
427 +
428 +const deptTreeCheckedChange = (arr) => { // 组织结构勾选回调
429 + checkedGroup.value.dept = arr.map((item) => {
430 + return {
431 + id: item.id,
432 + name: item.name,
433 + type: 'dept'
434 + }
435 + });
436 +}
437 +/**************** END *****************/
438 +
439 +/************* 角色模块 ***************/
440 +const role_checked = ref([]); // 角色多选选中值
441 +const roleList = ref([]);
442 +
443 +const roleChangeMethod = (val) => { // 角色多选组点击回调
444 + let result = val.map(id => roleList.value.find(obj => obj.id === id));
445 + // 过滤掉未找到的项(即返回undefined的项)
446 + checkedGroup.value.role = result.filter(item => item !== undefined);
447 +}
448 +/**************** END *****************/
449 +
450 +/************* 成员模块 ***************/
451 +const userTreeRef = ref();
452 +const select_user_value = ref(); // 成员树形选中值
453 +const user_checked = ref([]); // 成员多选选中值
454 +
455 +const onUserTreeClick = (node) => { // 点击成员树形回调
456 + userList.value = node.user;
457 + user_checked.value = checkedGroup.value.user.map(item => item.id)
458 +}
459 +
460 +const onUserChange = (val) => { // 成员多选组点击回调
461 +}
462 +
463 +const onCheckUserChange = (val, evt) => {
464 + nextTick(() => {
465 + let checked = false;
466 + let id = '';
467 + let name = '';
468 + let type = '';
469 + if ($(evt.target).attr('aria-checked') === undefined) {
470 + checked = $(evt.target).parents('.van-checkbox').attr('aria-checked');
471 + id = $(evt.target).parents('.van-checkbox').attr('id');
472 + name = $(evt.target).parents('.van-checkbox').attr('text');
473 + type = $(evt.target).parents('.van-checkbox').attr('type');
474 + } else {
475 + checked = $(evt.target).attr('aria-checked');
476 + id = $(evt.target).attr('id');
477 + name = $(evt.target).attr('text');
478 + type = $(evt.target).attr('type');
479 + }
480 + let obj = {
481 + id: +id,
482 + name,
483 + type
484 + }
485 + checkedGroup.value.user.push(obj);
486 + checkedGroup.value.user = _.uniqBy(checkedGroup.value.user, 'id');
487 + //
488 + if (checked === 'false') {
489 + if (val.type === 'user') {
490 + const index = checkedGroup.value.user.indexOf(val);
491 + checkedGroup.value.user.splice(index, 1);
492 + }
493 + }
494 + })
495 +}
496 +/**************** END *****************/
497 +
498 +/***************** 搜索结果集模块 ********************/
499 +
500 +// 模拟数据
501 +const user_dept_role = ref({
502 + "dept": [
503 + {
504 + "type": "dept",
505 + "name": "男10组",
506 + "id": 137571
507 + }, {
508 + "type": "dept",
509 + "name": "主持组",
510 + "id": 107700
511 + }
512 + ],
513 + "role": [
514 + {
515 + "type": "role",
516 + "name": "八关斋戒",
517 + "id": 624337
518 + },
519 + {
520 + "id": 82983,
521 + "name": "场地管理",
522 + "type": "role"
523 + }
524 + ],
525 + "user": [
526 + {
527 + "id": 137918,
528 + "name": "10组寝室长",
529 + "type": "user"
530 + },
531 + {
532 + "id": 137919,
533 + "name": "11组寝室长",
534 + "type": "user"
535 + }
536 + ]
537 +});
538 +
539 +const onSearchResultChange = (val) => { // 监听搜索结果集点击回调,结果集为选中项
540 +}
541 +
542 +const onSearchResultClick = (val, evt) => { // 搜索结果集项点击回调
543 + nextTick(() => {
544 + let checked = false;
545 + let id = '';
546 + let name = '';
547 + let type = '';
548 +
549 + if ($(evt.target).attr('aria-checked') === undefined) { // 点击子元素
550 + checked = $(evt.target).parents('.van-checkbox').attr('aria-checked');
551 + id = $(evt.target).parents('.van-checkbox').attr('id');
552 + name = $(evt.target).parents('.van-checkbox').attr('text');
553 + type = $(evt.target).parents('.van-checkbox').attr('type');
554 + } else { // 点击父元素
555 + checked = $(evt.target).attr('aria-checked');
556 + id = $(evt.target).attr('id');
557 + name = $(evt.target).attr('text');
558 + type = $(evt.target).attr('type');
559 + }
560 +
561 + let obj = { // 点击元素属性
562 + id: +id,
563 + name,
564 + type
565 + }
566 +
567 + // 对应类型添加到选中组
568 + checkedGroup.value[type].push(obj);
569 + checkedGroup.value[type] = _.uniqBy(checkedGroup.value[type], 'id');
570 +
571 + // 取消选中处理
572 + if (checked === 'false') {
573 + // 移除对应的数据集
574 + const index = checkedGroup.value[type].findIndex(item => item.id === obj.id);
575 + checkedGroup.value[type].splice(index, 1);
576 +
577 + if (type === 'dept') {
578 + // 树形 组织结构移除对应ID
579 + deptTreeRef.value?.setChecked(obj.id, false);
580 + }
581 + }
582 + });
583 +}
584 +
585 +// 此处传入的值会替代 Field 组件内部的 value
586 +useCustomFieldValue(() => tree_select_value.value);
587 +
588 +// defineExpose({ handleReset, show_control });
589 +</script>
590 +
591 +<style lang="less" scoped>
592 +.tree-field-page {
593 + .select-tree-box {
594 + height: 4rem;
595 + width: calc(100vw - 5rem);
596 + border: 1px dashed #dfdfdf;
597 + margin: 1rem 0;
598 + overflow: scroll;
599 + border-radius: 5px;
600 + padding: 0.5rem;
601 + display: flex;
602 + flex-wrap: wrap;
603 + .select-tree-item {
604 + margin-right: 5px;
605 + margin-bottom: 5px;
606 + font-size: 0.85rem;
607 + padding: 5px 8px;
608 + background-color: #C2915F;
609 + color: #fff;
610 + height: 1.2rem;
611 + display: flex;
612 + justify-content: center;
613 + align-items: center;
614 + }
615 + }
616 +}
617 +
618 +:deep(.van-field__body) {
619 + border: 1px solid #eaeaea;
620 + border-radius: 0.25rem;
621 + padding: 0.25rem 0.5rem;
622 +}
623 +
624 +.search-box {
625 + display: flex;
626 + align-items: center;
627 + justify-content: center;
628 + background-color: #eee;
629 + margin: 1rem;
630 + border-radius: 3px;
631 + padding: 0.6rem;
632 + font-size: 0.9rem;
633 +}
634 +
635 +:deep(.ctree-tree-node__checkbox_checked) {
636 + border-color: #C2915F;
637 + background-color: #C2915F;
638 +}
639 +
640 +:deep(.ctree-tree-node__title_selected) {
641 + background-color: #f8e2cb;
642 +}
643 +
644 +// :deep(.ctree-tree__scroll-area) {
645 +// margin-bottom: 2rem;
646 +// }
647 +
648 +.select-box {
649 + height: 4rem;
650 + border: 1px dashed #dfdfdf;
651 + margin: 0 1rem;
652 + overflow: scroll;
653 + border-radius: 5px;
654 + padding: 0.5rem;
655 + display: flex;
656 + flex-wrap: wrap;
657 + .select-item {
658 + margin-right: 5px;
659 + margin-bottom: 5px;
660 + font-size: 0.85rem;
661 + padding: 5px 8px;
662 + background-color: #C2915F;
663 + color: #fff;
664 + height: 1.2rem;
665 + display: flex;
666 + justify-content: center;
667 + align-items: center;
668 + }
669 +}
670 +</style>
1 <!-- 1 <!--
2 * @Date: 2022-08-29 14:31:20 2 * @Date: 2022-08-29 14:31:20
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-05-31 18:05:23 4 + * @LastEditTime: 2024-06-03 13:29:18
5 * @FilePath: /data-table/src/components/TreeField/index.vue 5 * @FilePath: /data-table/src/components/TreeField/index.vue
6 * @Description: 树形组件 6 * @Description: 树形组件
7 --> 7 -->
...@@ -12,224 +12,25 @@ ...@@ -12,224 +12,25 @@
12 {{ item.component_props.label }} 12 {{ item.component_props.label }}
13 </div> 13 </div>
14 14
15 - <div class="select-tree-box" @click="openTree"> 15 + <van-field :name="item.key" :rules="rules" style="padding: 0 1rem;">
16 - <div class="select-tree-item" v-for="(dept) in emitCheckedGroup.dept" :key="dept.id"> 16 + <template #input>
17 - {{ dept.name }} 17 + <my-component ref="refComponent" />
18 - </div> 18 + </template>
19 - <div class="select-tree-item" v-for="(role) in emitCheckedGroup.role" :key="role.id"> 19 + </van-field>
20 - {{ role.name }}
21 - </div>
22 - <div class="select-tree-item" v-for="(user) in emitCheckedGroup.user" :key="user.id">
23 - {{ user.name }}
24 - </div>
25 - </div>
26 -
27 - <van-popup
28 - v-model:show="showPopover"
29 - position="bottom"
30 - :close-on-click-overlay="false"
31 - :style="{ height: '90vh' }"
32 - >
33 - <div v-if="!is_search" class="search-box" @click="onSearchFocus">
34 - <van-icon name="search" size="1.1rem" />&nbsp;点击搜索
35 - </div>
36 - <van-field
37 - v-else
38 - ref="searchInputRef"
39 - v-model="searchValue"
40 - placeholder="可通过名称,手机号或邮箱查询"
41 - :border="false"
42 - @blur="onSearchBlur"
43 - @focus="onSearchFocus"
44 - >
45 - <template #button>
46 - <van-button size="small" type="primary" @click="onCloseSearch">关闭</van-button>
47 - </template>
48 - </van-field>
49 -
50 - <div class="select-box">
51 - <div class="select-item" v-for="(dept) in checkedGroup.dept" :key="dept.id">
52 - {{ dept.name }}&nbsp;<van-icon @click="onRemoveDeptTag(dept)" name="close" />
53 - </div>
54 - <div class="select-item" v-for="(role) in checkedGroup.role" :key="role.id">
55 - {{ role.name }}&nbsp;<van-icon @click="onRemoveRoleTag(role)" name="close" />
56 - </div>
57 - <div class="select-item" v-for="(user) in checkedGroup.user" :key="user.id">
58 - {{ user.name }}&nbsp;<van-icon @click="onRemoveUserTag(user)" name="close" />
59 - </div>
60 - </div>
61 -
62 - <div v-show="!is_search" class="tab-tree-container">
63 - <van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" style="margin-bottom: 1rem;">
64 - <van-tab title="组织结构" :name="0"></van-tab>
65 - <van-tab title="角色" :name="1"></van-tab>
66 - <van-tab title="成员" :name="2"></van-tab>
67 - </van-tabs>
68 -
69 - <div v-show="tabActive === 0" style="padding: 0 0 1rem 1rem;">
70 - <Vtree
71 - ref="deptTreeRef"
72 - v-model="select_dept_value"
73 - checkable
74 - titleField="name"
75 - keyField="id"
76 - :expandOnFilter="false"
77 - :showCheckedButton="false"
78 - :showFooter="false"
79 - :cascade="false"
80 - :defaultExpandAll="false"
81 - @checked-change="deptTreeCheckedChange"
82 - style=" height: 55vh; overflow: scroll;"
83 - >
84 - <span slot="empty">暂无数据</span>
85 - </Vtree>
86 - </div>
87 -
88 - <div v-if="tabActive === 1" style="padding: 0 0 1rem 1rem;">
89 - <van-checkbox-group
90 - v-model="role_checked"
91 - @change="roleChangeMethod"
92 - >
93 - <van-checkbox
94 - v-for="(role, index) in roleList"
95 - :key="index"
96 - :name="role.id"
97 - shape="square"
98 - icon-size="13px"
99 - :checked-color="styleColor.baseColor"
100 - style="margin-bottom: 0.5rem;"
101 - >{{ role.name }}</van-checkbox>
102 - </van-checkbox-group>
103 - </div>
104 -
105 - <div v-if="tabActive === 2" style="padding: 0 0 0 1rem;">
106 - <van-row gutter="">
107 - <van-col span="10" style="border-right: 1px solid #eee; height: 55vh; overflow: scroll;">
108 - <Vtree
109 - ref="userTreeRef"
110 - v-model="select_user_value"
111 - selectable
112 - titleField="name"
113 - keyField="id"
114 - :expandOnFilter="false"
115 - :showCheckedButton="false"
116 - @update:modelValue="() => {}"
117 - @click="onUserTreeClick"
118 - >
119 - <span slot="empty">暂无数据</span>
120 - </Vtree>
121 - </van-col>
122 - <van-col span="14">
123 - <van-checkbox-group
124 - v-model="user_checked"
125 - style="padding: 0 0 1rem 1rem;"
126 - @change="onUserChange"
127 - >
128 - <van-checkbox
129 -
130 - @click="onCheckUserChange(user, $event)"
131 - v-for="(user, index) in userList"
132 - :id="user.id"
133 - :type="user.type"
134 - :text="user.name"
135 - :key="index"
136 - :name="user.id"
137 - shape="square"
138 - icon-size="13px"
139 - :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">{{ user.name }}</van-checkbox>
140 - </van-checkbox-group>
141 - </van-col>
142 - </van-row>
143 - </div>
144 - </div>
145 -
146 - <div v-show="is_search" class="search-container">
147 - <van-checkbox-group
148 - v-model="search_result_checked"
149 - style="padding: 0 0 1rem 1rem;"
150 - @change="onSearchResultChange"
151 - >
152 - <div>
153 - <p>部门</p>
154 - <div>
155 - <van-checkbox
156 - @click="onSearchResultClick(dept, $event)"
157 - v-for="(dept) in user_dept_role.dept"
158 - :id="dept.id"
159 - :type="dept.type"
160 - :text="dept.name"
161 - :key="dept.id"
162 - :name="dept.id"
163 - shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">
164 - {{ dept.name }}
165 - </van-checkbox>
166 - </div>
167 - </div>
168 - <div>
169 - <p>角色</p>
170 - <div>
171 - <van-checkbox
172 - @click="onSearchResultClick(role, $event)"
173 - v-for="(role) in user_dept_role.role"
174 - :id="role.id"
175 - :type="role.type"
176 - :text="role.name"
177 - :key="role.id"
178 - :name="role.id"
179 - shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.8rem;">
180 - {{ role.name }}
181 - </van-checkbox>
182 - </div>
183 - </div>
184 - <div>
185 - <p>成员</p>
186 - <div>
187 - <van-checkbox
188 - @click="onSearchResultClick(user, $event)"
189 - v-for="(user) in user_dept_role.user"
190 - :id="user.id"
191 - :type="user.type"
192 - :text="user.name"
193 - :key="user.id"
194 - :name="user.id"
195 - shape="square" icon-size="13px" :checked-color="styleColor.baseColor" style="margin-bottom: 0.5rem;">
196 - {{ user.name }}
197 - </van-checkbox>
198 - </div>
199 - </div>
200 -
201 - {{ search_result_checked }}
202 - </van-checkbox-group>
203 - </div>
204 -
205 - <div style="position: fixed; bottom: 0; left: 0; width: 100%;">
206 - <van-row gutter="0">
207 - <van-col span="12">
208 - <van-button block type="default" @click="onCancelClick">取消</van-button>
209 - </van-col>
210 - <van-col span="12">
211 - <van-button block type="primary" @click="onConfirmClick">确定</van-button>
212 - </van-col>
213 - </van-row>
214 - </div>
215 - </van-popup>
216 20
217 </div> 21 </div>
218 </template> 22 </template>
219 23
220 <script setup> 24 <script setup>
221 -import { styleColor } from "@/constant.js"; 25 +import MyComponent from './MyComponent.vue';
222 -// 大家可以根据需要是否引入VTreeNode, VTreeSearch, VTreeDrop
223 -import Vtree, { VTreeNode, VTreeSearch, VTreeDrop } from '@wsfe/vue-tree'
224 -import '@wsfe/vue-tree/style.css';
225 -import role_list from './flow_role_list.json'
226 -import dept_list from './flow_dept_list.json'
227 -import $ from 'jquery';
228 -import _ from 'lodash';
229 26
230 const props = defineProps({ 27 const props = defineProps({
231 item: Object, 28 item: Object,
232 }); 29 });
30 +// 注入子组件属性
31 +provide('props', props.item);
32 +
33 +const refComponent = ref(null)
233 34
234 // 隐藏显示 35 // 隐藏显示
235 const HideShow = computed(() => { 36 const HideShow = computed(() => {
...@@ -242,324 +43,42 @@ const isGroup = computed(() => { ...@@ -242,324 +43,42 @@ const isGroup = computed(() => {
242 }); 43 });
243 44
244 onMounted(() => { 45 onMounted(() => {
245 - props.item.value = props.item.component_props.default; 46 + // props.item.value = props.item.component_props.default;
246 -}); 47 + // TODO:获取已选中数据
247 - 48 + // emitCheckedGroup.value = {
248 -const openTree = () => { 49 + // dept: [{
249 - showPopover.value = true; 50 + // "id": 107691,
250 - // TODO:获取数据 51 + // "name": "插花组",
251 - nextTick(() => { 52 + // "type": "dept"
252 - getDeptTreeData(); 53 + // }],
253 - }) 54 + // role: [{
254 -} 55 + // "id": 137902,
255 - 56 + // "name": "大道大商营员组长",
256 -const emitCheckedGroup = ref({ 57 + // "type": "role"
257 - dept: [], // 组织结构 58 + // }],
258 - role: [], // 角色 59 + // user: [{
259 - user: [] // 成员 60 + // "id": 107707,
260 -}); 61 + // "name": "场地组长",
261 - 62 + // "type": "user"
262 -const onCancelClick = () => { 63 + // }]
263 - showPopover.value = false;
264 -}
265 -const onConfirmClick = () => {
266 - showPopover.value = false;
267 - //
268 - emitCheckedGroup.value = _.cloneDeep(checkedGroup.value);
269 -}
270 -
271 -/******* 搜索输入项 *******/
272 -const showPopover = ref(false); // 显示/隐藏弹框
273 -
274 -const searchInputRef = ref(null);
275 -const searchValue = ref('');
276 -const is_search = ref(false); // 默认不显示搜索框
277 -/**
278 - * 搜索选中结果集
279 - * @param {Number} id
280 - */
281 -const search_result_checked = ref([]);
282 -
283 -const onSearchBlur = () => { // 搜索框失去焦点
284 -}
285 -
286 -const onSearchFocus = () => { // 搜索框获取焦点回调
287 - is_search.value = true; // 打开搜索状态
288 -
289 - // 自动选中搜索框
290 - nextTick(() => {
291 - searchInputRef.value.focus();
292 - });
293 -
294 - // 如果选中框有值,点击搜索框后把结果选中到搜索结果集里面
295 - // TODO:待实现 真实情况应该是请求搜索结果后做
296 - handleSelectToSearch();
297 -}
298 -
299 -const handleSelectToSearch = () => { // 把选中结果集同步,到搜索结果集上勾中显示
300 - let dept = checkedGroup.value.dept.map(item => item.id);
301 - let role = checkedGroup.value.role.map(item => item.id);
302 - let user = checkedGroup.value.user.map(item => item.id);
303 - search_result_checked.value = [...dept, ...role, ...user]; // 搜索选中结果集
304 -}
305 -
306 -const onCloseSearch = () => { // 点击搜索关闭按钮回调
307 - tabActive.value = 0; // 默认选中组织结构
308 - is_search.value = false; // 关闭搜索状态
309 - // 组织结构,勾选状态还原
310 - deptTreeRef.value?.setCheckedKeys(checkedGroup.value.dept.map(item => item.id), true);
311 - // 角色选中,勾选状态还原
312 - role_checked.value = checkedGroup.value.role.map(item => item.id);
313 - // 成员选中,勾选状态还原
314 - user_checked.value = checkedGroup.value.user.map(item => item.id)
315 -}
316 -/****************************** END ********************************/
317 -
318 -/**
319 - * 中间通用显示勾选结果集
320 - */
321 -
322 -const checkedGroup = ref({
323 - dept: [], // 组织结构
324 - role: [], // 角色
325 - user: [] // 成员
326 }); 64 });
327 65
328 -const onRemoveDeptTag = (dept) => { // 移除部门标签 66 +// 规则校验
329 - // 移除选中框显示 67 +const required = props.item.component_props.required;
330 - const index = checkedGroup.value.dept.indexOf(dept); 68 +const validator = (val) => {
331 - checkedGroup.value.dept.splice(index, 1); 69 + if (required && !val.length) {
332 - // 组织结构移除对应ID 70 + return false;
333 - deptTreeRef.value?.setChecked(dept.id, false); 71 + } else {
334 - // 移除搜索结果选中显示 72 + return true;
335 - const idx = search_result_checked.value.indexOf(dept); 73 + }
336 - search_result_checked.value.splice(idx, 1);
337 -}
338 -
339 -const onRemoveRoleTag = (role) => { // 移除角色标签
340 - const index = checkedGroup.value.role.indexOf(role);
341 - checkedGroup.value.role.splice(index, 1);
342 - //
343 - const idx = role_checked.value.indexOf(role);
344 - role_checked.value.splice(index, 1);
345 - // 移除搜索结果选中显示
346 - const i = search_result_checked.value.indexOf(role);
347 - search_result_checked.value.splice(i, 1);
348 -}
349 -
350 -const onRemoveUserTag = (user) => { // 移除成员标签
351 - const index = checkedGroup.value.user.indexOf(user);
352 - checkedGroup.value.user.splice(index, 1);
353 - //
354 - const idx = user_checked.value.indexOf(user);
355 - user_checked.value.splice(index, 1);
356 - // 移除搜索结果选中显示
357 - const i = search_result_checked.value.indexOf(user);
358 - search_result_checked.value.splice(i, 1);
359 -}
360 -
361 -/*************** Tab 功能模块 ****************/
362 -const tabRef = ref(null);
363 -const tabActive = ref(0);
364 -const deptTreeRef = ref();
365 -
366 -
367 -const userList = ref([]);
368 -
369 -const onClickTab = ({ title }) => { // tab点击事件
370 - nextTick(() => {
371 - if (title === '组织结构') {
372 - deptListReset();
373 - }
374 - if (title === '角色') {
375 - roleListReset();
376 - }
377 - if (title === '成员') {
378 - userListReset();
379 - }
380 - });
381 }; 74 };
382 - 75 +// 错误提示文案
383 -const deptListReset = () => { // 组织重置列表 76 +const validatorMessage = (val, rule) => {
384 - deptTreeRef.value.setData(role_list); 77 + if (required && !val.length) {
385 - deptTreeRef.value.setExpand(35697, true) 78 + return "选择不能为空";
386 -} 79 + }
387 - 80 +};
388 -const roleListReset = () => { // 角色重置列表 81 +const rules = [{ validator, message: validatorMessage }];
389 - roleList.value = dept_list;
390 -}
391 -
392 -const userListReset = () => { // 成员重置列表
393 - userTreeRef.value.setData(role_list);
394 - userTreeRef.value.setExpand(35697, true)
395 -}
396 -/**************** END *****************/
397 -
398 -/************* 组织结构模块 ***************/
399 -const select_dept_value = ref(); // 组织结构树形选中值
400 -
401 -const getDeptTreeData = () => { // 获取组织结构数据
402 - deptTreeRef.value.setData(role_list);
403 - // 默认展开第一个
404 - deptTreeRef.value.setExpand(35697, true);
405 -}
406 -
407 -const deptTreeCheckedChange = (arr) => { // 组织结构勾选回调
408 - checkedGroup.value.dept = arr.map((item) => {
409 - return {
410 - id: item.id,
411 - name: item.name,
412 - type: 'dept'
413 - }
414 - });
415 -}
416 -/**************** END *****************/
417 -
418 -/************* 角色模块 ***************/
419 -const role_checked = ref([]); // 角色多选选中值
420 -const roleList = ref([]);
421 -
422 -const roleChangeMethod = (val) => { // 角色多选组点击回调
423 - let result = val.map(id => roleList.value.find(obj => obj.id === id));
424 - // 过滤掉未找到的项(即返回undefined的项)
425 - checkedGroup.value.role = result.filter(item => item !== undefined);
426 -}
427 -/**************** END *****************/
428 -
429 -/************* 成员模块 ***************/
430 -const userTreeRef = ref();
431 -const select_user_value = ref(); // 成员树形选中值
432 -const user_checked = ref([]); // 成员多选选中值
433 -
434 -const onUserTreeClick = (node) => { // 点击成员树形回调
435 - userList.value = node.user;
436 - user_checked.value = checkedGroup.value.user.map(item => item.id)
437 -}
438 -
439 -const onUserChange = (val) => { // 成员多选组点击回调
440 -}
441 -
442 -const onCheckUserChange = (val, evt) => {
443 - nextTick(() => {
444 - let checked = false;
445 - let id = '';
446 - let name = '';
447 - let type = '';
448 - if ($(evt.target).attr('aria-checked') === undefined) {
449 - checked = $(evt.target).parents('.van-checkbox').attr('aria-checked');
450 - id = $(evt.target).parents('.van-checkbox').attr('id');
451 - name = $(evt.target).parents('.van-checkbox').attr('text');
452 - type = $(evt.target).parents('.van-checkbox').attr('type');
453 - } else {
454 - checked = $(evt.target).attr('aria-checked');
455 - id = $(evt.target).attr('id');
456 - name = $(evt.target).attr('text');
457 - type = $(evt.target).attr('type');
458 - }
459 - let obj = {
460 - id: +id,
461 - name,
462 - type
463 - }
464 - checkedGroup.value.user.push(obj);
465 - checkedGroup.value.user = _.uniqBy(checkedGroup.value.user, 'id');
466 - //
467 - if (checked === 'false') {
468 - if (val.type === 'user') {
469 - const index = checkedGroup.value.user.indexOf(val);
470 - checkedGroup.value.user.splice(index, 1);
471 - }
472 - }
473 - })
474 -}
475 -/**************** END *****************/
476 -
477 -/***************** 搜索结果集模块 ********************/
478 -
479 -// 模拟数据
480 -const user_dept_role = ref({
481 - "dept": [
482 - {
483 - "type": "dept",
484 - "name": "男10组",
485 - "id": 137571
486 - }, {
487 - "type": "dept",
488 - "name": "主持组",
489 - "id": 107700
490 - }
491 - ],
492 - "role": [
493 - {
494 - "type": "role",
495 - "name": "八关斋戒",
496 - "id": 624337
497 - },
498 - {
499 - "id": 82983,
500 - "name": "场地管理",
501 - "type": "role"
502 - }
503 - ],
504 - "user": [
505 - {
506 - "id": 137918,
507 - "name": "10组寝室长",
508 - "type": "user"
509 - },
510 - {
511 - "id": 137919,
512 - "name": "11组寝室长",
513 - "type": "user"
514 - }
515 - ]
516 -});
517 -
518 -const onSearchResultChange = (val) => { // 监听搜索结果集点击回调,结果集为选中项
519 -}
520 -
521 -const onSearchResultClick = (val, evt) => { // 搜索结果集项点击回调
522 - nextTick(() => {
523 - let checked = false;
524 - let id = '';
525 - let name = '';
526 - let type = '';
527 -
528 - if ($(evt.target).attr('aria-checked') === undefined) { // 点击子元素
529 - checked = $(evt.target).parents('.van-checkbox').attr('aria-checked');
530 - id = $(evt.target).parents('.van-checkbox').attr('id');
531 - name = $(evt.target).parents('.van-checkbox').attr('text');
532 - type = $(evt.target).parents('.van-checkbox').attr('type');
533 - } else { // 点击父元素
534 - checked = $(evt.target).attr('aria-checked');
535 - id = $(evt.target).attr('id');
536 - name = $(evt.target).attr('text');
537 - type = $(evt.target).attr('type');
538 - }
539 -
540 - let obj = { // 点击元素属性
541 - id: +id,
542 - name,
543 - type
544 - }
545 -
546 - // 对应类型添加到选中组
547 - checkedGroup.value[type].push(obj);
548 - checkedGroup.value[type] = _.uniqBy(checkedGroup.value[type], 'id');
549 -
550 - // 取消选中处理
551 - if (checked === 'false') {
552 - // 移除对应的数据集
553 - const index = checkedGroup.value[type].findIndex(item => item.id === obj.id);
554 - checkedGroup.value[type].splice(index, 1);
555 -
556 - if (type === 'dept') {
557 - // 树形 组织结构移除对应ID
558 - deptTreeRef.value?.setChecked(obj.id, false);
559 - }
560 - }
561 - });
562 -}
563 </script> 82 </script>
564 83
565 <style lang="less" scoped> 84 <style lang="less" scoped>
...@@ -586,81 +105,5 @@ const onSearchResultClick = (val, evt) => { // 搜索结果集项点击回调 ...@@ -586,81 +105,5 @@ const onSearchResultClick = (val, evt) => { // 搜索结果集项点击回调
586 color: red; 105 color: red;
587 } 106 }
588 } 107 }
589 -
590 - .select-tree-box {
591 - height: 4rem;
592 - border: 1px dashed #dfdfdf;
593 - margin: 1rem;
594 - overflow: scroll;
595 - border-radius: 5px;
596 - padding: 0.5rem;
597 - display: flex;
598 - flex-wrap: wrap;
599 - .select-tree-item {
600 - margin-right: 5px;
601 - margin-bottom: 5px;
602 - font-size: 0.85rem;
603 - padding: 5px 8px;
604 - background-color: #C2915F;
605 - color: #fff;
606 - height: 1.2rem;
607 - display: flex;
608 - justify-content: center;
609 - align-items: center;
610 - }
611 - }
612 -}
613 -
614 -:deep(.van-field__body) {
615 - border: 1px solid #eaeaea;
616 - border-radius: 0.25rem;
617 - padding: 0.25rem 0.5rem;
618 -}
619 -
620 -.search-box {
621 - display: flex;
622 - align-items: center;
623 - justify-content: center;
624 - background-color: #eee;
625 - margin: 1rem;
626 - border-radius: 3px;
627 - padding: 0.6rem;
628 - font-size: 0.9rem;
629 -}
630 -
631 -:deep(.ctree-tree-node__checkbox_checked) {
632 - border-color: #C2915F;
633 - background-color: #C2915F;
634 -}
635 -
636 -:deep(.ctree-tree-node__title_selected) {
637 - background-color: #f8e2cb;
638 -}
639 -
640 -// :deep(.ctree-tree__scroll-area) {
641 -// margin-bottom: 2rem;
642 -// }
643 -
644 -.select-box {
645 - height: 4rem;
646 - border: 1px dashed #dfdfdf;
647 - margin: 0 1rem;
648 - overflow: scroll;
649 - border-radius: 5px;
650 - padding: 0.5rem;
651 - display: flex;
652 - flex-wrap: wrap;
653 - .select-item {
654 - margin-right: 5px;
655 - margin-bottom: 5px;
656 - font-size: 0.85rem;
657 - padding: 5px 8px;
658 - background-color: #C2915F;
659 - color: #fff;
660 - height: 1.2rem;
661 - display: flex;
662 - justify-content: center;
663 - align-items: center;
664 - }
665 } 108 }
666 </style> 109 </style>
......