548 lines
12 KiB
Markdown
548 lines
12 KiB
Markdown
# 日历组件 (Calendar Component)
|
||
|
||
## 功能需求分析
|
||
|
||
基于设计图片分析,该日历组件是一个酒店预订场景的低价日历弹窗,支持通过日期图标点击触发显示,需要实现以下细分功能:
|
||
|
||
## 交互使用方式
|
||
|
||
### 基础用法
|
||
通过点击日期图标打开日历组件:
|
||
|
||
```vue
|
||
<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 属性
|
||
```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
|
||
<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>
|
||
```
|
||
|
||
### 高级用法
|
||
```vue
|
||
<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>
|
||
```
|
||
|
||
## 测试用例
|
||
|
||
### 单元测试
|
||
- [ ] 日期计算函数测试
|
||
- [ ] 选择逻辑测试
|
||
- [ ] 价格数据绑定测试
|
||
- [ ] 边界条件测试
|
||
|
||
### 集成测试
|
||
- [ ] 用户交互流程测试
|
||
- [ ] 不同设备适配测试
|
||
- [ ] 性能压力测试
|
||
|
||
### 可访问性测试
|
||
- [ ] 键盘导航测试
|
||
- [ ] 屏幕阅读器兼容性
|
||
- [ ] 色彩对比度检查 |