Skip to content

DatePicker 日付選択 - Vant 4

日付を選択するためのピッカーコンポーネントで、年/月/日の選択が可能です。

📦 導入

インポート

js
import { DatePicker } from 'vant';

🔨 基本的な使い方

基礎的な使い方

value 属性を使用して、選択された日付を制御します。columns-type 属性で表示する列のタイプを設定します。

vue
<template>
  <div class="demo-date-picker">
    <van-field
      v-model="value"
      is-link
      label="日付"
      placeholder="日付を選択"
      @click="showPicker = true"
    />
    <van-popup v-model:show="showPicker" round position="bottom">
      <van-date-picker
        v-model="currentDate"
        title="日付を選択"
        :columns-type="['year', 'month', 'day']"
        @confirm="onConfirm"
        @cancel="showPicker = false"
      />
    </van-popup>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const showPicker = ref(false);
const currentDate = ref(['2024', '01', '01']);
const value = computed({
  get() {
    return currentDate.value.join('-');
  },
  set(val) {
    if (val) {
      currentDate.value = val.split('-');
    }
  }
});

const onConfirm = ({ selectedValues }) => {
  currentDate.value = selectedValues;
  showPicker.value = false;
};
</script>

オプションタイプ

columns-type 属性で、表示する列のタイプをカスタマイズできます。

vue
<template>
  <div class="demo-date-picker-types">
    <!-- 年月日 -->
    <van-date-picker
      v-model="date1"
      title="年月日"
      :columns-type="['year', 'month', 'day']"
    />
    
    <!-- 年月 -->
    <van-date-picker
      v-model="date2"
      title="年月"
      :columns-type="['year', 'month']"
    />
    
    <!-- 月日 -->
    <van-date-picker
      v-model="date3"
      title="月日"
      :columns-type="['month', 'day']"
    />
    
    <!-- 年 -->
    <van-date-picker
      v-model="date4"
      title="年"
      :columns-type="['year']"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue';

const date1 = ref(['2024', '01', '01']);
const date2 = ref(['2024', '01']);
const date3 = ref(['01', '01']);
const date4 = ref(['2024']);
</script>

オプションのフォーマット

formatter 属性で、各列の表示テキストをカスタマイズできます。

vue
<template>
  <van-date-picker
    v-model="currentDate"
    title="カスタムフォーマット"
    :columns-type="['year', 'month', 'day']"
    :formatter="formatter"
  />
</template>

<script setup>
import { ref } from 'vue';

const currentDate = ref(['2024', '01', '01']);

const formatter = (type, option) => {
  if (type === 'year') {
    option.text += '年';
  } else if (type === 'month') {
    option.text += '月';
  } else if (type === 'day') {
    option.text += '日';
  }
  return option;
};
</script>

オプションのフィルタリング

filter 属性で、各列に表示されるオプションをカスタマイズできます。

vue
<template>
  <van-date-picker
    v-model="currentDate"
    title="曜日でフィルタリング"
    :columns-type="['year', 'month', 'day']"
    :filter="filter"
  />
</template>

<script setup>
import { ref } from 'vue';

const currentDate = ref(['2024', '01', '01']);

const filter = (type, options, values) => {
  // 日付列のみフィルタリング
  if (type === 'day') {
    // 土曜日と日曜日を除外
    const year = values[0];
    const month = values[1];
    
    return options.filter((option) => {
      const date = new Date(year, month - 1, option.value);
      const day = date.getDay();
      return day !== 0 && day !== 6;
    });
  }
  return options;
};
</script>

API

Props

パラメータ説明デフォルト値
v-model選択された日付string[]-
columns-type列のタイプstring[]['year', 'month', 'day']
titleピッカーのタイトルstring''
confirm-button-text確認ボタンのテキストstring'確認'
cancel-button-textキャンセルボタンのテキストstring'キャンセル'
formatterオプションのフォーマット関数(type, option) => option-
filterオプションのフィルタリング関数(type, options, values) => options-
min-date最小日付制限Date十年前
max-date最大日付制限Date十年後
min-year最小年number1900
max-year最大年number2100
show-toolbarツールバーを表示するかどうかbooleantrue
loadingローディング状態booleanfalse
readonly読み取り専用booleanfalse
item-heightオプションの高さnumber44
visible-option-num可視オプションの数number6
swipe-durationスワイプアニメーションの期間number100

