vuex应用状态管理和axios网络请求响应

vuex应用状态管理和axios网络请求响应

Vuex插件的安装

vue项目目录下执行如下命令:

npm install vuex@3.6.2 --save

安装完成后,在​​package.json​​​的项目配置文件中显示出​​vuex​​安装版本.手动在项目的src目录下创建store文件夹和index.js文件,index.js文件内容如下。该文件内容就是Vuex进行状态集中管理的仓库

import Vue from 'vue'
import Vuex from 'vuex'

//使用Vuex插件
Vue.use(Vuex)

//注意这里创建的是store对象,不是vuex对象
const store = new Vuex.Store({
state:{
},
mutations:{},
actions:{},
getters:{},
modules:{}
})
//导出对象
export default store

从上图中可以看到,在store对象中我们定义了一系列的子对象state、mutations、actions、getters、modules,这就是我们学习Vuex的主要内容。填空题,我们一点点来做。我们建好了“仓库”,还得把它引入到项目里面来。在main.js中加入如下代码

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store, //加这里
router,
components: { App },
template: '<App/>'
})

计数器组件基础代码

定义两个组件,一个组件name为VuexCpn1,另一个组件name为VuexCpn2。其他代码完全相同,如下:

<template>
<div>
<div>计数值:{{count}}</div>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
</div>
</template>

<script>
export default {
name:'VuexCpn1',
data() {
return {
count: 0
}
},
}
</script>

如上图所示,我们定义的两个组件,除了名字不同,其他的代码完全一致。代码中定义了一个数据变量count。然后我们将这两个组件引入到同一个父组件里面,也就是说这两个组件是兄弟组件。下图是父组件的代码:

  • CPN.vue
<template>
<div>
<h1>CPN</h1>
<vuex-cpn-1></vuex-cpn-1>
<vuex-cpn-2></vuex-cpn-2>
</div>
</template>
<script>
import VuexCpn1 from './VuexCpn1.vue'
import VuexCpn2 from './VuexCpn2.vue'
export default {
name:'CPN',
data() {
return {
count: 0
}
},
components:{
VuexCpn2,
VuexCpn1
}
}
</script>

实现效果如下:

vuex应用状态管理和axios网络请求响应

两个组件的count的值不同步,因为count是分别定义在两个组件里面的,从程序运行的角度来讲,count是一个局部变量。如果我们希望点击组件VuexCpn1的按钮,同步影响组件VuexCpn2的count值;点击组件VuexCpn2的按钮,同步影响组件VuexCpn1的count值。该怎么做?比较麻烦的做法就是:点击VuexCpn1按钮,向父组件传递点击事件,父组件向VuexCpn2传递counter数据(参考组件化开发章节内容进行回顾)。还有一种简单的做法就是将count变成一个全局变量。

Vuex集中化存储

那么我们怎么让counter变成一个全局变量?答案就是使用vuex,将count定义在store/index.js中的store对象的state状态对象里面。

state:{
count:0
},

然后修改VuexCpn1组件和VuexCpn2组件的代码如下:

<template>
<div>
<h1>CPN1</h1>
<div>计数值:{{$store.state.count}}</div>
<button @click="$store.state.count++">+1</button>
<button @click="$store.state.count--">-1</button>
</div>
</template>
<script>

export default {
name:'VuexCpn1'
}
</script>
  • $store代表项目全局的集中存储的store对象,即store/index.js中定义的store对象
  • store对象中的state属性,用于定义全局变量,多个组件都可以使用该变量

实现效果如下:无论我们点击哪一个组件的加1减1按钮,count的值在两个组件里面的显示都是同步的一致的。由此我们可以知道$store.state.count状态是响应式的,即:我们点击组件VuexCpn1的按钮,组件VuexCpn2中的count也发生变化

vuex应用状态管理和axios网络请求响应

