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

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

1. 组件概述

1.1 组件名称

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

1.2 组件功能

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

1.3 设计理念

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

2. 组件结构设计

2.1 目录结构

bash 复制代码
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 基础类型

typescript 复制代码
// SelectPersonDept.types.ts

// 选择项类型
export interface SelectPersonDeptProps<T extends string = 'user' | 'role' | 'dept' | 'group' | 'quick_search'> {
  tabs?: Array<{
    tab: T;
    tab_name: string;
    uniKey: string;
    [key: string]: any;
  }>;
  // 其他props保持不变
}

// 标签配置
export interface TabConfig {
  tab: SelectItemType;
  tab_name: string;
  interface?: 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[];
  quickContacts: UserInfo[];
}


// 组件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': [];                                 // 关闭组件
}
js 复制代码
<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),提前加载下一页数据

  • 示例代码:

    ini 复制代码
    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个字符)

  • 搜索结果支持分页加载

  • 示例代码:

    ini 复制代码
    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-model SelectedResult - 双向绑定的已选结果
v-model:visible boolean false 控制组件显示/隐藏
tabs TabConfig[] 见默认配置 标签配置,包含快速查找项
multiple boolean true 是否支持多选,true=多选,false=单选
placeholder string '请输入搜索关键词' 搜索框占位符
showSelectedList boolean true 是否显示已选列表
showConfirmBtn boolean true 是否显示确认按钮
confirmBtnText string '确认' 确认按钮文本
cancelBtnText string '取消' 取消按钮文本
currentList SelectOption[] [] 当前列表数据
loading boolean false 加载状态
lowcodeStoreId string '' 低代码存储ID
customUserList UserInfo[] [] 自定义用户列表

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. 修复测试中发现的问题 | 中 | 8 | | 中 | 8 |
| 第12天 | 所有旧组件替换并调试 | 1. 接口联调 2. 重构原有功能点 | 中 | 8 |
| 第13天 | 所有旧组件替换并调试 | 1. 接口联调 2. 重构原有功能点 | 中 | 8 |
| 第14天 | 所有旧组件替换并调试 | 1. 测试移动端适配 2. 修复测试中发现的问题 | 中 | 8 |

8. 总结

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

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

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

相关推荐
Jingyou31 分钟前
JavaScript 封装无感 token 刷新
前端·javascript
quan263134 分钟前
20251204,vue列表实现自定义筛选和列
前端·vue.js·elementui
蜗牛攻城狮35 分钟前
JavaScript `Array.prototype.reduce()` 的妙用:不只是求和!
前端·javascript·数组
一入程序无退路40 分钟前
若依框架导出显示中文,而不是数字
java·服务器·前端
m0_6265352043 分钟前
代码分析 关于看图像是否包括损坏
java·前端·javascript
wangbing112543 分钟前
layer.open打开的jsf页面刷新问题
前端
Mintopia44 分钟前
🌏 父子组件 i18n(国际化)架构设计方案
前端·架构·前端工程化
WebGISer_白茶乌龙桃44 分钟前
前端又要凉了吗
前端·javascript·vue.js·js
小飞侠在吗1 小时前
vue2 watch 和vue3 watch 的区别
前端·javascript·vue.js