Vue中的事件修饰符和按键绑定


Vue 事件修饰符与按键绑定 从入门到精通

前言

在原生 JavaScript 开发中,我们处理 DOM 事件时,总要写大量重复代码:比如阻止表单提交刷新页面要写 event.preventDefault(),阻止点击事件向上传递要写 event.stopPropagation(),监听回车提交表单还要判断按键编码……

而 Vue 为我们封装了事件修饰符和按键绑定两大核心能力,用极简的声明式写法,一行代码就能搞定这些复杂逻辑,不用再关心 DOM 事件的底层细节,专注写业务代码就行。

前置基础:2 分钟搞懂核心前提

1. Vue 基础事件绑定

Vue 中我们用 v-on: 指令绑定事件,简写为 @,比如最基础的点击事件:

1
2
3
4
5
6
7
8
9
10
11
<template>
<!-- 点击按钮,触发 handleClick 函数 -->
<button @click="handleClick">点击我</button>
</template>

<script setup>
// 事件处理函数
const handleClick = () => {
console.log("按钮被点击了");
};
</script>

而事件修饰符,就是加在事件名后面的后缀,用来给事件增加额外的规则,格式为 @事件名.修饰符="处理函数",比如 @click.prevent="handleClick"

2. 大白话讲 DOM 事件流

很多人看不懂修饰符,核心是没搞懂 DOM 事件流,这里用最简单的话讲清楚:

DOM 事件流分为 3 个阶段,就像俄罗斯套娃,你点击了最里面的娃娃,事件会经历 3 个过程:

  • 捕获阶段:事件从最外层的html标签,一层一层往里传,直到传到你点击的元素
  • 目标阶段:事件到达你点击的那个元素,触发它的点击事件
  • 冒泡阶段:事件从你点击的元素,一层一层往外传,直到传到最外层的html标签

Vue 默认监听的是冒泡阶段的事件,这也是我们日常开发 99% 的场景。

一、Vue 事件修饰符全解

我们把修饰符分为「常用修饰符」和「进阶实用修饰符」,循序渐进讲解,可以先把前 3 个高频修饰符吃透,再学进阶内容。

1. 常用修饰符(开发天天用)

(1).prevent:阻止事件默认行为

大白话作用:阻止浏览器给元素自带的默认行为,只执行我们自己写的函数逻辑。

最常见的默认行为

  • 点击<a href="xxx">链接,浏览器会自动跳转页面
  • 点击表单的提交按钮,浏览器会自动刷新页面
  • 右键点击页面,浏览器会弹出默认右键菜单

正确示例 & 业务场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<!-- 场景1:a标签自定义跳转,阻止默认页面刷新 -->
<a href="/home" @click.prevent="goHome">跳转到首页</a>

<!-- 场景2:表单提交,阻止默认页面刷新,最常用场景 -->
<form @submit.prevent="submitForm">
<input placeholder="请输入用户名" />
<button type="submit">提交表单</button>
</form>
</template>

<script setup>
const goHome = () => {
// 这里可以写自定义跳转逻辑,比如权限判断、埋点统计
console.log("执行自定义跳转");
};

const submitForm = () => {
// 这里写表单提交逻辑,比如接口请求、校验
console.log("表单已提交");
};
</script>

(2).stop:阻止事件冒泡

大白话作用:让事件只在当前元素触发,不要往外传给父元素,彻底解决嵌套元素的事件冲突。

正确示例 & 业务场景
最典型的场景:弹窗蒙层,点击弹窗内容区域,不要触发蒙层的关闭事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 父元素:蒙层,点击会触发关闭弹窗 -->
<div class="modal-mask" @click="closeModal">
<!-- 子元素:弹窗内容,加.stop阻止冒泡,点击不会触发父元素的关闭事件 -->
<div class="modal-content" @click.stop>
<p>弹窗内容</p>
<button>确认</button>
</div>
</div>
</template>