mutations的作用

  • 当我们点击任何一个组件中的按钮,实际上调用了“和store.state.count–”,就修改了公共状态count计数的值,从而影响另一个组件的显示内容同步。
  • 但是我在上一节中也说到了直接对$store.state中的状态进行赋值操作,是可以实现响应式的,即:state的变化影响所有引用它的视图的变化。

但是存在一个问题:我们没有办法进行状态跟踪,也就是我们只能看到状态的结果,无法知道状态改变的过程。如果我们很多个组件引用很多的公共状态state,该如何跟踪每一次点击按钮操作之后的state变化?如何留下痕迹?

直接对$store.state中的状态进行赋值操作是无法留痕的,我们需要使用mutation(改变)。mutation是实际上就是对state状态进行操作的自定义方法,通过触发mutations方法修改的state可以留痕,可以被vue devtools调试工具跟踪。

但是注意不要在mutation自定义方法里面进行异步操作,比如发送网络请求。因为异步回调无法有效的被mutation跟踪,所以mutation自定义方法里面必须是同步操作

mutations的基本使用

VuexCpn1和VuexCpn2的例子的基础上进行代码修改,首先定义mutations:

mutations:{
add(state){
state.count++
},
sub(state){
state.count--
}

},

mutations自定义方法的第一个参数是就是state,我们可以通过state参数修改状态。然后在组件中使用**$store.commit()方法触发mutation&**,commit的第一个参数是mutation的方法名。如下:

<button @click="$store.commit('add')">+1</button>
<button @click="$store.commit('sub')">-1</button>

视图效果和使用“store.state.counter–”是一样的。但是使用mutation改变的state会留痕,可以被跟踪。

mutations方法携带参数

希望每点击一次按钮加5减5或加n减n,不再是加1减1。这样我们就希望mutation自定义的方法能够传参,这样我们就能够针对state状态做更灵活的操作,适应更广泛的需求。当然,这个功能是一定有的,语法如下:

mutations:{
add(state){
state.count++
},
sub(state){
state.coun--
},
add_num(state,num){
state.count= state.count+num
},
sub_num(state,num){
state.count= state.count-num
}

},

以双组件计数器的例子,我们触发mutation的时候,是这样的代码:

<button @click="$store.commit('add_num',5)">+</button>
<button @click="$store.commit('sub_num',5)">-</button>

mutations常量

  • mutation函数一次定义,在多个组件内多次commit调用。
  • 触发mutation的方法commit的第一个参数,就是mutation函数的名称。

如果有开发人员修改了mutation函数的方法名,那么我们如何保证对应的commit方法的参数一也对应的进行修改?比较笨的方式就是字符串查找,然后一个一个改。还有一种情况就是:通过查找的方式一个一个改,漏掉了怎么办?这种可能性很大。所以我们在一开始就要避免这个问题:将mutation函数名称定义为常量。新建一个文件叫做store/mutation-types.js定义常量

let COUNTER_ADD ='add1'
let COUNTER_SUB ='sub1'
export{
COUNTER_ADD,
COUNTER_SUB
}

在mutation函数定义的时候,先导入mutation-types,再使用[]引用常量

import { 
COUNTER_ADD ,
COUNTER_SUB
} from './mutation-types'


mutations:{
add(state){
state.count++
},
sub(state){
state.count--
},
add_num(state,num){
state.count= state.count+num
},
sub_num(state,num){
state.count= state.count-num
},
[COUNTER_ADD] (state,payload){
state.count = state.count + payload.num * payload.multiple
},
[COUNTER_SUB] (state,payload){
state.count = state.count - payload.num * payload.multiple
},
handleFirstNameVal(state,payload){
state.firstName = payload
}

},

在调用commit触发mutation的时候,同样先导入mutation-types,再使用常量。注意通过js模块导入的COUNTER_ADD 不能再html里面被使用,所以我们需要单独定义method,在method中使用常量。

