TimePicker.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <template>
  2. <uni-popup ref="popup" type="center" :safe-area="false">
  3. <view class="time-picker-container">
  4. <view class="picker-header">
  5. <text class="title">取件时间</text>
  6. <view class="close-btn" @click="close">
  7. <zui-svg-icon icon="general-close" :width="18" :height="18" color="#666" />
  8. </view>
  9. </view>
  10. <picker-view v-if="visible" class="picker-view" :indicator-style="indicatorStyle" :value="pickerValue" @change="handleChange" :immediate-change="true">
  11. <picker-view-column>
  12. <view class="picker-item" v-for="(item, index) in days" :key="index">{{ item.label }}</view>
  13. </picker-view-column>
  14. <picker-view-column>
  15. <view class="picker-item" v-for="(item, index) in hours" :key="index">{{ item.label }}</view>
  16. </picker-view-column>
  17. <picker-view-column>
  18. <view class="picker-item" v-for="(item, index) in minutes" :key="index">{{ item.label }}</view>
  19. </picker-view-column>
  20. </picker-view>
  21. <view class="confirm-button-wrapper">
  22. <button class="confirm-btn" @click="handleConfirm">确认</button>
  23. </view>
  24. </view>
  25. </uni-popup>
  26. </template>
  27. <script>
  28. import dayjs from 'dayjs';
  29. const global_minutes = [0, 10, 20, 30, 40, 50];
  30. export default {
  31. name: 'TimePicker',
  32. data() {
  33. return {
  34. visible: false, // 控制picker-view的渲染
  35. pickerValue: [0, 0, 0],
  36. tempPickerValue: [0, 0, 0], // 临时存储滚动过程中的值
  37. debounceTimer: null,
  38. days: [],
  39. hours: [],
  40. minutes: [],
  41. indicatorStyle: `height: 50upx; border-top: 1rpx solid #eee; border-bottom: 1rpx solid #eee;`,
  42. };
  43. },
  44. created() {
  45. this.initTime();
  46. },
  47. methods: {
  48. open() {
  49. this.visible = true;
  50. this.initTime();
  51. this.$refs.popup.open();
  52. },
  53. close() {
  54. this.visible = false;
  55. this.$refs.popup.close();
  56. },
  57. initTime() {
  58. const now = dayjs(); // Use current time
  59. this.days = this.generateDays();
  60. const isToday = true;
  61. let availableHoursToday = this.generateHours(now, isToday);
  62. // If no hours available today, switch to tomorrow
  63. if (availableHoursToday.length <= 1) { // Only "立即"
  64. const tomorrow = dayjs().add(1, 'day').startOf('day');
  65. this.days = this.generateDays(); // Regenerate days if needed
  66. this.hours = this.generateHours(tomorrow, false);
  67. this.minutes = this.generateMinutes(tomorrow, false, this.hours[0].value);
  68. this.pickerValue = [1, 0, 0]; // Select "Tomorrow", first hour, first minute
  69. this.tempPickerValue = [1, 0, 0];
  70. return;
  71. }
  72. this.hours = availableHoursToday;
  73. // Default to "立即"
  74. const initialHourIndex = 0;
  75. const selectedHour = this.hours[initialHourIndex].value;
  76. this.minutes = this.generateMinutes(now, isToday, selectedHour);
  77. const initialMinuteIndex = 0;
  78. this.pickerValue = [0, initialHourIndex, initialMinuteIndex];
  79. this.tempPickerValue = [0, initialHourIndex, initialMinuteIndex];
  80. },
  81. generateDays() {
  82. return [
  83. { label: '今天', value: dayjs().format('YYYY-MM-DD') },
  84. { label: '明天', value: dayjs().add(1, 'day').format('YYYY-MM-DD') },
  85. ];
  86. },
  87. generateHours(now, isToday) {
  88. const hours = [];
  89. if (isToday) {
  90. hours.push({ label: '立即', value: -1 });
  91. let startHour = now.hour() + 1; // 当前小时的下一个小时
  92. // Find if any minutes are available in the current hour
  93. const firstAvailableMinute = global_minutes.find(m => m > now.minute());
  94. // If no minutes available in current hour, or if the last minute has passed, start from the next hour
  95. if (firstAvailableMinute === undefined) {
  96. startHour++;
  97. }
  98. for (let i = startHour; i <= 23; i++) {
  99. hours.push({ label: `${i}时`, value: i });
  100. }
  101. } else {
  102. for (let i = 0; i <= 23; i++) {
  103. hours.push({ label: `${i}时`, value: i });
  104. }
  105. }
  106. return hours;
  107. },
  108. generateMinutes(now, isToday, selectedHour) {
  109. if (selectedHour === -1) {
  110. return [{ label: '', value: -1 }];
  111. }
  112. let availableMinutes = global_minutes;
  113. if (isToday && selectedHour === now.hour()) {
  114. const currentMinute = now.minute();
  115. availableMinutes = global_minutes.filter(m => m > currentMinute);
  116. }
  117. return availableMinutes.map(m => ({ label: `${m}分`, value: m }));
  118. },
  119. handleChange(e) {
  120. const val = e.detail.value;
  121. this.tempPickerValue = val;
  122. if (this.debounceTimer) {
  123. clearTimeout(this.debounceTimer);
  124. }
  125. this.debounceTimer = setTimeout(() => {
  126. this.updatePickers(val);
  127. }, 100);
  128. },
  129. updatePickers(val, isSync = false) {
  130. const [dayIndex, hourIndex, minuteIndex] = val;
  131. const oldPickerValue = this.pickerValue;
  132. const now = dayjs();
  133. const isToday = dayIndex === 0;
  134. let newHours = this.hours;
  135. let newMinutes = this.minutes;
  136. let finalHourIndex = hourIndex;
  137. let finalMinuteIndex = minuteIndex;
  138. // Day changed
  139. if (oldPickerValue[0] !== dayIndex) {
  140. newHours = this.generateHours(now, isToday);
  141. // 修正越界,尽量保持用户的选择,而不是强制归零
  142. if (finalHourIndex >= newHours.length) {
  143. finalHourIndex = 0;
  144. }
  145. }
  146. // Calculate selected hour based on current indices
  147. const selectedHour = newHours[finalHourIndex] ? newHours[finalHourIndex].value : -1;
  148. // Always regenerate minutes to ensure correctness (e.g. today/tomorrow switch, or hour switch)
  149. // Optimization: we could check if generation parameters actually changed, but it's fast enough.
  150. newMinutes = this.generateMinutes(now, isToday, selectedHour);
  151. // 修正越界
  152. if (finalMinuteIndex >= newMinutes.length) {
  153. finalMinuteIndex = 0;
  154. }
  155. this.hours = newHours;
  156. this.minutes = newMinutes;
  157. this.debounceTimer = null;
  158. const newValue = [dayIndex, finalHourIndex, finalMinuteIndex];
  159. if (isSync) {
  160. this.pickerValue = newValue;
  161. } else {
  162. // Use setTimeout to avoid visual glitch on picker reset
  163. setTimeout(() => {
  164. this.pickerValue = newValue;
  165. }, 0);
  166. }
  167. },
  168. handleConfirm() {
  169. if (this.debounceTimer) {
  170. clearTimeout(this.debounceTimer);
  171. this.updatePickers(this.tempPickerValue, true);
  172. }
  173. const [dayIndex, hourIndex, minuteIndex] = this.pickerValue;
  174. const selectedDay = this.days[dayIndex];
  175. const selectedHour = this.hours[hourIndex];
  176. const selectedMinute = this.minutes[minuteIndex];
  177. let result = {};
  178. if (selectedHour && selectedHour.value === -1) {
  179. result = {
  180. label: '立即取件',
  181. value: dayjs().format('YYYY-MM-DD HH:mm'),
  182. };
  183. } else {
  184. const date = selectedDay.value;
  185. const hour = selectedHour?.value ?? 0;
  186. const minute = selectedMinute?.value ?? 0;
  187. const finalDate = dayjs(`${date} ${hour}:${minute}`).toDate();
  188. result = {
  189. label: dayjs(finalDate).format('YYYY-MM-DD HH:mm'),
  190. value: dayjs(finalDate).format('YYYY-MM-DD HH:mm'),
  191. };
  192. }
  193. this.$emit('confirm', result);
  194. this.close();
  195. },
  196. },
  197. };
  198. </script>
  199. <style lang="scss" scoped>
  200. .time-picker-container {
  201. background-color: #fff;
  202. border-radius: 20upx;
  203. width: 300upx;
  204. height: 400upx;
  205. display: flex;
  206. flex-direction: column;
  207. overflow: hidden;
  208. }
  209. .picker-header {
  210. position: relative;
  211. text-align: center;
  212. padding: 20upx;
  213. flex-shrink: 0;
  214. .title {
  215. font-size: 24upx;
  216. font-weight: 350;
  217. }
  218. .close-btn {
  219. position: absolute;
  220. right: 24upx;
  221. top: 50%;
  222. transform: translateY(-50%);
  223. padding: 10upx;
  224. }
  225. }
  226. .picker-view {
  227. width: 100%;
  228. flex: 1;
  229. overflow: hidden;
  230. }
  231. .picker-item {
  232. display: flex;
  233. align-items: center;
  234. justify-content: center;
  235. font-size: 22upx;
  236. max-height: 50upx;
  237. line-height: 50upx;
  238. padding: 0;
  239. width: 100%;
  240. white-space: nowrap;
  241. box-sizing: border-box;
  242. /* #ifdef APP-PLUS */
  243. min-height: 50upx;
  244. /* #endif */
  245. }
  246. .confirm-button-wrapper {
  247. padding: 15upx 20upx;
  248. padding-bottom: 25upx;
  249. flex-shrink: 0;
  250. }
  251. .confirm-btn {
  252. background: linear-gradient(to right, #409eff, #67c2ff);
  253. color: white;
  254. border-radius: 50upx;
  255. font-size: 24upx;
  256. height: 50upx;
  257. line-height: 50upx;
  258. border: none;
  259. &::after {
  260. border: none;
  261. }
  262. }
  263. </style>