Events

イベント名説明コールバック引数
confirm確認ボタンがクリックされたときにトリガー{ selectedValues, selectedOptions }
cancelキャンセルボタンがクリックされたときにトリガー-
change選択が変更されたときにトリガー{ selectedValues, selectedOptions }

Slots

名前説明パラメータ
toolbarツールバーのカスタムコンテンツ-
titleタイトルのカスタムコンテンツ-
confirm確認ボタンのカスタムコンテンツ-
cancelキャンセルボタンのカスタムコンテンツ-
optionオプションのカスタムコンテンツoption: { text, value }
columns-top列の上のカスタムコンテンツ-
columns-bottom列の下のカスタムコンテンツ-

メソッド

メソッド名説明パラメータ戻り値
confirm現在の選択を確認する-void
cancel現在の選択をキャンセルする-void
getSelectedOptions選択されたオプションを取得する-object[]

型定義

コンポーネントの型定義は、次のようにインポートできます:

js
import type { DatePickerProps, DatePickerColumnType } from 'vant';

テーマカスタマイズ

CSS 変数

次の CSS 変数を設定することで、スタイルをカスタマイズできます。カスタマイズ方法については、テーマカスタマイズ を参照してください。

変数名説明デフォルト値
--van-date-picker-active-colorアクティブな状態のテキスト色var(--van-primary-color)
--van-date-picker-text-colorテキスト色var(--van-text-color)
--van-date-picker-disabled-opacity無効状態の不透明度var(--van-disabled-opacity)
--van-date-picker-option-heightオプションの高さ44px

💡 ベストプラクティス

日付範囲の設定

日付の範囲を合理的に設定することで、ユーザーエクスペリエンスを向上させることができます。

vue
<template>
  <van-popup v-model:show="showPicker" round position="bottom">
    <van-date-picker
      v-model="selectedDate"
      title="日付を選択"
      :columns-type="['year', 'month', 'day']"
      :min-date="minDate"
      :max-date="maxDate"
      :formatter="formatter"
      @confirm="onConfirm"
      @cancel="showPicker = false"
    />
  </van-popup>
</template>

<script setup>
import { ref } from 'vue';

const showPicker = ref(false);
const selectedDate = ref(['2024', '01', '01']);

// 合理的な日付範囲を設定
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);

// フォーマット表示
const formatter = (type, option) => {
  const suffixMap = {
    year: '年',
    month: '月',
    day: '日'
  };
  option.text += suffixMap[type] || '';
  return option;
};

// 確認選択
const onConfirm = ({ selectedValues }) => {
  selectedDate.value = selectedValues;
  showPicker.value = false;
};

// 日付をフォーマット表示
const formatDate = (date) => {
  if (!date || date.length < 3) return '日付を選択してください';
  return `${date[0]}年${date[1]}月${date[2]}日`;
};
</script>

レスポンシブな日付範囲設定

javascript
// 日付範囲を動的に設定
const setupDateRange = (type) => {
  const now = new Date();
  const ranges = {
    // 誕生日選択:100年前から今日まで
    birthday: {
      min: new Date(now.getFullYear() - 100, 0, 1),
      max: now
    },
    // 予約選択:今日から30日後まで
    appointment: {
      min: now,
      max: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000)
    },
    // 履歴記録:10年前から今日まで
    history: {
      min: new Date(now.getFullYear() - 10, 0, 1),
      max: now
    },
    // 計画安排:今日から1年後まで
    planning: {
      min: now,
      max: new Date(now.getFullYear() + 1, now.getMonth(), now.getDate())
    }
  };
  
  return ranges[type] || ranges.appointment;
};

// 使用例
const { min: minDate, max: maxDate } = setupDateRange('birthday');

スマートなデフォルト値設定

javascript
// スマートにデフォルト日付を設定
const getSmartDefaultDate = (scenario) => {
  const now = new Date();
  const year = now.getFullYear().toString();
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
  const day = now.getDate().toString().padStart(2, '0');
  
  const scenarios = {
    // 誕生日シナリオ:デフォルト30年前
    birthday: [(year - 30).toString(), month, day],
    // 予約シナリオ:デフォルト明日
    appointment: [
      year, 
      month, 
      (now.getDate() + 1).toString().padStart(2, '0')
    ],
    // 記念日シナリオ:デフォルト今日
    anniversary: [year, month, day],
    // 計画シナリオ:デフォルト来週
    planning: [
      year,
      month,
      (now.getDate() + 7).toString().padStart(2, '0')
    ]
  };
  
  return scenarios[scenario] || [year, month, day];
};

