来源
工作需要季度选择器,在网上找了很多,依照该大佬的博文,改动少许自用,将代码记录一下,以后取用方便
引用自页面原文
上正文
<!--
季度选择组件
value/v-model 绑定值
format 显示在输入框中的值的格式,引入季度:q(阿拉伯数字)、Q(中文数字)YYYY年第Q/q季度
placeholder 占位内容
value-format/valueFormat 绑定值的格式,不指定则为Date对象,格式包含YYYYMM、YYYYMm、YYYYmm、YYYYQQ、YYYYqq、YYYYQ、YYYYq
其中
YYYYMM:绑定值为每季度的第一月,例如2023年第一季度:202301,2023年第二季度:202304,2023年第三季度:202307,2023年第四季度:202310
YYYYMm:绑定值为每季度的中间月,例如2023年第一季度:202302, 2023年第二季度:202305,2023年第三季度:202308,2023年第四季度:202311
YYYYmm:绑定值为每季度的最后月,例如2023年第一季度:202303, 2023年第二季度:202306,2023年第三季度:202309,2023年第四季度:202312
YYYYQQ/YYYYqq:绑定值为季度,例如2023年第一季度:202301,2023年第二季度:202302,2023年第三季度:202303,2023年第四季度:202304
YYYYQ/YYYYq:绑定值为季度,例如2023年第一季度:20231,2023年第二季度:20232,2023年第三季度:20233,2023年第四季度:20234
使用示例
import QuarterPicker from '@/components/quarter-picker'
const range = ref({
quarter: '202401'
})
QuarterPicker ref="quarterValue" format="YYYY年第q季度" value-format="YYYYQQ" :disabled-date="disabledQuarter"
@change="handleChangeQuarter" v-model="range.quarter" />
-->
<template>
<ElPopover ref="quarterPopover" :visible="pickerVisible" trigger="click" popper-class="quarter-popover el-date-picker"
transition="el-zoom-in-top" :width="width">
<template #reference>
<ElInput class="el-date-editor" :prefix-icon="Calendar" :clearable="clearable" v-model="displayValue"
:placeholder="placeholder" @click="pickerVisible = true" @clear="clearModelValue">
</ElInput>
</template>
<div v-click-outside="closePopover" class="el-date-picker">
<div class="el-date-picker__header el-date-picker__header--bordered">
<span role="button" class="el-date-picker__prev-btn">
<button type="button" aria-label="前一年" class="el-picker-panel__icon-btn" @click="getPrevYear">
<ElIcon>
<DArrowLeft />
</ElIcon>
</button>
</span>
<span role="button" class="el-date-picker__header-label">{{ year }}年</span>
<span role="button" class="el-date-picker__next-btn">
<button type="button" aria-label="后一年" class="el-picker-panel__icon-btn" @click="getNextYear">
<ElIcon>
<DArrowRight />
</ElIcon>
</button>
</span>
</div>
<div class="el-picker-panel__content" style="margin: 10px 15px">
<table class="quarter-table" @click="handleTableClick">
<tbody>
<tr>
<td class="available" :class="getCellStyle(0)">
<a class="cell">第一季度</a>
</td>
<td class="available" :class="getCellStyle(1)">
<a class="cell">第二季度</a>
</td>
</tr>
<tr>
<td class="available" :class="getCellStyle(2)">
<a class="cell">第三季度</a>
</td>
<td class="available" :class="getCellStyle(3)">
<a class="cell">第四季度</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ElPopover>
</template>
<script lang="ts" setup>
import {
ElPopover,
ElInput,
ElIcon,
dayjs,
ClickOutside as vClickOutside,
} from "element-plus";
import { Calendar, DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
import { computed, ref, watch } from "vue";
import { hasClass } from "element-plus/es/utils/dom/style";
const getDayCountOfMonth = function (year: number, month: number) {
if (isNaN(+month)) return 31;
return new Date(year, +month + 1, 0).getDate();
};
const modifyDate = function (date: Date, y: number, m: number, d: number) {
return new Date(
y,
m,
d,
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
};
const changeYearMonthAndClampDate = function (
date: Date,
year: number,
month: number
) {
const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
return modifyDate(date, year, month, monthDate);
};
const formatDate = function (date: Date, format: string) {
if (!date) return "";
const result = dayjs(date).format(format);
return result;
};
const range = function (n: number) {
return Array.from({ length: n }).map((_, n: number) => n);
};
const nextDate = function (date: Date, amount = 1) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
};
const prevYear = function (date: Date, amount = 1) {
const year = date.getFullYear();
const month = date.getMonth();
return changeYearMonthAndClampDate(date, year - amount, month);
};
const nextYear = function (date: Date, amount = 1) {
const year = date.getFullYear();
const month = date.getMonth();
return changeYearMonthAndClampDate(date, year + amount, month);
};
const props = withDefaults(
defineProps<{
format: string;
modelValue: string;
disabled?: boolean;
valueFormat?: string;
width?: string;
clearable?: boolean;
placeholder?: string;
disabledDate: (val: Date) => boolean;
}>(),
{
/**
* 季度弹出框宽度
*/
width: "324px",
/**
* 显示在输入框中的格式,引入季度:Q(阿拉伯数字)、q(中文数字)
*/
format: "",
modelValue: "",
/**
* 输出值的格式 这里采用dayjs来格式化
*/
valueFormat: "",
/**
* 是否可清除
*/
clearable: true,
placeholder: "请选择季度",
/**
* 禁用日期
*/
disabledDate: (val: Date) => false,
}
);
const pickerVisible = ref(false);
const date = ref();
const year = computed(() => date.value.getFullYear());
const quarter = ref();
const quarterText = ["一", "二", "三", "四"];
// 局部指令
const closePopover = () => {
pickerVisible.value = false;
};
const parsedValue = computed(() => {
if (!props.modelValue) {
return new Date();
}
if (props.valueFormat) {
//改动
return valueFormatQuarter(props.modelValue);
}
// 非时间格式且未设置valueFormat,再尝试转换时间
return new Date(props.modelValue);
});
const displayValue = computed({
get: () => {
if (!props.modelValue) return null;
// 季度,从0开始 TODO
const quarter = parsedValue.value.getMonth() == 11 ? 3 : ~~(parsedValue.value.getMonth() / 3);
let fDate = formatDate(parsedValue.value, props.format);
fDate = fDate
.replace(/Q/, quarter + 1 + "")
.replace(/q/, quarterText[quarter]);
return fDate;
},
set: () => { },
});
watch(
() => props.modelValue,
(val) => {
if (props.valueFormat) {
//改动
date.value = val ? valueFormatQuarter(val) : new Date();
} else {
date.value = val ? parsedValue.value : new Date();
}
},
{
immediate: true,
}
);
function valueFormatQuarter(date: string) {
//改动
if (props.valueFormat.includes('Q') || props.valueFormat.includes('q')) {
//valueFormat 格式为YYYYQQ或YYYYqq时,做以下处理
const mon = parseInt(date.slice(date.length - 1, date.length)) * 3;
const str = date.slice(0, 4) + mon;
return dayjs(str).add(8, 'h').toDate()
} else {
//认为valueFormat是正常YYYYMMDD或YYYYMM类型
return dayjs(date).add(8, 'h').toDate()
}
}
const clearModelValue = () => {
emit("update:modelValue", "");
};
const emit = defineEmits(["update:modelValue"]);
const handleTableClick = (event: MouseEvent) => {
let target = event.target as HTMLElement;
if (target.tagName === "A") {
target = target.parentNode as HTMLTableCellElement;
}
if (target.tagName !== "TD") return;
if (hasClass(target, "disabled")) return;
const column = (target as HTMLTableCellElement).cellIndex;
const row = (target.parentNode as HTMLTableRowElement).rowIndex;
// 季度,从0开始
const currentQuarter = row * 2 + column;
// 季度开始月份,从0开始
const month = currentQuarter * 3;
let newDate: Date | string = new Date(year.value, month, 1);
if (props.valueFormat) {
//改动
if (props.valueFormat.includes('Q') || props.valueFormat.includes('q')) {
//valueFormat 格式为YYYYQQ或YYYYqq时,做以下处理
let midStr = props.valueFormat.length == 5 ? '' : '0'
newDate = year.value + midStr + (currentQuarter + 1)
} else {
//认为valueFormat是YYYYMM类型
if (props.valueFormat.includes('MM')) {
let mon = month + 1;// 1,4,7,10
newDate = (mon + '').length == 2 ? year.value + '' + mon : year.value + '0' + mon;
} else if (props.valueFormat.includes('Mm')) {
let mon = month + 2;// 2,5,8,11
newDate = (mon + '').length == 2 ? year.value + '' + mon : year.value + '0' + mon;
} else if (props.valueFormat.includes('Mm')) {
let mon = month + 3;// 3,6,9,12
newDate = (mon + '').length == 2 ? year.value + '' + mon : year.value + '0' + mon;
} else {
//默认为YYYYMM
let mon = month + 1;// 1,4,7,10
newDate = (mon + '').length == 2 ? year.value + '' + mon : year.value + '0' + mon;
}
}
}
quarter.value = currentQuarter + 1;
emit("update:modelValue", newDate.toString());
pickerVisible.value = false;
};
const getPrevYear = () => {
date.value = prevYear(date.value);
};
const getNextYear = () => {
date.value = nextYear(date.value);
};
/**
* 获取指定年份和季度总天数
* @param year
* @param quarter
*/
const getDayCountOfQuarter = (year: number, quarter: number) => {
switch (quarter) {
case 0: // 第一季度包含二月,需要对是否闰年进行判断处理
if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
return 91;
} else {
return 90;
}
case 1:
return 91;
default:
return 92;
}
};
/**
* 获取指定年份和季度的所有日期
* @param year
* @param quarter
*/
const datesInYearAndQuarter = (year: number, quarter: number) => {
const numOfDays = getDayCountOfQuarter(year, quarter);
const firstDay = new Date(year, quarter * 3, 1);
return range(numOfDays).map((n: number) => nextDate(firstDay, n));
};
const getCellStyle = (quarter: number) => {
const today = new Date();
const date = parsedValue.value;
return {
disabled:
typeof props.disabledDate === "function"
? datesInYearAndQuarter(year.value, quarter).every(props.disabledDate)
: false,
// 当前选中的季度样式
current:
date.getFullYear() === year.value && ~~(date.getMonth() / 3) === quarter,
// 今日所在季度样式
quarter:
today.getFullYear() === year.value &&
~~(today.getMonth() / 3) === quarter,
};
};
defineExpose({
year,
quarter,
});
</script>
<style>
.quarter-popover {
padding: 0 !important;
}
.quarter-table {
border-collapse: collapse;
width: 100%;
margin: -1px;
font-size: 12px;
}
.quarter-table td {
padding: 20px 3px;
text-align: center;
cursor: pointer;
}
.quarter-table td .cell {
display: block;
height: 32px;
margin: 0 auto;
color: #606266;
line-height: 32px;
}
.quarter-table td .cell:hover {
color: #FF9900;
}
.quarter-table td.current:not(.disabled) .cell {
font-weight: bold;
color: #FF9900;
}
.quarter-table td.disabled .cell {
color: #A8ABB2;
background-color: #f5f7fa;
cursor: not-allowed;
}
</style>
使用示例
<script setup>
import { ref } from 'vue';
import dayjs from "dayjs";
import QuarterPicker from '@/components/quarter-picker'
const quarterValue = ref("");
const range = ref({
quarter: '202401'
})
function disabledQuarter(date) {
return date > dayjs('202501', 'YYYYMM') || date < dayjs('20230401', 'YYYYMMDD');
}
function handleChangeQuarter(date) {
//change事件
console.log(date)
//quarterValue.value.year 为年(yyyy)
//quarterValue.value.quarter 为季度(1,2,3,4)
console.log(quarterValue.value.year + '0' + quarterValue.value.quarter);
}
</script>
<template>
<QuarterPicker ref="quarterValue" format="YYYY年第q季度" value-format="YYYYQQ" :disabled-date="disabledQuarter"
@valueChange="handleChangeQuarter" v-model="range.quarter" />
</template>
效果图
创建一个表格
一个简单的表格是这么创建的:
属性名 | 说明 | 类型 | 可选值 |
---|---|---|---|
value/v-model | 绑定值 | string | |
format | 显示在输入框中的值的格式,引入季度:q(阿拉伯数字)、Q(中文数字)YYYY年第Q/q季度 | string | YYYY年第q季度 、YYYY年第Q季度 |
width | 弹窗宽度,可选,默认为324px | string | |
valueFormat | 绑定值的格式,绑定值的格式,不指定则为Date对象 | string | 格式包含YYYYMM、YYYYMm、YYYYmm、YYYYQQ、YYYYqq、YYYYQ、YYYYq ,每个格式详细介绍请看下半段 |
value/v-model | 绑定值 | string |
绑定值格式详细介绍
YYYYMM:绑定值为每季度的第一月,例如2023年第一季度:202301,2023年第二季度:202304,2023年第三季度:202307,2023年第四季度:202310
YYYYMm:绑定值为每季度的中间月,例如2023年第一季度:202302, 2023年第二季度:202305,2023年第三季度:202308,2023年第四季度:202311
YYYYmm:绑定值为每季度的最后月,例如2023年第一季度:202303, 2023年第二季度:202306,2023年第三季度:202309,2023年第四季度:202312
YYYYQQ/YYYYqq:绑定值为季度,例如2023年第一季度:202301,2023年第二季度:202302,2023年第三季度:202303,2023年第四季度:202304
YYYYQ/YYYYq:绑定值为季度,例如2023年第一季度:20231,2023年第二季度:20232,2023年第三季度:20233,2023年第四季度:20234