month.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <template>
  2. <view>
  3. <wd-toast selector="wd-month" />
  4. <view class="month">
  5. <view class="wd-month">
  6. <view class="wd-month__title" v-if="showTitle">{{ monthTitle(date) }}</view>
  7. <view class="wd-month__days">
  8. <view
  9. v-for="(item, index) in days"
  10. :key="index"
  11. :class="`wd-month__day ${item.disabled ? 'is-disabled' : ''} ${item.isLastRow ? 'is-last-row' : ''} ${
  12. item.type ? dayTypeClass(item.type) : ''
  13. }`"
  14. :style="index === 0 ? firstDayStyle : ''"
  15. @click="handleDateClick(index)"
  16. >
  17. <view class="wd-month__day-container">
  18. <view class="wd-month__day-top">{{ item.topInfo }}</view>
  19. <view class="wd-month__day-text">
  20. {{ item.text }}
  21. </view>
  22. <view class="wd-month__day-bottom">{{ item.bottomInfo }}</view>
  23. </view>
  24. </view>
  25. </view>
  26. </view>
  27. </view>
  28. </view>
  29. </template>
  30. <script lang="ts">
  31. export default {
  32. options: {
  33. addGlobalClass: true,
  34. virtualHost: true,
  35. styleIsolation: 'shared'
  36. }
  37. }
  38. </script>
  39. <script lang="ts" setup>
  40. import wdToast from '../../wd-toast/wd-toast.vue'
  41. import { computed, ref, watch, type CSSProperties } from 'vue'
  42. import {
  43. compareDate,
  44. formatMonthTitle,
  45. getDateByDefaultTime,
  46. getDayByOffset,
  47. getDayOffset,
  48. getItemClass,
  49. getMonthEndDay,
  50. getNextDay,
  51. getPrevDay,
  52. getWeekRange
  53. } from '../utils'
  54. import { useToast } from '../../wd-toast'
  55. import { deepClone, isArray, isFunction, objToStyle } from '../../common/util'
  56. import { useTranslate } from '../../composables/useTranslate'
  57. import type { CalendarDayItem, CalendarDayType } from '../types'
  58. import { monthProps } from './types'
  59. const props = defineProps(monthProps)
  60. const emit = defineEmits(['change'])
  61. const { translate } = useTranslate('calendar-view')
  62. const days = ref<Array<CalendarDayItem>>([])
  63. const toast = useToast('wd-month')
  64. const offset = computed(() => {
  65. const firstDayOfWeek = props.firstDayOfWeek >= 7 ? props.firstDayOfWeek % 7 : props.firstDayOfWeek
  66. const offset = (7 + new Date(props.date).getDay() - firstDayOfWeek) % 7
  67. return offset
  68. })
  69. const dayTypeClass = computed(() => {
  70. return (monthType: CalendarDayType) => {
  71. return getItemClass(monthType, props.value, props.type)
  72. }
  73. })
  74. const monthTitle = computed(() => {
  75. return (date: number) => {
  76. return formatMonthTitle(date)
  77. }
  78. })
  79. const firstDayStyle = computed(() => {
  80. const dayStyle: CSSProperties = {}
  81. dayStyle.marginLeft = `${(100 / 7) * offset.value}%`
  82. return objToStyle(dayStyle)
  83. })
  84. const isLastRow = (date: number) => {
  85. const currentDate = new Date(date)
  86. const currentDay = currentDate.getDate()
  87. const daysInMonth = getMonthEndDay(currentDate.getFullYear(), currentDate.getMonth() + 1)
  88. const totalDaysShown = offset.value + daysInMonth
  89. const totalRows = Math.ceil(totalDaysShown / 7)
  90. return Math.ceil((offset.value + currentDay) / 7) === totalRows
  91. }
  92. watch(
  93. [() => props.type, () => props.date, () => props.value, () => props.minDate, () => props.maxDate, () => props.formatter],
  94. () => {
  95. setDays()
  96. },
  97. {
  98. deep: true,
  99. immediate: true
  100. }
  101. )
  102. function setDays() {
  103. const dayList: Array<CalendarDayItem> = []
  104. const date = new Date(props.date)
  105. const year = date.getFullYear()
  106. const month = date.getMonth()
  107. const totalDay = getMonthEndDay(year, month + 1)
  108. let value = props.value
  109. if ((props.type === 'week' || props.type === 'weekrange') && value) {
  110. value = getWeekValue()
  111. }
  112. for (let day = 1; day <= totalDay; day++) {
  113. const date = new Date(year, month, day).getTime()
  114. let type: CalendarDayType = getDayType(date, value as number | number[] | null)
  115. if (!type && compareDate(date, Date.now()) === 0) {
  116. type = 'current'
  117. }
  118. const dayObj = getFormatterDate(date, day, type)
  119. dayList.push(dayObj)
  120. }
  121. days.value = dayList
  122. }
  123. function getDayType(date: number, value: number | number[] | null): CalendarDayType {
  124. switch (props.type) {
  125. case 'date':
  126. case 'datetime':
  127. return getDateType(date)
  128. case 'dates':
  129. return getDatesType(date)
  130. case 'daterange':
  131. case 'datetimerange':
  132. return getDatetimeType(date, value)
  133. case 'week':
  134. return getWeektimeType(date, value)
  135. case 'weekrange':
  136. return getWeektimeType(date, value)
  137. default:
  138. return getDateType(date)
  139. }
  140. }
  141. function getDateType(date: number): CalendarDayType {
  142. if (props.value && compareDate(date, props.value as number) === 0) {
  143. return 'selected'
  144. }
  145. return ''
  146. }
  147. function getDatesType(date: number): CalendarDayType {
  148. const { value } = props
  149. let type: CalendarDayType = ''
  150. if (!isArray(value)) return type
  151. const isSelected = (day: number) => {
  152. return value.some((item) => compareDate(day, item) === 0)
  153. }
  154. if (isSelected(date)) {
  155. const prevDay = getPrevDay(date)
  156. const nextDay = getNextDay(date)
  157. const prevSelected = isSelected(prevDay)
  158. const nextSelected = isSelected(nextDay)
  159. if (prevSelected && nextSelected) {
  160. type = 'multiple-middle'
  161. } else if (prevSelected) {
  162. type = 'end'
  163. } else if (nextSelected) {
  164. type = 'start'
  165. } else {
  166. type = 'multiple-selected'
  167. }
  168. }
  169. return type
  170. }
  171. function getDatetimeType(date: number, value: number | number[] | null) {
  172. const [startDate, endDate] = isArray(value) ? value : []
  173. if (startDate && compareDate(date, startDate) === 0) {
  174. if (props.allowSameDay && endDate && compareDate(startDate, endDate) === 0) {
  175. return 'same'
  176. }
  177. return 'start'
  178. } else if (endDate && compareDate(date, endDate) === 0) {
  179. return 'end'
  180. } else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
  181. return 'middle'
  182. } else {
  183. return ''
  184. }
  185. }
  186. function getWeektimeType(date: number, value: number | number[] | null) {
  187. const [startDate, endDate] = isArray(value) ? value : []
  188. if (startDate && compareDate(date, startDate) === 0) {
  189. return 'start'
  190. } else if (endDate && compareDate(date, endDate) === 0) {
  191. return 'end'
  192. } else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
  193. return 'middle'
  194. } else {
  195. return ''
  196. }
  197. }
  198. function getWeekValue() {
  199. if (props.type === 'week') {
  200. return getWeekRange(props.value as number, props.firstDayOfWeek)
  201. } else {
  202. const [startDate, endDate] = (props.value as any) || []
  203. if (startDate) {
  204. const firstWeekRange = getWeekRange(startDate, props.firstDayOfWeek)
  205. if (endDate) {
  206. const endWeekRange = getWeekRange(endDate, props.firstDayOfWeek)
  207. return [firstWeekRange[0], endWeekRange[1]]
  208. } else {
  209. return firstWeekRange
  210. }
  211. }
  212. return []
  213. }
  214. }
  215. function handleDateClick(index: number) {
  216. const date = days.value[index]
  217. switch (props.type) {
  218. case 'date':
  219. case 'datetime':
  220. handleDateChange(date)
  221. break
  222. case 'dates':
  223. handleDatesChange(date)
  224. break
  225. case 'daterange':
  226. case 'datetimerange':
  227. handleDateRangeChange(date)
  228. break
  229. case 'week':
  230. handleWeekChange(date)
  231. break
  232. case 'weekrange':
  233. handleWeekRangeChange(date)
  234. break
  235. default:
  236. handleDateChange(date)
  237. }
  238. }
  239. function getDate(date: number, isEnd: boolean = false) {
  240. date = props.defaultTime && props.defaultTime.length > 0 ? getDateByDefaultTime(date, isEnd ? props.defaultTime[1] : props.defaultTime[0]) : date
  241. if (date < props.minDate) return props.minDate
  242. if (date > props.maxDate) return props.maxDate
  243. return date
  244. }
  245. function handleDateChange(date: CalendarDayItem) {
  246. if (date.disabled) return
  247. if (date.type !== 'selected') {
  248. emit('change', {
  249. value: getDate(date.date),
  250. type: 'start'
  251. })
  252. }
  253. }
  254. function handleDatesChange(date: CalendarDayItem) {
  255. if (date.disabled) return
  256. const currentValue = deepClone(isArray(props.value) ? props.value : [])
  257. const dateIndex = currentValue.findIndex((item) => item && compareDate(item, date.date) === 0)
  258. const value = dateIndex === -1 ? [...currentValue, getDate(date.date)] : currentValue.filter((_, index) => index !== dateIndex)
  259. emit('change', { value })
  260. }
  261. function handleDateRangeChange(date: CalendarDayItem) {
  262. if (date.disabled) return
  263. let value: (number | null)[] = []
  264. let type: CalendarDayType = ''
  265. const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
  266. const compare = compareDate(date.date, startDate)
  267. // 禁止选择同个日期
  268. if (!props.allowSameDay && compare === 0 && (props.type === 'daterange' || props.type === 'datetimerange') && !endDate) {
  269. return
  270. }
  271. if (startDate && !endDate && compare > -1) {
  272. // 不能选择超过最大范围的日期
  273. if (props.maxRange && getDayOffset(date.date, startDate) > props.maxRange) {
  274. const maxEndDate = getDayByOffset(startDate, props.maxRange - 1)
  275. value = [startDate, getDate(maxEndDate, true)]
  276. toast.show({
  277. msg: props.rangePrompt || translate('rangePrompt', props.maxRange)
  278. })
  279. } else {
  280. value = [startDate, getDate(date.date, true)]
  281. }
  282. } else if (props.type === 'datetimerange' && startDate && endDate) {
  283. // 时间范围类型,且有开始时间和结束时间,需要支持重新点击开始日期和结束日期可以重新修改时间
  284. if (compare === 0) {
  285. type = 'start'
  286. value = props.value as number[]
  287. } else if (compareDate(date.date, endDate) === 0) {
  288. type = 'end'
  289. value = props.value as number[]
  290. } else {
  291. value = [getDate(date.date), null]
  292. }
  293. } else {
  294. value = [getDate(date.date), null]
  295. }
  296. emit('change', {
  297. value,
  298. type: type || (value[1] ? 'end' : 'start')
  299. })
  300. }
  301. function handleWeekChange(date: CalendarDayItem) {
  302. const [weekStart] = getWeekRange(date.date, props.firstDayOfWeek)
  303. // 周的第一天如果是禁用状态,则不可选中
  304. if (getFormatterDate(weekStart, new Date(weekStart).getDate()).disabled) return
  305. emit('change', {
  306. value: getDate(weekStart) + 24 * 60 * 60 * 1000
  307. })
  308. }
  309. function handleWeekRangeChange(date: CalendarDayItem) {
  310. const [weekStartDate] = getWeekRange(date.date, props.firstDayOfWeek)
  311. // 周的第一天如果是禁用状态,则不可选中
  312. if (getFormatterDate(weekStartDate, new Date(weekStartDate).getDate()).disabled) return
  313. let value: (number | null)[] = []
  314. const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
  315. const [startWeekStartDate] = startDate ? getWeekRange(startDate, props.firstDayOfWeek) : []
  316. const compare = compareDate(weekStartDate, startWeekStartDate)
  317. if (startDate && !endDate && compare > -1) {
  318. if (!props.allowSameDay && compare === 0) return
  319. value = [getDate(startWeekStartDate) + 24 * 60 * 60 * 1000, getDate(weekStartDate) + 24 * 60 * 60 * 1000]
  320. } else {
  321. value = [getDate(weekStartDate) + 24 * 60 * 60 * 1000, null]
  322. }
  323. emit('change', {
  324. value
  325. })
  326. }
  327. function getFormatterDate(date: number, day: string | number, type?: CalendarDayType) {
  328. let dayObj: CalendarDayItem = {
  329. date: date,
  330. text: day,
  331. topInfo: '',
  332. bottomInfo: '',
  333. type,
  334. disabled: compareDate(date, props.minDate) === -1 || compareDate(date, props.maxDate) === 1,
  335. isLastRow: isLastRow(date)
  336. }
  337. if (props.formatter) {
  338. if (isFunction(props.formatter)) {
  339. dayObj = props.formatter(dayObj)
  340. } else {
  341. console.error('[wot-design] error(wd-calendar-view): the formatter prop of wd-calendar-view should be a function')
  342. }
  343. }
  344. return dayObj
  345. }
  346. </script>
  347. <style lang="scss" scoped>
  348. @import './index.scss';
  349. </style>