Vue基础
Vue基础
MVVM
- M - Model,模型,数据
- V - View,视图
- VM - ViewModel
采用数据双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然
Vue
- v2.x
- v3.x
Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 (Object.defineProperty())特性
安装两种版本:
- 开发版本:包含了警告与调试信息
- 生产版本:删除了与调试相关的信息,作了压缩混淆
声明式渲染
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统
<div id="app">
{{ message }}
</div>
<script>
const vm = new Vue({
el: '#app', // element,代表了 view
data: { // 代表了 Model
message: 'Hello'
}
})
</script>
- el 表示关联的视图
- data 表示要渲染的数据
- 创建的 Vue() 实例相当于是 MVVM 架构中的 VM (ViewModel)
- 所有东西都是响应式的(数据被修改后,视图会自动重新渲染)
Vue 实例
数据与方法
数据:
- 当一个 Vue 实例被创建时,它将
data
对象中的所有的 property 加入到 Vue 的响应式系统中,即会将 data 中定义的各属性直接挂载到 Vue 实例下,可以使用 Vue 实例直接调用到 data 中定义的各属性(如果 data 对象中有以$
或_
开头的属性,不会直接挂载到 Vue 实例下),当data中数据被更新时,会自动更新页面渲染。 - 只有当实例被创建时就已经存在于
data
中的 property 才是响应式的。 - 在 vue 实例下有
$data
属性,代表的就是 data 对象。 - 在 vue 实例下有
$options
属性,代表的是创建 Vue 实例时的选项对象。
方法:
- 创建 Vue 实例时,在
methods
选项中定义的方法也会被直接挂载到 Vue 实例下。
模板语法
插值
文本
{{ expression }}
双花括号之间书写的是 JS 表达式,主要用于绑定渲染文本数据。
双大括号会将数据解释为普通文本,而非 HTML 代码。(对于 HTML 文本,{{ }} 语法会进行转义处理,目的是为了避免出现 XSS 攻击)
html
v-html 指令
attributes
v-bind 指令
指令
就是在 HTML 标签中新添加的一些(以 v-*
开头的)有特殊意义的属性,利用这些属性,在 Vue 中可以实现相应的功能处理。
-
v-html: 渲染 HTML 文本
-
v-text: 渲染纯文本
-
v-bind: 动态绑定属性值,在标签中绑定属性值是不能使用
{{}}
语法,而是需要使用v-bind
指令。可简写为:
-
v-on: 注册事件监听,可简写为
@
-
与条件渲染相关:
- v-if: 操作的是 DOM 树中节点的销毁、重建
- v-else-if:
- v-else:
- v-show: 操作的是 CSS 样式中的 display
-
v-if vs v-show(面试):
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。 -
与列表渲染相关:
- v-for:
<li v-for="item in array"></li> <li v-for="item of array"></li> <li v-for="(item, index) in array"></li> <li v-for="(item, index) of array"></li> <li v-for="value in object"></li> <li v-for="value of object"></li> <li v-for="(value, key) in object"></li> <li v-for="(value, key) of object"></li> <li v-for="(value, key, index) in object"></li> <li v-for="(value, key, index) of object"></li> <li v-for="n in number"></li> <li v-for="n of number"></li>
- v-for 与 v-if 一起使用**(面试)**:不推荐同时使用
v-if
和v-for
。2.x 中当v-if
与v-for
一起使用时,v-for
具有比v-if
更高的优先级;而 3.x 的版本中则反过来。 - 建议尽可能在使用
v-for
时提供key
attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
-
v-once: 只渲染元素和组件一次
-
v-pre: 原样显示
-
v-cloak: 这个指令保持在元素上直到关联实例结束编译。通常结合 css 规则:
[v-cloak] { display: none }
一起使用,避免看到未经编译的视图模板内容。 -
v-model:
v-model
指令在表单<input>
、<textarea>
及<select>
元素上创建双向数据绑定。 -
v-slot: 具名插槽使用,可简写为
#
计算属性与侦听器
计算属性
特点:
- 对复杂的逻辑表达式进行简化运算,方便阅读
- 计算属性值能够被缓存,只要依赖项不发生变化,就会一直使用缓存的值。只在相关响应式依赖发生改变时它们才会重新求值。
vs method(面试)
- 计算属性可被缓存,方法不能被缓存
为什么需要缓存?
假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。
侦听器(侦听属性)
当需要在数据变化时执行异步或开销较大的操作时,使用侦听器这个方式是最有用的。
vs 计算属性(面试)
- 计算属性可被缓存,侦听属性不能被缓存
- 侦听属性中可包含异步操作等副作用动作,而计算属性中不能包含
- 计算属性通常是由一个或多个响应式数据计算出一个值返回;而侦听属性通常是监听一个数据的变化,由这一个数据的变化可能影响到另外的一个或多个数据的变化
class 与 style 绑定
vue 对节点绑定 class 与 style 进行了功能增加,除了可以绑定字符串外,也可以绑定对象或数组。
条件渲染
v-if、v-show 指令
列表渲染
v-for 指令
绑定 key
数组更新检测
变更方法(变异方法)
调用这些方法后,原数组本身会受影响:
- push()
- pop()
- unshift()
- shift()
- splice()
- sort()
- reverse()
调用变更方法后修改数组后,页面会响应式渲染
替换数组
如 slice() 、concat() 这类方法,调用后原数组本身不受影响,页面没有响应式渲染,如果需要将调用结果响应式渲染,可使用替换数组的方式,即利用 =
赋值运算符重新将数组数据值修改。
事件处理
v-on 指令
内联处理器中的方法
可以传递 $event 代表事件对象
事件修饰符:
- .stop
- .prevent
按键修饰符:
- tab
- enter
- …
表单处理
v-model 指令
v-model 是语法糖。
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
组件系统
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。
在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。
定义组件
选项
const options = {
// template 是关联视图的html模板
template: `
<h1>自定义的组件</h1>
`
}
注册组件
全局:
Vue.component(name, options)
局部:
// 定义组件的选项对象
const options = {
// template 是关联视图的html模板
template: `
<h1>自定义的组件</h1>
`,
components: {
'sub-header': subOptions
}
}
在父组件的选项对象中添加 components
字段,注册父组件中可以使用到的子组件(即局部注册)
使用组件
将组件名作为自定义标签名来使用,使用标签名时,采用短横线命名规范(即多个单词之间使用 -
分隔)
单文件组件
后缀名为 *.vue
的文件,在 webpack 中处理该类型的文件,会使用 vue-loader
的 loader
来处理。
<template>
<!-- View 布局结构 -->
</template>
<script>
// JS 交互逻辑
</script>
<style lang="scss">
/* 样式 */
</style>
在 vs-code 中可安装 Vetur
插件,在 *.vue
文件中才会有代码高亮及部分代码提示功能。
data 为什么必须是一个函数(面试)
组件是可被复用的,同一个组件创建出的不同实例彼此之间应该是相互独立的。对象是引用数据类型,如果组件选项中的 data 属性是一个普通对象,则同一个组件所创建出来的所有实例会共享(共用)同一个data对象的数据,则任意一个实例对 data 中数据修改时,其它实例都会受影响。
将 data 定义为一个函数,在该函数体内部返回一个普通对象,这样,当创建组件实例时,会调用 data() 方法,获取返回的新数据对象,则各组件实例间就有自己独立的数据对象了(互不受影响),这与实际开发更符合。
组件通信(面试)
组件与组件之间传递数据
父子组件通信:
-
**父传子:**利用
props
属性传递 -
**子传父:**利用事件。在父组件中使用子组件时,绑定一个自定义事件;在子组件中需要传递数据时,触发($emit())在父组件中绑定的事件 即可。
跨组件跨层级通信:
- 转换为父子组件通信
- event-bus(事件总线):借助事件,将数据从一个组件中传递到另一个组件中。
- 利用 Vue 对象实例的
$on()
与$emit()
方法 $on()
用于注册事件监听$emit()
用于触发指定的事件- 通信(传递数据):在 Vue 原型上添加
b
u
s
属性;在需要接收数据的组件中,调用
‘
bus 属性;在需要接收数据的组件中,调用 `
bus属性;在需要接收数据的组件中,调用‘on()
注册事件监听,在需要传递数据的组件中,调用
$emit()` 触发事件并传递数据
- 利用 Vue 对象实例的
- vuex
Props 属性
props 是组件从父组件接收到的数据:
{
props: ['propName1', 'propName2']
}
- 可以简单定义一个数组,声明组件所能接收并处理的属性名称。
- 组件 props 属性中接收到的数据也会被直接挂载到组件实例下。
{
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
}
- 也可以将 props 定义为一个对象,可实现对属性的验证
注意:
- props 应该是只读的,即不要修改组件获得到的属性值
- 如果业务中涉及到可能修改属性的值,则需要尝试转换为 data 或 computed 计算属性的方式重新设计。
插槽
作用:在使用组件时,向组件内部分发内容。
使用:<slot>
<slot>
相当于是一个占位符
命名插槽(具名插槽)
<slot name="slot-name">
在使用具名插槽分发内容时,可以使用 slot
属性或 v-slot
指令
- slot 属性是 v2.6 之前版本中使用的
- v-slot 指令是 v2.6 中新增的语法,现在推荐使用
v-slot
指令- v-slot 指令需要用在
<template>
标签内部,即:<template v-slot:slotName>
v-slot
可以简写为#
- v-slot 指令需要用在
生命周期
生命周期:指的是一个 Vue 实例或组件从开始创建,到最终销毁,全阶段所经历的所有过程。
生命周期钩子(函数)(面试)
就是在组件正常的生命周期过程中,提供了一些回调函数,在这些回调函数中可以添加用户自己的代码,完成特定的自定义功能。
create 阶段
- beforeCreate()
- created() - 可以使用实例直接调用到 data 中的数据
mount 阶段
- beforeMount()
- mounted() - 到该生命周期钩子函数中,才可以获取到渲染后的 DOM 节点
update 阶段
当数据发生变化时,会进行 update 更新阶段
- beforeUpdate()
- updated()
destroy 阶段
通常在销毁阶段会销毁哪些资源(面试):启动的定时器、主动注册的事件监听、未完成的网络请求、打开的 socket 连接等
- beforeDestroy()
- destroyed()
keep-alive 组件
- activated(): 被 keep-alive 缓存的组件激活时调用
- deactivated():被 keep-alive 缓存的组件失活时调用。
渲染函数(render)-了解
render(createElement) {
// render
}
- createElement() 创建的虚拟节点元素
- createElement() 有三个参数:标签、属性、孩子节点
虚拟DOM
<div class="container">
<span title="提示" class="_span">span_1</span>
<a href="/">链接</a>
</div>
会被编译为 render() 渲染函数中的内容:
render(createElement) {
return createElement(
'div',
{ className: 'container' },
[
createElement(
'span',
{ title: '提示', className: '_span' },
['span_1']
),
createElement(
'a',
{ href: '/' },
['链接']
),
]
)
}
调用后,返回一个对象:
{
tag: 'div',
props: { className: 'container' },
children: [
{
tag: 'span',
props: { title: '提示', className: '_span' },
children: ['span_1']
},
{
tag: 'a',
props: { href: '/' },
children: ['链接']
},
]
}
这是一个保存在内存中的虚拟 DOM 元素结构,这个结构与实体 DOM 树结构映射。
响应式原理(了解)
数据劫持
v2.x 版本中,使用 Object.defineProperty()
实现数据劫持
v3.x 版本中,使用 Proxy
实现数据劫持
过滤器
作用:可被用于一些常见的文本格式化。
过滤器可以用在两个地方:双花括号插值和 v-bind
表达式
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”(|
)符号指示。
全局
Vue.filter()
局部
options = {
filters: {}
}
keep-alive
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
nextTick()–面试
在下次 DOM 更新循环结束之后执行延迟回调。
在修改数据之后立即使用这个方法,获取更新后的 DOM。
Vue Devtools
开发工具
Vue CLI
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,其内部封装了 webpack 的相关配置。
安装
版本问题:
2.x 版本创建的项目,会在项目根目录中有多余的关于 webpack 配置的目录及文件。通常是 config
与 build
目录及目录内的文件。
3.x/4.x 版本创建的项目,是将 webpack 的配置专门封装到npm包中,在项目根目录下没有相关的 webpack 配置文件存在。
全局安装 vue-cli:
$ npm install -g @vue/cli
安装成功后,可在 cmd 命令提示符下执行 vue --version
测试查看版本信息:
$ vue --version
创建项目
图形化用户界面(GUI):
$ vue ui
命令行:
- 执行创建项目命令:
$ vue create vue-cli-demo
- 开始利用向导选择项目中使用到的特性:
Please pick a preset:
> Manually select features
- 确认项目中的特性:
Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Choose Vue version
>(*) Babel
>(*) CSS Pre-processors
>(*) Linter / Formatter
- 选择 vue 版本:
Choose a version of Vue.js that you want to start the project with (Use arrow keys)
> 2.x
- 选择 css 预处理器:
Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
> Sass/SCSS (with node-sass)
- 选择 ESLint
Pick a linter / formatter config: (Use arrow keys)
> ESLint + Standard config
- 保存代码时验证代码是否符合规范
Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Lint on save
- 选择在独立文件中保存配置文件
Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
- 是否将此前步骤中的选择保存为模板供以后创建项目使用
Save this as a preset for future projects? (y/N) N
- 自动根据前述选择,开始创建项目结构并安装依赖资源
package.json
npm scripts
{
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
}
- serve:启动开发环境的任务,会打开开发环境下的 webpack-dev-server 服务器,默认监听 8080 端口
- build:启动构建任务,主要用于构建生产环境下的代码
- lint:启动规范格式化任务,可将不满足规范的代码格式化
可以使用 npm run
执行
关于 @vue/cli-service 包
内部封装了 webpack 配置,npm scripts
中的任务执行就依赖于这个包。
项目目录结构
|--project
|--public # 单页面应用页面所在目录
|--src # 源代码
|--|--main.js # 应用的入口JS文件
|--|--App.vue # 单文件组件
|--.eslintrc.js # eslint 规范配置文件
|--babel.config.js # babel 配置文件
|--package.json # 应用配置文件
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
通常使用 vuex 来管理应用中各组件会共享共用的数据。
概念
-
store:仓库,是一个容器,包含着你的应用中大部分的状态 (state)
-
state:状态,组件需要共享的数据
-
getter:是 store 中的计算属性,相当于组件中的 computed
-
mutation:是函数结构,修改状态数据,用于进行 state 的同步更新,约定,mutation 是唯一能够进行状态更新的位置
-
action:是函数结构,可包括异步任务,action 中不能直接更新状态,而是需要通过提交 mutation 来实现
-
module:模块,可将 store 分割成一个个的小的 module,各 module 有自己的 state、getter、mutation、action,甚至子 module
使用
安装
$ npm i vuex
创建 store
import Vue from 'vue'
import Vuex from 'vuex'
// 使用核心插件 Vuex
Vue.use(Vuex)
// 创建 Store
const store = new Vuex.Store({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {}
})
// 导出
export default store
注入到 Vue 根实例中
import Vue from 'vue'
import store from './store'
new Vue({
// ...
store
})
将 store 注入 Vue 根实例后,在所有的组件实例中,都可以使用 this.$store
调用到 store
对象
编写 state及更新 state 的mutation
// 略
组件中调用 state 及 mutation
{
computed: {
todos() {
this.$store.state.todos
}
},
methods: {
handler() {
this.$store.commit('mutation-type', 'payload')
}
}
}
注意,在组件中不能直接调用到 mutation 方法,而是需要使用 $store.commit()
来提交 mutation
(类似于使用 $emit() 触发事件)
SPA
Single Page Application
单页面应用
是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。
前端路由
前端路由,不会发送 html 页面请求到服务器,就能直接实现界面切换(是在 JS 中完成)。
由于是单页面应用,服务器上只保存了一个 html 页面,所以浏览器访问到该页面时,也会将引用的 js 下载到浏览器本地。
在页面中仍然存在超级链接可以点击切换页面,但这种切换页面并不是真正访问服务器上的 html 页面,而是通过 JS 来实现界面切换(DOM操作)。
模式
- hash:在URL中采用#号来作为当前视图的地址,改变#号后的参数,页面并不会重载。
- history:利用 h5 中 history 新增的 API: pushState()、replaceState() ,在调用这些 API 时不会发送新的请求到后端(页面不会重新加载)。
- history 模式的路由表面上与访问服务端的路由一致(相比 hash 模式来说,没有
#
) - history 模式的路由使用需要在服务器上进行相关配置
- history 模式的路由表面上与访问服务端的路由一致(相比 hash 模式来说,没有
Vue Router
Vue.js 官方的路由管理器
使用
安装
$ npm i vue-router
创建 VueRouter 实例
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home'
import About from '../views/About'
import Profile from '../views/Profile'
// 使用 vue-router 核心插件
Vue.use(VueRouter)
// 创建 VueRouter 实例
const router = new VueRouter({
mode: 'hash', // 前端路由模式:hash、history
routes: [ // 静态路由配置信息
{
path: '/home',
component: Home
}
]
})
export default router
注入到 Vue 根实例中
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
将 router 注入 Vue 根实例后,可在后代组件中使用到两个属性(面试):
$router
:指代的是 VueRouter 实例$route
:当前激活的路由
两个组件
<router-link>
:链接,导航,默认会渲染为<a>
超级链接<router-view>
:占位,将满足路径中当前访问路由对应的component
组件进行渲染。
路由元信息
在进行路由相关功能开发时,需要与当前路由相关使用到的数据,可以携带在元信息中,使用 meta
字段表示。
命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。
为 <router-view />
组件添加 name
属性(即命名视图),可使用对应名称指定在 <router-view />
视图中渲染的组件。如果 <router-view >
没有 name 属性,其 name
值默认为 default
命名路由
是在路由配置项中添加 name
属性,有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候
编程式的导航
除了使用 <router-link />
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
router.push(location) - 会添加历史记录
router.replace(location) - 不会添加历史记录
router.back()
router.forward()
router.go()
参数 location:
- 可取字符串
- 可取对象
{path, query}
{name, params}
- 注意:params 必须与 name 配合,不能与 path 共存。
- 通常
query
这个单词与传递查询字符串(?)参数有关,params
这个单词与传递动态路径参数(/path/to/:params)有关
嵌套路由
通常对应组件的嵌套结构
导航守卫
作用:守卫导航
导航守卫主要用来通过跳转或取消的方式守卫导航
钩子函数
全局
- beforeEach(callback) - 全局前置守卫,可以在这个钩子函数中进行用户权限拦截
- 回调函数有三个参数:to、from、next
- to:即将进入的目标路由对象
- from:正要离开的路由
- next:下一步,是在导航解析流程中的“下一步”,是一个函数结构。
- next():正常走下一步
- next(false):中断导航
- next(path):重新跳转到新地址
- 确保
next
函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
- beforeResolve()
- afterEach()
路由独享
- beforeEnter()
组件内
-
beforeRouteEnter() - 不能独 this 获取到组件实例,如果需要使用组件实例,则在 next(callback) 的回调函数中会传递组件实例作为参数
-
beforeRouteUpdate() - 在重用的组件中被调用到
-
beforeRouteLeave()
UI 组件库
PC端
移动端
使用
以 Element-UI 组件库为例:
安装
$ npm i element-ui
引入
全部引入
将组件库中的所有组件一次性引入项目中,在使用组件时,不需要再考虑引入哪些就可以直接使用。
缺点:在打包构建时,项目中未使用到的组件也会打包进最后的 bundle 中,会增大代码包的体积。
// 在 main.js 中引入:
// ......
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
按需引入
是组件库中使用到哪个组件时,才引入使用,未使用到的不需要引入。
优点:可以有效的保证打包后代码包的体积最优
缺点:比较繁琐
安装 babel 插件包:
$ npm install babel-plugin-component -D
配置 babel 相关(修改 babel.config.js
配置文件):
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
]
]
}
在自定义的组件中引入 ElementUI 组件:
// main.js
// 按需引入
import {
Button,
DatePicker,
Badge
} from 'element-ui'
// 全局注册组件
Vue.component(Button.name, Button)
// 也可以写作:
Vue.use(DatePicker)
Vue.use(Badge)
// 在组件中仍然使用 <el-xxx> 来使用到 ElementUI 组件
注意:全部引入与按需引入通常不能共存
vue-element-admin
网络请求
axios
- 浏览器中对 XMLHttpRequest 进行封装
- 支持 Promise API
- 支持请求/响应拦截处理
- 支持取消请求
更多推荐
所有评论(0)