index.js 8.3 KB

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