cl-calendar.uvue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. <template>
  2. <view class="cl-calendar" :class="[pt.className]">
  3. <!-- 年月选择器弹窗 -->
  4. <calendar-picker
  5. :year="currentYear"
  6. :month="currentMonth"
  7. :ref="refs.set('picker')"
  8. @change="onYearMonthChange"
  9. ></calendar-picker>
  10. <!-- 头部导航栏 -->
  11. <view class="cl-calendar__header" v-if="showHeader">
  12. <!-- 上一月按钮 -->
  13. <view
  14. class="cl-calendar__header-prev"
  15. :class="{ 'is-dark': isDark }"
  16. @tap.stop="gotoPrevMonth"
  17. >
  18. <cl-icon name="arrow-left-s-line"></cl-icon>
  19. </view>
  20. <!-- 当前年月显示区域 -->
  21. <view class="cl-calendar__header-date" @tap="refs.open('picker')">
  22. <slot name="current-date">
  23. <cl-text :pt="{ className: 'text-lg' }">{{
  24. $t(`{year}年{month}月`, { year: currentYear, month: currentMonth })
  25. }}</cl-text>
  26. </slot>
  27. </view>
  28. <!-- 下一月按钮 -->
  29. <view
  30. class="cl-calendar__header-next"
  31. :class="{ 'is-dark': isDark }"
  32. @tap.stop="gotoNextMonth"
  33. >
  34. <cl-icon name="arrow-right-s-line"></cl-icon>
  35. </view>
  36. </view>
  37. <!-- 星期标题行 -->
  38. <view class="cl-calendar__weeks" :style="{ gap: `${cellGap}px` }" v-if="showWeeks">
  39. <view class="cl-calendar__weeks-item" v-for="weekName in weekLabels" :key="weekName">
  40. <cl-text>{{ weekName }}</cl-text>
  41. </view>
  42. </view>
  43. <!-- 日期网格容器 -->
  44. <view
  45. class="cl-calendar__view"
  46. ref="viewRef"
  47. :style="{ height: `${viewHeight}px`, gap: `${cellGap}px` }"
  48. @tap="onTap"
  49. >
  50. <!-- #ifndef APP -->
  51. <view
  52. class="cl-calendar__view-row"
  53. :style="{ gap: `${cellGap}px` }"
  54. v-for="(weekRow, rowIndex) in dateMatrix"
  55. :key="rowIndex"
  56. >
  57. <view
  58. class="cl-calendar__view-cell"
  59. v-for="(dateCell, cellIndex) in weekRow"
  60. :key="cellIndex"
  61. :class="{
  62. 'is-selected': dateCell.isSelected,
  63. 'is-range': dateCell.isRange,
  64. 'is-hide': dateCell.isHide,
  65. 'is-disabled': dateCell.isDisabled,
  66. 'is-today': dateCell.isToday,
  67. 'is-other-month': !dateCell.isCurrentMonth
  68. }"
  69. :style="{
  70. height: cellHeight + 'px',
  71. backgroundColor: getCellBgColor(dateCell)
  72. }"
  73. @click.stop="selectDateCell(dateCell)"
  74. >
  75. <!-- 顶部文本 -->
  76. <cl-text
  77. :size="20"
  78. :color="getCellTextColor(dateCell)"
  79. :pt="{
  80. className: 'absolute top-[2px]'
  81. }"
  82. >{{ dateCell.topText }}</cl-text
  83. >
  84. <!-- 主日期数字 -->
  85. <cl-text
  86. :color="getCellTextColor(dateCell)"
  87. :size="`${fontSize}px`"
  88. :pt="{
  89. className: 'font-bold'
  90. }"
  91. >{{ dateCell.date }}</cl-text
  92. >
  93. <!-- 底部文本 -->
  94. <cl-text
  95. :size="20"
  96. :color="getCellTextColor(dateCell)"
  97. :pt="{
  98. className: 'absolute bottom-[2px]'
  99. }"
  100. >{{ dateCell.bottomText }}</cl-text
  101. >
  102. </view>
  103. </view>
  104. <!-- #endif -->
  105. </view>
  106. </view>
  107. </template>
  108. <script lang="ts" setup>
  109. import { computed, nextTick, onMounted, ref, watch, type PropType } from "vue";
  110. import { ctx, dayUts, first, isDark, isHarmony, parsePt, useRefs } from "@/cool";
  111. import CalendarPicker from "./picker.uvue";
  112. import { $t, t } from "@/locale";
  113. import type { ClCalendarDateConfig, ClCalendarMode } from "../../types";
  114. import { useSize } from "../../hooks";
  115. defineOptions({
  116. name: "cl-calendar"
  117. });
  118. // 组件属性定义
  119. const props = defineProps({
  120. // 透传样式配置
  121. pt: {
  122. type: Object,
  123. default: () => ({})
  124. },
  125. // 当前选中的日期值(单选模式)
  126. modelValue: {
  127. type: String as PropType<string | null>,
  128. default: null
  129. },
  130. // 选中的日期数组(多选/范围模式)
  131. date: {
  132. type: Array as PropType<string[]>,
  133. default: () => []
  134. },
  135. // 日期选择模式:单选/多选/范围选择
  136. mode: {
  137. type: String as PropType<ClCalendarMode>,
  138. default: "single"
  139. },
  140. // 日期配置
  141. dateConfig: {
  142. type: Array as PropType<ClCalendarDateConfig[]>,
  143. default: () => []
  144. },
  145. // 开始日期,可选日期的开始
  146. start: {
  147. type: String
  148. },
  149. // 结束日期,可选日期的结束
  150. end: {
  151. type: String
  152. },
  153. // 设置年份
  154. year: {
  155. type: Number
  156. },
  157. // 设置月份
  158. month: {
  159. type: Number
  160. },
  161. // 是否显示其他月份的日期
  162. showOtherMonth: {
  163. type: Boolean,
  164. default: true
  165. },
  166. // 是否显示头部导航栏
  167. showHeader: {
  168. type: Boolean,
  169. default: true
  170. },
  171. // 是否显示星期
  172. showWeeks: {
  173. type: Boolean,
  174. default: true
  175. },
  176. // 单元格高度
  177. cellHeight: {
  178. type: Number,
  179. default: 66
  180. },
  181. // 单元格间距
  182. cellGap: {
  183. type: Number,
  184. default: 0
  185. },
  186. // 主色
  187. color: {
  188. type: String,
  189. default: ""
  190. },
  191. // 当前月份日期颜色
  192. textColor: {
  193. type: String,
  194. default: ""
  195. },
  196. // 其他月份日期颜色
  197. textOtherMonthColor: {
  198. type: String,
  199. default: ""
  200. },
  201. // 禁用日期颜色
  202. textDisabledColor: {
  203. type: String,
  204. default: ""
  205. },
  206. // 今天日期颜色
  207. textTodayColor: {
  208. type: String,
  209. default: "#ff6b6b"
  210. },
  211. // 选中日期颜色
  212. textSelectedColor: {
  213. type: String,
  214. default: "#ffffff"
  215. },
  216. // 选中日期背景颜色
  217. bgSelectedColor: {
  218. type: String,
  219. default: ""
  220. },
  221. // 范围选择背景颜色
  222. bgRangeColor: {
  223. type: String,
  224. default: ""
  225. }
  226. });
  227. // 事件发射器定义
  228. const emit = defineEmits(["update:modelValue", "update:date", "change"]);
  229. // 日期单元格数据结构
  230. type DateCell = {
  231. date: string; // 显示的日期数字
  232. isCurrentMonth: boolean; // 是否属于当前显示月份
  233. isToday: boolean; // 是否为今天
  234. isSelected: boolean; // 是否被选中
  235. isRange: boolean; // 是否在选择范围内
  236. fullDate: string; // 完整日期格式 YYYY-MM-DD
  237. isDisabled: boolean; // 是否被禁用
  238. isHide: boolean; // 是否隐藏显示
  239. topText: string; // 顶部文案
  240. bottomText: string; // 底部文案
  241. color: string; // 颜色
  242. };
  243. // 透传样式属性类型
  244. type PassThrough = {
  245. className?: string;
  246. };
  247. // 解析透传样式配置
  248. const pt = computed(() => parsePt<PassThrough>(props.pt));
  249. // 字体大小
  250. const { getPxValue } = useSize();
  251. // 主色
  252. const color = computed(() => {
  253. if (props.color != "") {
  254. return props.color;
  255. }
  256. return ctx.color["primary-500"] as string;
  257. });
  258. // 单元格高度
  259. const cellHeight = computed(() => props.cellHeight);
  260. // 单元格间距
  261. const cellGap = computed(() => props.cellGap);
  262. // 字体大小
  263. const fontSize = computed(() => {
  264. // #ifdef APP
  265. return getPxValue("14px");
  266. // #endif
  267. // #ifndef APP
  268. return 14;
  269. // #endif
  270. });
  271. // 当前月份日期颜色
  272. const textColor = computed(() => {
  273. if (props.textColor != "") {
  274. return props.textColor;
  275. }
  276. return isDark.value ? "white" : (ctx.color["surface-700"] as string);
  277. });
  278. // 其他月份日期颜色
  279. const textOtherMonthColor = computed(() => {
  280. if (props.textOtherMonthColor != "") {
  281. return props.textOtherMonthColor;
  282. }
  283. return isDark.value
  284. ? (ctx.color["surface-500"] as string)
  285. : (ctx.color["surface-300"] as string);
  286. });
  287. // 禁用日期颜色
  288. const textDisabledColor = computed(() => {
  289. if (props.textDisabledColor != "") {
  290. return props.textDisabledColor;
  291. }
  292. return isDark.value
  293. ? (ctx.color["surface-500"] as string)
  294. : (ctx.color["surface-300"] as string);
  295. });
  296. // 今天日期颜色
  297. const textTodayColor = computed(() => props.textTodayColor);
  298. // 选中日期颜色
  299. const textSelectedColor = computed(() => props.textSelectedColor);
  300. // 选中日期背景颜色
  301. const bgSelectedColor = computed(() => {
  302. if (props.bgSelectedColor != "") {
  303. return props.bgSelectedColor;
  304. }
  305. return color.value;
  306. });
  307. // 范围选择背景颜色
  308. const bgRangeColor = computed(() => {
  309. if (props.bgRangeColor != "") {
  310. return props.bgRangeColor;
  311. }
  312. return isHarmony() ? (ctx.color["primary-50"] as string) : color.value + "11";
  313. });
  314. // 组件引用管理器
  315. const refs = useRefs();
  316. // 日历视图DOM元素引用
  317. const viewRef = ref<UniElement | null>(null);
  318. // 当前显示的年份
  319. const currentYear = ref(0);
  320. // 当前显示的月份
  321. const currentMonth = ref(0);
  322. // 视图高度
  323. const viewHeight = computed(() => {
  324. return cellHeight.value * 6;
  325. });
  326. // 单元格宽度
  327. const cellWidth = ref(0);
  328. // 星期标签数组
  329. const weekLabels = computed(() => {
  330. return [t("周日"), t("周一"), t("周二"), t("周三"), t("周四"), t("周五"), t("周六")];
  331. });
  332. // 日历日期矩阵数据(6行7列)
  333. const dateMatrix = ref<DateCell[][]>([]);
  334. // 当前选中的日期列表
  335. const selectedDates = ref<string[]>([]);
  336. /**
  337. * 获取日历视图元素的位置信息
  338. */
  339. async function getViewRect(): Promise<DOMRect | null> {
  340. return viewRef.value!.getBoundingClientRectAsync();
  341. }
  342. /**
  343. * 判断指定日期是否被选中
  344. * @param dateStr 日期字符串 YYYY-MM-DD
  345. */
  346. function isDateSelected(dateStr: string): boolean {
  347. if (props.mode == "single") {
  348. // 单选模式:检查是否为唯一选中日期
  349. return selectedDates.value[0] == dateStr;
  350. } else {
  351. // 多选/范围模式:检查是否在选中列表中
  352. return selectedDates.value.includes(dateStr);
  353. }
  354. }
  355. /**
  356. * 判断指定日期是否被禁用
  357. * @param dateStr 日期字符串 YYYY-MM-DD
  358. */
  359. function isDateDisabled(dateStr: string): boolean {
  360. // 大于开始日期
  361. if (props.start != null) {
  362. if (dayUts(dateStr).isBefore(dayUts(props.start))) {
  363. return true;
  364. }
  365. }
  366. // 小于结束日期
  367. if (props.end != null) {
  368. if (dayUts(dateStr).isAfter(dayUts(props.end))) {
  369. return true;
  370. }
  371. }
  372. return props.dateConfig.some((config) => config.date == dateStr && config.disabled == true);
  373. }
  374. /**
  375. * 判断指定日期是否在选择范围内(不包括端点)
  376. * @param dateStr 日期字符串 YYYY-MM-DD
  377. */
  378. function isDateInRange(dateStr: string): boolean {
  379. // 仅范围选择模式且已选择两个端点时才有范围
  380. if (props.mode != "range" || selectedDates.value.length != 2) {
  381. return false;
  382. }
  383. const [startDate, endDate] = selectedDates.value;
  384. const currentDate = dayUts(dateStr);
  385. return currentDate.isAfter(startDate) && currentDate.isBefore(endDate);
  386. }
  387. /**
  388. * 获取单元格字体颜色
  389. * @param dateCell 日期单元格数据
  390. * @returns 字体颜色
  391. */
  392. function getCellTextColor(dateCell: DateCell): string {
  393. // 选中的日期文字颜色
  394. if (dateCell.isSelected) {
  395. return textSelectedColor.value;
  396. }
  397. if (dateCell.color != "") {
  398. return dateCell.color;
  399. }
  400. // 范围选择日期颜色
  401. if (dateCell.isRange) {
  402. return color.value;
  403. }
  404. // 禁用的日期颜色
  405. if (dateCell.isDisabled) {
  406. return textDisabledColor.value;
  407. }
  408. // 今天日期颜色
  409. if (dateCell.isToday) {
  410. return textTodayColor.value;
  411. }
  412. // 当前月份日期颜色
  413. if (dateCell.isCurrentMonth) {
  414. return textColor.value;
  415. }
  416. // 其他月份日期颜色
  417. return textOtherMonthColor.value;
  418. }
  419. /**
  420. * 获取单元格背景颜色
  421. * @param dateCell 日期单元格数据
  422. * @returns 背景颜色
  423. */
  424. function getCellBgColor(dateCell: DateCell): string {
  425. console.log(bgSelectedColor.value);
  426. if (dateCell.isSelected) {
  427. return bgSelectedColor.value;
  428. }
  429. if (dateCell.isRange) {
  430. return bgRangeColor.value;
  431. }
  432. return "transparent";
  433. }
  434. /**
  435. * 计算并生成日历矩阵数据
  436. * 生成6行7列共42个日期,包含上月末尾和下月开头的日期
  437. */
  438. function calculateDateMatrix() {
  439. const weekRows: DateCell[][] = [];
  440. const todayStr = dayUts().format("YYYY-MM-DD"); // 今天的日期字符串
  441. // 获取当前月第一天
  442. const monthFirstDay = dayUts(`${currentYear.value}-${currentMonth.value}-01`);
  443. const firstDayWeekIndex = monthFirstDay.getDay(); // 第一天是星期几 (0=周日, 6=周六)
  444. // 计算日历显示的起始日期(可能是上个月的日期)
  445. const calendarStartDate = monthFirstDay.subtract(firstDayWeekIndex, "day");
  446. // 生成6周的日期数据(6行 × 7列 = 42天)
  447. let iterateDate = calendarStartDate;
  448. for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
  449. const weekDates: DateCell[] = [];
  450. for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
  451. const fullDateStr = iterateDate.format("YYYY-MM-DD");
  452. const nativeDate = iterateDate.toDate();
  453. const dayNumber = nativeDate.getDate();
  454. // 判断是否属于当前显示月份
  455. const belongsToCurrentMonth =
  456. nativeDate.getMonth() + 1 == currentMonth.value &&
  457. nativeDate.getFullYear() == currentYear.value;
  458. // 日期配置
  459. const dateConfig = props.dateConfig.find((config) => config.date == fullDateStr);
  460. // 构建日期单元格数据
  461. const dateCell = {
  462. date: `${dayNumber}`,
  463. isCurrentMonth: belongsToCurrentMonth,
  464. isToday: fullDateStr == todayStr,
  465. isSelected: isDateSelected(fullDateStr),
  466. isRange: isDateInRange(fullDateStr),
  467. fullDate: fullDateStr,
  468. isDisabled: isDateDisabled(fullDateStr),
  469. isHide: false,
  470. topText: dateConfig?.topText ?? "",
  471. bottomText: dateConfig?.bottomText ?? "",
  472. color: dateConfig?.color ?? ""
  473. } as DateCell;
  474. // 根据配置决定是否隐藏相邻月份的日期
  475. if (!props.showOtherMonth && !belongsToCurrentMonth) {
  476. dateCell.isHide = true;
  477. }
  478. weekDates.push(dateCell);
  479. iterateDate = iterateDate.add(1, "day"); // 移动到下一天
  480. }
  481. weekRows.push(weekDates);
  482. }
  483. dateMatrix.value = weekRows;
  484. }
  485. /**
  486. * 使用Canvas绘制日历(仅APP端)
  487. * Web端使用DOM渲染,APP端使用Canvas提升性能
  488. */
  489. async function renderCalendar() {
  490. // #ifdef APP
  491. await nextTick(); // 等待DOM更新完成
  492. const ctx = viewRef.value!.getDrawableContext();
  493. if (ctx == null) return;
  494. ctx!.reset(); // 清空画布
  495. /**
  496. * 绘制单个日期单元格
  497. * @param dateCell 日期单元格数据
  498. * @param colIndex 列索引 (0-6)
  499. * @param rowIndex 行索引 (0-5)
  500. */
  501. function drawSingleCell(dateCell: DateCell, colIndex: number, rowIndex: number) {
  502. // 计算单元格位置
  503. const cellX = colIndex * cellWidth.value;
  504. const cellY = rowIndex * cellHeight.value;
  505. const centerX = cellX + cellWidth.value / 2;
  506. const centerY = cellY + cellHeight.value / 2;
  507. // 绘制背景(选中状态或范围状态)
  508. if (dateCell.isSelected || dateCell.isRange) {
  509. const padding = cellGap.value; // 使用间距作为内边距
  510. const bgX = cellX + padding;
  511. const bgY = cellY + padding;
  512. const bgWidth = cellWidth.value - padding * 2;
  513. const bgHeight = cellHeight.value - padding * 2;
  514. // 设置背景颜色
  515. if (dateCell.isSelected) {
  516. ctx!.fillStyle = bgSelectedColor.value;
  517. }
  518. if (dateCell.isRange) {
  519. ctx!.fillStyle = bgRangeColor.value;
  520. }
  521. ctx!.fillRect(bgX, bgY, bgWidth, bgHeight); // 绘制背景矩形
  522. }
  523. // 获取单元格文字颜色
  524. const cellTextColor = getCellTextColor(dateCell);
  525. ctx!.textAlign = "center";
  526. // 绘制顶部文本
  527. if (dateCell.topText != "") {
  528. ctx!.font = `${Math.floor(fontSize.value * 0.75)}px sans-serif`;
  529. ctx!.fillStyle = cellTextColor;
  530. const topY = cellY + 16; // 距离顶部
  531. ctx!.fillText(dateCell.topText, centerX, topY);
  532. }
  533. // 绘制主日期数字
  534. ctx!.font = `${fontSize.value}px sans-serif`;
  535. ctx!.fillStyle = cellTextColor;
  536. const textOffsetY = (fontSize.value / 2) * 0.7;
  537. ctx!.fillText(dateCell.date.toString(), centerX, centerY + textOffsetY);
  538. // 绘制底部文本
  539. if (dateCell.bottomText != "") {
  540. ctx!.font = `${Math.floor(fontSize.value * 0.75)}px sans-serif`;
  541. ctx!.fillStyle = cellTextColor;
  542. const bottomY = cellY + cellHeight.value - 8; // 距离底部
  543. ctx!.fillText(dateCell.bottomText, centerX, bottomY);
  544. }
  545. }
  546. // 获取容器尺寸信息
  547. const viewRect = await getViewRect();
  548. if (viewRect == null) {
  549. return;
  550. }
  551. // 计算单元格宽度(总宽度除以7列)
  552. const cellSize = viewRect.width / 7;
  553. // 更新渲染配置
  554. cellWidth.value = cellSize;
  555. // 遍历日期矩阵进行绘制
  556. for (let rowIndex = 0; rowIndex < dateMatrix.value.length; rowIndex++) {
  557. const weekRow = dateMatrix.value[rowIndex];
  558. for (let colIndex = 0; colIndex < weekRow.length; colIndex++) {
  559. const dateCell = weekRow[colIndex];
  560. if (!dateCell.isHide) {
  561. drawSingleCell(dateCell, colIndex, rowIndex);
  562. }
  563. }
  564. }
  565. ctx!.update(); // 更新画布显示
  566. // #endif
  567. }
  568. /**
  569. * 处理日期单元格选择逻辑
  570. * @param dateCell 被点击的日期单元格
  571. */
  572. function selectDateCell(dateCell: DateCell) {
  573. // 隐藏或禁用的日期不可选择
  574. if (dateCell.isHide || dateCell.isDisabled) {
  575. return;
  576. }
  577. if (props.mode == "single") {
  578. // 单选模式:直接替换选中日期
  579. selectedDates.value = [dateCell.fullDate];
  580. emit("update:modelValue", dateCell.fullDate);
  581. } else if (props.mode == "multiple") {
  582. // 多选模式:切换选中状态
  583. const existingIndex = selectedDates.value.indexOf(dateCell.fullDate);
  584. if (existingIndex >= 0) {
  585. // 已选中则移除
  586. selectedDates.value.splice(existingIndex, 1);
  587. } else {
  588. // 未选中则添加
  589. selectedDates.value.push(dateCell.fullDate);
  590. }
  591. } else {
  592. // 范围选择模式
  593. if (selectedDates.value.length == 0) {
  594. // 第一次点击:设置起始日期
  595. selectedDates.value = [dateCell.fullDate];
  596. } else if (selectedDates.value.length == 1) {
  597. // 第二次点击:设置结束日期
  598. const startDate = dayUts(selectedDates.value[0]);
  599. const endDate = dayUts(dateCell.fullDate);
  600. if (endDate.isBefore(startDate)) {
  601. // 结束日期早于开始日期时自动交换
  602. selectedDates.value = [dateCell.fullDate, selectedDates.value[0]];
  603. } else {
  604. selectedDates.value = [selectedDates.value[0], dateCell.fullDate];
  605. }
  606. } else {
  607. // 已有范围时重新开始选择
  608. selectedDates.value = [dateCell.fullDate];
  609. }
  610. }
  611. // 发射更新事件
  612. emit("update:date", [...selectedDates.value]);
  613. emit("change", selectedDates.value);
  614. // 重新计算日历数据并重绘
  615. calculateDateMatrix();
  616. renderCalendar();
  617. }
  618. /**
  619. * 处理年月选择器的变化事件
  620. * @param yearMonthArray [年份, 月份] 数组
  621. */
  622. function onYearMonthChange(yearMonthArray: number[]) {
  623. currentYear.value = yearMonthArray[0];
  624. currentMonth.value = yearMonthArray[1];
  625. // 重新计算日历数据并重绘
  626. calculateDateMatrix();
  627. renderCalendar();
  628. }
  629. /**
  630. * 处理点击事件(APP端点击检测)
  631. */
  632. async function onTap(e: UniPointerEvent) {
  633. // 获取容器位置信息
  634. const viewRect = await getViewRect();
  635. if (viewRect == null) {
  636. return;
  637. }
  638. // 计算触摸点相对于容器的坐标
  639. const relativeX = e.clientX - viewRect.left;
  640. const relativeY = e.clientY - viewRect.top;
  641. // 根据坐标计算对应的行列索引
  642. const columnIndex = Math.floor(relativeX / cellWidth.value);
  643. const rowIndex = Math.floor(relativeY / cellHeight.value);
  644. // 边界检查:确保索引在有效范围内
  645. if (
  646. rowIndex < 0 ||
  647. rowIndex >= dateMatrix.value.length ||
  648. columnIndex < 0 ||
  649. columnIndex >= 7
  650. ) {
  651. return;
  652. }
  653. const targetDateCell = dateMatrix.value[rowIndex][columnIndex];
  654. selectDateCell(targetDateCell);
  655. }
  656. /**
  657. * 切换到上一个月
  658. */
  659. function gotoPrevMonth() {
  660. const [newYear, newMonth] = dayUts(`${currentYear.value}-${currentMonth.value}-01`)
  661. .subtract(1, "month")
  662. .toArray();
  663. currentYear.value = newYear;
  664. currentMonth.value = newMonth;
  665. // 重新计算并渲染日历
  666. calculateDateMatrix();
  667. renderCalendar();
  668. }
  669. /**
  670. * 切换到下一个月
  671. */
  672. function gotoNextMonth() {
  673. const [newYear, newMonth] = dayUts(`${currentYear.value}-${currentMonth.value}-01`)
  674. .add(1, "month")
  675. .toArray();
  676. currentYear.value = newYear;
  677. currentMonth.value = newMonth;
  678. // 重新计算并渲染日历
  679. calculateDateMatrix();
  680. renderCalendar();
  681. }
  682. /**
  683. * 解析选中日期
  684. */
  685. function parseDate(flag: boolean | null = null) {
  686. // 根据选择模式初始化选中日期
  687. if (props.mode == "single") {
  688. selectedDates.value = props.modelValue != null ? [props.modelValue] : [];
  689. } else {
  690. selectedDates.value = [...props.date];
  691. }
  692. // 获取初始显示日期(优先使用选中日期,否则使用当前日期)
  693. let [year, month] = dayUts(first(selectedDates.value)).toArray();
  694. if (flag == true) {
  695. year = props.year ?? year;
  696. month = props.month ?? month;
  697. }
  698. currentYear.value = year;
  699. currentMonth.value = month;
  700. // 计算初始日历数据
  701. calculateDateMatrix();
  702. // 渲染日历视图
  703. renderCalendar();
  704. }
  705. // 组件挂载时的初始化逻辑
  706. onMounted(() => {
  707. // 解析日期
  708. parseDate(true);
  709. // 监听单选模式的值变化
  710. watch(
  711. computed(() => props.modelValue ?? ""),
  712. (newValue: string) => {
  713. selectedDates.value = [newValue];
  714. parseDate();
  715. }
  716. );
  717. // 监听多选/范围模式的值变化
  718. watch(
  719. computed(() => props.date),
  720. (newDateArray: string[]) => {
  721. selectedDates.value = [...newDateArray];
  722. parseDate();
  723. }
  724. );
  725. // 重新渲染
  726. watch(
  727. computed(() => [props.dateConfig, props.showOtherMonth]),
  728. () => {
  729. calculateDateMatrix();
  730. renderCalendar();
  731. },
  732. {
  733. deep: true
  734. }
  735. );
  736. });
  737. </script>
  738. <style lang="scss" scoped>
  739. /* 日历组件主容器 */
  740. .cl-calendar {
  741. @apply relative;
  742. /* 头部导航栏样式 */
  743. &__header {
  744. @apply flex flex-row items-center justify-between p-3 w-full;
  745. /* 上一月/下一月按钮样式 */
  746. &-prev,
  747. &-next {
  748. @apply flex flex-row items-center justify-center rounded-full bg-surface-100;
  749. width: 60rpx;
  750. height: 60rpx;
  751. /* 暗色模式适配 */
  752. &.is-dark {
  753. @apply bg-surface-700;
  754. }
  755. }
  756. /* 当前年月显示区域 */
  757. &-date {
  758. @apply flex flex-row items-center justify-center;
  759. }
  760. }
  761. /* 星期标题行样式 */
  762. &__weeks {
  763. @apply flex flex-row;
  764. /* 单个星期标题样式 */
  765. &-item {
  766. @apply flex flex-row items-center justify-center flex-1;
  767. height: 80rpx;
  768. }
  769. }
  770. /* 日期网格容器样式 */
  771. &__view {
  772. @apply w-full;
  773. // #ifndef APP
  774. /* 日期行样式 */
  775. &-row {
  776. @apply flex flex-row;
  777. }
  778. /* 日期单元格样式 */
  779. &-cell {
  780. @apply flex-1 flex flex-col items-center justify-center relative;
  781. height: 80rpx;
  782. /* 隐藏状态(相邻月份日期) */
  783. &.is-hide {
  784. opacity: 0;
  785. }
  786. /* 禁用状态 */
  787. &.is-disabled {
  788. @apply opacity-50;
  789. }
  790. }
  791. // #endif
  792. }
  793. }
  794. </style>