基于Vant的移动端公共选人/选部门组件设计文档

164 阅读15分钟

基于Vant的移动端公共选人/选部门组件设计文档

1. 组件概述

1.1 组件名称

SelectPersonDeptMobile - 基于Vant的移动端公共选人/选部门组件

1.2 组件功能

  • 支持选择用户、角色、组织架构、群组、快速查找
  • 支持多选/单选模式(通过multiple布尔值控制)
  • 支持搜索功能
  • 支持平铺列表展示,通过下级按钮加载新数据
  • 支持已选人员/部门展示与管理
  • 移动端友好的交互设计
  • 基于Vant组件库实现
  • 支持自定义选择类型

1.3 设计理念

  • 模块化设计,便于复用
  • 基于Vant组件库,保证移动端体验一致性
  • 支持灵活配置,适应不同业务场景
  • 类型安全,使用TypeScript确保类型正确
  • 良好的用户体验,流畅的交互效果
  • 轻量级设计,性能优化
  • 纯UI组件设计,与数据逻辑分离

2. 组件结构设计

2.1 目录结构

components/
└── SelectPersonDeptMobile/
    ├── SelectPersonDeptMobile.vue    # 主组件
    ├── SelectPersonDept.types.ts     # 类型定义文件
    ├── components/                   # 子组件
    │   ├── SearchBar.vue             # 搜索栏组件(基于Vant Search)
    │   ├── TabNav.vue                # 标签导航组件(基于Vant Tabs)
    │   ├── SelectList.vue            # 平铺列表组件(基于Vant List/Cell)
    │   └── SelectedList.vue          # 已选列表组件(基于Vant Tag/Cell)
    └── utils/                        # 工具函数
        └── helper.ts                 # 辅助函数

3. 类型定义

3.1 基础类型

// SelectPersonDept.types.ts

// 选择项类型 - 支持灵活扩展
export type SelectItemType = 'user' | 'role' | 'dept' | 'group' | 'quick_search' | string;

// 标签配置
export interface TabConfig {
  tab: SelectItemType;
  tab_name: string;
  uniKey: string;
  [key: string]: any;
}

// 用户信息
export interface UserInfo {
  userId: string;
  userName: string;
  deptName?: string;
  userCode: string;
  [key: string]: any;
}

// 部门信息
export interface DeptInfo {
  deptId: string;
  deptName: string;
  parentId?: string;
  hasChildren?: boolean; // 是否有下级数据
  [key: string]: any;
}

// 角色信息
export interface RoleInfo {
  roleId: string;
  roleName: string;
  [key: string]: any;
}

// 群组信息
export interface GroupInfo {
  groupId: string;
  groupName: string;
  [key: string]: any;
}

// 通用选择项(适配Vant组件)
export interface SelectOption {
  id: string;            // 对应Vant的value
  name: string;          // 对应Vant的text
  type: SelectItemType;
  hasChildren?: boolean; // 是否有下级数据
  disabled?: boolean;    // 是否禁用
  [key: string]: any;
}


// 已选结果 - 支持自定义扩展
export interface SelectedResult {
  users: UserInfo[];
  depts: DeptInfo[];
  roles: RoleInfo[];
  groups: GroupInfo[];
  [key: string]: Array<{ id: string; name: string; [key: string]: any }>;
}

// 泛型版本(推荐用于高度自定义场景)
export interface SelectedResult<T extends string = 'users' | 'depts' | 'roles' | 'groups'> {
  [key in T]: Array<{ id: string; name: string; [key: string]: any }>;
}

// 类型安全的自定义扩展示例
export interface CustomSelectedResult extends SelectedResult {
  vendors: Array<{ id: string; name: string; company: string; [key: string]: any }>;
  partners: Array<{ id: string; name: string; type: string; [key: string]: any }>;
}

// 组件Props
export interface SelectPersonDeptProps {
  modelValue: SelectedResult;       // 已选结果(用于回显和双向绑定)
  visible?: boolean;               // 组件显示状态
  tabs?: TabConfig[];              // 标签配置
  multiple?: boolean;              // 是否多选,true=多选,false=单选
  placeholder?: string;            // 搜索框占位符
  showSelectedList?: boolean;      // 是否显示已选列表
  showConfirmBtn?: boolean;        // 是否显示确认按钮
  confirmBtnText?: string;         // 确认按钮文本
  cancelBtnText?: string;          // 取消按钮文本
  currentList?: SelectOption[];    // 当前列表数据
  loading?: boolean;               // 加载状态
  customUserList?: UserInfo[];     // 自定义用户列表

}

