cl-calendar 支持添加上下文案

This commit is contained in:
icssoa
2025-09-10 10:58:31 +08:00
parent 91e79a4df7
commit 21dd6a5eed
5 changed files with 410 additions and 161 deletions

View File

@@ -5,37 +5,188 @@
<cl-calendar-select v-model="date"></cl-calendar-select>
</demo-item>
<demo-item :label="t('范围选择器')">
<cl-calendar-select v-model:date="dateArr" mode="range"></cl-calendar-select>
<demo-item :label="t('多选')">
<cl-calendar-select v-model:date="dateArr" mode="multiple"></cl-calendar-select>
</demo-item>
<demo-item :label="t('范围选')">
<cl-calendar-select v-model:date="dateRange" mode="range"></cl-calendar-select>
</demo-item>
<demo-item :label="t('禁用部分日期')">
<cl-calendar-select
v-model="date"
:disabled-date="disabledDate"
></cl-calendar-select>
<cl-calendar-select v-model="date" :date-config="dateConfig"></cl-calendar-select>
</demo-item>
<!-- <demo-item :label="t('日历长列表')">
<cl-button>{{ t("打开日历长列表") }}</cl-button>
</demo-item> -->
<demo-item :label="t('日历面板')">
<cl-calendar v-model="date"></cl-calendar>
<cl-calendar
v-model:date="dateRange2"
mode="range"
:month="10"
:show-header="isShowHeader"
:show-weeks="isShowWeeks"
:show-other-month="isShowOtherMonth"
:date-config="dateConfig2"
@change="onChange"
></cl-calendar>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item :label="t('自定义文案和颜色')">
<cl-switch v-model="isCustomDateConfig"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示头')">
<cl-switch v-model="isShowHeader"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示星期')">
<cl-switch v-model="isShowWeeks"></cl-switch>
</cl-list-item>
<cl-list-item :label="t('显示其他月份')">
<cl-switch v-model="isShowOtherMonth"></cl-switch>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { computed, ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { dayUts } from "@/cool";
import { dayUts, first, last } from "@/cool";
import type { ClCalendarDateConfig } from "@/uni_modules/cool-ui";
const date = ref<string | null>(dayUts().format("YYYY-MM-DD"));
const dateArr = ref<string[]>(["2025-09-02", "2025-09-09"]);
const disabledDate = ref<string[]>([
dayUts().add(2, "day").format("YYYY-MM-DD"),
dayUts().add(4, "day").format("YYYY-MM-DD"),
dayUts().add(6, "day").format("YYYY-MM-DD")
const dateArr = ref<string[]>([
dayUts().format("YYYY-MM-DD"),
dayUts().add(1, "day").format("YYYY-MM-DD")
]);
const dateRange = ref<string[]>([
dayUts().format("YYYY-MM-DD"),
dayUts().add(10, "day").format("YYYY-MM-DD")
]);
const dateConfig = ref<ClCalendarDateConfig[]>([
{
date: dayUts().add(1, "day").format("YYYY-MM-DD"),
disabled: true
},
{
date: dayUts().add(2, "day").format("YYYY-MM-DD"),
disabled: true
},
{
date: dayUts().add(3, "day").format("YYYY-MM-DD"),
disabled: true
}
]);
const isShowHeader = ref(true);
const isShowWeeks = ref(true);
const isShowOtherMonth = ref(true);
const isCustomDateConfig = ref(true);
const dateRange2 = ref<string[]>([]);
const dateConfig2 = computed(() => {
const dates = (
isCustomDateConfig.value
? [
{
date: "2025-10-01",
topText: "国庆节",
bottomText: "¥958",
color: "red"
},
{
date: "2025-10-02",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-03",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-04",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-05",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-06",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-07",
topText: "休",
bottomText: "¥613",
color: "red"
},
{
date: "2025-10-08",
topText: "休",
bottomText: "¥613",
color: "red"
}
]
: []
) as ClCalendarDateConfig[];
const startDate = first(dateRange2.value);
const endDate = last(dateRange2.value);
if (startDate != null) {
const item = dates.find((e) => e.date == startDate);
if (item == null) {
dates.push({
date: startDate,
bottomText: "入住"
} as ClCalendarDateConfig);
} else {
item.bottomText = "入住";
}
}
if (endDate != null && dateRange2.value.length > 1) {
const item = dates.find((e) => e.date == endDate);
if (item == null) {
dates.push({
date: endDate,
bottomText: "离店"
} as ClCalendarDateConfig);
} else {
item.bottomText = "离店";
}
}
return dates;
});
function onChange(date: string[]) {
console.log("日期变化:", date);
}
</script>

View File

@@ -33,7 +33,8 @@
v-model="value"
v-model:date="date"
:mode="mode"
:disabled-date="disabledDate"
:date-config="dateConfig"
@change="onCalendarChange"
></cl-calendar>
</view>
@@ -68,7 +69,7 @@
<script setup lang="ts">
import { ref, computed, type PropType } from "vue";
import type { ClCalendarMode } from "../../types";
import type { ClCalendarDateConfig, ClCalendarMode } from "../../types";
import { isEmpty, parsePt } from "@/cool";
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
import type { ClPopupPassThrough } from "../cl-popup/props";
@@ -105,9 +106,9 @@ const props = defineProps({
type: String as PropType<ClCalendarMode>,
default: "single"
},
// 禁用的日期
disabledDate: {
type: Array as PropType<string[]>,
// 日期配置
dateConfig: {
type: Array as PropType<ClCalendarDateConfig[]>,
default: () => []
},
// 选择器标题
@@ -132,6 +133,11 @@ const props = defineProps({
},
// 分隔符
splitor: {
type: String,
default: "、"
},
// 范围分隔符
rangeSplitor: {
type: String,
default: () => t(" 至 ")
},
@@ -158,7 +164,7 @@ const props = defineProps({
});
// 定义事件
const emit = defineEmits(["update:modelValue", "update:date", "change"]);
const emit = defineEmits(["update:modelValue", "update:date", "change", "select"]);
// 弹出层引用
const popupRef = ref<ClPopupComponentPublicInstance | null>(null);
@@ -180,7 +186,16 @@ const date = ref<string[]>([]);
// 显示文本
const text = computed(() => {
return props.mode == "single" ? (props.modelValue ?? "") : props.date.join(props.splitor);
switch (props.mode) {
case "single":
return props.modelValue ?? "";
case "multiple":
return props.date.join(props.splitor);
case "range":
return props.date.join(props.rangeSplitor);
default:
return "";
}
});
// 选择器显示状态
@@ -249,6 +264,11 @@ function confirm() {
close();
}
// 日历变化
function onCalendarChange(date: string[]) {
emit("select", date);
}
defineExpose({
open,
close

View File

@@ -40,7 +40,7 @@
</view>
<!-- 星期标题行 -->
<view class="cl-calendar__weeks" :style="{ gap: `${cellGap}px` }">
<view class="cl-calendar__weeks" :style="{ gap: `${cellGap}px` }" v-if="showWeeks">
<view class="cl-calendar__weeks-item" v-for="weekName in weekLabels" :key="weekName">
<cl-text>{{ weekName }}</cl-text>
</view>
@@ -53,7 +53,6 @@
:style="{ height: `${viewHeight}px`, gap: `${cellGap}px` }"
@tap="onTap"
>
<!-- Web端使用DOM渲染 -->
<!-- #ifndef APP -->
<view
class="cl-calendar__view-row"
@@ -73,12 +72,41 @@
'is-today': dateCell.isToday,
'is-other-month': !dateCell.isCurrentMonth
}"
:style="{ height: cellHeight + 'px' }"
:style="{
height: cellHeight + 'px',
backgroundColor: getCellBgColor(dateCell)
}"
@click.stop="selectDateCell(dateCell)"
>
<cl-text :color="getCellColor(dateCell)" :size="`${fontSize}px`">{{
dateCell.date
}}</cl-text>
<!-- 顶部文本 -->
<cl-text
:size="20"
:color="getCellTextColor(dateCell)"
:pt="{
className: 'absolute top-[2px]'
}"
>{{ dateCell.topText }}</cl-text
>
<!-- 主日期数字 -->
<cl-text
:color="getCellTextColor(dateCell)"
:size="`${fontSize}px`"
:pt="{
className: 'font-bold'
}"
>{{ dateCell.date }}</cl-text
>
<!-- 底部文本 -->
<cl-text
:size="20"
:color="getCellTextColor(dateCell)"
:pt="{
className: 'absolute bottom-[2px]'
}"
>{{ dateCell.bottomText }}</cl-text
>
</view>
</view>
<!-- #endif -->
@@ -91,7 +119,7 @@ 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";
import type { ClCalendarDateConfig, ClCalendarMode } from "../../types";
defineOptions({
name: "cl-calendar"
@@ -107,6 +135,9 @@ type DateCell = {
fullDate: string; // 完整日期格式 YYYY-MM-DD
isDisabled: boolean; // 是否被禁用
isHide: boolean; // 是否隐藏显示
topText: string; // 顶部文案
bottomText: string; // 底部文案
color: string; // 颜色
};
// 组件属性定义
@@ -116,16 +147,6 @@ const props = defineProps({
type: Object,
default: () => ({})
},
// 是否显示头部导航栏
showHeader: {
type: Boolean,
default: true
},
// 日期选择模式:单选/多选/范围选择
mode: {
type: String as PropType<ClCalendarMode>,
default: "single"
},
// 当前选中的日期值(单选模式)
modelValue: {
type: String as PropType<string | null>,
@@ -136,65 +157,38 @@ const props = defineProps({
type: Array as PropType<string[]>,
default: () => []
},
// 日期选择模式:单选/多选/范围选择
mode: {
type: String as PropType<ClCalendarMode>,
default: "single"
},
// 日期配置
dateConfig: {
type: Array as PropType<ClCalendarDateConfig[]>,
default: () => []
},
// 设置年份
year: {
type: Number
},
// 设置月份
month: {
type: Number
},
// 是否显示其他月份的日期
showOtherMonth: {
type: Boolean,
default: true
},
// 禁用的日期列表
disabledDate: {
type: Array as PropType<string[]>,
default: () => []
// 是否显示头部导航栏
showHeader: {
type: Boolean,
default: true
},
// 单元格高度
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
// 是否显示星期
showWeeks: {
type: Boolean,
default: true
}
});
@@ -209,6 +203,39 @@ type PassThrough = {
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 主色
const color = ref(ctx.color["primary-500"] as string);
// 单元格高度
const cellHeight = ref(66);
// 单元格间距
const cellGap = ref(0);
// 字体大小
const fontSize = ref(14);
// 当前月份日期颜色
const textColor = computed(() => {
return isDark.value ? "white" : (ctx.color["surface-700"] as string);
});
// 其他月份日期颜色
const textOtherMonthColor = computed(() => {
return isDark.value
? (ctx.color["surface-500"] as string)
: (ctx.color["surface-300"] as string);
});
// 禁用日期颜色
const textDisabledColor = computed(() => {
return isDark.value
? (ctx.color["surface-500"] as string)
: (ctx.color["surface-300"] as string);
});
// 今天日期颜色
const textTodayColor = ref("#ff6b6b");
// 选中日期颜色
const textSelectedColor = ref("#ffffff");
// 选中日期背景颜色
const bgSelectedColor = ref(color.value);
// 范围选择背景颜色
const bgRangeColor = ref(color.value + "11");
// 组件引用管理器
const refs = useRefs();
@@ -223,7 +250,7 @@ const currentMonth = ref(0);
// 视图高度
const viewHeight = computed(() => {
return props.cellHeight * 6 + "px";
return cellHeight.value * 6;
});
// 单元格宽度
@@ -266,7 +293,7 @@ function isDateSelected(dateStr: string): boolean {
* @param dateStr 日期字符串 YYYY-MM-DD
*/
function isDateDisabled(dateStr: string): boolean {
return props.disabledDate.includes(dateStr);
return props.dateConfig.some((config) => config.date == dateStr && config.disabled == true);
}
/**
@@ -284,6 +311,63 @@ function isDateInRange(dateStr: string): boolean {
return currentDate.isAfter(startDate) && currentDate.isBefore(endDate);
}
/**
* 获取单元格字体颜色
* @param dateCell 日期单元格数据
* @returns 字体颜色
*/
function getCellTextColor(dateCell: DateCell): string {
// 选中的日期文字颜色
if (dateCell.isSelected) {
return textSelectedColor.value;
}
if (dateCell.color != "") {
return dateCell.color;
}
// 范围选择日期颜色
if (dateCell.isRange) {
return color.value;
}
// 禁用的日期颜色
if (dateCell.isDisabled) {
return textDisabledColor.value;
}
// 今天日期颜色
if (dateCell.isToday) {
return textTodayColor.value;
}
// 当前月份日期颜色
if (dateCell.isCurrentMonth) {
return textColor.value;
}
// 其他月份日期颜色
return textOtherMonthColor.value;
}
/**
* 获取单元格背景颜色
* @param dateCell 日期单元格数据
* @returns 背景颜色
*/
function getCellBgColor(dateCell: DateCell): string {
if (dateCell.isSelected) {
return bgSelectedColor.value;
}
if (dateCell.isRange) {
return bgRangeColor.value;
}
return "transparent";
}
/**
* 计算并生成日历矩阵数据
* 生成6行7列共42个日期包含上月末尾和下月开头的日期
@@ -314,6 +398,9 @@ function calculateDateMatrix() {
nativeDate.getMonth() + 1 == currentMonth.value &&
nativeDate.getFullYear() == currentYear.value;
// 日期配置
const dateConfig = props.dateConfig.find((config) => config.date == fullDateStr);
// 构建日期单元格数据
const dateCell = {
date: `${dayNumber}`,
@@ -323,7 +410,10 @@ function calculateDateMatrix() {
isRange: isDateInRange(fullDateStr),
fullDate: fullDateStr,
isDisabled: isDateDisabled(fullDateStr),
isHide: false
isHide: false,
topText: dateConfig?.topText ?? "",
bottomText: dateConfig?.bottomText ?? "",
color: dateConfig?.color ?? ""
} as DateCell;
// 根据配置决定是否隐藏相邻月份的日期
@@ -364,47 +454,54 @@ async function renderCalendarCanvas() {
function drawSingleCell(dateCell: DateCell, colIndex: number, rowIndex: number) {
// 计算单元格位置
const cellX = colIndex * cellWidth.value;
const cellY = rowIndex * props.cellHeight;
const cellY = rowIndex * cellHeight.value;
const centerX = cellX + cellWidth.value / 2;
const centerY = cellY + props.cellHeight / 2;
const centerY = cellY + cellHeight.value / 2;
// 绘制背景(选中状态或范围状态)
if (dateCell.isSelected || dateCell.isRange) {
const padding = props.cellGap; // 使用间距作为内边距
const padding = cellGap.value; // 使用间距作为内边距
const bgX = cellX + padding;
const bgY = cellY + padding;
const bgWidth = cellWidth.value - padding * 2;
const bgHeight = props.cellHeight - padding * 2;
const bgHeight = cellHeight.value - padding * 2;
// 设置背景颜色
if (dateCell.isSelected) {
canvasContext!.fillStyle = props.selectedBgColor;
canvasContext!.fillStyle = bgSelectedColor.value;
}
if (dateCell.isRange) {
canvasContext!.fillStyle = props.rangeBgColor;
canvasContext!.fillStyle = bgRangeColor.value;
}
canvasContext!.fillRect(bgX, bgY, bgWidth, bgHeight); // 绘制背景矩形
}
// 设置文字样式
canvasContext!.font = `${props.fontSize}px sans-serif`;
// 获取单元格文字颜色
const cellTextColor = getCellTextColor(dateCell);
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;
// 绘制顶部文本
if (dateCell.topText != "") {
canvasContext!.font = `${Math.floor(fontSize.value * 0.75)}px sans-serif`;
canvasContext!.fillStyle = cellTextColor;
const topY = cellY + 16; // 距离顶部
canvasContext!.fillText(dateCell.topText, centerX, topY);
}
// 绘制日期数字(垂直居中对齐)
const textOffsetY = (props.fontSize / 2) * 0.7;
// 绘制日期数字
canvasContext!.font = `${fontSize.value}px sans-serif`;
canvasContext!.fillStyle = cellTextColor;
const textOffsetY = (fontSize.value / 2) * 0.7;
canvasContext!.fillText(dateCell.date.toString(), centerX, centerY + textOffsetY);
// 绘制底部文本
if (dateCell.bottomText != "") {
canvasContext!.font = `${Math.floor(fontSize.value * 0.75)}px sans-serif`;
canvasContext!.fillStyle = cellTextColor;
const bottomY = cellY + cellHeight.value - 8; // 距离底部
canvasContext!.fillText(dateCell.bottomText, centerX, bottomY);
}
}
// 获取容器尺寸信息
@@ -425,9 +522,12 @@ async function renderCalendarCanvas() {
const weekRow = dateMatrix.value[rowIndex];
for (let colIndex = 0; colIndex < weekRow.length; colIndex++) {
const dateCell = weekRow[colIndex];
if (!dateCell.isHide) {
drawSingleCell(dateCell, colIndex, rowIndex);
}
}
}
canvasContext!.update(); // 更新画布显示
// #endif
@@ -489,42 +589,11 @@ function selectDateCell(dateCell: DateCell) {
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];
@@ -550,7 +619,7 @@ async function onTap(e: UniPointerEvent) {
// 根据坐标计算对应的行列索引
const columnIndex = Math.floor(relativeX / cellWidth.value);
const rowIndex = Math.floor(relativeY / props.cellHeight);
const rowIndex = Math.floor(relativeY / cellHeight.value);
// 边界检查:确保索引在有效范围内
if (
@@ -613,8 +682,8 @@ function parseDate() {
const initialDate = first(selectedDates.value);
const [initialYear, initialMonth] = dayUts(initialDate).toArray();
currentYear.value = initialYear;
currentMonth.value = initialMonth;
currentYear.value = props.year ?? initialYear;
currentMonth.value = props.month ?? initialMonth;
// 计算初始日历数据
calculateDateMatrix();
@@ -648,6 +717,18 @@ onMounted(() => {
immediate: true
}
);
// 重新渲染
watch(
computed(() => [props.dateConfig, props.showOtherMonth]),
() => {
calculateDateMatrix();
renderCalendarCanvas();
},
{
deep: true
}
);
});
</script>
@@ -694,7 +775,6 @@ onMounted(() => {
&__view {
@apply w-full;
/* Web端DOM渲染样式 */
// #ifndef APP
/* 日期行样式 */
&-row {
@@ -706,16 +786,6 @@ onMounted(() => {
@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;

View File

@@ -140,7 +140,7 @@ const list = computed(() => {
// - 年份模式下显示“起始年 - 结束年”
const title = computed(() => {
return mode.value == "month"
? `${year.value}`
? `${year.value}`
: `${first(list.value)?.label} - ${last(list.value)?.label}`;
});

View File

@@ -200,3 +200,11 @@ export type ClTreeNodeInfo = {
};
export type ClCalendarMode = "single" | "multiple" | "range";
export type ClCalendarDateConfig = {
date: string;
topText?: string;
bottomText?: string;
disabled?: boolean;
color?: string;
};