Files
YGChatCS/components/Calender
2025-08-02 13:59:29 +08:00
..
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00
2025-08-02 13:59:29 +08:00

日历组件 (Calendar Component)

功能需求分析

基于设计图片分析,该日历组件是一个酒店预订场景的低价日历弹窗,支持通过日期图标点击触发显示,需要实现以下细分功能:

交互使用方式

基础用法

通过点击日期图标打开日历组件:

<template>
  <view class="date-picker" @tap="openCalendar">
    <view class="date-display">
      <text class="date-label">选择日期</text>
      <text class="date-value" v-if="selectedDate">{{ selectedDate }}</text>
      <text class="date-placeholder" v-else>请点击日期图标选择</text>
    </view>
    <view class="date-icon">
      <uni-icons type="calendar-filled" size="24" color="#1890ff"></uni-icons>
    </view>
  </view>
  
  <!-- 日历组件 -->
  <Calendar
    :visible="calendarVisible"
    :price-data="priceData"
    mode="single"
    @close="handleCalendarClose"
    @select="handleDateSelect"
  />
</template>

<script setup>
import { ref } from 'vue'
import Calendar from './index.vue'

const calendarVisible = ref(false)
const selectedDate = ref('')

const openCalendar = () => {
  calendarVisible.value = true
}

const handleCalendarClose = () => {
  calendarVisible.value = false
}

const handleDateSelect = (data) => {
  selectedDate.value = data.date
  calendarVisible.value = false
}
</script>

演示文件

  • year-demo.vue - 全年日历演示(推荐)
  • demo.vue - 完整的交互演示页面
  • example.vue - 详细的使用示例,包含单选和范围选择

跨年日历

组件支持显示从当前月份到明年同月份的13个月日历用户可以连续选择入住和离店日期

  1. 点击第一个日期设置入住日期
  2. 点击第二个日期设置离店日期
  3. 自动显示"入住"和"离店"标签
  4. 支持滚动浏览跨年日期范围

1. 弹窗容器结构

1.1 弹窗基础框架

  • 实现遮罩层背景(半透明黑色)
  • 弹窗居中显示,支持垂直居中
  • 弹窗圆角设计建议12px
  • 弹窗阴影效果
  • 弹窗动画效果(淡入淡出)

1.2 弹窗头部区域

  • 标题文字:"低价日历"
  • 副标题文字:"以下价格为单晚参考价"
  • 右上角关闭按钮X图标
  • 关闭按钮点击交互
  • 头部区域背景色和分割线

2. 日历主体结构

2.1 周标题行

  • 显示周几标题:一、二、三、四、五、六、日
  • 周标题居中对齐
  • 周标题字体样式(灰色、小字号)

2.2 月份导航

  • 月份标题显示:"2024年5月"、"2024年6月"
  • 月份标题居中显示
  • 月份标题字体加粗
  • 支持上下滑动切换月份(可选)

3. 日期网格系统

3.1 日期格子基础

  • 7列网格布局对应周一到周日
  • 每个格子固定宽高比
  • 格子间距设计
  • 格子圆角设计

3.2 日期内容显示

  • 日期数字显示(居中对齐)
  • 价格信息显示¥449格式
  • 日期数字字体大小和颜色
  • 价格字体大小和颜色(较小、灰色)

3.3 日期状态管理

  • 普通可选日期(白色背景)
  • 当前选中日期(蓝色背景,白色文字)
  • 入住日期标记("入住"标签)
  • 离店日期标记("离店"标签)
  • 选择范围内日期(浅蓝色背景)
  • 不可选日期(灰色背景,禁用状态)

4. 交互功能实现

4.1 日期选择逻辑

  • 单击日期选择功能
  • 日期范围选择(入住-离店)
  • 选择状态视觉反馈
  • 选择完成后的回调事件

4.2 用户体验优化

  • 点击动画效果
  • 触摸反馈
  • 防止快速重复点击
  • 选择范围的视觉连接线(可选)

5. 数据处理功能

5.1 价格数据管理

  • 价格数据结构设计
  • 价格数据绑定到日期
  • 价格格式化显示
  • 无价格数据的处理

5.2 日期计算功能

  • 月份天数计算
  • 月份第一天星期几计算
  • 跨月份日期处理
  • 日期有效性验证

6. 响应式适配

