index.js 7.1 KB

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