// 组件事件
export interface SelectPersonDeptEmits {
  'update:modelValue': [value: SelectedResult]; // 已选结果变化
  'update:visible': [visible: boolean];         // 组件显示状态变化
  'confirm': [value: SelectedResult];           // 确认选择
  'cancel': [];                                // 取消选择
  'search': [keyword: string];                  // 搜索
  'tab-change': [tab: SelectItemType];         // 标签切换
  'next-level': [item: SelectOption];          // 点击下级按钮
  'back-level': [breadcrumb: Array<{ name: string; id: string; type: SelectItemType }>]; // 返回上一级
  'load-data': [tab: SelectItemType, params?: any]; // 请求加载数据
  'load-quick-search': [];                     // 请求加载快速查找数据
  'select-all': [checked: boolean];            // 全选/取消全选
  'close': [];                                 // 关闭组件
}

<template>
  <SelectPersonDept<MyCustomType>
    :tabs="customTabs"
    @tab-change="handleTabChange"
  />
</template>

<script setup lang="ts">
type MyCustomType = 'user1' | 'user2' | 'dept' | 'role';

const customTabs = [
  { tab: 'user1', tab_name: '普通用户', uniKey: 'user1' },
  { tab: 'user2', tab_name: 'VIP用户', uniKey: 'user2' },
  { tab: 'dept', tab_name: '部门', uniKey: 'dept' },
  { tab: 'role', tab_name: '角色', uniKey: 'role' }
] as const;

const handleTabChange = (tab: MyCustomType) => {
  // 获得完整的类型提示
  console.log('Tab changed:', tab);
};
</script>

4. 移动端纯UI组件设计(SelectPersonDeptMobile)

4.1 组件定位

  • 纯UI组件:仅负责界面渲染和用户交互
  • 数据驱动:所有数据由父组件通过props传入
  • 事件驱动:通过事件与父组件通信,触发数据获取等操作
  • 可复用:适用于各种需要选人/选部门的移动端场景

4.2 核心设计原则

  • UI与数据分离:组件不包含任何数据获取逻辑
  • 事件驱动通信:通过事件通知父组件执行数据操作
  • 单向数据流:数据从父组件流向子组件,变化通过事件反馈
  • 灵活配置:支持通过props自定义各种行为和样式

5. 数据获取渲染流程

5.1 完整流程概览

sequenceDiagram
    participant 父组件
    participant 纯UI组件
    participant API接口
    
    %% 0. 回显数据初始化(新增)
    父组件->>父组件: A. 准备初始已选数据(回显数据)
    父组件->>纯UI组件: B. 传入初始modelValue(已选数据)
    
    %% 1. 组件初始化与显示
    父组件->>纯UI组件: 1. 设置 visible=true
    纯UI组件->>纯UI组件: 2. 组件渲染显示
    
    %% 2. 初始数据加载
    父组件->>API接口: 3. 调用默认标签数据接口
    API接口-->>父组件: 4. 返回列表数据
    父组件->>纯UI组件: 5. 更新 currentList props
    纯UI组件->>纯UI组件: 6. 渲染列表数据并根据本地选中状态标记已选项
    纯UI组件->>纯UI组件: 7. 根据modelValue更新本地选中状态(回显处理)
    
    %% 3. 用户交互:标签切换
    纯UI组件->>父组件: 8. 触发 tab-change 事件 (新标签)
    父组件->>API接口: 9. 调用新标签数据接口
    API接口-->>父组件: 10. 返回新标签数据
    父组件->>纯UI组件: 11. 更新 currentList props
    纯UI组件->>纯UI组件: 12. 渲染新标签数据并标记已选项
    
    %% 4. 用户交互:搜索
    纯UI组件->>父组件: 19. 触发 search 事件 (关键词)
    父组件->>API接口: 20. 调用搜索接口
    API接口-->>父组件: 21. 返回搜索结果
    父组件->>纯UI组件: 22. 更新 currentList props
    纯UI组件->>纯UI组件: 23. 渲染搜索结果并标记已选项
    
    %% 5. 用户交互:点击下级按钮
    纯UI组件->>父组件: 24. 触发 next-level 事件 (item)
    父组件->>API接口: 25. 调用下级数据接口
    API接口-->>父组件: 26. 返回下级数据
    父组件->>纯UI组件: 27. 更新 currentList props
    纯UI组件->>纯UI组件: 28. 渲染下级数据并标记已选项
    
    %% 6. 用户交互:返回上一级
    纯UI组件->>父组件: 29. 触发 back-level 事件 (breadcrumb)
    父组件->>API接口: 30. 调用上一级数据接口
    API接口-->>父组件: 31. 返回上一级数据
    父组件->>纯UI组件: 32. 更新 currentList props
    纯UI组件->>纯UI组件: 33. 渲染上一级数据并标记已选项
    
    %% 7. 用户交互:选择项
    纯UI组件->>父组件: 34. 触发 select 事件 (item, checked)
    父组件->>父组件: 35. 处理选择逻辑,更新已选结果
    父组件->>纯UI组件: 36. 更新 modelValue props
    纯UI组件->>纯UI组件: 37. 更新本地选中状态和已选列表显示
    
    %% 8. 组件关闭
    纯UI组件->>父组件: 38. 触发 confirm/cancel 事件
    父组件->>父组件: 39. 处理最终结果
    父组件->>纯UI组件: 40. 设置 visible=false
    纯UI组件->>纯UI组件: 41. 组件隐藏

