index.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { VantComponent } from '../common/component';
  2. import {
  3. ROW_HEIGHT,
  4. getNextDay,
  5. compareDay,
  6. copyDates,
  7. calcDateNum,
  8. formatMonthTitle,
  9. compareMonth,
  10. getMonths,
  11. getDayByOffset,
  12. } from './utils';
  13. import Toast from '../toast/toast';
  14. import { requestAnimationFrame } from '../common/utils';
  15. VantComponent({
  16. props: {
  17. title: {
  18. type: String,
  19. value: '日期选择',
  20. },
  21. color: String,
  22. show: {
  23. type: Boolean,
  24. observer(val) {
  25. if (val) {
  26. this.initRect();
  27. this.scrollIntoView();
  28. }
  29. },
  30. },
  31. formatter: null,
  32. confirmText: {
  33. type: String,
  34. value: '确定',
  35. },
  36. rangePrompt: String,
  37. defaultDate: {
  38. type: [Number, Array],
  39. observer(val) {
  40. this.setData({ currentDate: val });
  41. this.scrollIntoView();
  42. },
  43. },
  44. allowSameDay: Boolean,
  45. confirmDisabledText: String,
  46. type: {
  47. type: String,
  48. value: 'single',
  49. observer: 'reset',
  50. },
  51. minDate: {
  52. type: null,
  53. value: Date.now(),
  54. },
  55. maxDate: {
  56. type: null,
  57. value: new Date(
  58. new Date().getFullYear(),
  59. new Date().getMonth() + 6,
  60. new Date().getDate()
  61. ).getTime(),
  62. },
  63. position: {
  64. type: String,
  65. value: 'bottom',
  66. },
  67. rowHeight: {
  68. type: [Number, String],
  69. value: ROW_HEIGHT,
  70. },
  71. round: {
  72. type: Boolean,
  73. value: true,
  74. },
  75. poppable: {
  76. type: Boolean,
  77. value: true,
  78. },
  79. showMark: {
  80. type: Boolean,
  81. value: true,
  82. },
  83. showTitle: {
  84. type: Boolean,
  85. value: true,
  86. },
  87. showConfirm: {
  88. type: Boolean,
  89. value: true,
  90. },
  91. showSubtitle: {
  92. type: Boolean,
  93. value: true,
  94. },
  95. safeAreaInsetBottom: {
  96. type: Boolean,
  97. value: true,
  98. },
  99. closeOnClickOverlay: {
  100. type: Boolean,
  101. value: true,
  102. },
  103. maxRange: {
  104. type: [Number, String],
  105. value: null,
  106. },
  107. },
  108. data: {
  109. subtitle: '',
  110. currentDate: null,
  111. scrollIntoView: '',
  112. },
  113. created() {
  114. this.setData({
  115. currentDate: this.getInitialDate(),
  116. });
  117. },
  118. mounted() {
  119. if (this.data.show || !this.data.poppable) {
  120. this.initRect();
  121. this.scrollIntoView();
  122. }
  123. },
  124. methods: {
  125. reset() {
  126. this.setData({ currentDate: this.getInitialDate() });
  127. this.scrollIntoView();
  128. },
  129. initRect() {
  130. if (this.contentObserver != null) {
  131. this.contentObserver.disconnect();
  132. }
  133. const contentObserver = this.createIntersectionObserver({
  134. thresholds: [0, 0.1, 0.9, 1],
  135. observeAll: true,
  136. });
  137. this.contentObserver = contentObserver;
  138. contentObserver.relativeTo('.van-calendar__body');
  139. contentObserver.observe('.month', (res) => {
  140. if (res.boundingClientRect.top <= res.relativeRect.top) {
  141. // @ts-ignore
  142. this.setData({ subtitle: formatMonthTitle(res.dataset.date) });
  143. }
  144. });
  145. },
  146. getInitialDate() {
  147. const { type, defaultDate, minDate } = this.data;
  148. if (type === 'range') {
  149. const [startDay, endDay] = defaultDate || [];
  150. return [
  151. startDay || minDate,
  152. endDay || getNextDay(new Date(minDate)).getTime(),
  153. ];
  154. }
  155. if (type === 'multiple') {
  156. return defaultDate || [minDate];
  157. }
  158. return defaultDate || minDate;
  159. },
  160. scrollIntoView() {
  161. requestAnimationFrame(() => {
  162. const {
  163. currentDate,
  164. type,
  165. show,
  166. poppable,
  167. minDate,
  168. maxDate,
  169. } = this.data;
  170. // @ts-ignore
  171. const targetDate = type === 'single' ? currentDate : currentDate[0];
  172. const displayed = show || !poppable;
  173. if (!targetDate || !displayed) {
  174. return;
  175. }
  176. const months = getMonths(minDate, maxDate);
  177. months.some((month, index) => {
  178. if (compareMonth(month, targetDate) === 0) {
  179. this.setData({ scrollIntoView: `month${index}` });
  180. return true;
  181. }
  182. return false;
  183. });
  184. });
  185. },
  186. onOpen() {
  187. this.$emit('open');
  188. },
  189. onOpened() {
  190. this.$emit('opened');
  191. },
  192. onClose() {
  193. this.$emit('close');
  194. },
  195. onClosed() {
  196. this.$emit('closed');
  197. },
  198. onClickDay(event) {
  199. const { date } = event.detail;
  200. const { type, currentDate, allowSameDay } = this.data;
  201. if (type === 'range') {
  202. // @ts-ignore
  203. const [startDay, endDay] = currentDate;
  204. if (startDay && !endDay) {
  205. const compareToStart = compareDay(date, startDay);
  206. if (compareToStart === 1) {
  207. this.select([startDay, date], true);
  208. } else if (compareToStart === -1) {
  209. this.select([date, null]);
  210. } else if (allowSameDay) {
  211. this.select([date, date]);
  212. }
  213. } else {
  214. this.select([date, null]);
  215. }
  216. } else if (type === 'multiple') {
  217. let selectedIndex;
  218. // @ts-ignore
  219. const selected = currentDate.some((dateItem, index) => {
  220. const equal = compareDay(dateItem, date) === 0;
  221. if (equal) {
  222. selectedIndex = index;
  223. }
  224. return equal;
  225. });
  226. if (selected) {
  227. // @ts-ignore
  228. const cancelDate = currentDate.splice(selectedIndex, 1);
  229. this.setData({ currentDate });
  230. this.unselect(cancelDate);
  231. } else {
  232. // @ts-ignore
  233. this.select([...currentDate, date]);
  234. }
  235. } else {
  236. this.select(date, true);
  237. }
  238. },
  239. unselect(dateArray) {
  240. const date = dateArray[0];
  241. if (date) {
  242. this.$emit('unselect', copyDates(date));
  243. }
  244. },
  245. select(date, complete) {
  246. if (complete && this.data.type === 'range') {
  247. const valid = this.checkRange(date);
  248. if (!valid) {
  249. // auto selected to max range if showConfirm
  250. if (this.data.showConfirm) {
  251. this.emit([
  252. date[0],
  253. getDayByOffset(date[0], this.data.maxRange - 1),
  254. ]);
  255. } else {
  256. this.emit(date);
  257. }
  258. return;
  259. }
  260. }
  261. this.emit(date);
  262. if (complete && !this.data.showConfirm) {
  263. this.onConfirm();
  264. }
  265. },
  266. emit(date) {
  267. const getTime = (date) => (date instanceof Date ? date.getTime() : date);
  268. this.setData({
  269. currentDate: Array.isArray(date) ? date.map(getTime) : getTime(date),
  270. });
  271. this.$emit('select', copyDates(date));
  272. },
  273. checkRange(date) {
  274. const { maxRange, rangePrompt } = this.data;
  275. if (maxRange && calcDateNum(date) > maxRange) {
  276. Toast({
  277. context: this,
  278. message: rangePrompt || `选择天数不能超过 ${maxRange} 天`,
  279. });
  280. return false;
  281. }
  282. return true;
  283. },
  284. onConfirm() {
  285. if (
  286. this.data.type === 'range' &&
  287. !this.checkRange(this.data.currentDate)
  288. ) {
  289. return;
  290. }
  291. wx.nextTick(() => {
  292. // @ts-ignore
  293. this.$emit('confirm', copyDates(this.data.currentDate));
  294. });
  295. },
  296. },
  297. });