feat: 订单详情交互对接
This commit is contained in:
parent
7ff542e57a
commit
9d6abe3e2a
548
components/Calender/README.md
Normal file
548
components/Calender/README.md
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
# 日历组件 (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>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
- [ ] 日期计算函数测试
|
||||||
|
- [ ] 选择逻辑测试
|
||||||
|
- [ ] 价格数据绑定测试
|
||||||
|
- [ ] 边界条件测试
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
- [ ] 用户交互流程测试
|
||||||
|
- [ ] 不同设备适配测试
|
||||||
|
- [ ] 性能压力测试
|
||||||
|
|
||||||
|
### 可访问性测试
|
||||||
|
- [ ] 键盘导航测试
|
||||||
|
- [ ] 屏幕阅读器兼容性
|
||||||
|
- [ ] 色彩对比度检查
|
||||||
241
components/Calender/demo.vue
Normal file
241
components/Calender/demo.vue
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<view class="demo-page">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="demo-header">
|
||||||
|
<text class="demo-title">日历组件交互演示</text>
|
||||||
|
<text class="demo-subtitle">点击日期图标打开日历</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<view class="date-picker-container">
|
||||||
|
<view class="date-picker" @tap="openCalendar">
|
||||||
|
<view class="date-display">
|
||||||
|
<text class="date-label">选择日期</text>
|
||||||
|
<text class="date-value" v-if="selectedDate">{{ formatDate(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>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 选择结果展示 -->
|
||||||
|
<view class="result-display" v-if="selectedDate">
|
||||||
|
<view class="result-card">
|
||||||
|
<text class="result-title">已选择日期</text>
|
||||||
|
<text class="result-date">{{ formatDate(selectedDate) }}</text>
|
||||||
|
<text class="result-price" v-if="selectedPrice">价格:¥{{ selectedPrice }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日历组件 -->
|
||||||
|
<Calendar
|
||||||
|
:visible="calendarVisible"
|
||||||
|
:price-data="priceData"
|
||||||
|
mode="single"
|
||||||
|
:default-value="selectedDate"
|
||||||
|
@close="handleCalendarClose"
|
||||||
|
@select="handleDateSelect"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import Calendar from './index.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const calendarVisible = ref(false)
|
||||||
|
const selectedDate = ref('')
|
||||||
|
const selectedPrice = ref(null)
|
||||||
|
|
||||||
|
// 模拟价格数据
|
||||||
|
const priceData = ref({
|
||||||
|
'2024-01-15': 299,
|
||||||
|
'2024-01-16': 399,
|
||||||
|
'2024-01-17': 199,
|
||||||
|
'2024-01-18': 299,
|
||||||
|
'2024-01-19': 399,
|
||||||
|
'2024-01-20': 499,
|
||||||
|
'2024-01-21': 599,
|
||||||
|
'2024-01-22': 299,
|
||||||
|
'2024-01-23': 199,
|
||||||
|
'2024-01-24': 299,
|
||||||
|
'2024-01-25': 399,
|
||||||
|
'2024-01-26': 299,
|
||||||
|
'2024-01-27': 199,
|
||||||
|
'2024-01-28': 299,
|
||||||
|
'2024-02-01': 399,
|
||||||
|
'2024-02-02': 299,
|
||||||
|
'2024-02-03': 199,
|
||||||
|
'2024-02-04': 299,
|
||||||
|
'2024-02-05': 399,
|
||||||
|
'2024-02-06': 499,
|
||||||
|
'2024-02-07': 599,
|
||||||
|
'2024-02-08': 299,
|
||||||
|
'2024-02-09': 199,
|
||||||
|
'2024-02-10': 299
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
// 打开日历
|
||||||
|
const openCalendar = () => {
|
||||||
|
calendarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理日历关闭
|
||||||
|
const handleCalendarClose = () => {
|
||||||
|
calendarVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理日期选择
|
||||||
|
const handleDateSelect = (data) => {
|
||||||
|
selectedDate.value = data.date
|
||||||
|
selectedPrice.value = data.price
|
||||||
|
calendarVisible.value = false
|
||||||
|
|
||||||
|
// 输出选择结果到控制台
|
||||||
|
console.log('选择的日期:', data.date)
|
||||||
|
console.log('日期价格:', data.price)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期显示
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
const weekDay = weekDays[date.getDay()]
|
||||||
|
|
||||||
|
return `${year}年${month}月${day}日 周${weekDay}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.demo-page {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker-container {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e8e8e8;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-display {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #262626;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-placeholder {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker:active .date-icon {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-display {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-date {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-price {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff2e8;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
253
components/Calender/example.vue
Normal file
253
components/Calender/example.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<template>
|
||||||
|
<view class="calendar-example">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="page-header">
|
||||||
|
<text class="page-title">日历组件使用示例</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 触发器区域 -->
|
||||||
|
<view class="trigger-section">
|
||||||
|
<view class="date-input" @tap="openCalendar">
|
||||||
|
<view class="input-content">
|
||||||
|
<text class="input-label">选择日期</text>
|
||||||
|
<text class="input-value" v-if="selectedDate">{{ selectedDate }}</text>
|
||||||
|
<text class="input-placeholder" v-else>请选择日期</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="calendar" size="20" color="#1890ff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 范围选择示例 -->
|
||||||
|
<view class="date-input" @tap="openRangeCalendar">
|
||||||
|
<view class="input-content">
|
||||||
|
<text class="input-label">选择日期范围</text>
|
||||||
|
<text class="input-value" v-if="selectedRange.start && selectedRange.end">
|
||||||
|
{{ selectedRange.start }} 至 {{ selectedRange.end }}
|
||||||
|
</text>
|
||||||
|
<text class="input-placeholder" v-else>请选择日期范围</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="calendar" size="20" color="#1890ff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 结果显示区域 -->
|
||||||
|
<view class="result-section" v-if="selectedDate || (selectedRange.start && selectedRange.end)">
|
||||||
|
<text class="result-title">选择结果:</text>
|
||||||
|
<view class="result-item" v-if="selectedDate">
|
||||||
|
<text class="result-label">单选日期:</text>
|
||||||
|
<text class="result-value">{{ selectedDate }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-item" v-if="selectedRange.start && selectedRange.end">
|
||||||
|
<text class="result-label">日期范围:</text>
|
||||||
|
<text class="result-value">{{ selectedRange.start }} 至 {{ selectedRange.end }}</text>
|
||||||
|
<text class="result-days">(共{{ rangeDays }}天)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日历组件 - 单选模式 -->
|
||||||
|
<Calendar
|
||||||
|
:visible="calendarVisible"
|
||||||
|
:price-data="priceData"
|
||||||
|
mode="single"
|
||||||
|
:default-value="selectedDate"
|
||||||
|
@close="handleCalendarClose"
|
||||||
|
@select="handleDateSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 日历组件 - 范围选择模式 -->
|
||||||
|
<Calendar
|
||||||
|
:visible="rangeCalendarVisible"
|
||||||
|
:price-data="priceData"
|
||||||
|
mode="range"
|
||||||
|
:default-value="[selectedRange.start, selectedRange.end]"
|
||||||
|
@close="handleRangeCalendarClose"
|
||||||
|
@range-select="handleRangeSelect"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import Calendar from './index.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const calendarVisible = ref(false)
|
||||||
|
const rangeCalendarVisible = ref(false)
|
||||||
|
const selectedDate = ref('')
|
||||||
|
const selectedRange = ref({
|
||||||
|
start: '',
|
||||||
|
end: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模拟价格数据
|
||||||
|
const priceData = ref({
|
||||||
|
'2024-01-15': 299,
|
||||||
|
'2024-01-16': 399,
|
||||||
|
'2024-01-17': 199,
|
||||||
|
'2024-01-18': 299,
|
||||||
|
'2024-01-19': 399,
|
||||||
|
'2024-01-20': 499,
|
||||||
|
'2024-01-21': 599,
|
||||||
|
'2024-01-22': 299,
|
||||||
|
'2024-01-23': 199,
|
||||||
|
'2024-01-24': 299,
|
||||||
|
'2024-01-25': 399,
|
||||||
|
'2024-01-26': 299,
|
||||||
|
'2024-01-27': 199,
|
||||||
|
'2024-01-28': 299
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const rangeDays = computed(() => {
|
||||||
|
if (!selectedRange.value.start || !selectedRange.value.end) return 0
|
||||||
|
const start = new Date(selectedRange.value.start)
|
||||||
|
const end = new Date(selectedRange.value.end)
|
||||||
|
const diffTime = Math.abs(end - start)
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
// 打开单选日历
|
||||||
|
const openCalendar = () => {
|
||||||
|
calendarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开范围选择日历
|
||||||
|
const openRangeCalendar = () => {
|
||||||
|
rangeCalendarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单选日历关闭
|
||||||
|
const handleCalendarClose = () => {
|
||||||
|
calendarVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理范围选择日历关闭
|
||||||
|
const handleRangeCalendarClose = () => {
|
||||||
|
rangeCalendarVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理日期选择
|
||||||
|
const handleDateSelect = (data) => {
|
||||||
|
selectedDate.value = data.date
|
||||||
|
calendarVisible.value = false
|
||||||
|
console.log('选择的日期:', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理范围选择
|
||||||
|
const handleRangeSelect = (data) => {
|
||||||
|
selectedRange.value = {
|
||||||
|
start: data.startDate,
|
||||||
|
end: data.endDate
|
||||||
|
}
|
||||||
|
rangeCalendarVisible.value = false
|
||||||
|
console.log('选择的日期范围:', data)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.calendar-example {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-value {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #262626;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-days {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #52c41a;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
components/Calender/images/日期-价格弹窗.png
Normal file
BIN
components/Calender/images/日期-价格弹窗.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
423
components/Calender/index.vue
Normal file
423
components/Calender/index.vue
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
<template>
|
||||||
|
<uni-popup ref="popup" type="bottom" @maskClick="handleMaskClick">
|
||||||
|
<!-- 弹窗主体 -->
|
||||||
|
<view class="calendar-popup" @tap.stop>
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<view class="calendar-header">
|
||||||
|
<view class="header-content">
|
||||||
|
<text class="header-title">日历选择</text>
|
||||||
|
<text class="header-subtitle"
|
||||||
|
>选择入住和离店日期,以下价格为单晚参考价</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<view class="header-close" @tap="handleClose">
|
||||||
|
<uni-icons type="closeempty" size="20" color="#8c8c8c"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 周标题行 - 固定显示 -->
|
||||||
|
<view class="week-header">
|
||||||
|
<text class="week-day" v-for="day in weekDays" :key="day">
|
||||||
|
{{ day }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日历主体区域 -->
|
||||||
|
<view class="calendar-body">
|
||||||
|
<!-- 全年月份显示区域 -->
|
||||||
|
<view class="year-container">
|
||||||
|
<view
|
||||||
|
class="month-section"
|
||||||
|
v-for="monthData in yearMonthsGrid"
|
||||||
|
:key="monthData.key"
|
||||||
|
>
|
||||||
|
<text class="month-title">{{ monthData.title }}</text>
|
||||||
|
<view class="date-grid">
|
||||||
|
<view
|
||||||
|
class="date-cell"
|
||||||
|
v-for="(dateInfo, index) in monthData.grid"
|
||||||
|
:key="index"
|
||||||
|
:class="getDateCellClass(dateInfo)"
|
||||||
|
@tap="handleDateClick(dateInfo)"
|
||||||
|
>
|
||||||
|
<template v-if="dateInfo">
|
||||||
|
<text class="date-label" v-if="dateInfo.label">{{
|
||||||
|
dateInfo.label
|
||||||
|
}}</text>
|
||||||
|
<text class="date-number">{{ dateInfo.day }}</text>
|
||||||
|
<text class="date-price" v-if="dateInfo.price"
|
||||||
|
>¥{{ dateInfo.price }}</text
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
} from "vue";
|
||||||
|
|
||||||
|
// 定义组件名称
|
||||||
|
defineOptions({
|
||||||
|
name: "Calendar",
|
||||||
|
});
|
||||||
|
|
||||||
|
// uni-popup组件引用
|
||||||
|
const popup = ref(null);
|
||||||
|
|
||||||
|
// 定义Props
|
||||||
|
const props = defineProps({
|
||||||
|
// 弹窗显示控制
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 价格数据对象
|
||||||
|
priceData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 默认选中日期
|
||||||
|
defaultValue: {
|
||||||
|
type: [String, Array],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择模式
|
||||||
|
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: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义Emits
|
||||||
|
const emit = defineEmits([
|
||||||
|
"close",
|
||||||
|
"select",
|
||||||
|
"range-select",
|
||||||
|
"date-click",
|
||||||
|
"month-change",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const weekDays = ref(["一", "二", "三", "四", "五", "六", "日"]);
|
||||||
|
const selectedDates = ref([]);
|
||||||
|
const currentYear = ref(new Date().getFullYear());
|
||||||
|
const isRangeSelecting = ref(false);
|
||||||
|
const rangeStart = ref(null);
|
||||||
|
const rangeEnd = ref(null);
|
||||||
|
const clickTimer = ref(null);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
// 生成从当前月份到明年同月份的日历数据
|
||||||
|
const yearMonthsGrid = computed(() => {
|
||||||
|
const months = [];
|
||||||
|
const now = new Date();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const startMonth = now.getMonth() + 1; // 从当前月份开始(getMonth()返回0-11,需要+1)
|
||||||
|
const totalMonths = 13; // 显示13个月(当前月份到明年同月份)
|
||||||
|
|
||||||
|
for (let i = 0; i < totalMonths; i++) {
|
||||||
|
const month = ((startMonth - 1 + i) % 12) + 1;
|
||||||
|
const year = currentYear + Math.floor((startMonth - 1 + i) / 12);
|
||||||
|
|
||||||
|
months.push({
|
||||||
|
key: `${year}-${month}`,
|
||||||
|
title: `${year}年${month}月`,
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
grid: generateCalendarGrid(year, month),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 方法定义
|
||||||
|
// 处理遮罩点击
|
||||||
|
const handleMaskClick = () => {
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
if (popup.value) {
|
||||||
|
popup.value.close();
|
||||||
|
}
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取月份天数
|
||||||
|
const getDaysInMonth = (year, month) => {
|
||||||
|
return new Date(year, month, 0).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取月份第一天是星期几 (0=周日, 1=周一...)
|
||||||
|
const getFirstDayOfMonth = (year, month) => {
|
||||||
|
const day = new Date(year, month - 1, 1).getDay();
|
||||||
|
return day === 0 ? 6 : day - 1; // 转换为周一开始 (0=周一, 6=周日)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成日历网格数据
|
||||||
|
const generateCalendarGrid = (year, month) => {
|
||||||
|
const daysInMonth = getDaysInMonth(year, month);
|
||||||
|
const firstDay = 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: props.priceData[dateStr] || null,
|
||||||
|
disabled: isDateDisabled(dateStr),
|
||||||
|
selected: isDateSelected(dateStr),
|
||||||
|
inRange: isDateInRange(dateStr),
|
||||||
|
label: getDateLabel(dateStr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断日期是否禁用
|
||||||
|
const isDateDisabled = (dateStr) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const minDate = new Date(props.minDate);
|
||||||
|
const maxDate = props.maxDate ? new Date(props.maxDate) : null;
|
||||||
|
|
||||||
|
if (date < minDate) return true;
|
||||||
|
if (maxDate && date > maxDate) return true;
|
||||||
|
if (props.disabledDates.includes(dateStr)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断日期是否选中
|
||||||
|
const isDateSelected = (dateStr) => {
|
||||||
|
if (props.mode === "single") {
|
||||||
|
return selectedDates.value.includes(dateStr);
|
||||||
|
} else {
|
||||||
|
return dateStr === rangeStart.value || dateStr === rangeEnd.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断日期是否在选择范围内
|
||||||
|
const isDateInRange = (dateStr) => {
|
||||||
|
if (props.mode !== "range" || !rangeStart.value || !rangeEnd.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const start = new Date(rangeStart.value);
|
||||||
|
const end = new Date(rangeEnd.value);
|
||||||
|
|
||||||
|
return date > start && date < end;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取日期标签
|
||||||
|
const getDateLabel = (dateStr) => {
|
||||||
|
// 优先使用自定义标签
|
||||||
|
if (props.customLabels[dateStr]) {
|
||||||
|
return props.customLabels[dateStr];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为范围选择模式自动生成入住/离店标签
|
||||||
|
if (props.mode === "range") {
|
||||||
|
if (dateStr === rangeStart.value) {
|
||||||
|
return "入住";
|
||||||
|
}
|
||||||
|
if (dateStr === rangeEnd.value) {
|
||||||
|
return "离店";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取日期格子样式类
|
||||||
|
const getDateCellClass = (dateInfo) => {
|
||||||
|
if (!dateInfo) return "date-cell-empty";
|
||||||
|
|
||||||
|
const classes = ["date-cell-content"];
|
||||||
|
|
||||||
|
if (dateInfo.disabled) classes.push("date-cell-disabled");
|
||||||
|
if (dateInfo.selected) classes.push("date-cell-selected");
|
||||||
|
if (dateInfo.inRange) classes.push("date-cell-in-range");
|
||||||
|
|
||||||
|
return classes.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理日期点击
|
||||||
|
const handleDateClick = (dateInfo) => {
|
||||||
|
if (!dateInfo || dateInfo.disabled) return;
|
||||||
|
|
||||||
|
// 防抖处理
|
||||||
|
if (clickTimer.value) {
|
||||||
|
clearTimeout(clickTimer.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickTimer.value = setTimeout(() => {
|
||||||
|
if (props.mode === "single") {
|
||||||
|
handleSingleSelect(dateInfo);
|
||||||
|
} else if (props.mode === "range") {
|
||||||
|
handleRangeSelection(dateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发点击事件
|
||||||
|
emit("date-click", {
|
||||||
|
date: dateInfo.date,
|
||||||
|
price: dateInfo.price,
|
||||||
|
disabled: dateInfo.disabled,
|
||||||
|
selected: dateInfo.selected,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理单选
|
||||||
|
const handleSingleSelect = (dateInfo) => {
|
||||||
|
selectedDates.value = [dateInfo.date];
|
||||||
|
emit("select", {
|
||||||
|
date: dateInfo.date,
|
||||||
|
price: dateInfo.price,
|
||||||
|
mode: "single",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理范围选择
|
||||||
|
const handleRangeSelection = (dateInfo) => {
|
||||||
|
if (!rangeStart.value || (rangeStart.value && rangeEnd.value)) {
|
||||||
|
// 开始新的范围选择
|
||||||
|
rangeStart.value = dateInfo.date;
|
||||||
|
rangeEnd.value = null;
|
||||||
|
isRangeSelecting.value = true;
|
||||||
|
} else {
|
||||||
|
// 完成范围选择
|
||||||
|
rangeEnd.value = dateInfo.date;
|
||||||
|
isRangeSelecting.value = false;
|
||||||
|
|
||||||
|
// 确保开始日期小于结束日期
|
||||||
|
if (new Date(rangeStart.value) > new Date(rangeEnd.value)) {
|
||||||
|
[rangeStart.value, rangeEnd.value] = [rangeEnd.value, rangeStart.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查日期跨度是否超过28天
|
||||||
|
const daysBetween = calculateDaysBetween(rangeStart.value, rangeEnd.value);
|
||||||
|
if (daysBetween > 28) {
|
||||||
|
// 使用uni.showToast显示错误提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '预定时间不能超过28天',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置选择状态
|
||||||
|
rangeStart.value = null;
|
||||||
|
rangeEnd.value = null;
|
||||||
|
isRangeSelecting.value = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("range-select", {
|
||||||
|
startDate: rangeStart.value,
|
||||||
|
endDate: rangeEnd.value,
|
||||||
|
startPrice: props.priceData[rangeStart.value],
|
||||||
|
endPrice: props.priceData[rangeEnd.value],
|
||||||
|
totalDays: daysBetween,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算两个日期之间的天数
|
||||||
|
const calculateDaysBetween = (startDate, endDate) => {
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
|
const diffTime = Math.abs(end - start);
|
||||||
|
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听visible属性变化,控制uni-popup显示
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(newVisible) => {
|
||||||
|
if (newVisible) {
|
||||||
|
popup.value?.open();
|
||||||
|
} else {
|
||||||
|
popup.value?.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化选中状态
|
||||||
|
if (props.defaultValue) {
|
||||||
|
if (Array.isArray(props.defaultValue)) {
|
||||||
|
rangeStart.value = props.defaultValue[0];
|
||||||
|
rangeEnd.value = props.defaultValue[1];
|
||||||
|
} else {
|
||||||
|
selectedDates.value = [props.defaultValue];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (clickTimer.value) {
|
||||||
|
clearTimeout(clickTimer.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 引入样式文件
|
||||||
|
@import "./styles/index.scss";
|
||||||
|
</style>
|
||||||
265
components/Calender/styles/index.scss
Normal file
265
components/Calender/styles/index.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
269
components/Calender/year-demo.vue
Normal file
269
components/Calender/year-demo.vue
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<view class="demo-container">
|
||||||
|
<view class="demo-header">
|
||||||
|
<text class="demo-title">跨年日历演示</text>
|
||||||
|
<text class="demo-subtitle">支持从当前月份到明年同月份的日期连续选择</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="demo-content">
|
||||||
|
<!-- 触发按钮 -->
|
||||||
|
<view class="trigger-section">
|
||||||
|
<view class="date-input" @tap="openCalendar">
|
||||||
|
<view class="date-icon">📅</view>
|
||||||
|
<view class="date-text">
|
||||||
|
<text v-if="!selectedRange.start && !selectedRange.end" class="placeholder">
|
||||||
|
选择入住和离店日期
|
||||||
|
</text>
|
||||||
|
<text v-else class="selected-text">
|
||||||
|
{{ formatDateRange() }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 选择结果显示 -->
|
||||||
|
<view class="result-section" v-if="selectedRange.start || selectedRange.end">
|
||||||
|
<view class="result-title">选择结果:</view>
|
||||||
|
<view class="result-item" v-if="selectedRange.start">
|
||||||
|
<text class="result-label">入住日期:</text>
|
||||||
|
<text class="result-value">{{ formatDate(selectedRange.start) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-item" v-if="selectedRange.end">
|
||||||
|
<text class="result-label">离店日期:</text>
|
||||||
|
<text class="result-value">{{ formatDate(selectedRange.end) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-item" v-if="selectedRange.start && selectedRange.end">
|
||||||
|
<text class="result-label">住宿天数:</text>
|
||||||
|
<text class="result-value">{{ calculateDays() }}晚</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日历组件 -->
|
||||||
|
<Calender
|
||||||
|
:visible="calendarVisible"
|
||||||
|
mode="range"
|
||||||
|
:price-data="priceData"
|
||||||
|
:min-date="minDate"
|
||||||
|
@close="handleCalendarClose"
|
||||||
|
@select="handleDateSelect"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import Calender from './index.vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const calendarVisible = ref(false)
|
||||||
|
const selectedRange = ref({
|
||||||
|
start: '',
|
||||||
|
end: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 最小日期(今天)
|
||||||
|
const minDate = new Date().toISOString().split('T')[0]
|
||||||
|
|
||||||
|
// 动态生成价格数据(从当前月份到明年同月份)
|
||||||
|
const generatePriceData = () => {
|
||||||
|
const priceData = {};
|
||||||
|
const now = new Date();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const currentMonth = now.getMonth() + 1;
|
||||||
|
|
||||||
|
// 生成13个月的示例价格数据
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
|
const month = ((currentMonth - 1 + i) % 12) + 1;
|
||||||
|
const year = currentYear + Math.floor((currentMonth - 1 + i) / 12);
|
||||||
|
|
||||||
|
// 为每个月生成几个示例价格
|
||||||
|
const sampleDates = [1, 15, 25]; // 每月的1号、15号、25号
|
||||||
|
sampleDates.forEach(day => {
|
||||||
|
if (day <= new Date(year, month, 0).getDate()) { // 确保日期存在
|
||||||
|
const dateKey = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
||||||
|
// 生成随机价格(299-1599之间)
|
||||||
|
priceData[dateKey] = Math.floor(Math.random() * 1300) + 299;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return priceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const priceData = reactive(generatePriceData());
|
||||||
|
|
||||||
|
// 打开日历
|
||||||
|
const openCalendar = () => {
|
||||||
|
calendarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭日历
|
||||||
|
const handleCalendarClose = () => {
|
||||||
|
calendarVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理日期选择
|
||||||
|
const handleDateSelect = (dates) => {
|
||||||
|
if (dates.length >= 2) {
|
||||||
|
selectedRange.value = {
|
||||||
|
start: dates[0],
|
||||||
|
end: dates[1]
|
||||||
|
}
|
||||||
|
} else if (dates.length === 1) {
|
||||||
|
selectedRange.value = {
|
||||||
|
start: dates[0],
|
||||||
|
end: ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedRange.value = {
|
||||||
|
start: '',
|
||||||
|
end: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
const weekDay = weekDays[date.getDay()]
|
||||||
|
return `${month}月${day}日 周${weekDay}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期范围
|
||||||
|
const formatDateRange = () => {
|
||||||
|
if (selectedRange.value.start && selectedRange.value.end) {
|
||||||
|
return `${formatDate(selectedRange.value.start)} - ${formatDate(selectedRange.value.end)}`
|
||||||
|
} else if (selectedRange.value.start) {
|
||||||
|
return `${formatDate(selectedRange.value.start)} - 选择离店日期`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算住宿天数
|
||||||
|
const calculateDays = () => {
|
||||||
|
if (!selectedRange.value.start || !selectedRange.value.end) return 0
|
||||||
|
const start = new Date(selectedRange.value.start)
|
||||||
|
const end = new Date(selectedRange.value.end)
|
||||||
|
const diffTime = Math.abs(end - start)
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||||
|
return diffDays
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.demo-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.demo-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-content {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-text {
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -23,8 +23,13 @@
|
|||||||
<text class="value">{{ formattedAmount }}</text>
|
<text class="value">{{ formattedAmount }}</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 根据订单状态动态显示按钮 -->
|
<!-- 根据订单状态动态显示按钮 -->
|
||||||
<button v-if="shouldShowButton" :class="buttonClass">
|
<button
|
||||||
{{ buttonText }}
|
v-if="shouldShowButton"
|
||||||
|
:class="['reserve-button', { loading: isLoading }]"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="handleButtonClick"
|
||||||
|
>
|
||||||
|
{{ isLoading ? "处理中..." : buttonText }}
|
||||||
</button>
|
</button>
|
||||||
<view class="feedback">
|
<view class="feedback">
|
||||||
<text @click="openFeedback">投诉反馈</text>
|
<text @click="openFeedback">投诉反馈</text>
|
||||||
@ -33,7 +38,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed } from "vue";
|
import { defineProps, computed, ref } from "vue";
|
||||||
|
import {
|
||||||
|
preOrder,
|
||||||
|
orderPayNow,
|
||||||
|
orderCancel,
|
||||||
|
orderRefund,
|
||||||
|
} from "@/request/api/OrderApi";
|
||||||
|
|
||||||
// 支付方式映射常量
|
// 支付方式映射常量
|
||||||
const PAY_WAY_MAP = {
|
const PAY_WAY_MAP = {
|
||||||
@ -42,6 +53,9 @@ const PAY_WAY_MAP = {
|
|||||||
2: "云闪付",
|
2: "云闪付",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
orderData: {
|
orderData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -86,18 +100,60 @@ const shouldShowButton = computed(() => {
|
|||||||
return props.orderData.orderStatus !== "4"; // 4-退款中
|
return props.orderData.orderStatus !== "4"; // 4-退款中
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按钮样式类逻辑
|
// 处理按钮点击事件
|
||||||
const buttonClass = computed(() => {
|
const handleButtonClick = async () => {
|
||||||
const status = props.orderData.orderStatus;
|
if (isLoading.value) return; // 防止重复点击
|
||||||
const baseClass = "reserve-button";
|
|
||||||
|
const status = props.orderData.orderStatus;
|
||||||
|
const orderId = props.orderData.orderId;
|
||||||
|
// 支付方式
|
||||||
|
const payWay = props.orderData.payWay;
|
||||||
|
// 支付渠道
|
||||||
|
const paySource = "1";
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
// 申请退款状态(待使用状态)保持原样式,其他状态添加pre-btn类
|
|
||||||
if (status === "2") {
|
if (status === "2") {
|
||||||
return baseClass; // 申请退款状态,背景色不变
|
// 情况2:待使用状态,直接申请退款
|
||||||
|
await orderRefund({ orderId });
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: "退款申请已提交",
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return `${baseClass} pre-btn`; // 其他状态,添加pre-btn样式
|
// 情况1:待支付状态或其他状态,先预下单再支付
|
||||||
|
// 第一步:预下单
|
||||||
|
const res = await orderPayNow({ orderId, payWay, paySource });
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
// 仅作为示例,非真实参数信息。
|
||||||
|
uni.requestPayment({
|
||||||
|
provider: "wxpay",
|
||||||
|
timeStamp: String(Date.now()),
|
||||||
|
nonceStr: "A1B2C3D4E5",
|
||||||
|
package: "prepay_id=wx20180101abcdefg",
|
||||||
|
signType: "MD5",
|
||||||
|
paySign: "",
|
||||||
|
success: (res) => {
|
||||||
|
console.log("success:" + JSON.stringify(res));
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.log("fail:" + JSON.stringify(err));
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error("操作失败:", error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || "操作失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 投诉电话
|
// 投诉电话
|
||||||
const openFeedback = () => {
|
const openFeedback = () => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ $order-bg-color: #fff;
|
|||||||
$text-color-primary: #333;
|
$text-color-primary: #333;
|
||||||
$text-color-secondary: #666;
|
$text-color-secondary: #666;
|
||||||
$text-color-accent: #ff5722;
|
$text-color-accent: #ff5722;
|
||||||
$button-color: #ffa500;
|
$button-color: #00a6ff;
|
||||||
$button-hover-color: darken($button-color, 8%);
|
$button-hover-color: darken($button-color, 8%);
|
||||||
$button-disabled-color: #ccc;
|
$button-disabled-color: #ccc;
|
||||||
$border-color: #ececec;
|
$border-color: #ececec;
|
||||||
@ -31,8 +31,15 @@ $font-weight-semibold: 600;
|
|||||||
$transition-fast: 0.2s ease;
|
$transition-fast: 0.2s ease;
|
||||||
$transition-normal: 0.3s ease;
|
$transition-normal: 0.3s ease;
|
||||||
|
|
||||||
// 响应式断点
|
// 动画关键帧
|
||||||
$breakpoint-mobile: 480px;
|
@keyframes loading-spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.order-info {
|
.order-info {
|
||||||
background-color: $order-bg-color;
|
background-color: $order-bg-color;
|
||||||
@ -54,13 +61,6 @@ $breakpoint-mobile: 480px;
|
|||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
transition: background-color $transition-fast;
|
transition: background-color $transition-fast;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 -4px 8px;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
color: $text-color-secondary;
|
color: $text-color-secondary;
|
||||||
@ -102,18 +102,6 @@ $breakpoint-mobile: 480px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端适配
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
|
||||||
.value {
|
|
||||||
max-width: 50%;
|
|
||||||
font-size: $font-size-small - 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.amount .value {
|
|
||||||
font-size: $font-size-medium + 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
@ -130,13 +118,10 @@ $breakpoint-mobile: 480px;
|
|||||||
|
|
||||||
.reserve-button {
|
.reserve-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: linear-gradient(
|
background: linear-gradient(179deg, #00a6ff 0%, #0256ff 100%);
|
||||||
135deg,
|
|
||||||
$button-color 0%,
|
|
||||||
darken($button-color, 5%) 100%
|
|
||||||
);
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -150,12 +135,6 @@ $breakpoint-mobile: 480px;
|
|||||||
transition: all $transition-normal;
|
transition: all $transition-normal;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
// 再次预定按钮背景样式
|
|
||||||
&.pre-btn {
|
|
||||||
background: linear-gradient(179deg, #00a6ff 0%, #0256ff 100%);
|
|
||||||
border: 2px solid #00a6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮波纹效果
|
// 按钮波纹效果
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
@ -206,13 +185,31 @@ $breakpoint-mobile: 480px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端触摸优化
|
// 加载状态样式
|
||||||
@media (max-width: $breakpoint-mobile) {
|
&.loading {
|
||||||
height: $button-height + 4px;
|
background: $button-disabled-color;
|
||||||
font-size: $font-size-medium + 1px;
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&::before {
|
||||||
transform: none; // 移动端禁用悬浮效果
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: -8px 0 0 -8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: loading-spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,5 +3,5 @@
|
|||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
background-size: 100% 242px;
|
background-size: 100% 242px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
padding: 60px 15px;
|
// padding: 60px 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,51 @@
|
|||||||
import request from "../base/request";
|
import request from "../base/request";
|
||||||
// 获取用户订单列表
|
// 获取用户订单列表
|
||||||
const userOrderList = (args) => {
|
const userOrderList = (args) => {
|
||||||
return request.post('/hotelBiz/order/userOrderList', args);
|
return request.post("/hotelBiz/order/userOrderList", args);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 获取用户订单列表
|
// 获取用户订单列表
|
||||||
const userWorkOrderList = (args) => {
|
const userWorkOrderList = (args) => {
|
||||||
return request.post('/hotelBiz/workOrder/userWorkOrderList', args);
|
return request.post("/hotelBiz/workOrder/userWorkOrderList", args);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
// 获取订单详情
|
// 获取订单详情
|
||||||
const userOrderDetail = (args) => {
|
const userOrderDetail = (args) => {
|
||||||
return request.post('/hotelBiz/order/userOrderDetail', args);
|
return request.post("/hotelBiz/order/userOrderDetail", args);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// 预下单
|
||||||
|
const preOrder = (args) => {
|
||||||
|
return request.post("/hotelBiz/trade/preOrder", args);
|
||||||
|
};
|
||||||
|
|
||||||
export { userOrderList, userWorkOrderList, userOrderDetail }
|
// 订单支付
|
||||||
|
const orderPay = (args) => {
|
||||||
|
return request.post("/hotelBiz/trade/order", args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 订单取消
|
||||||
|
const orderCancel = (args) => {
|
||||||
|
return request.post("/hotelBiz/trade/cancelRefund", args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 申请退款
|
||||||
|
const orderRefund = (args) => {
|
||||||
|
return request.post("/hotelBiz/trade/applyRefund", args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 未支付订单立即支付
|
||||||
|
const orderPayNow = (args) => {
|
||||||
|
return request.post("/hotelBiz/trade/applyPay", args);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
userOrderList,
|
||||||
|
userWorkOrderList,
|
||||||
|
userOrderDetail,
|
||||||
|
preOrder,
|
||||||
|
orderPay,
|
||||||
|
orderCancel,
|
||||||
|
orderRefund,
|
||||||
|
orderPayNow,
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user