JTimePicker.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <template>
  2. <!--时间选择弹窗-->
  3. <u-popup ref="popup" :show="showPopup">
  4. <view class="jtime-picker-content">
  5. <view class="picker-view-content-top">
  6. <view @click="cancelPickerShow" class="cancel">{{ cancelText }}</view>
  7. <view class="title" v-if="isShowSeletTimeTitle">{{ seletTimeTitle }}</view>
  8. <view @click="confirmPickerValue" class="confirm">{{ confirmText }}</view>
  9. </view>
  10. <view class="jtime-container">
  11. <view class="short-select-time" v-if="isShowShortTimeList">
  12. <view class="time-title">{{ shortTimeTitle }}</view>
  13. <view class="time-list">
  14. <view :class="index == currentShortTimeIndex ? 'short-time-active' : ''"
  15. v-for="(item, index) in shortTimeList" :key="item.key" @click="getShortTime(item, index)">
  16. {{ item.key }}
  17. </view>
  18. </view>
  19. </view>
  20. <view class="time-custom">
  21. <view class="time-tile" v-if="isShowShortTimeList">
  22. <view>自定义</view>
  23. <image src="@/uni_modules/jtime-picker-popup/static/delete.png" @click="deleteSelectedTime" />
  24. </view>
  25. <view v-if="isShowSelectedTimeEcho" class="time-value">
  26. <view :class="['time-begin', beginTimeStr ? 'time' : '']">
  27. <!-- <input :class="['uni-input', beginTimeFlag ? 'begin-focus-style' : '']" :readonly="true" v-model="beginTimeStr" placeholder="开始时间" @click="timeSelect(1)" /> -->
  28. <view
  29. :class="['uni-input', beginTimeFlag ? 'begin-focus-style' : '', isDateTypeRange ? '' : 'uni-input-w-full']"
  30. @click="timeSelect(1)">{{ beginTimeStr ? beginTimeStr : beginTimePlaceHolder }}</view>
  31. </view>
  32. <view v-if="isDateTypeRange" class="time-sync">至</view>
  33. <view v-if="isDateTypeRange" :class="['time-end', endTimeStr ? 'time' : '']">
  34. <!-- <input :class="['uni-input', endTimeFlag ? 'end-focus-style' : '']" readonly v-model="endTimeStr" placeholder="结束时间" @click="timeSelect(2)" /> -->
  35. <view :class="['uni-input', endTimeFlag ? 'end-focus-style' : '']" @click="timeSelect(2)">
  36. {{ endTimeStr ? endTimeStr : endTimePlaceHolder }}
  37. </view>
  38. </view>
  39. </view>
  40. </view>
  41. <picker-view v-if="visible" indicator-class="indicatorStyle" indicator-style="height: 40px"
  42. :value="pickerValue" class="picker-view" @change="pickerDateChange">
  43. <picker-view-column>
  44. <view :class="['item', item == year ? 'current-item-year' : '']" v-for="(item,index) in years"
  45. :key="index">{{item}}年</view>
  46. </picker-view-column>
  47. <picker-view-column>
  48. <view :class="['item', item == month ? 'current-item-month' : '']"
  49. v-for="(item,index) in months" :key="index">{{item}}月</view>
  50. </picker-view-column>
  51. <picker-view-column>
  52. <view :class="['item', item == day ? 'current-item-day' : '']" v-for="(item,index) in days"
  53. :key="index">{{item}}日</view>
  54. </picker-view-column>
  55. </picker-view>
  56. </view>
  57. </view>
  58. </u-popup>
  59. <!--end 时间选择弹窗-->
  60. </template>
  61. <script>
  62. import dayjs from 'dayjs';
  63. export default {
  64. name: 'JTimePicker',
  65. props: {
  66. // 快捷时间列表
  67. defaultSelect : {
  68. type : Number,
  69. default : ()=> null
  70. },
  71. shortTimeList: {
  72. type: Array,
  73. default: () => {
  74. return [{
  75. unit: 'day',
  76. key: '近半月',
  77. value: 15
  78. },
  79. {
  80. unit: 'month',
  81. key: '近三月',
  82. value: 3
  83. },
  84. {
  85. unit: 'year',
  86. key: '近一年',
  87. value: 1
  88. },
  89. ]
  90. }
  91. },
  92. // 是否显示快捷时间选择列表
  93. isShowShortTimeList: {
  94. type: Boolean,
  95. default: true
  96. },
  97. // 快捷时间标题
  98. shortTimeTitle: {
  99. type: String,
  100. default: '开票时间'
  101. },
  102. // 是否显示时间选择标题
  103. isShowSeletTimeTitle: {
  104. type: Boolean,
  105. default: true
  106. },
  107. // 标题
  108. seletTimeTitle: {
  109. type: String,
  110. default: '时间选择'
  111. },
  112. // 取消按钮文字
  113. cancelText: {
  114. type: String,
  115. default: '取消'
  116. },
  117. // 确认按钮文字
  118. confirmText: {
  119. type: String,
  120. default: '确认'
  121. },
  122. // 时间选择开始年份
  123. beginSelectYear: {
  124. type: Number,
  125. default: 1930
  126. },
  127. // 时间选择结束年份
  128. endSelectYear: {
  129. type: Number,
  130. default: new Date().getFullYear()
  131. },
  132. // 时间选择结束月份, -1=>当前月份
  133. endSelectMonth: {
  134. type: Number,
  135. default: -1
  136. },
  137. // 时间选择结束天数 -1=> 当天
  138. endSelectDay: {
  139. type: Number,
  140. default: dayjs().daysInMonth()
  141. },
  142. beginTimePlaceHolder: {
  143. type: String,
  144. default: '开始时间'
  145. },
  146. endTimePlaceHolder: {
  147. type: String,
  148. default: '结束时间'
  149. },
  150. // 选择器是否为时间选范围选择
  151. isDateTypeRange: {
  152. type: Boolean,
  153. default: false
  154. },
  155. // 是否回显选择日期
  156. isShowSelectedTimeEcho: {
  157. type: Boolean,
  158. default: true
  159. },
  160. // 当前传进来的开始时间
  161. originBeginTime: {
  162. type: String,
  163. default: ''
  164. },
  165. // 当前传进来的结束时间
  166. originEndTime: {
  167. type: String,
  168. default: ''
  169. },
  170. },
  171. data() {
  172. const date = new Date()
  173. const years = []
  174. const year = date.getFullYear()
  175. const months = []
  176. const month = date.getMonth() + 1
  177. const days = []
  178. const day = date.getDate()
  179. for (let i = this.beginSelectYear; i <= this.endSelectYear; i++) {
  180. years.push(i)
  181. }
  182. for (let i = 1; i <= 12; i++) {
  183. months.push(String(i).padStart(2, '0'))
  184. }
  185. for (let i = 1; i <= 31; i++) {
  186. days.push(String(i).padStart(2, '0'))
  187. }
  188. return {
  189. years,
  190. year,
  191. months,
  192. month,
  193. days,
  194. day,
  195. visible: true,
  196. pickerValue: [9999, month - 1, day - 1],
  197. beginTimeStr: this.originBeginTime,
  198. endTimeStr: this.originEndTime,
  199. beginTimeFlag: false,
  200. endTimeFlag: false,
  201. currentShortTimeIndex: -1,
  202. showPopup: false,
  203. };
  204. },
  205. mounted() {
  206. // 默认展示开始时间
  207. this.beginTimeFlag = true
  208. if (this.originBeginTime) {
  209. this.year = parseInt(this.originBeginTime.split('/')[0])
  210. this.month = parseInt(this.originBeginTime.split('/')[1])
  211. this.day = parseInt(this.originBeginTime.split('/')[2])
  212. this.pickerValue = [this.year - 1930, this.month - 1, this.day - 1]
  213. };
  214. },
  215. methods: {
  216. handleInit(){
  217. if(this.defaultSelect){
  218. const index = this.shortTimeList.findIndex(v=>v.value === this.defaultSelect);
  219. if(index != -1){
  220. this.getShortTime(this.shortTimeList[index],index);
  221. }
  222. this.confirmPickerValue();
  223. }
  224. },
  225. pickerShow() {
  226. this.showPopup = true;
  227. },
  228. cancelPickerShow() {
  229. this.showPopup = false;
  230. },
  231. updateMonths(selectedYear) {
  232. const date = new Date();
  233. const currentYear = date.getFullYear()
  234. const currentMonth = date.getMonth() + 1
  235. this.months.length = 0; // 清空原数组
  236. if (selectedYear === currentYear && currentYear === this.endSelectYear) {
  237. if (this.endSelectMonth && this.endSelectMonth > 0) {
  238. for (let i = 1; i <= this.endSelectMonth; i++) {
  239. this.months.push(String(i).padStart(2, '0'))
  240. }
  241. } else {
  242. // 最多只能选择到当前月份
  243. for (let i = 1; i <= currentMonth; i++) {
  244. this.months.push(String(i).padStart(2, '0'))
  245. }
  246. }
  247. } else {
  248. for (let i = 1; i <= 12; i++) {
  249. this.months.push(String(i).padStart(2, '0'))
  250. }
  251. }
  252. },
  253. updateDays(selectedYear, selectedMonth) {
  254. const date = new Date();
  255. const currentYear = date.getFullYear()
  256. const currentMonth = date.getMonth() + 1
  257. const currentDay = date.getDate()
  258. this.days.length = 0; // 清空原数组
  259. const dayCount = this.getDaysInMonth(selectedYear, selectedMonth);
  260. if (selectedYear === currentYear && selectedMonth === currentMonth && (currentMonth === this
  261. .endSelectMonth || this.endSelectMonth == -1)) {
  262. if (this.endSelectDay && this.endSelectDay > -1) {
  263. for (let i = 1; i <= this.endSelectDay; i++) {
  264. this.days.push(String(i).padStart(2, '0'));
  265. }
  266. } else {
  267. // 最多选择到今天
  268. for (let i = 1; i <= currentDay; i++) {
  269. this.days.push(String(i).padStart(2, '0'));
  270. }
  271. }
  272. } else {
  273. if (this.endSelectDay > -1 && selectedMonth === this.endSelectMonth) {
  274. for (let i = 1; i <= Math.min(dayCount, this.endSelectDay); i++) {
  275. this.days.push(String(i).padStart(2, '0'));
  276. }
  277. } else {
  278. for (let i = 1; i <= dayCount; i++) {
  279. this.days.push(String(i).padStart(2, '0'));
  280. }
  281. }
  282. }
  283. },
  284. getDaysInMonth(year, month) {
  285. // 二月特殊处理
  286. if (month == 2) {
  287. // 闰年规则:能被4整除且不可被100整除,或能被400整除
  288. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
  289. }
  290. // 其他月份:4/6/9/11月为30天
  291. return [4, 6, 9, 11].includes(month) ? 30 : 31;
  292. },
  293. pickerDateChange(e) {
  294. const val = e.detail.value
  295. this.year = this.years[val[0]]
  296. this.month = this.months[val[1]]
  297. this.day = this.days[val[2]]
  298. this.updateMonths(this.year)
  299. this.updateDays(this.year, parseFloat(this.month));
  300. if (this.beginTimeFlag) {
  301. this.beginTimeStr = `${this.year}/${this.month}/${this.day}`
  302. } else if (this.endTimeFlag) {
  303. this.endTimeStr = `${this.year}/${this.month}/${this.day}`
  304. }
  305. this.currentShortTimeIndex = -1
  306. },
  307. confirmPickerValue() {
  308. if(this.beginTimeStr || this.endTimeStr){
  309. if (!this.beginTimeStr) {
  310. return uni.showToast({
  311. title: '请选择开始时间',
  312. icon: 'none'
  313. })
  314. } else if (this.isDateTypeRange && !this.endTimeStr) {
  315. this.endTimeFlag = true
  316. this.beginTimeFlag = false
  317. return uni.showToast({
  318. title: '请选择结束时间',
  319. icon: 'none'
  320. })
  321. } else if (this.isDateTypeRange && dayjs(this.endTimeStr).isBefore(this.beginTimeStr)) {
  322. return uni.showToast({
  323. title: '结束时间不能小于开始时间',
  324. icon: 'none'
  325. })
  326. }
  327. }
  328. this.$emit('confirm', {
  329. beginTime: this.beginTimeStr,
  330. endTime: this.isDateTypeRange ? this.endTimeStr : ''
  331. })
  332. this.showPopup = false;
  333. },
  334. // 点击开始时间(结束时间)框 => 当前处于开始时间(结束时间),进行时间选择
  335. timeSelect(type) {
  336. if (type == 1) {
  337. this.beginTimeFlag = true
  338. this.endTimeFlag = false
  339. } else {
  340. this.beginTimeFlag = false
  341. this.endTimeFlag = true
  342. }
  343. },
  344. deleteSelectedTime() {
  345. this.beginTimeFlag = true
  346. this.endTimeFlag = false
  347. this.beginTimeStr = ''
  348. this.endTimeStr = ''
  349. },
  350. // 点击时间快捷方式,获取起始时间、结束时间
  351. getShortTime(item, index) {
  352. this.currentShortTimeIndex = index;
  353. let beginTime = '';
  354. let endTime = '';
  355. switch (item.key) {
  356. case '全部':
  357. // 特殊处理 实际就是不传值
  358. this.deleteSelectedTime();
  359. break;
  360. case '今天':
  361. beginTime = dayjs().format('YYYY/MM/DD');
  362. endTime = beginTime;
  363. break;
  364. case '昨天':
  365. const yesterday = dayjs().subtract(1, 'day').format('YYYY/MM/DD');
  366. beginTime = yesterday;
  367. endTime = yesterday;
  368. break;
  369. case '本月':
  370. beginTime = dayjs().startOf('month').format('YYYY/MM/DD');
  371. endTime = dayjs().endOf('month').format('YYYY/MM/DD');
  372. break;
  373. case '上月':
  374. const firstDayOfLastMonth = dayjs().subtract(1, 'month').startOf('month');
  375. beginTime = firstDayOfLastMonth.format('YYYY/MM/DD');
  376. endTime = firstDayOfLastMonth.endOf('month').format('YYYY/MM/DD');
  377. break;
  378. default:
  379. // 处理其他情况(近3天/近7天/近15天)
  380. const result = this.getDateRange(item.unit, item.value);
  381. beginTime = result.beginTime;
  382. endTime = result.endTime;
  383. }
  384. if (beginTime && endTime) {
  385. this.beginTimeStr = beginTime;
  386. this.endTimeStr = endTime;
  387. // 仅当不是"全部"时更新选择器位置
  388. if (item.key !== '全部') {
  389. const [y, m, d] = beginTime.split('/').map(Number);
  390. this.year = y;
  391. this.month = m;
  392. this.day = d;
  393. // 更新月份和日期数组
  394. this.updateMonths(this.year);
  395. this.updateDays(this.year, parseFloat(this.month));
  396. // 计算选择器位置
  397. const yearIndex = this.years.indexOf(y);
  398. let monthIndex = this.months.indexOf(String(m).padStart(2, '0'));
  399. let dayIndex = this.days.indexOf(String(d).padStart(2, '0'));
  400. // 处理索引越界情况
  401. if (monthIndex === -1) monthIndex = this.months.length - 1;
  402. if (dayIndex === -1) dayIndex = this.days.length - 1;
  403. this.pickerValue = [yearIndex, monthIndex, dayIndex];
  404. }
  405. }
  406. this.beginTimeFlag = true;
  407. this.endTimeFlag = false;
  408. },
  409. /**
  410. * 获取时间区间(格式化 YYYY/MM/DD)
  411. * @param {string} rangeType - 时间范围类型 ('day' | 'month' | 'year')
  412. * @param {number} num - 数量(如 15天、3个月、1年)
  413. * @returns { { beginTime: string, endTime: string } }
  414. */
  415. getDateRange(rangeType = 'day', num = 15) {
  416. // const end = dayjs.format(new Date());
  417. // const startDate = dayjs.subtract(new Date(), unit == 'day' ? value - 1 : value, unit);
  418. // const start = dayjs.format(startDate);
  419. // return { beginTime: start, endTime: end };
  420. const endTime = dayjs();
  421. const beginTime = endTime.subtract(rangeType == 'day' ? num - 1 : num, rangeType); // 包含今天,所以减 (num-1)
  422. return {
  423. beginTime: beginTime.format('YYYY/MM/DD'),
  424. endTime: endTime.format('YYYY/MM/DD'),
  425. };
  426. }
  427. }
  428. }
  429. </script>
  430. <style scoped lang="scss">
  431. ::v-deep uni-picker-view {
  432. .uni-picker-view-wrapper {
  433. .indicatorStyle {
  434. background-color: #F3F3F3;
  435. &::before {
  436. display: none;
  437. }
  438. &::after {
  439. display: none;
  440. }
  441. }
  442. .uni-picker-view-mask {
  443. z-index: 9;
  444. }
  445. .uni-picker-view-content {
  446. z-index: 8;
  447. }
  448. uni-picker-view-column:nth-child(1) {
  449. .indicatorStyle {
  450. border-top-left-radius: 6px;
  451. border-bottom-left-radius: 6px;
  452. }
  453. }
  454. uni-picker-view-column:nth-child(3) {
  455. .indicatorStyle {
  456. border-top-right-radius: 6px;
  457. border-bottom-right-radius: 6px;
  458. }
  459. }
  460. }
  461. }
  462. .jtime-picker-content {
  463. width: 100%;
  464. background-color: #fff;
  465. .picker-view-content-top {
  466. display: flex;
  467. align-items: center;
  468. justify-content: space-between;
  469. font-size: 14px;
  470. padding: 10px 20px;
  471. cursor: pointer;
  472. border-bottom: 1px solid #eee;
  473. .cancel {
  474. color: #86909C;
  475. }
  476. .confirm {
  477. color: #004677;
  478. font-weight: 500;
  479. }
  480. .title {
  481. font-size: 18px;
  482. font-weight: 600;
  483. }
  484. }
  485. .jtime-container {
  486. padding: 0 16px;
  487. .short-select-time {
  488. margin-top: 8px;
  489. .time-title {
  490. color: #4E5969;
  491. font-size: 12px;
  492. padding-bottom: 16px;
  493. }
  494. .time-list {
  495. display: flex;
  496. align-items: center;
  497. flex-wrap: wrap;
  498. font-size: 12px;
  499. &>view {
  500. padding: 2px 8px;
  501. border-radius: 3px;
  502. border: 1px solid var(--Gray-Gray4, #DCDCDC);
  503. margin-right: 8px;
  504. margin-bottom: 10px;
  505. }
  506. .short-time-active {
  507. color: #004677;
  508. border: 1px solid #004677;
  509. background: rgba(43, 63, 108, 0.08);
  510. }
  511. }
  512. }
  513. .time-custom {
  514. margin: 24px 0 16px 0;
  515. .time-tile {
  516. display: flex;
  517. align-items: center;
  518. justify-content: space-between;
  519. font-size: 12px;
  520. color: #4E5969;
  521. img,
  522. image {
  523. width: 16px;
  524. height: 16px;
  525. }
  526. }
  527. .time-value {
  528. margin-top: 16px;
  529. display: flex;
  530. align-items: center;
  531. justify-content: space-between;
  532. .time-begin,
  533. .time-end {
  534. text-align: center;
  535. flex: 1;
  536. .uni-input {
  537. padding: 9px 26px;
  538. height: 22px;
  539. line-height: 22px;
  540. color: #86909C;
  541. font-size: 16px;
  542. width: 90px;
  543. border-bottom: 1px solid var(---color-border-2, #E5E6EB);
  544. }
  545. .begin-focus-style,
  546. .end-focus-style {
  547. color: #004677 !important;
  548. border-bottom: 1px solid #004677;
  549. }
  550. .uni-input-w-full {
  551. width: calc(100% - 52px);
  552. }
  553. }
  554. .time-sync {
  555. color: #4E5969;
  556. padding: 9px 16px;
  557. font-size: 14px;
  558. }
  559. .time {
  560. .uni-input {
  561. color: #1D2129;
  562. font-weight: 600;
  563. }
  564. }
  565. }
  566. }
  567. .picker-view {
  568. width: 100%;
  569. height: 200px;
  570. }
  571. .item {
  572. line-height: 40px;
  573. text-align: center;
  574. font-size: 16px;
  575. font-weight: 400;
  576. color: var(--text-icon-font-gy-260, rgba(0, 0, 0, 0.60));
  577. }
  578. .current-item-year,
  579. .current-item-month,
  580. .current-item-day {
  581. font-size: 16px;
  582. font-weight: 600;
  583. color: var(--text-icon-font-gy-190, rgba(0, 0, 0, 0.90));
  584. }
  585. }
  586. }
  587. </style>