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';
export default {
  setup() {
    const chosenContactId = ref('1');
    const list = ref([
      { id: '1', name: '张三', tel: '13000000000', isDefault: true },
      { id: '2', name: '李四', tel: '1310000000' },
    ]);
    const onAdd = () => showToast('新規追加');
    const onEdit = (contact) => showToast(`編集: ${contact.id}`);
    const onSelect = (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連絡先の一意 ID*number
name氏名string
tel電話番号*number
isDefaultデフォルトフラグ*boolean

型定義

コンポーネントは以下の型定義をエクスポートします:

ts
import type { 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('モバイル端末でご利用ください')
  }
}

// SMS を送信
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 に基づく企業向けモバイルソリューション