index.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <template>
  2. <!--
  3. ss-datetime-picker 智能日期时间选择组件
  4. 基于 uview-plus 的 datetime-picker
  5. 自动绑定ValidatedTd的事件处理函数
  6. -->
  7. <view class="smart-datetime-picker">
  8. <up-datetime-picker
  9. ref="datetimePicker"
  10. :show="show"
  11. v-model="innerValue"
  12. :mode="mode"
  13. :min-date="minDate"
  14. :max-date="maxDate"
  15. :formatter="formatter"
  16. :filter="filter"
  17. :default-index="defaultIndex"
  18. :item-height="itemHeight"
  19. :cancel-text="cancelText"
  20. :confirm-text="confirmText"
  21. :cancel-color="cancelColor"
  22. :confirm-color="confirmColor"
  23. :visible-item-count="visibleItemCount"
  24. :close-on-click-overlay="closeOnClickOverlay"
  25. :safe-area-inset-bottom="safeAreaInsetBottom"
  26. :z-index="zIndex"
  27. :disabled="disabled"
  28. @confirm="handleConfirm"
  29. @cancel="handleCancel"
  30. @close="handleClose"
  31. />
  32. <!-- 显示区域 -->
  33. <view
  34. class="smart-datetime-picker__display"
  35. :class="{ 'is-disabled': disabled }"
  36. :aria-disabled="disabled ? 'true' : 'false'"
  37. @click="handleDisplayClick"
  38. >
  39. <text
  40. v-if="displayValue"
  41. class="smart-datetime-picker__value"
  42. >
  43. {{ displayValue }}
  44. </text>
  45. <text
  46. v-else
  47. class="smart-datetime-picker__placeholder"
  48. >
  49. {{ placeholder }}
  50. </text>
  51. <Icon name="icon-xiangxiajiantou" size="32" color="#999"/>
  52. </view>
  53. </view>
  54. </template>
  55. <script setup>
  56. import { ref, computed, inject, watch } from 'vue'
  57. import Icon from '@/components/icon/index.vue';
  58. import dayjs from 'dayjs'
  59. const props = defineProps({
  60. modelValue: {
  61. type: [String, Number, Date],
  62. default: ''
  63. },
  64. // 是否禁用
  65. disabled: {
  66. type: Boolean,
  67. default: false
  68. },
  69. // 选择器模式:date-日期,time-时间,datetime-日期时间
  70. mode: {
  71. type: String,
  72. default: 'date',
  73. validator: (value) => ['date', 'time', 'datetime'].includes(value)
  74. },
  75. // 最小日期
  76. minDate: {
  77. type: [String, Number, Date],
  78. default: () => dayjs().subtract(10, 'year').valueOf()
  79. },
  80. // 最大日期
  81. maxDate: {
  82. type: [String, Number, Date],
  83. default: () => dayjs().add(10, 'year').valueOf()
  84. },
  85. // 占位符
  86. placeholder: {
  87. type: String,
  88. default: '请选择'
  89. },
  90. // 格式化函数
  91. formatter: {
  92. type: Function,
  93. default: null
  94. },
  95. // 过滤函数
  96. filter: {
  97. type: Function,
  98. default: null
  99. },
  100. // 默认选中的索引
  101. defaultIndex: {
  102. type: Array,
  103. default: () => []
  104. },
  105. // 选项高度
  106. itemHeight: {
  107. type: [String, Number],
  108. default: 44
  109. },
  110. // 取消按钮文字
  111. cancelText: {
  112. type: String,
  113. default: '取消'
  114. },
  115. // 确认按钮文字
  116. confirmText: {
  117. type: String,
  118. default: '确认'
  119. },
  120. // 取消按钮颜色
  121. cancelColor: {
  122. type: String,
  123. default: '#909193'
  124. },
  125. // 确认按钮颜色
  126. confirmColor: {
  127. type: String,
  128. default: '#3c9cff'
  129. },
  130. // 可见选项数量
  131. visibleItemCount: {
  132. type: [String, Number],
  133. default: 5
  134. },
  135. // 点击遮罩是否关闭
  136. closeOnClickOverlay: {
  137. type: Boolean,
  138. default: true
  139. },
  140. // 是否开启底部安全区适配
  141. safeAreaInsetBottom: {
  142. type: Boolean,
  143. default: false
  144. },
  145. // 显示格式
  146. displayFormat: {
  147. type: String,
  148. default: ''
  149. },
  150. // z-index层级
  151. zIndex: {
  152. type: [String, Number],
  153. default: 20000 // 设置一个很高的默认值,确保在SsBottom之上
  154. }
  155. })
  156. const emit = defineEmits(['update:modelValue', 'confirm', 'cancel', 'change'])
  157. // 从ValidatedTd注入事件处理函数(兼容旧方式)
  158. const onInput = inject('onInput', null)
  159. const onBlur = inject('onBlur', null)
  160. // 控制选择器显示
  161. const show = ref(false)
  162. // 内部值
  163. const innerValue = ref(Date.now())
  164. // 日期选择器引用
  165. const datetimePicker = ref(null)
  166. // 初始化内部值
  167. const initInnerValue = () => {
  168. if (props.mode === 'time') {
  169. // 时间模式下,直接使用时间字符串
  170. if (props.modelValue) {
  171. innerValue.value = props.modelValue
  172. } else {
  173. // 如果没有值,使用当前时间
  174. const now = new Date()
  175. const hours = now.getHours().toString().padStart(2, '0')
  176. const minutes = now.getMinutes().toString().padStart(2, '0')
  177. innerValue.value = `${hours}:${minutes}`
  178. }
  179. } else {
  180. // 日期和日期时间模式
  181. if (props.modelValue) {
  182. if (typeof props.modelValue === 'string') {
  183. innerValue.value = dayjs(props.modelValue).valueOf()
  184. } else if (typeof props.modelValue === 'number') {
  185. innerValue.value = props.modelValue
  186. } else if (props.modelValue instanceof Date) {
  187. innerValue.value = props.modelValue.getTime()
  188. }
  189. } else {
  190. innerValue.value = dayjs().valueOf()
  191. }
  192. }
  193. }
  194. // 监听 modelValue 变化
  195. watch(() => props.modelValue, () => {
  196. initInnerValue()
  197. }, { immediate: true })
  198. // 计算显示值
  199. const displayValue = computed(() => {
  200. if (!props.modelValue) return ''
  201. if (props.mode === 'time') {
  202. // 时间模式下直接显示值
  203. return props.modelValue
  204. }
  205. let format = props.displayFormat
  206. if (!format) {
  207. switch (props.mode) {
  208. case 'date':
  209. format = 'YYYY-MM-DD'
  210. break
  211. case 'datetime':
  212. format = 'YYYY-MM-DD HH:mm'
  213. break
  214. default:
  215. format = 'YYYY-MM-DD'
  216. }
  217. }
  218. const date = dayjs(props.modelValue)
  219. return date.isValid() ? date.format(format) : props.modelValue
  220. })
  221. // 打开选择器
  222. const openPicker = () => {
  223. show.value = true
  224. console.log('打开日期选择器', show.value)
  225. }
  226. // 处理显示区域点击(考虑禁用态)
  227. const handleDisplayClick = () => {
  228. if (props.disabled) return
  229. openPicker()
  230. }
  231. // 确认选择
  232. const handleConfirm = (value) => {
  233. console.log('原始值:', value)
  234. let formattedValue = ''
  235. try {
  236. if (props.mode === 'time') {
  237. // 时间模式下,value.value 已经是 "HH:mm" 格式
  238. formattedValue = value.value
  239. console.log('处理后的时间值:', formattedValue)
  240. } else {
  241. // 日期和日期时间模式
  242. const date = dayjs(value.value)
  243. switch (props.mode) {
  244. case 'date':
  245. formattedValue = date.format('YYYY-MM-DD')
  246. break
  247. case 'datetime':
  248. formattedValue = date.format('YYYY-MM-DD HH:mm')
  249. break
  250. default:
  251. formattedValue = date.format('YYYY-MM-DD')
  252. }
  253. }
  254. // 1. 支持v-model
  255. emit('update:modelValue', formattedValue)
  256. // 2. 兼容旧的inject方式
  257. if (onInput) {
  258. onInput({ detail: { value: formattedValue } })
  259. }
  260. // 3. 触发确认事件
  261. emit('confirm', { value: formattedValue, timestamp: value.value })
  262. emit('change', formattedValue)
  263. } catch (error) {
  264. console.error('时间处理错误:', error)
  265. formattedValue = ''
  266. }
  267. show.value = false
  268. console.log(`SsDatetimePicker handleConfirm: ${formattedValue}`)
  269. }
  270. // 取消选择
  271. const handleCancel = () => {
  272. show.value = false
  273. emit('cancel')
  274. }
  275. // 关闭选择器
  276. const handleClose = () => {
  277. show.value = false
  278. // 触发失焦事件
  279. if (onBlur) {
  280. onBlur()
  281. }
  282. }
  283. // 初始化
  284. initInnerValue()
  285. </script>
  286. <style lang="scss" scoped>
  287. .smart-datetime-picker {
  288. width: 100%;
  289. &__display {
  290. width: 100%;
  291. height: 60rpx;
  292. display: flex;
  293. align-items: center;
  294. justify-content: space-between;
  295. font-size: 32rpx;
  296. line-height: 60rpx;
  297. color: #333;
  298. background-color: transparent;
  299. border: none;
  300. outline: none;
  301. box-sizing: border-box;
  302. cursor: pointer;
  303. }
  304. &__value {
  305. flex: 1;
  306. color: #333;
  307. font-size: 32rpx;
  308. }
  309. &__placeholder {
  310. flex: 1;
  311. color: #999;
  312. font-size: 32rpx;
  313. }
  314. &__icon {
  315. margin-left: 16rpx;
  316. transition: transform 0.3s ease;
  317. }
  318. &__display:active &__icon {
  319. transform: rotate(180deg);
  320. }
  321. &__display.is-disabled {
  322. opacity: 0.6;
  323. cursor: not-allowed;
  324. }
  325. }
  326. </style>
  327. <!-- 全局样式,确保uview picker的z-index足够高 -->
  328. <style lang="scss">
  329. /* 覆盖uview-plus的datetime-picker样式 */
  330. .u-datetime-picker,
  331. .u-picker,
  332. .u-popup {
  333. z-index: 99999 !important;
  334. }
  335. .u-popup__content {
  336. z-index: 99999 !important;
  337. }
  338. .u-overlay {
  339. z-index: 99998 !important;
  340. }
  341. </style>