5.2 详细流程说明

5.2.0 0. 回显数据初始化(新增)

触发条件:父组件准备使用组件并需要显示已选数据时

数据流向

  1. 父组件根据业务需求准备初始已选数据(回显数据)
  2. 父组件将初始已选数据通过 modelValue props传入纯UI组件
  3. 纯UI组件在初始化时接收 modelValue

交互说明

  • 回显数据可以是来自API请求、表单数据或其他业务逻辑
  • 父组件需要确保回显数据格式符合 SelectedResult 接口定义
  • 支持空回显数据(初始未选择任何项)
5.2.1 1. 组件初始化与显示

触发条件:父组件需要打开选人组件时

数据流向

  1. 父组件通过设置 visible=true 打开组件
  2. 纯UI组件渲染并显示
  3. 纯UI组件根据传入的 modelValue 更新本地选中状态(回显处理核心逻辑)

交互说明

  • 组件打开时自动处理回显数据,并同步选中状态
  • 支持通过props预设初始数据,避免白屏
5.2.2 2. 初始数据加载

触发条件:组件打开后

数据流向

  1. 父组件根据事件类型调用对应API接口
  2. API返回数据后,父组件通过props更新到纯UI组件
  3. 纯UI组件接收到新数据后渲染
  4. 纯UI组件根据本地保存的选中状态,自动标记列表中的已选项(回显显示)

交互说明

  • 父组件负责处理API调用和数据转换
  • 支持设置 loading 状态,显示加载动画
  • 支持处理空数据
  • 数据加载完成后自动显示已选项标记,实现回显效果
5.2.3 3. 标签切换数据加载

触发条件:用户点击不同标签时

数据流向

  1. 纯UI组件触发 tab-change 事件,携带当前标签类型
  2. 父组件根据标签类型调用对应API接口
  3. API返回数据后,父组件通过 currentList props更新
  4. 纯UI组件重新渲染对应标签的数据
  5. 纯UI组件根据本地保存的选中状态,自动标记列表中的已选项(回显显示)

交互说明

  • 标签切换时自动重置面包屑
  • 支持不同标签使用不同的API接口
  • 标签切换时显示加载状态
  • 不同标签下的数据会自动根据全局选中状态标记已选项
5.2.4 4. 搜索数据加载

触发条件:用户在搜索框输入关键词时

数据流向

  1. 纯UI组件触发 search 事件,携带搜索关键词
  2. 父组件调用搜索API接口
  3. API返回搜索结果后,父组件通过 currentList props更新
  4. 纯UI组件渲染搜索结果
  5. 纯UI组件根据本地保存的选中状态,自动标记搜索结果中的已选项(回显显示)

交互说明

  • 支持实时搜索或防抖搜索
  • 搜索结果为空时显示空状态
  • 支持清空搜索条件,返回原始列表
  • 搜索结果中会自动标记已选项,保持回显状态
5.2.5 5. 下级数据加载

触发条件:用户点击列表项右侧的"下级"按钮时

数据流向

  1. 纯UI组件触发 next-level 事件,携带当前项信息
  2. 父组件调用获取下级数据的API接口
  3. API返回下级数据后,父组件通过 currentList props更新
  4. 纯UI组件渲染下级数据
  5. 同时更新面包屑导航
  6. 纯UI组件根据本地保存的选中状态,自动标记下级数据中的已选项(回显显示)

交互说明

  • 仅当列表项 hasChildren=true 时显示"下级"按钮
  • 点击后自动更新面包屑,便于返回
  • 支持多级嵌套导航
  • 下级数据中会自动标记已选项,保持回显状态
5.2.6 6. 返回上一级数据加载

触发条件:用户点击"返回上一级"按钮或面包屑项时