<template>
<div>
<h1>CPN1</h1>
<div>计数值:{{$store.state.count}}</div>
<button @click="addCounter()">+1</button>
<button @click="subCounter()">-1</button>
</div>
</template>

<script>
import {
COUNTER_ADD ,
COUNTER_SUB
} from '../store/mutation-types'

export default {
name:'VuexCpn1',
data() {
return {
counter: 0
}
},
methods: {
addCounter(){
this.$store.commit(COUNTER_ADD,{num:5,multiple:2})
},
subCounter(){
this.$store.commit(COUNTER_SUB,{num:5,multiple:2})
}
}
}
</script>

全局计算属性getters

  • store.state的数据定义如下:
state:{
count:0,
firstName:"",
lastName:""
},
  • allName1.vue
<template>
<div>
<label for="firstName">firstName:</label>
<input type="text" v-model="$store.state.firstName" id="firstName">
<label for="lastName">lastName:</label>
<input type="text" v-model="$store.state.lastName" id="lastName">

<div>VuexCpn1组件</div>
<div>fullName:{{$store.state.firstName }}-{{$store.state.lastName }}</div>
</div>
</template>
  • allName2.vue
<template>
<div>
<div>VuexCpn2组件</div>
<div>fullName:{{$store.state.firstName }}-{{$store.state.lastName }}</div>
</div>
</template>
  • AllName.vue
<template>
<div>
<h1>ALLNAME</h1>
<all-name-1></all-name-1>
<all-name-2></all-name-2>
</div>
</template>

<script>
import allName1 from './allName1.vue'
import allName2 from './allName2.vue'
export default {
name:'ALLNAME',
data() {
return {
counter: 0
}
},
components:{
allName1,
allName2
}
}
</script>

最后的显示结果如下:

vuex应用状态管理和axios网络请求响应

  • 当我们在第一个组件里面输入firstName和lastName的时候,另一个组件fullName也随之发生变化
  • 使用v-model指令绑定了store.state.lastName状态数据。
  • 我们看到上面代码中的fullName是通过{{$store.state.firstName }}-{{$store.state.lastName }}拼接而成的,在两个组件里面分别进行计算得出fullName。

v-model绑定state状态数据的标准做法

上文中我们使用v-model绑定了$store.state状态数据,实现了输入框与state状态数据之间的绑定,但是不推荐这种做法。因为这种直接的绑定方式,状态无法被devtools跟踪

vuex应用状态管理和axios网络请求响应

从devtools调试工具中可以看到:浏览器显示上已经为kobe-byrant,但是调试工具中的firstName和lastName仍然是空串。比较正规的做法是:v-model绑定计算属性。

<template>
<div>
<h1>AllName1</h1>
<label for="firstName">firstName:</label>
<input type="text" v-model="firstName" id="firstName">
<label for="lastName">lastName:</label>
<input type="text" v-model="$store.state.lastName" id="lastName">

<div>AllName1组件</div>
<div>fullName:{{$store.state.firstName }}-{{$store.state.lastName }}</div>
<!-- <div>fullName:{{fullName}}</div> -->

</div>
</template>

然后在computed计算属性的get和set方法里面进行state变量的状态管理。

export default {
name:'AllName1',
computed: {
firstName:{
get(){
return this.$store.state.firstName
},
set(newVal){
this.$store.commit('handleFirstNameVal', newVal)
}
}
},
}
</script>

在mutation是里面定义handleFirstNameVal,对firstName状态赋值。这种方式虽然较上一小节的实现麻烦了很多,但确实是标准的做法。这样做完之后firstName状态数据就可以正确的被devtools所跟踪。

mutations:{
add(state){
state.count++
},
sub(state){
state.count--
},
add_num(state,num){
state.count= state.count+num
},
sub_num(state,num){
state.count= state.count-num
},
[COUNTER_ADD] (state,payload){
state.count = state.count + payload.num * payload.multiple
},
[COUNTER_SUB] (state,payload){
state.count = state.count - payload.num * payload.multiple
},
handleFirstNameVal(state,payload){
state.firstName = payload
}

},

