DatePicker.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <script setup lang="ts">
  2. /**
  3. * 日期选择器组件
  4. * @description 支持年月切换、日期选择、价格/库存状态展示
  5. */
  6. interface DayItem {
  7. day: number
  8. status?: string
  9. price?: number
  10. selected?: boolean
  11. disabled?: boolean
  12. }
  13. interface Props {
  14. /** 当前选中的日期 */
  15. modelValue?: Date
  16. /** 标题 */
  17. title?: string
  18. /** 日期数据 */
  19. dayData?: DayItem[]
  20. /** 是否显示星期标题 */
  21. showWeekHeader?: boolean
  22. /** 自定义类名 */
  23. customClass?: string
  24. }
  25. const props = withDefaults(defineProps<Props>(), {
  26. modelValue: () => new Date(),
  27. title: '选择日期',
  28. dayData: () => [],
  29. showWeekHeader: true,
  30. customClass: '',
  31. })
  32. const emit = defineEmits<{
  33. /** 选中日期变化 */
  34. (e: 'update:modelValue', value: Date): void
  35. /** 选中日期 */
  36. (e: 'select', value: { date: Date, day: number, item?: DayItem }): void
  37. /** 月份变化 */
  38. (e: 'monthChange', value: { year: number, month: number }): void
  39. }>()
  40. // 星期标题
  41. const weekDays = ['一', '二', '三', '四', '五', '六', '日']
  42. // 当前显示的日期
  43. const currentDate = ref(new Date(props.modelValue))
  44. // 当前年月显示文本
  45. const currentMonthText = computed(() => {
  46. return `${currentDate.value.getFullYear()}年${currentDate.value.getMonth() + 1}月`
  47. })
  48. // 当前年月值
  49. const currentYear = computed(() => currentDate.value.getFullYear())
  50. const currentMonth = computed(() => currentDate.value.getMonth())
  51. // 计算当前月日历数据
  52. const calendarDays = computed(() => {
  53. const year = currentYear.value
  54. const month = currentMonth.value
  55. // 获取当月第一天是星期几 (0=周日, 1=周一...)
  56. const firstDayOfMonth = new Date(year, month, 1).getDay()
  57. // 转换为周一开头 (0=周一, 6=周日)
  58. const firstDayIndex = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1
  59. // 获取当月天数
  60. const daysInMonth = new Date(year, month + 1, 0).getDate()
  61. // 获取上月天数(用于填充前置空白)
  62. const daysInPrevMonth = new Date(year, month, 0).getDate()
  63. const days: Array<{ day: number, isCurrentMonth: boolean, item?: DayItem, date: Date }> = []
  64. // 填充上月日期(灰色显示)
  65. for (let i = firstDayIndex - 1; i >= 0; i--) {
  66. const day = daysInPrevMonth - i
  67. days.push({
  68. day,
  69. isCurrentMonth: false,
  70. date: new Date(year, month - 1, day),
  71. })
  72. }
  73. // 填充当月日期
  74. for (let day = 1; day <= daysInMonth; day++) {
  75. // 查找对应的日期数据
  76. const dayItem = props.dayData.find(d => d.day === day)
  77. const date = new Date(year, month, day)
  78. // 判断是否选中
  79. const isSelected = props.modelValue
  80. && date.getFullYear() === props.modelValue.getFullYear()
  81. && date.getMonth() === props.modelValue.getMonth()
  82. && date.getDate() === props.modelValue.getDate()
  83. days.push({
  84. day,
  85. isCurrentMonth: true,
  86. item: dayItem ? { ...dayItem, selected: isSelected } : undefined,
  87. date,
  88. })
  89. }
  90. // 填充下月日期(补全到6行或5行)
  91. const remainingCells = 42 - days.length // 6行 x 7列 = 42
  92. for (let day = 1; day <= remainingCells; day++) {
  93. days.push({
  94. day,
  95. isCurrentMonth: false,
  96. date: new Date(year, month + 1, day),
  97. })
  98. }
  99. return days
  100. })
  101. // 切换到上月
  102. function prevMonth() {
  103. const newDate = new Date(currentDate.value)
  104. newDate.setMonth(newDate.getMonth() - 1)
  105. currentDate.value = newDate
  106. emit('monthChange', { year: newDate.getFullYear(), month: newDate.getMonth() + 1 })
  107. }
  108. // 切换到下月
  109. function nextMonth() {
  110. const newDate = new Date(currentDate.value)
  111. newDate.setMonth(newDate.getMonth() + 1)
  112. currentDate.value = newDate
  113. emit('monthChange', { year: newDate.getFullYear(), month: newDate.getMonth() + 1 })
  114. }
  115. // 选择日期
  116. function selectDay(dayInfo: typeof calendarDays.value[0]) {
  117. if (!dayInfo.isCurrentMonth)
  118. return
  119. const newDate = new Date(currentDate.value)
  120. newDate.setDate(dayInfo.day)
  121. emit('update:modelValue', newDate)
  122. emit('select', {
  123. date: newDate,
  124. day: dayInfo.day,
  125. item: dayInfo.item,
  126. })
  127. }
  128. // 监听外部modelValue变化
  129. watch(() => props.modelValue, (newVal) => {
  130. if (newVal) {
  131. currentDate.value = new Date(newVal)
  132. }
  133. })
  134. </script>
  135. <template>
  136. <view class="rounded-16rpx bg-#F6F6F6 p-20rpx" :class="customClass">
  137. <view class="text-32rpx font-semibold">
  138. {{ title }}
  139. </view>
  140. <!-- 年月切换 -->
  141. <view class="mt20rpx flex items-center justify-between">
  142. <view class="h60rpx w60rpx flex items-center justify-center" @click="prevMonth">
  143. <wd-icon name="arrow-left" size="20px" color="#999" />
  144. </view>
  145. <view class="rounded-24rpx bg-#F5F5F5 px32rpx py12rpx text-28rpx">
  146. {{ currentMonthText }}
  147. </view>
  148. <view class="h60rpx w60rpx flex items-center justify-center" @click="nextMonth">
  149. <wd-icon name="arrow-right" size="20px" color="#999" />
  150. </view>
  151. </view>
  152. <!-- 星期标题 -->
  153. <view v-if="showWeekHeader" class="mt20rpx flex items-center justify-between px12rpx">
  154. <view
  155. v-for="day in weekDays"
  156. :key="day"
  157. class="h60rpx w60rpx flex items-center justify-center text-28rpx"
  158. :class="day === '六' || day === '日' ? 'text-#52c41a' : 'text-#333'"
  159. >
  160. {{ day }}
  161. </view>
  162. </view>
  163. <!-- 日期网格 -->
  164. <view class="mt12rpx flex flex-wrap">
  165. <view
  166. v-for="(item, index) in calendarDays"
  167. :key="index"
  168. class="mb16rpx h90rpx w-14.28% flex flex-col items-center justify-center"
  169. @click="selectDay(item)"
  170. >
  171. <view
  172. class="h100rpx w76rpx flex flex-col items-center justify-center rounded-16rpx"
  173. :class="[
  174. item.item?.selected ? 'bg-#E2FF91' : '',
  175. item.item?.status === '售罄' || item.item?.disabled ? 'bg-#F5F5F5' : '',
  176. !item.isCurrentMonth ? 'opacity-30' : '',
  177. ]"
  178. >
  179. <text
  180. v-if="item.item?.status && item.isCurrentMonth"
  181. class="mt4rpx text-20rpx"
  182. :class="item.item.status === '充足' ? 'text-#52c41a' : 'text-#999'"
  183. >
  184. {{ item.item.status }}
  185. </text>
  186. <text
  187. class="text-28rpx"
  188. :class="[
  189. item.item?.selected ? 'text-#333 font-semibold' : 'text-#333',
  190. item.item?.status === '售罄' || item.item?.disabled ? 'text-#999' : '',
  191. !item.isCurrentMonth ? 'text-#999' : '',
  192. ]"
  193. >
  194. {{ item.day }}
  195. </text>
  196. <text
  197. v-if="item.item?.price && item.isCurrentMonth && item.item.status !== '售罄'"
  198. class="mt4rpx text-20rpx text-#FF4D3A"
  199. >
  200. ¥{{ item.item.price }}
  201. </text>
  202. </view>
  203. </view>
  204. </view>
  205. </view>
  206. </template>
  207. <style lang="scss" scoped></style>