年份与季度筛选组件封装
先总结一下整体架构:
📚 组件结构总览

quarterlyScreening 组件
│
├── 模板部分 (template)
│ ├── 起始时间选择(年份 + 季度)
│ ├── 结束时间选择(年份 + 季度)
│ ├── 错误提示框
│ └── 操作按钮(重置 + 确定)
│
├── 脚本部分 (script)
│ ├── QUARTER_MAP 常量(季度映射)
│ ├── props(父组件传入的配置)
│ ├── data(组件内部状态)
│ ├── computed(计算属性 - 6个核心优化)
│ │ ├── yearOptions - 年份选项
│ │ ├── quarterOptions - 季度选项
│ │ ├── isAllFieldsFilled - 是否填写完整
│ │ ├── startTimestamp - 起始时间戳 ⭐
│ │ ├── endTimestamp - 结束时间戳 ⭐
│ │ └── isValidTimeRange - 时间范围是否有效 ⭐
│ ├── watch(监听器 - 自动验证)
│ └── methods(方法)
│ ├── handleFromYearChange - 起始年份变化
│ ├── handleToYearChange - 结束年份变化
│ ├── validate - 统一验证 ⭐
│ ├── handleReset - 重置
│ └── handleSubmit - 提交
│
└── 样式部分 (style)└── 引入外部 SCSS 文件
🎯 核心优化点(6个)
1️⃣ 季度映射(第 129-134 行)
// 将 Q1~Q4 映射为数字,方便比较
const QUARTER_MAP = { Q1: 1, Q2: 2, Q3: 3, Q4: 4 };
2️⃣ 统一表单数据(第 187-192 行)
// 使用合理的数据类型(null 表示未选择)
form: {fromYear: null, // 数字startingQuarter: null, // 字符串terminationYear: null,endQuarter: null
}
3️⃣ 计算属性(第 201-260 行)
自动计算、自动缓存、代码清晰
4️⃣ 时间戳概念(第 230-250 行)
// 年 * 4 + 季度数字 = 可比较的时间戳
// 2020Q2 = 8082, 2021Q1 = 8085
startTimestamp() {return this.form.fromYear * 4 + QUARTER_MAP[this.form.startingQuarter];
}
5️⃣ 自动监听验证(第 264-274 行)
// 表单任何字段改变时,自动验证
watch: {form: { handler() { this.validate(); }, deep: true }
}
6️⃣ 统一验证方法(第 300-317 行)
// 所有验证逻辑集中在一个方法中
validate() {// 清空错误 → 检查是否填完 → 验证时间范围
}
💡 关键技术点
| 技术 | 说明 | 位置 |
|---|---|---|
v-model | 双向绑定 | 第 10, 28, 47, 65 行 |
v-for | 列表渲染 | 第 15, 32, 52, 69 行 |
v-if | 条件渲染 | 第 79 行 |
:class | 动态类名 | 第 83 行 |
computed | 计算属性 | 第 201-260 行 |
watch | 监听器 | 第 264-274 行 |
$emit | 事件触发 | 第 334, 363 行 |
📖 使用示例
<!-- 父组件中使用 -->
<quarterlyScreening:fromWhatYear="2019":cycleYearValue="10"colorChange="red"@determineFilterDate="handleDateChange"
/>
现在你应该能完全理解这个组件的每一行代码了!
组件模块
<template><!-- 季度筛选组件的根容器 --><div class="quarterlyScreening"><!-- 上部时间选择区域 --><div class="qs-top"><!-- 起始年份选择器 --><a-select placeholder="年" <!-- 占位符文字 -->style="width: 80px" <!-- 设置宽度为 80px -->v-model="form.fromYear" <!-- 双向绑定到 form.fromYear,选择后自动更新数据 -->@change="handleFromYearChange" <!-- 监听改变事件,用于联动调整结束年份 -->><!-- 循环渲染年份选项,yearOptions 是计算属性,返回年份数组 --><a-select-option v-for="year in yearOptions" <!-- 遍历年份数组 -->:key="year" <!-- 设置唯一 key,提升渲染性能 -->:value="year" <!-- 选项的值,选中后赋值给 v-model 绑定的变量 -->>{{ year }} <!-- 显示的年份文字 --></a-select-option></a-select><!-- 起始季度选择器 --><a-select class="offset-l15" <!-- 左边距 15px 的样式类 -->placeholder="季度" <!-- 占位符文字 -->style="width: 80px" <!-- 设置宽度 -->v-model="form.startingQuarter" <!-- 双向绑定到起始季度 -->><!-- 循环渲染季度选项 Q1~Q4 --><a-select-option v-for="quarter in quarterOptions" <!-- 遍历季度数组 ['Q1', 'Q2', 'Q3', 'Q4'] -->:key="quarter" <!-- 唯一 key -->:value="quarter" <!-- 选项值,如 'Q1' -->>{{ quarter }} <!-- 显示文字,如 Q1 --></a-select-option></a-select><!-- 中间的分隔文字 --><samp class="offset-l15 offset-r15">至</samp> <!-- 左右各 15px 边距 --><!-- 结束年份选择器 --><a-select placeholder="年" <!-- 占位符 -->style="width: 80px" <!-- 宽度 -->v-model="form.terminationYear" <!-- 双向绑定到结束年份 -->@change="handleToYearChange" <!-- 监听改变事件,用于验证时间范围 -->><!-- 循环渲染年份选项(与起始年份选项相同) --><a-select-option v-for="year in yearOptions" <!-- 遍历年份数组 -->:key="year" <!-- 唯一 key -->:value="year" <!-- 选项值 -->>{{ year }} <!-- 显示年份 --></a-select-option></a-select><!-- 结束季度选择器 --><a-select class="offset-l15" <!-- 左边距 -->placeholder="季度" <!-- 占位符 -->style="width: 80px" <!-- 宽度 -->v-model="form.endQuarter" <!-- 双向绑定到结束季度 -->><!-- 循环渲染季度选项 --><a-select-option v-for="quarter in quarterOptions" <!-- 遍历季度数组 -->:key="quarter" <!-- 唯一 key -->:value="quarter" <!-- 选项值 -->>{{ quarter }} <!-- 显示季度 --></a-select-option></a-select><!-- 错误提示框区域 --><!-- 只有当 errorMessage 有值时才显示(v-if) --><div class="amount-label-area" v-if="errorMessage"><div class="offset-t10" <!-- 上边距 10px --><!-- 动态 class:如果 colorChange 有值则显示红色样式,否则显示黄色样式 -->:class="colorChange ? 'redPromptBar' : 'reusable-prompt'"><!-- 感叹号图标 --><a-icon type="exclamation-circle" class="exclamation-circle"/><!-- 显示错误信息文字 --><span class="rp-TextArea">{{ errorMessage }}</span></div></div></div><!-- 下部按钮区域 --><div class="qs-bottom"><!-- 重置按钮 --><my-buttoncolor="#0070C9" <!-- 文字颜色为蓝色 -->:width="64" <!-- 宽度 64px -->:height="32" <!-- 高度 32px -->class="offset-r15" <!-- 右边距 15px -->@handleClick="handleReset" <!-- 点击时调用重置方法 -->>重置 <!-- 按钮文字 --></my-button><!-- 确定按钮 --><my-button color="#fff" <!-- 文字颜色为白色 -->backgroundColor="#D71921" <!-- 背景色为红色 -->borderColor="#D71921" <!-- 边框色为红色 -->:width="64" <!-- 宽度 -->:loading="submitLoading" <!-- 是否显示加载状态 -->:height="32" <!-- 高度 -->@handleClick="handleSubmit" <!-- 点击时调用提交方法 -->>确定 <!-- 按钮文字 --></my-button></div></div>
</template><script>
// 导入自定义按钮组件
import MyButton from "@/components/Button.vue";// 【核心优化点1】季度映射对象:将 Q1-Q4 转换为数字便于比较
// 原代码使用 substr(1,1) 截取字符串来比较,不够优雅
// 优化后:直接通过映射获取数字,如 QUARTER_MAP['Q1'] = 1
const QUARTER_MAP = {Q1: 1, // 第一季度对应数字 1Q2: 2, // 第二季度对应数字 2Q3: 3, // 第三季度对应数字 3Q4: 4 // 第四季度对应数字 4
};export default {name: 'quarterlyScreening', // 组件名称// 注册子组件components: {MyButton // 自定义按钮组件},// 接收父组件传入的参数props: {// 起始年份(从哪一年开始显示)fromWhatYear: {type: Number, // 类型:数字default: 2019 // 默认值:2019 年},// 年份范围(显示多少年的选项)// 例如:fromWhatYear=2019,cycleYearValue=10,则显示 2019-2028cycleYearValue: {type: Number, // 类型:数字default: 10 // 默认值:显示 10 年},// 提示框为空时的文字itemEmptyPrompt: {type: String, // 类型:字符串default: '年份与季度不能为空' // 默认提示文字},// 时间范围错误时的提示文字timeErrorPrompt: {type: String, // 类型:字符串default: '选择的年份或季度不能早于起始年份或季度' // 默认提示},// 颜色样式控制:传 'red' 显示红色提示,否则显示黄色colorChange: {type: String, // 类型:字符串default: null // 默认值:null(显示黄色)}},// 组件的响应式数据data() {return {// 提交按钮的加载状态submitLoading: false,// 【核心优化点2】统一的表单数据对象,使用合理的数据类型// 原代码使用数组存储单个值(如 fromYear: []),不合理// 优化后:使用 null 表示未选择状态,选择后变为对应的值form: {fromYear: null, // 起始年份(数字或 null)startingQuarter: null, // 起始季度(字符串或 null,如 'Q1')terminationYear: null, // 结束年份(数字或 null)endQuarter: null // 结束季度(字符串或 null)},// 错误提示信息(空字符串表示没有错误)errorMessage: ''};},// 【核心优化点3】计算属性 - 根据其他数据动态计算得出的值// 好处:自动缓存、自动更新、代码更清晰computed: {// 年份选项数组// 根据 fromWhatYear 和 cycleYearValue 动态生成// 例如:fromWhatYear=2019, cycleYearValue=10// 返回:[2019, 2020, 2021, ..., 2028]yearOptions() {return Array.from({ length: this.cycleYearValue }, // 创建指定长度的数组(_, i) => this.fromWhatYear + i // 每个元素为起始年份+索引);},// 季度选项数组(固定返回 Q1-Q4)quarterOptions() {return ['Q1', 'Q2', 'Q3', 'Q4'];},// 判断是否所有字段都已填写// 使用 !! 转换为布尔值// 只有当所有字段都有值时才返回 trueisAllFieldsFilled() {return !!(this.form.fromYear && // 起始年份已选择this.form.startingQuarter && // 起始季度已选择this.form.terminationYear && // 结束年份已选择this.form.endQuarter // 结束季度已选择);},// 【核心优化点4】时间戳概念 - 将"年+季度"转换为可比较的数字// 计算公式:年份 * 4 + 季度数字// 例如:2020年Q2 = 2020 * 4 + 2 = 8082// 2021年Q1 = 2021 * 4 + 1 = 8085// 这样就能直接比较大小:8085 > 8082,说明 2021Q1 晚于 2020Q2startTimestamp() {// 如果起始年份或季度未选择,返回 0if (!this.form.fromYear || !this.form.startingQuarter) return 0;// 计算时间戳:年 * 4 + 季度数字return this.form.fromYear * 4 + QUARTER_MAP[this.form.startingQuarter];},// 结束时间的时间戳(计算方式同上)endTimestamp() {// 如果结束年份或季度未选择,返回 0if (!this.form.terminationYear || !this.form.endQuarter) return 0;// 计算时间戳return this.form.terminationYear * 4 + QUARTER_MAP[this.form.endQuarter];},// 验证时间范围是否有效// 规则:结束时间必须 >= 起始时间isValidTimeRange() {// 如果还没填写完整,先不验证(返回 true 表示通过)if (!this.isAllFieldsFilled) return true;// 比较时间戳:结束时间戳 >= 起始时间戳return this.endTimestamp >= this.startTimestamp;}},// 【核心优化点5】监听器 - 自动监听数据变化并执行操作watch: {// 监听整个表单对象的变化form: {handler() {// 每当表单任何字段改变时,自动执行验证// 好处:实时反馈,用户体验更好this.validate();},deep: true // 深度监听,监听对象内部属性的变化}},// 组件的方法methods: {// 起始年份改变时的处理函数// 智能联动:如果起始年份 > 结束年份,自动调整结束年份handleFromYearChange() {// 如果已选择结束年份,且起始年份 > 结束年份if (this.form.terminationYear && this.form.fromYear > this.form.terminationYear) {// 自动将结束年份调整为与起始年份相同// 例如:原来选的 2019-2021,起始改为 2023,自动变为 2023-2023this.form.terminationYear = this.form.fromYear;}// 执行验证this.validate();},// 结束年份改变时的处理函数handleToYearChange() {// 只需执行验证即可(由 watch 自动触发)this.validate();},// 【核心优化点6】统一的验证方法// 原代码在每个 change 事件中都写验证逻辑,重复度高// 优化后:统一在这里验证,代码清晰易维护validate() {// 第一步:清空之前的错误信息this.errorMessage = '';// 第二步:如果没有填写完整,不显示错误// 原因:用户还在填写过程中,不应该提示错误if (!this.isAllFieldsFilled) return true;// 第三步:验证时间范围是否有效if (!this.isValidTimeRange) {// 如果无效,设置错误信息this.errorMessage = this.timeErrorPrompt;return false; // 返回 false 表示验证失败}// 验证通过return true;},// 重置按钮点击处理handleReset() {// 第一步:清空表单所有数据this.form = {fromYear: null, // 重置为 nullstartingQuarter: null,terminationYear: null,endQuarter: null};// 第二步:清空错误信息this.errorMessage = '';// 第三步:触发父组件事件,传递空对象// 父组件收到空对象后可以清空筛选条件this.$emit('determineFilterDate', {});},// 确定按钮点击处理handleSubmit() {// 第一步:验证是否填写完整if (!this.isAllFieldsFilled) {// 如果未填写完整,显示错误信息this.errorMessage = this.itemEmptyPrompt;return; // 终止执行}// 第二步:验证时间范围if (!this.validate()) {// 如果验证失败,validate() 方法已经设置了 errorMessagereturn; // 终止执行}// 第三步:准备要提交的数据// 注意:年份转换为字符串,因为父组件可能需要字符串格式const data = {fromYear: String(this.form.fromYear), // 数字转字符串startingQuarter: this.form.startingQuarter, // 保持字符串格式terminationYear: String(this.form.terminationYear),// 数字转字符串endQuarter: this.form.endQuarter // 保持字符串格式};// 第四步:触发父组件事件,传递筛选数据// 父组件通过 @determineFilterDate 监听这个事件this.$emit('determineFilterDate', data);}}
};
</script><style scoped lang='scss'>
@import "./index.scss";
</style>