消毒机设备
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.

604 lines
15 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <script lang="ts" setup>
  2. import { computed, defineEmits, defineProps, onMounted, onUnmounted, ref, watch } from 'vue'
  3. const props = defineProps({
  4. modelValue: {
  5. type: [String, Date, null],
  6. default: null,
  7. },
  8. minDate: {
  9. type: [String, Date],
  10. default: null,
  11. },
  12. maxDate: {
  13. type: [String, Date],
  14. default: null,
  15. },
  16. format: {
  17. type: String,
  18. default: 'YYYY-MM-DD HH:mm:ss',
  19. },
  20. })
  21. const emits = defineEmits(['update:modelValue', 'change'])
  22. const dateText = ref()
  23. const hoursOptions = ref(Array.from({ length: 24 }, (_, i) => i))
  24. const minuteOptions = ref(Array.from({ length: 60 }, (_, i) => i))
  25. const secondOptions = ref(Array.from({ length: 60 }, (_, i) => i))
  26. // 初始化日期时间
  27. const selectedDate = ref<Date | null>(null)
  28. const currentYear = ref(0)
  29. const currentMonth = ref(0)
  30. const today = new Date()
  31. const weekdays = ['日', '一', '二', '三', '四', '五', '六']
  32. const isDropdownVisible = ref(false)
  33. const selectedHour = ref('00')
  34. const selectedMinute = ref('00')
  35. const selectedSecond = ref('00')
  36. // 格式化显示值
  37. const displayValue = () => {
  38. if (!selectedDate.value) {
  39. return ''
  40. }
  41. const year = selectedDate.value.getFullYear()
  42. const month = (selectedDate.value.getMonth() + 1).toString().padStart(2, '0')
  43. const date = selectedDate.value.getDate().toString().padStart(2, '0')
  44. const hour = selectedDate.value.getHours().toString().padStart(2, '0')
  45. const minute = selectedDate.value.getMinutes().toString().padStart(2, '0')
  46. const second = selectedDate.value.getSeconds().toString().padStart(2, '0')
  47. dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}`
  48. }
  49. watch(selectedDate, (newVal) => {
  50. if (newVal) {
  51. const year = newVal.getFullYear()
  52. const month = (newVal.getMonth() + 1).toString().padStart(2, '0')
  53. const date = newVal.getDate().toString().padStart(2, '0')
  54. const hour = newVal.getHours().toString().padStart(2, '0')
  55. const minute = newVal.getMinutes().toString().padStart(2, '0')
  56. const second = newVal.getSeconds().toString().padStart(2, '0')
  57. dateText.value = `${year}-${month}-${date} ${hour}:${minute}:${second}`
  58. }
  59. })
  60. // 计算当前月份的天数及上个月和下个月的部分天数
  61. const daysInMonth: any = computed(() => {
  62. const days: Record<string, any>[] = []
  63. // 获取当前月份第一天是星期几
  64. const firstDay = new Date(currentYear.value, currentMonth.value, 1).getDay()
  65. // 获取上个月的最后几天
  66. const prevMonthLastDay = new Date(currentYear.value, currentMonth.value, 0).getDate()
  67. for (let i = firstDay; i > 0; i--) {
  68. const date = new Date(currentYear.value, currentMonth.value - 1, prevMonthLastDay - i + 1)
  69. days.push({ date, month: currentMonth.value - 1 })
  70. }
  71. // 获取当前月份的所有天数
  72. const currentMonthDays = new Date(currentYear.value, currentMonth.value + 1, 0).getDate()
  73. for (let i = 1; i <= currentMonthDays; i++) {
  74. const date = new Date(currentYear.value, currentMonth.value, i)
  75. days.push({ date, month: currentMonth.value })
  76. }
  77. // 获取下个月的前几天
  78. const remainingDays = 42 - days.length // 6行7列共42个格子
  79. for (let i = 1; i <= remainingDays; i++) {
  80. const date = new Date(currentYear.value, currentMonth.value + 1, i)
  81. days.push({ date, month: currentMonth.value + 1 })
  82. }
  83. return days
  84. })
  85. // 初始化
  86. onMounted(() => {
  87. const initialDate = props.modelValue ? new Date(Number(props.modelValue)) : new Date()
  88. selectedDate.value = initialDate
  89. currentYear.value = initialDate.getFullYear()
  90. currentMonth.value = initialDate.getMonth()
  91. selectedHour.value = initialDate.getHours().toString().padStart(2, '0')
  92. selectedMinute.value = initialDate.getMinutes().toString().padStart(2, '0')
  93. selectedSecond.value = initialDate.getSeconds().toString().padStart(2, '0')
  94. })
  95. // 监听props变化
  96. watch(() => props.modelValue, (newVal) => {
  97. if (newVal) {
  98. const date = new Date(Number(newVal))
  99. if (!Number.isNaN(date.getTime())) {
  100. selectedDate.value = date
  101. currentYear.value = date.getFullYear()
  102. currentMonth.value = date.getMonth()
  103. selectedHour.value = date.getHours().toString().padStart(2, '0')
  104. selectedMinute.value = date.getMinutes().toString().padStart(2, '0')
  105. selectedSecond.value = date.getSeconds().toString().padStart(2, '0')
  106. }
  107. }
  108. else {
  109. selectedDate.value = null
  110. }
  111. }, { deep: true })
  112. // 日期操作
  113. const prevMonth = () => {
  114. if (currentMonth.value === 0) {
  115. currentMonth.value = 11
  116. currentYear.value--
  117. }
  118. else {
  119. currentMonth.value--
  120. }
  121. }
  122. const nextMonth = () => {
  123. if (currentMonth.value === 11) {
  124. currentMonth.value = 0
  125. currentYear.value++
  126. }
  127. else {
  128. currentMonth.value++
  129. }
  130. }
  131. const prevYear = () => {
  132. currentYear.value--
  133. }
  134. const nextYear = () => {
  135. currentYear.value++
  136. }
  137. // 日期选择
  138. const selectDate = (date: Date) => {
  139. if (!isDateInRange(date)) {
  140. return
  141. }
  142. if (!selectedDate.value) {
  143. selectedDate.value = new Date(date)
  144. }
  145. else {
  146. // 保留原来的时分秒
  147. selectedDate.value.setFullYear(date.getFullYear())
  148. selectedDate.value.setMonth(date.getMonth())
  149. selectedDate.value.setDate(date.getDate())
  150. }
  151. displayValue()
  152. }
  153. // 时间更新
  154. const updateTime = () => {
  155. console.log('selectedDate.value--', selectedDate.value, selectedHour.value)
  156. if (selectedDate.value) {
  157. selectedDate.value.setHours(Number(selectedHour.value))
  158. selectedDate.value.setMinutes(Number(selectedMinute.value))
  159. selectedDate.value.setSeconds(Number(selectedSecond.value))
  160. }
  161. displayValue()
  162. }
  163. // 确认选择
  164. const confirmSelection = () => {
  165. if (selectedDate.value) {
  166. const dateValue = formatDate(selectedDate.value, props.format)
  167. // emits('update:modelValue', dateValue)
  168. emits('change', dateValue)
  169. }
  170. isDropdownVisible.value = false
  171. }
  172. // 日期格式化工具函数
  173. const formatDate = (date: Date, format: string): string => {
  174. if (!date) {
  175. return ''
  176. }
  177. const year = date.getFullYear()
  178. const month = date.getMonth() + 1
  179. const day = date.getDate()
  180. const hour = date.getHours()
  181. const minute = date.getMinutes()
  182. const second = date.getSeconds()
  183. return format
  184. .replace('YYYY', year.toString())
  185. .replace('MM', month.toString().padStart(2, '0'))
  186. .replace('DD', day.toString().padStart(2, '0'))
  187. .replace('HH', hour.toString().padStart(2, '0'))
  188. .replace('mm', minute.toString().padStart(2, '0'))
  189. .replace('ss', second.toString().padStart(2, '0'))
  190. }
  191. // 取消选择
  192. const cancelSelection = () => {
  193. // 恢复之前的值
  194. // if (props.modelValue) {
  195. // const date = new Date(props.modelValue)
  196. // if (!isNaN(date.getTime())) {
  197. // selectedDate.value = date
  198. // selectedHour.value = date.getHours().toString().padStart(2, '0')
  199. // selectedMinute.value = date.getMinutes().toString().padStart(2, '0')
  200. // selectedSecond.value = date.getSeconds().toString().padStart(2, '0')
  201. // }
  202. // } else {
  203. // selectedDate.value = null
  204. // }
  205. isDropdownVisible.value = false
  206. }
  207. // 切换下拉框显示
  208. const toggleDropdown = (event?: MouseEvent) => {
  209. if (event) {
  210. event.stopPropagation()
  211. }
  212. isDropdownVisible.value = !isDropdownVisible.value
  213. }
  214. // 工具函数
  215. const isSameDay = (date1: Date, date2: Date | null) => {
  216. if (!date2) {
  217. return false
  218. }
  219. return (
  220. date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate()
  221. )
  222. }
  223. const isDateInRange = (date: Date) => {
  224. const min = props.minDate ? new Date(props.minDate) : null
  225. const max = props.maxDate ? new Date(props.maxDate) : null
  226. if (min && date < min) {
  227. return false
  228. }
  229. if (max && date > max) {
  230. return false
  231. }
  232. return true
  233. }
  234. // 点击外部关闭下拉框
  235. const closeDropdownOnClickOutside = (event: MouseEvent) => {
  236. if (!isDropdownVisible.value) {
  237. return
  238. }
  239. const target = event.target as HTMLElement
  240. const container = document.querySelector('.date-time-picker')
  241. if (container && !container.contains(target)) {
  242. isDropdownVisible.value = false
  243. }
  244. }
  245. // 添加和移除事件监听
  246. onMounted(() => {
  247. document.addEventListener('click', closeDropdownOnClickOutside)
  248. })
  249. onUnmounted(() => {
  250. document.removeEventListener('click', closeDropdownOnClickOutside)
  251. })
  252. </script>
  253. <template>
  254. <div class="date-time-picker">
  255. <div class="input-group">
  256. <input
  257. v-model="dateText"
  258. type="text"
  259. readonly
  260. placeholder="请选择日期时间"
  261. class="form-input"
  262. @click="toggleDropdown"
  263. >
  264. <span class="input-icon" @click="toggleDropdown">
  265. <i class="fa fa-calendar" />
  266. </span>
  267. </div>
  268. <transition name="fade">
  269. <div v-show="isDropdownVisible" class="dropdown-container">
  270. <div class="calendar-header">
  271. <button class="nav-btn" @click="prevYear">
  272. <el-icon><DArrowLeft /></el-icon>
  273. </button>
  274. <button class="nav-btn" @click="prevMonth">
  275. <el-icon><ArrowLeft /></el-icon>
  276. </button>
  277. <span class="current-date">
  278. {{ currentYear }} {{ currentMonth + 1 }}
  279. </span>
  280. <button class="nav-btn" @click="nextMonth">
  281. <el-icon><ArrowRight /></el-icon>
  282. </button>
  283. <button class="nav-btn" @click="nextYear">
  284. <el-icon><DArrowRight /></el-icon>
  285. </button>
  286. </div>
  287. <div class="weekdays">
  288. <div v-for="day in weekdays" :key="day" class="weekday">
  289. {{ day }}
  290. </div>
  291. </div>
  292. <div class="days">
  293. <div
  294. v-for="(day, index) in daysInMonth"
  295. :key="index"
  296. class="day"
  297. :class="{
  298. 'other-month': day.month !== currentMonth,
  299. 'selected': isSameDay(day.date, selectedDate),
  300. 'today': isSameDay(day.date, today),
  301. 'disabled': !isDateInRange(day.date),
  302. }"
  303. @click="selectDate(day.date)"
  304. >
  305. {{ day.date.getDate() }}
  306. </div>
  307. </div>
  308. <div class="time-selector">
  309. <div class="time-label">
  310. 时间:
  311. </div>
  312. <el-select v-model="selectedHour" class="time-select" @change="updateTime">
  313. <el-option v-for="i in hoursOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
  314. {{ i.toString().padStart(2, '0') }}
  315. </el-option>
  316. </el-select>
  317. <span class="time-separator">:</span>
  318. <el-select v-model="selectedMinute" class="time-select" @change="updateTime">
  319. <el-option v-for="i in minuteOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
  320. {{ i.toString().padStart(2, '0') }}
  321. </el-option>
  322. </el-select>
  323. <span class="time-separator">:</span>
  324. <el-select v-model="selectedSecond" class="time-select" @change="updateTime">
  325. <el-option v-for="i in secondOptions" :key="i" :value="i.toString().padStart(2, '0')" style="font-size: 20px;line-height: 2px;height: 5rem;display: flex; align-items: center;">
  326. {{ i.toString().padStart(2, '0') }}
  327. </el-option>
  328. </el-select>
  329. </div>
  330. <div class="calendar-footer">
  331. <button class="confirm-btn" @click="confirmSelection">
  332. 确定
  333. </button>
  334. <button class="cancel-btn" @click="cancelSelection">
  335. 取消
  336. </button>
  337. </div>
  338. </div>
  339. </transition>
  340. </div>
  341. </template>
  342. <style lang="scss" scoped>
  343. $fontSize: 1.5rem;
  344. :deep(body) {
  345. --el-font-size-base: 20px;
  346. }
  347. .date-time-picker {
  348. position: relative;
  349. width: 340px;
  350. }
  351. .input-group {
  352. display: flex;
  353. position: relative;
  354. }
  355. .form-input {
  356. flex: 1;
  357. padding: 10px 14px;
  358. border: 1px solid #e2e8f0;
  359. border-radius: 4px;
  360. font-size: $fontSize;
  361. outline: none;
  362. transition: border-color 0.2s;
  363. }
  364. .form-input:focus {
  365. border-color: #4f46e5;
  366. }
  367. .input-icon {
  368. position: absolute;
  369. right: 12px;
  370. top: 50%;
  371. transform: translateY(-50%);
  372. color: #94a3b8;
  373. cursor: pointer;
  374. font-size: $fontSize;
  375. }
  376. .dropdown-container {
  377. position: absolute;
  378. top: calc(100% + 4px);
  379. right: 0;
  380. width: 28rem;
  381. background-color: white;
  382. border: 1px solid #e2e8f0;
  383. border-radius: 4px;
  384. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  385. z-index: 100;
  386. padding: 12px;
  387. }
  388. .calendar-header {
  389. display: flex;
  390. justify-content: space-between;
  391. align-items: center;
  392. margin-bottom: 12px;
  393. }
  394. .nav-btn {
  395. background: none;
  396. border: none;
  397. cursor: pointer;
  398. color: #64748b;
  399. font-size: $fontSize;
  400. padding: 6px;
  401. border-radius: 4px;
  402. transition: background-color 0.2s;
  403. }
  404. .nav-btn:hover {
  405. background-color: #f1f5f9;
  406. }
  407. .current-date {
  408. font-weight: 500;
  409. color: #334155;
  410. font-size: $fontSize;
  411. }
  412. .weekdays {
  413. display: grid;
  414. grid-template-columns: repeat(7, 1fr);
  415. margin-bottom: 6px;
  416. }
  417. .weekday {
  418. text-align: center;
  419. font-size: $fontSize;
  420. color: #64748b;
  421. padding: 6px;
  422. }
  423. .days {
  424. display: grid;
  425. grid-template-columns: repeat(7, 1fr);
  426. gap: 3px;
  427. margin-bottom: 12px;
  428. }
  429. .day {
  430. text-align: center;
  431. padding: 8px;
  432. border-radius: 4px;
  433. font-size: $fontSize;
  434. cursor: pointer;
  435. transition: background-color 0.2s;
  436. }
  437. .day:hover:not(.disabled) {
  438. background-color: #e0f2fe;
  439. }
  440. .day.other-month {
  441. color: #94a3b8;
  442. }
  443. .day.selected {
  444. background-color: #3b82f6;
  445. color: white;
  446. }
  447. .day.today:not(.selected) {
  448. font-weight: 500;
  449. color: #3b82f6;
  450. }
  451. .day.disabled {
  452. color: #cbd5e1;
  453. cursor: not-allowed;
  454. }
  455. .time-selector {
  456. display: flex;
  457. align-items: center;
  458. margin-bottom: 12px;
  459. .time-select {
  460. padding: 4px 8px;
  461. border-radius: 4px;
  462. font-size: 20px;
  463. outline: none;
  464. background-color: white;
  465. margin-right: 4px;
  466. /* 最大高度限制,溢出滚动 */
  467. max-height: 3rem;
  468. width: 8rem;
  469. border: none;
  470. }
  471. }
  472. select option {
  473. max-height: 3rem;
  474. border: 1px solid red;
  475. }
  476. .time-label {
  477. margin-right: 12px;
  478. font-size: $fontSize;
  479. color: #334155;
  480. width: 5rem;
  481. }
  482. .select-option{
  483. max-height: 3rem;
  484. }
  485. select {
  486. padding: 6px 10px;
  487. border: 1px solid #e2e8f0;
  488. border-radius: 4px;
  489. font-size: $fontSize;
  490. outline: none;
  491. background-color: white;
  492. margin-right: 6px;
  493. width: 60px;
  494. }
  495. .time-separator {
  496. margin: 0 3px;
  497. font-size: $fontSize;
  498. color: #334155;
  499. }
  500. .calendar-footer {
  501. display: flex;
  502. justify-content: flex-end;
  503. gap: 10px;
  504. }
  505. .confirm-btn, .cancel-btn {
  506. padding: 8px 16px;
  507. border: none;
  508. border-radius: 4px;
  509. font-size: $fontSize;
  510. cursor: pointer;
  511. transition: background-color 0.2s;
  512. }
  513. .confirm-btn {
  514. background-color: #3b82f6;
  515. color: white;
  516. }
  517. .confirm-btn:hover {
  518. background-color: #2563eb;
  519. }
  520. .cancel-btn {
  521. background-color: #f1f5f9;
  522. color: #334155;
  523. }
  524. .cancel-btn:hover {
  525. background-color: #e2e8f0;
  526. }
  527. .fade-enter-active, .fade-leave-active {
  528. transition: opacity 0.2s;
  529. }
  530. .fade-enter-from, .fade-leave-to {
  531. opacity: 0;
  532. }
  533. // 修改选择框本身的宽高
  534. :deep(.el-select__wrapper){
  535. height: 3rem;
  536. font-size: 20px;
  537. }
  538. </style>