wd-datetime-picker-view.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. <template>
  2. <wd-picker-view
  3. ref="datePickerview"
  4. :custom-class="customClass"
  5. :custom-style="customStyle"
  6. :immediate-change="immediateChange"
  7. v-model="pickerValue"
  8. :columns="columns"
  9. :columns-height="columnsHeight"
  10. :item-height="itemHeight"
  11. :columnChange="columnChange"
  12. :loading="loading"
  13. :loading-color="loadingColor"
  14. @change="onChange"
  15. @pickstart="onPickStart"
  16. @pickend="onPickEnd"
  17. ></wd-picker-view>
  18. </template>
  19. <script lang="ts">
  20. export default {
  21. name: 'wd-datetime-picker-view',
  22. virtualHost: true,
  23. addGlobalClass: true,
  24. styleIsolation: 'shared'
  25. }
  26. </script>
  27. <script lang="ts" setup>
  28. import wdPickerView from '../wd-picker-view/wd-picker-view.vue'
  29. import { getCurrentInstance, onBeforeMount, ref, watch } from 'vue'
  30. import { debounce, isDef, padZero, range, isArray, isString } from '../common/util'
  31. import { datetimePickerViewProps, type DatetimePickerViewColumnType, type DatetimePickerViewOption, type DatetimePickerViewExpose } from './types'
  32. import type { PickerViewInstance } from '../wd-picker-view/types'
  33. import { getPickerValue } from './util'
  34. // 本地时间戳
  35. /** @description 判断时间戳是否合法 */
  36. const isValidDate = (date: string | number | Date) => isDef(date) && !Number.isNaN(date)
  37. /**
  38. * @description 生成n个元素,并使用iterator接口进行填充
  39. * @param n
  40. * @param iteratee
  41. * @return {any[]}
  42. */
  43. const times = (n: number, iteratee: (index: number) => number) => {
  44. let index: number = -1
  45. const length = n < 0 ? 0 : n
  46. const result: number[] = Array(length)
  47. while (++index < n) {
  48. result[index] = iteratee(index)
  49. }
  50. return result
  51. }
  52. /**
  53. * @description 获取某年某月有多少天
  54. * @param {Number} year
  55. * @param {Number} month
  56. * @return {Number} day
  57. */
  58. const getMonthEndDay = (year: number, month: number) => {
  59. return 32 - new Date(year, month - 1, 32).getDate()
  60. }
  61. const props = defineProps(datetimePickerViewProps)
  62. const emit = defineEmits(['change', 'pickstart', 'pickend', 'update:modelValue'])
  63. // pickerview
  64. const datePickerview = ref<PickerViewInstance>()
  65. // 内部保持时间戳的
  66. const innerValue = ref<null | string | number>(null)
  67. // 传递给pickerView的columns的数据
  68. const columns = ref<DatetimePickerViewOption[][]>([])
  69. // 传递给pickerView的value的数据
  70. const pickerValue = ref<string | number | boolean | string[] | number[] | boolean[]>([])
  71. // 是否已经初始化
  72. const created = ref<boolean>(false)
  73. const { proxy } = getCurrentInstance() as any
  74. /**
  75. * @description updateValue 防抖函数的占位符
  76. */
  77. const updateValue = debounce(() => {
  78. if (!created.value) return
  79. const val = correctValue(props.modelValue)
  80. const isEqual = val === innerValue.value
  81. if (!isEqual) {
  82. updateColumnValue(val)
  83. } else {
  84. columns.value = updateColumns()
  85. }
  86. }, 50)
  87. watch(
  88. () => props.modelValue,
  89. (val, oldVal) => {
  90. if (val === oldVal) return
  91. // 外部传入值更改时 更新picker数据
  92. const value = correctValue(val)
  93. updateColumnValue(value)
  94. },
  95. { deep: true, immediate: true }
  96. )
  97. watch(
  98. () => props.type,
  99. (target) => {
  100. const type = ['date', 'year-month', 'time', 'datetime', 'year']
  101. if (type.indexOf(target) === -1) {
  102. console.error(`type must be one of ${type}`)
  103. }
  104. },
  105. { deep: true, immediate: true }
  106. )
  107. watch(
  108. [
  109. () => props.type,
  110. () => props.filter,
  111. () => props.formatter,
  112. () => props.columnFormatter,
  113. () => props.minDate,
  114. () => props.maxDate,
  115. () => props.minHour,
  116. () => props.maxHour,
  117. () => props.minMinute,
  118. () => props.maxMinute,
  119. () => props.minSecond,
  120. () => props.maxSecond,
  121. () => props.useSecond
  122. ],
  123. () => {
  124. updateValue()
  125. },
  126. {
  127. deep: true,
  128. immediate: true
  129. }
  130. )
  131. onBeforeMount(() => {
  132. // 初始化完毕,打开observer触发render的开关
  133. created.value = true
  134. const innerValue = correctValue(props.modelValue)
  135. updateColumnValue(innerValue)
  136. })
  137. /** pickerView触发change事件,同步修改pickerValue */
  138. function onChange({ value }: { value: string | string[] }) {
  139. // 更新pickerView的value
  140. pickerValue.value = value
  141. // pickerValue => innerValue
  142. const result = updateInnerValue()
  143. emit('update:modelValue', result)
  144. // 这个地方的value返回的是picker数组,实际上在此处我们应该返回 change 的是 value date类型的值
  145. emit('change', {
  146. value: result,
  147. picker: proxy.$.exposed
  148. })
  149. }
  150. /**
  151. * @description 使用formatter格式化getOriginColumns的结果
  152. * @return {Array<Array<Number>>} 用于传入picker的columns
  153. */
  154. function updateColumns(): DatetimePickerViewOption[][] {
  155. const { formatter, columnFormatter } = props
  156. if (columnFormatter) {
  157. return columnFormatter(proxy.$.exposed)
  158. } else {
  159. return getOriginColumns().map((column) => {
  160. return column.values.map((value) => {
  161. return {
  162. label: formatter ? formatter(column.type, padZero(value)) : padZero(value),
  163. value
  164. }
  165. })
  166. })
  167. }
  168. }
  169. /**
  170. * 设置数据列
  171. * @param columnList 数据列
  172. */
  173. function setColumns(columnList: DatetimePickerViewOption[][]) {
  174. columns.value = columnList
  175. }
  176. /**
  177. * @description 根据getRanges得到的范围计算所有的列的数据
  178. * @return {{values: any[], type: String}[]} 年
  179. */
  180. function getOriginColumns() {
  181. const { filter } = props
  182. return getRanges().map(({ type, range }) => {
  183. let values = times(range[1] - range[0] + 1, (index: number) => {
  184. return range[0] + index
  185. })
  186. if (filter) {
  187. values = filter(type, values)
  188. }
  189. return {
  190. type,
  191. values
  192. }
  193. })
  194. }
  195. /**
  196. * @description 根据时间戳生成年月日时分的边界范围
  197. * @return {Array<{type:String,range:Array<Number>}>}
  198. */
  199. function getRanges(): Array<{ type: DatetimePickerViewColumnType; range: number[] }> {
  200. if (props.type === 'time') {
  201. const result: Array<{ type: DatetimePickerViewColumnType; range: number[] }> = [
  202. {
  203. type: 'hour',
  204. range: [props.minHour, props.maxHour]
  205. },
  206. {
  207. type: 'minute',
  208. range: [props.minMinute, props.maxMinute]
  209. }
  210. ]
  211. if (props.useSecond) {
  212. result.push({
  213. type: 'second',
  214. range: [props.minSecond, props.maxSecond]
  215. })
  216. }
  217. return result
  218. }
  219. const { maxYear, maxDate, maxMonth, maxHour, maxMinute, maxSecond } = getBoundary('max', innerValue.value as number)
  220. const { minYear, minDate, minMonth, minHour, minMinute, minSecond } = getBoundary('min', innerValue.value as number)
  221. const result: Array<{ type: DatetimePickerViewColumnType; range: number[] }> = [
  222. {
  223. type: 'year',
  224. range: [minYear, maxYear]
  225. },
  226. {
  227. type: 'month',
  228. range: [minMonth, maxMonth]
  229. },
  230. {
  231. type: 'date',
  232. range: [minDate, maxDate]
  233. },
  234. {
  235. type: 'hour',
  236. range: [minHour, maxHour]
  237. },
  238. {
  239. type: 'minute',
  240. range: [minMinute, maxMinute]
  241. }
  242. ]
  243. if (props.type === 'datetime' && props.useSecond) {
  244. result.push({
  245. type: 'second',
  246. range: [minSecond, maxSecond]
  247. })
  248. }
  249. if (props.type === 'date') result.splice(3, 2)
  250. if (props.type === 'year-month') result.splice(2, 3)
  251. if (props.type === 'year') result.splice(1, 4)
  252. return result
  253. }
  254. /**
  255. * @description 修正时间入参,判定是否为规范时间类型
  256. * @param {String | Number} value
  257. * @return {String | Number} innerValue
  258. */
  259. function correctValue(value: string | number | Date): string | number {
  260. const isDateType = props.type !== 'time'
  261. if (isDateType && !isValidDate(value)) {
  262. // 是Date类型,但入参不可用,使用最小时间戳代替
  263. value = props.minDate
  264. } else if (!isDateType && !value) {
  265. // 非Date类型,无入参,使用最小小时代替
  266. value = props.useSecond ? `${padZero(props.minHour)}:00:00` : `${padZero(props.minHour)}:00`
  267. }
  268. // 当type为time时
  269. if (!isDateType) {
  270. // 非Date类型,直接走此逻辑
  271. let [hour, minute, second = '00'] = (isString(value) ? value : value.toString()).split(':')
  272. hour = padZero(range(Number(hour), props.minHour, props.maxHour))
  273. minute = padZero(range(Number(minute), props.minMinute, props.maxMinute))
  274. if (props.useSecond) {
  275. second = padZero(range(Number(second), props.minSecond, props.maxSecond))
  276. return `${hour}:${minute}:${second}`
  277. }
  278. return `${hour}:${minute}`
  279. }
  280. // date type
  281. value = Math.min(Math.max(Number(value), props.minDate), props.maxDate)
  282. return value
  283. }
  284. /**
  285. * @description 根据时间戳,计算所有选项的范围
  286. * @param {'min'|'max'} type 类型
  287. * @param {Number} innerValue 时间戳
  288. */
  289. function getBoundary(type: 'min' | 'max', innerValue: number) {
  290. const value = new Date(innerValue)
  291. const boundary = new Date(props[`${type}Date`])
  292. const year = boundary.getFullYear()
  293. let month: number = 1
  294. let date: number = 1
  295. let hour: number = 0
  296. let minute: number = 0
  297. let second: number = 0
  298. if (type === 'max') {
  299. month = 12
  300. date = getMonthEndDay(value.getFullYear(), value.getMonth() + 1)
  301. hour = 23
  302. minute = 59
  303. second = 59
  304. }
  305. if (value.getFullYear() === year) {
  306. month = boundary.getMonth() + 1
  307. if (value.getMonth() + 1 === month) {
  308. date = boundary.getDate()
  309. if (value.getDate() === date) {
  310. hour = boundary.getHours()
  311. if (value.getHours() === hour) {
  312. minute = boundary.getMinutes()
  313. if (value.getMinutes() === minute) {
  314. second = boundary.getSeconds()
  315. }
  316. }
  317. }
  318. }
  319. }
  320. return {
  321. [`${type}Year`]: year,
  322. [`${type}Month`]: month,
  323. [`${type}Date`]: date,
  324. [`${type}Hour`]: hour,
  325. [`${type}Minute`]: minute,
  326. [`${type}Second`]: second
  327. }
  328. }
  329. /**
  330. * @description 根据传入的value以及type,初始化innerValue,期间会使用format格式化数据
  331. * @param value
  332. * @return {Array}
  333. */
  334. function updateColumnValue(value: string | number) {
  335. const values = getPickerValue(value, props.type, props.useSecond)
  336. // 更新pickerView的value,columns
  337. if (props.modelValue !== value) {
  338. emit('update:modelValue', value)
  339. emit('change', {
  340. value,
  341. picker: proxy.$.exposed
  342. })
  343. }
  344. innerValue.value = value
  345. columns.value = updateColumns()
  346. pickerValue.value = values
  347. }
  348. /**
  349. * @description 根据当前的选中项 处理innerValue
  350. * @return {date} innerValue
  351. */
  352. function updateInnerValue() {
  353. const { type, useSecond } = props
  354. let innerValue: string | number = ''
  355. const pickerVal = datePickerview.value?.getValues() || []
  356. const values = isArray(pickerVal) ? pickerVal : [pickerVal]
  357. if (type === 'time') {
  358. if (useSecond) {
  359. innerValue = `${padZero(values[0])}:${padZero(values[1])}:${padZero(values[2])}`
  360. } else {
  361. innerValue = `${padZero(values[0])}:${padZero(values[1])}`
  362. }
  363. return innerValue
  364. }
  365. // 处理年份 索引位0
  366. const year = values[0] && parseInt(values[0])
  367. // 处理月 索引位1
  368. const month = type === 'year' ? 1 : values[1] && parseInt(values[1])
  369. const maxDate = getMonthEndDay(Number(year), Number(month))
  370. // 处理 date 日期 索引位2
  371. let date: string | number = 1
  372. if (type !== 'year-month' && type !== 'year') {
  373. date = (Number(values[2]) && parseInt(String(values[2]))) > maxDate ? maxDate : values[2] && parseInt(String(values[2]))
  374. }
  375. // 处理 时分秒 索引位3,4,5
  376. let hour = 0
  377. let minute = 0
  378. let second = 0
  379. if (type === 'datetime') {
  380. hour = Number(values[3]) && parseInt(values[3])
  381. minute = Number(values[4]) && parseInt(values[4])
  382. if (useSecond) {
  383. second = Number(values[5]) && parseInt(values[5])
  384. }
  385. }
  386. const value = new Date(Number(year), Number(month) - 1, Number(date), hour, minute, second).getTime()
  387. innerValue = correctValue(value)
  388. return innerValue
  389. }
  390. /**
  391. * @description 选中项改变,多级联动
  392. */
  393. function columnChange(picker: PickerViewInstance) {
  394. // time year-mouth year 无需联动
  395. if (props.type === 'time' || props.type === 'year-month' || props.type === 'year') {
  396. return
  397. }
  398. /** 重新计算年月日时分秒,修正时间。 */
  399. const values = picker.getValues() as string[]
  400. const year = Number(values[0])
  401. const month = Number(values[1])
  402. const maxDate = getMonthEndDay(year, month)
  403. let date = Number(values[2])
  404. date = date > maxDate ? maxDate : date
  405. let hour: number = 0
  406. let minute: number = 0
  407. let second: number = 0
  408. if (props.type === 'datetime') {
  409. hour = Number(values[3])
  410. minute = Number(values[4])
  411. if (props.useSecond) {
  412. second = Number(values[5])
  413. }
  414. }
  415. const value = new Date(year, month - 1, date, hour, minute, second).getTime()
  416. /** 根据计算选中项的时间戳,重新计算所有的选项列表 */
  417. // 更新选中时间戳
  418. innerValue.value = correctValue(value)
  419. // 根据innerValue获取最新的时间表,重新生成对应的数据源
  420. const newColumns = updateColumns()
  421. // 深拷贝联动之前的选中项
  422. const selectedIndex = picker.getSelectedIndex().slice(0)
  423. /**
  424. * 选中年会修改对应的年份的月数,和月份对应的日期。
  425. * 选中月,会修改月份对应的日数
  426. */
  427. newColumns.forEach((_columns, index) => {
  428. const nextColumnIndex = index + 1
  429. const nextColumnData = newColumns[nextColumnIndex]
  430. if (nextColumnIndex > newColumns.length - 1) return
  431. picker.setColumnData(
  432. nextColumnIndex,
  433. nextColumnData,
  434. selectedIndex[nextColumnIndex] <= nextColumnData.length - 1 ? selectedIndex[nextColumnIndex] : 0
  435. )
  436. })
  437. }
  438. function onPickStart() {
  439. emit('pickstart')
  440. }
  441. function onPickEnd() {
  442. emit('pickend')
  443. }
  444. function getSelects() {
  445. const pickerVal = datePickerview.value?.getSelects()
  446. if (pickerVal == null) return undefined
  447. if (isArray(pickerVal)) return pickerVal
  448. return [pickerVal]
  449. }
  450. defineExpose<DatetimePickerViewExpose>({
  451. updateColumns,
  452. setColumns,
  453. getSelects,
  454. correctValue,
  455. getOriginColumns
  456. })
  457. </script>