<script setup>
const closeModal = () => {
console.log("弹窗已关闭");
};
</script>

注意:如果不加.stop,你点击弹窗里的按钮,事件会冒泡到外面的蒙层,直接把弹窗关了,这就是新手最常踩的坑。

(3).once:事件只触发一次

大白话作用:绑定的事件,无论用户操作多少次,只会执行一次,后续操作完全无效。

正确示例 & 业务场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<!-- 场景1:防止表单重复提交,避免用户多次点击发起多次接口请求 -->
<button @click.once="submitOrder">提交订单</button>

<!-- 场景2:页面初始化逻辑,只需要执行一次 -->
<button @click.once="initWelcome">查看新人福利</button>
</template>

<script setup>
const submitOrder = () => {
console.log("订单已提交,不会重复执行");
};

const initWelcome = () => {
console.log("新人福利弹窗,只弹出一次");
};
</script>

2. 进阶实用・修饰符(解决特定场景问题)

(1).capture:开启事件捕获模式

大白话作用:把事件监听从默认的「冒泡阶段」改成「捕获阶段」,让父元素的事件,先于子元素执行。

正确示例 & 业务场景
比如全局事件埋点,需要在用户点击任何元素之前,先统计点击行为,就可以用捕获模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 父元素加.capture,点击按钮时,会先执行handleLog,再执行handleClick -->
<div @click.capture="handleLog">
<button @click="handleClick">点击按钮</button>
</div>
</template>

<script setup>
const handleLog = () => {
console.log("先执行:统计用户点击行为");
};

const handleClick = () => {
console.log("后执行:按钮的点击逻辑");
};
</script>

(2).self:仅点击元素自身时才触发事件

大白话作用:只有你真正点击了这个元素本身,才会触发事件;如果是子元素冒泡上来的事件,完全不响应。

很多人会把它和.stop搞混,后面会专门讲两者的区别,先看正确示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<!-- 父元素加.self,只有点击蒙层的空白区域,才会触发closeModal -->
<div class="modal-mask" @click.self="closeModal">
<!-- 点击子元素弹窗内容,不会触发父元素的事件 -->
<div class="modal-content">
<p>弹窗内容</p>
<button>确认</button>
</div>
</div>
</template>

<script setup>
const closeModal = () => {
console.log("只有点击蒙层空白处,才会关闭弹窗");
};
</script>

(3).passive:优化滚动 / 触摸性能,解决移动端卡顿

大白话作用:提前告诉浏览器「这个事件回调里,绝对不会阻止默认行为」,浏览器不用等回调执行完,直接执行默认的滚动 / 触摸行为,从根源解决移动端滚动卡顿问题。

核心规则 & 避坑

  • .passive.prevent 绝对不能同时使用,否则.prevent会完全失效,浏览器还会抛出警告
  • 核心适用场景:scroll滚动事件、移动端touchstart/touchmove触摸事件
  • 不能用它来阻止默认行为,它的作用就是告诉浏览器「我不阻止默认行为」

正确示例 & 业务场景

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<!-- 移动端长列表,加.passive大幅提升滚动流畅度 -->
<div class="long-list" @scroll.passive="handleScroll">
<div v-for="item in 100" :key="item">第{{ item }}条数据</div>
</div>
</template>

<script setup>
const handleScroll = () => {
// 这里写滚动逻辑,比如触底加载更多、滚动位置统计
console.log("滚动事件执行,不影响页面滚动流畅度");
};
</script>

3. 修饰符的核心使用规则

(1)修饰符可以组合使用,顺序决定最终效果

多个修饰符可以链式写在一起,书写顺序直接决定了事件的执行逻辑,顺序错了,效果会完全不一样,这是很多人高频踩坑点。

举个最典型的例子

1
2
3
4
5
<!-- 写法1:先阻止所有点击的默认行为,再判断是否是自身点击,才执行回调 -->
<div @click.prevent.self="handleClick"></div>

