Skip to content

ContactList 联系人列表 - Vant 4

📋 ContactList 联系人列表

📇 介绍

ContactList 组件就像你的私人通讯录管家,优雅地展示着每一位联系人的信息!✨ 它不仅能让你轻松浏览联系人列表,还贴心地提供了选择、编辑和新增功能。就像翻阅一本精美的通讯录,每一页都整齐有序,让联系人管理变得如此简单而愉悦!无论是商务场合还是日常使用,它都能让你的联系人管理井井有条,一目了然。

📦 引入

通过以下方式来全局注册组件,更多注册方式请参考组件注册

js
import { createApp } from'vue'; import { ContactList } from'vant'; const app = createApp(); app.use(ContactList);

🎯 代码演示

🔧 基础用法 - 打造你的专属通讯录

通过简单的配置,就能创建一个功能完整的联系人列表!支持选择联系人、编辑信息和添加新联系人,让通讯录管理变得轻松愉快。

html
js
import { ref } from'vue'; import { showToast } from'vant'; exportdefault { setup() { const chosenContactId = ref('1'); const list = ref([ { id: '1', name: '张三', tel: '13000000000', isDefault: true, }, { id: '2', name: '李四', tel: '1310000000', }, ]); constonAdd = () => showToast('新增'); constonEdit = (contact) => showToast('编辑' + contact.id); constonSelect = (contact) => showToast('选择' + contact.id); return { list, onAdd, onEdit, onSelect, chosenContactId, }; }, };

📖 API

Props

参数说明类型默认值
v-model当前选中联系人的 id*numberstring*
list联系人列表ContactListItem[][]
add-text新建按钮文案string新建联系人
default-tag-text默认联系人标签文案string-

Events

事件名说明回调参数
add点击新增按钮时触发-
edit点击编辑按钮时触发contact: ContactListItem,index: number
select切换选中的联系人时触发contact: ContactListItem,index: number

ContactListItem 数据结构

键名说明类型
id每位联系人的唯一标识*number
name联系人姓名string
tel联系人手机号*number
isDefault是否为默认联系人*boolean

类型定义

组件导出以下类型定义:

ts
importtype { ContactListItem, ContactListProps } from'vant';

🎨 主题定制

样式变量

组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 ConfigProvider 组件

名称默认值描述
--van-contact-list-paddingvar(--van-padding-sm) var(--van-padding-sm) 80px-
--van-contact-list-edit-icon-size16px-
--van-contact-list-add-button-z-index999-
--van-contact-list-radio-colorvar(--van-primary-color)-
--van-contact-list-item-paddingvar(--van-padding-md)-

🎯 最佳实践

📱 移动端优化建议

  1. 列表性能优化 - 使用虚拟滚动处理大量联系人
  2. 搜索功能集成 - 提供快速查找联系人的能力
  3. 分组显示 - 按首字母或分类组织联系人
  4. 快捷操作 - 支持滑动删除、长按多选等手势

🔍 高级搜索功能

vue
<template>
  <div class="contact-list-with-search">
    <!-- 搜索栏 -->
    <Search
      v-model="searchKeyword"
      placeholder="搜索联系人姓名或手机号"
      @search="handleSearch"
      @clear="handleClear"
    />
    
    <!-- 联系人列表 -->
    <ContactList
      v-model="chosenContactId"
      :list="filteredContacts"
      :add-text="addButtonText"
      @add="handleAdd"
      @edit="handleEdit"
      @select="handleSelect"
    />
    
    <!-- 空状态 -->
    <Empty
      v-if="filteredContacts.length === 0 && searchKeyword"
      description="未找到相关联系人"
      image="search"
    />
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { showToast } from 'vant'

const searchKeyword = ref('')
const chosenContactId = ref('1')
const addButtonText = ref('添加新联系人')