数据流向

  1. 纯UI组件触发 back-level 事件,携带当前面包屑信息
  2. 父组件调用获取上一级数据的API接口
  3. API返回上一级数据后,父组件通过 currentList props更新
  4. 纯UI组件渲染上一级数据
  5. 同时更新面包屑导航
  6. 纯UI组件根据本地保存的选中状态,自动标记上一级数据中的已选项(回显显示)

交互说明

  • 支持通过面包屑直接跳转到任意层级
  • 面包屑项支持点击事件
  • 无上级数据时自动隐藏返回按钮
  • 上一级数据中会自动标记已选项,保持回显状态
5.2.7 7. 选择项处理

触发条件:用户点击选择框(Checkbox/Radio)时

数据流向

  1. 纯UI组件触发 select 事件,携带选择项和选中状态
  2. 父组件根据选择状态更新已选结果
  3. 父组件通过 modelValue props更新已选结果
  4. 纯UI组件更新已选列表显示

交互说明

  • 支持多选和单选模式
  • 支持全选/取消全选功能
  • 实时更新已选列表
5.2.8 8. 组件关闭

触发条件:用户点击"确认"或"取消"按钮时

数据流向

  1. 纯UI组件触发 confirmcancel 事件
  2. 父组件处理最终结果(保存或丢弃)
  3. 父组件设置 visible=false 关闭组件
  4. 纯UI组件隐藏

交互说明

  • 确认时返回最终已选结果
  • 取消时不保存任何更改
5.2.9 9. 虚拟滚动

针对大数据量(如10000+条人员数据)的情况,我从后端、前端和交互体验三个层面设计了完整的优化策略:

1. 后端层面:分页查询是基础
  • 接口必须支持分页,默认每页返回20-50条数据
  • 包含page(当前页码)、size(每页条数)和total(总条数)参数
  • 示例请求:api.loadData(tab, { page: 1, size: 30 })
  • 确保分页查询的性能,建议对查询字段建立索引
2. 前端层面:虚拟滚动是核心
  • 使用Vant的List组件实现虚拟滚动,只渲染可视区域内的列表项

  • 配置预加载偏移量(50-100px),提前加载下一页数据

  • 示例代码:

    Vue
    <van-list
      v-model:loading="loading"
      :finished="finished"
      @load="onLoad"
      :offset="50"
    >
      <van-cell
        v-for="item in currentList"
        :key="item.id"
        :title="item.name"
      >
        <!-- 列表项内容 -->
      </van-cell>
    </van-list>
    
3. 搜索优化:减少不必要的请求
  • 200-300ms防抖处理,避免频繁输入导致的多次请求

  • 搜索关键词长度限制(至少2个字符)

  • 搜索结果支持分页加载

  • 示例代码:

    TypeScript
    const debouncedSearch = debounce
    ((keyword: string) => {
      if (keyword.trim().length >= 2) {
        handleSearch(keyword);
      }
    }, 300);
    

5.3 事件与Props映射关系

事件名触发时机对应Props更新说明
tab-change标签切换currentList切换标签时触发
search搜索输入currentList搜索关键词变化时触发
next-level点击下级按钮currentList加载下级数据
back-level返回上一级currentList加载上一级数据
select选择/取消选择modelValue更新已选结果
confirm确认选择-确认最终结果
cancel取消选择-取消选择
-组件初始化modelValue父组件传入回显数据,纯UI组件处理并更新本地选中状态

5.4 父组件核心职责

  1. 数据管理

    • 调用API获取各种数据
    • 处理数据转换和格式化
    • 维护已选结果状态
    • 准备和管理回显数据(初始已选数据)
  2. 事件处理

    • 监听纯UI组件发出的所有事件
    • 根据事件类型执行对应逻辑
    • 更新组件props
    • 处理回显数据的初始化和更新
  3. 数据缓存策略

  • 使用Map数据结构缓存已加载的数据

    • 高效的查找性能 :Map的查找时间复杂度为O(1),比数组遍历更高效

    • 灵活的键类型 :可以使用复合字符串作为键,便于区分不同tab和参数组合的数据

    • 避免重复请求 :切换tab或返回上一级时,直接从缓存获取数据,减少API调用次数

    • 提升用户体验 :缓存数据可立即显示,避免加载等待,提升交互流畅度

  • 组件关闭时清空缓存

    • 避免内存泄漏 :长时间不清除缓存可能导致内存占用过高
    • 数据时效性 :确保下次打开组件时获取最新数据
    • 减少不必要的资源占用 :组件不再使用时释放相关资源
    • 避免数据不一致 :防止缓存数据与实际业务数据不一致