<!-- 写法2:只有点击元素自身时,才阻止默认行为,同时执行回调 -->
<div @click.self.prevent="handleClick"></div>

说明:写法 1,无论你点击元素本身还是子元素,都会阻止默认行为;写法 2,只有点击元素本身,才会阻止默认行为,两者效果天差地别。

推荐写法:按照「执行优先级」从左到右书写,比如先阻止冒泡,再阻止默认行为:@click.stop.prevent="handleClick"

(2).stop 和 .self 核心区别对照表

很多人最容易混淆的两个修饰符,一张表讲清楚,再也不会用错:

修饰符核心作用对事件冒泡的影响适用场景
.stop直接终止事件传播事件完全停止向上冒泡,所有父元素的事件都不会触发嵌套元素的独立事件,比如弹窗里的按钮,完全阻止事件往外传
.self仅过滤非自身触发的事件不会阻止事件冒泡,子元素的事件依然会传给更外层的父元素弹窗蒙层关闭,只响应自身的点击,不影响子元素的事件冒泡

二、Vue 按键绑定与按键别名全解

在开发中,我们经常需要处理键盘事件:比如回车提交表单、ESC 关闭弹窗、方向键切换列表选中项、Ctrl+S 保存文件等。Vue 为常用按键封装了别名,不用记忆复杂的按键编码,一行代码就能搞定键盘事件绑定。

前置说明

键盘事件必须绑定在keyup(按键松开触发)或keydown(按键按下触发)上,推荐优先使用keydown,触发时机更稳定,不会出现焦点丢失导致事件不触发的问题。

1. 基础常用按键别名全解

按键Vue 别名正确示例 & 业务场景说明
回车键enter示例:<input @keydown.enter="submitForm" />
用户按下回车键,立即触发表单提交,是表单开发最常用的按键绑定
删除 / 退格键delete示例:<input @keydown.delete="clearInput" />
同时兼容键盘上的「删除(Delete)」和「退格(Backspace)」键,统一处理内容清空、删除逻辑
ESC 退出键esc示例:<div @keydown.esc="closeModal" v-if="isShowModal">弹窗</div>
按下 ESC 键立即关闭弹窗、下拉框、抽屉等组件,是提升用户体验的必备快捷操作
空格键space示例:<button @keydown.space="togglePlay">播放/暂停</button>
按下空格键触发状态切换,适配播放器、复选框、开关等组件的键盘操作
Tab 制表键tab示例:<input @keydown.tab.prevent="handleTabSwitch" />
⚠️ 重点注意:仅支持keydown事件,必须配合.prevent阻止默认的焦点切换行为,用于表单输入框的自定义焦点跳转、标签页切换逻辑
向上方向键up示例:<ul @keydown.up="selectPrev">列表</ul>
按下向上方向键,切换列表上一个选中项,适配下拉选择器、表格导航、快捷键菜单
向下方向键down示例:<ul @keydown.down="selectNext">列表</ul>
与up对应,按下向下方向键切换下一个选中项,是列表类组件的核心键盘交互
向左方向键left示例:<div @keydown.left="prevImage">轮播图</div>
按下向左方向键,切换上一张图片 / 上一个标签,适配轮播图、标签页、滑块组件
向右方向键right示例:<div @keydown.right="nextImage">轮播图</div>
与left对应,按下向右方向键切换下一个内容,是横向导航类组件的常用快捷键

2. 进阶按键绑定用法

(1)无别名的按键,直接用原生 key 值绑定

Vue 没有提供别名的按键,我们可以直接用按键原生的key值,转为kebab-case(短横线命名) 即可绑定,无需任何额外注册,Vue2 和 Vue3 都支持。

怎么看按键的key值? 打开浏览器控制台,输入这段代码,按下按键就能看到对应的key值:

1
document.addEventListener("keydown", (e) => console.log("按键key值:", e.key));

正确示例