vuex的getters的定义与使用

在上面的实现中,我们看到全名fullName的值是通过{{$store.state.firstName }}-{{$store.state.lastName }}拼接而成的,在两个组件里面分别使用。还有一种方式就是将firstName 和 lastName先计算出fullName,然后在组件里面使用fullName,这种方法就是getters,在stroe/index.js中添加

getters:{
fullName(state){

return state.firstName + "-" + state.lastName
}
},

下文代码显示效果和使用{{$store.state.firstName }}-{{$store.state.lastName }}是一样的。

<template>
<div>
<h1>AllName1</h1>
<label for="firstName">firstName:</label>
<input type="text" v-model="firstName" id="firstName">
<label for="lastName">lastName:</label>
<input type="text" v-model="$store.state.lastName" id="lastName">

<div>AllName1组件</div>
<div>fullName:{{fullName}}</div>
<!-- <div>fullName:{{fullName}}</div> -->

</div>
</template>
<script>


export default {
name:'AllName1',
computed: {
firstName:{
get(){
return this.$store.state.firstName
},
set(newVal){
this.$store.commit('handleFirstNameVal', newVal)
}
},
fullName(){
return this.$store.getters.fullName
}
},
}
</script>

vuex状态异步操作

Mutation中只能定义同步操作,异步操作交给Action

Action基本用法

vuex应用状态管理和axios网络请求响应

  • 在组件中使用dispatch触发异步操作Action(如网络请求backend API,后端服务API)
  • 在异步操作Action中对Mutation操作进行commit。
mutations:{
add(state){
state.count++
},
sub(state){
state.count--
},
add_num(state,num){
state.count= state.count+num
},
sub_num(state,num){
state.count= state.count-num
},
[COUNTER_ADD] (state,payload){
state.count = state.count + payload.num * payload.multiple
},
[COUNTER_SUB] (state,payload){
state.count = state.count - payload.num * payload.multiple
},
handleFirstNameVal(state,payload){
state.firstName = payload
},

actionTest(state){
state.firstName = "actionTest"
}

},
actions:{
submitAction(context){
setTimeout(() => {//异步操作
//state数据的修改还是由mutation执行
context.commit('actionTest');
}, 1000);
}

},
  • action的方法的参数context意为上下文,在我们还没有学习vuex的module之前可以暂且认为它是$store对象。我们后文再做详解。
  • 最后我们可以在组件中通过dispatch方法触发Action。这样的操作结果就是:state.firstName在异步操作中也可以被devtools跟踪状态。
<template>
<div>
<h1>AllName1</h1>
<label for="firstName">firstName:</label>
<input type="text" v-model="firstName" id="firstName">
<label for="lastName">lastName:</label>
<input type="text" v-model="$store.state.lastName" id="lastName">

<button @click="$store.dispatch('submitAction')">触发异步操作</button>

<div>AllName1组件</div>
<div>fullName:{{fullName}}</div>
<!-- <div>fullName:{{fullName}}</div> -->

</div>
</template>
<script>


export default {
name:'AllName1',
computed: {
firstName:{
get(){
return this.$store.state.firstName
},
set(newVal){
this.$store.commit('handleFirstNameVal', newVal)
}
},
fullName(){
return this.$store.getters.fullName
}
},
}
</script>

axios网络请求响应

axios的简介及优势

基于Promise的,用在浏览器和nodeJS环境下的HTTP客户端。(Promise based HTTP client for the browser and node.js)

  • 支持从浏览器中创建XMLHttpRequests
  • 支持从 node.js 创建http请求(这个是其他框架不具备的)
  • 支持PromiseAPI(最重要特点)
  • 支持拦截器,拦截请求和响应(核心功能)
  • 支持转换请求数据和响应数据(核心功能)
  • 自动转换 JSON 数据(核心功能)
  • 客户端支持防御XSRF攻击