5.5 纯UI组件核心职责

  1. 界面渲染

    • 根据props渲染列表数据
    • 渲染已选列表
    • 渲染加载状态和空状态
    • 根据回显数据标记已选项
  2. 用户交互

    • 处理用户点击、滑动等操作
    • 维护本地临时状态(如选中状态)
    • 触发相应事件
  3. 状态同步

    • 响应props变化,更新UI
    • 保持与父组件数据一致
    • 处理回显数据:将modelValue转换为本地选中状态
    • 在数据加载完成后,根据本地选中状态标记已选项

6. 组件API

6.1 Props

参数名类型默认值说明
v-modelSelectedResult-双向绑定的已选结果
v-model:visiblebooleanfalse控制组件显示/隐藏
tabsTabConfig[]见默认配置标签配置,包含快速查找项
multiplebooleantrue是否支持多选,true=多选,false=单选
placeholderstring'请输入搜索关键词'搜索框占位符
showSelectedListbooleantrue是否显示已选列表
showConfirmBtnbooleantrue是否显示确认按钮
confirmBtnTextstring'确认'确认按钮文本
cancelBtnTextstring'取消'取消按钮文本
currentListSelectOption[][]当前列表数据
loadingbooleanfalse加载状态
lowcodeStoreIdstring''低代码存储ID
customUserListUserInfo[][]自定义用户列表

6.2 Emits

事件名类型说明
update:modelValue(value: SelectedResult) => void已选结果变化时触发
update:visible(visible: boolean) => void组件显示状态变化时触发
confirm(value: SelectedResult) => void确认选择时触发
cancel() => void取消选择时触发
search(keyword: string) => void搜索时触发,携带搜索关键词
tab-change(tab: SelectItemType) => void标签切换时触发,携带标签类型
next-level(item: SelectOption) => void点击下级按钮时触发,携带当前项信息
back-level(breadcrumb: Array<{ name: string; id: string; type: SelectItemType }>) => void返回上一级时触发,携带面包屑信息
select-all(checked: boolean) => void全选/取消全选时触发,携带选中状态
close() => void关闭组件时触发

7. 工时表

日期任务名称详细工作内容优先级计划工时(h)
第1天组件结构搭建1. 创建组件目录结构
2. 创建主组件文件
3. 搭建基础布局框架
8
第2天子组件开发(搜索栏+标签)1. 开发SearchBar搜索栏组件
2. 开发TabNav标签导航组件
3. 集成子组件到主组件
4. 实现搜索和标签切换事件
8
第3天平铺列表核心功能1. 开发SelectList平铺列表组件
2. 实现列表项渲染
3. 开发"下级"按钮功能
4. 实现面包屑导航
8
第4天选择功能实现1. 实现单选/多选逻辑
2. 开发选择状态管理
3. 实现全选/取消全选功能
4. 集成到列表组件
8
第5天已选列表开发1. 开发SelectedList已选列表组件
2. 实现已选项展示
3. 开发已选项删除功能
4. 集成到主组件
8
第6天快速查找功能1. 开发快速查找模块
4. 集成到主组件
8
第7天回显数据逻辑1. 实现modelValue双向绑定
2. 开发回显数据处理
3. 实现本地选中状态管理
8
第8天组件集成与调试1. 整合所有子组件
2. 调试组件间通信
3. 修复发现的问题
8
第9天单个旧组件替换并调试1. 接口联调
2. 重构原有功能点
8
第10天单个旧组件替换并调试1. 接口联调
2. 重构原有功能点
8
第11天单个旧组件替换并调试1. 测试移动端适配
2. 修复测试中发现的问题
88
第12天所有旧组件替换并调试1. 接口联调
2. 重构原有功能点
8
第13天所有旧组件替换并调试1. 接口联调
2. 重构原有功能点
8
第14天所有旧组件替换并调试1. 测试移动端适配
2. 修复测试中发现的问题
8

8. 总结

SelectPersonDeptMobile 是一个基于Vant的纯UI移动端公共选人/选部门组件,通过事件驱动的方式与父组件通信,实现了UI与数据逻辑的分离。组件支持多种选择类型、搜索功能、多级嵌套导航和灵活的配置选项,能够适应各种业务场景。

通过清晰的数据流向和交互流程设计,组件实现了高效的数据流管理和良好的用户体验。父组件负责数据获取和业务逻辑,纯UI组件负责界面渲染和用户交互,两者分工明确,便于维护和扩展。

这种设计模式使得组件具有良好的可复用性和可扩展性,能够快速适配不同的业务需求,是一个可靠的移动端公共选人/选部门组件解决方案。