cl-calendar.uvue 20 KB

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