💡 使用技巧

多言語日付フォーマット

javascript
// 国際化日付フォーマット
const createI18nFormatter = (locale = 'zh-CN') => {
  const formatters = {
    'zh-CN': {
      year: (text) => `${text}年`,
      month: (text) => `${text}月`,
      day: (text) => `${text}日`
    },
    'en-US': {
      year: (text) => text,
      month: (text) => new Date(2000, parseInt(text) - 1).toLocaleDateString('en-US', { month: 'short' }),
      day: (text) => `${text}${getOrdinalSuffix(parseInt(text))}`
    },
    'ja-JP': {
      year: (text) => `${text}年`,
      month: (text) => `${text}月`,
      day: (text) => `${text}日`
    }
  };
  
  const currentFormatter = formatters[locale] || formatters['zh-CN'];
  
  return (type, option) => {
    if (currentFormatter[type]) {
      option.text = currentFormatter[type](option.text);
    }
    return option;
  };
};

// 英語序数詞接尾辞
const getOrdinalSuffix = (num) => {
  const j = num % 10;
  const k = num % 100;
  if (j === 1 && k !== 11) return 'st';
  if (j === 2 && k !== 12) return 'nd';
  if (j === 3 && k !== 13) return 'rd';
  return 'th';
};

// 使用例
const formatter = createI18nFormatter('en-US');

高度なフィルタリング機能

javascript
// 平日フィルター
const createWorkdayFilter = () => {
  return (type, options) => {
    if (type === 'day') {
      return options.filter(option => {
        const date = new Date(2024, 0, parseInt(option.value)); // 2024年1月を基準に使用
        const dayOfWeek = date.getDay();
        return dayOfWeek !== 0 && dayOfWeek !== 6; // 週末を除外
      });
    }
    return options;
  };
};

