diff --git a/components/Calender/README.md b/components/Calender/README.md new file mode 100644 index 0000000..d341d7c --- /dev/null +++ b/components/Calender/README.md @@ -0,0 +1,548 @@ +# 日历组件 (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 + + + +``` + +## 测试用例 + +### 单元测试 +- [ ] 日期计算函数测试 +- [ ] 选择逻辑测试 +- [ ] 价格数据绑定测试 +- [ ] 边界条件测试 + +### 集成测试 +- [ ] 用户交互流程测试 +- [ ] 不同设备适配测试 +- [ ] 性能压力测试 + +### 可访问性测试 +- [ ] 键盘导航测试 +- [ ] 屏幕阅读器兼容性 +- [ ] 色彩对比度检查 \ No newline at end of file diff --git a/components/Calender/demo.vue b/components/Calender/demo.vue new file mode 100644 index 0000000..809c4a5 --- /dev/null +++ b/components/Calender/demo.vue @@ -0,0 +1,241 @@ + + + + + \ No newline at end of file diff --git a/components/Calender/example.vue b/components/Calender/example.vue new file mode 100644 index 0000000..d120867 --- /dev/null +++ b/components/Calender/example.vue @@ -0,0 +1,253 @@ + + + + + \ No newline at end of file diff --git a/components/Calender/images/日期-价格弹窗.png b/components/Calender/images/日期-价格弹窗.png new file mode 100644 index 0000000..543d484 Binary files /dev/null and b/components/Calender/images/日期-价格弹窗.png differ diff --git a/components/Calender/index.vue b/components/Calender/index.vue new file mode 100644 index 0000000..b95225c --- /dev/null +++ b/components/Calender/index.vue @@ -0,0 +1,423 @@ + + + + + \ No newline at end of file diff --git a/components/Calender/styles/index.scss b/components/Calender/styles/index.scss new file mode 100644 index 0000000..bba01e7 --- /dev/null +++ b/components/Calender/styles/index.scss @@ -0,0 +1,265 @@ +// 颜色系统 +$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-max-height: 80vh; +$modal-border-radius: 12px; +$modal-padding: 12px; + +// 日期格子尺寸 +$date-cell-size: 40px; +$date-cell-gap: 4px; +$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; + +// uni-popup会自动处理遮罩层和定位,这里移除相关样式 + +// 弹窗主体 +.calendar-popup { + position: relative; + width: 100%; + background-color: $background-white; + border-radius: $modal-border-radius; + overflow: hidden; +} + +// 头部区域 +.calendar-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: $modal-padding; + border-bottom: 1px solid $border-color; + background-color: $background-white; +} + +.header-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.header-title { + font-size: $font-size-title; + font-weight: 600; + color: $text-primary; + line-height: 1.4; +} + +.header-subtitle { + font-size: $font-size-subtitle; + color: $text-secondary; + line-height: 1.4; +} + +.header-close { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + transition: background-color 0.2s; + + &:active { + background-color: $background-gray; + } +} + +// 周标题行 - 固定显示 +.week-header { + display: flex; + padding: 16px $modal-padding 8px; + background-color: $background-white; + border-bottom: 1px solid #eeeeee; +} + +// 日历主体区域 +.calendar-body { + padding: 8px $modal-padding $modal-padding; + max-height: calc(#{$modal-max-height} - 140px); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { + display: none; + } +} + +.week-day { + flex: 1; + text-align: center; + font-size: $font-size-subtitle; + color: $text-secondary; + font-weight: 500; + line-height: 1.4; +} + +// 全年容器 +.year-container { + display: flex; + flex-direction: column; + gap: 32px; +} + +// 月份区域 +.month-section { + display: flex; + flex-direction: column; +} + +.month-title { + font-size: $font-size-title; + font-weight: 600; + color: $text-primary; + text-align: center; + margin-bottom: 16px; + margin-top: 8px; + line-height: 1.4; +} + +// 日期网格 +.date-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: $date-cell-gap; +} + +// 日期格子基础样式 +.date-cell { + position: relative; + width: $date-cell-size; + height: $date-cell-size; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 2px; + border-radius: $date-cell-border-radius; + transition: all 0.2s ease; + cursor: pointer; +} + +// 空白格子 +.date-cell-empty { + background-color: transparent; + cursor: default; +} + +// 有内容的格子 +.date-cell-content { + background-color: $background-white; + border: 1px solid transparent; + + &:hover { + background-color: $background-gray; + } + + &:active { + transform: scale(0.95); + } +} + +// 禁用状态 +.date-cell-disabled { + background-color: $background-gray !important; + color: $text-disabled !important; + cursor: not-allowed !important; + + .date-number, + .date-price { + color: $text-disabled !important; + } + + &:hover { + background-color: $background-gray !important; + transform: none !important; + } +} + +// 选中状态 +.date-cell-selected { + background-color: $primary-color !important; + border-color: $primary-color !important; + + .date-number, + .date-price, + .date-label { + color: $background-white !important; + } + + &:hover { + background-color: $primary-dark !important; + } +} + +// 范围内状态 +.date-cell-in-range { + background-color: $primary-light !important; + border-color: $primary-light !important; + + .date-number { + color: $primary-color !important; + } + + .date-price { + color: $primary-dark !important; + } +} + +// 日期数字 +.date-number { + font-size: $font-size-date; + font-weight: 500; + color: $text-primary; + line-height: 1; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +// 价格文字 +.date-price { + font-size: $font-size-price; + color: $text-secondary; + line-height: 1; + font-weight: 400; + text-align: center; + min-height: 14px; +} + +// 自定义标签 +.date-label { + font-size: $font-size-label; + color: $primary-color; + padding: 1px 4px; + border-radius: 2px; + line-height: 1; + white-space: nowrap; + font-weight: 500; + text-align: center; + min-height: 12px; +} diff --git a/components/Calender/year-demo.vue b/components/Calender/year-demo.vue new file mode 100644 index 0000000..ccdaf02 --- /dev/null +++ b/components/Calender/year-demo.vue @@ -0,0 +1,269 @@ + + + + + \ No newline at end of file diff --git a/pages/order/components/OrderInfo/index.vue b/pages/order/components/OrderInfo/index.vue index 56f820b..dfa1165 100644 --- a/pages/order/components/OrderInfo/index.vue +++ b/pages/order/components/OrderInfo/index.vue @@ -23,8 +23,13 @@ {{ formattedAmount }} - 投诉反馈 @@ -33,7 +38,13 @@