<think>我们正在对一个Vue组件进行代码评审。这个组件是一个日历视图,用于显示某个月份的异常情况。
首先,我们注意到这个组件使用了Ant Design Vue的月份选择器(a-month-picker),并且展示了一个日历表格,其中标出了有异常的日子(errDay)和当前日子(currentDay)。
组件中使用了moment.js来处理日期。
接下来,我们将逐部分审查代码,并提出改进建议。
1. 模板部分(Template):
- 使用了a-month-picker,并设置了相关属性:v-model绑定currentMonth,placeholder使用国际化,valueFormat设置为"YYYY-MM",并监听change事件。
- 日历表格分为标题(星期几)和内容(日期格子)。
- 在日期格子中,根据条件添加currentDay和errDay类,并点击事件openModel。
- 每个格子中显示日期和“今日”标记(如果是今天),并在有异常时显示异常数量。
2. 脚本部分(Script):
- 引入了一个工厂模块(factory)和一个子组件errorList。
- 数据部分:定义了daysOfWeek(星期数组)、currentMonth(当前月份)、currentDay(当前日期字符串)、errDateArr(异常数据数组)、errDates(异常日期数组)和calendar(日历数据数组)。
- 属性:orgInfo,用于接收组织信息。
- 监听器:监听orgInfo的变化,触发获取日历数据。
- 生命周期:mounted中设置月份选择器的输入框为只读(防止用户输入),并初始化currentMonth为当前月份。
- 方法:
changeDate: 当月份选择器变化时,更新currentMonth和currentDay,并重新获取日历数据。
getAbnormalCount: 根据日期获取该日期的异常数量。
getCalendar: 核心方法,生成日历数据并获取异常数据。
openModel: 打开异常列表弹窗,如果该日期有异常则显示,否则提示无异常。
3. 样式部分:引入了less文件。
代码评审中可能发现的问题及改进建议:
1. 使用moment.js的问题:
- 由于moment.js已经不再推荐使用(处于维护模式),建议考虑使用day.js或date-fns等更轻量级的库。但如果不是必须,可以继续使用。
- 在代码中,我们多次使用moment,注意避免重复创建对象。
2. 在getCalendar方法中:
- 我们首先根据currentMonth生成日历数组。这里使用了moment,但注意today的获取:`const today = moment(this.currentMonth)`,这里假设this.currentMonth是有效的,否则可能出错。
- 生成日历的逻辑:先计算第一天是星期几(isoWeekday,星期一为1,星期日为7),然后补全前面的空白,再填充日期,最后补全后面的空白。
- 然后调用factory.getMeterAbnormalCalendarList获取异常数据。这里有一个问题:在获取异常数据之前,我们已经生成了日历(calendar),但获取异常数据后,我们只是更新了errDateArr和errDates,然后强制更新视图($forceUpdate)。但是,生成日历的数据(calendar)并没有包含异常数据,而是通过模板中的计算(errDates.includes(date.date))来判断。这样设计是可以的,但注意calendar数组中的每个日期对象只包含day和date,没有异常数量,异常数量是通过getAbnormalCount方法计算。这样在渲染每个格子时,会多次调用getAbnormalCount,可能影响性能(尤其是当一个月有多个异常日期时,每个格子都会遍历数组)。
改进建议:在生成calendar之后,将异常数据合并到calendar的每个日期对象中。这样在模板中可以直接通过date.abnormalCount来显示,避免每次调用函数。同时,在获取异常数据后,我们可以遍历calendar,为每个日期对象添加异常数量(如果有的话)。这样在模板中,我们可以直接使用`date.abnormalCount`来判断是否显示异常,以及显示异常数量。
3. 性能问题:
- 在模板中,每个日期格子都使用了`errDates.includes(date.date)`来判断是否是异常日期,而includes方法的时间复杂度是O(n),当异常日期较多时,可能会影响性能。另外,在同一个格子里,还调用了getAbnormalCount,这个方法也是遍历数组。因此,每个格子都会进行两次遍历(一次判断是否是异常日期,一次获取异常数量)。如果一个月有31天,那么最多会进行31*2次遍历(每次都是遍历异常日期数组)。
改进:在获取异常数据后,可以将异常数据数组转换为一个以日期为键,异常数量为值的对象。这样在模板中可以直接通过`errDatesMap[date.date]`来获取,判断是否存在异常日期,以及获取异常数量,时间复杂度为O(1)。
例如:
// 在getCalendar方法中
if (res.success) {
this.errDateArr = res.data || [];
// 创建一个映射
const errMap = {};
res.data.forEach(item => {
errMap[item.date] = item.abnormalCount;
});
this.errDatesMap = errMap; // 在data中定义errDatesMap: {}
// 这样在模板中,我们可以这样判断:if (errDatesMap[date.date]) 来显示异常标记,并且数量就是errDatesMap[date.date]
}
同时,在getAbnormalCount方法可以移除,改为直接从映射中获取。
4. 代码健壮性:
- 在openModel方法中,我们调用了this.getAbnormalCount(date.date),如果该日期没有异常,则提示。但是,我们已经在模板中通过errDates.includes(date.date)来判断是否添加errDay类,所以用户只能点击有errDay类的格子(因为无异常的格子没有绑定点击事件?)。但是注意,模板中每个格子都绑定了点击事件,包括非当前月的空白格子(因为date可能为空)。所以,在openModel中,首先应该判断date是否存在,以及date.date是否存在。
实际上,在模板中,空白格子(date为空)不会显示内容,但依然绑定了点击事件。当点击空白格子时,date为undefined,会导致错误。
改进:在模板中,空白格子(即date为空)不应该绑定点击事件。我们可以修改v-for循环中的条件:`v-for="date in week" :key="date.day"`,但注意当date为空时,key为undefined,这会导致警告。所以,我们最好在生成日历时,空白格子用一个固定的结构(比如{ empty: true })来表示,然后在模板中根据empty属性来避免绑定事件。
或者,在openModel方法中,首先判断date是否有效:
if (!date || !date.date) return;
目前代码中已经做了:`if (date && +this.getAbnormalCount(date.date) > 0)`,但是当date为空时,date.date会报错。所以需要先判断date是否存在。
5. 其他问题:
- 在mounted中,我们通过`document.querySelector`来设置输入框为只读。这种做法不够Vue,而且可能因为类名变化而失效。建议使用组件的API或者避免直接操作DOM。Ant Design Vue的MonthPicker是否有属性可以设置只读?查看文档,有readonly属性,我们可以这样:在a-month-picker上添加属性`:inputReadOnly="true"`,这样就不用手动操作DOM了。
6. 代码风格和可读性:
- 在getCalendar方法中,生成日历的逻辑可以提取出来作为一个独立的方法,这样getCalendar方法就只负责获取数据并更新状态,生成日历的逻辑可以复用或单独测试。
7. 国际化:
- 使用了this.$t来国际化,这很好。但注意daysOfWeek的定义,目前是在data中通过this.$t来初始化的,但data在组件初始化时执行,而语言切换可能不会更新daysOfWeek。所以,建议将daysOfWeek改为计算属性,这样当语言变化时,可以更新。
8. 控制台日志:
- 在getCalendar方法中,打印了this.calendar,可以移除。
9. 错误处理:
- 在getCalendar中,调用factory.getMeterAbnormalCalendarList时,没有处理错误情况,应该加上try-catch。
10. 关于currentDay的更新:
在changeDate方法中,如果选择的月份是当前月份,则设置currentDay为今天,否则置空。这样在日历中,只有当前月份才会显示“今日”标记。但是,在切换月份时,currentDay可能会被置空,而实际上在非当前月份,我们不需要显示“今日”。所以这个逻辑是合理的。
11. 强制更新:在获取异常数据后,使用了this.$forceUpdate(),这是因为我们修改了errDates和errDateArr,而calendar数组本身没有变化(但calendar中的每个日期对象在模板中会重新计算样式)。如果我们按照前面建议,将异常数据合并到calendar的每个日期对象中,那么就需要更新calendar数组(重新生成或者使用Vue.set),这样就不需要强制更新了。
12. 生成日历的算法:当前算法是可行的,但注意isoWeekday()返回的是ISO星期(星期一为1,星期日为7)。我们前面补白是从1到startWeek-1,即补startWeek-1个空白。例如,如果第一天是星期三(3),那么补2个空白(星期一和星期二)。然后从1号开始填充,直到月末。最后如果一周不满7天,则在后面补空白。
13. 在模板中,我们使用了`v-for="(week, index) in calendar"`,然后里面又使用`v-for="date in week"`。注意,当date为空字符串时,在模板中绑定:class和@click会出错,因为空字符串没有date属性。所以,我们在生成空白格子时,应该使用一个空对象(或者有标识的对象)而不是空字符串。
改进:在生成空白格子时,我们使用`{ empty: true }`,然后在模板中根据date.empty来判断。
修改生成日历的代码:
for (let i = 1; i < startWeek; i++) {
week.push({ empty: true });
}
... 填充日期
for (let i = week.length; i < 7; i++) {
week.push({ empty: true });
}
然后在模板中:
<div v-for="date in week" :key="date.day || index">
<div v-if="!date.empty" ...> ... </div>
<div v-else></div> <!-- 空白格子 -->
</div>
但是注意,空白格子不需要绑定事件和显示内容。
14. 在模板中,每个日期格子的key使用了`date.day`,但在空白格子中,day不存在,所以需要另外的key。我们可以使用周索引和日期索引组合:`:key="index + '-' + idx"`(在外层循环和内层循环分别有索引)。
但是,在生成日历时,我们最好给每个格子一个唯一的key,比如日期格子的key就是日期,空白格子可以用`'empty-'+周索引+格子索引`。
由于我们生成日历后,calendar的结构是固定的,所以我们可以这样:
- 在生成空白格子时,不设置day,但我们可以设置一个唯一的id(比如用周数+位置)?或者直接使用循环索引作为key(不推荐,但在这个场景下,日历不会变化,所以可以接受)。
另一种简单做法:在模板中,使用外层循环的index和内层循环的index组合:
<div class="date-tr" v-for="(week, weekIndex) in calendar" :key="weekIndex">
<div v-for="(date, dateIndex) in week" :key="`${weekIndex}-${dateIndex}`" ...>
</div>
这样每个格子都有唯一的key。
15. 在getCalendar方法中,我们使用了async/await,但是获取异常数据后,更新了errDateArr和errDates,然后强制更新。但是,如果我们按照前面的建议,将异常数据合并到calendar中,那么就需要在获取异常数据后,重新遍历calendar数组,更新每个日期对象的异常数量。这样,我们就不需要errDates和errDateArr了,只需要一个errDatesMap(用于快速查找)和一个包含异常数量的calendar数组。
具体步骤:
- 生成日历数组(calendar)时,每个日期对象为:{ day: 日期几号(字符串), date: 完整日期(YYYY-MM-DD) },空白格子为{ empty: true }。
- 获取异常数据,将异常数据数组转换为映射errDatesMap(日期为键,异常数量为值)。
- 遍历calendar数组的每一周,再遍历一周中的每一天,如果该格子不是空白,则检查errDatesMap中是否有该日期,如果有,则给这个日期对象添加一个abnormalCount属性,值为异常数量。
- 这样,在模板中,我们可以直接通过`date.abnormalCount`来显示异常数量,并且可以通过`date.abnormalCount`是否存在来判断是否是异常日期。
这样修改后,模板中的class绑定可以改为:
:class="['date-td', {'currentDay': date && currentDay === date.date},{'errDay': date && date.abnormalCount}]"
显示异常数量:
<div class="bottom-err" v-if="date && date.abnormalCount">
{{ $t('异常') }}:{{ date.abnormalCount }}
</div>
同时,在openModel方法中,我们可以直接通过`date.abnormalCount`来判断:
if (date && date.abnormalCount) {
// 打开弹窗
}
这样避免了多次调用getAbnormalCount函数,也避免了在模板中使用includes。
16. 关于currentDay的标记:在模板中,我们通过`{'currentDay': date && currentDay === date.date}`来标记当前天。注意,currentDay是字符串(YYYY-MM-DD),而date.date也是字符串,所以可以直接比较。但是,当非当前月份时,currentDay为空字符串,所以不会匹配。
17. 在changeDate方法中,我们使用moment(date)来格式化,但是date已经是字符串(YYYY-MM),所以可以直接使用。但是,当date为空时,需要处理。
18. 在mounted中,我们初始化currentMonth为当前月份,但是注意,在watch中,orgInfo变化会触发getCalendar,而mounted中初始化currentMonth也会触发orgInfo的watch(因为immediate:true)?实际上,在mounted中设置currentMonth,然后触发changeDate,然后调用getCalendar,而getCalendar中使用了orgInfo.orgCode。所以,在mounted时,orgInfo是否已经传入?如果orgInfo初始为空,可能会出错。
改进:在mounted中,我们只设置currentMonth,然后调用getCalendar(如果orgInfo.orgCode存在的话)。或者,我们可以在watch中添加currentMonth的监听,当月份变化时也调用getCalendar,这样就不需要在changeDate中手动调用getCalendar了。
但是,目前changeDate方法中调用了getCalendar。我们可以将月份变化和orgInfo变化都作为触发获取日历数据的因素。我们可以使用watch来监听这两个变量,然后调用getCalendar。这样,代码会更清晰。
修改建议:
- 移除changeDate方法中调用getCalendar的代码,改为在watch中监听currentMonth和orgInfo,当它们变化时,调用getCalendar。
watch: {
orgInfo: {
handler() {
this.getCalendar();
},
deep: true,
immediate: true
},
currentMonth: {
handler() {
this.getCalendar();
}
}
}
- 在mounted中,只设置currentMonth,因为currentMonth的变化会被watch捕获,从而调用getCalendar。
这样,changeDate方法就只负责更新currentMonth(和currentDay):
changeDate (date) {
let checkDate = date || undefined
if (moment().format("YYYY-MM") === moment(checkDate).format("YYYY-MM")) {
this.currentDay = moment().format("YYYY-MM-DD")
} else {
this.currentDay = ""
}
this.currentMonth = checkDate
// 这里不需要再调用getCalendar,因为watch会捕获currentMonth的变化
}
19. 在getCalendar方法中,我们需要检查orgInfo.orgCode是否存在,如果不存在则不请求。
20. 在getCalendar方法中,我们使用了moment(today).date(days)来生成日期,注意days是当月的第几天,这样生成日期是有效的。但是,如果currentMonth为空,则使用当前月份,但代码中使用了`params.date = this.currentMonth || moment().format("YYYY-MM")`,所以在请求时是有值的。
21. 在模板中,我们使用了`v-if="date"`来判断,但是在生成日历时,空白格子我们改为{ empty: true },所以date总是存在的(非空)。因此,我们可以将`v-if="date"`去掉,改为判断`!date.empty`。
22. 在模板中,日期格子的点击事件:`@click="openModel(date)"`,现在空白格子也有date对象({empty:true}),所以我们需要在openModel中判断:
if (date.empty) return;
if (!date.abnormalCount) {
this.$message.warning(...);
return;
}
// 否则打开弹窗
或者,在模板中,空白格子不绑定点击事件:
<div v-if="!date.empty" @click="openModel(date)"> ... </div>
但是,这样会导致模板结构变化,可能影响样式。我们可以在绑定事件时判断:`@click="!date.empty && openModel(date)"`,但这样如果date.empty,则不会触发。
23. 在getCalendar方法中,生成日历后,我们并没有立即更新calendar,而是等待异步请求完成后才更新。这样,用户会先看到日历,再看到异常标记。这是可以的。但是,如果请求失败,用户将看不到异常标记。
24. 关于异常数据请求:在getCalendar方法中,我们使用params.orgCode,这个orgCode来自orgInfo.orgCode。如果orgInfo.orgCode不存在,我们应该避免请求。
修改后的getCalendar方法:
async getCalendar() {
// 如果orgInfo.orgCode不存在,则不请求
if (!this.orgInfo.orgCode) {
return;
}
// 生成日历(不依赖请求结果)
const today = moment(this.currentMonth || moment().format("YYYY-MM"));
// ... 生成日历数组(calendar)
this.calendar = calendar; // 先更新日历显示
try {
const params = { ... };
const res = await factory.getMeterAbnormalCalendarList(params);
if (res.success) {
// 将异常数据转换为映射
const errMap = {};
(res.data || []).forEach(item => {
errMap[item.date] = item.abnormalCount;
});
// 遍历calendar,更新每个日期对象的abnormalCount
this.calendar = this.calendar.map(week => {
return week.map(dateObj => {
if (!dateObj.empty) {
// 如果该日期在errMap中存在,则设置abnormalCount,否则设为0(或者不设置,但模板中判断abnormalCount>0)
// 这里我们设为undefined表示没有异常,或者设为0,然后在模板中判断>0
// 建议:设置为0,这样我们可以统一使用dateObj.abnormalCount,避免undefined
const count = errMap[dateObj.date] || 0;
return { ...dateObj, abnormalCount: count };
}
return dateObj; // 空白格子不变
});
});
// 不需要forceUpdate,因为我们已经更新了calendar(响应式)
} else {
// 处理错误
}
} catch (error) {
// 处理异常
}
}
这样,在模板中,我们可以直接使用dateObj.abnormalCount,并且当abnormalCount>0时显示。
25. 在模板中,显示异常数量:`{{ $t('异常') }}:{{ date.abnormalCount }}`,注意abnormalCount可能为0,所以用v-if="date.abnormalCount>0"来显示。
26. 在openModel方法中,我们使用`this.$refs.errorList.showModal(date, this.orgInfo)`,这里传递了date和orgInfo。注意,date是当前点击的日期对象,其中包含date.date(日期字符串)和date.abnormalCount。在errorList组件中,可能需要日期字符串和组织信息。
27. 在mounted中,我们设置月份选择器的输入框为只读,现在改为使用属性`:inputReadOnly="true"`,这样更安全。
28. 关于daysOfWeek的国际化:改为计算属性,这样当语言切换时会更新。
computed: {
daysOfWeek() {
return [this.$t('一'), this.$t('二'), this.$t('三'), this.$t('四'), this.$t('五'), this.$t('六'), this.$t('日')];
}
}
29. 在模板中,我们使用`v-for="(day, index) in daysOfWeek"`,key使用index是可以的,因为顺序固定。
30. 在模板中,日期格子的key:使用`weekIndex`和`dateIndex`组合,确保唯一。
31. 在生成日历时,我们使用了`moment(today).date(days)`,注意days从1开始,然后使用format("DD")得到两位数的日期(不足两位补零)。这样显示是两位,比如“01”。但是,在有些设计中,可能不需要前导零。所以,我们可以使用format("D")来得到没有前导零的日期。但是,目前模板中显示的是{{ date.day }},所以如果希望显示没有前导零,可以在这里改。
32. 在getCalendar方法中,我们使用`moment(today).date(days).format("YYYY-MM-DD")`来生成date,这个格式是固定的。
33. 在changeDate方法中,我们使用`moment(checkDate).format("YYYY-MM")`来比较,如果checkDate是undefined,moment(undefined)会返回当前时间,所以需要处理。实际上,a-month-picker的change事件返回的date是选中的moment对象(因为我们设置了valueFormat为字符串,所以返回的是字符串)。所以,checkDate是字符串(YYYY-MM)或undefined。当清空时(但allowClear为false,所以不会清空),所以checkDate不会是undefined。
34. 在changeDate方法中,我们设置this.currentMonth = checkDate,而checkDate就是字符串(YYYY-MM),所以直接使用。
35. 在mounted中,初始化currentMonth:`this.currentMonth = moment().format("YYYY-MM")`,注意这里使用了moment(),然后格式化为字符串。
36. 在模板中,月份选择器:v-model绑定currentMonth(字符串),这样初始化后显示当前月份。
37. 在getCalendar方法中,我们使用`this.currentMonth || moment().format("YYYY-MM")`作为参数,所以如果currentMonth为空,则使用当前月份。
38. 在生成日历时,我们使用了`const startMonth = moment(today).startOf('month');`,这里today是当前月份(YYYY-MM)的moment对象,然后startOf('month')是当月的第一天。
39. 关于空白格子的生成:我们使用{ empty: true },然后在模板中根据empty属性来跳过渲染内容。这样,空白格子就是一个占位的div,但没有任何内容。
40. 在样式部分,使用了单独的less文件,这里不评审样式。
由于修改点较多,下面我们给出修改后的代码示例。
注意:由于时间关系,我们只给出主要修改点,并尽量保持原有结构。
修改后的代码:</think>以下是对日历视图组件的代码评审,包括问题分析、改进建议和优化代码:
### 代码评审总结
#### 主要问题:
1. **DOM操作问题**:直接操作Ant Design组件DOM(`document.querySelector`)可能导致兼容性问题
2. **性能问题**:`errDates.includes()`和`getAbnormalCount()`在渲染时频繁遍历数组
3. **空值处理**:未处理空白日期格子的点击事件
4. **响应式问题**:使用`$forceUpdate()`强制刷新,非Vue推荐做法
5. **代码组织**:日历生成逻辑与数据获取逻辑耦合
6. **Moment过时**:使用已停止维护的moment.js
---
### 改进方案
#### 1. 移除直接DOM操作(Ant Design兼容性问题)
```javascript
// 移除mounted中的DOM操作
mounted() {
// 删除这行
// document.querySelector(".standard-page .ant-calendar-picker-input").readOnly = true
this.currentMonth = moment().format("YYYY-MM")
}
```
#### 2. 性能优化 - 使用映射替代数组遍历
```javascript
// data中新增
data() {
return {
abnormalMap: {} // 异常数据映射 {日期: 数量}
}
}
// 修改获取日历数据方法
async getCalendar() {
// ...原有日历生成逻辑...
const res = await factory.getMeterAbnormalCalendarList(params)
if (res.success) {
// 创建快速查询映射
const newMap = {}
res.data.forEach(item => {
newMap[item.date] = item.abnormalCount
})
this.abnormalMap = newMap
// 移除以下两行
// this.errDateArr = res.data || []
// this.errDates = res.data.map(item => item.date) || []
}
}
```
#### 3. 模板优化 - 使用映射提高性能
```html
<!-- 修改模板中的判断逻辑 -->
<div :class="[
'date-td',
{'currentDay': date && currentDay === date.date},
{'errDay': date && abnormalMap[date.date]}
]">
<!-- ... -->
<div v-if="date && abnormalMap[date.date]" class="bottom-err">
{{ $t('异常') }}:{{ abnormalMap[date.date] }}
</div>
</div>
```
#### 4. 空白日期处理
```javascript
// 修改openModel方法
openModel(date) {
if (!date || typeof date !== 'object') return // 处理空白格子
const count = this.abnormalMap[date.date]
if (count > 0) {
this.$refs.errorList.showModal(date, this.orgInfo)
} else {
this.$message.warning(this.$t("当前日期没有异常值"))
}
}
```
#### 5. 移除$forceUpdate
```javascript
// 删除getCalendar方法中的
// this.$forceUpdate()
```
#### 6. 使用day.js替代moment.js(推荐)
```bash
# 安装替代库
npm install dayjs
```
```javascript
// 替换moment
import dayjs from 'dayjs'
// 修改所有moment调用
mounted() {
this.currentMonth = dayjs().format('YYYY-MM')
}
changeDate(date) {
if (dayjs().format('YYYY-MM') === dayjs(date).format('YYYY-MM')) {
this.currentDay = dayjs().format('YYYY-MM-DD')
} else {
this.currentDay = ""
}
// ...
}
```
#### 7. 拆分日历生成逻辑
```javascript
// 新增方法
generateCalendar(month) {
const today = dayjs(month)
const startMonth = today.startOf('month')
const endMonth = today.endOf('month')
const startWeek = startMonth.isoWeekday()
const dayMonth = endMonth.date()
let calendar = []
let week = []
// ...生成逻辑(同原getCalendar中的逻辑)...
return calendar
}
// 修改getCalendar
async getCalendar() {
const month = this.currentMonth || dayjs().format('YYYY-MM')
this.calendar = this.generateCalendar(month)
// ...数据获取逻辑...
}
```
#### 8. 添加请求错误处理
```javascript
async getCalendar() {
try {
// ...请求代码...
} catch (error) {
console.error('获取日历数据失败:', error)
this.$message.error(this.$t('数据加载失败'))
}
}
```
---
### 完整优化代码
```vue
<template>
<div class="calendar-view">
<!-- 月份选择器 -->
<div class="date-content">
<a-month-picker
v-model="currentMonth"
:placeholder="$t('请选择月份')"
valueFormat="YYYY-MM"
@change="changeDate"
:allowClear="false"
:inputReadOnly="true" />
</div>
<!-- 日历表格 -->
<div class="calendar-table">
<div class="calendar-title">
<span v-for="(day, index) in daysOfWeek" :key="index">{{ day }}</span>
</div>
<div class="calendar-content">
<div class="date-tr" v-for="(week, weekIndex) in calendar" :key="weekIndex">
<div
v-for="(date, dateIndex) in week"
:key="`${weekIndex}-${dateIndex}`"
:class="[
'date-td',
{
'currentDay': date && currentDay === date.date,
'errDay': date && abnormalMap[date.date]
}
]"
@click="openModel(date)">
<div class="top-title" v-if="date">
<span>{{ date.day }}</span>
<span v-if="currentDay === date.date">{{ $t('今日') }}</span>
</div>
<div
v-if="date && abnormalMap[date.date]"
class="bottom-err">
{{ $t('异常') }}:{{ abnormalMap[date.date] }}
</div>
</div>
</div>
</div>
</div>
<!-- 异常列表 -->
<error-list ref="errorList"></error-list>
</div>
</template>
<script>
import factory from '../factory';
import errorList from './errorList.vue'
import dayjs from 'dayjs'
export default {
components: { errorList },
data() {
return {
daysOfWeek: [
this.$t('一'), this.$t('二'), this.$t('三'),
this.$t('四'), this.$t('五'), this.$t('六'), this.$t('日')
],
currentMonth: undefined,
currentDay: dayjs().format("YYYY-MM-DD"),
abnormalMap: {}, // 异常数据映射 {日期: 数量}
calendar: [],
}
},
props: {
orgInfo: {
type: Object,
default: () => ({}),
},
},
watch: {
orgInfo: {
handler(val) {
val?.orgCode && this.getCalendar()
},
deep: true,
immediate: true
},
currentMonth() {
this.getCalendar()
}
},
mounted() {
this.currentMonth = dayjs().format("YYYY-MM")
},
methods: {
changeDate(date) {
this.currentDay = dayjs().isSame(date, 'month')
? dayjs().format("YYYY-MM-DD")
: ""
},
generateCalendar(month) {
const today = dayjs(month)
const startMonth = today.startOf('month')
const endMonth = today.endOf('month')
const startWeek = startMonth.isoWeekday()
const dayMonth = endMonth.date()
let calendar = []
let week = []
// 添加空白日期
for (let i = 1; i < startWeek; i++) {
week.push(null)
}
// 添加本月的日期
for (let day = 1; day <= dayMonth; day++) {
const dateObj = {
day: day.toString().padStart(2, '0'),
date: today.date(day).format("YYYY-MM-DD")
}
week.push(dateObj)
if (week.length === 7) {
calendar.push(week)
week = []
}
}
// 补充空白
if (week.length > 0) {
while (week.length < 7) week.push(null)
calendar.push(week)
}
return calendar
},
async getCalendar() {
if (!this.orgInfo.orgCode) return
try {
// 生成日历
this.calendar = this.generateCalendar(this.currentMonth)
// 获取异常数据
const params = {
orgCode: this.orgInfo.orgCode,
date: this.currentMonth,
}
const res = await factory.getMeterAbnormalCalendarList(params)
if (res.success) {
// 创建快速查询映射
const newMap = {}
res.data.forEach(item => {
newMap[item.date] = item.abnormalCount
})
this.abnormalMap = newMap
}
} catch (error) {
console.error('获取日历数据失败:', error)
this.$message.error(this.$t('数据加载失败'))
}
},
openModel(date) {
// 处理空白格子
if (!date || typeof date !== 'object') return
const count = this.abnormalMap[date.date]
if (count > 0) {
this.$refs.errorList.showModal(date, this.orgInfo)
} else {
this.$message.warning(this.$t("当前日期没有异常值"))
}
}
},
}
</script>
```
### 主要优化点说明:
1. **性能优化**:
- 使用`abnormalMap`对象映射替代数组遍历(O(1)查询)
- 避免在模板中调用方法(减少计算开销)
- 使用复合键名确保v-for键值唯一性
2. **健壮性提升**:
- 添加空值检查(空白日期格子)
- 增加请求错误处理
- 修复orgInfo为空时的边界情况
3. **代码结构优化**:
- 拆分日历生成逻辑为独立方法
- 使用watch监听currentMonth变化
- 移除$forceUpdate强制刷新
4. **现代库替代**:
- 使用day.js替代moment.js(体积小70%)
- 直接使用Ant Design的inputReadOnly属性
5. **可维护性**:
- 移除DOM直接操作
- 简化条件判断逻辑
- 添加完整错误处理