支持多种请求方法

  • request(config)
  • get(url[, config])
  • delete(url[, config])
  • head(url[, config])
  • post(url[, data[, config]])
  • put(url[, data[, config]])
  • patch(url[, data[, config]])

安装使用axios

npm安装方式:

npm install axios --save

axios发送GET请求

在使用axios发送HTTP请求之前,先向大家介绍一个网站:https://jsonplaceholder.typicode.com/,这个网站提供了免费的HTTP请求及相应服务,我们可以利用这个服务测试我们的axios。

<template>
<div>
<h1>ALLNAME</h1>
<button @click="testaxios()">testaxios</button>
<all-name-1></all-name-1>
<all-name-2></all-name-2>
</div>
</template>

<script>
import allName1 from './allName1.vue'
import allName2 from './allName2.vue'
import axios from 'axios'
export default {
name:'ALLNAME',
components:{
allName1,
allName2
},
methods:{
testaxios(){
axios({
method:'get', //http请求方法
url:"https://jsonplaceholder.typicode.com/posts/1" //http服务地址
})
.then( (response) =>{
console.log(response);
}).catch(function (error) {
//请求异常响应结果
console.log(error);
});
}
}
}
</script>

请求成功响应之后,浏览器打印结果如下:

vuex应用状态管理和axios网络请求响应

  • 使用axios(config)发送http请求,config为该请求的配置信息对象
  • axios.get等同于axios使用method:get。
  • axios是基于Promise的HTTP客户端,所以可以使用then、catch对请求结果进行处理。
    axios发送POST请求

axios发送HTTP post请求的基本方法如下:

postaxios(){

axios({
method:'post', //http请求方法
url:"http://httpbin.org/post" , //http服务地址
data: {
firstName: 'kobe',
lastName: 'byrant'
}
})
.then( (response) =>{
console.log(response);
}).catch(function (error) {
//请求异常响应结果
console.log(error);
});
}

在模板中添加一个按钮

<button @click="postaxios()">postaxios</button>

发送POST请求成功之后,浏览器的打印结果如下:

vuex应用状态管理和axios网络请求响应

axios的配置详解

常用数据格式

在实际的开发过程中,服务端接收数据的方式可能是多种数据格式的,比如:JSON、QueryString表单数据等形式。

JSON数据格式

当data参数是JSON对象的时候,默认的Content-Type是application/json。所以下面代码中的headers的content-type设置可以省略。

postaxios(){

axios({
method:'post', //http请求方法
url:"http://httpbin.org/post" , //http服务地址
data: {
firstName: 'kobe',
lastName: 'byrant'
},

headers:{"Content-Type":"application/json"}

})
.then( (response) =>{
console.log(response);
}).catch(function (error) {
//请求异常响应结果
console.log(error);
});
}
}

vuex应用状态管理和axios网络请求响应

表单数据格式

如果服务端不接收JSON对象数据,而是接收表单数据,那么我们该如何调整上面的代码?

  • 首先我们需要将JSON对象转换为QueryString字符串,使用qs.stringify()函数
  • 在项目中使用命令行工具输入:​​npm install qs​
  • 安装完成后在需要用到的代码中:​​import qs from 'qs'​
  • qs.parse()是将URL解析成对象的形式,qs.stringify()是将对象 序列化成URL的形式,以&进行拼接
  • 当data参数是字符串的时候,默认的Content-Type是application/x-www-form-urlencoded。所以下面代码中的headers的content-type设置可以省略。
postaxios(){

axios.post('http://httpbin.org/post',
qs.stringify({
firstName: 'kobe',
lastName: 'byrant'
}),
{
headers:{"content-type":"application/x-www-form-urlencoded"}
}
).then(function (response) {
console.log(response);
});

// axios({
// method:'post', //http请求方法
// url:"http://httpbin.org/post" , //http服务地址
// data: {
// firstName: 'kobe',
// lastName: 'byrant'
// },

// headers:{"Content-Type":"application/json"}

// })
// .then( (response) =>{
// console.log(response);
// }).catch(function (error) {
// //请求异常响应结果
// console.log(error);
// });
}

