hookehuyr

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

- 将硬编码的标签页改为基于组件属性动态配置的模式,支持通过org_type和org_type_title控制显示的标签类型
- 调整标签状态管理,使用类型字符串替代数字索引作为标签标识,修复标题本地化导致的标签切换异常
- 优化搜索结果区域渲染逻辑,使用循环复用代码减少重复编写
- 修复标签切换、默认激活页及搜索关闭后的状态处理逻辑
- 新增标签可见性校验,过滤无效的已选数据
- 为van-tabs新增shrink属性优化布局展示
- 移除组件中过时的TODO注释
<!--
* @Date: 2022-08-29 14:31:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2024-11-25 15:34:53
* @LastEditTime: 2026-05-27 17:53:57
* @FilePath: /data-table/src/components/OrgPickerField/MyComponent.vue
* @Description: 树形组件
-->
......@@ -69,14 +69,14 @@
</div>
<div v-show="!is_search" class="tab-tree-container">
<van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" style="margin-bottom: 1rem;">
<van-tabs ref="tabRef" :color="styleColor.baseColor" v-model:active="tabActive" @click-tab="onClickTab" shrink style="margin-bottom: 1rem;">
<!-- <van-tab title="组织结构" :name="0"></van-tab>
<van-tab title="角色" :name="1"></van-tab>
<van-tab title="成员" :name="2"></van-tab> -->
<van-tab v-for="(tab, index) in tabList" :key="index" :title="tab.title" :name="tab.name"></van-tab>
</van-tabs>
<div v-show="tabActive === 0" style="padding: 0 0 0 1rem;">
<div v-if="isTabVisible('dept')" v-show="tabActive === 'dept'" style="padding: 0 0 0 1rem;">
<Vtree
id="deptTree"
ref="deptTreeRef"
......@@ -99,7 +99,7 @@
</Vtree>
</div>
<div v-if="tabActive === 1" style="padding: 0 0 0 1rem; overflow: scroll; height: 60vh;" @click.stop>
<div v-if="isTabVisible('role') && tabActive === 'role'" style="padding: 0 0 0 1rem; overflow: scroll; height: 60vh;" @click.stop>
<van-checkbox-group
v-model="role_checked"
@change="roleChangeMethod"
......@@ -121,7 +121,7 @@
<div style="height: 10vh;"></div>
</div>
<div v-if="tabActive === 2" style="padding: 0 0 0 1rem;">
<div v-if="isTabVisible('user') && tabActive === 'user'" style="padding: 0 0 0 1rem;">
<van-row gutter="">
<van-col span="10" style="border-right: 1px solid #eee; height: 60vh; overflow: scroll;">
<Vtree
......@@ -173,77 +173,41 @@
style="padding: 0 0 1rem 1rem;"
@change="onSearchResultChange"
>
<div>
<p style="font-weight: bold;">部门</p>
<div v-for="group in searchGroupList" :key="group.type">
<p :style="{ fontWeight: group.type === 'dept' ? 'bold' : 'normal' }">{{ group.title }}</p>
<div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
<div style="margin-bottom: 1rem;">
<div :style="{ marginBottom: '1rem', overflow: group.type === 'user' ? 'auto' : 'visible' }">
<van-checkbox
@click="onSearchResultClick(dept, $event)"
v-for="(dept) in user_dept_role.dept"
:id="dept.id"
:type="dept.type"
:text="dept.name"
:key="dept.id"
:name="dept.id"
shape="square" icon-size="13px"
:checked-color="styleColor.baseColor"
:disabled="dept.disabled"
style="margin-bottom: 0.5rem;">
{{ dept.name }}
</van-checkbox>
<div v-if="!user_dept_role.dept.length" style="color: #999;">暂无数据</div>
</div>
</div>
<div>
<p>角色</p>
<div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
<div style="margin-bottom: 1rem;">
<van-checkbox
@click="onSearchResultClick(role, $event)"
v-for="(role) in user_dept_role.role"
:id="role.id"
:type="role.type"
:text="role.name"
:key="role.id"
:name="role.id"
@click="onSearchResultClick(option, $event)"
v-for="(option) in user_dept_role[group.type]"
:id="option.id"
:type="option.type"
:text="option.name"
:key="option.id"
:name="option.id"
shape="square"
icon-size="13px"
:checked-color="styleColor.baseColor"
:disabled="role.disabled"
style="margin-bottom: 0.8rem;">
{{ role.name }}
</van-checkbox>
<div v-if="!user_dept_role.role.length" style="color: #999;">暂无数据</div>
</div>
</div>
<div>
<p>成员</p>
<div style="border-top: 1px solid #eee; margin: 0.5rem 0;"></div>
<div style="margin-bottom: 1rem; overflow: auto;">
<van-checkbox
@click="onSearchResultClick(user, $event)"
v-for="(user) in user_dept_role.user"
:id="user.id"
:type="user.type"
:text="user.name"
:key="user.id"
:name="user.id"
shape="square"
icon-size="13px"
:checked-color="styleColor.baseColor"
:disabled="user.disabled"
style="margin-bottom: 0.5rem;">
<div class="van-ellipsis" :style="{ maxWidth: maxWidth + 'px' }">
<span>{{ user.name }}</span>
<span v-if="user.role_list.length">/
<span v-for="(role, index) in user?.role_list" :key="role.id">{{ role.name }}&nbsp;</span>
:disabled="option.disabled"
:style="{ marginBottom: group.type === 'role' ? '0.8rem' : '0.5rem' }">
<div v-if="group.type === 'user'" class="van-ellipsis" :style="{ maxWidth: maxWidth + 'px' }">
<span>{{ option.name }}</span>
<span v-if="option.role_list.length">/
<span v-for="(role, index) in option?.role_list" :key="role.id">
{{ role.name }}<span v-if="index !== (option?.role_list?.length - 1)">,</span>
</span>
</span>
<span v-if="option?.dept_list.length">/
<span v-for="(dept, index) in option?.dept_list" :key="dept.id">
{{ dept.name }}<span v-if="index !== (option?.dept_list?.length - 1)">,</span>
</span>
<span v-if="user?.dept_list.length">/
<span v-for="(dept, index) in user?.dept_list" :key="dept.id">{{ dept.name }}&nbsp;</span>
</span>
</div>
<template v-else>
{{ option.name }}
</template>
</van-checkbox>
<div v-if="!user_dept_role.user.length" style="color: #999;">暂无数据</div>
<div v-if="!user_dept_role[group.type].length" style="color: #999;">暂无数据</div>
</div>
</div>
</van-checkbox-group>
......@@ -299,32 +263,53 @@ let dept_list = [];
let check_type = ref(''); // 单选/多选模式
// 处理Tab显示问题
// TODO:等待后台数据
const tree_tabs = ['dept', 'role', 'user'];
const tabList = computed(() => {
let arr = [];
if (tree_tabs.indexOf('dept') !== -1) {
arr.push({
title: '组织结构',
name: 0
})
}
if (tree_tabs.indexOf('role') !== -1) {
arr.push({
title: '角色',
name: 1
})
}
if (tree_tabs.indexOf('user') !== -1) {
arr.push({
title: '成员',
name: 2
})
const ORG_TYPE_FALLBACK_MAP = {
dept: '部门',
role: '角色',
user: '用户'
};
const ALL_ORG_TYPES = ['dept', 'role', 'user'];
/**
* 按后端字段配置生成可见类型。
* 后端这里固定返回单个 org_type 字符串;未配置时回退为全部,兼容旧表单。
*/
const orgTypeConfigList = computed(() => {
const orgType = String(props.component_props.org_type || '').trim();
const orgTypeTitle = String(props.component_props.org_type_title || '').trim();
if (ALL_ORG_TYPES.includes(orgType)) {
return [{
type: orgType,
title: orgTypeTitle || ORG_TYPE_FALLBACK_MAP[orgType],
}];
}
return arr;
return ALL_ORG_TYPES.map((type) => ({
type,
title: ORG_TYPE_FALLBACK_MAP[type],
}));
});
const enabledOrgTypes = computed(() => orgTypeConfigList.value.map(item => item.type));
const tabList = computed(() => {
return orgTypeConfigList.value.map((item) => ({
title: item.title,
name: item.type
}));
});
const searchGroupList = computed(() => {
return orgTypeConfigList.value.map((item) => ({
type: item.type,
title: item.title
}));
});
const isTabVisible = (type) => enabledOrgTypes.value.includes(type);
const getDefaultTabActive = () => tabList.value[0]?.name || 'dept';
const maxWidth = ref(0);
/**
......@@ -396,6 +381,9 @@ const mountedLogic = async () => {
//
if (props.value) {
props.value.forEach(item => {
if (!enabledOrgTypes.value.includes(item.type)) {
return;
}
if (item.type === 'dept') {
emitCheckedGroup.value.dept.push(item);
} else if (item.type === 'role') {
......@@ -425,7 +413,8 @@ const openTree = async () => { // 点击组件展示框回调
// 获取数据
nextTick(() => {
// 动态判断点击显示的tab,默认点击第一个
onClickTab({ title: tabList.value[0]['title'] })
tabActive.value = getDefaultTabActive();
onClickTab({ name: tabActive.value })
// getDeptTreeData();
setTimeout(() => { // 延时处理,防止数据未加载完就执行
// 获取已选择的数据
......@@ -493,6 +482,9 @@ const tree_select_value = ref([]);
const showPopover = ref(false); // 显示/隐藏弹框
const onCancelClick = () => { // 取消操作
if (is_search.value) {
onCloseSearch();
}
showPopover.value = false;
}
......@@ -594,7 +586,7 @@ const onCloseSearch = () => { // 点击搜索关闭按钮回调
role: [],
user: []
}; // 清空搜索结果
tabActive.value = 0; // 默认选中组织结构
tabActive.value = getDefaultTabActive(); // 默认选中第一个可见类型
is_search.value = false; // 关闭搜索状态
syncResultToList(); // 同步勾选状态
}
......@@ -669,18 +661,18 @@ const onRemoveUserTag = (user) => { // 移除成员标签
/*************** Tab 功能模块 ****************/
const tabRef = ref(null);
const tabActive = ref(0);
const tabActive = ref(getDefaultTabActive());
const deptTreeRef = ref();
const onClickTab = ({ title }) => { // tab点击事件
const onClickTab = ({ name }) => { // tab点击事件
nextTick(() => {
if (title === '组织结构') {
if (name === 'dept') {
deptListReset();
}
if (title === '角色') {
if (name === 'role') {
roleListReset();
}
if (title === '成员') {
if (name === 'user') {
userListReset();
// 树形结构底部高度可视度
if (!$('#userTree').find('.tree-placeholder').length) {
......