领峰UI库,封装一些经常使用到的组件,自定义样式,模块化js函数,调用简单快速上手。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

521 lines
14 KiB

  1. <template>
  2. <view>
  3. <view v-if="pickerShow" class="picker" @click="backClick">
  4. <view class="main">
  5. <view class="header" @touchmove.stop.prevent @click.stop>
  6. <view class="btn" @tap="getResult('cancel')" :style="{color: cancelTextColor}">{{cancelText}}</view>
  7. <view class="btn" @tap="getResult('confirm')" :style="{color: confirmTextColor}">{{confirmText}}</view>
  8. </view>
  9. <view class="body" @click.stop @touchmove.stop.prevent>
  10. <picker-view :value="valueArr" @change="change" class="picker-view" @pickstart="pickstart"
  11. @pickend="pickend">
  12. <picker-view-column v-if="!reset && params.year">
  13. <view class="column" v-for="(item, index) in years" :key="index">
  14. {{ item }}
  15. <text class="text" v-if="showTimeTag"></text>
  16. </view>
  17. </picker-view-column>
  18. <picker-view-column v-if="!reset && params.month">
  19. <view class="column" v-for="(item, index) in months" :key="index">
  20. {{ formatNumber(item) }}
  21. <text class="text" v-if="showTimeTag"></text>
  22. </view>
  23. </picker-view-column>
  24. <picker-view-column v-if="!reset && params.day">
  25. <view class="column" v-for="(item, index) in days" :key="index">
  26. {{ formatNumber(item) }}
  27. <text class="text" v-if="showTimeTag"></text>
  28. </view>
  29. </picker-view-column>
  30. <picker-view-column v-if="!reset && params.hour">
  31. <view class="column" v-for="(item, index) in hours" :key="index">
  32. {{ formatNumber(item) }}
  33. <text class="text" v-if="showTimeTag"></text>
  34. </view>
  35. </picker-view-column>
  36. <picker-view-column v-if="!reset && params.minute">
  37. <view class="column" v-for="(item, index) in minutes" :key="index">
  38. {{ formatNumber(item) }}
  39. <text class="text" v-if="showTimeTag"></text>
  40. </view>
  41. </picker-view-column>
  42. <picker-view-column v-if="!reset && params.second">
  43. <view class="column" v-for="(item, index) in seconds" :key="index">
  44. {{ formatNumber(item) }}
  45. <text class="text" v-if="showTimeTag"></text>
  46. </view>
  47. </picker-view-column>
  48. </picker-view>
  49. </view>
  50. </view>
  51. </view>
  52. <slot></slot>
  53. </view>
  54. </template>
  55. <script>
  56. export default {
  57. name: 'timePicker',
  58. props: {
  59. value: {
  60. default: true,
  61. type: Boolean
  62. },
  63. // picker中需要显示的参数
  64. params: {
  65. type: Object,
  66. default () {
  67. return {
  68. year: true,
  69. month: true,
  70. day: true,
  71. hour: true,
  72. minute: true,
  73. second: true,
  74. timestamp: false,
  75. }
  76. }
  77. },
  78. //左上角文字
  79. cancelText: {
  80. type: String,
  81. default: '取消',
  82. },
  83. //右上角文字
  84. confirmText: {
  85. type: String,
  86. default: '确认',
  87. },
  88. //年月日开始时间
  89. beginDate: {
  90. type: [String, Number],
  91. default: '1990-1-1'
  92. },
  93. //年月日结束时间
  94. endDate: {
  95. type: [String, Number],
  96. default: '2050-12-31'
  97. },
  98. // 默认显示的时间,2025-07-02 || 2025-07-02 13:01:00 || 2025/07/02 || 默认当前时间
  99. defaultTime: {
  100. type: String,
  101. default: function(){
  102. let time = new Date();
  103. let year = time.getFullYear();
  104. let month = time.getMonth() + 1;
  105. let day = time.getDate();
  106. let hour = time.getHours();
  107. let min = time.getMinutes();
  108. let ppn = time.getSeconds();
  109. let cover = function(par) {
  110. par = par.toString()[1] ? par : "0" + par;
  111. return par;
  112. }
  113. let str = [year, month, day].map(cover).join('-') +" "+ [hour, min, ppn].map(cover).join(":");
  114. return str;
  115. }
  116. },
  117. // 是否显示后面的年月日中文提示
  118. showTimeTag: {
  119. type: Boolean,
  120. default: true
  121. },
  122. // 是否允许通过点击背景关闭Picker
  123. backClickClose: {
  124. type: Boolean,
  125. default: true
  126. },
  127. //开始小时
  128. hourBegin: {
  129. type: [Number, String],
  130. default: '0'
  131. },
  132. //结束小时
  133. hourEnd: {
  134. type: [Number, String],
  135. default: '23'
  136. },
  137. //分钟间隔
  138. minutesInterval: {
  139. type: [Number, String],
  140. default: '1'
  141. },
  142. //左上角文字颜色
  143. cancelTextColor: {
  144. type: String,
  145. default: '#000'
  146. },
  147. //右上角文字颜色
  148. confirmTextColor: {
  149. type: String,
  150. default: '#1E7DEB'
  151. },
  152. },
  153. data() {
  154. return {
  155. pickerShow: false,
  156. years: [],
  157. months: [],
  158. days: [],
  159. hours: [],
  160. minutes: [],
  161. seconds: [],
  162. year: 0,
  163. month: 0,
  164. day: 0,
  165. hour: 0,
  166. minute: 0,
  167. second: 0,
  168. reset: false,
  169. valueArr: [],
  170. moving: false // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
  171. }
  172. },
  173. mounted() {
  174. this.init()
  175. },
  176. computed: {
  177. currentYear() {
  178. return `${this.year}`
  179. },
  180. currentMonth() {
  181. return `${this.month}`
  182. },
  183. beginYear() {
  184. return (new Date(this.beginDate).getFullYear())
  185. },
  186. endYear() {
  187. return (new Date(this.endDate).getFullYear())
  188. },
  189. beginMonth() {
  190. return (new Date(this.beginDate).getMonth() + 1)
  191. },
  192. endMonth() {
  193. return (new Date(this.endDate).getMonth() + 1)
  194. },
  195. beginDay() {
  196. return (new Date(this.beginDate).getDate())
  197. },
  198. endDay() {
  199. return (new Date(this.endDate).getDate())
  200. },
  201. },
  202. watch: {
  203. currentYear() {
  204. if (this.params.year) this.setMonths()
  205. },
  206. currentMonth() {
  207. this.setDays()
  208. },
  209. value(val) {
  210. this.pickerShow = val;
  211. },
  212. pickerShow(val) {
  213. this.init()
  214. this.$emit("input", val);
  215. }
  216. },
  217. methods: {
  218. //日期时间处理
  219. initTimeValue() {
  220. // 格式化时间,在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
  221. let dfdate = this.defaultTime.replace(/\-/g, '/')
  222. let bdate = this.beginDate.replace(/\-/g, '/')
  223. let edate = this.endDate.replace(/\-/g, '/')
  224. let time = null
  225. time = new Date(bdate)
  226. if (!!dfdate) {
  227. time = new Date(dfdate) > new Date(bdate) ? new Date(dfdate) : new Date(bdate)
  228. time = new Date(dfdate) > new Date(edate) ? new Date(edate) : new Date(dfdate)
  229. }
  230. // 获取年日月时分秒
  231. this.year = time.getFullYear()
  232. this.month = Number(time.getMonth()) + 1
  233. this.day = time.getDate()
  234. this.hour = time.getHours()
  235. this.minute = time.getMinutes()
  236. this.second = time.getSeconds()
  237. },
  238. // 生成递进的数组
  239. generateArray: function(start, end) {
  240. // 转为数值格式,否则用户给end-year等传递字符串值时,下面的end+1会导致字符串拼接,而不是相加
  241. start = Number(start)
  242. end = Number(end)
  243. end = end > start ? end : start
  244. // 生成数组,获取其中的索引,并剪出来
  245. return [...Array(end + 1).keys()].slice(start)
  246. },
  247. getIndex: function(arr, val) {
  248. let index = arr.indexOf(val)
  249. // 如果index为-1(即找不到index值),~(-1)=-(-1)-1=0 (转成相反数在减一),导致条件不成立
  250. return ~index ? index : 0
  251. },
  252. init() {
  253. this.valueArr = []
  254. this.reset = false
  255. this.initTimeValue()
  256. if (this.params.year) {
  257. this.valueArr.push(0)
  258. this.setYears()
  259. }
  260. if (this.params.month) {
  261. this.valueArr.push(0)
  262. this.setMonths()
  263. }
  264. if (this.params.day) {
  265. this.valueArr.push(0)
  266. this.setDays()
  267. }
  268. if (this.params.hour) {
  269. this.valueArr.push(0)
  270. this.setHours()
  271. }
  272. if (this.params.minute) {
  273. this.valueArr.push(0)
  274. this.setMinutes()
  275. }
  276. if (this.params.second) {
  277. this.valueArr.push(0)
  278. this.setSeconds()
  279. }
  280. },
  281. setYears() {
  282. // 获取年份集合
  283. this.years = this.generateArray(this.beginYear, this.endYear)
  284. // 设置this.valueArr某一项的值,是为了让picker预选中某一个值
  285. this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.years, this.year))
  286. },
  287. setMonths() {
  288. //结束开始年是同年
  289. if (this.endYear == this.beginYear) {
  290. this.months = this.generateArray(this.beginMonth, this.endMonth)
  291. }
  292. //开始年为当前选择年
  293. else if (this.beginYear == this.year) {
  294. this.months = this.generateArray(this.beginMonth, 12)
  295. }
  296. //结束年为当前选择年
  297. else if (this.endYear == this.year) {
  298. this.months = this.generateArray(1, this.endMonth)
  299. } else {
  300. this.months = this.generateArray(1, 12)
  301. }
  302. let index = 0
  303. if (this.params.year && this.params.month) index = 1
  304. else index = 0
  305. this.valueArr.splice(index, 1, this.getIndex(this.months, this.month))
  306. this.month = this.months[this.valueArr[index]]
  307. },
  308. setDays() {
  309. let totalDays = new Date(this.year, this.month, 0).getDate()
  310. //结束开始月份是同月份
  311. if (this.beginMonth == this.endMonth) {
  312. this.days = this.generateArray(this.beginDay, this.endDay)
  313. }
  314. //开始月份为当前选择月
  315. else if (this.beginMonth == this.month && this.year == this.beginYear) {
  316. this.days = this.generateArray(this.beginDay, totalDays)
  317. }
  318. //结束月份为当前选择月
  319. else if (this.endMonth == this.month && this.year == this.endYear) {
  320. this.days = this.generateArray(1, this.endDay)
  321. }
  322. //正常月份
  323. else {
  324. this.days = this.generateArray(1, totalDays)
  325. }
  326. let index = 0
  327. // 这里不能使用类似setMonths()中的this.valueArr.splice(this.valueArr.length - 1, xxx)做法
  328. // 因为this.month和this.year变化时,会触发watch中的this.setDays(),导致this.valueArr.length计算有误
  329. if (this.params.year && this.params.month) index = 2
  330. else if (this.params.month) index = 1
  331. else if (this.params.year) index = 1
  332. else index = 0
  333. // 当月份变化时,会导致日期的天数也会变化,如果原来选的天数大于变化后的天数,则重置为变化后的最大值
  334. // 比如原来选中3月31日,调整为2月后,日期变为最大29,这时如果day值继续为31显然不合理,于是将其置为29(picker-column从1开始)
  335. if (this.day > this.days.length) this.day = this.days.length
  336. this.valueArr.splice(index, 1, this.getIndex(this.days, this.day))
  337. },
  338. setHours() {
  339. let bh = +this.hourBegin
  340. let eh = +this.hourEnd
  341. this.hours = this.generateArray(bh, eh)
  342. this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.hours, this.hour))
  343. },
  344. setMinutes() {
  345. if (+this.minutesInterval > 1) {
  346. let inter = +this.minutesInterval < 60 ? +this.minutesInterval : 59;
  347. for (let i = 0; i < 60; i += inter) {
  348. this.minutes.push(i);
  349. }
  350. } else {
  351. this.minutes = this.generateArray(0, 59)
  352. }
  353. this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.minutes, this.minute))
  354. },
  355. setSeconds() {
  356. this.seconds = this.generateArray(0, 59)
  357. this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.seconds, this.second))
  358. },
  359. backClick() {
  360. this.backClickClose && this.close()
  361. },
  362. open() {
  363. this.pickerShow = true
  364. },
  365. close() {
  366. this.pickerShow = false
  367. },
  368. // 小于10前面补0,用于月份,日期,时分秒等
  369. formatNumber(num) {
  370. return +num < 10 ? '0' + num : String(num)
  371. },
  372. // 标识滑动开始,只有微信小程序才有这样的事件
  373. pickstart() {
  374. // #ifdef MP-WEIXIN
  375. this.moving = true;
  376. // #endif
  377. },
  378. // 标识滑动结束
  379. pickend() {
  380. // #ifdef MP-WEIXIN
  381. this.moving = false;
  382. // #endif
  383. },
  384. // 用户更改picker的列选项
  385. change(e) {
  386. console.log('change', e);
  387. this.valueArr = e.detail.value;
  388. let i = 0;
  389. // 这里使用i++,是因为this.valueArr数组的长度是不确定长度的,它根据this.params的值来配置长度
  390. // 进入if规则,i会加1,保证了能获取准确的值
  391. if (this.params.year) this.year = this.years[this.valueArr[i++]];
  392. if (this.params.month) this.month = this.months[this.valueArr[i++]];
  393. if (this.params.day) this.day = this.days[this.valueArr[i++]];
  394. if (this.params.hour) this.hour = this.hours[this.valueArr[i++]];
  395. if (this.params.minute) this.minute = this.minutes[this.valueArr[i++]];
  396. if (this.params.second) this.second = this.seconds[this.valueArr[i++]];
  397. },
  398. // 用户点击确定按钮
  399. getResult(event = null) {
  400. // #ifdef MP-WEIXIN
  401. if (this.moving) return;
  402. // #endif
  403. let result = {};
  404. // 只返回用户在this.params中配置了为true的字段
  405. if (this.params.year) result.year = this.formatNumber(this.year || 0);
  406. if (this.params.month) result.month = this.formatNumber(this.month || 0);
  407. if (this.params.day) result.day = this.formatNumber(this.day || 0);
  408. if (this.params.hour) result.hour = this.formatNumber(this.hour || 0);
  409. if (this.params.minute) result.minute = this.formatNumber(this.minute || 0);
  410. if (this.params.second) result.second = this.formatNumber(this.second || 0);
  411. if (this.params.timestamp) result.timestamp = this.getTimestamp();
  412. if (event) this.$emit(event, result);
  413. this.close();
  414. },
  415. // 获取时间戳
  416. getTimestamp() {
  417. // yyyy-mm-dd为安卓写法,不支持iOS,需要使用"/"分隔,才能二者兼容
  418. let time = this.year + '/' + this.month + '/' + this.day + ' ' + this.hour + ':' + this.minute + ':' + this
  419. .second;
  420. return new Date(time).getTime() / 1000;
  421. }
  422. }
  423. }
  424. </script>
  425. <style lang="scss" scoped>
  426. .picker {
  427. /* #ifndef APP-NVUE */
  428. display: block;
  429. /* #endif */
  430. position: fixed;
  431. z-index: 10;
  432. bottom: 0;
  433. left: 0;
  434. right: 0;
  435. height: 100vh;
  436. background: rgba(0, 0, 0, 0.6);
  437. display: flex;
  438. flex-direction: column;
  439. justify-content: flex-end;
  440. .main {
  441. width: 100%;
  442. .column{
  443. display: flex;
  444. justify-content: center;
  445. align-items: center;
  446. }
  447. .header {
  448. width: 100%;
  449. height: 90rpx;
  450. padding: 0 40rpx;
  451. display: flex;
  452. flex-direction: row;
  453. justify-content: space-between;
  454. align-items: center;
  455. box-sizing: border-box;
  456. font-size: 30rpx;
  457. background: #fff;
  458. position: relative;
  459. &::after {
  460. content: '';
  461. position: absolute;
  462. border-bottom: 1rpx solid #eaeef1;
  463. -webkit-transform: scaleY(0.5);
  464. transform: scaleY(0.5);
  465. bottom: 0;
  466. right: 0;
  467. left: 0;
  468. }
  469. .btn {
  470. padding: 16rpx;
  471. box-sizing: border-box;
  472. text-align: center;
  473. text-decoration: none;
  474. }
  475. }
  476. .body {
  477. width: 100%;
  478. height: 500rpx;
  479. overflow: hidden;
  480. background-color: #fff;
  481. .picker-view {
  482. height: 100%;
  483. box-sizing: border-box;
  484. .item {
  485. display: flex;
  486. align-items: center;
  487. justify-content: center;
  488. font-size: 32rpx;
  489. padding: 0 8rpx;
  490. .text {
  491. font-size: 24rpx;
  492. padding-left: 8rpx;
  493. }
  494. }
  495. }
  496. }
  497. }
  498. }
  499. </style>