calendar-comp.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <script lang="ts" setup>
  2. import { computed, ref } from 'vue'
  3. import chineseDays from "chinese-days"
  4. const { getDayDetail, getLunarDate, getSolarTermsInRange, isInLieu } = chineseDays
  5. const props = withDefaults(
  6. defineProps<{
  7. lang: 'zh' | 'en'
  8. startOfWeek?: 1 | 2 | 3 | 4 | 5 | 6 | 0
  9. }>(),
  10. {
  11. lang: 'zh',
  12. startOfWeek: 1,
  13. },
  14. )
  15. const currentDate = ref(new Date())
  16. const currentMonth = ref(currentDate.value.getMonth())
  17. const currentYear = ref(currentDate.value.getFullYear())
  18. const daysOfWeek = computed(() =>
  19. props.lang === 'zh'
  20. ? ['日', '一', '二', '三', '四', '五', '六']
  21. : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  22. )
  23. const monthNames = [
  24. 'January',
  25. 'February',
  26. 'March',
  27. 'April',
  28. 'May',
  29. 'June',
  30. 'July',
  31. 'August',
  32. 'September',
  33. 'October',
  34. 'November',
  35. 'December',
  36. ]
  37. const daysInMonth = computed(() => {
  38. const year = currentYear.value
  39. const month = currentMonth.value
  40. const firstDay = new Date(year, month, 1)
  41. const lastDay = new Date(year, month + 1, 0)
  42. const days: Date[] = []
  43. let firstDayIndex = firstDay.getDay() - props.startOfWeek
  44. if (firstDayIndex < 0) {
  45. firstDayIndex += 7
  46. }
  47. for (let i = firstDayIndex; i > 0; i--) {
  48. days.push(new Date(year, month, -i + 1))
  49. }
  50. for (let day = 1; day <= lastDay.getDate(); day++) {
  51. days.push(new Date(year, month, day))
  52. }
  53. let lastDayIndex = lastDay.getDay() - props.startOfWeek
  54. if (lastDayIndex < 0) {
  55. lastDayIndex += 7
  56. }
  57. for (let i = 1; i < 7 - lastDayIndex; i++) {
  58. days.push(new Date(year, month + 1, i))
  59. }
  60. return days
  61. })
  62. function prevMonth() {
  63. if (currentMonth.value === 0) {
  64. currentMonth.value = 11
  65. currentYear.value--
  66. }
  67. else {
  68. currentMonth.value--
  69. }
  70. }
  71. function nextMonth() {
  72. if (currentMonth.value === 11) {
  73. currentMonth.value = 0
  74. currentYear.value++
  75. }
  76. else {
  77. currentMonth.value++
  78. }
  79. }
  80. function isToday(date: Date) {
  81. const today = new Date()
  82. return (
  83. date.getDate() === today.getDate()
  84. && date.getMonth() === today.getMonth()
  85. && date.getFullYear() === today.getFullYear()
  86. )
  87. }
  88. function getDayInfo(date: Date) {
  89. const dayDetail = getDayDetail(date)
  90. const holidayName = dayDetail.name.split(',')[1]
  91. return {
  92. disable: currentMonth.value !== date.getMonth(),
  93. isToday: isToday(date),
  94. isInLieu: isInLieu(date),
  95. solarTerm: getSolarTermsInRange(date)[0],
  96. ...getLunarDate(date),
  97. ...dayDetail,
  98. holidayName,
  99. date,
  100. }
  101. }
  102. const selectedDate = ref(getDayInfo(new Date()))
  103. function selectDate(date: any) {
  104. selectedDate.value = date
  105. }
  106. function isSelected(date: Date) {
  107. return (
  108. date.getDate() === selectedDate.value?.date.getDate()
  109. && date.getMonth() === selectedDate.value?.date.getMonth()
  110. && date.getFullYear() === selectedDate.value?.date.getFullYear()
  111. )
  112. }
  113. const daysInfo = computed(() => daysInMonth.value.map((date: Date) => getDayInfo(date)))
  114. </script>
  115. <template>
  116. <div class="calendar">
  117. <header class="calendar-header">
  118. <button @click="prevMonth">
  119. <svg
  120. viewBox="0 0 1024 1024"
  121. version="1.1"
  122. xmlns="http://www.w3.org/2000/svg"
  123. width="200"
  124. height="200"
  125. >
  126. <path
  127. d="M684.29 799.276L393.929 513.019 684.29 226.762c37.685-37.153 38.003-97.625 0.707-134.384-37.297-36.758-98.646-36.435-136.331 0.718l-357.43 352.378c-0.155 0.153-0.297 0.314-0.451 0.468-0.084 0.082-0.172 0.157-0.256 0.239-18.357 18.092-27.581 41.929-27.743 65.902-0.004 0.311-0.017 0.623-0.018 0.934 0.001 0.316 0.014 0.632 0.018 0.948 0.165 23.97 9.389 47.803 27.743 65.892 0.083 0.082 0.171 0.157 0.255 0.239 0.154 0.154 0.296 0.315 0.452 0.468l357.43 352.378c37.685 37.153 99.034 37.476 136.331 0.718 37.297-36.758 36.979-97.231-0.707-134.384z"
  128. fill="currentColor"
  129. />
  130. </svg>
  131. </button>
  132. <h2 v-if="lang === 'zh'">
  133. <select v-model="currentYear" style="width: 130px;">
  134. <option v-for="(y, index) in 201" :key="index" :value="1900 + index">
  135. {{ 1900 + index }}
  136. </option>
  137. </select>
  138. <select v-model="currentMonth">
  139. <option v-for="(month, index) in 12" :key="index" :value="index">
  140. {{ month < 10 ? `0${month}` : month }}
  141. </option>
  142. </select>
  143. </h2>
  144. <h2 v-else>
  145. <select v-model="currentMonth" style="width: 160px;">
  146. <option v-for="(month, index) in 12" :key="index" :value="index">
  147. {{ monthNames[month - 1] }}
  148. </option>
  149. </select>
  150. <select v-model="currentYear">
  151. <option v-for="(y, index) in 201" :key="index" :value="1900 + index">
  152. {{ 1900 + index }}
  153. </option>
  154. </select>
  155. </h2>
  156. <button @click="nextMonth">
  157. <svg
  158. class="r"
  159. viewBox="0 0 1024 1024"
  160. version="1.1"
  161. xmlns="http://www.w3.org/2000/svg"
  162. width="200"
  163. height="200"
  164. >
  165. <path
  166. d="M684.29 799.276L393.929 513.019 684.29 226.762c37.685-37.153 38.003-97.625 0.707-134.384-37.297-36.758-98.646-36.435-136.331 0.718l-357.43 352.378c-0.155 0.153-0.297 0.314-0.451 0.468-0.084 0.082-0.172 0.157-0.256 0.239-18.357 18.092-27.581 41.929-27.743 65.902-0.004 0.311-0.017 0.623-0.018 0.934 0.001 0.316 0.014 0.632 0.018 0.948 0.165 23.97 9.389 47.803 27.743 65.892 0.083 0.082 0.171 0.157 0.255 0.239 0.154 0.154 0.296 0.315 0.452 0.468l357.43 352.378c37.685 37.153 99.034 37.476 136.331 0.718 37.297-36.758 36.979-97.231-0.707-134.384z"
  167. fill="currentColor"
  168. />
  169. </svg>
  170. </button>
  171. </header>
  172. <div class="calendar-grid">
  173. <div v-for="(day, i) in 7" :key="day" class="calendar-day">
  174. {{ daysOfWeek[daysInfo[i].date.getDay()] }}
  175. </div>
  176. <div
  177. v-for="(day, index) in daysInfo"
  178. :key="index"
  179. class="calendar-cell"
  180. :class="{
  181. today: day.isToday,
  182. disable: day.disable,
  183. holiday: day.holidayName,
  184. inlieu: day.isInLieu,
  185. work: day.holidayName && day.work,
  186. solar: day.solarTerm?.index === 1,
  187. selected: isSelected(day.date),
  188. }"
  189. @click="selectDate(day)"
  190. >
  191. <span v-if="day.isToday" class="today-dot">{{ lang === 'en' ? 'Today' : '今' }}</span>
  192. <span v-if="day.holidayName" class="holiday-dot">{{
  193. day.work ? '班' : day.isInLieu ? '调' : '休'
  194. }}</span>
  195. <span class="day">{{ day.date.getDate() }}</span>
  196. <span class="desc">{{
  197. day.solarTerm?.index === 1 ? day.solarTerm?.name : day.holidayName || day.lunarDayCN
  198. }}</span>
  199. </div>
  200. </div>
  201. </div>
  202. <div class="calendar-day-info">
  203. <div class="left">
  204. <p>
  205. {{ selectedDate.lunarYearCN }}
  206. {{ selectedDate.lunarMonCN }}{{ selectedDate.lunarDayCN }}
  207. </p>
  208. <p>
  209. {{ selectedDate.yearCyl }}{{ selectedDate.zodiac }}年 {{ selectedDate.monCyl }}月
  210. {{ selectedDate.dayCyl }}日
  211. </p>
  212. </div>
  213. <div class="right">
  214. <p>
  215. {{ selectedDate.isToday ? '今天是' : '此日是' }}
  216. <span>{{ selectedDate.solarTerm?.name }}</span> 节气的第
  217. <span>{{ selectedDate.solarTerm?.index }}</span> 天。
  218. </p>
  219. <p>
  220. {{
  221. selectedDate.work
  222. ? '又是需要工作的一天!😥'
  223. : selectedDate.isInLieu
  224. ? '虽然调休,但要补班还回来的!🤬'
  225. : '休息啦~😃'
  226. }}
  227. </p>
  228. </div>
  229. </div>
  230. </template>
  231. <style lang="postcss">
  232. body {
  233. --calendar-max-width: 660px;
  234. --calendar-padding: 30px;
  235. --calendar-border-width: 1px;
  236. --calendar-grid-gap: 18px 12px;
  237. --calendar-border-radius: 10px;
  238. }
  239. @media screen and (max-width: 560px) {
  240. body {
  241. --calendar-padding: 10px;
  242. --calendar-border-width: 0;
  243. --calendar-grid-gap: 12px 6px;
  244. --calendar-border-radius: 0;
  245. }
  246. }
  247. </style>
  248. <style lang="postcss" scoped>
  249. .calendar {
  250. max-width: var(--calendar-max-width);
  251. margin: 0 auto;
  252. padding: var(--calendar-padding);
  253. border: var(--calendar-border-width) solid var(--vp-c-gray-2);
  254. border-radius: var(--calendar-border-radius);
  255. position: relative;
  256. background: var(--vp-c-bg);
  257. z-index: 1;
  258. h2, p {
  259. margin: 0;
  260. padding: 0;
  261. border: 0;
  262. }
  263. select {
  264. font-size: 24px;
  265. width: 100px;
  266. margin: 0 15px;
  267. font-weight: bold;
  268. text-align: center;
  269. border: 1px solid var(--vp-c-default-3);
  270. border-radius: 6px;
  271. }
  272. .calendar-header {
  273. display: flex;
  274. justify-content: space-between;
  275. align-items: center;
  276. margin-bottom: calc(20px + var(--calendar-padding));
  277. button {
  278. display: flex;
  279. flex-flow: column nowrap;
  280. align-items: center;
  281. justify-content: center;
  282. cursor: pointer;
  283. width: 40px;
  284. height: 40px;
  285. opacity: 0.5;
  286. transition: all 0.2s ease;
  287. &:hover {
  288. opacity: 0.8;
  289. }
  290. svg {
  291. width: 22px;
  292. height: 22px;
  293. &.r {
  294. transform: rotate(180deg);
  295. }
  296. }
  297. }
  298. h2 {
  299. font-size: 24px;
  300. font-weight: bold;
  301. }
  302. }
  303. @media screen and (max-width: 560px) {
  304. .calendar-header {
  305. button {
  306. width: 30px;
  307. height: 30px;
  308. svg {
  309. width: 18px;
  310. height: 18px;
  311. }
  312. }
  313. h2 {
  314. font-size: 18px;
  315. }
  316. }
  317. }
  318. .calendar-grid {
  319. display: grid;
  320. grid-template-columns: repeat(7, 1fr);
  321. gap: var(--calendar-grid-gap);
  322. .calendar-day {
  323. font-weight: bold;
  324. text-align: center;
  325. line-height: 3;
  326. }
  327. .calendar-cell {
  328. min-height: 70px;
  329. display: flex;
  330. flex-flow: column nowrap;
  331. align-items: center;
  332. justify-content: center;
  333. cursor: pointer;
  334. border-radius: var(--calendar-border-radius);
  335. position: relative;
  336. transition: all 0.2s ease;
  337. color: var(--vp-c-text-1);
  338. &:nth-child(7n + 6),
  339. &:nth-child(7n + 7) {
  340. .day {
  341. color: #eb3333;
  342. }
  343. }
  344. .day {
  345. font-size: 24px;
  346. font-weight: bold;
  347. }
  348. .desc {
  349. font-size: 12px;
  350. }
  351. .today-dot,
  352. .holiday-dot {
  353. position: absolute;
  354. right: -6px;
  355. top: -6px;
  356. font-size: 12px;
  357. padding: 0 4px;
  358. border-radius: 4px;
  359. min-width: 20px;
  360. line-height: 20px;
  361. transform: scale(0.9);
  362. }
  363. &.work {
  364. background: transparent;
  365. }
  366. &.holiday {
  367. &:not(&.work) {
  368. background: rgba(235, 51, 51, 0.05);
  369. color: #eb3333;
  370. }
  371. /** background: #f28c28; */
  372. .holiday-dot {
  373. background: #eb3333;
  374. color: #fff;
  375. }
  376. &.work {
  377. .day {
  378. color: #4e5877;
  379. }
  380. .holiday-dot {
  381. background: #4e5877;
  382. color: #fff;
  383. }
  384. }
  385. }
  386. &:hover {
  387. background: rgba(118, 142, 240, 0.2);
  388. color: var(--vp-c-text-1);
  389. }
  390. &.solar {
  391. .desc {
  392. color: #f28c28;
  393. border: 1px solid #f28c28;
  394. border-radius: 4px;
  395. padding: 0 4px;
  396. }
  397. }
  398. &.today {
  399. color: #4e6ef2;
  400. .today-dot {
  401. background: #6b88ff;
  402. color: #fff;
  403. }
  404. &.selected {
  405. background: #4e6ef2;
  406. color: #fff;
  407. }
  408. }
  409. &.selected {
  410. background: #4e6ef2 !important;
  411. color: #fff !important;
  412. .day {
  413. color: #fff !important;
  414. }
  415. &.solar {
  416. .desc {
  417. color: #fff;
  418. border: 1px solid #fff;
  419. }
  420. }
  421. }
  422. &.disable {
  423. opacity: 0.2;
  424. pointer-events: none;
  425. }
  426. }
  427. }
  428. }
  429. .calendar-day-info {
  430. max-width: var(--calendar-max-width);
  431. margin: 0 auto;
  432. padding: 50px 20px 30px;
  433. background: var(--vp-c-gray-3);
  434. border: var(--calendar-border-width) solid var(--vp-c-gray-3);
  435. border-radius: var(--calendar-border-radius);
  436. position: relative;
  437. top: -20px;
  438. z-index: 0;
  439. font-size: 16px;
  440. display: flex;
  441. align-items: center;
  442. .left {
  443. display: flex;
  444. flex-flow: column nowrap;
  445. align-items: flex-start;
  446. margin-right: var(--calendar-padding);
  447. p {
  448. font-weight: bold;
  449. font-size: 14px;
  450. margin: 0;
  451. &:first-child {
  452. font-size: 22px;
  453. }
  454. }
  455. }
  456. .right {
  457. display: flex;
  458. flex-flow: column nowrap;
  459. align-items: flex-start;
  460. padding-left: var(--calendar-padding);
  461. border-left: 1px solid var(--vp-c-gray-2);
  462. p {
  463. font-size: 14px;
  464. margin: 0;
  465. span {
  466. font-weight: bold;
  467. }
  468. &:first-child {
  469. font-size: 18px;
  470. }
  471. }
  472. }
  473. @media screen and (max-width: 560px) {
  474. .left,
  475. .right {
  476. p {
  477. font-size: 12px;
  478. &:first-child {
  479. font-size: 14px;
  480. }
  481. }
  482. }
  483. }
  484. }
  485. </style>