6.1 移动端适配

  • 触摸屏操作优化
  • 不同屏幕尺寸适配
  • 横竖屏切换适配
  • 安全区域适配(刘海屏等)

6.2 字体和尺寸适配

  • 字体大小响应式调整
  • 格子尺寸响应式调整
  • 间距响应式调整

技术实现细节

组件接口设计

Props 属性

props: {
  // 弹窗显示控制
  visible: {
    type: Boolean,
    default: false,
    required: true
  },
  
  // 价格数据对象
  priceData: {
    type: Object,
    default: () => ({}),
    // 格式: { '2024-05-17': 449, '2024-05-18': 399 }
  },
  
  // 默认选中日期
  defaultValue: {
    type: [String, Array],
    default: '',
    // 单选: '2024-05-17'
    // 范围选择: ['2024-05-17', '2024-05-19']
  },
  
  // 选择模式
  mode: {
    type: String,
    default: 'single',
    validator: (value) => ['single', 'range'].includes(value)
  },
  
  // 最小可选日期
  minDate: {
    type: String,
    default: () => new Date().toISOString().split('T')[0]
  },
  
  // 最大可选日期
  maxDate: {
    type: String,
    default: ''
  },
  
  // 禁用日期数组
  disabledDates: {
    type: Array,
    default: () => []
  },
  
  // 自定义标签
  customLabels: {
    type: Object,
    default: () => ({})
    // 格式: { '2024-05-17': '入住', '2024-05-19': '离店' }
  }
}

Events 事件

// 日期选择事件
this.$emit('select', {
  date: '2024-05-17',
  price: 449,
  mode: 'single'
})

// 范围选择事件
this.$emit('range-select', {
  startDate: '2024-05-17',
  endDate: '2024-05-19',
  startPrice: 449,
  endPrice: 399,
  totalDays: 2
})

// 弹窗关闭事件
this.$emit('close')

// 月份切换事件
this.$emit('month-change', {
  year: 2024,
  month: 5,
  direction: 'next' // 'prev' | 'next'
})

// 日期点击事件(包含所有点击)
this.$emit('date-click', {
  date: '2024-05-17',
  price: 449,
  disabled: false,
  selected: true
})

核心算法实现

日期计算工具函数

// 获取月份天数
getDaysInMonth(year, month) {
  return new Date(year, month, 0).getDate()
}

// 获取月份第一天是星期几
getFirstDayOfMonth(year, month) {
  return new Date(year, month - 1, 1).getDay()
}

// 生成日期网格数据
generateCalendarGrid(year, month) {
  const daysInMonth = this.getDaysInMonth(year, month)
  const firstDay = this.getFirstDayOfMonth(year, month)
  const grid = []
  
  // 填充空白格子
  for (let i = 0; i < firstDay; i++) {
    grid.push(null)
  }
  
  // 填充日期
  for (let day = 1; day <= daysInMonth; day++) {
    const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
    grid.push({
      date: dateStr,
      day: day,
      price: this.priceData[dateStr] || null,
      disabled: this.isDateDisabled(dateStr),
      selected: this.isDateSelected(dateStr),
      inRange: this.isDateInRange(dateStr),
      label: this.customLabels[dateStr] || ''
    })
  }
  
  return grid
}

选择状态管理

data() {
  return {
    selectedDates: [],
    currentMonth: new Date().getMonth() + 1,
    currentYear: new Date().getFullYear(),
    isRangeSelecting: false,
    rangeStart: null,
    rangeEnd: null
  }
},

methods: {
  handleDateClick(dateInfo) {
    if (dateInfo.disabled) return
    
    if (this.mode === 'single') {
      this.selectedDates = [dateInfo.date]
      this.$emit('select', dateInfo)
    } else if (this.mode === 'range') {
      this.handleRangeSelection(dateInfo)
    }
  },
  
  handleRangeSelection(dateInfo) {
    if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) {
      // 开始新的范围选择
      this.rangeStart = dateInfo.date
      this.rangeEnd = null
      this.isRangeSelecting = true
    } else {
      // 完成范围选择
      this.rangeEnd = dateInfo.date
      this.isRangeSelecting = false
      
      // 确保开始日期小于结束日期
      if (new Date(this.rangeStart) > new Date(this.rangeEnd)) {
        [this.rangeStart, this.rangeEnd] = [this.rangeEnd, this.rangeStart]
      }
      
      this.$emit('range-select', {
        startDate: this.rangeStart,
        endDate: this.rangeEnd,
        startPrice: this.priceData[this.rangeStart],
        endPrice: this.priceData[this.rangeEnd],
        totalDays: this.calculateDaysBetween(this.rangeStart, this.rangeEnd)
      })
    }
  }
}

