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 | *number | string* |
| 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-padding | var(--van-padding-sm) var(--van-padding-sm) 80px | - |
| --van-contact-list-edit-icon-size | 16px | - |
| --van-contact-list-add-button-z-index | 999 | - |
| --van-contact-list-radio-color | var(--van-primary-color) | - |
| --van-contact-list-item-padding | var(--van-padding-md) | - |
🎯 ベストプラクティス
📱 モバイル最適化のヒント
- リスト性能 - 仮想スクロールで大量データに対応
- 検索機能 - 氏名/電話番号の高速検索
- グループ表示 - 先頭文字やカテゴリで整理
- クイック操作 - スワイプ削除・長押し多選など
🔍 高度な検索
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)
}🎨 デザインインスピレーション
🌈 テーマスタイル案
ビジネス - シンプルでプロフェッショナル
css--van-contact-list-padding: 20px 16px 80px; --van-contact-list-item-padding: 20px; --van-contact-list-radio-color: #1890ff;モダン - 先進的でポップ
css--van-contact-list-padding: 16px 16px 80px; --van-contact-list-item-padding: 18px; --van-contact-list-radio-color: #ff6b6b;ミニマル - クリーンでエレガント
css--van-contact-list-padding: 24px 20px 80px; --van-contact-list-item-padding: 24px; --van-contact-list-radio-color: #52c41a;
🎯 インタラクションの提案
- スワイプ操作 - 左スワイプで操作ボタン
- 長押し選択 - 長押しで多選モード
- プル・トゥ・リフレッシュ - 下拉でリスト更新
- 文字ナビゲーション - 右側インデックスで高速移動
📚 関連ドキュメント
- ContactCard 連絡先カード - 連絡先カード表示
- ContactEdit 連絡先編集 - 連絡先編集
- Contact 連絡先 - 連絡先選択
- Radio ラジオボタン - 単一選択
🔗 参考資料
- モバイルのリスト最適化 - Web.dev
- 連絡先管理ベストプラクティス - Material Design
- アクセシブルなリスト設計 - W3C
- Vue 3 リストレンダリング - Vue 公式