1
2
3
4
5
6
7
8
<template>
<!-- F1帮助键 -->
<input @keydown.f1="showHelp" />
<!-- PageDown翻页键 -->
<div @keydown.page-down="handlePageDown" />
<!-- 小键盘0键 -->
<input @keydown.numpad-0="handleNum0" />
</template>

(2)系统修饰键:实现组合快捷键

系统修饰键包括:ctrlaltshiftmeta(Windows 系统的 Win 键、Mac 系统的 Command 键),用来实现我们常用的组合快捷键,比如 Ctrl+S 保存、Ctrl+Enter 提交等。

使用规则

  • 配合keydown使用(强烈推荐):按下组合键的瞬间立即触发,按住会连续触发,逻辑稳定,是组合键的首选方案
  • 配合keyup使用:必须满足「释放普通按键时,修饰键仍处于按下状态」,事件才会触发;仅按下 / 释放修饰键本身,不会触发事件

正确示例

1
2
3
4
5
6
7
8
<template>
<!-- Ctrl+S 保存文件 -->
<div @keydown.ctrl.s="saveFile">按 Ctrl+S 保存</div>
<!-- Alt+Enter 全屏 -->
<div @keydown.alt.enter="fullScreen">按 Alt+Enter 全屏</div>
<!-- Shift+Ctrl+A 截图 -->
<div @keydown.shift.ctrl.a="handleScreenshot">按 Shift+Ctrl+A 截图</div>
</template>

(3).exact 精确匹配修饰符:解决组合键误触发

写组合键时,90% 会遇到这个问题:写了@keydown.ctrl.enter="submit",结果按下ctrl+shift+enterctrl+alt+enter时,也会误触发事件。

.exact 修饰符就是专门解决这个问题的,它强制要求只有按下指定的修饰键,没有其他任何修饰键按下时,事件才会触发,彻底杜绝误触发。

正确示例 & 对比

1
2
3
4
5
6
7
8
9
10
<template>
<!-- 普通写法:易误触发,按下ctrl+shift+enter也会执行 -->
<input @keydown.ctrl.enter="submitForm" />

<!-- 精确写法:只有仅按下ctrl+enter时,才会执行,无任何误触发 -->
<input @keydown.ctrl.enter.exact="submitForm" />

<!-- 极致精确:只有单独按下回车,没有任何修饰键时,才会触发 -->
<input @keydown.enter.exact="submitForm" />
</template>

(4)自定义按键别名:Vue2 与 Vue3 差异

这里有一个非常重要的版本差异,大家一定要注意,避免写了代码不生效:

Vue2 方案:全局配置自定义按键别名

在项目入口文件main.js中,通过Vue.config.keyCodes全局注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js Vue2 写法
import Vue from "vue";
import App from "./App.vue";

// 全局注册自定义按键别名
Vue.config.keyCodes = {
"num-0": 96, // 小键盘0的键码
"custom-enter": 13,
};

new Vue({
render: (h) => h(App),
}).$mount("#app");

模板中直接使用:

1
<input @keydown.num-0="handleNum0" />

Vue3 方案:已完全废弃该 API

Vue3 已经彻底废弃了Vue.config.keyCodes配置,同时移除了对keyCode修饰符的支持,因为 DOM 标准已经废弃了KeyboardEvent.keyCode属性。

Vue3 替代方案:直接使用上文中的原生key值短横线命名即可,无需任何全局注册,比如<input @keydown.numpad-0="handleNum0" />

三、补充:易混淆的 v-model 内置修饰符

很多人容易把 v-model 修饰符和事件修饰符搞混,这里补充 Vue 最常用的 3 个表单输入修饰符,都是开发中天天用的,大家必须掌握:

修饰符核心作用正确示例
.lazy从「输入时实时更新」改为「失焦 / 回车时才更新」绑定值<input v-model.lazy="username" />,避免输入过程中频繁触发数据更新,适合校验场景
.number自动将输入的字符串转为数字类型,无法转换则保留原值<input v-model.number="age" type="number" />,解决表单输入值默认是字符串的问题,适合数字输入框
.trim自动过滤输入值首尾的空白字符<input v-model.trim="email" />,避免用户输入首尾空格导致的邮箱校验、登录失败问题