样式设计规范

颜色系统

// 主色调
$primary-color: #1890ff;
$primary-light: #e6f7ff;
$primary-dark: #0050b3;

// 中性色
$text-primary: #262626;
$text-secondary: #8c8c8c;
$text-disabled: #bfbfbf;
$background-white: #ffffff;
$background-gray: #f5f5f5;
$border-color: #d9d9d9;

// 状态色
$success-color: #52c41a;
$warning-color: #faad14;
$error-color: #ff4d4f;

尺寸规范

// 弹窗尺寸
$modal-width: 350px;
$modal-max-height: 80vh;
$modal-border-radius: 12px;
$modal-padding: 20px;

// 日期格子尺寸
$date-cell-size: 44px;
$date-cell-gap: 2px;
$date-cell-border-radius: 6px;

// 字体大小
$font-size-title: 18px;
$font-size-subtitle: 14px;
$font-size-date: 16px;
$font-size-price: 12px;
$font-size-label: 10px;

开发实施计划

第一阶段基础框架1-2天

  • 创建组件基础结构
  • 实现弹窗容器和遮罩
  • 添加头部区域和关闭功能
  • 建立基础样式系统

第二阶段日历核心2-3天

  • 实现日期计算算法
  • 构建日期网格布局
  • 添加周标题和月份显示
  • 实现基础日期显示

第三阶段交互功能2-3天

  • 实现日期选择逻辑
  • 添加范围选择功能
  • 实现状态管理和视觉反馈
  • 添加价格数据绑定

第四阶段优化完善1-2天

  • 添加动画效果
  • 优化移动端体验
  • 完善边界情况处理
  • 性能优化和测试

使用示例

基础用法

<template>
  <div>
    <button @click="showCalendar = true">选择日期</button>
    
    <Calendar
      :visible="showCalendar"
      :price-data="priceData"
      mode="range"
      @range-select="handleRangeSelect"
      @close="showCalendar = false"
    />
  </div>
</template>

<script>
import Calendar from '@/components/Calendar'

export default {
  components: {
    Calendar
  },
  
  data() {
    return {
      showCalendar: false,
      priceData: {
        '2024-05-17': 449,
        '2024-05-18': 399,
        '2024-05-19': 459,
        '2024-05-20': 429
      }
    }
  },
  
  methods: {
    handleRangeSelect(range) {
      console.log('选择范围:', range)
      this.showCalendar = false
      
      // 处理选择结果
      this.processBooking(range)
    },
    
    processBooking(range) {
      // 处理预订逻辑
      const { startDate, endDate, totalDays } = range
      // ...
    }
  }
}
</script>

高级用法

<template>
  <Calendar
    :visible="visible"
    :price-data="priceData"
    :custom-labels="customLabels"
    :disabled-dates="disabledDates"
    :min-date="minDate"
    :max-date="maxDate"
    mode="range"
    @range-select="handleSelect"
    @month-change="handleMonthChange"
    @close="handleClose"
  />
</template>

<script>
export default {
  data() {
    return {
      visible: false,
      priceData: {
        // 动态价格数据
      },
      customLabels: {
        '2024-05-17': '入住',
        '2024-05-19': '离店'
      },
      disabledDates: [
        '2024-05-16', // 已满房
        '2024-05-25'  // 维护日
      ],
      minDate: '2024-05-01',
      maxDate: '2024-12-31'
    }
  },
  
  methods: {
    handleMonthChange(monthInfo) {
      // 动态加载月份数据
      this.loadMonthData(monthInfo.year, monthInfo.month)
    },
    
    async loadMonthData(year, month) {
      // 从API获取价格数据
      const data = await this.fetchPriceData(year, month)
      this.priceData = { ...this.priceData, ...data }
    }
  }
}
</script>

测试用例

单元测试

  • 日期计算函数测试
  • 选择逻辑测试
  • 价格数据绑定测试
  • 边界条件测试

集成测试

  • 用户交互流程测试
  • 不同设备适配测试
  • 性能压力测试

可访问性测试

  • 键盘导航测试
  • 屏幕阅读器兼容性
  • 色彩对比度检查