import * as modules from './moduels'
export default createStore({
modules: modules,
经过验证(找到正确的写法时才得到验证,一开始还以为 dispatch 的语法改了) ,组件中的调用方式没有问题,与 vuex 3.x 的语法一致,都是 store.dispatch('moduleA/someAction')
。
尝试过将模块取消,直接放在顶层的 index.js 中,也就是不通过模块直接调用,验证通过。说明是导入 store 的模块时发生异常。
尝试对照官方示例,但并没有说明如何多层级导出模块。
然后搜索他人的示例,发现有一种写法的导出语句有差别。他是先定义一个变量,然后 export default 这个变量。因此我尝试着修改 modules/index.js 的导出方式。
实际原因分析看这里:
根本原因是将解构语法与 JS 模块的导入语法混淆了。JS 在批量导出时可以使用类似对象的花括号,内部包含一组导出值的形式。
由于记忆不到位,错误地将这里的导出语法认知为导出对象,而其中的每个导出值认知为对象的属性;然后理所应当地认为——导入语句中的花括号就是解构语法。虽然看起来很像,但对于 JS 解析器,这完全是两种毫不相干的语法。
理解了以上的原因后,解决起来就很容易了。需要选择性导入时,导出的模块最好选用批量导出语法而不是默认语法,因为默认语法只能导入为一个值,并不支持解构获取部分值。
以下是之前的分析思路,虽然能解决问题,但其实问题的原因与这里的分析不同。
之前是 `export default {...}`,经过修改和尝试,以下两种写法均可正常导出:
- 默认导出一个变量,而不是一组变量的列表
```javascript
// modules/index.js
const modules = {...};
export default modules
// index.js
import modules from './modules
- 不使用默认导出,只导出具名变量
```javascript
// modules/index.js
export const modules = {...}
// index.js
import {modules} from './modules
不过 moduleA 和 moduleB 不一定要按照上面的写法,原先的 export default 也能正常工作。这里的问题主要是出在中间的再导出文件。
给 el-drop-menu-item
添加 active
类,表示当前选中项。尝试了给 el-dropdown
、el-drop-menu
添加自定义类名,但样式都无法生效。
el-drop-menu
以及内部所有组件并不是挂载到 #app
根元素下,而是挂载到一个 id 为 el-popper-container-1992
的元素下。由于未指定 popper-options
,刚开始添加的 css 类的层级结构,与视图的实际结构并不一致,导致浏览器将其识别为不存在。
视图结构:
查询官方文档可知,el-dropdown
组件有一个 popper-class
的 prop ,用于指定弹出层的类名。同时,由于弹出层 el-popper-container
与 #app
元素是兄弟元素,因此,样式表中需要将 dropdown menu 和 item 相关的样式定义在顶层。
解决刷新后全局状态丢失的问题,其中包含了 i18n 的相关变量,但无法处理“ i18n 模块加载” 与 “ localStorage 的数据复原到状态库” 之间的冲突。
总体思路并不复杂:
构建项目的 i18n
模块,创建相关文件,添加初始化相关语句,完成后导入到 main.js
中
在导航组件中使用 $t()
获取项目 i18n
模块的配置文本;并给菜单项添加事件侦听器,将选中的语言保存到全局状态库
在 App.vue
中侦听全局状态库的语言状态,并同步给项目 i18n
模块,实现自动适配语言
一开始是将两个动作都放在 App.vue
中执行:
第一次加载或刷新后重新加载时,将本地 localStorage 备份的状态还原到全局状态库
添加侦听事件 beforeunload
,在刷新开始前,将全局状态库备份到 localStorage
这种方式的问题在于,每次刷新时,都会重新加载资源并重新生成视图,此时,项目的 i18n
模块也会重新执行。而我在 i18m
模块中添加了 locale
的初始化语句(会根据状态库的语言是否存在,来决定是否需要执行初始化) ,因此,每次刷新页面时,初始化语句都会再次执行,导致本地备份被还原后,又被这里的初始化给覆盖了。
因此,将备份与还原的代码单独成模块,然后分别导入到 App.vue
和 i18n
模块,并在主模块中 i18n
模块初始化前,先执行还原动作。
需求是给子元素设置图片或者背景颜色,并填满父元素的空间。
父元素设置了圆角,子元素未设置。此时子元素与父元素之间会有空隙。
一开始找了很多方法,但始终会有一些空隙。后来想到,图片或背景色可以直接作用于父元素作为 background 的值,直接从源头解决了问题。
至于子元素想要占满父元素空间不留任何空隙,目前为止尚未找到绝对有效的解决方案。理论上虽然将子元素放大后通过绝对定位就能覆盖父元素,但实际渲染后还是有可能留下空隙,具体原因未知。
类似的问题
类似地,圆角还会产生另一个更加常见的问题:父元素与子元素同时设置圆角,中间也会产生空隙。
不过这里的问题比较好理解,生成圆角时,即使是最细的 1px 边框,也是存在外边缘和内边缘的,在放大后可以更加明显地看到。而内外边缘的曲线弧度是不一样的,也因此导致了父元素与子元素设置相同大小的圆角时无法贴合(可以看做将同一个元素圆角边框的内外边缘贴到一起)。
为了更加简单地理解这个问题,可以想象一下,将边框的宽度缩小到某个临界值,此时外边缘仍然是存在的,但由于两条边框过于靠近,内边缘极限接近于直角。
el-scrollbar
滚动条无法显示。
我的需求是做一个标签组,通过 flex
实现水平布局;同时由于 flex
内元素的宽度非固定,而是浏览器根据 el-tag
的文字内容和字号经过计算得到,因此,等间距使用 margin
实现。
对比官方示例与我自己的页面,发现了一个较大的区别:
官方示例中的 flex
元素不论垂直还是水平排列,都是固定尺寸的,比如水平排列时固定了其宽度
在我的页面中, el-tag
作为 flex item
并不是固定宽度,而是由内部的文字撑开
打开控制台,可以看到 el-scrollbar
的结构如下:
第一个元素之下的 tag-group
是我传入组件插槽的实际内容;第二个元素代表水平滚动条;第三个元素代表垂直滚动条。由于我的需求是水平布局,因此只需要关注前两个元素。
根据 el-scrollbar
源码的逻辑,组件初始化时,需要先确定作为容器的 el-scrollbar
的宽度(offsetWidth),以及作为内容的 tag-group
的宽度;而此时由于 el-tag
宽度不确定,作为 flex
容器的元素的宽度也就无法确定。
el-scrollbar
组件的原理介绍可以参考这篇文章:https://www.qiyuandi.com/zhanzhang/zonghe/13085.html
大致原理是先获取容器以及内容元素的总宽度,确定是否需要显示滚动条及其初始位置;并在触发滚动事件时,重新计算滚动条位置并更新。而通过控制台可以看到,代表滚动条的第二个元素,其初始宽度为 auto
,当鼠标悬停在元素之上,才会将宽度置为父元素宽度(这里的计算值是 124px),同时样式中的 display: none
被取消。
根据以上分析,重点就是如何在不修改原有的计算逻辑基础上(el-tag
根据内容自动计算宽度,而非固定宽度) ,让组件或元素得到固定的宽度(重点是让 flex
容器具有固定的宽度)。
一开始考虑的是根据 el-tag
的内外边距加上文字内容的长度来计算出单个标签的总宽度,并作为 css 变量传入。但这种方式会产生大量计算,而且样式表也需要根据 css 变量一一匹配,带来额外的维护成本。
原本打算先放弃 el-scrollbar
,使用浏览器默认的滚动条搭配 ::-webkit-scrollbar
修改其样式。不过后来在解决 “问题 8” 的时候发现一个 width 属性的新值 max-content
同时解决了这个问题,也就是:
给作为 flex
容器的元素添加 width: max-content
这条属性,可以同时解决问题 7 和 8。设置该属性值后,元素的宽/高始终都是其内容的总宽/高,且行内元素会忽略换行效果。
PS: 此方法只适合不换行的 flex 固定行/列布局。若项目中需要支持 flex-wrap: wrap
时,请向 el-scrollbar
组件的 height
prop 传递,或在样式表中指定 height: ***
,使得 el-scrollbar_wrap
元素能获取到正确的宽度/高度。
PPS:经过验证,若 el-scrollbar
的父元素具有固定宽/高度,此时可以将 el-scrollbar
设置为 width: 100%
或 height: 100%
;否则,还是需要设置为固定数值。
问题描述及分析
同问题 7, Flex 布局中,最后一个子元素的右侧边距缺失(不论是设置元素外边距或是设置容器的内边距都无效)。产生原因与问题 7 相同,也是容器宽度值/高度值缺失,导致某些尺寸值的计算错误。此处的表现就是最后一个元素与容器的直接相连,边距效果丢失。
解决方法与问题 7 相同,给 Flex 容器添加 width: max-content
即可。注意,问题 8 并不能通过给 el-scrollbar
设置 width
属性解决,因为此处有问题的元素是我们自定义的 group 元素,而不是 el-scrollbar
组件内部的元素。
问题 9 (完成但未解决)
定义通用的 css 变量并导入到页面模块,变量值未生效。在浏览器中查看,sass 编译正常,但显示变量未定义。
下面是有问题的代码:
vars.scss:
:root {
--border-width: 1px;
--border-color: #333;
--border-base: var(--border-width) solid var(--border-color, #dcdfe6);
--card-padding-base: 20px;
--card-padding-small: calc(var(--card-padding-base) / 2);
@use '../../vars.scss' as *;
.tag-picker {
--card-padding-small: calc(var(--el-card-padding) / 2); // 如果不在文件内部定义的话就用不了
.el-card__header,
.el-card__body {
padding: var(--card-padding-small);
更深层次嵌套
.tag-group {
.tag {
.el-radio-button__inner {
border-left: var(--border-base); // 同个外部文件定义的,这里就能用
原因可能是 @use 规则并没有处理通过 :root 规则声明的 css 变量,导致模块中获取不到全局定义的变量。因此,可以将 @use 这里改为原生的 @import 指令,这是浏览器原生支持的 css 模块导入指令。
其他解决方法:
一种就是像上面这样在单个文件内定义,但这对于项目内通用的全局变量来说很不方便
或者可以换成 sass 变量,也就是 $card-padding-small: calc(var(--el-card-padding) / 2);
,sass 变量不需要定义通过 :root 规则使用,直接定义后就能作为全局变量导入到任意 css 模块
问题 10
问题描述与分析
开发多标签组选中:使用组件 el-radio-group
作为容器 tag-group
,el-radio
作为元素 tag
。
原先的设计是在业务组件中添加 radio-group 和 radio 组件,然后绑定一个本地的 ref
对象。但考虑到需要手动同步到状态库比较繁琐,想要调整为计算属性,直接从状态库获取值。但没有考虑到 el-radio-group
组件内部会直接修改所绑定的计算属性对象,而这会导致 vue 发出以下警告:
Write operation failed: computed value is readonly
因此,还是需要改回原先的模式,即:若数据待绑定的目标组件会修改所绑定的对象的值时,应当由业务组件负责数据的初始化,在业务组件内修改数据并将修改同步到状态库;状态库只负责保存数据副本,以便其他组件获取。同理,若某些公共数据需要由状态库负责维护时,业务组件就不能存在直接修改公共数据的逻辑。
<>PS:这里是后期回看时的补充:这里出现报错的原因可能是想通过 v-model 绑定一个计算属性,而计算属性又没有设置 setter 导致的报错,因此,解决方法也很简单:
可以将 v-model 指令改为 b-ding(或 :),并通过自定义事件接收更新的选中项
或者为计算属性设置一个 setter,允许修改计算属性
问题 11
针对多个 el-radio-group
,统计当前选中的标签数,且仅在选中新标签组时计数,也就是若某个标签组已有选中项,想要更换选中项时,不能计数。
一开始考虑的是直接判断绑定的数据源,判定当前选中的标签组(数组中的元素值)是否为空,若为空则计数值自增,否则忽略。但尝试了多种方式都无法准确判定:
直接在本地的数据源上判断(失败,每次都只能得到 radio 选中项的值,也就是选中之后的新值,无法获取旧值)
在状态库的数据副本上进行判定,因为考虑到本地与状态库之间的数据同步是手动执行的,可以在同步动作前先判定(失败,即使在同步动作之前先获取值,结果还是与方法 1 相同)
尝试给以上操作添加一层 Promise,将同步动作放在判定并自增完成后的 then 方法中(还是失败)
原因很简单,因为我使用了 v-model
绑定 el-radio-group
,其内部 change
事件的触发要晚于内部 value
属性的赋值,因此通过 @change 无法获取到旧值,也就无法判定新旧值是否发生变化。
因此,可以自己维护一个数组,数组用于保存各 el-radio-group
的选中状态;或者不通过 v-model
而是改用 v-bind
。(这个问题跟问题 10 类似,都是因为滥用了 v-model 但又没注意双向绑定需要考虑的执行顺序问题,导致各种操作无法在合适的时机执行)
原先的分析:
方法 1 的问题好分析,应该是因为数据源的修改早于 `@change` 事件,所以不论我如何修改事件处理函数的逻辑,都不可早于数据源被修改。但方法 2 和 3 还是失败却找不到原因,我这里是手动执行本地向状态库的数据同步的,并没有将状态库走位数据源,但还是无法获取数据修改前的旧值。
最后的解决方案是创建一个单独的列表,保存各单选组是否有被选中过。由于这里是完全由自己控制的同步过程,因此可以保证判定的有效性。
至于其他几种方式不成功,可能是其中还有一些没注意到的机制在起作用。
问题 12
部分组件内部数据需要在刷新前保存,但又不需要升级为公共状态,因此考虑直接通过 localStorage 存储。原本是通过 onBeforeMount
钩子将数据传递给 localStorage
。
以下是基本思路:
在组件外部为该组件创建单独的与 localStorage
交互的模块,包括 get/set/remove 三种接口,并导入当前组件。
在组件被销毁前或页面刷新前,将数据通过 1 定义的接口保存
在组件重新加载或页面重新加载时,将 localStorage
的备份数据恢复到组件中
需求本身比较简单,但有几点需要注意:
组件的挂载与销毁属于 vue 组件的生命周期,而页面刷新属于浏览器的事件,因此需要分别执行备份与恢复
import { recoverFromLocal, backup2Local } from '@utils/backupState.js'
const init = () => {
const backup = recoverFromLocal();
if (backup) {
... // 将 backup 对象中的数据依次赋予当前组件内的状态变量
} else resetPicked();
const backupState = () =>
backup2Local({
... // 将组件内的状态变量作为对象的属性,并将变量传给接口函数
onMounted(() => {
init(); // 在初始化函数中恢复本地备份的数据
// 页面刷新事件 beforeunload 与 组件销毁钩子 onMounted 并不是一起触发的,需要单独添加备份方法
window.addEventListener('beforeunload', backupState);
onBeforeUnmount(() => {
backupState();
window.removeEventListener('beforeunload', backupState);
backupState.js
import { getItem, setItem, removeItem } from './utils' // 这里是将 localStorage.getItem 等方法,替换成 getItem = (key)=> localStorage.getItem(key) 的形式
const backupKey = 'backupComponentState'; // 不同的组件可以使用单独的 key 来保存,避免冲突
const getBackupLocal = (backupKey) => {
const backup = getBackupLocal(backupKey);
// 避免某些场景下未获取到需要保存的数据就调用 setBackupLocal 方法,导致值为 `undefined` 字符串,这会导致 JSON.parse 函数解析报错
if (backup) {
store.replaceState(Object.assign({}, store.state, backup));
const setBackupLocal = (backupKey, datas) => {
setItem(backupKey, JSON.stringify(datas));
const removeBackupLocal = (backupKey) => removeItem(backupKey);
export { getBackupLocal, setBackupLocal, removeBackupLocal }
由于我是需要直接使用备份数据覆盖组件的初始状态,使用了 ref
定义数组和对象类型的状态变量(若是 reactive
定义的变量则不能直接覆盖,只能操作数组或对象的属性,操作起来较为繁琐,因此此处使用 ref
) ,调用备份接口时只需要将 变量.value
作为参数即可,不需要将整个响应式对象都保存下来
问题 13
问题描述与分析
给侧边栏菜单嵌套一个 el-scrollbar
,并根据侧边栏总高度和内部各元素的高度,计算菜单限高,使得侧边栏固定,但菜单可滚动。
使用 calc()
计算后发现菜单实际高度比预期多了一点,检查发现是由于侧边栏中的小标题使用了 font-weight: bold
(这是浏览器的默认样式表带来的),因此实际渲染的文字是 font-size
的 1.3 倍(谷歌与火狐浏览器都是 1.3) 。
调整倍数后,侧边栏显示正常。
问题 14
问题描述与分析
封装一个同时用于展示信息、新增项目、修改信息的内嵌表单的公共对话框组件,但底部插入的按钮无法正确关闭对话框。
检查后发现,父组件开启对话框是正常的,也就是父组件向子组件传值这个环节正常;而且子组件内部修改对话框的显示变量也是正常。因此,猜测是子组件向父组件回传新值是出现问题。
重新检查了父组件和子组件的相关代码,发现父组件绑定值的语法写错了,正确的应该是 v-model:isDialogVisible=isDialogVisible
,我把中间的 :isDialogVisible
给漏了,导致子组件内部的自定义事件与预期不同名。
vue3 实现双向绑定要求 emit
触发的自定义事件必须是 update:
前缀加上同名的 prop
(也就是此处的 isDialogVisible
) 。因此,子组件内部想要正确地触发自定义事件应该这样写:
<template>
<!-- 此处省去其他属性和内部细节 -->
<el-dialog v-model="visible">
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="closeDialog">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
dialogVisible: Boolean,
const emit = defineEmits(['update:dialogVisible']);
const visible = computed({
get() {
return props.dialogVisible;
set(value) {
console.log('value', value);
emit('update:dialogVisible', value);
const closeDialog = () => {
visible.value = false;
const handleCloseDialog = (done) => {
done();
<!-- 正确写法 -->
<CommonForm v-model:dialogVisible="dialogVisible" />
<!-- 错误写法 -->
<CommonForm v-model="dialogVisible" />
PS:若组件的值与表单组件类似,是表示当前组件的某种选中、输入等状态,可以直接定义一个名为 value 的 prop,并通过自定义的 input 事件向父组件回传,这样就能直接使用 v-model 指令而不需要后面的 :isDialogVisible 这样的后缀的。不过此处的 isDialogVisible 是表示对话框的显示状态,而不是输入或选中状态,因此不适合用 value 这个命名作为代替。
问题 15
问题描述与分析
表单验证环节,如果在保持打开控制台的同时运行验证后失败,会导致运行中断,并报错 Error: Async Validation Error 。
看了 el-form 的源码后,发现虽然有捕获错误,但并没有默认的处理过程,而是将错误继续向外抛出,因此开发者必须自行提供错误处理的回调。当然,只要不要控制台,对于用户而言是没有影响的,不过保险起见,最好要有完整的错误处理链。
问题 16
问题描述与分析
结合 elemnt-plus 中 tooltip
与 popover
官方示例的虚拟触发示例,给左侧垂直菜单项添加一个“单例模式”下的右键菜单(因为在右侧,表现形式会类似二级菜单) 。同时,为了解决官方示例的 TIP “已知问题:当使用单例模式时,tooltip 的触发元素发生改变的时候可能会发生弹跳”,不采用示例中的 v-model:visible 属性,而是通过 v-show 指令自行控制菜单的显示与隐藏。
若此时直接在 el-popover
上添加 v-show 指令会提示:
[Vue warn]: Runtime directive used on component with non-element root node. The directives will not function as intended.
测试过将 popover 放在其他元素内部,也就是不将其作为当前业务组件的根节点,但仍然存在以上提示。
因此,需要给 popover 组件添加一个父元素 div ,然后将 v-show 指令绑定在父元素上,这样就能避免闪烁,如下所示:
<div v-show="isShowCtxMenu">
<el-popover>
</el-popover>
问题 17
问题描述与分析
给 popover
组件添加自定义样式失败,检查元素发现 class 并没有添加成功。查看文档,发现 poper
类的组件(也就是带弹出功能)有一个 popper-class
属性,用于给当前的 popper
组件添加自定义 class,如果像我一开始那样直接添加 class 属性就会发现没有添加成功。
同时要注意,popover
组件内,根元素的样式选择器是 '.el-poper.el-popover',添加自定义样式时需要让选择器的权重大于内置样式选择器。
之后,点击菜单项时,发现菜单并没有隐藏。参考官方示例,在点击事件中手动隐藏菜单 unref(ctxMenuRef).popperRef?.delayHide?.()
并没有生效。搜索后发现了一种解决方法 ctxMenuRef.value.hide()
就能关闭。
问题 17
问题描述与分析
在 vue3 的 setup 中使用 dexie.js 操作 indexedDB,调用 table.put()
新增数据时,提示以下信息:
DataCloneError: Failed to execute 'add' on 'IDBObjectStore': # could not be cloned. DataCloneError: Failed to execute 'add' on 'IDBObjectStore': # could not be cloned. Error: Failed to execute 'add' on 'IDBObjectStore': # could no
分析后发现,传给 put()
方法的参数并不是原生 js 对象,因此数据库判定该对象不是数据库声明中定义的序列化对象。因此,需要先通过 vue 的 toRaw()
方法获取原生对象,再将原生对象作为参数。
问题 18
问题描述与分析
由于 indexedDB 的机制,单纯声明数据库是不会创建的,必须在首次调用表格方法前,创建一次与该数据库的连接;若是跳过这第一次连接直接调用表格的操作方法如 add/put,则会提示表格对象是 undefined
。原因是此时 db
对象不存在,自然地,所有的 db.table
对象都不存在。
因此,不论是操作原生 indexedDB 接口或是使用类似 dexie.js 的工具库,都需要在初始进入页面时,手动创建一次空的连接(没尝试的过其他 indexedDB 的库,所以不确定是否存在这样的工具库可以省略这一步自动创建好数据库)。Dexie 的操作方式是调用已声明的 db = new Dexie(databaseName)
对象上的 transaction 方法,创建一次空的事务:db.transaction('r', db.table)
,这里的 table 可以是任意一张表。
注意,由于 transaction
方法的参数规则,前两个参数必须提供,但第三个参数(也就是回调函数)可以不传。回调函数未传时控制台可能会提示以下信息,但数据库是可以正常创建的:
Unhandled rejection: InvalidAccessError: Failed to execute 'transaction' on 'IDBDatabase': The storeNames parameter was empty.
InvalidAccessError: Failed to execute 'transaction' on 'IDBDatabase': The storeNames parameter was empty.
Error: Failed to execute 'transaction' on 'IDBDatabase': The storeNames parameter was empty.
若想避免警告,可以传入一个空的回调。
问题 19
问题描述与分析
运行项目构建后,通过 live server 插件打开,提示找不到 js 和 css 文件:
Refused to apply style from * because its MIME type ('text/html') is not a supported stylesheet
原因是 live server 插件默认按照当前打开的文件夹为根路径,也就是整个项目的文件夹根路径,而非 /dist
目录。因此,只需打开 /dist
文件夹再启动 live server 即可。
问题 20
问题描述与分析
将 i18n 的 messages 的 key 限定为只能从我定义的语言中选择:
i18n/index.ts
import type { Language } from 'element-plus/es/locale'
enum LANGS = { 'zh-CN', 'en' }
export declare type ElLangs = Record<LANGS, Language>
const elLocalLangs: ElLangs = { [LANGS.zhCN]: el_zhCN, [LANGS.en]: el_en };
App.vue
import { Language } from 'element-plus/es/locale';
setup() {
const currentLang = computed(() => store.state.currentLang); // 语言保存在状态库
const locale = computed(
// 通过数组索引来获取对应的 element i18n 配置对象,方便后续添加更多语言
// 同时,添加类型断言可以避免出现可能的错误提示:
// 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "ElLangs"。
// 在类型 "ElLangs" 上找不到具有类型为 "string" 的参数的索引签名。ts(7053)
() => (elLocalLangs as Record<string, Language>)[currentLang.value]
会出现以下提示:
类型“{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<{} & {} & {}> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, options?: WatchOptions<...> | undefined): WatchStopHandle; } & ... 4 more ... & ComponentC...”上不存在属性“local”
原因是漏掉了 setup() 方法的 return 语句,导致模板在编译时找不到对应的变量。由于之前一直是直接使用语法糖 <script setup>
,更换组件定义方式后没注意到遗漏了。