refactor: restructure pinia stores, fix calendar, add utilities
- Restructure Pinia store organization from src/stores/ to src/store/ with modular stores - Update main.ts to use locally created Pinia instance instead of imported pre-configured one - Fix template syntax errors in Calendar component: correct missing class spacing and incorrect closing tags - Remove unused calendar demo/example files and documentation - Add new constant files, token management utilities, and client configuration constants - Add custom hooks useGoHome and useGoLogin for navigation and login logic
This commit is contained in:
@@ -1,586 +0,0 @@
|
||||
# 日历组件 (Calendar Component)
|
||||
|
||||
## 功能需求分析
|
||||
|
||||
基于设计图片分析,该日历组件是一个酒店预订场景的低价日历弹窗,支持通过日期图标点击触发显示,需要实现以下细分功能:
|
||||
|
||||
## 交互使用方式
|
||||
|
||||
### 基础用法
|
||||
|
||||
通过点击日期图标打开日历组件:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="date-picker" @tap="openCalendar">
|
||||
<div class="date-display">
|
||||
<text class="date-label">选择日期</text>
|
||||
<text class="date-value" v-if="selectedDate">{{ selectedDate }}</text>
|
||||
<text class="date-placeholder" v-else>请点击日期图标选择</text>
|
||||
</div>
|
||||
<div class="date-icon">
|
||||
<uni-icons type="calendar-filled" size="24" color="#1890ff"></uni-icons>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<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>
|
||||
```
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 单元测试
|
||||
|
||||
- [ ] 日期计算函数测试
|
||||
- [ ] 选择逻辑测试
|
||||
- [ ] 价格数据绑定测试
|
||||
- [ ] 边界条件测试
|
||||
|
||||
### 集成测试
|
||||
|
||||
- [ ] 用户交互流程测试
|
||||
- [ ] 不同设备适配测试
|
||||
- [ ] 性能压力测试
|
||||
|
||||
### 可访问性测试
|
||||
|
||||
- [ ] 键盘导航测试
|
||||
- [ ] 屏幕阅读器兼容性
|
||||
- [ ] 色彩对比度检查
|
||||
@@ -1,249 +0,0 @@
|
||||
<template>
|
||||
<div class="demo-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="demo-header">
|
||||
<spanclass="demo-title">日历组件交互演示</text>
|
||||
<spanclass="demo-subtitle">点击日期图标打开日历</text>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<div class="date-picker-container">
|
||||
<div class="date-picker" @tap="openCalendar">
|
||||
<div class="date-display">
|
||||
<spanclass="date-label">选择日期</text>
|
||||
<spanclass="date-value" v-if="selectedDate">{{
|
||||
formatDate(selectedDate)
|
||||
}}</text>
|
||||
<spanclass="date-placeholder" v-else>请点击日期图标选择</text>
|
||||
</div>
|
||||
<div class="date-icon">
|
||||
<uni-icons
|
||||
type="calendar-filled"
|
||||
size="24"
|
||||
color="#1890ff"
|
||||
></uni-icons>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择结果展示 -->
|
||||
<div class="result-display" v-if="selectedDate">
|
||||
<div class="result-card">
|
||||
<spanclass="result-title">已选择日期</text>
|
||||
<spanclass="result-date">{{ formatDate(selectedDate) }}</text>
|
||||
<spanclass="result-price" v-if="selectedPrice"
|
||||
>价格:¥{{ selectedPrice }}</text
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calendar
|
||||
:visible="calendarVisible"
|
||||
:price-data="priceData"
|
||||
mode="single"
|
||||
:default-value="selectedDate"
|
||||
@close="handleCalendarClose"
|
||||
@select="handleDateSelect"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
@@ -1,337 +0,0 @@
|
||||
<template>
|
||||
<div class="calendar-example">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<spanclass="page-title">日历组件使用示例</text>
|
||||
</div>
|
||||
|
||||
<!-- 触发器区域 -->
|
||||
<div class="trigger-section">
|
||||
<div class="date-input" @tap="openCalendar">
|
||||
<div class="input-content">
|
||||
<spanclass="input-label">选择日期</text>
|
||||
<spanclass="input-value" v-if="selectedDate">{{
|
||||
selectedDate
|
||||
}}</text>
|
||||
<spanclass="input-placeholder" v-else>请选择日期</text>
|
||||
</div>
|
||||
<uni-icons type="calendar" size="20" color="#1890ff"></uni-icons>
|
||||
</div>
|
||||
|
||||
<!-- 范围选择示例 -->
|
||||
<div class="date-input" @tap="openRangeCalendar">
|
||||
<div class="input-content">
|
||||
<spanclass="input-label">选择日期范围</text>
|
||||
<text
|
||||
class="input-value"
|
||||
v-if="selectedRange.start && selectedRange.end"
|
||||
>
|
||||
{{ selectedRange.start }} 至 {{ selectedRange.end }}
|
||||
</text>
|
||||
<spanclass="input-placeholder" v-else>请选择日期范围</text>
|
||||
</div>
|
||||
<uni-icons type="calendar" size="20" color="#1890ff"></uni-icons>
|
||||
</div>
|
||||
|
||||
<!-- 价格区间选择示例 -->
|
||||
<div class="date-input" @tap="openPriceRangeCalendar">
|
||||
<div class="input-content">
|
||||
<spanclass="input-label">选择有价格的日期范围</text>
|
||||
<text
|
||||
class="input-value"
|
||||
v-if="selectedRange.start && selectedRange.end"
|
||||
>
|
||||
{{ selectedRange.start }} 至 {{ selectedRange.end }}
|
||||
</text>
|
||||
<spanclass="input-placeholder" v-else
|
||||
>请选择价格区间(仅含有价夜晚)</text
|
||||
>
|
||||
</div>
|
||||
<uni-icons type="calendar" size="20" color="#1890ff"></uni-icons>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果显示区域 -->
|
||||
<div
|
||||
class="result-section"
|
||||
v-if="selectedDate || (selectedRange.start && selectedRange.end)"
|
||||
>
|
||||
<spanclass="result-title">选择结果:</text>
|
||||
<div class="result-item" v-if="selectedDate">
|
||||
<spanclass="result-label">单选日期:</text>
|
||||
<spanclass="result-value">{{ selectedDate }}</text>
|
||||
<spanclass="result-value" v-if="selectedDatePrice">
|
||||
¥{{ selectedDatePrice }}
|
||||
</text>
|
||||
</div>
|
||||
<div class="result-item" v-if="selectedRange.start && selectedRange.end">
|
||||
<spanclass="result-label">日期范围:</text>
|
||||
<spanclass="result-value"
|
||||
>{{ selectedRange.start }} 至 {{ selectedRange.end }}</text
|
||||
>
|
||||
<spanclass="result-days">(共{{ rangeDays }}天)</text>
|
||||
<spanclass="result-value" v-if="selectedRangeTotal">
|
||||
总价:¥{{ selectedRangeTotal }}
|
||||
</text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历组件 - 单选模式 -->
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 日历组件 - 价格区间选择模式(必须有价格的夜晚) -->
|
||||
<Calendar
|
||||
:visible="priceRangeCalendarVisible"
|
||||
:price-data="priceData"
|
||||
mode="range"
|
||||
:range-require-price="true"
|
||||
:default-value="[selectedRange.start, selectedRange.end]"
|
||||
@close="handleRangeCalendarClose"
|
||||
@range-select="handlePriceRangeSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import Calendar from "./index.vue";
|
||||
|
||||
// 响应式数据
|
||||
const calendarVisible = ref(false);
|
||||
const rangeCalendarVisible = ref(false);
|
||||
const priceRangeCalendarVisible = ref(false);
|
||||
const selectedDate = ref("");
|
||||
const selectedDatePrice = ref(null);
|
||||
const selectedRange = ref({
|
||||
start: "",
|
||||
end: "",
|
||||
});
|
||||
const selectedRangeTotal = 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,
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
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 openPriceRangeCalendar = () => {
|
||||
priceRangeCalendarVisible.value = true;
|
||||
};
|
||||
|
||||
// 处理单选日历关闭
|
||||
const handleCalendarClose = () => {
|
||||
calendarVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理范围选择日历关闭
|
||||
const handleRangeCalendarClose = () => {
|
||||
rangeCalendarVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理日期选择
|
||||
const handleDateSelect = (data) => {
|
||||
selectedDate.value = data.date;
|
||||
selectedDatePrice.value =
|
||||
data.price !== undefined && data.price !== null && data.price !== "-"
|
||||
? data.price
|
||||
: null;
|
||||
calendarVisible.value = false;
|
||||
console.log("选择的日期:", data);
|
||||
};
|
||||
|
||||
// 处理范围选择
|
||||
const handleRangeSelect = (data) => {
|
||||
selectedRange.value = {
|
||||
start: data.startDate,
|
||||
end: data.endDate,
|
||||
};
|
||||
if (data.dateRange && Array.isArray(data.dateRange)) {
|
||||
const total = data.dateRange.reduce((sum, d) => {
|
||||
const p = d.price;
|
||||
return sum + (typeof p === "number" ? p : 0);
|
||||
}, 0);
|
||||
selectedRangeTotal.value = total || null;
|
||||
} else {
|
||||
selectedRangeTotal.value = null;
|
||||
}
|
||||
rangeCalendarVisible.value = false;
|
||||
console.log("选择的日期范围:", data);
|
||||
};
|
||||
|
||||
// 处理价格区间选择
|
||||
const handlePriceRangeSelect = (data) => {
|
||||
selectedRange.value = {
|
||||
start: data.startDate,
|
||||
end: data.endDate,
|
||||
};
|
||||
if (data.dateRange && Array.isArray(data.dateRange)) {
|
||||
const total = data.dateRange.reduce((sum, d) => {
|
||||
const p = d.price;
|
||||
return sum + (typeof p === "number" ? p : 0);
|
||||
}, 0);
|
||||
selectedRangeTotal.value = total || null;
|
||||
} else {
|
||||
selectedRangeTotal.value = null;
|
||||
}
|
||||
priceRangeCalendarVisible.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>
|
||||
@@ -1,19 +1,12 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="bottom"
|
||||
:safe-area="false"
|
||||
@maskClick="handleMaskClick"
|
||||
>
|
||||
<uni-popup ref="popup" type="bottom" :safe-area="false" @maskClick="handleMaskClick">
|
||||
<!-- 弹窗主体 -->
|
||||
<div class="calendar-popup" @tap.stop>
|
||||
<!-- 头部区域 -->
|
||||
<div class="calendar-header">
|
||||
<div class="header-content">
|
||||
<spanclass="header-title">日历选择</text>
|
||||
<spanv-if="props.rangeRequirePrice" class="header-subtitle"
|
||||
>选择住宿日期,以下价格为单晚参考价</text
|
||||
>
|
||||
<span class="header-title">日历选择</span>
|
||||
<span v-if="props.rangeRequirePrice" class="header-subtitle">选择住宿日期,以下价格为单晚参考价</span>
|
||||
</div>
|
||||
<div class="header-close" @tap="handleClose">
|
||||
<uni-icons type="closeempty" size="20" color="#8c8c8c"></uni-icons>
|
||||
@@ -22,41 +15,28 @@
|
||||
|
||||
<!-- 周标题行 - 固定显示 -->
|
||||
<div class="week-header">
|
||||
<spanclass="week-day" v-for="day in weekDays" :key="day">
|
||||
<span class="week-day" v-for="day in weekDays" :key="day">
|
||||
{{ day }}
|
||||
</text>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 日历主体区域 -->
|
||||
<div class="calendar-body">
|
||||
<!-- 全年月份显示区域 -->
|
||||
<div class="year-container">
|
||||
<div
|
||||
class="month-section"
|
||||
v-for="monthData in yearMonthsGrid"
|
||||
:key="monthData.key"
|
||||
>
|
||||
<spanclass="month-title">{{ monthData.title }}</text>
|
||||
<div class="month-section" v-for="monthData in yearMonthsGrid" :key="monthData.key">
|
||||
<span class="month-title">{{ monthData.title }}</span>
|
||||
<div class="date-grid">
|
||||
<div
|
||||
class="date-cell"
|
||||
v-for="(dateInfo, index) in monthData.grid"
|
||||
:key="index"
|
||||
:class="getDateCellClass(dateInfo)"
|
||||
@tap="handleDateClick(dateInfo)"
|
||||
>
|
||||
<div class="date-cell" v-for="(dateInfo, index) in monthData.grid" :key="index"
|
||||
:class="getDateCellClass(dateInfo)" @tap="handleDateClick(dateInfo)">
|
||||
<template v-if="dateInfo">
|
||||
<spanclass="date-label" v-if="dateInfo.label">
|
||||
<span class="date-label" v-if="dateInfo.label">
|
||||
{{ dateInfo.label }}
|
||||
</text>
|
||||
<spanclass="date-number">{{ dateInfo.day }}</text>
|
||||
<text
|
||||
class="date-price"
|
||||
v-if="
|
||||
dateInfo.price !== null && dateInfo.price !== undefined
|
||||
"
|
||||
>¥{{ dateInfo.price }}</text
|
||||
>
|
||||
</span>
|
||||
<span class="date-number">{{ dateInfo.day }}</span>
|
||||
<span class="date-price" v-if="
|
||||
dateInfo.price !== null && dateInfo.price !== undefined
|
||||
">¥{{ dateInfo.price }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<spanclass="demo-title">跨年日历演示</text>
|
||||
<spanclass="demo-subtitle"
|
||||
>支持从当前月份到明年同月份的日期连续选择</text
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 触发按钮 -->
|
||||
<div class="trigger-section">
|
||||
<div class="date-input" @tap="openCalendar">
|
||||
<div class="date-icon">📅</div>
|
||||
<div class="date-text">
|
||||
<text
|
||||
v-if="!selectedRange.start && !selectedRange.end"
|
||||
class="placeholder"
|
||||
>
|
||||
选择入住和离店日期
|
||||
</text>
|
||||
<spanv-else class="selected-text">
|
||||
{{ formatDateRange() }}
|
||||
</text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择结果显示 -->
|
||||
<div
|
||||
class="result-section"
|
||||
v-if="selectedRange.start || selectedRange.end"
|
||||
>
|
||||
<div class="result-title">选择结果:</div>
|
||||
<div class="result-item" v-if="selectedRange.start">
|
||||
<spanclass="result-label">入住日期:</text>
|
||||
<spanclass="result-value">{{
|
||||
formatDate(selectedRange.start)
|
||||
}}</text>
|
||||
</div>
|
||||
<div class="result-item" v-if="selectedRange.end">
|
||||
<spanclass="result-label">离店日期:</text>
|
||||
<spanclass="result-value">{{ formatDate(selectedRange.end) }}</text>
|
||||
</div>
|
||||
<div
|
||||
class="result-item"
|
||||
v-if="selectedRange.start && selectedRange.end"
|
||||
>
|
||||
<spanclass="result-label">住宿天数:</text>
|
||||
<spanclass="result-value">{{ calculateDays() }}晚</text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calender
|
||||
:visible="calendarVisible"
|
||||
mode="range"
|
||||
:price-data="priceData"
|
||||
:min-date="minDate"
|
||||
@close="handleCalendarClose"
|
||||
@select="handleDateSelect"
|
||||
/>
|
||||
</div>
|
||||
</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-span{
|
||||
flex: 1;
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.selected-span{
|
||||
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>
|
||||
Reference in New Issue
Block a user