const contacts = ref([
  { id: '1', name: '张三', tel: '13000000000', isDefault: true, group: 'Z' },
  { id: '2', name: '李四', tel: '13100000000', group: 'L' },
  { id: '3', name: '王五', tel: '13200000000', group: 'W' },
  { id: '4', name: 'Alice', tel: '13300000000', group: 'A' },
  { id: '5', name: 'Bob', tel: '13400000000', group: 'B' }
])

// 过滤联系人
const filteredContacts = computed(() => {
  if (!searchKeyword.value) return contacts.value
  
  const keyword = searchKeyword.value.toLowerCase()
  return contacts.value.filter(contact => 
    contact.name.toLowerCase().includes(keyword) ||
    contact.tel.includes(keyword)
  )
})

// 搜索处理
const handleSearch = (value) => {
  console.log('搜索:', value)
}

// 清除搜索
const handleClear = () => {
  searchKeyword.value = ''
}

// 添加联系人
const handleAdd = () => {
  showToast('跳转到添加联系人页面')
  // router.push('/contact/add')
}

// 编辑联系人
const handleEdit = (contact, index) => {
  showToast(`编辑联系人: ${contact.name}`)
  // router.push(`/contact/edit/${contact.id}`)
}

// 选择联系人
const handleSelect = (contact, index) => {
  showToast(`选择了联系人: ${contact.name}`)
  chosenContactId.value = contact.id
}
</script>

📊 分组显示功能

vue
<template>
  <div class="grouped-contact-list">
    <!-- 字母索引 -->
    <IndexBar :sticky="false">
      <IndexAnchor
        v-for="group in groupedContacts"
        :key="group.letter"
        :index="group.letter"
      >
        <ContactList
          v-model="chosenContactId"
          :list="group.contacts"
          :add-text="false"
          @edit="handleEdit"
          @select="handleSelect"
        />
      </IndexAnchor>
    </IndexBar>
    
    <!-- 悬浮添加按钮 -->
    <FloatingBubble
      axis="xy"
      icon="plus"
      @click="handleAdd"
    />
  </div>
</template>

<script setup>
import { computed } from 'vue'

// 按首字母分组
const groupedContacts = computed(() => {
  const groups = {}
  
  contacts.value.forEach(contact => {
    const firstLetter = getFirstLetter(contact.name)
    if (!groups[firstLetter]) {
      groups[firstLetter] = []
    }
    groups[firstLetter].push(contact)
  })
  
  return Object.keys(groups)
    .sort()
    .map(letter => ({
      letter,
      contacts: groups[letter].sort((a, b) => a.name.localeCompare(b.name))
    }))
})

// 获取首字母
const getFirstLetter = (name) => {
  const firstChar = name.charAt(0).toUpperCase()
  return /[A-Z]/.test(firstChar) ? firstChar : '#'
}
</script>

🎨 自定义样式主题

vue
<template>
  <div class="custom-contact-list">
    <ContactList
      v-model="chosenContactId"
      :list="contacts"
      class="themed-list"
      @add="handleAdd"
      @edit="handleEdit"
      @select="handleSelect"
    />
  </div>
</template>

<style>
.themed-list {
  --van-contact-list-padding: 16px 16px 100px;
  --van-contact-list-item-padding: 20px;
  --van-contact-list-radio-color: #ff6b6b;
  --van-contact-list-edit-icon-size: 18px;
}

.custom-contact-list {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
}

/* 自定义联系人项样式 */
.themed-list .van-contact-list__item {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 12px;
  margin-bottom: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: all 0.3s ease;
}

.themed-list .van-contact-list__item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

