hookehuyr

feat(OrgPickerField): 实现动态配置标签页并优化搜索展示

- 将硬编码的标签页改为基于组件属性动态配置的模式,支持通过org_type和org_type_title控制显示的标签类型
- 调整标签状态管理,使用类型字符串替代数字索引作为标签标识,修复标题本地化导致的标签切换异常
- 优化搜索结果区域渲染逻辑,使用循环复用代码减少重复编写
- 修复标签切换、默认激活页及搜索关闭后的状态处理逻辑
- 新增标签可见性校验,过滤无效的已选数据
- 为van-tabs新增shrink属性优化布局展示
- 移除组件中过时的TODO注释
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-11-25 15:34:53 4 + * @LastEditTime: 2026-05-27 17:53:57
5 * @FilePath: /data-table/src/components/OrgPickerField/MyComponent.vue 5 * @FilePath: /data-table/src/components/OrgPickerField/MyComponent.vue
6 * @Description: 树形组件 6 * @Description: 树形组件
7 --> 7 -->
...@@ -69,14 +69,14 @@ ...@@ -69,14 +69,14 @@
69 </div> 69 </div>
70 70
71 <div v-show="!is_search" class="tab-tree-container"> 71 <div v-show="!is_search" class="tab-tree-container">
72 - <van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" style="margin-bottom: 1rem;"> 72 + <van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" shrink style="margin-bottom: 1rem;">
73 <!-- <van-tab title="组织结构" :name="0"></van-tab> 73 <!-- <van-tab title="组织结构" :name="0"></van-tab>
74 <van-tab title="角色" :name="1"></van-tab> 74 <van-tab title="角色" :name="1"></van-tab>
75 <van-tab title="成员" :name="2"></van-tab> --> 75 <van-tab title="成员" :name="2"></van-tab> -->
76 <van-tab v-for="(tab, index) in tabList" :key="index" :title="tab.title" :name="tab.name"></van-tab> 76 <van-tab v-for="(tab, index) in tabList" :key="index" :title="tab.title" :name="tab.name"></van-tab>
77 </van-tabs> 77 </van-tabs>
78 78
79 - <div v-show="tabActive === 0" style="padding: 0 0 0 1rem;"> 79 + <div v-if="isTabVisible('dept')" v-show="tabActive === 'dept'" style="padding: 0 0 0 1rem;">
80 <Vtree 80 <Vtree
81 id="deptTree" 81 id="deptTree"
82 ref="deptTreeRef" 82 ref="deptTreeRef"
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
99 </Vtree> 99 </Vtree>
100 </div> 100 </div>
101 101
102 - <div v-if="tabActive === 1" style="padding: 0 0 0 1rem; overflow: scroll; height: 60vh;" @click.stop> 102 + <div v-if="isTabVisible('role') && tabActive === 'role'" style="padding: 0 0 0 1rem; overflow: scroll; height: 60vh;" @click.stop>
103 <van-checkbox-group 103 <van-checkbox-group
104 v-model="role_checked" 104 v-model="role_checked"
105 @change="roleChangeMethod" 105 @change="roleChangeMethod"
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
121 <div style="height: 10vh;"></div> 121 <div style="height: 10vh;"></div>
122 </div> 122 </div>
123 123
124 - <div v-if="tabActive === 2" style="padding: 0 0 0 1rem;"> 124 + <div v-if="isTabVisible('user') && tabActive === 'user'" style="padding: 0 0 0 1rem;">
125 <van-row gutter=""> 125 <van-row gutter="">
126 <van-col span="10" style="border-right: 1px solid #eee; height: 60vh; overflow: scroll;"> 126 <van-col span="10" style="border-right: 1px solid #eee; height: 60vh; overflow: scroll;">
127 <Vtree 127 <Vtree
...@@ -173,77 +173,41 @@ ...@@ -173,77 +173,41 @@
173 style="padding: 0 0 1rem 1rem;" 173 style="padding: 0 0 1rem 1rem;"
174 @change="onSearchResultChange" 174 @change="onSearchResultChange"
175 > 175 >
176 - <div> 176 + <div v-for="group in searchGroupList" :key="group.type">
177 - <p style="font-weight: bold;">部门</p> 177 + <p :style="{ fontWeight: group.type === 'dept' ? 'bold' : 'normal' }">{{ group.title }}</p>
178 <div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div> 178 <div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
179 - <div style="margin-bottom: 1rem;"> 179 + <div :style="{ marginBottom: '1rem', overflow: group.type === 'user' ? 'auto' : 'visible' }">
180 <van-checkbox 180 <van-checkbox
181 - @click="onSearchResultClick(dept, $event)" 181 + @click="onSearchResultClick(option, $event)"
182 - v-for="(dept) in user_dept_role.dept" 182 + v-for="(option) in user_dept_role[group.type]"
183 - :id="dept.id" 183 + :id="option.id"
184 - :type="dept.type" 184 + :type="option.type"
185 - :text="dept.name" 185 + :text="option.name"
186 - :key="dept.id" 186 + :key="option.id"
187 - :name="dept.id" 187 + :name="option.id"
188 - shape="square" icon-size="13px"
189 - :checked-color="styleColor.baseColor"
190 - :disabled="dept.disabled"
191 - style="margin-bottom: 0.5rem;">
192 - {{ dept.name }}
193 - </van-checkbox>
194 - <div v-if="!user_dept_role.dept.length" style="color: #999;">暂无数据</div>
195 - </div>
196 - </div>
197 - <div>
198 - <p>角色</p>
199 - <div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
200 - <div style="margin-bottom: 1rem;">
201 - <van-checkbox
202 - @click="onSearchResultClick(role, $event)"
203 - v-for="(role) in user_dept_role.role"
204 - :id="role.id"
205 - :type="role.type"
206 - :text="role.name"
207 - :key="role.id"
208 - :name="role.id"
209 shape="square" 188 shape="square"
210 icon-size="13px" 189 icon-size="13px"
211 :checked-color="styleColor.baseColor" 190 :checked-color="styleColor.baseColor"
212 - :disabled="role.disabled" 191 + :disabled="option.disabled"
213 - style="margin-bottom: 0.8rem;"> 192 + :style="{ marginBottom: group.type === 'role' ? '0.8rem' : '0.5rem' }">
214 - {{ role.name }} 193 + <div v-if="group.type === 'user'" class="van-ellipsis" :style="{ maxWidth: maxWidth + 'px' }">
215 - </van-checkbox> 194 + <span>{{ option.name }}</span>
216 - <div v-if="!user_dept_role.role.length" style="color: #999;">暂无数据</div> 195 + <span v-if="option.role_list.length">/
217 - </div> 196 + <span v-for="(role, index) in option?.role_list" :key="role.id">
218 - </div> 197 + {{ role.name }}<span v-if="index !== (option?.role_list?.length - 1)">,</span>
219 - <div> 198 + </span>
220 - <p>成员</p>
221 - <div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
222 - <div style="margin-bottom: 1rem; overflow: auto;">
223 - <van-checkbox
224 - @click="onSearchResultClick(user, $event)"
225 - v-for="(user) in user_dept_role.user"
226 - :id="user.id"
227 - :type="user.type"
228 - :text="user.name"
229 - :key="user.id"
230 - :name="user.id"
231 - shape="square"
232 - icon-size="13px"
233 - :checked-color="styleColor.baseColor"
234 - :disabled="user.disabled"
235 - style="margin-bottom: 0.5rem;">
236 - <div class="van-ellipsis" :style="{ maxWidth: maxWidth + 'px' }">
237 - <span>{{ user.name }}</span>
238 - <span v-if="user.role_list.length">/
239 - <span v-for="(role, index) in user?.role_list" :key="role.id">{{ role.name }}&nbsp;</span>
240 </span> 199 </span>
241 - <span v-if="user?.dept_list.length">/ 200 + <span v-if="option?.dept_list.length">/
242 - <span v-for="(dept, index) in user?.dept_list" :key="dept.id">{{ dept.name }}&nbsp;</span> 201 + <span v-for="(dept, index) in option?.dept_list" :key="dept.id">
202 + {{ dept.name }}<span v-if="index !== (option?.dept_list?.length - 1)">,</span>
203 + </span>
243 </span> 204 </span>
244 </div> 205 </div>
206 + <template v-else>
207 + {{ option.name }}
208 + </template>
245 </van-checkbox> 209 </van-checkbox>
246 - <div v-if="!user_dept_role.user.length" style="color: #999;">暂无数据</div> 210 + <div v-if="!user_dept_role[group.type].length" style="color: #999;">暂无数据</div>
247 </div> 211 </div>
248 </div> 212 </div>
249 </van-checkbox-group> 213 </van-checkbox-group>
...@@ -299,32 +263,53 @@ let dept_list = []; ...@@ -299,32 +263,53 @@ let dept_list = [];
299 263
300 let check_type = ref(''); // 单选/多选模式 264 let check_type = ref(''); // 单选/多选模式
301 265
302 -// 处理Tab显示问题 266 +const ORG_TYPE_FALLBACK_MAP = {
303 -// TODO:等待后台数据 267 + dept: '部门',
304 -const tree_tabs = ['dept', 'role', 'user']; 268 + role: '角色',
305 -const tabList = computed(() => { 269 + user: '用户'
306 - let arr = []; 270 +};
307 - if (tree_tabs.indexOf('dept') !== -1) { 271 +const ALL_ORG_TYPES = ['dept', 'role', 'user'];
308 - arr.push({ 272 +
309 - title: '组织结构', 273 +/**
310 - name: 0 274 + * 按后端字段配置生成可见类型。
311 - }) 275 + * 后端这里固定返回单个 org_type 字符串;未配置时回退为全部,兼容旧表单。
312 - } 276 + */
313 - if (tree_tabs.indexOf('role') !== -1) { 277 +const orgTypeConfigList = computed(() => {
314 - arr.push({ 278 + const orgType = String(props.component_props.org_type || '').trim();
315 - title: '角色', 279 + const orgTypeTitle = String(props.component_props.org_type_title || '').trim();
316 - name: 1 280 +
317 - }) 281 + if (ALL_ORG_TYPES.includes(orgType)) {
318 - } 282 + return [{
319 - if (tree_tabs.indexOf('user') !== -1) { 283 + type: orgType,
320 - arr.push({ 284 + title: orgTypeTitle || ORG_TYPE_FALLBACK_MAP[orgType],
321 - title: '成员', 285 + }];
322 - name: 2
323 - })
324 } 286 }
325 - return arr; 287 +
288 + return ALL_ORG_TYPES.map((type) => ({
289 + type,
290 + title: ORG_TYPE_FALLBACK_MAP[type],
291 + }));
326 }); 292 });
327 293
294 +const enabledOrgTypes = computed(() => orgTypeConfigList.value.map(item => item.type));
295 +
296 +const tabList = computed(() => {
297 + return orgTypeConfigList.value.map((item) => ({
298 + title: item.title,
299 + name: item.type
300 + }));
301 +});
302 +
303 +const searchGroupList = computed(() => {
304 + return orgTypeConfigList.value.map((item) => ({
305 + type: item.type,
306 + title: item.title
307 + }));
308 +});
309 +
310 +const isTabVisible = (type) => enabledOrgTypes.value.includes(type);
311 +const getDefaultTabActive = () => tabList.value[0]?.name || 'dept';
312 +
328 const maxWidth = ref(0); 313 const maxWidth = ref(0);
329 314
330 /** 315 /**
...@@ -396,6 +381,9 @@ const mountedLogic = async () => { ...@@ -396,6 +381,9 @@ const mountedLogic = async () => {
396 // 381 //
397 if (props.value) { 382 if (props.value) {
398 props.value.forEach(item => { 383 props.value.forEach(item => {
384 + if (!enabledOrgTypes.value.includes(item.type)) {
385 + return;
386 + }
399 if (item.type === 'dept') { 387 if (item.type === 'dept') {
400 emitCheckedGroup.value.dept.push(item); 388 emitCheckedGroup.value.dept.push(item);
401 } else if (item.type === 'role') { 389 } else if (item.type === 'role') {
...@@ -425,7 +413,8 @@ const openTree = async () => { // 点击组件展示框回调 ...@@ -425,7 +413,8 @@ const openTree = async () => { // 点击组件展示框回调
425 // 获取数据 413 // 获取数据
426 nextTick(() => { 414 nextTick(() => {
427 // 动态判断点击显示的tab,默认点击第一个 415 // 动态判断点击显示的tab,默认点击第一个
428 - onClickTab({ title: tabList.value[0]['title'] }) 416 + tabActive.value = getDefaultTabActive();
417 + onClickTab({ name: tabActive.value })
429 // getDeptTreeData(); 418 // getDeptTreeData();
430 setTimeout(() => { // 延时处理,防止数据未加载完就执行 419 setTimeout(() => { // 延时处理,防止数据未加载完就执行
431 // 获取已选择的数据 420 // 获取已选择的数据
...@@ -493,6 +482,9 @@ const tree_select_value = ref([]); ...@@ -493,6 +482,9 @@ const tree_select_value = ref([]);
493 const showPopover = ref(false); // 显示/隐藏弹框 482 const showPopover = ref(false); // 显示/隐藏弹框
494 483
495 const onCancelClick = () => { // 取消操作 484 const onCancelClick = () => { // 取消操作
485 + if (is_search.value) {
486 + onCloseSearch();
487 + }
496 showPopover.value = false; 488 showPopover.value = false;
497 } 489 }
498 490
...@@ -594,7 +586,7 @@ const onCloseSearch = () => { // 点击搜索关闭按钮回调 ...@@ -594,7 +586,7 @@ const onCloseSearch = () => { // 点击搜索关闭按钮回调
594 role: [], 586 role: [],
595 user: [] 587 user: []
596 }; // 清空搜索结果 588 }; // 清空搜索结果
597 - tabActive.value = 0; // 默认选中组织结构 589 + tabActive.value = getDefaultTabActive(); // 默认选中第一个可见类型
598 is_search.value = false; // 关闭搜索状态 590 is_search.value = false; // 关闭搜索状态
599 syncResultToList(); // 同步勾选状态 591 syncResultToList(); // 同步勾选状态
600 } 592 }
...@@ -669,18 +661,18 @@ const onRemoveUserTag = (user) => { // 移除成员标签 ...@@ -669,18 +661,18 @@ const onRemoveUserTag = (user) => { // 移除成员标签
669 661
670 /*************** Tab 功能模块 ****************/ 662 /*************** Tab 功能模块 ****************/
671 const tabRef = ref(null); 663 const tabRef = ref(null);
672 -const tabActive = ref(0); 664 +const tabActive = ref(getDefaultTabActive());
673 const deptTreeRef = ref(); 665 const deptTreeRef = ref();
674 666
675 -const onClickTab = ({ title }) => { // tab点击事件 667 +const onClickTab = ({ name }) => { // tab点击事件
676 nextTick(() => { 668 nextTick(() => {
677 - if (title === '组织结构') { 669 + if (name === 'dept') {
678 deptListReset(); 670 deptListReset();
679 } 671 }
680 - if (title === '角色') { 672 + if (name === 'role') {
681 roleListReset(); 673 roleListReset();
682 } 674 }
683 - if (title === '成员') { 675 + if (name === 'user') {
684 userListReset(); 676 userListReset();
685 // 树形结构底部高度可视度 677 // 树形结构底部高度可视度
686 if (!$('#userTree').find('.tree-placeholder').length) { 678 if (!$('#userTree').find('.tree-placeholder').length) {
......