section A 移动端适配方案
Vue移动端h5适配解决方案(lib-flexible+px2rem-loader)
- px2rem-loader:将css中的px转为rem单位,用了它就不用自己计算rem值了
- lib-flexible:根据设备宽度,修改根元素html的font-size,以适配不同终端
配置
- 安装1:npm i px2rem-loader --save -dev
- 安装2:npm i lib-flexible --save
- 配置1:入口文件main.js中引入:import 'lib-flexible/flexible.js'
- 配置2:vue.config.js里面
css: {
loaderOptions: {
css: {
// options here will be passed to css-loader
},
postcss: {
// options here will be passed to postcss-loader
plugins: [require('postcss-px2rem')({
remUnit: 37.5 //说明1rem为37.5px,根据设计稿来定一般为750,我这个是375
})]
}
}
友情提示:
行内样式是无法转化rem的
section B 项目过程中遇到的问题
UI库使用的是vant,虽然没用过感觉UI库基本都是一样的,很容易上手
one A 下拉刷新+加载更多
先看下效果图
移动端下拉刷新、加载更多还是比较常见的
整个页面列表相对简单、局部列表就有点麻烦了,下拉刷新+加载更多结合体还是需要结合场景做出相应改变的,生搬硬套是不行的。(文章最后会给出完整代码
)
就以20条数据为一页,下拉加载更多为例 由于一个页面可切换列表切都具备加载更多、下拉刷新功能,同一个组件在一个页面使用多次,担心会互相影响,咱们可以这么写
const detailGold = () => import('./components/detail');
const detailMoney = () => import('./components/detail');
(父组件)动态给与外层盒子高度适配各种手机
<div
class="list-content"
ref="listbox"
:style="'height:' + listheight + 'px'"
>
<detail-gold
@childMsg="childMsg"
:list="listGold"
v-if="gold"
:finishList="finishList"
></detail-gold>
<detail-money
@childMsg="childMsg"
:list="listMoney"
v-if="money"
:finishList="finishList"
></detail-money>
</div>
data() {
return {
// 动态获取页面
clientHeight: window.screen.height,
listheight: '',
// 子组件传参
listGold: [],
listMoney: [],
// 下一页
lastindicateGold: '',
lastindicateCash: '',
//判断tab切换
gold: true,
money: false,
//计时器
timer: null,
//刚刚好20条数据,下次请求无数据,避免一直loading
finishList: true
}
},
created() {
let id = this.$route.query.id;
if (id == 1) {
this.isactive = true;
this.subtitle = '金币收益';
this.company = '(个)';
this.gold = true;
this.money = false;
} else if (id == 2) {
this.isactive = false;
this.subtitle = '现金收益';
this.company = '(元)';
this.gold = false;
this.money = true;
}
this.getData(id);
// 动态获取高度
this.$nextTick(() => {
let top = this.$refs.listbox.offsetTop;
this.listheight = this.clientHeight - top;
})
},
把列表写成一个子组件
<template>
<div class="detail" ref="detail">
<van-pull-refresh
ref="detailpull"
class="pull"
:style="'height:' + pullH + 'px'"
v-model="refreshing"
@refresh="onRefresh"
>
<van-list
v-model="loading"
:offset="10"
:finished="finished"
finished-text="没有更多了"
:error.sync="error"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<div class="detail-box" v-for="(item, index) in list" :key="index">
<div class="left">
<div class="title">{{ item.title }}</div>
<div class="time">
{{ item.transFullTime | timeFormat("YYYY-MM-DD HH:mm:ss") }}
</div>
</div>
<div class="right">
<div class="sybol">+</div>
<div class="number">{{ item.number }}</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
1. 进页面会触发一次onload事件,第一次如果在子组件触发的话会很慢,出现局部空白的情况,所以选择在父组件触发,看下onload
props: {
friendsList: Array,
finishList:Boolean
},
data() {
return {
loading: false,
finished: false,
more: 0
}
},
mounted() {
// 切换得归零
this.more = 0
},
methods:{
//加载更多
onLoad(refresh) {
if (this.finishList == false && this.refreshing == false) {
this.loading = false
this.finished = true
return
}
this.more++
if (this.more > 1) {
if (refresh) {
this.$emit('childMsg', refresh)
} else {
this.$emit('childMsg')
}
},
//下拉刷新
onRefresh() {
// 清空列表数据
this.finished = false;
// 重新加载数据
// 将 loading 设置为 true,表示处于加载状态
this.loading = true;
this.onLoad('refresh');
},
}
2. 下滑会多次触发onload事件,需要在父组件进行防抖处理
tip:
使用定时器注意this指向问题
childMsg(msg) {
let that = this;
console.log(msg)
if (!that.timer) {
that.timer = setTimeout(() => {
if (msg) {
that.finishList = true
if (that.subtitle == '金币收益') {
that.listGold = []
that.lastindicateGold = ''
that.getData(1)
that.timer = null;
return
} else {
that.listMoney = []
that.lastindicateCash = ''
that.getData(2)
that.timer = null;
return
}
} else {
if (that.subtitle == '金币收益') {
if (that.lastindicateGold == '') {
that.getData(1)
} else {
that.getData(1, that.lastindicateGold)
}
} else {
if (that.lastindicateCash == '') {
that.getData(2)
} else {
that.getData(2, that.lastindicateCash)
}
}
}
that.timer = null;
}, 1000)
}
},
3. 子组件中如果出现无数据情况特殊处理,下拉只能拉动局部问题,获取list高度,如果list高度小于父组件,则直接等于父组件高度就可以拉动全屏啦动态给list高度
props: {
friendsList: Array,
finishList:Boolean
},
data() {
return {
loading: false,
finished: false,
more: 0
}
},
methods:{
onLoad(refresh) {
this.more++
if (this.more > 1) {
this.$emit('childMsg')
}
},
watch: {
list: function (val) {
console.log(val)
if (this.refreshing) {
this.refreshing = false;
return
}
if (val.length < 20 || val.length % 20 > 0) {
this.finished = true;
}else{
this.finished = false
}
this.loading = false;
},
//刚刚好20条数据,下次请求无数据,避免一直loading
finishList: function (val) {
if (val == false) {
this.loading = false
this.finished = true
} else {
this.loading = true
this.finished = false
}
// 动态给下拉高度,防止无数据时出现下拉问题
this.$nextTick(() => {
let detailHeight = this.$refs.detail.offsetHeight;
let pullHeight = this.$refs.detailpull.$el.offsetHeight
console.log(detailHeight, pullHeight)
if (pullHeight < detailHeight) {
this.pullH = detailHeight
}
})
}
},
4. 这时候觉得OK了,仔细看看效果你会发现下拉出现两个loading效果,出现这个是因为下拉函数的问题,需要改进下
onLoad(refresh) {
if (this.finishList == false && this.refreshing == false) {
this.loading = false
this.finished = true
return
}
this.more++
if (this.more > 1) {
this.$emit('childMsg')
}
},
onRefresh() {
this.$emit('childMsg', 'refresh')
},
two B 移动端长按保存
利用touchstart、touchmove、touchend实现移动端长按保存 直接用代码说话吧
<template>
<div
class="qrcode"
:style="'height:' + qrheight + 'px'"
@touchstart="gotouchstart"
@touchmove="gotouchmove"
@touchend="gotouchend"
>
</div>
</template>
<script>
export default {
data() {
return {
timeOutEvent: null,
}
},
methods: {
gotouchstart() {
let that = this;
clearTimeout(that.timeOutEvent);//清除定时器
that.timeOutEvent = null;
that.timeOutEvent = setTimeout(function () {
//执行长按要执行的内容,
}, 800);//这里设置定时
},
//手释放,如果在500毫秒内就释放,则取消长按事件,此时可以执行onclick应该执行的事件
gotouchend() {
let that = this;
clearTimeout(that.timeOutEvent);
that.timeOutEvent = null
if (that.timeOutEvent != 0) {
//这里写要执行的内容(尤如onclick事件)
}
},
//如果手指有移动,则取消所有事件,此时说明用户只是要移动而不是长按
gotouchmove(e) {
e.preventDefault()
let that = this;
clearTimeout(that.timeOutEvent);//清除定时器
that.timeOutEvent = null
},
}
}
</script>
three C 保存二维码到手机相册
这个我真的尝试了很多方法,在浏览器可以但是放到APP中用webview打开就不行了,为什么呢?因为webview打开的页面把所有的事件都默认阻止了,这就需要安卓/IOS原生配合做下面两项工作了解惑小火车
- webview监听网页的下载链接。
- 使用系统的DownloadManager进行下载。
我想到两种解决方案
1. 把图片转换成二进制文件流,传给安卓/IOS
data() {
return {
saveImgpath: require('../../../public/img/qrcode.png')
}
},
image2Base64(img) {
var canvas = document.createElement("canvas");
let w = window.screen.width;
let h = window.screen.height;
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
var dataURL = canvas.toDataURL("image/png");
return dataURL;
},
getImgBase64(url) {//url就是saveImgpath
let that = this
var base64 = "";
var img = new Image();
img.src = url;
img.onload = function () {
base64 = that.image2Base64(img);
console.log(base64);
//传给原生
that.$bridge.signalCommunication('saveImg',base64)
}
}
2. 让安卓/IOS监听网页的下载行为,给与“通行”
data() {
return {
saveImgpath: require('../../../public/img/qrcode.png')
}
},
saveImg(Url) { //Url就是saveImgpath
var blob = new Blob([''], { type: 'application/octet-stream' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = Url;
a.download = Url.replace(/(.*\/)*([^.]+.*)/ig, "$2").split("?")[0];
var e = document.createEvent('MouseEvents');
e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(e);
URL.revokeObjectURL(url);
},
section C 实用小技巧
1. data里面准备传给子组件的值必须标明类型,否则子组件无法接受会报错
2. 使用vant的Overlay 遮罩层lock-scroll这个属性是默认开启的,无法滚动背景上的任何元素,只有将其赋值false才可以滚动背景上的元素
3. 移动端阻止双击手机屏幕放大
* {
margin: 0;
padding: 0;
-webkit-touch-callout: none;
/**系统默认菜单被禁用*/
-webkit-user-select: none;
/**webkit浏览器*/
-khtml-user-select: none;
/**早期浏览器*/
-moz-user-select: none;
/**火狐*/
-ms-user-select: none;
/**IE10*/
user-select: none;
}
input,
textarea {
-webkit-user-select: auto;
}
4. 阻止IOS双击页面上滑/下滑
<script type="text/javascript">
var agent = navigator.userAgent.toLowerCase();
var iLastTouch = null;
if (agent.indexOf("iphone") >= 0 || agent.indexOf("ipad") >= 0) {
document.body.addEventListener("touchend", function (event) {
var a = new Date().getTime();
iLastTouch = iLastTouch || a + 1;
var c = a - iLastTouch;
if (c < 500 && c > 0) {
event.preventDefault();
return false;
}
iLastTouch = a
}, false);
};
document.documentElement.addEventListener('touchmove', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
}, false);
</script>
5. 安装/卸载依赖
npm install 模块
本地安装的时候,将依赖包信息写入package.json中
注意一个问题,在团队协作中,一个常见的情景是他人从github上clone你的项目,然后通过npm install安装必要的依赖,(刚从github上clone下来是没有node_modules的,需要安装)那么根据什么信息安装依赖呢?就是你的package.json中的dependencies和devDepencies。所以,在本地安装的同时,将依赖包的信息(要求的名称和版本)写入package.json中是很重要的!
npm install 模块
:安装好后不写入package.json中
npm install 模块 --save
安装好后写入package.json的dependencies中(生产环境依赖)
npm install 模块 --save-dev
安装好后写入package.json的devDepencies中(开发环境依赖)
npm uninstall 模块
删除本地模块时你应该思考的问题:是否将在package.json上的相应依赖信息也消除?
npm uninstall 模块
:删除模块,但不删除模块留在package.json中的对应信息
npm uninstall 模块 --save
删除模块,同时删除模块留在package.json中dependencies下的对应信息
npm uninstall 模块 --save-dev
删除模块,同时删除模块留在package.json中devDependencies下的对应信息
6. PC、移动端通用禁止输入汉字空格,只允许输入数字
<!-- 禁止输入汉字空格,只允许输入数字 -->
<input
class="inp"
type="text"
v-model.trim="info.account"
placeholder="请输入数字"
onkeyup="value=value.replace(/^(0+)|[^\d]+/g,'')"
/>
<!-- 禁止输入空格 -->
<input
class="inp"
type="text"
v-model.trim="info.name"
placeholder=""
onkeyup="this.value=this.value.replace(/\s+/g,'')"
/>
7. 好看的滚动条样式
.block-tips::-webkit-scrollbar {
width: 5px;
height: 13px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.block-tips::-webkit-scrollbar-thumb {
background-color: #DEDEE4;
background-clip: padding-box;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
min-height: 28px;
}
附录
section B one A 下拉刷新+加载更多完整代码 父组件
<template>
<div class="profit">
<div class="profit-box">
<div class="pro-box">
<headers :header="headData"></headers>
<div class="subtitle">
<div class="sub">
<span>{{ subtitle }}</span>
<span class="company">{{ company }}</span>
</div>
<div class="number">{{ balance }}</div>
</div>
</div>
<div class="content">
<div class="list-header">
<div class="pro" :class="{ active: gold }" @click="proClick(1)">
金币收益
</div>
<div class="pro" :class="{ active: money }" @click="proClick(2)">
现金收益
</div>
</div>
<div
class="list-content"
ref="listbox"
:style="'height:' + listheight + 'px'"
>
<detail-gold
@childMsg="childMsg"
:list="listGold"
v-if="gold"
:finishList="finishList"
></detail-gold>
<detail-money
@childMsg="childMsg"
:list="listMoney"
v-if="money"
:finishList="finishList"
></detail-money>
</div>
</div>
</div>
</div>
</template>
<script>
import headers from '../../components/headers'
const detailGold = () => import('./components/detail');
const detailMoney = () => import('./components/detail');
import { incomeRecord } from '@/api/index.js'
export default {
name: 'money',
components: {
headers,
detailGold,
detailMoney
},
data() {
return {
headData: {
title: '我的收益',
other: false
},
isGold: false,
isMoney: false,
isactive: true,
subtitle: '金币收益',
company: '(个)',
balance: '',
clientHeight: window.screen.height,
listheight: '',
listGold: [],
listMoney: [],
// 下一页
lastindicateGold: '',
lastindicateCash: '',
gold: true,
money: false,
timer: null,
//刚刚好20条数据,下次请求无数据,避免一直loading
finishList: true
}
},
created() {
let id = this.$route.query.id;
if (id == 1) {
this.isactive = true;
this.subtitle = '金币收益';
this.company = '(个)';
this.gold = true;
this.money = false;
} else if (id == 2) {
this.isactive = false;
this.subtitle = '现金收益';
this.company = '(元)';
this.gold = false;
this.money = true;
}
this.getData(id);
// 动态获取高度
this.$nextTick(() => {
let top = this.$refs.listbox.offsetTop;
this.listheight = this.clientHeight - top;
})
},
methods: {
back() {
this.$router.go(-1);//返回上一层
},
// 金币/现金切换
proClick(id) {
// this.getData (id);
if (id == 1) {
if (this.gold == true) {
return
}
this.finishList = true
this.subtitle = '金币收益'
this.company = '(个)'
this.listGold = []
this.getData(1)
this.gold = true
this.money = false
} else {
if (this.money == true) {
return
}
this.finishList = true
this.subtitle = '现金收益'
this.company = '(元)'
this.listMoney = []
this.getData(2)
this.gold = false
this.money = true
}
},
// 获取数据
getData(type, lastindicate) {
let data = {};
data.userId = this.$global.userId;
data.type = type;
data.limit = 20;
if (lastindicate) {
data.lastindicate = lastindicate;
}
incomeRecord(data).then(res => {
if (type == 1) {
this.balance = res.data.gold.toFixed(2);
} else if (type == 2) {
this.balance = res.data.balance.toFixed(2);
}
if (res.data.info.length == 0) {
this.finishList = false
return
}
// 金币收益
if (type == 1) {
if (this.listGold.length == 0) {
// this.balance = res.data.gold.toFixed(2);
this.listGold = res.data.info;
this.listGold.map((item, i) => {
this.listGold[i].title = this.goldType(this.listGold[i].goldType)
this.listGold[i].number = this.listGold[i].gold
})
} else {
let newArr = res.data.info
newArr.map((item, i) => {
newArr[i].title = this.goldType(newArr[i].goldType)
newArr[i].number = newArr[i].gold
})
for (let i = 0; i < newArr.length; i++) {
this.listGold.push(newArr[i])
}
}
this.lastindicateGold = res.data.lastindicate
} else {
// 现金收益
if (this.listMoney.length == 0) {
this.balance = res.data.balance.toFixed(2);
this.listMoney = res.data.info;
this.listMoney.map((item, i) => {
this.listMoney[i].title = this.cashType(this.listMoney[i].cashType)
this.listMoney[i].number = this.listMoney[i].cash
})
} else {
let newArr = res.data.info
newArr.map((item, i) => {
newArr[i].title = this.goldType(newArr[i].cashType)
newArr[i].number = newArr[i].cash
})
for (let i = 0; i < newArr.length; i++) {
this.listMoney.push(newArr[i])
}
}
this.lastindicateCash = res.data.lastindicate
}
})
},
// 金币收益类型
goldType(type) {
switch (type) {
case 1:
return '金币兑换现金';
case 2:
return '好友阅读贡献';
case 3:
return '填写邀请码';
case 4:
return '阅读内容';
case 5:
return '优质内容';
case 6:
return '每日登录';
case 7:
return '朋友圈邀请';
case 8:
return '微信群邀请';
case 9:
return '明日之星';
case 10:
return '阅读奖励';
case 11:
return '好友贡献';
case 12:
return '好友贡献(名下好友)';
}
},
// 现金收益类型
cashType(type) {
switch (type) {
case 1:
return '金币兑换现金'
case 2:
return '提现'
case 3:
return '首次邀请好友完成任务'
case 4:
return '首次邀请好友(1元立刻到帐)'
case 5:
return '新用户奖励'
}
},
childMsg(msg) {
let that = this;
console.log(msg)
if (!that.timer) {
that.timer = setTimeout(() => {
if (msg) {
that.finishList = true
if (that.subtitle == '金币收益') {
that.listGold = []
that.lastindicateGold = ''
that.getData(1)
that.timer = null;
return
} else {
that.listMoney = []
that.lastindicateCash = ''
that.getData(2)
that.timer = null;
return
}
} else {
if (that.subtitle == '金币收益') {
if (that.lastindicateGold == '') {
that.getData(1)
} else {
that.getData(1, that.lastindicateGold)
}
} else {
if (that.lastindicateCash == '') {
that.getData(2)
} else {
that.getData(2, that.lastindicateCash)
}
}
}
that.timer = null;
}, 1000)
}
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.profit {
.profit-box {
background: #efeff5 100%;
.pro-box {
background-color: rgba(255, 255, 255, 1);
border-radius: 0px 0px 10px 10px;
.header {
margin-top: 52px;
display: flex;
position: relative;
padding-bottom: 12px;
color: rgba(255, 255, 255, 0);
font-size: 20px;
text-align: center;
box-shadow: 0px -1px 20px 0px rgba(245, 245, 245, 1);
font-family: Arial;
.return {
margin-left: 15px;
}
.text {
width: 65px;
height: 16px;
color: rgba(0, 0, 0, 1);
font-size: 16px;
text-align: center;
font-family: Arial;
font-weight: bold;
position: absolute;
left: 50%;
top: -3px;
transform: translateX(-50%);
vertical-align: top;
}
}
.subtitle {
// padding-top: 12px;
height: 90px;
border-radius: 0px 0px 10px 10px;
text-align: center;
.sub {
margin-top: 20px;
height: 13px;
color: rgba(0, 0, 0, 1);
font-size: 13px;
text-align: center;
font-family: Arial;
.company {
margin-left: 3px;
}
}
.number {
margin-top: 20px;
height: 30px;
color: rgba(255, 0, 0, 1);
font-size: 30px;
text-align: center;
font-family: Arial;
}
}
}
.content {
background: #ffffff;
margin-top: 10px;
border-radius: 10px 10px 0px 0px;
.list-header {
border-radius: 10px 10px 0px 0px;
height: 46px;
border: 1px solid rgba(239, 239, 245, 1);
box-sizing: border-box;
display: flex;
align-items: center;
.pro {
height: 16px;
color: rgba(144, 143, 143, 1);
font-size: 16px;
text-align: center;
font-family: Arial;
width: 50%;
}
.active {
height: 16px;
color: rgba(255, 0, 0, 1);
font-size: 16px;
text-align: center;
font-family: Arial;
}
}
.list-content {
// padding: 15px 15px 30px 15px;
// height: 566px;
overflow: auto;
}
}
}
}
</style>
子组件
<template>
<div class="detail" ref="detail">
<van-pull-refresh
ref="detailpull"
class="pull"
:style="'height:' + pullH + 'px'"
v-model="refreshing"
@refresh="onRefresh"
>
<van-list
v-model="loading"
:offset="10"
:finished="finished"
finished-text="没有更多了"
:error.sync="error"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<div class="detail-box" v-for="(item, index) in list" :key="index">
<div class="left">
<div class="title">{{ item.title }}</div>
<div class="time">
{{ item.transFullTime | timeFormat("YYYY-MM-DD HH:mm:ss") }}
</div>
</div>
<div class="right">
<div class="sybol">+</div>
<div class="number">{{ item.number }}</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
export default {
props: ['list', 'finishList'],
data() {
return {
loading: false,
finished: false,
error: false,
more: 0,
timer: null,
refreshing: false,
pullH: ''
}
},
mounted() {
// this.listData = this.list
this.more = 0
},
watch: {
list: function (val) {
console.log(val)
if (this.refreshing) {
this.refreshing = false;
return
}
if (val.length < 20 || val.length % 20 > 0) {
this.finished = true;
}else{
this.finished = false
}
this.loading = false;
},
//刚刚好20条数据,下次请求无数据,避免一直loading
finishList: function (val) {
if (val == false) {
this.loading = false
this.finished = true
} else {
this.loading = true
this.finished = false
}
// 动态给下拉高度,防止无数据时出现下拉问题
this.$nextTick(() => {
let detailHeight = this.$refs.detail.offsetHeight;
let pullHeight = this.$refs.detailpull.$el.offsetHeight
console.log(detailHeight, pullHeight)
if (pullHeight < detailHeight) {
this.pullH = detailHeight
}
})
}
},
methods: {
onLoad(refresh) {
if (this.finishList == false && this.refreshing == false) {
this.loading = false
this.finished = true
return
}
this.more++
if (this.more > 1) {
this.$emit('childMsg')
}
},
onRefresh() {
this.$emit('childMsg', 'refresh')
},
}
}
</script>
<style scoped lang='scss'>
.detail {
height: 100%;
.detail-box {
padding: 0px 15px 0px 15px;
display: flex;
margin-top: 30px;
.left {
width: 50%;
text-align: left;
.title {
height: 14px;
color: rgba(0, 0, 0, 1);
font-size: 14px;
font-family: Arial;
}
.time {
margin-top: 10px;
height: 10px;
line-height: 17px;
color: rgba(144, 143, 143, 1);
font-size: 12px;
font-family: Arial;
}
}
.right {
width: 50%;
color: rgba(255, 0, 0, 1);
align-items: center;
justify-content: flex-end;
display: flex;
.sybol {
width: 13px;
height: 28px;
font-size: 20px;
font-family: PingFangSC-regular;
}
.number {
margin-left: 5px;
height: 28px;
font-size: 20px;
text-align: left;
font-family: PingFangSC-regular;
}
}
.active {
color: rgba(68, 139, 242, 1);
}
}
.detail-box:nth-child(1) {
margin-top: 0px;
padding-top: 15px;
}
}
.pull {
background: #efeff5 !important;
}
</style>
写在最后
我是凉城a,一个前端,热爱技术也热爱生活。
与你相逢,我很开心。
-
文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
-
本文首发于掘金,未经许可禁止转载💌