/* 默认联系人标签样式 */
.themed-list .van-contact-list__item--default::after {
  background: linear-gradient(45deg, #ff6b6b, #ffa500);
  border-radius: 8px;
  color: white;
  font-weight: bold;
}
</style>

💡 使用技巧

🔄 批量操作功能

vue
<template>
  <div class="batch-contact-list">
    <!-- 批量操作工具栏 -->
    <div class="batch-toolbar" v-if="isSelectionMode">
      <div class="selection-info">
        已选择 {{ selectedContacts.length }} 个联系人
      </div>
      <div class="batch-actions">
        <Button size="small" @click="batchDelete">删除</Button>
        <Button size="small" type="primary" @click="batchExport">导出</Button>
        <Button size="small" @click="exitSelectionMode">取消</Button>
      </div>
    </div>
    
    <!-- 联系人列表 -->
    <div class="contact-items">
      <div
        v-for="contact in contacts"
        :key="contact.id"
        class="contact-item-wrapper"
        @long-press="enterSelectionMode"
      >
        <!-- 多选模式下的复选框 -->
        <Checkbox
          v-if="isSelectionMode"
          :model-value="selectedContacts.includes(contact.id)"
          @update:model-value="toggleSelection(contact.id)"
        />
        
        <!-- 联系人信息 -->
        <div class="contact-info" @click="handleContactClick(contact)">
          <div class="contact-name">{{ contact.name }}</div>
          <div class="contact-tel">{{ contact.tel }}</div>
          <Tag v-if="contact.isDefault" type="primary" size="small">
            默认
          </Tag>
        </div>
        
        <!-- 操作按钮 -->
        <div class="contact-actions" v-if="!isSelectionMode">
          <Button
            size="small"
            type="primary"
            @click="handleEdit(contact)"
          >
            编辑
          </Button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
const isSelectionMode = ref(false)
const selectedContacts = ref([])

// 进入选择模式
const enterSelectionMode = () => {
  isSelectionMode.value = true
}

// 退出选择模式
const exitSelectionMode = () => {
  isSelectionMode.value = false
  selectedContacts.value = []
}

// 切换选择状态
const toggleSelection = (contactId) => {
  const index = selectedContacts.value.indexOf(contactId)
  if (index > -1) {
    selectedContacts.value.splice(index, 1)
  } else {
    selectedContacts.value.push(contactId)
  }
}

// 批量删除
const batchDelete = async () => {
  const result = await showDialog({
    title: '批量删除',
    message: `确定要删除选中的 ${selectedContacts.value.length} 个联系人吗?`
  })
  
  if (result === 'confirm') {
    // 执行删除操作
    showToast('删除成功')
    exitSelectionMode()
  }
}

// 批量导出
const batchExport = () => {
  const selectedData = contacts.value.filter(c => 
    selectedContacts.value.includes(c.id)
  )
  
  // 导出为 CSV 或其他格式
  exportContacts(selectedData)
  showToast('导出成功')
}
</script>

📞 快捷拨号功能

vue
<template>
  <ContactList
    v-model="chosenContactId"
    :list="contacts"
    @select="handleSelect"
  >
    <!-- 自定义联系人项 -->
    <template #contact-item="{ contact, index }">
      <div class="custom-contact-item">
        <div class="contact-info">
          <div class="contact-name">{{ contact.name }}</div>
          <div class="contact-tel">{{ contact.tel }}</div>
        </div>
        
        <div class="quick-actions">
          <Button
            icon="phone-o"
            size="small"
            type="primary"
            @click="makeCall(contact.tel)"
          />
          <Button
            icon="chat-o"
            size="small"
            @click="sendMessage(contact.tel)"
          />
        </div>
      </div>
    </template>
  </ContactList>
</template>

<script setup>
// 拨打电话
const makeCall = (phoneNumber) => {
  if (window.navigator && window.navigator.userAgent.includes('Mobile')) {
    window.location.href = `tel:${phoneNumber}`
  } else {
    showToast('请在移动设备上使用拨号功能')
  }
}

// 发送短信
const sendMessage = (phoneNumber) => {
  if (window.navigator && window.navigator.userAgent.includes('Mobile')) {
    window.location.href = `sms:${phoneNumber}`
  } else {
    showToast('请在移动设备上使用短信功能')
  }
}
</script>

❓ 常见问题解决

🔧 列表性能优化

问题:联系人数量过多时列表滚动卡顿 解决方案

vue
<template>
  <!-- 使用虚拟滚动优化大列表性能 -->
  <VirtualList
    :list="contacts"
    :item-height="80"
    :container-height="400"
  >
    <template #default="{ item, index }">
      <div class="virtual-contact-item">
        <ContactList
          :list="[item]"
          @edit="handleEdit"
          @select="handleSelect"
        />
      </div>
    </template>
  </VirtualList>
</template>

<script setup>
// 分页加载联系人
const pageSize = 20
const currentPage = ref(1)
const loading = ref(false)

const loadMoreContacts = async () => {
  if (loading.value) return
  
  loading.value = true
  try {
    const newContacts = await fetchContacts(currentPage.value, pageSize)
    contacts.value.push(...newContacts)
    currentPage.value++
  } catch (error) {
    showToast('加载失败')
  } finally {
    loading.value = false
  }
}

// 监听滚动到底部
const handleScroll = (event) => {
  const { scrollTop, scrollHeight, clientHeight } = event.target
  if (scrollTop + clientHeight >= scrollHeight - 10) {
    loadMoreContacts()
  }
}
</script>

📱 移动端适配问题

问题:在不同屏幕尺寸下显示异常 解决方案

css
/* 响应式设计 */
@media (max-width: 768px) {
  .van-contact-list {
    --van-contact-list-padding: 12px 12px 80px;
    --van-contact-list-item-padding: 16px;
  }
}

@media (max-width: 480px) {
  .van-contact-list {
    --van-contact-list-padding: 8px 8px 80px;
    --van-contact-list-item-padding: 12px;
    --van-contact-list-edit-icon-size: 14px;
  }
}

/* 安全区域适配 */
.van-contact-list {
  padding-bottom: calc(80px + env(safe-area-inset-bottom));
}

🔍 搜索功能优化

问题:搜索结果不准确或性能差 解决方案

javascript
// 使用防抖优化搜索性能
import { debounce } from 'lodash-es'

const searchContacts = debounce((keyword) => {
  if (!keyword.trim()) {
    filteredContacts.value = contacts.value
    return
  }
  
  // 支持拼音搜索
  filteredContacts.value = contacts.value.filter(contact => {
    const searchText = keyword.toLowerCase()
    return (
      contact.name.toLowerCase().includes(searchText) ||
      contact.tel.includes(searchText) ||
      getPinyin(contact.name).toLowerCase().includes(searchText)
    )
  })
}, 300)

// 拼音搜索支持
const getPinyin = (text) => {
  // 使用 pinyin 库或自定义拼音转换
  return pinyinUtil.getPinyin(text, '', true, false)
}

🎨 设计灵感

🌈 主题风格建议

  1. 商务风格 - 简洁专业

    css
    --van-contact-list-padding: 20px 16px 80px;
    --van-contact-list-item-padding: 20px;
    --van-contact-list-radio-color: #1890ff;
  2. 时尚风格 - 现代活泼

    css
    --van-contact-list-padding: 16px 16px 80px;
    --van-contact-list-item-padding: 18px;
    --van-contact-list-radio-color: #ff6b6b;
  3. 极简风格 - 纯净优雅

    css
    --van-contact-list-padding: 24px 20px 80px;
    --van-contact-list-item-padding: 24px;
    --van-contact-list-radio-color: #52c41a;

🎯 交互设计建议

  • 滑动操作 - 左滑显示快捷操作按钮
  • 长按选择 - 长按进入多选模式
  • 下拉刷新 - 支持下拉刷新联系人列表
  • 字母导航 - 右侧字母索引快速定位

📚 相关文档

🔗 延伸阅读

基於Vant構建的企業級移動端解決方案