|
@@ -0,0 +1,732 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <!-- 日历组件主容器 -->
|
|
|
|
|
+ <view class="cl-calendar" :class="[pt.className]">
|
|
|
|
|
+ <!-- 年月选择器弹窗 -->
|
|
|
|
|
+ <calendar-picker
|
|
|
|
|
+ :year="currentYear"
|
|
|
|
|
+ :month="currentMonth"
|
|
|
|
|
+ :ref="refs.set('picker')"
|
|
|
|
|
+ @change="onYearMonthChange"
|
|
|
|
|
+ ></calendar-picker>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 头部导航栏 -->
|
|
|
|
|
+ <view class="cl-calendar__header" v-if="showHeader">
|
|
|
|
|
+ <!-- 上一月按钮 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="cl-calendar__header-prev"
|
|
|
|
|
+ :class="{ 'is-dark': isDark }"
|
|
|
|
|
+ @tap.stop="gotoPrevMonth"
|
|
|
|
|
+ >
|
|
|
|
|
+ <cl-icon name="arrow-left-s-line"></cl-icon>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 当前年月显示区域 -->
|
|
|
|
|
+ <view class="cl-calendar__header-date" @tap="refs.open('picker')">
|
|
|
|
|
+ <slot name="current-date">
|
|
|
|
|
+ <cl-text :pt="{ className: 'text-lg' }">{{
|
|
|
|
|
+ $t(`{year}年{month}月`, { year: currentYear, month: currentMonth })
|
|
|
|
|
+ }}</cl-text>
|
|
|
|
|
+ </slot>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 下一月按钮 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="cl-calendar__header-next"
|
|
|
|
|
+ :class="{ 'is-dark': isDark }"
|
|
|
|
|
+ @tap.stop="gotoNextMonth"
|
|
|
|
|
+ >
|
|
|
|
|
+ <cl-icon name="arrow-right-s-line"></cl-icon>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 星期标题行 -->
|
|
|
|
|
+ <view class="cl-calendar__weeks" :style="{ gap: `${cellGap}px` }">
|
|
|
|
|
+ <view class="cl-calendar__weeks-item" v-for="weekName in weekLabels" :key="weekName">
|
|
|
|
|
+ <cl-text>{{ weekName }}</cl-text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 日期网格容器 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="cl-calendar__view"
|
|
|
|
|
+ ref="calendarViewRef"
|
|
|
|
|
+ :style="{ height: `${viewHeight}px`, gap: `${cellGap}px` }"
|
|
|
|
|
+ @tap="onTap"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- Web端使用DOM渲染 -->
|
|
|
|
|
+ <!-- #ifndef APP -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="cl-calendar__view-row"
|
|
|
|
|
+ :style="{ gap: `${cellGap}px` }"
|
|
|
|
|
+ v-for="(weekRow, rowIndex) in dateMatrix"
|
|
|
|
|
+ :key="rowIndex"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="cl-calendar__view-cell"
|
|
|
|
|
+ v-for="(dateCell, cellIndex) in weekRow"
|
|
|
|
|
+ :key="cellIndex"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'is-selected': dateCell.isSelected,
|
|
|
|
|
+ 'is-range': dateCell.isRange,
|
|
|
|
|
+ 'is-hide': dateCell.isHide,
|
|
|
|
|
+ 'is-disabled': dateCell.isDisabled,
|
|
|
|
|
+ 'is-today': dateCell.isToday,
|
|
|
|
|
+ 'is-other-month': !dateCell.isCurrentMonth
|
|
|
|
|
+ }"
|
|
|
|
|
+ :style="{ height: cellHeight + 'px' }"
|
|
|
|
|
+ @click.stop="selectDateCell(dateCell)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <cl-text :color="getCellColor(dateCell)" :size="`${fontSize}px`">{{
|
|
|
|
|
+ dateCell.date
|
|
|
|
|
+ }}</cl-text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- #endif -->
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+import { computed, nextTick, onMounted, ref, watch, type PropType } from "vue";
|
|
|
|
|
+import { ctx, dayUts, first, isDark, parsePt, useRefs } from "@/cool";
|
|
|
|
|
+import CalendarPicker from "./picker.uvue";
|
|
|
|
|
+import { $t, t } from "@/locale";
|
|
|
|
|
+import type { ClCalendarMode } from "../../types";
|
|
|
|
|
+
|
|
|
|
|
+defineOptions({
|
|
|
|
|
+ name: "cl-calendar"
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 日期单元格数据结构
|
|
|
|
|
+type DateCell = {
|
|
|
|
|
+ date: string; // 显示的日期数字
|
|
|
|
|
+ isCurrentMonth: boolean; // 是否属于当前显示月份
|
|
|
|
|
+ isToday: boolean; // 是否为今天
|
|
|
|
|
+ isSelected: boolean; // 是否被选中
|
|
|
|
|
+ isRange: boolean; // 是否在选择范围内
|
|
|
|
|
+ fullDate: string; // 完整日期格式 YYYY-MM-DD
|
|
|
|
|
+ isDisabled: boolean; // 是否被禁用
|
|
|
|
|
+ isHide: boolean; // 是否隐藏显示
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 组件属性定义
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ // 透传样式配置
|
|
|
|
|
+ pt: {
|
|
|
|
|
+ type: Object,
|
|
|
|
|
+ default: () => ({})
|
|
|
|
|
+ },
|
|
|
|
|
+ // 是否显示头部导航栏
|
|
|
|
|
+ showHeader: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true
|
|
|
|
|
+ },
|
|
|
|
|
+ // 日期选择模式:单选/多选/范围选择
|
|
|
|
|
+ mode: {
|
|
|
|
|
+ type: String as PropType<ClCalendarMode>,
|
|
|
|
|
+ default: "single"
|
|
|
|
|
+ },
|
|
|
|
|
+ // 当前选中的日期值(单选模式)
|
|
|
|
|
+ modelValue: {
|
|
|
|
|
+ type: String as PropType<string | null>,
|
|
|
|
|
+ default: null
|
|
|
|
|
+ },
|
|
|
|
|
+ // 选中的日期数组(多选/范围模式)
|
|
|
|
|
+ date: {
|
|
|
|
|
+ type: Array as PropType<string[]>,
|
|
|
|
|
+ default: () => []
|
|
|
|
|
+ },
|
|
|
|
|
+ // 是否显示其他月份的日期
|
|
|
|
|
+ showOtherMonth: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true
|
|
|
|
|
+ },
|
|
|
|
|
+ // 禁用的日期列表
|
|
|
|
|
+ disabledDate: {
|
|
|
|
|
+ type: Array as PropType<string[]>,
|
|
|
|
|
+ default: () => []
|
|
|
|
|
+ },
|
|
|
|
|
+ // 单元格高度
|
|
|
|
|
+ cellHeight: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: 60
|
|
|
|
|
+ },
|
|
|
|
|
+ // 单元格间距
|
|
|
|
|
+ cellGap: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ // 字体大小
|
|
|
|
|
+ fontSize: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: 14
|
|
|
|
|
+ },
|
|
|
|
|
+ // 当前月日期颜色
|
|
|
|
|
+ currentMonthColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: () => ctx.color["surface-700"] as string
|
|
|
|
|
+ },
|
|
|
|
|
+ // 其他月日期颜色
|
|
|
|
|
+ otherMonthColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: () => ctx.color["surface-300"] as string
|
|
|
|
|
+ },
|
|
|
|
|
+ // 今天日期颜色
|
|
|
|
|
+ todayColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "#ff6b6b"
|
|
|
|
|
+ },
|
|
|
|
|
+ // 选中日期文字颜色
|
|
|
|
|
+ selectedTextColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: "#ffffff"
|
|
|
|
|
+ },
|
|
|
|
|
+ // 选中日期背景色
|
|
|
|
|
+ selectedBgColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: () => ctx.color["primary-500"] as string
|
|
|
|
|
+ },
|
|
|
|
|
+ // 范围选择背景色
|
|
|
|
|
+ rangeBgColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: () => ctx.color["primary-100"] as string
|
|
|
|
|
+ },
|
|
|
|
|
+ // 禁用的日期颜色
|
|
|
|
|
+ disabledColor: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: () => ctx.color["surface-300"] as string
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 事件发射器定义
|
|
|
|
|
+const emit = defineEmits(["update:modelValue", "update:date", "change"]);
|
|
|
|
|
+
|
|
|
|
|
+// 透传样式属性类型
|
|
|
|
|
+type PassThrough = {
|
|
|
|
|
+ className?: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 解析透传样式配置
|
|
|
|
|
+const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|
|
|
|
+
|
|
|
|
|
+// 组件引用管理器
|
|
|
|
|
+const refs = useRefs();
|
|
|
|
|
+
|
|
|
|
|
+// 日历视图DOM元素引用
|
|
|
|
|
+const calendarViewRef = ref<UniElement | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+// 当前显示的年份
|
|
|
|
|
+const currentYear = ref(0);
|
|
|
|
|
+
|
|
|
|
|
+// 当前显示的月份
|
|
|
|
|
+const currentMonth = ref(0);
|
|
|
|
|
+
|
|
|
|
|
+// 视图高度
|
|
|
|
|
+const viewHeight = computed(() => {
|
|
|
|
|
+ return props.cellHeight * 6 + "px";
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 单元格宽度
|
|
|
|
|
+const cellWidth = ref(0);
|
|
|
|
|
+
|
|
|
|
|
+// 星期标签数组
|
|
|
|
|
+const weekLabels = computed(() => {
|
|
|
|
|
+ return [t("日"), t("一"), t("二"), t("三"), t("四"), t("五"), t("六")];
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 日历日期矩阵数据(6行7列)
|
|
|
|
|
+const dateMatrix = ref<DateCell[][]>([]);
|
|
|
|
|
+
|
|
|
|
|
+// 当前选中的日期列表
|
|
|
|
|
+const selectedDates = ref<string[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取日历视图元素的位置信息
|
|
|
|
|
+ */
|
|
|
|
|
+async function getViewRect(): Promise<DOMRect | null> {
|
|
|
|
|
+ return calendarViewRef.value!.getBoundingClientRectAsync();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 判断指定日期是否被选中
|
|
|
|
|
+ * @param dateStr 日期字符串 YYYY-MM-DD
|
|
|
|
|
+ */
|
|
|
|
|
+function isDateSelected(dateStr: string): boolean {
|
|
|
|
|
+ if (props.mode == "single") {
|
|
|
|
|
+ // 单选模式:检查是否为唯一选中日期
|
|
|
|
|
+ return selectedDates.value[0] == dateStr;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 多选/范围模式:检查是否在选中列表中
|
|
|
|
|
+ return selectedDates.value.includes(dateStr);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 判断指定日期是否被禁用
|
|
|
|
|
+ * @param dateStr 日期字符串 YYYY-MM-DD
|
|
|
|
|
+ */
|
|
|
|
|
+function isDateDisabled(dateStr: string): boolean {
|
|
|
|
|
+ return props.disabledDate.includes(dateStr);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 判断指定日期是否在选择范围内(不包括端点)
|
|
|
|
|
+ * @param dateStr 日期字符串 YYYY-MM-DD
|
|
|
|
|
+ */
|
|
|
|
|
+function isDateInRange(dateStr: string): boolean {
|
|
|
|
|
+ // 仅范围选择模式且已选择两个端点时才有范围
|
|
|
|
|
+ if (props.mode != "range" || selectedDates.value.length != 2) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const [startDate, endDate] = selectedDates.value;
|
|
|
|
|
+ const currentDate = dayUts(dateStr);
|
|
|
|
|
+ return currentDate.isAfter(startDate) && currentDate.isBefore(endDate);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 计算并生成日历矩阵数据
|
|
|
|
|
+ * 生成6行7列共42个日期,包含上月末尾和下月开头的日期
|
|
|
|
|
+ */
|
|
|
|
|
+function calculateDateMatrix() {
|
|
|
|
|
+ const weekRows: DateCell[][] = [];
|
|
|
|
|
+ const todayStr = dayUts().format("YYYY-MM-DD"); // 今天的日期字符串
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前月第一天
|
|
|
|
|
+ const monthFirstDay = dayUts(`${currentYear.value}-${currentMonth.value}-01`);
|
|
|
|
|
+ const firstDayWeekIndex = monthFirstDay.getDay(); // 第一天是星期几 (0=周日, 6=周六)
|
|
|
|
|
+
|
|
|
|
|
+ // 计算日历显示的起始日期(可能是上个月的日期)
|
|
|
|
|
+ const calendarStartDate = monthFirstDay.subtract(firstDayWeekIndex, "day");
|
|
|
|
|
+
|
|
|
|
|
+ // 生成6周的日期数据(6行 × 7列 = 42天)
|
|
|
|
|
+ let iterateDate = calendarStartDate;
|
|
|
|
|
+ for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
|
|
|
|
|
+ const weekDates: DateCell[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
|
|
|
+ const fullDateStr = iterateDate.format("YYYY-MM-DD");
|
|
|
|
|
+ const nativeDate = iterateDate.toDate();
|
|
|
|
|
+ const dayNumber = nativeDate.getDate();
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否属于当前显示月份
|
|
|
|
|
+ const belongsToCurrentMonth =
|
|
|
|
|
+ nativeDate.getMonth() + 1 == currentMonth.value &&
|
|
|
|
|
+ nativeDate.getFullYear() == currentYear.value;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建日期单元格数据
|
|
|
|
|
+ const dateCell = {
|
|
|
|
|
+ date: `${dayNumber}`,
|
|
|
|
|
+ isCurrentMonth: belongsToCurrentMonth,
|
|
|
|
|
+ isToday: fullDateStr == todayStr,
|
|
|
|
|
+ isSelected: isDateSelected(fullDateStr),
|
|
|
|
|
+ isRange: isDateInRange(fullDateStr),
|
|
|
|
|
+ fullDate: fullDateStr,
|
|
|
|
|
+ isDisabled: isDateDisabled(fullDateStr),
|
|
|
|
|
+ isHide: false
|
|
|
|
|
+ } as DateCell;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据配置决定是否隐藏相邻月份的日期
|
|
|
|
|
+ if (!props.showOtherMonth && !belongsToCurrentMonth) {
|
|
|
|
|
+ dateCell.isHide = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ weekDates.push(dateCell);
|
|
|
|
|
+ iterateDate = iterateDate.add(1, "day"); // 移动到下一天
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ weekRows.push(weekDates);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dateMatrix.value = weekRows;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 使用Canvas绘制日历(仅APP端)
|
|
|
|
|
+ * Web端使用DOM渲染,APP端使用Canvas提升性能
|
|
|
|
|
+ */
|
|
|
|
|
+async function renderCalendarCanvas() {
|
|
|
|
|
+ // #ifdef APP
|
|
|
|
|
+ await nextTick(); // 等待DOM更新完成
|
|
|
|
|
+
|
|
|
|
|
+ const canvasContext = calendarViewRef.value!.getDrawableContext();
|
|
|
|
|
+
|
|
|
|
|
+ if (canvasContext == null) return;
|
|
|
|
|
+
|
|
|
|
|
+ canvasContext!.reset(); // 清空画布
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 绘制单个日期单元格
|
|
|
|
|
+ * @param dateCell 日期单元格数据
|
|
|
|
|
+ * @param colIndex 列索引 (0-6)
|
|
|
|
|
+ * @param rowIndex 行索引 (0-5)
|
|
|
|
|
+ */
|
|
|
|
|
+ function drawSingleCell(dateCell: DateCell, colIndex: number, rowIndex: number) {
|
|
|
|
|
+ // 计算单元格位置
|
|
|
|
|
+ const cellX = colIndex * cellWidth.value;
|
|
|
|
|
+ const cellY = rowIndex * props.cellHeight;
|
|
|
|
|
+ const centerX = cellX + cellWidth.value / 2;
|
|
|
|
|
+ const centerY = cellY + props.cellHeight / 2;
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制背景(选中状态或范围状态)
|
|
|
|
|
+ if (dateCell.isSelected || dateCell.isRange) {
|
|
|
|
|
+ const padding = props.cellGap; // 使用间距作为内边距
|
|
|
|
|
+ const bgX = cellX + padding;
|
|
|
|
|
+ const bgY = cellY + padding;
|
|
|
|
|
+ const bgWidth = cellWidth.value - padding * 2;
|
|
|
|
|
+ const bgHeight = props.cellHeight - padding * 2;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置背景颜色
|
|
|
|
|
+ if (dateCell.isSelected) {
|
|
|
|
|
+ canvasContext!.fillStyle = props.selectedBgColor;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dateCell.isRange) {
|
|
|
|
|
+ canvasContext!.fillStyle = props.rangeBgColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ canvasContext!.fillRect(bgX, bgY, bgWidth, bgHeight); // 绘制背景矩形
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置文字样式
|
|
|
|
|
+ canvasContext!.font = `${props.fontSize}px sans-serif`;
|
|
|
|
|
+ canvasContext!.textAlign = "center";
|
|
|
|
|
+
|
|
|
|
|
+ // 根据状态设置文字颜色
|
|
|
|
|
+ if (dateCell.isSelected) {
|
|
|
|
|
+ canvasContext!.fillStyle = props.selectedTextColor;
|
|
|
|
|
+ } else if (dateCell.isToday) {
|
|
|
|
|
+ canvasContext!.fillStyle = props.todayColor;
|
|
|
|
|
+ } else if (dateCell.isCurrentMonth) {
|
|
|
|
|
+ canvasContext!.fillStyle = props.currentMonthColor;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ canvasContext!.fillStyle = props.otherMonthColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制日期数字(垂直居中对齐)
|
|
|
|
|
+ const textOffsetY = (props.fontSize / 2) * 0.7;
|
|
|
|
|
+ canvasContext!.fillText(dateCell.date.toString(), centerX, centerY + textOffsetY);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取容器尺寸信息
|
|
|
|
|
+ const viewRect = await getViewRect();
|
|
|
|
|
+
|
|
|
|
|
+ if (viewRect == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算单元格宽度(总宽度除以7列)
|
|
|
|
|
+ const cellSize = viewRect.width / 7;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新渲染配置
|
|
|
|
|
+ cellWidth.value = cellSize;
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历日期矩阵进行绘制
|
|
|
|
|
+ for (let rowIndex = 0; rowIndex < dateMatrix.value.length; rowIndex++) {
|
|
|
|
|
+ const weekRow = dateMatrix.value[rowIndex];
|
|
|
|
|
+ for (let colIndex = 0; colIndex < weekRow.length; colIndex++) {
|
|
|
|
|
+ const dateCell = weekRow[colIndex];
|
|
|
|
|
+ drawSingleCell(dateCell, colIndex, rowIndex);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ canvasContext!.update(); // 更新画布显示
|
|
|
|
|
+ // #endif
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理日期单元格选择逻辑
|
|
|
|
|
+ * @param dateCell 被点击的日期单元格
|
|
|
|
|
+ */
|
|
|
|
|
+function selectDateCell(dateCell: DateCell) {
|
|
|
|
|
+ // 隐藏或禁用的日期不可选择
|
|
|
|
|
+ if (dateCell.isHide || dateCell.isDisabled) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (props.mode == "single") {
|
|
|
|
|
+ // 单选模式:直接替换选中日期
|
|
|
|
|
+ selectedDates.value = [dateCell.fullDate];
|
|
|
|
|
+ emit("update:modelValue", dateCell.fullDate);
|
|
|
|
|
+ } else if (props.mode == "multiple") {
|
|
|
|
|
+ // 多选模式:切换选中状态
|
|
|
|
|
+ const existingIndex = selectedDates.value.indexOf(dateCell.fullDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (existingIndex >= 0) {
|
|
|
|
|
+ // 已选中则移除
|
|
|
|
|
+ selectedDates.value.splice(existingIndex, 1);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 未选中则添加
|
|
|
|
|
+ selectedDates.value.push(dateCell.fullDate);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 范围选择模式
|
|
|
|
|
+ if (selectedDates.value.length == 0) {
|
|
|
|
|
+ // 第一次点击:设置起始日期
|
|
|
|
|
+ selectedDates.value = [dateCell.fullDate];
|
|
|
|
|
+ } else if (selectedDates.value.length == 1) {
|
|
|
|
|
+ // 第二次点击:设置结束日期
|
|
|
|
|
+ const startDate = dayUts(selectedDates.value[0]);
|
|
|
|
|
+ const endDate = dayUts(dateCell.fullDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (endDate.isBefore(startDate)) {
|
|
|
|
|
+ // 结束日期早于开始日期时自动交换
|
|
|
|
|
+ selectedDates.value = [dateCell.fullDate, selectedDates.value[0]];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ selectedDates.value = [selectedDates.value[0], dateCell.fullDate];
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 已有范围时重新开始选择
|
|
|
|
|
+ selectedDates.value = [dateCell.fullDate];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 发射更新事件
|
|
|
|
|
+ emit("update:date", [...selectedDates.value]);
|
|
|
|
|
+ emit("change", selectedDates.value);
|
|
|
|
|
+
|
|
|
|
|
+ // 重新计算日历数据并重绘
|
|
|
|
|
+ calculateDateMatrix();
|
|
|
|
|
+ renderCalendarCanvas();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 获取单元格字体颜色
|
|
|
|
|
+ * @param dateCell 日期单元格数据
|
|
|
|
|
+ * @returns 字体颜色
|
|
|
|
|
+ */
|
|
|
|
|
+function getCellColor(dateCell: DateCell): string {
|
|
|
|
|
+ // 禁用的日期颜色
|
|
|
|
|
+ if (dateCell.isDisabled) {
|
|
|
|
|
+ return props.disabledColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选中的日期文字颜色
|
|
|
|
|
+ if (dateCell.isSelected) {
|
|
|
|
|
+ return props.selectedTextColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 今天日期颜色
|
|
|
|
|
+ if (dateCell.isToday) {
|
|
|
|
|
+ return props.todayColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 当前月份日期颜色
|
|
|
|
|
+ if (dateCell.isCurrentMonth) {
|
|
|
|
|
+ return props.currentMonthColor;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 其他月份日期颜色
|
|
|
|
|
+ return props.otherMonthColor;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理年月选择器的变化事件
|
|
|
|
|
+ * @param yearMonthArray [年份, 月份] 数组
|
|
|
|
|
+ */
|
|
|
|
|
+function onYearMonthChange(yearMonthArray: number[]) {
|
|
|
|
|
+ console.log("年月选择器变化:", yearMonthArray);
|
|
|
|
|
+ currentYear.value = yearMonthArray[0];
|
|
|
|
|
+ currentMonth.value = yearMonthArray[1];
|
|
|
|
|
+
|
|
|
|
|
+ // 重新计算日历数据并重绘
|
|
|
|
|
+ calculateDateMatrix();
|
|
|
|
|
+ renderCalendarCanvas();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理点击事件(APP端Canvas点击检测)
|
|
|
|
|
+ */
|
|
|
|
|
+async function onTap(e: UniPointerEvent) {
|
|
|
|
|
+ // 获取容器位置信息
|
|
|
|
|
+ const viewRect = await getViewRect();
|
|
|
|
|
+
|
|
|
|
|
+ if (viewRect == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算触摸点相对于容器的坐标
|
|
|
|
|
+ const relativeX = e.clientX - viewRect.left;
|
|
|
|
|
+ const relativeY = e.clientY - viewRect.top;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据坐标计算对应的行列索引
|
|
|
|
|
+ const columnIndex = Math.floor(relativeX / cellWidth.value);
|
|
|
|
|
+ const rowIndex = Math.floor(relativeY / props.cellHeight);
|
|
|
|
|
+
|
|
|
|
|
+ // 边界检查:确保索引在有效范围内
|
|
|
|
|
+ if (
|
|
|
|
|
+ rowIndex < 0 ||
|
|
|
|
|
+ rowIndex >= dateMatrix.value.length ||
|
|
|
|
|
+ columnIndex < 0 ||
|
|
|
|
|
+ columnIndex >= 7
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const targetDateCell = dateMatrix.value[rowIndex][columnIndex];
|
|
|
|
|
+ selectDateCell(targetDateCell);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 切换到上一个月
|
|
|
|
|
+ */
|
|
|
|
|
+function gotoPrevMonth() {
|
|
|
|
|
+ const [newYear, newMonth] = dayUts(`${currentYear.value}-${currentMonth.value}-01`)
|
|
|
|
|
+ .subtract(1, "month")
|
|
|
|
|
+ .toArray();
|
|
|
|
|
+
|
|
|
|
|
+ currentYear.value = newYear;
|
|
|
|
|
+ currentMonth.value = newMonth;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新计算并渲染日历
|
|
|
|
|
+ calculateDateMatrix();
|
|
|
|
|
+ renderCalendarCanvas();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 切换到下一个月
|
|
|
|
|
+ */
|
|
|
|
|
+function gotoNextMonth() {
|
|
|
|
|
+ const [newYear, newMonth] = dayUts(`${currentYear.value}-${currentMonth.value}-01`)
|
|
|
|
|
+ .add(1, "month")
|
|
|
|
|
+ .toArray();
|
|
|
|
|
+
|
|
|
|
|
+ currentYear.value = newYear;
|
|
|
|
|
+ currentMonth.value = newMonth;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新计算并渲染日历
|
|
|
|
|
+ calculateDateMatrix();
|
|
|
|
|
+ renderCalendarCanvas();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析选中日期
|
|
|
|
|
+ */
|
|
|
|
|
+function parseDate() {
|
|
|
|
|
+ // 根据选择模式初始化选中日期
|
|
|
|
|
+ if (props.mode == "single") {
|
|
|
|
|
+ selectedDates.value = props.modelValue != null ? [props.modelValue] : [];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ selectedDates.value = [...props.date];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取初始显示日期(优先使用选中日期,否则使用当前日期)
|
|
|
|
|
+ const initialDate = first(selectedDates.value);
|
|
|
|
|
+ const [initialYear, initialMonth] = dayUts(initialDate).toArray();
|
|
|
|
|
+
|
|
|
|
|
+ currentYear.value = initialYear;
|
|
|
|
|
+ currentMonth.value = initialMonth;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算初始日历数据
|
|
|
|
|
+ calculateDateMatrix();
|
|
|
|
|
+
|
|
|
|
|
+ // 渲染日历视图
|
|
|
|
|
+ renderCalendarCanvas();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 组件挂载时的初始化逻辑
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ // 监听单选模式的值变化
|
|
|
|
|
+ watch(
|
|
|
|
|
+ computed(() => props.modelValue ?? ""),
|
|
|
|
|
+ (newValue: string) => {
|
|
|
|
|
+ selectedDates.value = [newValue];
|
|
|
|
|
+ parseDate();
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ immediate: true
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 监听多选/范围模式的值变化
|
|
|
|
|
+ watch(
|
|
|
|
|
+ computed(() => props.date),
|
|
|
|
|
+ (newDateArray: string[]) => {
|
|
|
|
|
+ selectedDates.value = [...newDateArray];
|
|
|
|
|
+ parseDate();
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ immediate: true
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+/* 日历组件主容器 */
|
|
|
|
|
+.cl-calendar {
|
|
|
|
|
+ @apply relative;
|
|
|
|
|
+
|
|
|
|
|
+ /* 头部导航栏样式 */
|
|
|
|
|
+ &__header {
|
|
|
|
|
+ @apply flex flex-row items-center justify-between p-3 w-full;
|
|
|
|
|
+
|
|
|
|
|
+ /* 上一月/下一月按钮样式 */
|
|
|
|
|
+ &-prev,
|
|
|
|
|
+ &-next {
|
|
|
|
|
+ @apply flex flex-row items-center justify-center rounded-full bg-surface-100;
|
|
|
|
|
+ width: 60rpx;
|
|
|
|
|
+ height: 60rpx;
|
|
|
|
|
+
|
|
|
|
|
+ /* 暗色模式适配 */
|
|
|
|
|
+ &.is-dark {
|
|
|
|
|
+ @apply bg-surface-700;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 当前年月显示区域 */
|
|
|
|
|
+ &-date {
|
|
|
|
|
+ @apply flex flex-row items-center justify-center;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 星期标题行样式 */
|
|
|
|
|
+ &__weeks {
|
|
|
|
|
+ @apply flex flex-row;
|
|
|
|
|
+
|
|
|
|
|
+ /* 单个星期标题样式 */
|
|
|
|
|
+ &-item {
|
|
|
|
|
+ @apply flex flex-row items-center justify-center flex-1;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 日期网格容器样式 */
|
|
|
|
|
+ &__view {
|
|
|
|
|
+ @apply w-full;
|
|
|
|
|
+
|
|
|
|
|
+ /* Web端DOM渲染样式 */
|
|
|
|
|
+ // #ifndef APP
|
|
|
|
|
+ /* 日期行样式 */
|
|
|
|
|
+ &-row {
|
|
|
|
|
+ @apply flex flex-row;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 日期单元格样式 */
|
|
|
|
|
+ &-cell {
|
|
|
|
|
+ @apply flex-1 flex flex-col items-center justify-center relative;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+
|
|
|
|
|
+ /* 选中状态样式 */
|
|
|
|
|
+ &.is-selected {
|
|
|
|
|
+ background-color: v-bind("props.selectedBgColor");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 范围选择背景样式 */
|
|
|
|
|
+ &.is-range {
|
|
|
|
|
+ background-color: v-bind("props.rangeBgColor");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 隐藏状态(相邻月份日期) */
|
|
|
|
|
+ &.is-hide {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 禁用状态 */
|
|
|
|
|
+ &.is-disabled {
|
|
|
|
|
+ @apply opacity-50;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|