// 特別な日付フィルター
const createSpecialDateFilter = (excludeDates = []) => {
  return (type, options, values) => {
    if (type === 'day' && values[0] && values[1]) {
      const year = parseInt(values[0]);
      const month = parseInt(values[1]) - 1;
      
      return options.filter(option => {
        const day = parseInt(option.value);
        const dateStr = `${year}-${(month + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
        return !excludeDates.includes(dateStr);
      });
    }
    return options;
  };
};

// 四半期フィルター
const createQuarterFilter = () => {
  return (type, options) => {
    if (type === 'month') {
      // 四半期の最初の月のみ表示:1月、4月、7月、10月
      return options.filter(option => {
        const month = parseInt(option.value);
        return [1, 4, 7, 10].includes(month);
      });
    }
    return options;
  };
};

動的な列タイプ切り替え

vue
<template>
  <div class="dynamic-date-picker">
    <!-- モード選択 -->
    <van-radio-group v-model="pickerMode" direction="horizontal">
      <van-radio name="full">完全な日付</van-radio>
      <van-radio name="yearMonth">年月</van-radio>
      <van-radio name="monthDay">月日</van-radio>
      <van-radio name="year">年のみ</van-radio>
    </van-radio-group>
    
    <!-- 日付ピッカー -->
    <van-date-picker
      v-model="selectedDate"
      :columns-type="currentColumnsType"
      :formatter="formatter"
      @change="onDateChange"
    />
  </div>
</template>

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

const pickerMode = ref('full');
const selectedDate = ref(['2024', '01', '01']);

// モードに応じて列タイプを動的に設定
const currentColumnsType = computed(() => {
  const modeMap = {
    full: ['year', 'month', 'day'],
    yearMonth: ['year', 'month'],
    monthDay: ['month', 'day'],
    year: ['year']
  };
  return modeMap[pickerMode.value];
});

// モードの変更を監視し、選択値を調整
watch(pickerMode, (newMode) => {
  const now = new Date();
  const year = now.getFullYear().toString();
  const month = (now.getMonth() + 1).toString().padStart(2, '0');
  const day = now.getDate().toString().padStart(2, '0');
  
  const defaultValues = {
    full: [year, month, day],
    yearMonth: [year, month],
    monthDay: [month, day],
    year: [year]
  };
  
  selectedDate.value = defaultValues[newMode];
});

const formatter = (type, option) => {
  const suffixMap = { year: '年', month: '月', day: '日' };
  option.text += suffixMap[type] || '';
  return option;
};

const onDateChange = ({ selectedValues }) => {
  console.log('日付が変更されました:', selectedValues);
};
</script>

🔧 よくある質問と解決策

iOS 日付の互換性問題

javascript
// iOS で安全な日付作成方法
const createSafeDate = (year, month, day) => {
  // iOS は 'YYYY-MM-DD' 形式をサポートしていないため、'YYYY/MM/DD' を使用する必要があります
  const safeYear = year || new Date().getFullYear();
  const safeMonth = month || 1;
  const safeDay = day || 1;
  
  // 方法1:スラッシュ区切りを使用
  return new Date(`${safeYear}/${safeMonth}/${safeDay}`);
  
  // 方法2:コンストラクタを使用(推奨)
  // return new Date(safeYear, safeMonth - 1, safeDay);
};

// 日付範囲設定のベストプラクティス
const setupDateRangeSafely = () => {
  // ❌ 間違った記述 - iOS で失敗する可能性があります
  // const minDate = new Date('2020-01-01');
  
  // ✅ 正しい記述 - クロスプラットフォーム互換
  const minDate = new Date(2020, 0, 1); // 月は0から始まります
  const maxDate = new Date(2030, 11, 31);
  
  return { minDate, maxDate };
};

パフォーマンス最適化のヒント

javascript
// 大規模データの最適化
const optimizeForLargeData = () => {
  // 仮想スクロールを使用してDOMノードを減らす
  const visibleOptionNum = 5; // 可視オプション数を減らす
  
  // オプションの遅延読み込み
  const lazyLoadOptions = (type, currentValues) => {
    if (type === 'year') {
      // 現在の年の前後10年のみ読み込む
      const currentYear = parseInt(currentValues[0]) || new Date().getFullYear();
      const startYear = currentYear - 10;
      const endYear = currentYear + 10;
      
      return Array.from({ length: endYear - startYear + 1 }, (_, i) => ({
        text: (startYear + i).toString(),
        value: (startYear + i).toString()
      }));
    }
    return [];
  };
  
  return { visibleOptionNum, lazyLoadOptions };
};

// デバウンス最適化
const createDebouncedHandler = (handler, delay = 300) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => handler(...args), delay);
  };
};

// 使用例
const debouncedChange = createDebouncedHandler((values) => {
  console.log('日付が変更されました:', values);
  // 複雑なビジネスロジックを実行
}, 300);

データ検証とエラー処理

javascript
// 日付の有効性検証
const validateDateSelection = (selectedValues, minDate, maxDate) => {
  if (!selectedValues || selectedValues.length === 0) {
    return { valid: false, error: '日付を選択してください' };
  }
  
  try {
    const [year, month, day] = selectedValues;
    const selectedDate = new Date(
      parseInt(year), 
      parseInt(month) - 1, 
      parseInt(day)
    );
    
    // 日付が有効かどうかを確認
    if (isNaN(selectedDate.getTime())) {
      return { valid: false, error: '無効な日付です' };
    }
    
    // 許可された範囲内にあるかどうかを確認
    if (minDate && selectedDate < minDate) {
      return { valid: false, error: '日付は最小日付より前にできません' };
    }
    
    if (maxDate && selectedDate > maxDate) {
      return { valid: false, error: '日付は最大日付より後にできません' };
    }
    
    return { valid: true, date: selectedDate };
  } catch (error) {
    return { valid: false, error: '日付形式が正しくありません' };
  }
};

// エラー処理コンポーネント
const DatePickerWithValidation = {
  setup() {
    const selectedDate = ref([]);
    const errorMessage = ref('');
    
    const onConfirm = ({ selectedValues }) => {
      const validation = validateDateSelection(
        selectedValues, 
        minDate.value, 
        maxDate.value
      );
      
      if (validation.valid) {
        selectedDate.value = selectedValues;
        errorMessage.value = '';
        // 確認ロジックを実行
      } else {
        errorMessage.value = validation.error;
        // エラーメッセージを表示
        showToast(validation.error);
      }
    };
    
    return { selectedDate, errorMessage, onConfirm };
  }
};

🎨 デザインのインスピレーション

テーマ化された日付ピッカー

css
/* 春のテーマ */
.date-picker-spring {
  --van-picker-option-text-color: #52c41a;
  --van-picker-option-selected-text-color: #389e0d;
  --van-picker-toolbar-height: 44px;
  --van-picker-action-text-color: #52c41a;
}

.date-picker-spring .van-picker__toolbar {
  background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%);
}

/* 夏のテーマ */
.date-picker-summer {
  --van-picker-option-text-color: #1890ff;
  --van-picker-option-selected-text-color: #096dd9;
  --van-picker-action-text-color: #1890ff;
}

.date-picker-summer .van-picker__toolbar {
  background: linear-gradient(135deg, #87ceeb 0%, #98d8e8 100%);
}

/* 秋のテーマ */
.date-picker-autumn {
  --van-picker-option-text-color: #fa8c16;
  --van-picker-option-selected-text-color: #d46b08;
  --van-picker-action-text-color: #fa8c16;
}

.date-picker-autumn .van-picker__toolbar {
  background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%);
}

/* 冬のテーマ */
.date-picker-winter {
  --van-picker-option-text-color: #722ed1;
  --van-picker-option-selected-text-color: #531dab;
  --van-picker-action-text-color: #722ed1;
}

.date-picker-winter .van-picker__toolbar {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

アニメーション効果の強化

css
/* オプション切り替えアニメーション */
.date-picker-animated .van-picker-column__item {
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.date-picker-animated .van-picker-column__item--selected {
  transform: scale(1.1);
  font-weight: bold;
  text-shadow: 0 0 8px rgba(24, 144, 255, 0.3);
}

/* ツールバーアニメーション */
.date-picker-animated .van-picker__toolbar {
  animation: slideInDown 0.3s ease-out;
}

@keyframes slideInDown {
  from {
    transform: translateY(-100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* オプションリストアニメーション */
.date-picker-animated .van-picker__columns {
  animation: fadeInUp 0.4s ease-out;
}

@keyframes fadeInUp {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* フローティング効果 */
.date-picker-floating {
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(10px);
  background: rgba(255, 255, 255, 0.95);
}

クリエイティブなインタラクション効果

vue
<template>
  <div class="creative-date-picker">
    <!-- 3D フリップ効果 -->
    <div class="flip-container" :class="{ flipped: isFlipped }">
      <div class="flipper">
        <div class="front">
          <van-cell 
            title="日付を選択" 
            :value="displayDate" 
            @click="showPicker"
          />
        </div>
        <div class="back">
          <van-date-picker
            v-model="selectedDate"
            @confirm="onConfirm"
            @cancel="hidePicker"
          />
        </div>
      </div>
    </div>
    
    <!-- パーティクル効果の背景 -->
    <div class="particles" v-if="showParticles">
      <div 
        v-for="i in 20" 
        :key="i" 
        class="particle"
        :style="getParticleStyle(i)"
      ></div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const isFlipped = ref(false);
const showParticles = ref(false);
const selectedDate = ref(['2024', '01', '01']);

const displayDate = computed(() => {
  const [year, month, day] = selectedDate.value;
  return `${year}年${month}月${day}日`;
});

const showPicker = () => {
  isFlipped.value = true;
  showParticles.value = true;
};

const hidePicker = () => {
  isFlipped.value = false;
  showParticles.value = false;
};

const onConfirm = ({ selectedValues }) => {
  selectedDate.value = selectedValues;
  hidePicker();
};

const getParticleStyle = (index) => {
  const angle = (index * 18) % 360;
  const radius = 100 + Math.random() * 50;
  const x = Math.cos(angle * Math.PI / 180) * radius;
  const y = Math.sin(angle * Math.PI / 180) * radius;
  
  return {
    left: `calc(50% + ${x}px)`,
    top: `calc(50% + ${y}px)`,
    animationDelay: `${index * 0.1}s`
  };
};
</script>

<style scoped>
.flip-container {
  perspective: 1000px;
  width: 100%;
  height: 300px;
}

.flipper {
  transition: transform 0.6s;
  transform-style: preserve-3d;
  position: relative;
  width: 100%;
  height: 100%;
}

.flipped .flipper {
  transform: rotateY(180deg);
}

.front, .back {
  backface-visibility: hidden;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.back {
  transform: rotateY(180deg);
}

.particle {
  position: absolute;
  width: 4px;
  height: 4px;
  background: #1890ff;
  border-radius: 50%;
  animation: float 3s ease-in-out infinite;
}

@keyframes float {
  0%, 100% { transform: translateY(0px) scale(1); opacity: 1; }
  50% { transform: translateY(-20px) scale(1.2); opacity: 0.7; }
}
</style>

🚀 高度な機能拡張

スマート日付推奨システム

javascript
// スマート日付推奨エンジン
class SmartDateRecommender {
  constructor() {
    this.userHistory = [];
    this.preferences = {};
  }
  
  // ユーザーの選択履歴を記録
  recordSelection(date, context) {
    this.userHistory.push({
      date,
      context,
      timestamp: Date.now()
    });
    
    // 履歴を合理的な範囲内に保つ
    if (this.userHistory.length > 100) {
      this.userHistory.shift();
    }
    
    this.updatePreferences();
  }
  
  // ユーザーの好みを更新
  updatePreferences() {
    const recentSelections = this.userHistory.slice(-20);
    
    // 好まれる曜日を分析
    const dayOfWeekCount = {};
    recentSelections.forEach(({ date }) => {
      const dayOfWeek = new Date(date).getDay();
      dayOfWeekCount[dayOfWeek] = (dayOfWeekCount[dayOfWeek] || 0) + 1;
    });
    
    this.preferences.preferredDayOfWeek = Object.keys(dayOfWeekCount)
      .reduce((a, b) => dayOfWeekCount[a] > dayOfWeekCount[b] ? a : b);
    
    // 好まれる時間帯を分析
    const monthCount = {};
    recentSelections.forEach(({ date }) => {
      const month = new Date(date).getMonth();
      monthCount[month] = (monthCount[month] || 0) + 1;
    });
    
    this.preferences.preferredMonth = Object.keys(monthCount)
      .reduce((a, b) => monthCount[a] > monthCount[b] ? a : b);
  }
  
  // スマート推奨を生成
  getRecommendations(context, count = 5) {
    const now = new Date();
    const recommendations = [];
    
    // コンテキストに基づく推奨
    switch (context) {
      case 'meeting':
        // 会議推奨:平日、午前中
        recommendations.push(...this.getWorkdayRecommendations(now, count));
        break;
      case 'birthday':
        // 誕生日推奨:履歴の誕生日選択に基づく
        recommendations.push(...this.getBirthdayRecommendations(count));
        break;
      case 'vacation':
        // 休暇推奨:週末または祝日
        recommendations.push(...this.getVacationRecommendations(now, count));
        break;
      default:
        recommendations.push(...this.getGeneralRecommendations(now, count));
    }
    
    return recommendations.slice(0, count);
  }
  
  // 平日推奨
  getWorkdayRecommendations(baseDate, count) {
    const recommendations = [];
    let date = new Date(baseDate);
    
    while (recommendations.length < count) {
      date.setDate(date.getDate() + 1);
      const dayOfWeek = date.getDay();
      
      // 週末をスキップ
      if (dayOfWeek !== 0 && dayOfWeek !== 6) {
        recommendations.push({
          date: new Date(date),
          reason: '平日推奨',
          confidence: 0.8
        });
      }
    }
    
    return recommendations;
  }
  
  // 誕生日推奨
  getBirthdayRecommendations(count) {
    const recommendations = [];
    const currentYear = new Date().getFullYear();
    
    // 履歴の誕生日選択に基づく推奨
    const birthdayHistory = this.userHistory.filter(h => h.context === 'birthday');
    
    birthdayHistory.forEach(({ date }) => {
      const birthDate = new Date(date);
      const thisYearBirthday = new Date(currentYear, birthDate.getMonth(), birthDate.getDate());
      
      recommendations.push({
        date: thisYearBirthday,
        reason: '履歴の誕生日記録',
        confidence: 0.9
      });
    });
    
    return recommendations.slice(0, count);
  }
  
  // 休暇推奨
  getVacationRecommendations(baseDate, count) {
    const recommendations = [];
    let date = new Date(baseDate);
    
    while (recommendations.length < count) {
      date.setDate(date.getDate() + 1);
      const dayOfWeek = date.getDay();
      
      // 週末を推奨
      if (dayOfWeek === 0 || dayOfWeek === 6) {
        recommendations.push({
          date: new Date(date),
          reason: '週末推奨',
          confidence: 0.7
        });
      }
    }
    
    return recommendations;
  }
  
  // 一般的な推奨
  getGeneralRecommendations(baseDate, count) {
    const recommendations = [];
    
    // 明日
    const tomorrow = new Date(baseDate);
    tomorrow.setDate(tomorrow.getDate() + 1);
    recommendations.push({
      date: tomorrow,
      reason: '明日',
      confidence: 0.6
    });
    
    // 来週の同日
    const nextWeek = new Date(baseDate);
    nextWeek.setDate(nextWeek.getDate() + 7);
    recommendations.push({
      date: nextWeek,
      reason: '来週の同日',
      confidence: 0.5
    });
    
    // 来月の同日
    const nextMonth = new Date(baseDate);
    nextMonth.setMonth(nextMonth.getMonth() + 1);
    recommendations.push({
      date: nextMonth,
      reason: '来月の同日',
      confidence: 0.4
    });
    
    return recommendations.slice(0, count);
  }
}

// 使用例
const recommender = new SmartDateRecommender();

// 日付ピッカーに推奨機能を統合
const DatePickerWithRecommendations = {
  setup() {
    const recommendations = ref([]);
    
    const loadRecommendations = (context) => {
      recommendations.value = recommender.getRecommendations(context);
    };
    
    const selectRecommendation = (recommendation) => {
      const date = recommendation.date;
      selectedDate.value = [
        date.getFullYear().toString(),
        (date.getMonth() + 1).toString().padStart(2, '0'),
        date.getDate().toString().padStart(2, '0')
      ];
      
      // 選択を記録
      recommender.recordSelection(date, currentContext.value);
    };
    
    return {
      recommendations,
      loadRecommendations,
      selectRecommendation
    };
  }
};

複数日付ピッカー

vue
<template>
  <div class="multi-date-picker">
    <div class="selected-dates">
      <h3>選択済みの日付 ({{ selectedDates.length }})</h3>
      <div class="date-chips">
        <van-tag
          v-for="(date, index) in selectedDates"
          :key="index"
          closeable
          @close="removeDate(index)"
        >
          {{ formatDate(date) }}
        </van-tag>
      </div>
    </div>
    
    <van-date-picker
      v-model="currentDate"
      @confirm="addDate"
      :min-date="minDate"
      :max-date="maxDate"
    />
    
    <div class="actions">
      <van-button @click="clearAll" type="default">すべてクリア</van-button>
      <van-button @click="confirmSelection" type="primary">選択を確認</van-button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const selectedDates = ref([]);
const currentDate = ref(['2024', '01', '01']);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);

const addDate = ({ selectedValues }) => {
  const dateStr = selectedValues.join('-');
  
  // 既に存在するかどうかを確認
  const exists = selectedDates.value.some(date => 
    date.join('-') === dateStr
  );
  
  if (!exists) {
    selectedDates.value.push([...selectedValues]);
    selectedDates.value.sort((a, b) => {
      const dateA = new Date(a[0], a[1] - 1, a[2]);
      const dateB = new Date(b[0], b[1] - 1, b[2]);
      return dateA - dateB;
    });
  } else {
    showToast('この日付は既に選択されています');
  }
};

const removeDate = (index) => {
  selectedDates.value.splice(index, 1);
};

const clearAll = () => {
  selectedDates.value = [];
};

const confirmSelection = () => {
  if (selectedDates.value.length === 0) {
    showToast('少なくとも1つの日付を選択してください');
    return;
  }
  
  emit('confirm', selectedDates.value);
};

const formatDate = (date) => {
  return `${date[0]}年${date[1]}月${date[2]}日`;
};
</script>

<style scoped>
.selected-dates {
  padding: 16px;
  background: #f7f8fa;
  margin-bottom: 16px;
}

.date-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.actions {
  display: flex;
  gap: 16px;
  padding: 16px;
}
</style>

📚 関連リソース

技術ドキュメント

デザインガイドライン

ユーザーエクスペリエンス

関連コンポーネント

Vant に基づく企業向けモバイルソリューション