四、高频踩坑避坑指南

这里总结了大家 90% 会踩的坑,每一条都能帮你少走弯路:

  1. Tab 键绑定keyup永远不触发:Tab 键的默认行为是按下瞬间就切换焦点,等松开按键触发keyup时,焦点已经离开当前元素,事件永远不会触发。必须绑定keydown.tab,且配合.prevent阻止默认行为。

  2. .passive和.prevent混用:两者绝对不能同时使用,否则.prevent会完全失效,浏览器会抛出警告,记住:用了.passive就绝对不能阻止默认行为。

  3. 修饰符顺序写反,逻辑完全失效:修饰符的书写顺序就是执行顺序,一定要按照业务的优先级从左到右写,比如@click.self.prevent@click.prevent.self效果天差地别。

  4. Vue3 中用keyCode绑定无效:Vue3 已经完全移除了对keyCode修饰符的支持,@keydown.13="submit"这类写法在 Vue3 中完全不生效,必须用按键别名或原生key值。

  5. 普通 div 绑定键盘事件不生效:键盘事件只能在可聚焦元素上触发(input、button、textarea 等),普通 div 需要加tabindex="0"属性,让它变成可聚焦元素,才能监听键盘事件。

  6. 组合键误触发:核心快捷键一定要加.exact精确匹配修饰符,避免带其他修饰键的组合键误触发事件。

  7. 系统修饰键单独绑定不生效@keyup.ctrl="handleCtrl"这类写法不会触发,系统修饰键必须配合其他按键使用,或者用keydown绑定。

    五、企业级开发・全场景实战案例

这里给大家整理了 4 个开发中天天用的实战案例,代码完整带注释,大家可以直接复制到项目里用。

案例 1:表单提交全场景优化(最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<!-- 表单提交:阻止默认刷新、只执行一次防止重复提交 -->
<form @submit.prevent.once="submitForm">
<!-- 用户名:自动过滤首尾空格,失焦才更新值,回车精确提交 -->
<input
v-model.trim.lazy="form.username"
placeholder="请输入用户名"
@keydown.enter.exact="submitForm"
/>
<!-- 年龄:自动转为数字类型 -->
<input v-model.number="form.age" type="number" placeholder="请输入年龄" />
<!-- 邮箱:自动过滤首尾空格 -->
<input v-model.trim="form.email" placeholder="请输入邮箱" />
<button type="submit">提交表单</button>
</form>
</template>

<script setup>
import { reactive } from "vue";

// 表单数据
const form = reactive({
username: "",
age: null,
email: "",
});

// 表单提交函数
const submitForm = () => {
// 这里写表单校验、接口请求逻辑
console.log("表单提交成功", form);
};
</script>

案例 2:弹窗蒙层 + 快捷键关闭(高频场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<!-- 弹窗蒙层:仅点击自身关闭、ESC键关闭、可聚焦 -->
<div
v-if="isShowModal"
class="modal-mask"
@click.self="closeModal"
@keydown.esc="closeModal"
tabindex="0"
>
<!-- 弹窗内容:阻止冒泡,点击不会关闭弹窗 -->
<div class="modal-content" @click.stop>
<h3>弹窗标题</h3>
<p>弹窗内容</p>
<div class="modal-footer">
<button @click="closeModal">取消</button>
<button @click="confirmModal">确认</button>
</div>
</div>
</div>
</template>

<script setup>
import { ref } from "vue";

// 弹窗显隐控制
const isShowModal = ref(false);

// 打开弹窗
const openModal = () => {
isShowModal.value = true;
};

// 关闭弹窗
const closeModal = () => {
isShowModal.value = false;
};

// 确认弹窗
const confirmModal = () => {
console.log("确认操作");
closeModal();
};
</script>

<style scoped>
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: #fff;
padding: 20px;
border-radius: 4px;
width: 400px;
}
</style>

案例 3:移动端长列表滚动性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<!-- 长列表:passive优化滚动流畅度,适配移动端 -->
<div
class="long-list"
@scroll.passive="handleScroll"
@touchmove.passive="handleTouchMove"
>
<div v-for="item in list" :key="item.id" class="list-item">
{{ item.content }}
</div>
</div>
</template>

<script setup>
import { ref, onMounted } from "vue";

// 列表数据
const list = ref([]);

// 模拟生成100条数据
const initList = () => {
for (let i = 0; i < 100; i++) {
list.value.push({
id: i,
content: `第${i + 1}条数据`,
});
}
};

// 滚动事件处理
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
// 触底加载更多逻辑
if (scrollTop + clientHeight >= scrollHeight - 20) {
console.log("触底了,加载更多数据");
}
};

