picker.uvue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <template>
  2. <view
  3. class="cl-calendar-picker"
  4. :class="{
  5. 'is-dark': isDark
  6. }"
  7. v-if="visible"
  8. >
  9. <view class="cl-calendar-picker__header">
  10. <view
  11. class="cl-calendar-picker__prev"
  12. :class="{
  13. 'is-dark': isDark
  14. }"
  15. @tap="prev"
  16. >
  17. <cl-icon name="arrow-left-double-line"></cl-icon>
  18. </view>
  19. <view class="cl-calendar-picker__date" @tap="toMode('year')">
  20. <cl-text :size="18">
  21. {{ title }}
  22. </cl-text>
  23. </view>
  24. <view
  25. class="cl-calendar-picker__next"
  26. :class="{
  27. 'is-dark': isDark
  28. }"
  29. @tap="next"
  30. >
  31. <cl-icon name="arrow-right-double-line"></cl-icon>
  32. </view>
  33. </view>
  34. <view class="cl-calendar-picker__list">
  35. <view
  36. class="cl-calendar-picker__item"
  37. v-for="item in list"
  38. :key="item.value"
  39. @tap="select(item.value)"
  40. >
  41. <cl-text
  42. :pt="{
  43. className: parseClass([[item.value == value, 'text-primary-500']])
  44. }"
  45. >{{ item.label }}</cl-text
  46. >
  47. </view>
  48. </view>
  49. </view>
  50. </template>
  51. <script lang="ts" setup>
  52. import { first, isDark, last, parseClass, t } from "@/.cool";
  53. import { computed, ref } from "vue";
  54. defineOptions({
  55. name: "cl-calendar-picker"
  56. });
  57. // 定义日历选择器的条目类型
  58. type Item = {
  59. label: string; // 显示的标签,如"1月"、"2024"
  60. value: number; // 对应的数值,如1、2024
  61. };
  62. // 定义组件接收的属性:年份和月份,均为数字类型,默认值为0
  63. const props = defineProps({
  64. year: {
  65. type: Number,
  66. default: 0
  67. },
  68. month: {
  69. type: Number,
  70. default: 0
  71. }
  72. });
  73. // 定义组件可触发的事件,这里只定义了"change"事件
  74. const emit = defineEmits(["change"]);
  75. // 当前选择的模式,"year"表示选择年份,"month"表示选择月份,默认是"month"
  76. const mode = ref<"year" | "month">("month");
  77. // 当前选中的年份
  78. const year = ref(0);
  79. // 当前选中的月份
  80. const month = ref(0);
  81. // 当前年份选择面板的起始年份(如2020-2029,则startYear为2020)
  82. const startYear = ref(0);
  83. // 当前选中的值,若为月份模式则为月份,否则为年份
  84. const value = computed(() => {
  85. return mode.value == "month" ? month.value : year.value;
  86. });
  87. // 计算可供选择的列表:
  88. // - 若为月份模式,返回1-12月
  89. // - 若为年份模式,返回以startYear为起点的连续10年
  90. const list = computed(() => {
  91. if (mode.value == "month") {
  92. return [
  93. { label: t("1月"), value: 1 },
  94. { label: t("2月"), value: 2 },
  95. { label: t("3月"), value: 3 },
  96. { label: t("4月"), value: 4 },
  97. { label: t("5月"), value: 5 },
  98. { label: t("6月"), value: 6 },
  99. { label: t("7月"), value: 7 },
  100. { label: t("8月"), value: 8 },
  101. { label: t("9月"), value: 9 },
  102. { label: t("10月"), value: 10 },
  103. { label: t("11月"), value: 11 },
  104. { label: t("12月"), value: 12 }
  105. ] as Item[];
  106. } else {
  107. const years: Item[] = [];
  108. // 生成10个连续年份
  109. for (let i = 0; i < 10; i++) {
  110. years.push({
  111. label: `${startYear.value + i}`,
  112. value: startYear.value + i
  113. });
  114. }
  115. return years;
  116. }
  117. });
  118. // 计算标题内容:
  119. // - 月份模式下显示“xxxx年”
  120. // - 年份模式下显示“起始年 - 结束年”
  121. const title = computed(() => {
  122. return mode.value == "month"
  123. ? `${year.value}`
  124. : `${first(list.value)?.label} - ${last(list.value)?.label}`;
  125. });
  126. // 控制选择器弹窗的显示与隐藏
  127. const visible = ref(false);
  128. /**
  129. * 打开选择器,并初始化年份、月份、起始年份
  130. */
  131. function open() {
  132. visible.value = true;
  133. // 初始化当前年份和月份为传入的props
  134. year.value = props.year;
  135. month.value = props.month;
  136. // 计算当前年份所在的十年区间的起始年份
  137. startYear.value = Math.floor(year.value / 10) * 10;
  138. }
  139. /**
  140. * 关闭选择器
  141. */
  142. function close() {
  143. visible.value = false;
  144. }
  145. /**
  146. * 切换选择模式(年份/月份)
  147. * @param val "year" 或 "month"
  148. */
  149. function toMode(val: "year" | "month") {
  150. mode.value = val;
  151. }
  152. /**
  153. * 选择某个值(年份或月份)
  154. * @param val 选中的值
  155. */
  156. function select(val: number) {
  157. if (mode.value == "month") {
  158. // 选择月份后,关闭弹窗并触发change事件
  159. month.value = val;
  160. close();
  161. emit("change", [year.value, month.value]);
  162. } else {
  163. // 选择年份后,切换到月份选择模式
  164. year.value = val;
  165. toMode("month");
  166. }
  167. }
  168. /**
  169. * 切换到上一个区间
  170. * - 月份模式下,年份减1
  171. * - 年份模式下,起始年份减10
  172. */
  173. function prev() {
  174. if (mode.value == "month") {
  175. year.value -= 1;
  176. } else {
  177. startYear.value -= 10;
  178. }
  179. }
  180. /**
  181. * 切换到下一个区间
  182. * - 月份模式下,年份加1
  183. * - 年份模式下,起始年份加10
  184. */
  185. function next() {
  186. if (mode.value == "month") {
  187. year.value += 1;
  188. } else {
  189. startYear.value += 10;
  190. }
  191. }
  192. defineExpose({
  193. open,
  194. close
  195. });
  196. </script>
  197. <style lang="scss" scoped>
  198. .cl-calendar-picker {
  199. @apply flex flex-col absolute left-0 top-0 w-full h-full bg-white z-10;
  200. &__header {
  201. @apply flex flex-row items-center justify-between w-full p-3;
  202. }
  203. &__prev,
  204. &__next {
  205. @apply flex flex-row items-center justify-center rounded-full bg-surface-100;
  206. width: 30px;
  207. height: 30px;
  208. &.is-dark {
  209. @apply bg-surface-700;
  210. }
  211. }
  212. &__date {
  213. @apply flex flex-row items-center justify-center;
  214. }
  215. &__list {
  216. @apply flex flex-row flex-wrap;
  217. }
  218. &__item {
  219. @apply flex flex-row items-center justify-center;
  220. height: 50px;
  221. width: 25%;
  222. &-bg {
  223. @apply px-4 py-2;
  224. &.is-active {
  225. @apply bg-primary-500 rounded-xl;
  226. }
  227. }
  228. }
  229. &.is-dark {
  230. @apply bg-surface-800 rounded-2xl;
  231. }
  232. }
  233. </style>