# 日历组件 (Calendar Component) ## 功能需求分析 基于设计图片分析,该日历组件是一个酒店预订场景的低价日历弹窗,支持通过日期图标点击触发显示,需要实现以下细分功能: ## 交互使用方式 ### 基础用法 通过点击日期图标打开日历组件: ```vue ``` ### 演示文件 - `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 属性 ```javascript 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 事件 ```javascript // 日期选择事件 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 }) ``` ### 核心算法实现 #### 日期计算工具函数 ```javascript // 获取月份天数 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 } ``` #### 选择状态管理 ```javascript 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) }) } } } ``` ### 样式设计规范 #### 颜色系统 ```scss // 主色调 $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; ``` #### 尺寸规范 ```scss // 弹窗尺寸 $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天) - [ ] 添加动画效果 - [ ] 优化移动端体验 - [ ] 完善边界情况处理 - [ ] 性能优化和测试 ## 使用示例 ### 基础用法 ```vue ``` ### 高级用法 ```vue ``` ## 测试用例 ### 单元测试 - [ ] 日期计算函数测试 - [ ] 选择逻辑测试 - [ ] 价格数据绑定测试 - [ ] 边界条件测试 ### 集成测试 - [ ] 用户交互流程测试 - [ ] 不同设备适配测试 - [ ] 性能压力测试 ### 可访问性测试 - [ ] 键盘导航测试 - [ ] 屏幕阅读器兼容性 - [ ] 色彩对比度检查