// 触摸事件处理
const handleTouchMove = () => {
console.log("触摸滑动,不影响页面流畅度");
};

// 页面初始化
onMounted(() => {
initList();
});
</script>

<style scoped>
.long-list {
height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* 适配iOS滚动 */
}
.list-item {
padding: 16px;
border-bottom: 1px solid #eee;
}
</style>

案例 4:列表快捷键导航(后台管理系统常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<!-- 列表:方向键切换选中、回车确认、可聚焦 -->
<ul
class="select-list"
@keydown.down="selectNext"
@keydown.up="selectPrev"
@keydown.enter.exact="confirmSelect"
tabindex="0"
>
<li
v-for="(item, index) in list"
:key="item.id"
:class="{ active: activeIndex === index }"
class="list-item"
@click="activeIndex = index"
>
{{ item.name }}
</li>
</ul>
<p>当前选中:{{ list[activeIndex]?.name || "无" }}</p>
</template>

<script setup>
import { ref, reactive } from "vue";

// 列表数据
const list = reactive([
{ id: 1, name: "选项1" },
{ id: 2, name: "选项2" },
{ id: 3, name: "选项3" },
{ id: 4, name: "选项4" },
{ id: 5, name: "选项5" },
]);

// 当前选中的索引
const activeIndex = ref(0);

// 向下切换
const selectNext = () => {
if (activeIndex.value < list.length - 1) {
activeIndex.value++;
}
};

// 向上切换
const selectPrev = () => {
if (activeIndex.value > 0) {
activeIndex.value--;
}
};

// 确认选中
const confirmSelect = () => {
console.log("确认选中:", list[activeIndex.value]);
};
</script>

<style scoped>
.select-list {
border: 1px solid #eee;
border-radius: 4px;
width: 300px;
padding: 0;
}
.list-item {
padding: 12px 16px;
list-style: none;
cursor: pointer;
}
.list-item.active {
background: #409eff;
color: #fff;
}
</style>

六、Vue2 与 Vue3 核心差异对照表

一张表帮你理清两个版本的核心差异,升级项目、切换开发时再也不会踩坑:

特性Vue2Vue3
自定义按键别名支持Vue.config.keyCodes全局配置完全废弃该 API,直接使用原生key值短横线命名
keyCode修饰符兼容支持(官方不推荐)完全移除支持,@keydown.13这类写法无效
基础事件修饰符支持所有基础修饰符100% 完全兼容,无任何破坏性变更
系统修饰键支持基础用法完全兼容,同时新增对更多标准key值的支持
v-model 修饰符支持.lazy/.number/.trim完全兼容,同时支持自定义 v-model 修饰符

写在最后

Vue 的事件修饰符和按键绑定,核心设计理念就是声明式编程,让我们不用再写一堆重复的原生 DOM 操作代码,用极简的语法就能实现复杂的事件逻辑,同时规避大量兼容性问题。


文章作者: 栖桐听雨声
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 栖桐听雨声 !
  目录