结果展示

vuex应用状态管理和axios网络请求响应

数据的转换

除了常用的表单数据格式、JSON数据格式,实际开发中还有很多的数据格式、内容需要进行处理。比如我们希望对发送出去的数据做特殊的自定义处理,axios也为我们提供了方法。

  • transfromRequest用于在向服务端发送请求之前,将数据做处理修改请求数据。该方法只对PUT、POST、PATCH请求生效。而且,此方法最后必须返回一个string、ArrayBuffer或者Stream。
  • transformResponse对响应结果数据,进行转换处理。即在收到响应之后,then/catch之前做数据转换处理工作。
{
transformRequest: [function (data) {
// 在这里对 data 进行任意转换处理

return data;
}],

// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 在这里对 data 进行任意转换处理

return data;
}]
}

axios请求配置

axios最基本的请求方法GET、POST的使用,PUT、DELETE请求的方法和POST请求使用方法是基本一致的。如果没有指定​​method​​​,请求将默认使用​​get​​​方法。此外,​​url​​是必需的请求参数。下面为大家介绍其他的请求参数:

{
// `url` 是用于请求的服务器 URL(必填)
url: '/user',

// `method` 是HTTP请求时使用的方法, 默认是 get
method: 'get',

// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',

// `headers` 是即将被发送的自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},

// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},

// `paramsSerializer` 是一个负责 `params` 序列化的函数。
//针对params的参数序列化通常axios自动完成,所以并不常用
paramsSerializer: function(params) {
return qs.stringify(params, {arrayFormat: 'brackets'})
},

// `data` 是作为请求body被发送的数据,只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
// 在没有设置 `transformRequest` 时,data数据必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属:FormData, File, Blob
// - Node 专属:Stream
data: {
firstName: 'Fred'
},

// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 1000,

// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // 默认的

// `adapter` 允许自定义处理请求,使用mock方式方便测试
// 返回一个 promise 并应用一个有效的响应
adapter: function (config) {
/* ... */
},

// `auth` 表示应该使用 HTTP 基础验证,并提供凭据
// 这将设置一个 `Authorization` 头,可以对应Http Basic基础认证模式。
//如果是java开发人员,可以简单的学一下Spring Security的Http Basic基础认证模式,就会理解。
auth: {
username: 'janedoe',
password: 's00pers3cret'
},

// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // 默认的

//以下两个配置与跨站攻击伪造相关的防御,须与服务端名称保持一致。
// `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
xsrfCookieName: 'XSRF-TOKEN', // default

// `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的

// `maxContentLength` 定义允许的响应内容的最大尺寸
maxContentLength: 2000,

// `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认的
},

// `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
// 如果设置为0,将不会 follow 任何重定向
maxRedirects: 5, // 默认的

// `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
// `keepAlive` 默认没有启用
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),

// 'proxy' 定义代理服务器的主机名称和端口
// `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
// 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
proxy: {
host: '127.0.0.1',
port: 9000,
auth: : {
username: 'mikeymike',
password: 'rapunz3l'
}
},

// `cancelToken` 指定用于取消请求的 cancel token
cancelToken: new CancelToken(function (cancel) {
})
}

axios响应数据结构

某个请求的响应包含以下信息

{
// `data` 由服务器提供的响应
data: {},

// `status` 来自服务器响应的 HTTP 状态码
status: 200,

// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',

// `headers` 服务器响应的头
headers: {},

// `config` 是为请求提供的配置信息
config: {}
}

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!

原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!

文章版权声明

 1 原创文章作者:4310,如若转载,请注明出处: https://www.52hwl.com/36181.html

 2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈

 3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)

 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年7月15日 上午11:17
下一篇 2023年7月15日 上午11:18